turbo-editors/turbo-gopublic Fork 0
3d7798b
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-go.git
git clone ssh://git@rickub.com/turbo-editors/turbo-go.git

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

📦 Turbo Go

k33g committed 2026-09-19T12:10:49+02:00 Browse files
3d7798b
added .github/workflows/release.yml +137 -0
new file mode 100644
@@ -0,0 +1,137 @@
1+name: Release
2+
3+# Publishes a release with the staged binaries whenever a tag v* is pushed —
4+# what ./01-release.tag.sh does at its last line. They are built by
5+# ./02-build-releases.sh, the same script one runs on a laptop, so a local
6+# build and a published one are the same pipeline.
7+#
8+# The tag alone already publishes the module: `go install …@TAG` works the
9+# moment 01 has run, with or without this workflow. What this adds is the page
10+# a person reads, and one binary per platform with a checksum to verify it
11+# against — the thing somebody without a Go toolchain needs.
12+#
13+# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts
14+# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API
15+# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused —
16+# and it is read-only unless the workflow asks for `contents: write` below.
17+# That is why there is no longer a token file to keep out of git, and no
18+# 02-release.publish.sh or 04-release.upload-binaries.sh to run by hand.
19+#
20+# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY
21+# dispatchable workflow of a ref, so a repository should declare at most one.
22+on:
23+ push:
24+ tags:
25+ - "v*"
26+
27+permissions:
28+ contents: write
29+
30+concurrency:
31+ group: release-${{ github.ref_name }}
32+ cancel-in-progress: false
33+
34+jobs:
35+ release:
36+ name: publish ${{ github.ref_name }}
37+ runs-on: ubuntu-latest
38+ steps:
39+ - name: Checkout
40+ uses: actions/checkout@v4
41+ with:
42+ # The whole history and the tags: the release notes below are read
43+ # from the annotated tag's message, and the Makefile's default
44+ # version comes from `git describe`.
45+ fetch-depth: 0
46+
47+ - name: Set up Go
48+ uses: actions/setup-go@v5
49+ with:
50+ go-version-file: go.mod
51+ cache: true
52+
53+ - name: go test
54+ # The suite includes tests that run ./01-release.tag.sh against a
55+ # throwaway clone. They skip themselves when they see this, exactly as
56+ # they do when the script itself calls make check — without it, a
57+ # release job would start a release inside itself.
58+ env:
59+ TURBO_GO_RELEASING: "1"
60+ run: go test ./... -count=1
61+
62+ - name: Build the release
63+ # release.env is git-ignored, so the tag is passed explicitly and the
64+ # script falls back to "Turbo Go <tag>" for the description.
65+ run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}"
66+
67+ - name: Release notes
68+ id: notes
69+ # The message ./01-release.tag.sh put on the annotated tag (ABOUT in
70+ # release.env), then the two ways to get the editor and the links to
71+ # the documentation AT THAT TAG — a release page is not inside the
72+ # repository tree, so a relative path from it 404s, and a link to the
73+ # branch would rot as the branch moves. A lightweight tag has no
74+ # message: the tag name stands in.
75+ run: |
76+ set -euo pipefail
77+ message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')"
78+ if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then
79+ message="Turbo Go ${GITHUB_REF_NAME}"
80+ fi
81+ tree="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}"
82+ version="${GITHUB_REF_NAME#v}"
83+ {
84+ printf '%s\n\n' "${message}"
85+ echo "Download the binary for your platform below, or install from the module proxy:"
86+ echo
87+ echo '```bash'
88+ echo "go install $(go list -m)@${GITHUB_REF_NAME}"
89+ echo '```'
90+ echo
91+ echo "Documentation: [English](${tree}/docs/en/README.md) · [Français](${tree}/docs/fr/README.md) · [how to install](${tree}/docs/en/how-to/install.md)"
92+ echo
93+ echo "- Commit: \`${GITHUB_SHA}\`"
94+ echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)"
95+ echo
96+ echo '## Running a download'
97+ echo
98+ echo '```bash'
99+ echo "chmod +x turbo-go-${version}-<platform>"
100+ echo "./turbo-go-${version}-<platform> main.go"
101+ echo '```'
102+ echo
103+ echo "On macOS, an unsigned download is quarantined until you say otherwise: \`xattr -d com.apple.quarantine turbo-go-${version}-darwin-arm64\`."
104+ echo
105+ echo '## Checksums'
106+ echo
107+ echo 'Verify a download with `sha256sum -c SHA256SUMS --ignore-missing` (`shasum -a 256 -c` on macOS).'
108+ echo
109+ echo '```'
110+ cat "release/${GITHUB_REF_NAME}/SHA256SUMS"
111+ echo '```'
112+ } > "${RUNNER_TEMP}/notes.md"
113+ echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT"
114+
115+ - name: Keep the binaries as a run artifact
116+ # Downloadable from the run page even if the publish step below fails
117+ # (an old CI node that does not forward /gh answers 403 there).
118+ uses: actions/upload-artifact@v4
119+ with:
120+ name: turbo-go-${{ github.ref_name }}
121+ path: release/${{ github.ref_name }}/
122+ if-no-files-found: error
123+ retention-days: 14
124+
125+ - name: Publish the release
126+ uses: softprops/action-gh-release@v2
127+ with:
128+ tag_name: ${{ github.ref_name }}
129+ name: ${{ github.ref_name }}
130+ body_path: ${{ steps.notes.outputs.path }}
131+ draft: false
132+ prerelease: ${{ contains(github.ref_name, '-') }}
133+ files: |
134+ release/${{ github.ref_name }}/turbo-go-*
135+ release/${{ github.ref_name }}/SHA256SUMS
136+ release/${{ github.ref_name }}/README.md
137+ fail_on_unmatched_files: true
new file mode 100644
@@ -0,0 +1,137 @@
1+name: Release
2+
3+# Publishes a release with the staged binaries whenever a tag v* is pushed —
4+# what ./01-release.tag.sh does at its last line. They are built by
5+# ./02-build-releases.sh, the same script one runs on a laptop, so a local
6+# build and a published one are the same pipeline.
7+#
8+# The tag alone already publishes the module: `go install …@TAG` works the
9+# moment 01 has run, with or without this workflow. What this adds is the page
10+# a person reads, and one binary per platform with a checksum to verify it
11+# against — the thing somebody without a Go toolchain needs.
12+#
13+# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts
14+# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API
15+# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused —
16+# and it is read-only unless the workflow asks for `contents: write` below.
17+# That is why there is no longer a token file to keep out of git, and no
18+# 02-release.publish.sh or 04-release.upload-binaries.sh to run by hand.
19+#
20+# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY
21+# dispatchable workflow of a ref, so a repository should declare at most one.
22+on:
23+ push:
24+ tags:
25+ - "v*"
26+
27+permissions:
28+ contents: write
29+
30+concurrency:
31+ group: release-${{ github.ref_name }}
32+ cancel-in-progress: false
33+
34+jobs:
35+ release:
36+ name: publish ${{ github.ref_name }}
37+ runs-on: ubuntu-latest
38+ steps:
39+ - name: Checkout
40+ uses: actions/checkout@v4
41+ with:
42+ # The whole history and the tags: the release notes below are read
43+ # from the annotated tag's message, and the Makefile's default
44+ # version comes from `git describe`.
45+ fetch-depth: 0
46+
47+ - name: Set up Go
48+ uses: actions/setup-go@v5
49+ with:
50+ go-version-file: go.mod
51+ cache: true
52+
53+ - name: go test
54+ # The suite includes tests that run ./01-release.tag.sh against a
55+ # throwaway clone. They skip themselves when they see this, exactly as
56+ # they do when the script itself calls make check — without it, a
57+ # release job would start a release inside itself.
58+ env:
59+ TURBO_GO_RELEASING: "1"
60+ run: go test ./... -count=1
61+
62+ - name: Build the release
63+ # release.env is git-ignored, so the tag is passed explicitly and the
64+ # script falls back to "Turbo Go <tag>" for the description.
65+ run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}"
66+
67+ - name: Release notes
68+ id: notes
69+ # The message ./01-release.tag.sh put on the annotated tag (ABOUT in
70+ # release.env), then the two ways to get the editor and the links to
71+ # the documentation AT THAT TAG — a release page is not inside the
72+ # repository tree, so a relative path from it 404s, and a link to the
73+ # branch would rot as the branch moves. A lightweight tag has no
74+ # message: the tag name stands in.
75+ run: |
76+ set -euo pipefail
77+ message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')"
78+ if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then
79+ message="Turbo Go ${GITHUB_REF_NAME}"
80+ fi
81+ tree="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}"
82+ version="${GITHUB_REF_NAME#v}"
83+ {
84+ printf '%s\n\n' "${message}"
85+ echo "Download the binary for your platform below, or install from the module proxy:"
86+ echo
87+ echo '```bash'
88+ echo "go install $(go list -m)@${GITHUB_REF_NAME}"
89+ echo '```'
90+ echo
91+ echo "Documentation: [English](${tree}/docs/en/README.md) · [Français](${tree}/docs/fr/README.md) · [how to install](${tree}/docs/en/how-to/install.md)"
92+ echo
93+ echo "- Commit: \`${GITHUB_SHA}\`"
94+ echo "- Published by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)"
95+ echo
96+ echo '## Running a download'
97+ echo
98+ echo '```bash'
99+ echo "chmod +x turbo-go-${version}-<platform>"
100+ echo "./turbo-go-${version}-<platform> main.go"
101+ echo '```'
102+ echo
103+ echo "On macOS, an unsigned download is quarantined until you say otherwise: \`xattr -d com.apple.quarantine turbo-go-${version}-darwin-arm64\`."
104+ echo
105+ echo '## Checksums'
106+ echo
107+ echo 'Verify a download with `sha256sum -c SHA256SUMS --ignore-missing` (`shasum -a 256 -c` on macOS).'
108+ echo
109+ echo '```'
110+ cat "release/${GITHUB_REF_NAME}/SHA256SUMS"
111+ echo '```'
112+ } > "${RUNNER_TEMP}/notes.md"
113+ echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT"
114+
115+ - name: Keep the binaries as a run artifact
116+ # Downloadable from the run page even if the publish step below fails
117+ # (an old CI node that does not forward /gh answers 403 there).
118+ uses: actions/upload-artifact@v4
119+ with:
120+ name: turbo-go-${{ github.ref_name }}
121+ path: release/${{ github.ref_name }}/
122+ if-no-files-found: error
123+ retention-days: 14
124+
125+ - name: Publish the release
126+ uses: softprops/action-gh-release@v2
127+ with:
128+ tag_name: ${{ github.ref_name }}
129+ name: ${{ github.ref_name }}
130+ body_path: ${{ steps.notes.outputs.path }}
131+ draft: false
132+ prerelease: ${{ contains(github.ref_name, '-') }}
133+ files: |
134+ release/${{ github.ref_name }}/turbo-go-*
135+ release/${{ github.ref_name }}/SHA256SUMS
136+ release/${{ github.ref_name }}/README.md
137+ fail_on_unmatched_files: true
added .gitignore +11 -0
new file mode 100644
@@ -0,0 +1,11 @@
1+bin/
2+kits
3+*.env
4+release
5+
6+# A go.work pointing at the checkout beside this one is how you build against an
7+# unreleased turbo-core. It is one person's local wiring, never the project's:
8+# committed, it would break every clone that has no such checkout.
9+go.work
10+go.work.sum
11+./turbo-go
new file mode 100644
@@ -0,0 +1,11 @@
1+bin/
2+kits
3+*.env
4+release
5+
6+# A go.work pointing at the checkout beside this one is how you build against an
7+# unreleased turbo-core. It is one person's local wiring, never the project's:
8+# committed, it would break every clone that has no such checkout.
9+go.work
10+go.work.sum
11+./turbo-go
added .memory/README.md +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+# `.memory/` — the project's durable record
2+
3+This folder is for whoever **continues building** turbo-go. It is committed to the repository, not gitignored: that is the whole point.
4+
5+Keep it apart from `docs/`. `docs/` is for whoever *uses* the editor; this is for whoever *works on it*.
6+
7+| File | What it is | How it is maintained |
8+| --- | --- | --- |
9+| `summary.md` | A **snapshot** of the project's current state: what it is, how it is built, what decisions are in force, what is not yet established. | **Edited in place.** Change only what a session establishes or invalidates; leave the rest byte for byte alone. Never regenerated wholesale. |
10+| `history.md` | An **append-only** log, one dated entry per session. | **Only ever grows.** Never rewritten, never tidied up, never corrected after the fact. A history you edit is not a history. |
11+| `handoffs/YYYY-MM-DD-slug.md` | What the next session needs to pick the work up: state, work in flight, next steps, blockers, traps. | One file per session and topic. Never overwrite someone else's. |
12+
13+## Reading it
14+
15+Start with `summary.md`, then the tail of `history.md`, then the most recent handoff. Between them they should answer "what is this, what state is it in, and what was the last person doing" without your having to read the code first.
16+
17+Do not ask the user for anything that is written here.
18+
19+## Writing it
20+
21+Match the length to the work — a one-line fix earns a few lines, not a filled-in template. Record what the next person **cannot re-derive**: where you stopped, what you tried that failed, the trap you hit, the decision still waiting on the user. Not a diff summary; git already has that.
22+
23+Anything you did not verify goes under `## Not yet established` in `summary.md`, stated as unknown. A summary that admits its gaps is useful; one that states guesses as fact is worse than none, because the next session will trust it.
new file mode 100644
@@ -0,0 +1,23 @@
1+# `.memory/` — the project's durable record
2+
3+This folder is for whoever **continues building** turbo-go. It is committed to the repository, not gitignored: that is the whole point.
4+
5+Keep it apart from `docs/`. `docs/` is for whoever *uses* the editor; this is for whoever *works on it*.
6+
7+| File | What it is | How it is maintained |
8+| --- | --- | --- |
9+| `summary.md` | A **snapshot** of the project's current state: what it is, how it is built, what decisions are in force, what is not yet established. | **Edited in place.** Change only what a session establishes or invalidates; leave the rest byte for byte alone. Never regenerated wholesale. |
10+| `history.md` | An **append-only** log, one dated entry per session. | **Only ever grows.** Never rewritten, never tidied up, never corrected after the fact. A history you edit is not a history. |
11+| `handoffs/YYYY-MM-DD-slug.md` | What the next session needs to pick the work up: state, work in flight, next steps, blockers, traps. | One file per session and topic. Never overwrite someone else's. |
12+
13+## Reading it
14+
15+Start with `summary.md`, then the tail of `history.md`, then the most recent handoff. Between them they should answer "what is this, what state is it in, and what was the last person doing" without your having to read the code first.
16+
17+Do not ask the user for anything that is written here.
18+
19+## Writing it
20+
21+Match the length to the work — a one-line fix earns a few lines, not a filled-in template. Record what the next person **cannot re-derive**: where you stopped, what you tried that failed, the trap you hit, the decision still waiting on the user. Not a diff summary; git already has that.
22+
23+Anything you did not verify goes under `## Not yet established` in `summary.md`, stated as unknown. A summary that admits its gaps is useful; one that states guesses as fact is worse than none, because the next session will trust it.
added .memory/handoffs/2026-08-30-completion-diagnosis.md +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-08-30 — the completion fix was still droppable
2+
3+## State
4+
5+Two more rounds of user feedback, both now closed.
6+
7+**The menu.** Reported as changed and broken; **could not be reproduced**. The real binary was driven under a pty and F10, Alt-F, Alt-E, a mouse click and Down+Enter all worked, with and without a language server. `internal/ui/menu.go` had not been touched. The user confirmed it works again — almost certainly a stale binary.
8+
9+**Completion.** Two separate things were wrong, and only one of them was the editor's fault.
10+
11+1. **The editor's fault, now fixed.** The previous session's re-announcement rode on `screen.PostEvent`, which **drops events when tcell's queue is full** — and start-up is exactly when gopls floods that queue with diagnostics for the whole module. The announcement could therefore never arrive, silently, and completion would be empty again. It is now checked on every turn of the event loop instead, and depends on no message at all.
12+
13+2. **Not the editor's fault, now explained.** The user had a `hello.go` in the repository root declaring `package main` and `func main()` beside the project's own `main.go`. The package does not compile, and gopls answers **nothing at all — no error, an empty list** — for a package it cannot load. Reproduced with a probe against real gopls in an identical two-`main` module: 0 completions. The editor used to say "No completions here", which is true and useless. It now says which problem is in the way, and `Run ▸ Language server status` reports the server path, the root, the file, and whether the server has even been told about it.
14+
15+## Two things to carry forward
16+
17+**Never make correctness depend on `PostEvent`.** It is best-effort by design. A redraw is a fine thing to lose; a state transition is not. Anything that must happen belongs in the loop's own turn, guarded by a flag.
18+
19+**`hello.go` in the repository root is the user's file**, not an artefact. The working tree is mounted from their machine. An empty one appeared earlier and was deleted as rubbish — it was theirs. It was empty and nothing was lost, but do not assume a file that appears in the tree is yours. It currently breaks `go build ./...` and `go vet ./...` for the root package; `./internal/...` is unaffected. Ask before removing it.
20+
21+## Next steps
22+
23+1. **Ask whether `hello.go` should go.** Until it does, the root package does not compile, which also means gopls will keep refusing to complete anywhere in this repository.
24+2. **Consider a second completion binding.** `Ctrl-Space` is claimed by tmux, screen and most IDE terminals before the editor sees it. Typing `.` and `Run ▸ Completion` both work and are documented, but a function key would be more reliable. Nothing has been chosen; it needs the user's opinion.
25+3. Unchanged from the previous handoffs: show diagnostics in the gutter rather than only on the status bar; add CI; try macOS and Windows.
26+4. **Resize is done and verified** (see the fifth history entry): windows carry a Turbo Vision grow mode and follow the terminal, checked live on a real pty with SIGWINCH. What remains unverified on hardware is mouse dragging and corner-resizing.
27+
28+## Watch out for
29+
30+Everything in the two previous handoffs still applies. And:
31+
32+- **A completion test whose fixture already contains the text being completed proves nothing** — gopls answers it from disk. The text must be typed into the buffer. Verified by removing the fix and watching the test fail.
33+- **`go build ./...` currently fails** because of the user's `hello.go`. Use `./internal/...` while it is there, and do not mistake that failure for one of your own.
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-08-30 — the completion fix was still droppable
2+
3+## State
4+
5+Two more rounds of user feedback, both now closed.
6+
7+**The menu.** Reported as changed and broken; **could not be reproduced**. The real binary was driven under a pty and F10, Alt-F, Alt-E, a mouse click and Down+Enter all worked, with and without a language server. `internal/ui/menu.go` had not been touched. The user confirmed it works again — almost certainly a stale binary.
8+
9+**Completion.** Two separate things were wrong, and only one of them was the editor's fault.
10+
11+1. **The editor's fault, now fixed.** The previous session's re-announcement rode on `screen.PostEvent`, which **drops events when tcell's queue is full** — and start-up is exactly when gopls floods that queue with diagnostics for the whole module. The announcement could therefore never arrive, silently, and completion would be empty again. It is now checked on every turn of the event loop instead, and depends on no message at all.
12+
13+2. **Not the editor's fault, now explained.** The user had a `hello.go` in the repository root declaring `package main` and `func main()` beside the project's own `main.go`. The package does not compile, and gopls answers **nothing at all — no error, an empty list** — for a package it cannot load. Reproduced with a probe against real gopls in an identical two-`main` module: 0 completions. The editor used to say "No completions here", which is true and useless. It now says which problem is in the way, and `Run ▸ Language server status` reports the server path, the root, the file, and whether the server has even been told about it.
14+
15+## Two things to carry forward
16+
17+**Never make correctness depend on `PostEvent`.** It is best-effort by design. A redraw is a fine thing to lose; a state transition is not. Anything that must happen belongs in the loop's own turn, guarded by a flag.
18+
19+**`hello.go` in the repository root is the user's file**, not an artefact. The working tree is mounted from their machine. An empty one appeared earlier and was deleted as rubbish — it was theirs. It was empty and nothing was lost, but do not assume a file that appears in the tree is yours. It currently breaks `go build ./...` and `go vet ./...` for the root package; `./internal/...` is unaffected. Ask before removing it.
20+
21+## Next steps
22+
23+1. **Ask whether `hello.go` should go.** Until it does, the root package does not compile, which also means gopls will keep refusing to complete anywhere in this repository.
24+2. **Consider a second completion binding.** `Ctrl-Space` is claimed by tmux, screen and most IDE terminals before the editor sees it. Typing `.` and `Run ▸ Completion` both work and are documented, but a function key would be more reliable. Nothing has been chosen; it needs the user's opinion.
25+3. Unchanged from the previous handoffs: show diagnostics in the gutter rather than only on the status bar; add CI; try macOS and Windows.
26+4. **Resize is done and verified** (see the fifth history entry): windows carry a Turbo Vision grow mode and follow the terminal, checked live on a real pty with SIGWINCH. What remains unverified on hardware is mouse dragging and corner-resizing.
27+
28+## Watch out for
29+
30+Everything in the two previous handoffs still applies. And:
31+
32+- **A completion test whose fixture already contains the text being completed proves nothing** — gopls answers it from disk. The text must be typed into the buffer. Verified by removing the fix and watching the test fail.
33+- **`go build ./...` currently fails** because of the user's `hello.go`. Use `./internal/...` while it is there, and do not mistake that failure for one of your own.
added .memory/handoffs/2026-08-30-initial-build.md +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+# Handoff — 2026-08-30 — initial build of the editor
2+
3+## State
4+
5+The editor is **finished and working** against the original request: Turbo C-style interface, Go syntax colouring, themes, and gopls completion.
6+
7+- `make build` produces `bin/turbo-go`; `make test` is green across every package; the quality gate passes with zero smells.
8+- The LSP client has been driven against a real gopls 0.23.0 and got `strings.Contains` back from a completion at `strings.`.
9+- Documentation is complete in English and French, and every package has its own `README.md`.
10+
11+Nothing is half-written. There is no work in flight.
12+
13+## The one thing to do before anything else
14+
15+**Run it in a real terminal.**
16+
17+Every visual claim in this repository comes from `tcell.SimulationScreen`. It is a real `Screen` and it exercises the real drawing code, but it is not a TTY. What has *not* been observed on an actual terminal emulator:
18+
19+- whether the 24-bit colours in `turbo-dark` and `borland-light` look as intended, and whether `turbo-classic` degrades correctly on a 16-colour terminal
20+- whether mouse reporting works — window dragging, corner resizing, click-to-place-cursor
21+- whether `Alt`-letter reaches the program, or is eaten by the terminal emulator (this varies a lot; if it is, `F10` plus arrows is the fallback and the docs already say so)
22+- whether resizing the window mid-session redraws cleanly
23+
24+```bash
25+make build && ./bin/turbo-go main.go
26+```
27+
28+If something is wrong there, it is almost certainly in `internal/ui/painter.go` or in how `main.go` sets the screen up — not in the widgets, which are well covered.
29+
30+## Next steps, in the order I would take them
31+
32+1. **Real-terminal pass** (above). Everything else is speculative until this is done.
33+2. **Show diagnostics where the error is.** `Language.Diagnostics(path)` already holds them per file; the status bar shows only the first error. A coloured marker in the gutter, or the offending span underlined, would use what is already there. The theme keys `diagnostic.error/warning/info` exist and are currently unused by anything that draws.
34+3. **CI.** There is none. `make check` plus the quality report is the whole gate and it runs by hand.
35+4. **Try it on macOS and Windows.** `lsp.PathToURI` has a drive-letter path and `theme.UserDir` uses `os.UserConfigDir`; neither has ever run outside Linux.
36+5. **Profile a large file.** The syntax cache re-scans on revision change, not on redraw, so scrolling should be free — but `buffer.Text()` rebuilds the whole string on every scan, and on a 50 000-line file that may well be the thing that hurts.
37+
38+## Open questions / blockers
39+
40+- **`.tickets/0001-specifications.yaml` is an empty issue** created before this work. It was never filled in, and I did not touch it. Worth either writing up or deleting.
41+- **No licence header policy.** `LICENSE` exists; no source file carries a header. Fine either way, but decide before the first outside contribution.
42+- **`kits/` is untracked and excluded from qlty.** If it is meant to be committed, the exclusion comment in `.qlty/qlty.toml` explains what it hides; if it is not, `.gitignore` would be the more honest place to put it.
43+
44+## Watch out for
45+
46+- **LSP columns are UTF-16, editor columns are runes.** `RuneToUTF16` / `UTF16ToRune` sit at the boundary. On ASCII the two agree, so a mistake here passes every test until a file has an accent in it. There is a test using a musical clef precisely because it needs a surrogate pair.
47+- **gopls waits for its client to answer `workspace/configuration`.** Ignore server-to-client requests and initialisation hangs with no error at all. `Client.handleRequest` is what stops that.
48+- **Measure screen widths in runes, never bytes.** `len("[■]")` is 5; the box is 3 columns. This produced a real off-by-two in the window close box.
49+- **In a dialog, the focused control must see the arrows before the focus ring does.** Getting this backwards makes every list box unusable by keyboard. Two tests in `internal/ui/dialog_test.go` pin it.
50+- **`Painter.Sub` takes absolute screen coordinates; every drawing call takes local ones.** That asymmetry is deliberate and documented at the method, but it is the one thing in `internal/ui` that will surprise you.
51+- **The quality gate is not `make test`.** Both have to pass, and the quality report is a separate command.
new file mode 100644
@@ -0,0 +1,51 @@
1+# Handoff — 2026-08-30 — initial build of the editor
2+
3+## State
4+
5+The editor is **finished and working** against the original request: Turbo C-style interface, Go syntax colouring, themes, and gopls completion.
6+
7+- `make build` produces `bin/turbo-go`; `make test` is green across every package; the quality gate passes with zero smells.
8+- The LSP client has been driven against a real gopls 0.23.0 and got `strings.Contains` back from a completion at `strings.`.
9+- Documentation is complete in English and French, and every package has its own `README.md`.
10+
11+Nothing is half-written. There is no work in flight.
12+
13+## The one thing to do before anything else
14+
15+**Run it in a real terminal.**
16+
17+Every visual claim in this repository comes from `tcell.SimulationScreen`. It is a real `Screen` and it exercises the real drawing code, but it is not a TTY. What has *not* been observed on an actual terminal emulator:
18+
19+- whether the 24-bit colours in `turbo-dark` and `borland-light` look as intended, and whether `turbo-classic` degrades correctly on a 16-colour terminal
20+- whether mouse reporting works — window dragging, corner resizing, click-to-place-cursor
21+- whether `Alt`-letter reaches the program, or is eaten by the terminal emulator (this varies a lot; if it is, `F10` plus arrows is the fallback and the docs already say so)
22+- whether resizing the window mid-session redraws cleanly
23+
24+```bash
25+make build && ./bin/turbo-go main.go
26+```
27+
28+If something is wrong there, it is almost certainly in `internal/ui/painter.go` or in how `main.go` sets the screen up — not in the widgets, which are well covered.
29+
30+## Next steps, in the order I would take them
31+
32+1. **Real-terminal pass** (above). Everything else is speculative until this is done.
33+2. **Show diagnostics where the error is.** `Language.Diagnostics(path)` already holds them per file; the status bar shows only the first error. A coloured marker in the gutter, or the offending span underlined, would use what is already there. The theme keys `diagnostic.error/warning/info` exist and are currently unused by anything that draws.
34+3. **CI.** There is none. `make check` plus the quality report is the whole gate and it runs by hand.
35+4. **Try it on macOS and Windows.** `lsp.PathToURI` has a drive-letter path and `theme.UserDir` uses `os.UserConfigDir`; neither has ever run outside Linux.
36+5. **Profile a large file.** The syntax cache re-scans on revision change, not on redraw, so scrolling should be free — but `buffer.Text()` rebuilds the whole string on every scan, and on a 50 000-line file that may well be the thing that hurts.
37+
38+## Open questions / blockers
39+
40+- **`.tickets/0001-specifications.yaml` is an empty issue** created before this work. It was never filled in, and I did not touch it. Worth either writing up or deleting.
41+- **No licence header policy.** `LICENSE` exists; no source file carries a header. Fine either way, but decide before the first outside contribution.
42+- **`kits/` is untracked and excluded from qlty.** If it is meant to be committed, the exclusion comment in `.qlty/qlty.toml` explains what it hides; if it is not, `.gitignore` would be the more honest place to put it.
43+
44+## Watch out for
45+
46+- **LSP columns are UTF-16, editor columns are runes.** `RuneToUTF16` / `UTF16ToRune` sit at the boundary. On ASCII the two agree, so a mistake here passes every test until a file has an accent in it. There is a test using a musical clef precisely because it needs a surrogate pair.
47+- **gopls waits for its client to answer `workspace/configuration`.** Ignore server-to-client requests and initialisation hangs with no error at all. `Client.handleRequest` is what stops that.
48+- **Measure screen widths in runes, never bytes.** `len("[■]")` is 5; the box is 3 columns. This produced a real off-by-two in the window close box.
49+- **In a dialog, the focused control must see the arrows before the focus ring does.** Getting this backwards makes every list box unusable by keyboard. Two tests in `internal/ui/dialog_test.go` pin it.
50+- **`Painter.Sub` takes absolute screen coordinates; every drawing call takes local ones.** That asymmetry is deliberate and documented at the method, but it is the one thing in `internal/ui` that will surprise you.
51+- **The quality gate is not `make test`.** Both have to pass, and the quality report is a separate command.
added .memory/handoffs/2026-08-30-real-terminal-fixes.md +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+# Handoff — 2026-08-30 — two defects found by running it for real
2+
3+## State
4+
5+The user ran the editor in an actual terminal for the first time and found two defects. Both are **fixed, tested and shipped**; the quality gate is back to PASS and the whole suite is green under `-race`.
6+
7+1. **The cursor was invisible under `turbo-dark`.** The editor relied entirely on the terminal to draw the cursor, and a terminal draws it in the *user's* colour, which owes nothing to the theme. The editor now paints the cursor cell itself, in a new `editor.cursor` theme key, and only the active window does so.
8+
9+2. **Completion returned nothing.** `main` opens the files named on the command line and *then* starts gopls, so the first `didOpen` reached nothing at all. gopls was never told the document was open, so the `didChange` sent on every keystroke referred to a document it did not have — and it answered completions from the **stale on-disk text**. Typing `fmt.` gave "No completions here". The editor now re-announces every open document once the server becomes ready.
10+
11+Along the way: the completion popup was anchored without allowing for the gutter or the horizontal scroll, so it opened several columns left of the cursor. Also fixed.
12+
13+## The lesson, which matters more than the fixes
14+
15+Both defects were invisible to a test suite that never leaves memory.
16+
17+- `tcell.SimulationScreen` exercises the drawing code faithfully, but it is not a TTY. It cannot tell you that a real terminal will draw a cursor you cannot see.
18+- The first version of the end-to-end completion test **passed with the fix removed**, because its fixture already contained `strings.` on disk and gopls answered from disk. A test whose fixture contains the answer proves nothing about what the editor said. It was rewritten to *type* the text, and only then did it fail without the fix.
19+
20+If you add a test involving a language server, make sure the thing being completed exists **only in the buffer**.
21+
22+## Next steps
23+
24+1. **Another real-terminal pass**, now that two of these have been found there. Still unverified on hardware: mouse reporting (drag, resize, click-to-place), `Alt`-key handling, resize mid-session, and how `borland-light` looks.
25+2. **Show diagnostics where the error is.** Unchanged from the previous handoff: `Language.Diagnostics(path)` holds them per file, the status bar shows only the first, and the `diagnostic.*` theme keys are drawn by nothing.
26+3. **CI**, so `make test` plus the quality report stop being a manual ritual.
27+4. macOS and Windows have still never run this.
28+
29+## Open questions / blockers
30+
31+- **Is the amber cursor right for `turbo-dark`?** `#ffd787` on `#262626` is loud on purpose, because the complaint was that it could not be seen. If it is now too loud, the key to change is `editor.cursor` and the how-to explains it.
32+- Unchanged from the previous handoff: the empty `.tickets/0001-specifications.yaml`, no licence headers, and `kits/` being untracked yet excluded from qlty.
33+
34+## Watch out for
35+
36+Everything in the previous handoff still applies. Two more:
37+
38+- **`Window.SetActive` now propagates the focus to its content.** A content widget that implements `ui.Focusable` will be focused and unfocused by the window. `editor.View` embeds `ui.FocusBox` for this reason, and a view built on its own starts focused so that it still shows a cursor outside a desktop.
39+- **`editor.cursor` must not be a reversal of `editor.currentline`.** Some terminals draw their cursor by inverting the cell; a reversed pair would be inverted straight back into invisibility. A test in `internal/editor` checks every theme for exactly that.
new file mode 100644
@@ -0,0 +1,39 @@
1+# Handoff — 2026-08-30 — two defects found by running it for real
2+
3+## State
4+
5+The user ran the editor in an actual terminal for the first time and found two defects. Both are **fixed, tested and shipped**; the quality gate is back to PASS and the whole suite is green under `-race`.
6+
7+1. **The cursor was invisible under `turbo-dark`.** The editor relied entirely on the terminal to draw the cursor, and a terminal draws it in the *user's* colour, which owes nothing to the theme. The editor now paints the cursor cell itself, in a new `editor.cursor` theme key, and only the active window does so.
8+
9+2. **Completion returned nothing.** `main` opens the files named on the command line and *then* starts gopls, so the first `didOpen` reached nothing at all. gopls was never told the document was open, so the `didChange` sent on every keystroke referred to a document it did not have — and it answered completions from the **stale on-disk text**. Typing `fmt.` gave "No completions here". The editor now re-announces every open document once the server becomes ready.
10+
11+Along the way: the completion popup was anchored without allowing for the gutter or the horizontal scroll, so it opened several columns left of the cursor. Also fixed.
12+
13+## The lesson, which matters more than the fixes
14+
15+Both defects were invisible to a test suite that never leaves memory.
16+
17+- `tcell.SimulationScreen` exercises the drawing code faithfully, but it is not a TTY. It cannot tell you that a real terminal will draw a cursor you cannot see.
18+- The first version of the end-to-end completion test **passed with the fix removed**, because its fixture already contained `strings.` on disk and gopls answered from disk. A test whose fixture contains the answer proves nothing about what the editor said. It was rewritten to *type* the text, and only then did it fail without the fix.
19+
20+If you add a test involving a language server, make sure the thing being completed exists **only in the buffer**.
21+
22+## Next steps
23+
24+1. **Another real-terminal pass**, now that two of these have been found there. Still unverified on hardware: mouse reporting (drag, resize, click-to-place), `Alt`-key handling, resize mid-session, and how `borland-light` looks.
25+2. **Show diagnostics where the error is.** Unchanged from the previous handoff: `Language.Diagnostics(path)` holds them per file, the status bar shows only the first, and the `diagnostic.*` theme keys are drawn by nothing.
26+3. **CI**, so `make test` plus the quality report stop being a manual ritual.
27+4. macOS and Windows have still never run this.
28+
29+## Open questions / blockers
30+
31+- **Is the amber cursor right for `turbo-dark`?** `#ffd787` on `#262626` is loud on purpose, because the complaint was that it could not be seen. If it is now too loud, the key to change is `editor.cursor` and the how-to explains it.
32+- Unchanged from the previous handoff: the empty `.tickets/0001-specifications.yaml`, no licence headers, and `kits/` being untracked yet excluded from qlty.
33+
34+## Watch out for
35+
36+Everything in the previous handoff still applies. Two more:
37+
38+- **`Window.SetActive` now propagates the focus to its content.** A content widget that implements `ui.Focusable` will be focused and unfocused by the window. `editor.View` embeds `ui.FocusBox` for this reason, and a view built on its own starts focused so that it still shows a cursor outside a desktop.
39+- **`editor.cursor` must not be a reversal of `editor.currentline`.** Some terminals draw their cursor by inverting the cell; a reversed pair would be inverted straight back into invisibility. A test in `internal/editor` checks every theme for exactly that.
added .memory/handoffs/2026-08-31-go-tools.md +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+# Handoff — 2026-08-31 — the Go menu, and a data race it exposed
2+
3+## State
4+
5+**Ticket 0017 is partly done and green**, on branch `feature/go-format-lint`, **uncommitted**.
6+
7+`Alt-G` opens a **Go** menu built from `.turbo-go/tools.toml`. Each entry says where its output goes — `popup` (the default), `terminal`, or `editor` — and files the command rewrote that have no unsaved changes are re-read afterwards. **Go ▸ Create tools file** writes a starter file holding the five Go commands, four `popup` and `Run` `terminal`.
8+
9+```
10+ … Window Snippets Go Help
11+ ┌───────────────┐ ┌──────── go vet ./... — exit 1 ────────┐
12+ │ Format │ │ main.go:6:2: unreachable code │
13+ │ Lint │ │ │
14+ │ Build │ │ [ Close ] │
15+ │ Test │ └───────────────────────────────────────┘
16+ │ Run │
17+ ├───────────────┤
18+ │ Create tools… │
19+ └───────────────┘
20+```
21+
22+- New `internal/tools` (95.0 %, both the file and the running of a command) and `internal/projectfile` (75.0 %).
23+- `terminal.Options.Args`, `terminal.ViewOptions`, `View.Exited()`; `Buffer.Reload` with `ErrModified`; `NewOutputDialog`; `App.tick`.
24+- Whole suite green under `-race`, five consecutive runs of the two most affected packages. Quality gate **PASS**: 0/0/0, complexity **down** from 1528 to 1513.
25+- Docs complete in EN and FR — three new pages each, six existing pages updated each. Diagram re-checked against `go list` (39 edges each side).
26+- **Verified in a real terminal**: `go build ./... — ok` with `(no output)`, `go vet ./... — exit 1` with its diagnostic, `gofmt -l -w . — ok` listing the file it rewrote, and the buffer reloading afterwards.
27+
28+Earlier the same day, PR #6 merged the snippets work.
29+
30+## In flight
31+
32+Nothing. Finished through Phase 8.
33+
34+## Next steps
35+
36+1. **Commit and open the PR.** `feature/go-format-lint` is the branch.
37+2. **Finish ticket 0017.** It also asked for `go mod init` + `touch main.go` — creating a project from nothing, which is a different shape from running a command in an existing one (it needs a name, and there is no project yet). Not done, and the ticket should stay open for it.
38+3. **Tickets.** 0002, 0003, 0006, 0007, 0009, 0013, 0014 are implemented and open; 0017 is partly implemented.
39+
40+## Open questions / blockers
41+
42+- **A modal popup holds the whole editor while a command runs.** That was chosen knowingly and is documented, with `output = "terminal"` as the way out for any command it annoys you on. If it turns out to annoy generally, a non-modal output window is the alternative — and it is a different feature, not a tweak.
43+- **`output = "editor"` shows a popup first, then a window.** The popup is how you watch it and how you stop it; the window arrives when you close the popup. It reads oddly written down and is fine in use, but it is worth a second look with real output.
44+
45+- **`go mod init` is not in the menu**, and does not fit the current shape: every other tool runs in the project that already exists, whereas this one creates it and needs a module path typed. It wants a prompt dialog and probably its own item rather than a `tools.toml` entry.
46+- **Nothing jumps to a compile error.** `go build` prints `internal/app/app.go:42:3: …` in the terminal window and you have to open the file yourself. Parsing that output and jumping would be a real win and is a feature of its own — it needs a per-language error format, and the output lives in a terminal emulator rather than in a captured buffer.
47+- **A menu panel still does not scroll** (carried over from the snippets handoff). A `tools.toml` with twenty entries draws a panel taller than the terminal and the bottom is clipped.
48+
49+## Watch out for
50+
51+- **Verify a new test by breaking the code it covers.** Two tests in this session passed while proving nothing, both because the test drove the step under test: `waitForLoopTurn` called `reloadAfterTools` itself (so the test passed with that step deleted from the loop — hence `App.tick`), and the process-group test killed the shell before it had forked (so it passed without the fix — it now waits for the child to print). Neither was visible by reading.
52+- **Stopping a command must take the process group.** Killing only the shell leaves a grandchild holding the output pipe, and the reading goroutine blocks until *it* ends — 20 seconds in the test suite, and for `go test ./...` it would be every test binary. `processGroup()` and `killGroup()` are build-tagged; `cmd.WaitDelay` is the backstop for anything that escapes.
53+
54+- **A constructor that starts a goroutine must take its callbacks as parameters.** `terminal.NewView` started the reader and callers assigned `OnChange`/`OnExit` afterwards — a data race present since the terminal feature that `-race` never caught, because a shell takes longer to produce its first output than an assignment takes to run. `sh -c "echo x"` finished immediately and the detector fired at once. `ViewOptions` fixes it structurally; do not add an assignable callback back.
55+- **A finished terminal must not swallow keys.** It used to write every key to the dead shell, where the write failed silently and the key was consumed anyway, so `Ctrl-W` could never close the window. `View.HandleKey` returns false after `Exited()`, except for the scrolling keys. `TestAFinishedTerminalStopsTakingKeys` covers it.
56+- **`Buffer.Reload` must refuse over unsaved work.** That restriction is the whole safety of it. `TestReloadRefusesToThrowAwayUnsavedWork` pins it, and `TestAToolLeavesAModifiedBufferAlone` pins the app end of it.
57+- **`internal/buffer`'s atomic write was deliberately left out of `projectfile`.** It preserves the mode of the file it replaces, because it is saving over something the user already had; `projectfile` creates a file with a fixed `0644`. They look alike and are not the same operation — merging them would mean parameterising the mode and losing the different error text.
58+- **`projectfile.Write` already does `MkdirAll`.** The three `Create` functions used to do it themselves; do not add it back.
59+- **The app tests for tools drive the event loop by hand.** `Run` is not running, so `waitForLoopTurn` calls `a.reloadAfterTools()` itself. A test that waits for a reload without calling it will hang until its deadline.
new file mode 100644
@@ -0,0 +1,59 @@
1+# Handoff — 2026-08-31 — the Go menu, and a data race it exposed
2+
3+## State
4+
5+**Ticket 0017 is partly done and green**, on branch `feature/go-format-lint`, **uncommitted**.
6+
7+`Alt-G` opens a **Go** menu built from `.turbo-go/tools.toml`. Each entry says where its output goes — `popup` (the default), `terminal`, or `editor` — and files the command rewrote that have no unsaved changes are re-read afterwards. **Go ▸ Create tools file** writes a starter file holding the five Go commands, four `popup` and `Run` `terminal`.
8+
9+```
10+ … Window Snippets Go Help
11+ ┌───────────────┐ ┌──────── go vet ./... — exit 1 ────────┐
12+ │ Format │ │ main.go:6:2: unreachable code │
13+ │ Lint │ │ │
14+ │ Build │ │ [ Close ] │
15+ │ Test │ └───────────────────────────────────────┘
16+ │ Run │
17+ ├───────────────┤
18+ │ Create tools… │
19+ └───────────────┘
20+```
21+
22+- New `internal/tools` (95.0 %, both the file and the running of a command) and `internal/projectfile` (75.0 %).
23+- `terminal.Options.Args`, `terminal.ViewOptions`, `View.Exited()`; `Buffer.Reload` with `ErrModified`; `NewOutputDialog`; `App.tick`.
24+- Whole suite green under `-race`, five consecutive runs of the two most affected packages. Quality gate **PASS**: 0/0/0, complexity **down** from 1528 to 1513.
25+- Docs complete in EN and FR — three new pages each, six existing pages updated each. Diagram re-checked against `go list` (39 edges each side).
26+- **Verified in a real terminal**: `go build ./... — ok` with `(no output)`, `go vet ./... — exit 1` with its diagnostic, `gofmt -l -w . — ok` listing the file it rewrote, and the buffer reloading afterwards.
27+
28+Earlier the same day, PR #6 merged the snippets work.
29+
30+## In flight
31+
32+Nothing. Finished through Phase 8.
33+
34+## Next steps
35+
36+1. **Commit and open the PR.** `feature/go-format-lint` is the branch.
37+2. **Finish ticket 0017.** It also asked for `go mod init` + `touch main.go` — creating a project from nothing, which is a different shape from running a command in an existing one (it needs a name, and there is no project yet). Not done, and the ticket should stay open for it.
38+3. **Tickets.** 0002, 0003, 0006, 0007, 0009, 0013, 0014 are implemented and open; 0017 is partly implemented.
39+
40+## Open questions / blockers
41+
42+- **A modal popup holds the whole editor while a command runs.** That was chosen knowingly and is documented, with `output = "terminal"` as the way out for any command it annoys you on. If it turns out to annoy generally, a non-modal output window is the alternative — and it is a different feature, not a tweak.
43+- **`output = "editor"` shows a popup first, then a window.** The popup is how you watch it and how you stop it; the window arrives when you close the popup. It reads oddly written down and is fine in use, but it is worth a second look with real output.
44+
45+- **`go mod init` is not in the menu**, and does not fit the current shape: every other tool runs in the project that already exists, whereas this one creates it and needs a module path typed. It wants a prompt dialog and probably its own item rather than a `tools.toml` entry.
46+- **Nothing jumps to a compile error.** `go build` prints `internal/app/app.go:42:3: …` in the terminal window and you have to open the file yourself. Parsing that output and jumping would be a real win and is a feature of its own — it needs a per-language error format, and the output lives in a terminal emulator rather than in a captured buffer.
47+- **A menu panel still does not scroll** (carried over from the snippets handoff). A `tools.toml` with twenty entries draws a panel taller than the terminal and the bottom is clipped.
48+
49+## Watch out for
50+
51+- **Verify a new test by breaking the code it covers.** Two tests in this session passed while proving nothing, both because the test drove the step under test: `waitForLoopTurn` called `reloadAfterTools` itself (so the test passed with that step deleted from the loop — hence `App.tick`), and the process-group test killed the shell before it had forked (so it passed without the fix — it now waits for the child to print). Neither was visible by reading.
52+- **Stopping a command must take the process group.** Killing only the shell leaves a grandchild holding the output pipe, and the reading goroutine blocks until *it* ends — 20 seconds in the test suite, and for `go test ./...` it would be every test binary. `processGroup()` and `killGroup()` are build-tagged; `cmd.WaitDelay` is the backstop for anything that escapes.
53+
54+- **A constructor that starts a goroutine must take its callbacks as parameters.** `terminal.NewView` started the reader and callers assigned `OnChange`/`OnExit` afterwards — a data race present since the terminal feature that `-race` never caught, because a shell takes longer to produce its first output than an assignment takes to run. `sh -c "echo x"` finished immediately and the detector fired at once. `ViewOptions` fixes it structurally; do not add an assignable callback back.
55+- **A finished terminal must not swallow keys.** It used to write every key to the dead shell, where the write failed silently and the key was consumed anyway, so `Ctrl-W` could never close the window. `View.HandleKey` returns false after `Exited()`, except for the scrolling keys. `TestAFinishedTerminalStopsTakingKeys` covers it.
56+- **`Buffer.Reload` must refuse over unsaved work.** That restriction is the whole safety of it. `TestReloadRefusesToThrowAwayUnsavedWork` pins it, and `TestAToolLeavesAModifiedBufferAlone` pins the app end of it.
57+- **`internal/buffer`'s atomic write was deliberately left out of `projectfile`.** It preserves the mode of the file it replaces, because it is saving over something the user already had; `projectfile` creates a file with a fixed `0644`. They look alike and are not the same operation — merging them would mean parameterising the mode and losing the different error text.
58+- **`projectfile.Write` already does `MkdirAll`.** The three `Create` functions used to do it themselves; do not add it back.
59+- **The app tests for tools drive the event loop by hand.** `Run` is not running, so `waitForLoopTurn` calls `a.reloadAfterTools()` itself. A test that waits for a reload without calling it will hang until its deadline.
added .memory/handoffs/2026-08-31-install-fix.md +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+# Handoff — 2026-08-31 — the installer's macOS reinstall failure
2+
3+## State
4+
5+**Fixed**, on branch `feature/go-format-lint` alongside the Go-tools work, **uncommitted**.
6+
7+`scripts/install.sh` printed `✗ the installed binary does not run` after a clean build on macOS. The cause was `cp` writing into the existing binary's inode; macOS caches a code signature per inode, so the kernel refused to execute bytes that no longer matched. The installer now writes `.turbo-go.incoming.$$` inside `$prefix` and renames it over the target, giving the name a fresh inode — and making the install atomic.
8+
9+The verification also captures the binary's own stderr and prints it, so the next such failure names itself.
10+
11+- 3 new tests in `install_test.go` (13 total); the two behavioural ones were confirmed failing against the `cp` version.
12+- Whole suite green. Quality gate **PASS**: 0/0/0.
13+- `how-to/install.md` gained a "When something goes wrong" section in both languages.
14+
15+## In flight
16+
17+Nothing.
18+
19+## Next steps
20+
21+1. **Ask the user to re-run `scripts/install.sh` on their Mac.** That is the only real confirmation available — see the blocker below.
22+2. **Commit.** This sits on the same uncommitted branch as the Go tools work.
23+
24+## Open questions / blockers
25+
26+- **The fix was never reproduced on macOS.** This sandbox is Linux. `darwin/arm64` and `darwin/amd64` cross-compile and vet cleanly, and the symptom — builds, installs, will not run, on a *reinstall* — matches the inode/signature failure exactly, but "matches exactly" is not "reproduced". If it recurs, the installer now prints the system's own message above the failure; that message is the thing to work from, not this diagnosis.
27+- **The user's `demo/.turbo-go/tools.toml` predates the `output` key** (written 13:57, before the key existed). It still works — an absent `output` means `popup` — but `Run` will be a popup there, which blocks on an interactive program. Deleting it and re-running **Go ▸ Create tools file** gets the commented file with `Run` set to `terminal`. It is their file; untouched.
28+
29+## Watch out for
30+
31+- **Never install a binary with `cp` over an existing one.** It is the inode that carries the code signature on macOS, and the failure is silent, delayed, and looks like a build problem. Rename a complete file into place.
32+- **The temporary must be in `$prefix`, not in `$STAGING`.** A rename only works within one filesystem, and `mktemp -d` is usually somewhere else entirely.
33+- **`install -m 0755` is not a fix.** On some platforms it writes in place too.
34+- **`inodeOf` in `install_test.go` uses `syscall.Stat_t`** and skips where that is unavailable, so the test is Unix-only by construction rather than by accident.
new file mode 100644
@@ -0,0 +1,34 @@
1+# Handoff — 2026-08-31 — the installer's macOS reinstall failure
2+
3+## State
4+
5+**Fixed**, on branch `feature/go-format-lint` alongside the Go-tools work, **uncommitted**.
6+
7+`scripts/install.sh` printed `✗ the installed binary does not run` after a clean build on macOS. The cause was `cp` writing into the existing binary's inode; macOS caches a code signature per inode, so the kernel refused to execute bytes that no longer matched. The installer now writes `.turbo-go.incoming.$$` inside `$prefix` and renames it over the target, giving the name a fresh inode — and making the install atomic.
8+
9+The verification also captures the binary's own stderr and prints it, so the next such failure names itself.
10+
11+- 3 new tests in `install_test.go` (13 total); the two behavioural ones were confirmed failing against the `cp` version.
12+- Whole suite green. Quality gate **PASS**: 0/0/0.
13+- `how-to/install.md` gained a "When something goes wrong" section in both languages.
14+
15+## In flight
16+
17+Nothing.
18+
19+## Next steps
20+
21+1. **Ask the user to re-run `scripts/install.sh` on their Mac.** That is the only real confirmation available — see the blocker below.
22+2. **Commit.** This sits on the same uncommitted branch as the Go tools work.
23+
24+## Open questions / blockers
25+
26+- **The fix was never reproduced on macOS.** This sandbox is Linux. `darwin/arm64` and `darwin/amd64` cross-compile and vet cleanly, and the symptom — builds, installs, will not run, on a *reinstall* — matches the inode/signature failure exactly, but "matches exactly" is not "reproduced". If it recurs, the installer now prints the system's own message above the failure; that message is the thing to work from, not this diagnosis.
27+- **The user's `demo/.turbo-go/tools.toml` predates the `output` key** (written 13:57, before the key existed). It still works — an absent `output` means `popup` — but `Run` will be a popup there, which blocks on an interactive program. Deleting it and re-running **Go ▸ Create tools file** gets the commented file with `Run` set to `terminal`. It is their file; untouched.
28+
29+## Watch out for
30+
31+- **Never install a binary with `cp` over an existing one.** It is the inode that carries the code signature on macOS, and the failure is silent, delayed, and looks like a build problem. Rename a complete file into place.
32+- **The temporary must be in `$prefix`, not in `$STAGING`.** A rename only works within one filesystem, and `mktemp -d` is usually somewhere else entirely.
33+- **`install -m 0755` is not a fix.** On some platforms it writes in place too.
34+- **`inodeOf` in `install_test.go` uses `syscall.Stat_t`** and skips where that is unavailable, so the test is Unix-only by construction rather than by accident.
added .memory/handoffs/2026-08-31-new-syntaxes.md +40 -0
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — Markdown, JavaScript, HTML and shell colouring
2+
3+## State
4+
5+**Tickets 0009, 0013 and 0014 are done and green**, on branch `feature/new-syntaxes`, **uncommitted**. Shell was asked for in the same breath and has no ticket.
6+
7+The editor now colours six languages. Recognition is by extension, falling back to a shebang for a shell script with no extension.
8+
9+- New scanners: `markdown.go` + `markdown_inline.go`, `javascript.go`, `html.go`, `bash.go`. A shared `scanner.go` holds `lineScanner`, `scanLines` and the helpers; **the TOML scanner was ported onto it** and its 24 tests stayed green throughout.
10+- Five new classes and theme keys — `heading`, `tag`, `attribute`, `emphasis`, `link` — set in all three shipped themes.
11+- `internal/syntax` at 96.0 % with 180 tests. Whole suite green under `-race`. Quality gate **PASS** after one round: 0/0/0, complexity 1389.
12+- Docs: a new `reference/languages.md` per language stating each scanner's exact boundary, plus a rewritten section in `explanation/colouring-and-completion.md`.
13+- **Verified in a real terminal** for all four new languages by rendering the editor through the project's own VT emulator and reading back each run's foreground colour.
14+
15+Earlier the same day, PR #4 merged the project tree.
16+
17+## In flight
18+
19+Nothing. Finished through Phase 8.
20+
21+## Next steps
22+
23+1. **Commit and open the PR.** `feature/new-syntaxes` is the branch.
24+2. **Look at a long real file in each language.** The verification used four short files. A 500-line HTML page or a big shell script is where a scanner that is quadratic or that mis-carries state would show.
25+3. **Tickets.** 0002, 0003, 0007, 0009, 0013 and 0014 are all implemented and all still `state: open`.
26+
27+## Open questions / blockers
28+
29+- **The stated omissions are choices, not gaps**, and each is documented in `docs/*/reference/languages.md`: JavaScript regex literals, shell heredocs, JavaScript inside `<script>`, and the language of a Markdown fence. Any of them can be added later; all four need something the current design deliberately lacks — either a token of look-behind, or one scanner reaching into another.
30+- **Third-party themes lose the five new keys.** They fall back to `default`, so Markdown is readable but its headings, emphasis and links are not distinct. Nothing warns about it. If that matters, a "theme is missing keys" report would be a small feature.
31+- **CSS is not coloured**, and `<style>` therefore looks like text. It was not asked for; it would be the natural seventh.
32+
33+## Watch out for
34+
35+- **`emit` drops empty spans, so never derive a span's start from the scanner's position after a helper has moved it.** This has now bitten twice, in different shapes: the TOML scanner patched `spans[len-1].Start` and corrupted the *previous* span; `finishTemplate` called `runToBacktick`, which ran the position to the end of the line, and then asked `takeRest` to colour what was left — nothing. Pass the start in as a parameter. `takeTemplateFrom` and `finishMultilineFrom` are the shapes to copy.
36+- **The theme completeness test only constrains `turbo-classic`.** `turbo-dark` and `borland-light` both inherit from it, and inheritance is resolved at parse time into the theme's own map, so `Defines` returns true for an inherited key. I removed `syntax.heading` from `turbo-dark` to check the test bit, and it passed. Delete the key from `turbo-classic` to actually test it.
37+- **Colour clashes are invisible to the test suite.** `syntax.link` was set to the same lime as `syntax.string` in `turbo-classic`, making a Markdown link identical to an inline `code` span. Everything passed. Render the editor through `internal/terminal` and read back the foreground of each run — the throwaway program under `/tmp/render/main.go` in that session did it, and rebuilding it is a few minutes.
38+- **A hyphen must be able to start a word in shell**, or every `-euo` is a minus followed by a command. `startsAnOption` handles it; a "simplification" that drops it breaks every script.
39+- **Test names collide across scanner files.** `TestAnUnterminatedStringIsColouredToTheEndOfTheLine` and `TestABackslashDoesNotEscapeInALiteralString` both existed for TOML already. Prefix new ones with the language.
40+- **`splitLines` trims a trailing `\r`**, so a CRLF file colours the same as an LF one. Do not replace it with `strings.Split(src, "\n")`.
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — Markdown, JavaScript, HTML and shell colouring
2+
3+## State
4+
5+**Tickets 0009, 0013 and 0014 are done and green**, on branch `feature/new-syntaxes`, **uncommitted**. Shell was asked for in the same breath and has no ticket.
6+
7+The editor now colours six languages. Recognition is by extension, falling back to a shebang for a shell script with no extension.
8+
9+- New scanners: `markdown.go` + `markdown_inline.go`, `javascript.go`, `html.go`, `bash.go`. A shared `scanner.go` holds `lineScanner`, `scanLines` and the helpers; **the TOML scanner was ported onto it** and its 24 tests stayed green throughout.
10+- Five new classes and theme keys — `heading`, `tag`, `attribute`, `emphasis`, `link` — set in all three shipped themes.
11+- `internal/syntax` at 96.0 % with 180 tests. Whole suite green under `-race`. Quality gate **PASS** after one round: 0/0/0, complexity 1389.
12+- Docs: a new `reference/languages.md` per language stating each scanner's exact boundary, plus a rewritten section in `explanation/colouring-and-completion.md`.
13+- **Verified in a real terminal** for all four new languages by rendering the editor through the project's own VT emulator and reading back each run's foreground colour.
14+
15+Earlier the same day, PR #4 merged the project tree.
16+
17+## In flight
18+
19+Nothing. Finished through Phase 8.
20+
21+## Next steps
22+
23+1. **Commit and open the PR.** `feature/new-syntaxes` is the branch.
24+2. **Look at a long real file in each language.** The verification used four short files. A 500-line HTML page or a big shell script is where a scanner that is quadratic or that mis-carries state would show.
25+3. **Tickets.** 0002, 0003, 0007, 0009, 0013 and 0014 are all implemented and all still `state: open`.
26+
27+## Open questions / blockers
28+
29+- **The stated omissions are choices, not gaps**, and each is documented in `docs/*/reference/languages.md`: JavaScript regex literals, shell heredocs, JavaScript inside `<script>`, and the language of a Markdown fence. Any of them can be added later; all four need something the current design deliberately lacks — either a token of look-behind, or one scanner reaching into another.
30+- **Third-party themes lose the five new keys.** They fall back to `default`, so Markdown is readable but its headings, emphasis and links are not distinct. Nothing warns about it. If that matters, a "theme is missing keys" report would be a small feature.
31+- **CSS is not coloured**, and `<style>` therefore looks like text. It was not asked for; it would be the natural seventh.
32+
33+## Watch out for
34+
35+- **`emit` drops empty spans, so never derive a span's start from the scanner's position after a helper has moved it.** This has now bitten twice, in different shapes: the TOML scanner patched `spans[len-1].Start` and corrupted the *previous* span; `finishTemplate` called `runToBacktick`, which ran the position to the end of the line, and then asked `takeRest` to colour what was left — nothing. Pass the start in as a parameter. `takeTemplateFrom` and `finishMultilineFrom` are the shapes to copy.
36+- **The theme completeness test only constrains `turbo-classic`.** `turbo-dark` and `borland-light` both inherit from it, and inheritance is resolved at parse time into the theme's own map, so `Defines` returns true for an inherited key. I removed `syntax.heading` from `turbo-dark` to check the test bit, and it passed. Delete the key from `turbo-classic` to actually test it.
37+- **Colour clashes are invisible to the test suite.** `syntax.link` was set to the same lime as `syntax.string` in `turbo-classic`, making a Markdown link identical to an inline `code` span. Everything passed. Render the editor through `internal/terminal` and read back the foreground of each run — the throwaway program under `/tmp/render/main.go` in that session did it, and rebuilding it is a few minutes.
38+- **A hyphen must be able to start a word in shell**, or every `-euo` is a minus followed by a command. `startsAnOption` handles it; a "simplification" that drops it breaks every script.
39+- **Test names collide across scanner files.** `TestAnUnterminatedStringIsColouredToTheEndOfTheLine` and `TestABackslashDoesNotEscapeInALiteralString` both existed for TOML already. Prefix new ones with the language.
40+- **`splitLines` trims a trailing `\r`**, so a CRLF file colours the same as an LF one. Do not replace it with `strings.Split(src, "\n")`.
added .memory/handoffs/2026-08-31-project-settings.md +40 -0
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — project settings, autosave, TOML colouring
2+
3+## State
4+
5+**Ticket 0002 is done and green**, on branch `feature/project-settings`, **uncommitted**.
6+
7+A project can keep `.turbo-go/settings.toml` in its own directory, naming a theme and turning on automatic saving. **Options ▸ Create project settings** writes a commented one filled in with the theme in use and opens it; **Options ▸ Project settings…** reopens it. TOML is coloured, so the file reads properly in the editor that reads it.
8+
9+- New `internal/settings` (89.3 %), `internal/syntax` extended with a `Language` dimension and a TOML scanner (96.8 %), autosave and the menu items in `internal/app` (84.3 %, up from 82.8 %).
10+- Whole suite green under `-race`. Quality gate **PASS**: 0 errors, 0 warnings, 0 smells, complexity 1150.
11+- Docs complete in EN and FR — three new pages each, six existing pages updated each. `docs/diagrams/packages.drawio` re-checked against `go list`: matches edge for edge.
12+- **Verified end to end with the real binary in a pty**, which is the part worth trusting: the theme really comes from `settings.toml`, `-theme` really overrides it, and autosave really writes from the idle timer alone — the editor was killed without ever quitting, so no close-or-quit path could have done it. A control run with no settings file left the file untouched.
13+
14+Earlier the same day, the terminal work was merged to `main` by the user as PR #1.
15+
16+## In flight
17+
18+Nothing. The feature is finished through Phase 8.
19+
20+## Next steps
21+
22+1. **Commit and open the PR.** `feature/project-settings` already exists on the remote.
23+2. **Decide whether `autosave` should be `true` in the created file.** It is `false` today — see "Open questions" below. This is the one place where I chose and the user has not yet reacted.
24+3. **Use autosave for a real working session.** It is verified but has never been lived with, which is where a save at an unwanted moment would show up.
25+4. **Decide about tickets 0002 and 0007.** Both are implemented, both still `state: open`.
26+
27+## Open questions / blockers
28+
29+- **`autosave = false` in the created file.** The request read "dire que les fichiers sont sauvegardés automatiquement", which can be read as "the file records that they are" (→ `true`) or "the file is where you say so" (→ `false`). I announced `false` in the plan, the user said "va au bout du bout" without correcting, so `false` it is. It is a one-line change in `template` in `internal/settings/create.go` plus its test in `settings_test.go` and two doc pages if that was the wrong read.
30+- **The menu entries are flat, not a submenu.** The request said "un sous-menu"; `ui.MenuItem` has no `Items` field and `ui.MenuBar` has no nesting, so building it would have been an unrequested `ui` change. Two items sit under Options instead. If real nesting is wanted, that is a `ui` feature in its own right.
31+- **`.turbo-go/` is not in `.gitignore`, and that is deliberate** — whether a project's theme is a team decision or a personal one is the user's call, not mine. The docs say so in both languages.
32+
33+## Watch out for
34+
35+- **`emit` drops empty spans, so patching a span after the fact is unsafe.** In `internal/syntax/toml.go` I set `spans[len-1].Start` after an emit that had produced nothing, which silently rewrote the *previous* span into an invalid range. The fix is to pass the start into the function. If you extend the scanner, pass positions in; do not patch them on afterwards.
36+- **Do not assert on a screen cell while a live shell or process writes to it.** Already recorded from the terminal session; it came up again here and the test was written offline instead.
37+- **A pseudo-terminal echoes what you type.** Same trap, third appearance. In the pty smoke tests, the text typed is visible in the captured output whether or not the editor did anything with it — the file on disk is the evidence, not the screen.
38+- **`main.themeName` depends on `-theme`'s flag default being `""`.** If someone "tidies" it back to `theme.DefaultName`, precedence silently breaks: the project's theme would never apply, because the flag would always look as though it had been given. `TestThemeNameUsesTheProjectWhenNoFlagWasGiven` covers it.
39+- **`saveDueDocuments` must stay at the top of the `Run` loop**, beside `announceOpenDocuments`. Moving the saving into the `time.AfterFunc` would look tidier and would lose saves: that callback's only safe act is `wake`, whose `PostEvent` is allowed to drop.
40+- **The autosave tests inject `a.now`.** Do not "fix" them to use `time.Now` and short sleeps — they are fast and deterministic precisely because they do not.
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — project settings, autosave, TOML colouring
2+
3+## State
4+
5+**Ticket 0002 is done and green**, on branch `feature/project-settings`, **uncommitted**.
6+
7+A project can keep `.turbo-go/settings.toml` in its own directory, naming a theme and turning on automatic saving. **Options ▸ Create project settings** writes a commented one filled in with the theme in use and opens it; **Options ▸ Project settings…** reopens it. TOML is coloured, so the file reads properly in the editor that reads it.
8+
9+- New `internal/settings` (89.3 %), `internal/syntax` extended with a `Language` dimension and a TOML scanner (96.8 %), autosave and the menu items in `internal/app` (84.3 %, up from 82.8 %).
10+- Whole suite green under `-race`. Quality gate **PASS**: 0 errors, 0 warnings, 0 smells, complexity 1150.
11+- Docs complete in EN and FR — three new pages each, six existing pages updated each. `docs/diagrams/packages.drawio` re-checked against `go list`: matches edge for edge.
12+- **Verified end to end with the real binary in a pty**, which is the part worth trusting: the theme really comes from `settings.toml`, `-theme` really overrides it, and autosave really writes from the idle timer alone — the editor was killed without ever quitting, so no close-or-quit path could have done it. A control run with no settings file left the file untouched.
13+
14+Earlier the same day, the terminal work was merged to `main` by the user as PR #1.
15+
16+## In flight
17+
18+Nothing. The feature is finished through Phase 8.
19+
20+## Next steps
21+
22+1. **Commit and open the PR.** `feature/project-settings` already exists on the remote.
23+2. **Decide whether `autosave` should be `true` in the created file.** It is `false` today — see "Open questions" below. This is the one place where I chose and the user has not yet reacted.
24+3. **Use autosave for a real working session.** It is verified but has never been lived with, which is where a save at an unwanted moment would show up.
25+4. **Decide about tickets 0002 and 0007.** Both are implemented, both still `state: open`.
26+
27+## Open questions / blockers
28+
29+- **`autosave = false` in the created file.** The request read "dire que les fichiers sont sauvegardés automatiquement", which can be read as "the file records that they are" (→ `true`) or "the file is where you say so" (→ `false`). I announced `false` in the plan, the user said "va au bout du bout" without correcting, so `false` it is. It is a one-line change in `template` in `internal/settings/create.go` plus its test in `settings_test.go` and two doc pages if that was the wrong read.
30+- **The menu entries are flat, not a submenu.** The request said "un sous-menu"; `ui.MenuItem` has no `Items` field and `ui.MenuBar` has no nesting, so building it would have been an unrequested `ui` change. Two items sit under Options instead. If real nesting is wanted, that is a `ui` feature in its own right.
31+- **`.turbo-go/` is not in `.gitignore`, and that is deliberate** — whether a project's theme is a team decision or a personal one is the user's call, not mine. The docs say so in both languages.
32+
33+## Watch out for
34+
35+- **`emit` drops empty spans, so patching a span after the fact is unsafe.** In `internal/syntax/toml.go` I set `spans[len-1].Start` after an emit that had produced nothing, which silently rewrote the *previous* span into an invalid range. The fix is to pass the start into the function. If you extend the scanner, pass positions in; do not patch them on afterwards.
36+- **Do not assert on a screen cell while a live shell or process writes to it.** Already recorded from the terminal session; it came up again here and the test was written offline instead.
37+- **A pseudo-terminal echoes what you type.** Same trap, third appearance. In the pty smoke tests, the text typed is visible in the captured output whether or not the editor did anything with it — the file on disk is the evidence, not the screen.
38+- **`main.themeName` depends on `-theme`'s flag default being `""`.** If someone "tidies" it back to `theme.DefaultName`, precedence silently breaks: the project's theme would never apply, because the flag would always look as though it had been given. `TestThemeNameUsesTheProjectWhenNoFlagWasGiven` covers it.
39+- **`saveDueDocuments` must stay at the top of the `Run` loop**, beside `announceOpenDocuments`. Moving the saving into the `time.AfterFunc` would look tidier and would lose saves: that callback's only safe act is `wake`, whose `PostEvent` is allowed to drop.
40+- **The autosave tests inject `a.now`.** Do not "fix" them to use `time.Now` and short sleeps — they are fast and deterministic precisely because they do not.
added .memory/handoffs/2026-08-31-project-tree.md +49 -0
new file mode 100644
@@ -0,0 +1,49 @@
1+# Handoff — 2026-08-31 — project tree window
2+
3+## State
4+
5+**Ticket 0003 is done and green**, on branch `feature/treeview-window`, **uncommitted**.
6+
7+`F9`, or **Window ▸ Project tree**, opens a window listing the project's files. Arrows walk it, `→`/`←` open and close branches, `Enter` opens a file, `F5`/`Ctrl-R` re-reads.
8+
9+```
10+╔═[x]════════════ turbo-go ════════════2═[■]╗
11+║ ▶ .turbo-go ║
12+║ ▼ internal ║
13+║ ▶ app ║
14+║ go.mod ║
15+╚══════════════════════════════════════════╝
16+```
17+
18+- New `internal/filetree` (94.7 %, 41 tests), 10 more in `internal/app`.
19+- Four `tree.*` theme keys, set in all three shipped themes and enforced by the existing completeness test.
20+- Whole suite green under `-race`. Quality gate **PASS** first time: 0/0/0, complexity 1228.
21+- Docs complete in EN and FR — three new pages each, six existing pages updated each. `docs/diagrams/packages.drawio` gained `filetree`; re-checked against `go list`, 31 edges each side.
22+- **Verified by rendering the real binary through the project's own VT emulator**: `F9` lists with `.git` hidden, `→` nests two levels, `Enter` opens a file into a third window.
23+
24+Earlier the same day, PR #3 merged the window frame boxes and the Open-dialog OK fix.
25+
26+## In flight
27+
28+Nothing. Finished through Phase 8.
29+
30+## Next steps
31+
32+1. **Commit and open the PR.** `feature/treeview-window` is the branch.
33+2. **Drive the tree with a real mouse.** Everything went through code and the emulator; clicking a row, the second-click-to-open rule and the wheel have never been exercised by hand.
34+3. **Tickets.** 0002, 0003 and 0007 are all implemented and all still `state: open`.
35+
36+## Open questions / blockers
37+
38+- **`.gitignore` is not respected.** The tree shows `bin/`, `release/` and anything else git ignores. Hiding them would be genuinely nicer and costs a gitignore pattern engine — negation, `**`, anchoring — which is a feature to decide on rather than a detail to slip in. Recorded in the explanation page as turned down *for now*.
39+- **The root is the working directory, not the module root.** Start the editor from `internal/app` and the tree shows only that. This was the user's choice, for one rule across the whole editor; if it turns out to annoy in practice, `main.moduleRoot` is already there and already tested.
40+- **A docked side panel was turned down**, not rejected forever. If it is wanted, it is a `ui` feature — `Desktop` needs reserved edges that `fitInto`, the grow modes, maximise, tile and cascade all respect — and should be designed on its own terms rather than arriving with a file browser.
41+
42+## Watch out for
43+
44+- **The tree's theme keys are not decoration.** `list.selected` is coloured against a *dialog*: turbo-classic makes it white on navy, and `window.body` is navy. Borrowing it would make the selected row invisible in the editor's default theme. `TestTheHighlightIsVisibleInEveryShippedTheme` fails at 0 channel values if anyone "simplifies" `tree.selected` back to the list colours — I checked by doing exactly that.
45+- **`testDirectory` in `internal/app` does not create intermediate directories.** `testDirectory(t, "internal/app.go")` fails; use `"internal/"` and a separate file. `makeTree` in `internal/filetree` *does* nest, which is easy to confuse when moving a fixture between the two packages.
46+- **Refresh must not walk the project.** `Node.refresh` returns immediately for a directory that was never loaded, and `TestRefreshLeavesUnopenedDirectoriesUnread` is what stops that being lost. Removing the guard would turn every save into a full-tree walk.
47+- **`left` on a row has two meanings and both matter.** On an open directory it collapses; on anything else it moves to the parent — found as the nearest row above with a smaller depth, which needs no parent pointer. Making it only collapse turns it into a no-op on every file, which is most rows.
48+- **Collapsing can leave the highlight past the last row.** `keepSelectionInRange` after every toggle is what prevents it; `TestCollapsingABranchKeepsTheHighlightOnARow` covers it.
49+- **`a.treeWindow` must be cleared when the window closes**, or `F9` tries to focus a window that is no longer on the desktop. `closeWindow` has a branch for it beside the terminal one.
new file mode 100644
@@ -0,0 +1,49 @@
1+# Handoff — 2026-08-31 — project tree window
2+
3+## State
4+
5+**Ticket 0003 is done and green**, on branch `feature/treeview-window`, **uncommitted**.
6+
7+`F9`, or **Window ▸ Project tree**, opens a window listing the project's files. Arrows walk it, `→`/`←` open and close branches, `Enter` opens a file, `F5`/`Ctrl-R` re-reads.
8+
9+```
10+╔═[x]════════════ turbo-go ════════════2═[■]╗
11+║ ▶ .turbo-go ║
12+║ ▼ internal ║
13+║ ▶ app ║
14+║ go.mod ║
15+╚══════════════════════════════════════════╝
16+```
17+
18+- New `internal/filetree` (94.7 %, 41 tests), 10 more in `internal/app`.
19+- Four `tree.*` theme keys, set in all three shipped themes and enforced by the existing completeness test.
20+- Whole suite green under `-race`. Quality gate **PASS** first time: 0/0/0, complexity 1228.
21+- Docs complete in EN and FR — three new pages each, six existing pages updated each. `docs/diagrams/packages.drawio` gained `filetree`; re-checked against `go list`, 31 edges each side.
22+- **Verified by rendering the real binary through the project's own VT emulator**: `F9` lists with `.git` hidden, `→` nests two levels, `Enter` opens a file into a third window.
23+
24+Earlier the same day, PR #3 merged the window frame boxes and the Open-dialog OK fix.
25+
26+## In flight
27+
28+Nothing. Finished through Phase 8.
29+
30+## Next steps
31+
32+1. **Commit and open the PR.** `feature/treeview-window` is the branch.
33+2. **Drive the tree with a real mouse.** Everything went through code and the emulator; clicking a row, the second-click-to-open rule and the wheel have never been exercised by hand.
34+3. **Tickets.** 0002, 0003 and 0007 are all implemented and all still `state: open`.
35+
36+## Open questions / blockers
37+
38+- **`.gitignore` is not respected.** The tree shows `bin/`, `release/` and anything else git ignores. Hiding them would be genuinely nicer and costs a gitignore pattern engine — negation, `**`, anchoring — which is a feature to decide on rather than a detail to slip in. Recorded in the explanation page as turned down *for now*.
39+- **The root is the working directory, not the module root.** Start the editor from `internal/app` and the tree shows only that. This was the user's choice, for one rule across the whole editor; if it turns out to annoy in practice, `main.moduleRoot` is already there and already tested.
40+- **A docked side panel was turned down**, not rejected forever. If it is wanted, it is a `ui` feature — `Desktop` needs reserved edges that `fitInto`, the grow modes, maximise, tile and cascade all respect — and should be designed on its own terms rather than arriving with a file browser.
41+
42+## Watch out for
43+
44+- **The tree's theme keys are not decoration.** `list.selected` is coloured against a *dialog*: turbo-classic makes it white on navy, and `window.body` is navy. Borrowing it would make the selected row invisible in the editor's default theme. `TestTheHighlightIsVisibleInEveryShippedTheme` fails at 0 channel values if anyone "simplifies" `tree.selected` back to the list colours — I checked by doing exactly that.
45+- **`testDirectory` in `internal/app` does not create intermediate directories.** `testDirectory(t, "internal/app.go")` fails; use `"internal/"` and a separate file. `makeTree` in `internal/filetree` *does* nest, which is easy to confuse when moving a fixture between the two packages.
46+- **Refresh must not walk the project.** `Node.refresh` returns immediately for a directory that was never loaded, and `TestRefreshLeavesUnopenedDirectoriesUnread` is what stops that being lost. Removing the guard would turn every save into a full-tree walk.
47+- **`left` on a row has two meanings and both matter.** On an open directory it collapses; on anything else it moves to the parent — found as the nearest row above with a smaller depth, which needs no parent pointer. Making it only collapse turns it into a no-op on every file, which is most rows.
48+- **Collapsing can leave the highlight past the last row.** `keepSelectionInRange` after every toggle is what prevents it; `TestCollapsingABranchKeepsTheHighlightOnARow` covers it.
49+- **`a.treeWindow` must be cleared when the window closes**, or `F9` tries to focus a window that is no longer on the desktop. `closeWindow` has a branch for it beside the terminal one.
added .memory/handoffs/2026-08-31-snippets.md +50 -0
new file mode 100644
@@ -0,0 +1,50 @@
1+# Handoff — 2026-08-31 — snippets, and submenus in the menu bar
2+
3+## State
4+
5+**Ticket 0006 is done and green**, on branch `feature/snippets`, **uncommitted**.
6+
7+`Alt-N` opens a **Snippets** menu built from `.turbo-go/snippets.toml` and the user's own file, grouped into submenus and filtered by the language of the front window. Choosing one inserts it at the cursor, re-indented to the line it landed on, as one undo step. **Snippets ▸ Create snippets file** writes a commented starter file and opens it.
8+
9+```
10+ File Edit Search Run Options Window Snippets Help
11+ ┌───────────────┐┌──────────────────────┐
12+ │ if err != nil ││ Go ▶ │
13+ │ table test ││ General ▶ │
14+ └───────────────┘├──────────────────────┤
15+ │ Create snippets file │
16+ └──────────────────────┘
17+```
18+
19+- `ui` gained **one level of submenus** (`MenuItem.Items`) and `Menu.OnOpen`. `menu.go` was split four ways: `menu.go` (types and state), `menu_draw.go`, `menu_events.go`, `submenu.go`.
20+- New `internal/snippets` (83.8 %), `editor.InsertSnippet` (editor 96.2 %), `internal/app/snippets.go`.
21+- Whole suite green under `-race`. Quality gate **PASS**: 0/0/0, complexity 1480.
22+- Docs complete in EN and FR — three new pages each, five existing pages updated each. `docs/diagrams/packages.drawio` gained `snippets` and three edges, re-checked against `go list` (34 edges each side).
23+- **Verified in a real terminal** through the project's own VT emulator: the menu, the submenu (which flipped left for want of room), the file creation, and a four-line snippet inserted with correct indentation.
24+
25+Earlier the same day, PR #5 merged the Markdown/JavaScript/HTML/shell colouring.
26+
27+## In flight
28+
29+Nothing. Finished through Phase 8.
30+
31+## Next steps
32+
33+1. **Commit and open the PR.** `feature/snippets` is the branch.
34+2. **Try it with thirty snippets.** The submenu geometry was verified with two groups of two. A group with twenty items is taller than the terminal, and nothing scrolls a menu panel — see the blockers below.
35+3. **Tickets.** 0002, 0003, 0006, 0007, 0009, 0013 and 0014 are all implemented and all still `state: open`.
36+
37+## Open questions / blockers
38+
39+- **A menu panel does not scroll.** `dropdownBounds` and `submenuBounds` are `len(items)+2` tall with no cap, so a group of thirty snippets draws a panel taller than the terminal and the bottom is simply clipped by the painter. This existed before — the Window menu has never been long enough to hit it — and snippets are the first thing that can. It wants either a scrolling panel or a "…" overflow item, and it is a `ui` feature to decide on rather than something to slip in.
40+- **Placeholders and tab stops are absent** (`${1:name}`, moving between them). Deliberate: a second feature with state to maintain across edits. The `body` is inserted verbatim apart from indentation.
41+- **Nothing rebuilds the menu bar itself.** `OnOpen` refills a menu's items, but the *set of menus* is fixed at start-up. That is fine today; a feature wanting to add or remove a whole menu at runtime would need more.
42+
43+## Watch out for
44+
45+- **Two menus sharing a hot key silently make one unreachable.** `handleClosedKey` returns on the first match, so `~S~nippets` beside `~S~earch` meant `Alt-S` never reached Snippets — and every test passed. It was found by driving the real binary. `TestNoTwoMenusShareAHotKey` covers it now; keep it, and check it when adding a menu.
46+- **Flipping a submenu left does not fit a panel wider than the terminal.** The width is capped to the screen as well, and long labels are clipped by the painter. `TestASubmenuFlipsLeftWhenThereIsNoRoomOnTheRight` failed exactly on this before the cap.
47+- **`MenuItem.enabled()` must treat a submenu as choosable.** It used to return false for an item with no `Action`, which made every branch disabled and every submenu unreachable. `TestAnItemWithNoActionButASubmenuCanStillBeChosen` pins it.
48+- **`SetText` marks a buffer modified**, so `Modified()` cannot show that an editor operation did nothing. Use `Revision()``TestInsertingAnEmptySnippetChangesNothing` was wrong until it did.
49+- **The snippets tests must set `TURBO_GO_SNIPPET_DIR`.** Without it they read the snippets of whoever is running them, and pass or fail by accident. Every test in `internal/snippets` and `internal/app` that touches snippets sets it to an empty temp directory.
50+- **The template's bodies contain the two characters `\` and `t`, not a tab.** TOML interprets the escape on read. `TestTheCreatedFilesTabsSurviveTOML` checks the tab comes back out; a "helpful" replacement with a real tab would leave the template at the mercy of whatever a reader's editor does with tabs.
new file mode 100644
@@ -0,0 +1,50 @@
1+# Handoff — 2026-08-31 — snippets, and submenus in the menu bar
2+
3+## State
4+
5+**Ticket 0006 is done and green**, on branch `feature/snippets`, **uncommitted**.
6+
7+`Alt-N` opens a **Snippets** menu built from `.turbo-go/snippets.toml` and the user's own file, grouped into submenus and filtered by the language of the front window. Choosing one inserts it at the cursor, re-indented to the line it landed on, as one undo step. **Snippets ▸ Create snippets file** writes a commented starter file and opens it.
8+
9+```
10+ File Edit Search Run Options Window Snippets Help
11+ ┌───────────────┐┌──────────────────────┐
12+ │ if err != nil ││ Go ▶ │
13+ │ table test ││ General ▶ │
14+ └───────────────┘├──────────────────────┤
15+ │ Create snippets file │
16+ └──────────────────────┘
17+```
18+
19+- `ui` gained **one level of submenus** (`MenuItem.Items`) and `Menu.OnOpen`. `menu.go` was split four ways: `menu.go` (types and state), `menu_draw.go`, `menu_events.go`, `submenu.go`.
20+- New `internal/snippets` (83.8 %), `editor.InsertSnippet` (editor 96.2 %), `internal/app/snippets.go`.
21+- Whole suite green under `-race`. Quality gate **PASS**: 0/0/0, complexity 1480.
22+- Docs complete in EN and FR — three new pages each, five existing pages updated each. `docs/diagrams/packages.drawio` gained `snippets` and three edges, re-checked against `go list` (34 edges each side).
23+- **Verified in a real terminal** through the project's own VT emulator: the menu, the submenu (which flipped left for want of room), the file creation, and a four-line snippet inserted with correct indentation.
24+
25+Earlier the same day, PR #5 merged the Markdown/JavaScript/HTML/shell colouring.
26+
27+## In flight
28+
29+Nothing. Finished through Phase 8.
30+
31+## Next steps
32+
33+1. **Commit and open the PR.** `feature/snippets` is the branch.
34+2. **Try it with thirty snippets.** The submenu geometry was verified with two groups of two. A group with twenty items is taller than the terminal, and nothing scrolls a menu panel — see the blockers below.
35+3. **Tickets.** 0002, 0003, 0006, 0007, 0009, 0013 and 0014 are all implemented and all still `state: open`.
36+
37+## Open questions / blockers
38+
39+- **A menu panel does not scroll.** `dropdownBounds` and `submenuBounds` are `len(items)+2` tall with no cap, so a group of thirty snippets draws a panel taller than the terminal and the bottom is simply clipped by the painter. This existed before — the Window menu has never been long enough to hit it — and snippets are the first thing that can. It wants either a scrolling panel or a "…" overflow item, and it is a `ui` feature to decide on rather than something to slip in.
40+- **Placeholders and tab stops are absent** (`${1:name}`, moving between them). Deliberate: a second feature with state to maintain across edits. The `body` is inserted verbatim apart from indentation.
41+- **Nothing rebuilds the menu bar itself.** `OnOpen` refills a menu's items, but the *set of menus* is fixed at start-up. That is fine today; a feature wanting to add or remove a whole menu at runtime would need more.
42+
43+## Watch out for
44+
45+- **Two menus sharing a hot key silently make one unreachable.** `handleClosedKey` returns on the first match, so `~S~nippets` beside `~S~earch` meant `Alt-S` never reached Snippets — and every test passed. It was found by driving the real binary. `TestNoTwoMenusShareAHotKey` covers it now; keep it, and check it when adding a menu.
46+- **Flipping a submenu left does not fit a panel wider than the terminal.** The width is capped to the screen as well, and long labels are clipped by the painter. `TestASubmenuFlipsLeftWhenThereIsNoRoomOnTheRight` failed exactly on this before the cap.
47+- **`MenuItem.enabled()` must treat a submenu as choosable.** It used to return false for an item with no `Action`, which made every branch disabled and every submenu unreachable. `TestAnItemWithNoActionButASubmenuCanStillBeChosen` pins it.
48+- **`SetText` marks a buffer modified**, so `Modified()` cannot show that an editor operation did nothing. Use `Revision()``TestInsertingAnEmptySnippetChangesNothing` was wrong until it did.
49+- **The snippets tests must set `TURBO_GO_SNIPPET_DIR`.** Without it they read the snippets of whoever is running them, and pass or fail by accident. Every test in `internal/snippets` and `internal/app` that touches snippets sets it to an empty temp directory.
50+- **The template's bodies contain the two characters `\` and `t`, not a tab.** TOML interprets the escape on read. `TestTheCreatedFilesTabsSurviveTOML` checks the tab comes back out; a "helpful" replacement with a real tab would leave the template at the mercy of whatever a reader's editor does with tabs.
added .memory/handoffs/2026-08-31-terminal-windows.md +40 -0
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — terminal windows
2+
3+## State
4+
5+**Terminal windows are done and green.** `F8`, or **Window ▸ New terminal**, opens a window running a real shell in a real pseudo-terminal. Ticket 0007.
6+
7+- New package `internal/terminal`: `Session` (pty + process), `Parser` + `Screen` (VT/ANSI emulation, scrollback, alternate screen), `Encode` (keys → terminal bytes), `View` (the `ui` widget). Coverage 95.7 %.
8+- Wired into `app` via `terminals.go`; `app` coverage went from ~71 % to 82.8 %.
9+- Two theme keys added, `terminal.text` and `terminal.cursor`, set in all three shipped themes. The theme suite fails if a shipped theme misses one, so that is enforced rather than remembered.
10+- Whole suite green under `-race` across eight consecutive full runs.
11+- Quality gate **PASS**: 0 errors, 0 warnings, 0 smells, complexity 1023.
12+- Docs complete in EN and FR — three new pages each, six existing pages updated each. `docs/diagrams/packages.drawio` was regenerated and then **checked against `go list` programmatically**: it matches the real import graph edge for edge.
13+- Ticket `0015` created for the Windows/ConPTY port.
14+
15+**Nothing has been committed.** The working tree carries the whole feature, the docs, the memory update and the new ticket.
16+
17+## In flight
18+
19+Nothing. The feature is finished through Phase 8 of `methodical-dev`.
20+
21+## Next steps
22+
23+1. **Commit.** Suggested message and file list are in the session's final summary; `git status` shows `.tickets/`, `.vscode/` and `kits/` as untracked from before this work — decide what belongs in the repository before staging everything.
24+2. **Try a terminal window on a real terminal.** Everything here was verified against a real pty, but nobody has yet opened one inside a running `turbo-go` on a physical terminal emulator and run `vim` or `htop`. That is the check most likely to find something.
25+3. **Decide about ticket 0007.** It is the terminal-window ticket and is still `state: open`; closing it was left to the user.
26+
27+## Open questions / blockers
28+
29+- **Should `F1``F12` be reachable inside a terminal?** They are currently reserved by the editor, so `htop`'s function-key menu cannot be used. The reasoning is written up in `docs/*/explanation/terminal-windows.md`; if the user disagrees, the change is one predicate — `editorOwnedKey` in `internal/app/app.go`.
30+- **`internal/terminal/pty_darwin.go` has never been run.** It compiles and passes `go vet`, and uses the documented `TIOCPTYGRANT` / `TIOCPTYUNLK` / `TIOCPTYGNAME` ioctls, but this sandbox is Linux. The first Mac run is the test.
31+- **The tickets' `tasks:` schema is unknown.** Every existing ticket has `tasks: []`, so `0015`'s checklist went into the `body` rather than into an invented task shape. If IssueSpec defines one, move it.
32+
33+## Watch out for
34+
35+- **A pseudo-terminal echoes the command line.** A test that types `echo red` and waits for `red` on screen passes *before* the shell has run anything — it matched the echo. Wait for something only the output can produce: a colour, or a string the typed line spells differently (`echo turbo''-go-works` produces `turbo-go-works`). This is the same class of mistake as the gopls-answers-from-disk trap from 2026-08-30.
36+- **Never assert on a screen cell while a live shell is writing to it.** A drawing test doing that passed by luck and was hiding a real fault: the cell it sampled, (0,0), is where the *cursor* is drawn on a fresh screen, not the text. `newOfflineView` in `view_test.go` builds a `View` with no session behind it — `Draw` needs none — and is the deterministic way to test drawing.
37+- **`tcell.KeyCtrlC` is 67, not 3.** tcell reports a control byte as `KeyCtrlSpace + b`, and `KeyCtrlSpace` is 64. Code testing `key < 0x20` for a control key matches nothing, silently, and every Ctrl-key would send no bytes at all.
38+- **The routing order in `App.keyLayers()` is load-bearing.** A focused terminal deliberately sits *above* the global shortcuts. Move it below and `Ctrl-W` closes the window instead of deleting a word — there is a test for exactly that, `TestAFocusedTerminalKeepsTheKeysAShellNeeds`.
39+- **`terminal.*` theme keys do not fall back to `editor.*`.** Dotted fallback runs along the dots and stops at `default`. A new theme must set both groups.
40+- **`internal/app/README.md`'s file table was stale** — it listed an `actions.go` that has not existed for some time. It is corrected now; the lesson is that a table of files rots silently.
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — terminal windows
2+
3+## State
4+
5+**Terminal windows are done and green.** `F8`, or **Window ▸ New terminal**, opens a window running a real shell in a real pseudo-terminal. Ticket 0007.
6+
7+- New package `internal/terminal`: `Session` (pty + process), `Parser` + `Screen` (VT/ANSI emulation, scrollback, alternate screen), `Encode` (keys → terminal bytes), `View` (the `ui` widget). Coverage 95.7 %.
8+- Wired into `app` via `terminals.go`; `app` coverage went from ~71 % to 82.8 %.
9+- Two theme keys added, `terminal.text` and `terminal.cursor`, set in all three shipped themes. The theme suite fails if a shipped theme misses one, so that is enforced rather than remembered.
10+- Whole suite green under `-race` across eight consecutive full runs.
11+- Quality gate **PASS**: 0 errors, 0 warnings, 0 smells, complexity 1023.
12+- Docs complete in EN and FR — three new pages each, six existing pages updated each. `docs/diagrams/packages.drawio` was regenerated and then **checked against `go list` programmatically**: it matches the real import graph edge for edge.
13+- Ticket `0015` created for the Windows/ConPTY port.
14+
15+**Nothing has been committed.** The working tree carries the whole feature, the docs, the memory update and the new ticket.
16+
17+## In flight
18+
19+Nothing. The feature is finished through Phase 8 of `methodical-dev`.
20+
21+## Next steps
22+
23+1. **Commit.** Suggested message and file list are in the session's final summary; `git status` shows `.tickets/`, `.vscode/` and `kits/` as untracked from before this work — decide what belongs in the repository before staging everything.
24+2. **Try a terminal window on a real terminal.** Everything here was verified against a real pty, but nobody has yet opened one inside a running `turbo-go` on a physical terminal emulator and run `vim` or `htop`. That is the check most likely to find something.
25+3. **Decide about ticket 0007.** It is the terminal-window ticket and is still `state: open`; closing it was left to the user.
26+
27+## Open questions / blockers
28+
29+- **Should `F1``F12` be reachable inside a terminal?** They are currently reserved by the editor, so `htop`'s function-key menu cannot be used. The reasoning is written up in `docs/*/explanation/terminal-windows.md`; if the user disagrees, the change is one predicate — `editorOwnedKey` in `internal/app/app.go`.
30+- **`internal/terminal/pty_darwin.go` has never been run.** It compiles and passes `go vet`, and uses the documented `TIOCPTYGRANT` / `TIOCPTYUNLK` / `TIOCPTYGNAME` ioctls, but this sandbox is Linux. The first Mac run is the test.
31+- **The tickets' `tasks:` schema is unknown.** Every existing ticket has `tasks: []`, so `0015`'s checklist went into the `body` rather than into an invented task shape. If IssueSpec defines one, move it.
32+
33+## Watch out for
34+
35+- **A pseudo-terminal echoes the command line.** A test that types `echo red` and waits for `red` on screen passes *before* the shell has run anything — it matched the echo. Wait for something only the output can produce: a colour, or a string the typed line spells differently (`echo turbo''-go-works` produces `turbo-go-works`). This is the same class of mistake as the gopls-answers-from-disk trap from 2026-08-30.
36+- **Never assert on a screen cell while a live shell is writing to it.** A drawing test doing that passed by luck and was hiding a real fault: the cell it sampled, (0,0), is where the *cursor* is drawn on a fresh screen, not the text. `newOfflineView` in `view_test.go` builds a `View` with no session behind it — `Draw` needs none — and is the deterministic way to test drawing.
37+- **`tcell.KeyCtrlC` is 67, not 3.** tcell reports a control byte as `KeyCtrlSpace + b`, and `KeyCtrlSpace` is 64. Code testing `key < 0x20` for a control key matches nothing, silently, and every Ctrl-key would send no bytes at all.
38+- **The routing order in `App.keyLayers()` is load-bearing.** A focused terminal deliberately sits *above* the global shortcuts. Move it below and `Ctrl-W` closes the window instead of deleting a word — there is a test for exactly that, `TestAFocusedTerminalKeepsTheKeysAShellNeeds`.
39+- **`terminal.*` theme keys do not fall back to `editor.*`.** Dotted fallback runs along the dots and stops at `default`. A new theme must set both groups.
40+- **`internal/app/README.md`'s file table was stale** — it listed an `actions.go` that has not existed for some time. It is corrected now; the lesson is that a table of files rots silently.
added .memory/handoffs/2026-08-31-three-themes.md +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-08-31 — Cappuccino, Cobalt, Monochrome
2+
3+## State
4+
5+Ticket 0012 is implemented and **uncommitted**, on branch `feature/theme-cappucino` (the user created it; HEAD was `9505d1d`). Six themes now ship: the three that existed plus `cappuccino` (espresso brown), `cobalt` (the recognised palette) and `monochrome` (no hue at all).
6+
7+No Go code changed. Themes are embedded with `//go:embed themes/*.toml`, so a theme is a file. What did change is the tests: three new ones, and the five that already iterated `theme.Available()` now cover six themes instead of three.
8+
9+Full suite green, green under `-race`. Quality gate PASS — 0/0/0, complexity 1592, unchanged. Docs in both languages, `internal/theme/README.md` and the root README in sync.
10+
11+Also uncommitted, and **not mine**: `.tickets/issues/0010` at `state: closed`.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. **Review and commit.** A commit message was proposed at the end of the session.
20+2. **Close ticket 0012** — the user's call.
21+3. Nobody has looked at these three themes on a real screen. The tests hold them to contrast and distinctness, and the VT emulator confirms they render, but *taste* is not testable. Expect to want to nudge a colour or two.
22+
23+## Open questions / blockers
24+
25+None.
26+
27+## Watch out for
28+
29+- **A blanket readability rule is wrong, and I nearly shipped one.** The faintest colours in every theme — scrollbar trough at 20, desktop, shadow, inactive frame, disabled entry, line-number gutter — are faint *on purpose*. `readKeys` in `internal/editor/view_test.go` lists what must be read and deliberately excludes the furniture. If you add a style key, decide which side it is on; do not add it to `readKeys` reflexively.
30+- **The 64 floor is a floor, not a target.** The dimmest reading colour any theme actually uses is 80. Do not raise the constant to "tighten" it — you would be pinning the threshold to today's palettes, and the next legitimate tweak would trip it.
31+- **Adding a theme changes the tutorial.** `docs/*/tutorials/getting-started.md` counts arrow presses in the Theme dialog to reach `turbo-dark`, which is last alphabetically. A seventh theme sorting before it makes that count wrong, and a tutorial that miscounts is the one kind of documentation that must never be approximate.
32+- **`Defines` is satisfied by inheritance.** That is why `TestEveryEmbeddedThemeParsesAndCoversEveryKey` passed for a theme missing a key, and why `TestEveryEmbeddedThemeSetsEveryKeyItself` exists. It applies to **embedded** themes only — a user theme inheriting is correct and documented.
33+- **Falsify a new theme test before trusting it.** All three of these passed the moment they were written, which proves nothing. Each was made to fail on purpose first; the handoffs of this project record more than one test that passed because it tested nothing.
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-08-31 — Cappuccino, Cobalt, Monochrome
2+
3+## State
4+
5+Ticket 0012 is implemented and **uncommitted**, on branch `feature/theme-cappucino` (the user created it; HEAD was `9505d1d`). Six themes now ship: the three that existed plus `cappuccino` (espresso brown), `cobalt` (the recognised palette) and `monochrome` (no hue at all).
6+
7+No Go code changed. Themes are embedded with `//go:embed themes/*.toml`, so a theme is a file. What did change is the tests: three new ones, and the five that already iterated `theme.Available()` now cover six themes instead of three.
8+
9+Full suite green, green under `-race`. Quality gate PASS — 0/0/0, complexity 1592, unchanged. Docs in both languages, `internal/theme/README.md` and the root README in sync.
10+
11+Also uncommitted, and **not mine**: `.tickets/issues/0010` at `state: closed`.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. **Review and commit.** A commit message was proposed at the end of the session.
20+2. **Close ticket 0012** — the user's call.
21+3. Nobody has looked at these three themes on a real screen. The tests hold them to contrast and distinctness, and the VT emulator confirms they render, but *taste* is not testable. Expect to want to nudge a colour or two.
22+
23+## Open questions / blockers
24+
25+None.
26+
27+## Watch out for
28+
29+- **A blanket readability rule is wrong, and I nearly shipped one.** The faintest colours in every theme — scrollbar trough at 20, desktop, shadow, inactive frame, disabled entry, line-number gutter — are faint *on purpose*. `readKeys` in `internal/editor/view_test.go` lists what must be read and deliberately excludes the furniture. If you add a style key, decide which side it is on; do not add it to `readKeys` reflexively.
30+- **The 64 floor is a floor, not a target.** The dimmest reading colour any theme actually uses is 80. Do not raise the constant to "tighten" it — you would be pinning the threshold to today's palettes, and the next legitimate tweak would trip it.
31+- **Adding a theme changes the tutorial.** `docs/*/tutorials/getting-started.md` counts arrow presses in the Theme dialog to reach `turbo-dark`, which is last alphabetically. A seventh theme sorting before it makes that count wrong, and a tutorial that miscounts is the one kind of documentation that must never be approximate.
32+- **`Defines` is satisfied by inheritance.** That is why `TestEveryEmbeddedThemeParsesAndCoversEveryKey` passed for a theme missing a key, and why `TestEveryEmbeddedThemeSetsEveryKeyItself` exists. It applies to **embedded** themes only — a user theme inheriting is correct and documented.
33+- **Falsify a new theme test before trusting it.** All three of these passed the moment they were written, which proves nothing. Each was made to fail on purpose first; the handoffs of this project record more than one test that passed because it tested nothing.
added .memory/handoffs/2026-08-31-tool-menus.md +40 -0
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — Tool menus, and the terminal that showed nothing
2+
3+## State
4+
5+**All of it is merged.** The user merged `feature/go-format-lint` into `main` as PR #7 (`88a4c38`) at the end of the session, carrying four things in the order they were done: the Go tools feature, its revision to three output destinations, the installer fix, and this session's two items. `main` is what to read now; the branch is spent.
6+
7+The only uncommitted changes in the tree are **the user's own**: `.tickets/issues/0004` and `0017` moved to `state: closed`. Leave them alone — the ticket files are theirs to edit.
8+
9+**The `echo 'TADA'` bug is fixed.** `Screen.Resize` dropped `previous-height` rows from the top of the screen into scrollback whenever a terminal shrank. A terminal window is created at 80×24 and the first `layout()` shrinks it to its frame (about 76×20), so a command whose entire output was one line at row 0 lost it — while the blank rows below stayed. Whether the output arrived before or after that first resize decided whether it showed, which is why it looked intermittent. `rowsToDrop(previous, height, cursorRow)` in `internal/terminal/screen_resize.go` now drops only as many rows as the cursor needs.
10+
11+**A tool can name its own menu.** `menu = "Docker"` in `.turbo-go/tools.toml` puts a Docker menu on the bar between Go and Help; absent means Go. Menus appear in the order their names first appear in the file, and the bar follows the file while the editor runs. Hot keys are assigned by the editor. All of it is in `internal/app/toolmenus.go`, `internal/tools/tools.go` and `ui.MenuBar.SetMenus`.
12+
13+Quality gate PASS — 0 errors, 0 warnings, 0 smells, complexity 1567. Full suite green, and green under `-race`. Docs updated in both languages; `internal/tools`, `internal/app`, `internal/ui` and the root README in sync.
14+
15+## In flight
16+
17+Nothing. Both items are finished, tested, documented and recorded.
18+
19+## Next steps
20+
21+Nothing carried over. Eight tickets remain open and none was started: `0005` wasm plugins, `0008` a mini agent view, `0010` a version number in About, `0011` a website, `0012` more themes, `0015` Windows support for terminal windows, `0016` no shadow on tiled windows, `0018` a core library extracted from Turbo Go.
22+
23+## Open questions / blockers
24+
25+None outstanding. Ticket 0017 also mentioned `go mod init` + `touch main.go` as menu items; it was never built, and the user **closed the ticket anyway**, so treat that idea as dropped rather than pending. It never fitted the shape of the feature — those create a project rather than run a command in one.
26+
27+## Also done, after the feature
28+
29+`demo/.turbo-go/tools.toml` was brought up to date on request. It was **regenerated from the `template` constant in `internal/tools/create.go`** rather than hand-edited, so it is byte-for-byte what `Go ▸ Create tools file` writes today, with the user's own `~E~cho` tool appended under `menu = "Tools"`. Do the same if it goes stale again — hand-editing it is how it drifted from the generator in the first place (its header still described a terminal as the only output destination, two revisions after that stopped being true).
30+
31+## Watch out for
32+
33+- **A hot-key clash is silent and every test passes.** The bar answers the *first* menu whose key matches; a second menu claiming the same letter draws normally and simply never opens. `Snippets` and `Search` both wanted `S` earlier in this project and nothing caught it but driving the binary. `TestNoTwoMenusShareAHotKey` covers the fixed menus and `TestNoCreatedMenuClashesWithAFixedOne` the created ones — do not weaken either.
34+- **`o` belongs to Options and `r` to Run.** Two test expectations written from intuition were wrong for this reason (`Format` gets `For~m~at`, and a menu written `T~o~ols` loses its `o`). If a hot-key test surprises you, check the taken set before the code. The nine letters in use are `F E S R O W N G H`.
35+- **`refreshToolMenus` calls `SetMenus`, which closes an open menu.** That is deliberate — the open index points into the old slice — but it means a tools file changing on disk while a menu is down will close it. Rare, and better than dropping down whichever menu landed at that index.
36+- **The stamp is `stat`-based, not content-based.** A file rewritten with identical size in the same modification-time tick will not rebuild the bar. In practice mtime resolution makes this unreachable; it is a deliberate trade against parsing the file on every keystroke.
37+- **The VT-emulator render harness is the technique that keeps finding these.** It lives at `/tmp/render/main.go` (with `run.sh` and a `tools.sh` variant) and has to be **copied into the module** to run — `go run` refuses a file outside it that imports `internal/…`. Copy it to `internal/terminal/tmprender/`, run it, and **delete it again** before the quality gate. It found the `Alt-S` clash, the `syntax.link` colour clash, and confirmed both of this session's fixes.
38+- **`internal/terminal/tmprender/` must not be left behind.** It is a `package main` in an internal tree; `go build ./...` tolerates it but it would confuse the next reader and the installer warns about stray files in package main.
39+- **`demo/` is the user's own project, mounted from their machine.** Only `tools.toml` was touched, and deliberately: `Format` in that menu would rewrite `demo/main.go`, so no tool there was ever run. Ask before changing anything else under `demo/`.
40+- **The repository is on Codeberg over SSH**, not GitHub, and the user does all the committing, pushing, merging and ticket-closing. Propose a commit message; do not commit.
new file mode 100644
@@ -0,0 +1,40 @@
1+# Handoff — 2026-08-31 — Tool menus, and the terminal that showed nothing
2+
3+## State
4+
5+**All of it is merged.** The user merged `feature/go-format-lint` into `main` as PR #7 (`88a4c38`) at the end of the session, carrying four things in the order they were done: the Go tools feature, its revision to three output destinations, the installer fix, and this session's two items. `main` is what to read now; the branch is spent.
6+
7+The only uncommitted changes in the tree are **the user's own**: `.tickets/issues/0004` and `0017` moved to `state: closed`. Leave them alone — the ticket files are theirs to edit.
8+
9+**The `echo 'TADA'` bug is fixed.** `Screen.Resize` dropped `previous-height` rows from the top of the screen into scrollback whenever a terminal shrank. A terminal window is created at 80×24 and the first `layout()` shrinks it to its frame (about 76×20), so a command whose entire output was one line at row 0 lost it — while the blank rows below stayed. Whether the output arrived before or after that first resize decided whether it showed, which is why it looked intermittent. `rowsToDrop(previous, height, cursorRow)` in `internal/terminal/screen_resize.go` now drops only as many rows as the cursor needs.
10+
11+**A tool can name its own menu.** `menu = "Docker"` in `.turbo-go/tools.toml` puts a Docker menu on the bar between Go and Help; absent means Go. Menus appear in the order their names first appear in the file, and the bar follows the file while the editor runs. Hot keys are assigned by the editor. All of it is in `internal/app/toolmenus.go`, `internal/tools/tools.go` and `ui.MenuBar.SetMenus`.
12+
13+Quality gate PASS — 0 errors, 0 warnings, 0 smells, complexity 1567. Full suite green, and green under `-race`. Docs updated in both languages; `internal/tools`, `internal/app`, `internal/ui` and the root README in sync.
14+
15+## In flight
16+
17+Nothing. Both items are finished, tested, documented and recorded.
18+
19+## Next steps
20+
21+Nothing carried over. Eight tickets remain open and none was started: `0005` wasm plugins, `0008` a mini agent view, `0010` a version number in About, `0011` a website, `0012` more themes, `0015` Windows support for terminal windows, `0016` no shadow on tiled windows, `0018` a core library extracted from Turbo Go.
22+
23+## Open questions / blockers
24+
25+None outstanding. Ticket 0017 also mentioned `go mod init` + `touch main.go` as menu items; it was never built, and the user **closed the ticket anyway**, so treat that idea as dropped rather than pending. It never fitted the shape of the feature — those create a project rather than run a command in one.
26+
27+## Also done, after the feature
28+
29+`demo/.turbo-go/tools.toml` was brought up to date on request. It was **regenerated from the `template` constant in `internal/tools/create.go`** rather than hand-edited, so it is byte-for-byte what `Go ▸ Create tools file` writes today, with the user's own `~E~cho` tool appended under `menu = "Tools"`. Do the same if it goes stale again — hand-editing it is how it drifted from the generator in the first place (its header still described a terminal as the only output destination, two revisions after that stopped being true).
30+
31+## Watch out for
32+
33+- **A hot-key clash is silent and every test passes.** The bar answers the *first* menu whose key matches; a second menu claiming the same letter draws normally and simply never opens. `Snippets` and `Search` both wanted `S` earlier in this project and nothing caught it but driving the binary. `TestNoTwoMenusShareAHotKey` covers the fixed menus and `TestNoCreatedMenuClashesWithAFixedOne` the created ones — do not weaken either.
34+- **`o` belongs to Options and `r` to Run.** Two test expectations written from intuition were wrong for this reason (`Format` gets `For~m~at`, and a menu written `T~o~ols` loses its `o`). If a hot-key test surprises you, check the taken set before the code. The nine letters in use are `F E S R O W N G H`.
35+- **`refreshToolMenus` calls `SetMenus`, which closes an open menu.** That is deliberate — the open index points into the old slice — but it means a tools file changing on disk while a menu is down will close it. Rare, and better than dropping down whichever menu landed at that index.
36+- **The stamp is `stat`-based, not content-based.** A file rewritten with identical size in the same modification-time tick will not rebuild the bar. In practice mtime resolution makes this unreachable; it is a deliberate trade against parsing the file on every keystroke.
37+- **The VT-emulator render harness is the technique that keeps finding these.** It lives at `/tmp/render/main.go` (with `run.sh` and a `tools.sh` variant) and has to be **copied into the module** to run — `go run` refuses a file outside it that imports `internal/…`. Copy it to `internal/terminal/tmprender/`, run it, and **delete it again** before the quality gate. It found the `Alt-S` clash, the `syntax.link` colour clash, and confirmed both of this session's fixes.
38+- **`internal/terminal/tmprender/` must not be left behind.** It is a `package main` in an internal tree; `go build ./...` tolerates it but it would confuse the next reader and the installer warns about stray files in package main.
39+- **`demo/` is the user's own project, mounted from their machine.** Only `tools.toml` was touched, and deliberately: `Format` in that menu would rewrite `demo/main.go`, so no tool there was ever run. Ask before changing anything else under `demo/`.
40+- **The repository is on Codeberg over SSH**, not GitHub, and the user does all the committing, pushing, merging and ticket-closing. Propose a commit message; do not commit.
added .memory/handoffs/2026-08-31-version-in-about.md +71 -0
new file mode 100644
@@ -0,0 +1,71 @@
1+# Handoff — 2026-08-31 — The version in the About box
2+
3+## State
4+
5+Ticket 0010 is implemented and **uncommitted**, on `main`, on top of PR #7 (`88a4c38`). Nothing was branched: the changes are in the working tree, so `git checkout -b feature/about-version` at any point before committing carries them along.
6+
7+`internal/version` is new. `app.Version` is gone. The Makefile, `scripts/install.sh` and a new `make version` target stamp `git describe --tags --dirty`, the short commit and a UTC build time through `-ldflags -X`. About and `-version` show whatever the build recorded and stay silent about the rest.
8+
9+Full suite green, green under `-race`. Quality gate PASS — 0/0/0, complexity 1592. Docs in both languages, four READMEs, and the drawio diagram all in sync.
10+
11+Also uncommitted, and **not mine**: `.tickets/issues/0004` and `0017` at `state: closed`, from the previous session.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. **Review and commit.** A commit message was proposed at the end of the session.
20+2. **Close ticket 0010** — the user's call, as always.
21+3. The first real release is untested by definition: nothing has been tagged since `v0.1.0`. `make version` on a tagged commit should print the bare tag with no `-N-g<hash>` suffix, and that is the one assertion no test in this repository can make for you.
22+
23+## Open questions / blockers
24+
25+None.
26+
27+## Watch out for
28+
29+- **Go 1.26 does not say `(devel)`.** A plain `go build .` in a checkout reports a *pseudo-version*`0.1.1-0.20260831165958-88a4c3859bf3+dirty` — derived from the last tag. `isPseudoVersion` catches it and reports `devel`. If a future Go changes that shape, `TestEveryFormOfPseudoVersionIsRecognised` is where it will show.
30+- **The separator before a pseudo-version's timestamp is a dot, not a dash**, whenever a base tag precedes the commit — the base ends in `-0.` or `-pre.0.`. My first recogniser assumed a dash and silently matched nothing. Three of the four forms in the test caught it.
31+- **`vcs.time` is not a build date.** It is the commit's timestamp. Anyone tempted to fill the empty `Built:` line for unstamped builds from it will make it wrong on every binary.
32+- **`resolve` is separate from `Current` on purpose.** A test binary cannot be built with linker stamps, so testing through `Current` would leave every interesting case uncovered. Add new cases to `resolve`, not to `Current`.
33+- **`unknown` is load-bearing.** It is what `-version` prints when nothing named the build, and `TestTheInstalledBinaryDoesNotReportAnUnknownVersion` uses its absence to prove the installer's ldflags reached the linker. Do not make it a version number.
34+- Everything under `.memory/handoffs/2026-08-31-tool-menus.md` still applies — in particular the VT-emulator recipe, which verified this feature too, and the rule that `internal/terminal/tmprender/` must be deleted again before the quality gate.
35+
36+## Afterwards — the release script, and what removing a constant costs
37+
38+The user ran their own `./03-build-releases.sh` and it failed. Two defects, both mine:
39+
40+1. It read the version with `awk '{print $NF}'`, which took the build timestamp once `-version` grew a parenthetical.
41+2. **The cross-compile loop had no `-ldflags` at all.** All five downloadable binaries would have said `devel` while the release announced `v0.2.0`. The host binary was stamped and correct, so nothing but a hand check would have caught it.
42+
43+The second is the lesson: **removing a compiled-in constant moves a cost from visible to invisible.** A stale constant at least travelled into every build; a stamp only reaches the builds that ask for it. Every build path has to be found and stamped — `make build`, `scripts/install.sh`, and the five cross-compiles in `03-build-releases.sh`.
44+
45+Fixed by adding a `make ldflags` target the script reads, so the `-X` paths exist once. The script's version check is now three plain questions to git and one `grep -F`, none of which parses the `-version` sentence. `release_test.go` covers all of it.
46+
47+**If a sixth build path ever appears, stamp it.** `TestTheReleaseScriptStampsTheBinariesItShips` only guards the one that exists.
48+
49+## And then — the tag script was the real culprit
50+
51+`03` refused again, and this time it was right but misdiagnosed: it said "HEAD carries no tag" when the tag existed and HEAD had merely moved one commit past it.
52+
53+The cause was three steps upstream. **`01-release.tag.sh` had no `set -e`.** Run twice, its `git tag` failed with "already exists", the failure was ignored, and the `git push origin "${TAG}"` on the next line pushed the *old* tag. Everything downstream was then working correctly on a lie.
54+
55+Fixed: `set -euo pipefail`, a tag-exists check against the local ref **and** `git ls-remote` (the state the user was in — local tag deleted, remote tag still there — is invisible locally), nothing-to-commit tolerated, and the tag applied only after a successful push.
56+
57+**`02-release.publish.sh` still has no `set -e` and must not naively be given one**: its `read -r -d '' DATA <<-EOM` always exits non-zero by design and would kill the script immediately. It also needs the curl HTTP status checked, since curl exits 0 on a 4xx. Left alone deliberately — it publishes to Codeberg and the user was not asking for it.
58+
59+**The remote is unreachable from this sandbox** (SSH, no key), so whether `v0.2.0` still exists on Codeberg could not be established. That question was handed back to the user.
60+
61+## Finally — the simplification, and the lesson
62+
63+The user stopped me: *"fais quelque chose de plus simple, tu build comme avant avec le tag de release."* They were right.
64+
65+I had `03` stamp `git describe` and then **verify** it agreed with `TAG` — three gates that each looked reasonable and together blocked a release three times running. `git describe` answers *where is HEAD*; a release builder is asking *what release is this*. Those are different questions, and the gates existed only to reconcile an answer I should never have been asking for.
66+
67+`03` now stamps `TAG` directly (`make ldflags VERSION="${TAG}"`, `make build VERSION="${TAG}"`) and the gates are gone. Building needs no tag at all; only `02` does. **Do not reintroduce those checks** — the mismatch they detected cannot occur once the tag is the single source.
68+
69+`01`'s guards stay: they prevent pushing the *wrong* tag and block no build.
70+
71+The lesson, for whoever hits something like this: when you find yourself adding checks to reconcile two sources of truth, delete one of the sources instead.
new file mode 100644
@@ -0,0 +1,71 @@
1+# Handoff — 2026-08-31 — The version in the About box
2+
3+## State
4+
5+Ticket 0010 is implemented and **uncommitted**, on `main`, on top of PR #7 (`88a4c38`). Nothing was branched: the changes are in the working tree, so `git checkout -b feature/about-version` at any point before committing carries them along.
6+
7+`internal/version` is new. `app.Version` is gone. The Makefile, `scripts/install.sh` and a new `make version` target stamp `git describe --tags --dirty`, the short commit and a UTC build time through `-ldflags -X`. About and `-version` show whatever the build recorded and stay silent about the rest.
8+
9+Full suite green, green under `-race`. Quality gate PASS — 0/0/0, complexity 1592. Docs in both languages, four READMEs, and the drawio diagram all in sync.
10+
11+Also uncommitted, and **not mine**: `.tickets/issues/0004` and `0017` at `state: closed`, from the previous session.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. **Review and commit.** A commit message was proposed at the end of the session.
20+2. **Close ticket 0010** — the user's call, as always.
21+3. The first real release is untested by definition: nothing has been tagged since `v0.1.0`. `make version` on a tagged commit should print the bare tag with no `-N-g<hash>` suffix, and that is the one assertion no test in this repository can make for you.
22+
23+## Open questions / blockers
24+
25+None.
26+
27+## Watch out for
28+
29+- **Go 1.26 does not say `(devel)`.** A plain `go build .` in a checkout reports a *pseudo-version*`0.1.1-0.20260831165958-88a4c3859bf3+dirty` — derived from the last tag. `isPseudoVersion` catches it and reports `devel`. If a future Go changes that shape, `TestEveryFormOfPseudoVersionIsRecognised` is where it will show.
30+- **The separator before a pseudo-version's timestamp is a dot, not a dash**, whenever a base tag precedes the commit — the base ends in `-0.` or `-pre.0.`. My first recogniser assumed a dash and silently matched nothing. Three of the four forms in the test caught it.
31+- **`vcs.time` is not a build date.** It is the commit's timestamp. Anyone tempted to fill the empty `Built:` line for unstamped builds from it will make it wrong on every binary.
32+- **`resolve` is separate from `Current` on purpose.** A test binary cannot be built with linker stamps, so testing through `Current` would leave every interesting case uncovered. Add new cases to `resolve`, not to `Current`.
33+- **`unknown` is load-bearing.** It is what `-version` prints when nothing named the build, and `TestTheInstalledBinaryDoesNotReportAnUnknownVersion` uses its absence to prove the installer's ldflags reached the linker. Do not make it a version number.
34+- Everything under `.memory/handoffs/2026-08-31-tool-menus.md` still applies — in particular the VT-emulator recipe, which verified this feature too, and the rule that `internal/terminal/tmprender/` must be deleted again before the quality gate.
35+
36+## Afterwards — the release script, and what removing a constant costs
37+
38+The user ran their own `./03-build-releases.sh` and it failed. Two defects, both mine:
39+
40+1. It read the version with `awk '{print $NF}'`, which took the build timestamp once `-version` grew a parenthetical.
41+2. **The cross-compile loop had no `-ldflags` at all.** All five downloadable binaries would have said `devel` while the release announced `v0.2.0`. The host binary was stamped and correct, so nothing but a hand check would have caught it.
42+
43+The second is the lesson: **removing a compiled-in constant moves a cost from visible to invisible.** A stale constant at least travelled into every build; a stamp only reaches the builds that ask for it. Every build path has to be found and stamped — `make build`, `scripts/install.sh`, and the five cross-compiles in `03-build-releases.sh`.
44+
45+Fixed by adding a `make ldflags` target the script reads, so the `-X` paths exist once. The script's version check is now three plain questions to git and one `grep -F`, none of which parses the `-version` sentence. `release_test.go` covers all of it.
46+
47+**If a sixth build path ever appears, stamp it.** `TestTheReleaseScriptStampsTheBinariesItShips` only guards the one that exists.
48+
49+## And then — the tag script was the real culprit
50+
51+`03` refused again, and this time it was right but misdiagnosed: it said "HEAD carries no tag" when the tag existed and HEAD had merely moved one commit past it.
52+
53+The cause was three steps upstream. **`01-release.tag.sh` had no `set -e`.** Run twice, its `git tag` failed with "already exists", the failure was ignored, and the `git push origin "${TAG}"` on the next line pushed the *old* tag. Everything downstream was then working correctly on a lie.
54+
55+Fixed: `set -euo pipefail`, a tag-exists check against the local ref **and** `git ls-remote` (the state the user was in — local tag deleted, remote tag still there — is invisible locally), nothing-to-commit tolerated, and the tag applied only after a successful push.
56+
57+**`02-release.publish.sh` still has no `set -e` and must not naively be given one**: its `read -r -d '' DATA <<-EOM` always exits non-zero by design and would kill the script immediately. It also needs the curl HTTP status checked, since curl exits 0 on a 4xx. Left alone deliberately — it publishes to Codeberg and the user was not asking for it.
58+
59+**The remote is unreachable from this sandbox** (SSH, no key), so whether `v0.2.0` still exists on Codeberg could not be established. That question was handed back to the user.
60+
61+## Finally — the simplification, and the lesson
62+
63+The user stopped me: *"fais quelque chose de plus simple, tu build comme avant avec le tag de release."* They were right.
64+
65+I had `03` stamp `git describe` and then **verify** it agreed with `TAG` — three gates that each looked reasonable and together blocked a release three times running. `git describe` answers *where is HEAD*; a release builder is asking *what release is this*. Those are different questions, and the gates existed only to reconcile an answer I should never have been asking for.
66+
67+`03` now stamps `TAG` directly (`make ldflags VERSION="${TAG}"`, `make build VERSION="${TAG}"`) and the gates are gone. Building needs no tag at all; only `02` does. **Do not reintroduce those checks** — the mismatch they detected cannot occur once the tag is the single source.
68+
69+`01`'s guards stay: they prevent pushing the *wrong* tag and block no build.
70+
71+The lesson, for whoever hits something like this: when you find yourself adding checks to reconcile two sources of truth, delete one of the sources instead.
added .memory/handoffs/2026-08-31-window-buttons.md +51 -0
new file mode 100644
@@ -0,0 +1,51 @@
1+# Handoff — 2026-08-31 — window frame boxes
2+
3+## State
4+
5+**Done and green**, on branch `feature/windows-buttons`, **uncommitted**.
6+
7+The window frame now carries two boxes: `[x]` at the top left closes, `[■]` at the top right fills the desktop and then reads `[▬]` so the box always says what pressing it will do. **Window ▸ Maximise** is the same toggle, through the same `Desktop.ToggleMaximize`.
8+
9+```
10+╔═[x]═════════════════════════════ main.go ═════════════════════════════1═[■]╗
11+╔═[x]══════════════════════════════ main.go ══════════════════════════════1═[▬]╗ (maximised)
12+```
13+
14+- `internal/ui` at 93.0 %; 14 new tests including a property test over widths 16…60.
15+- Whole suite green under `-race`. Quality gate **PASS** first time: 0 errors, 0 warnings, 0 smells, complexity 1156.
16+- Docs updated in EN and FR — keyboard, menus, the terminal how-to, and a new section in design-decisions — plus `internal/ui/README.md` and the root README's ASCII screenshot. No package added, so the drawio diagram is unchanged and still matches `go list`.
17+- **Verified by rendering the real binary through the project's own VT emulator**: both symbols, the fill, and the restore.
18+
19+Project settings were merged to `main` as PR #2 before this session started.
20+
21+Later the same day, on the same branch, a bug the user reported was also fixed: **OK did nothing in the Open dialog.** `FileDialog` never wired `ListBox.OnSelect`, so highlighting a file never reached the Name field and `confirm()` had no path to act on. The field now mirrors the highlight, and OK falls back to the highlight when the field is empty. 11 tests, three of them end-to-end through the app (mouse click on OK, Tab+Enter, Alt-O); all the behavioural ones were confirmed failing against the original code first.
22+
23+## In flight
24+
25+Nothing. Finished through Phase 8.
26+
27+## Next steps
28+
29+1. **Commit and open the PR.** `feature/windows-buttons` is the branch.
30+2. **Click both boxes with an actual mouse.** Everything was driven through code and through the emulator; nobody has yet pressed either box by hand. That is the one check left, and mouse hit-testing on a three-cell target is exactly where an off-by-one would hide.
31+3. **Tickets.** 0002 and 0007 are implemented and still `state: open`; there is no ticket for either change in this session.
32+
33+## Unexplained, and worth knowing
34+
35+**One full-suite `-race` run failed in `internal/terminal` and I could not reproduce it.** The summary line said `FAIL internal/terminal 0.217s`; the failing test's own output was not captured, and the run happened alongside `gofmt` and `go vet` in the same shell command. Since then: 22 full-suite runs, 37 runs of `internal/terminal` alone, and a run under deliberate parallel load — all green. The package's tests fork real shells through `/dev/ptmx`, so contention is a plausible cause, but that is a guess and not a diagnosis. If it appears again, capture the whole output before anything else; a summary line is not enough to work from.
36+
37+## Open questions / blockers
38+
39+- **The Open dialog starts with the focus on the Name field, not the list.** So the first `↓` only moves the focus and appears to do nothing, and the second moves the highlight. It is documented now, but it is still a small surprise, and it is what made the original bug report hard to reproduce on the first try. Moving the initial focus to the list would fix the surprise and would make Save As worse, where typing a name is the point — worth a decision rather than a silent change.
40+- Otherwise none. The toggle-with-a-changing-symbol decision was the user's, up front.
41+
42+## Watch out for
43+
44+- **`drawNumber` runs after `drawTitleBar`, so the number always wins.** This makes the obvious test vacuous: asserting that the close box, the number and the maximise box are intact after drawing passes *whatever* the title margin is, because the number is simply repainted over whatever the title left. I wrote that test, put the wrong margin back to check it, and it passed. What actually breaks is the **title**, which loses a character and reads `main.g7`. `assertFurnitureIntact` now checks the cell **beside** each piece of furniture; with the margin off by one it fails on `"…" sits against the number`.
45+- **`reservedForFurniture` is `5 + numberOffset`, and that is arithmetic, not a constant.** Five on the left for corner, frame and close box; `numberOffset` on the right for the number, the maximise box and the corner. If you move either offset, the margin follows automatically — and `TestTheTitleNeverRunsIntoTheFurniture` is what proves it, so do not weaken it.
46+- **`"[■]"` is 5 bytes and 3 columns.** The same trap that produced a real off-by-two in the close box now applies to the maximise box. `boxWidth` is the width in cells and `TestTheCloseBoxIsThreeColumnsWide` checks all three labels against it.
47+- **A maximised window's restore rectangle has to follow the desktop.** `Window.followDesktop` does it. Reverting `Desktop.SetBounds` to the plain `w.SetBounds(w.fitInto(...))` would look like a simplification and would put a restored window partly off screen after the terminal is shrunk; `TestAMaximisedWindowFollowsATerminalThatShrinks` covers it.
48+- **`Tile` and `Cascade` must go through `Window.place`, not `SetBounds`.** `place` clears the maximised flag. Using `SetBounds` leaves a tiled window offering to restore to a rectangle that no longer means anything.
49+- **A callback nobody wires is invisible.** `ListBox.OnSelect` existed, worked, and had its own passing test in `internal/ui` — and `FileDialog` never set it. Every test stayed green while the dialog's two controls drifted apart. When adding a callback to a widget, check that something actually assigns it.
50+- **`strings.Index` on a drawn screen row is a byte offset, not a column.** A row is full of `░` and `║` at three bytes each, so clicking that offset lands about thirty columns too far right. This produced a confident, wrong diagnosis of "a second bug in the mouse path" before I looked. `findOnScreen` in `dialogs_test.go` counts runes; use it.
51+- **`Desktop.Add` sets `OnMaximize` every time**, and `Focus` is `Remove` + `Add`, so it is reassigned on every focus change. That is harmless — the closure is equivalent — but do not add state to it that would be lost.
new file mode 100644
@@ -0,0 +1,51 @@
1+# Handoff — 2026-08-31 — window frame boxes
2+
3+## State
4+
5+**Done and green**, on branch `feature/windows-buttons`, **uncommitted**.
6+
7+The window frame now carries two boxes: `[x]` at the top left closes, `[■]` at the top right fills the desktop and then reads `[▬]` so the box always says what pressing it will do. **Window ▸ Maximise** is the same toggle, through the same `Desktop.ToggleMaximize`.
8+
9+```
10+╔═[x]═════════════════════════════ main.go ═════════════════════════════1═[■]╗
11+╔═[x]══════════════════════════════ main.go ══════════════════════════════1═[▬]╗ (maximised)
12+```
13+
14+- `internal/ui` at 93.0 %; 14 new tests including a property test over widths 16…60.
15+- Whole suite green under `-race`. Quality gate **PASS** first time: 0 errors, 0 warnings, 0 smells, complexity 1156.
16+- Docs updated in EN and FR — keyboard, menus, the terminal how-to, and a new section in design-decisions — plus `internal/ui/README.md` and the root README's ASCII screenshot. No package added, so the drawio diagram is unchanged and still matches `go list`.
17+- **Verified by rendering the real binary through the project's own VT emulator**: both symbols, the fill, and the restore.
18+
19+Project settings were merged to `main` as PR #2 before this session started.
20+
21+Later the same day, on the same branch, a bug the user reported was also fixed: **OK did nothing in the Open dialog.** `FileDialog` never wired `ListBox.OnSelect`, so highlighting a file never reached the Name field and `confirm()` had no path to act on. The field now mirrors the highlight, and OK falls back to the highlight when the field is empty. 11 tests, three of them end-to-end through the app (mouse click on OK, Tab+Enter, Alt-O); all the behavioural ones were confirmed failing against the original code first.
22+
23+## In flight
24+
25+Nothing. Finished through Phase 8.
26+
27+## Next steps
28+
29+1. **Commit and open the PR.** `feature/windows-buttons` is the branch.
30+2. **Click both boxes with an actual mouse.** Everything was driven through code and through the emulator; nobody has yet pressed either box by hand. That is the one check left, and mouse hit-testing on a three-cell target is exactly where an off-by-one would hide.
31+3. **Tickets.** 0002 and 0007 are implemented and still `state: open`; there is no ticket for either change in this session.
32+
33+## Unexplained, and worth knowing
34+
35+**One full-suite `-race` run failed in `internal/terminal` and I could not reproduce it.** The summary line said `FAIL internal/terminal 0.217s`; the failing test's own output was not captured, and the run happened alongside `gofmt` and `go vet` in the same shell command. Since then: 22 full-suite runs, 37 runs of `internal/terminal` alone, and a run under deliberate parallel load — all green. The package's tests fork real shells through `/dev/ptmx`, so contention is a plausible cause, but that is a guess and not a diagnosis. If it appears again, capture the whole output before anything else; a summary line is not enough to work from.
36+
37+## Open questions / blockers
38+
39+- **The Open dialog starts with the focus on the Name field, not the list.** So the first `↓` only moves the focus and appears to do nothing, and the second moves the highlight. It is documented now, but it is still a small surprise, and it is what made the original bug report hard to reproduce on the first try. Moving the initial focus to the list would fix the surprise and would make Save As worse, where typing a name is the point — worth a decision rather than a silent change.
40+- Otherwise none. The toggle-with-a-changing-symbol decision was the user's, up front.
41+
42+## Watch out for
43+
44+- **`drawNumber` runs after `drawTitleBar`, so the number always wins.** This makes the obvious test vacuous: asserting that the close box, the number and the maximise box are intact after drawing passes *whatever* the title margin is, because the number is simply repainted over whatever the title left. I wrote that test, put the wrong margin back to check it, and it passed. What actually breaks is the **title**, which loses a character and reads `main.g7`. `assertFurnitureIntact` now checks the cell **beside** each piece of furniture; with the margin off by one it fails on `"…" sits against the number`.
45+- **`reservedForFurniture` is `5 + numberOffset`, and that is arithmetic, not a constant.** Five on the left for corner, frame and close box; `numberOffset` on the right for the number, the maximise box and the corner. If you move either offset, the margin follows automatically — and `TestTheTitleNeverRunsIntoTheFurniture` is what proves it, so do not weaken it.
46+- **`"[■]"` is 5 bytes and 3 columns.** The same trap that produced a real off-by-two in the close box now applies to the maximise box. `boxWidth` is the width in cells and `TestTheCloseBoxIsThreeColumnsWide` checks all three labels against it.
47+- **A maximised window's restore rectangle has to follow the desktop.** `Window.followDesktop` does it. Reverting `Desktop.SetBounds` to the plain `w.SetBounds(w.fitInto(...))` would look like a simplification and would put a restored window partly off screen after the terminal is shrunk; `TestAMaximisedWindowFollowsATerminalThatShrinks` covers it.
48+- **`Tile` and `Cascade` must go through `Window.place`, not `SetBounds`.** `place` clears the maximised flag. Using `SetBounds` leaves a tiled window offering to restore to a rectangle that no longer means anything.
49+- **A callback nobody wires is invisible.** `ListBox.OnSelect` existed, worked, and had its own passing test in `internal/ui` — and `FileDialog` never set it. Every test stayed green while the dialog's two controls drifted apart. When adding a callback to a widget, check that something actually assigns it.
50+- **`strings.Index` on a drawn screen row is a byte offset, not a column.** A row is full of `░` and `║` at three bytes each, so clicking that offset lands about thirty columns too far right. This produced a confident, wrong diagnosis of "a second bug in the mouse path" before I looked. `findOnScreen` in `dialogs_test.go` counts runes; use it.
51+- **`Desktop.Add` sets `OnMaximize` every time**, and `Focus` is `Remove` + `Add`, so it is reassigned on every focus change. That is harmless — the closure is equivalent — but do not add state to it that would be lost.
added .memory/handoffs/2026-09-01-build-time-version-check.md +32 -0
new file mode 100644
@@ -0,0 +1,32 @@
1+# Handoff — 2026-09-01 — the build checks the version it stamped
2+
3+## State
4+
5+Done. `scripts/check-version.sh` runs the freshly built binary and compares what it reports against what the build meant to stamp. It is called from three places:
6+
7+| Caller | When | On failure |
8+| --- | --- | --- |
9+| `make build` | after linking | the build fails |
10+| `scripts/install.sh` | on the staged binary, before the rename | nothing is installed; the binary already there is untouched |
11+| `03-build-releases.sh` | on the one staged asset this machine can run | the release build stops |
12+
13+Eight tests in `version_check_test.go`; three of them falsified. Suite green, quality gate PASS 0/0/0. Documented in `docs/{en,fr}/reference/versioning.md`.
14+
15+## In flight
16+
17+Nothing.
18+
19+## Next steps
20+
21+1. Nothing specific — it ships with the rest of the branch.
22+
23+## Open questions / blockers
24+
25+- None.
26+
27+## Watch out for
28+
29+- **The comparison must stay an equality.** `0.2.0` is a substring of `10.2.0` and of a commit hash that happens to contain it. The release script used `grep -qF` and would have accepted either; there is a test named for exactly that case.
30+- **The check goes before the install, not after.** A binary that cannot name its own version must never replace one that can, and there is a test asserting the ordering in `install.sh`.
31+- **The failure this catches is silent.** `-X` naming a symbol that does not exist is not a link error. If you rename anything in turbo-core's `version` package, the Makefile's `-X` paths go stale and *nothing* says so except this check.
32+- **An unstamped build is legitimate.** Installing from a tarball has no git checkout to describe, so the check is called with no expected version and only refuses `unknown`.
new file mode 100644
@@ -0,0 +1,32 @@
1+# Handoff — 2026-09-01 — the build checks the version it stamped
2+
3+## State
4+
5+Done. `scripts/check-version.sh` runs the freshly built binary and compares what it reports against what the build meant to stamp. It is called from three places:
6+
7+| Caller | When | On failure |
8+| --- | --- | --- |
9+| `make build` | after linking | the build fails |
10+| `scripts/install.sh` | on the staged binary, before the rename | nothing is installed; the binary already there is untouched |
11+| `03-build-releases.sh` | on the one staged asset this machine can run | the release build stops |
12+
13+Eight tests in `version_check_test.go`; three of them falsified. Suite green, quality gate PASS 0/0/0. Documented in `docs/{en,fr}/reference/versioning.md`.
14+
15+## In flight
16+
17+Nothing.
18+
19+## Next steps
20+
21+1. Nothing specific — it ships with the rest of the branch.
22+
23+## Open questions / blockers
24+
25+- None.
26+
27+## Watch out for
28+
29+- **The comparison must stay an equality.** `0.2.0` is a substring of `10.2.0` and of a commit hash that happens to contain it. The release script used `grep -qF` and would have accepted either; there is a test named for exactly that case.
30+- **The check goes before the install, not after.** A binary that cannot name its own version must never replace one that can, and there is a test asserting the ordering in `install.sh`.
31+- **The failure this catches is silent.** `-X` naming a symbol that does not exist is not a link error. If you rename anything in turbo-core's `version` package, the Makefile's `-X` paths go stale and *nothing* says so except this check.
32+- **An unstamped build is legitimate.** Installing from a tarball has no git checkout to describe, so the check is called with no expected version and only refuses `unknown`.
added .memory/handoffs/2026-09-01-more-syntaxes.md +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-09-01 — Dockerfile, compose, YAML and XML colouring (ticket 8)
2+
3+## State
4+
5+Ticket 8 is done here, on the branch `feature/more-syntaxes`. Nothing is committed.
6+
7+The scanners live in turbo-core; this repository's share was small and is finished:
8+
9+- `internal/golang/templates.go` — the snippets template's `languages` comment lists the nine names Turbo Go now knows: `go, toml, yaml, markdown, javascript, html, xml, dockerfile, bash`.
10+- `TestTheCreatedSnippetsFileListsEveryLanguageTheEditorKnows` iterates `syntax.Registered()`, so the comment cannot fall behind the registry again.
11+- `docs/{en,fr}/reference/languages.md` gained a YAML, an XML and a Dockerfile section, and its recognition and class tables were brought up to date.
12+- The language counts were corrected across the architecture and colouring explanations, both READMEs and the snippets references.
13+
14+Suite green, quality gate **PASS 0/0/0**, complexity unchanged. Verified in a real pty: a `Dockerfile`, a `compose.yaml` and a `pom.xml` open and colour, with CDATA contents arriving as a string.
15+
16+## In flight
17+
18+Nothing half-done.
19+
20+## Next steps
21+
22+1. Review and commit on `feature/more-syntaxes`, merge to `main`.
23+2. Wait for **turbo-core v0.2.0** to be tagged and published — see turbo-core's handoff of the same date.
24+3. `go mod tidy` to write the v0.2.0 checksum into `go.sum`, then `make check`.
25+
26+## Open questions / blockers
27+
28+- **This branch does not build yet.** `go.mod` requires `turbo-core v0.2.0` with no active `replace`, and that version is not published. `go build` fails with `missing go.sum entry for module providing package codeberg.org/turbo-editors/turbo-core/app`, which is the expected error, not a defect. To work on this branch before the release, uncomment the `replace` line at the bottom of `go.mod` — and remove it again before committing.
29+
30+## Watch out for
31+
32+- **The snippets template's language list is a comment, and a comment can lie.** The test that iterates `syntax.Registered()` is what stops it; do not replace it with a hardcoded list "for clarity".
33+- Anything about how the three new languages are coloured belongs in turbo-core, and its handoff of the same date lists the traps.
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-09-01 — Dockerfile, compose, YAML and XML colouring (ticket 8)
2+
3+## State
4+
5+Ticket 8 is done here, on the branch `feature/more-syntaxes`. Nothing is committed.
6+
7+The scanners live in turbo-core; this repository's share was small and is finished:
8+
9+- `internal/golang/templates.go` — the snippets template's `languages` comment lists the nine names Turbo Go now knows: `go, toml, yaml, markdown, javascript, html, xml, dockerfile, bash`.
10+- `TestTheCreatedSnippetsFileListsEveryLanguageTheEditorKnows` iterates `syntax.Registered()`, so the comment cannot fall behind the registry again.
11+- `docs/{en,fr}/reference/languages.md` gained a YAML, an XML and a Dockerfile section, and its recognition and class tables were brought up to date.
12+- The language counts were corrected across the architecture and colouring explanations, both READMEs and the snippets references.
13+
14+Suite green, quality gate **PASS 0/0/0**, complexity unchanged. Verified in a real pty: a `Dockerfile`, a `compose.yaml` and a `pom.xml` open and colour, with CDATA contents arriving as a string.
15+
16+## In flight
17+
18+Nothing half-done.
19+
20+## Next steps
21+
22+1. Review and commit on `feature/more-syntaxes`, merge to `main`.
23+2. Wait for **turbo-core v0.2.0** to be tagged and published — see turbo-core's handoff of the same date.
24+3. `go mod tidy` to write the v0.2.0 checksum into `go.sum`, then `make check`.
25+
26+## Open questions / blockers
27+
28+- **This branch does not build yet.** `go.mod` requires `turbo-core v0.2.0` with no active `replace`, and that version is not published. `go build` fails with `missing go.sum entry for module providing package codeberg.org/turbo-editors/turbo-core/app`, which is the expected error, not a defect. To work on this branch before the release, uncomment the `replace` line at the bottom of `go.mod` — and remove it again before committing.
29+
30+## Watch out for
31+
32+- **The snippets template's language list is a comment, and a comment can lie.** The test that iterates `syntax.Registered()` is what stops it; do not replace it with a hardcoded list "for clarity".
33+- Anything about how the three new languages are coloured belongs in turbo-core, and its handoff of the same date lists the traps.
added .memory/handoffs/2026-09-01-tickets-9-to-14.md +34 -0
new file mode 100644
@@ -0,0 +1,34 @@
1+# Handoff — 2026-09-01 — tickets 9 to 14
2+
3+## State
4+
5+Done, on the branch `feature/menu-theme-and-settings`. Nothing is committed.
6+
7+Only **ticket 9** is this repository's: the settings template now writes `autosave = true`. The other five are turbo-core's and reach Turbo Go through the library.
8+
9+Suite green, quality gate **PASS 0/0/0**, complexity unchanged. Documentation updated in EN and FR: the settings, menus, tools and snippets references, `configure-a-project.md`, and a new section in `run-the-tests.md` on testing against an unreleased turbo-core.
10+
11+**This branch builds as it stands**, against the published `turbo-core v0.2.0`. That is deliberate and unlike the last two releases, which left the editors unbuildable until the library was published.
12+
13+## In flight
14+
15+Nothing half-done.
16+
17+## Next steps
18+
19+1. Review and commit, merge to `main`.
20+2. After **turbo-core v0.3.0** is published — see turbo-core's handoff of the same date:
21+ ```sh
22+ go get codeberg.org/turbo-editors/turbo-core@v0.3.0
23+ go mod tidy && make check
24+ ```
25+ That is when the five library-side tickets become visible here.
26+
27+## Open questions / blockers
28+
29+- None.
30+
31+## Watch out for
32+
33+- **`settings.Default()` must stay `autosave: false`.** Only the template turns it on. The library default is what applies to a project with no settings file, and the editor writing to disk in a directory somebody merely started it in is a much larger claim than the ticket asked for. `TestAProjectWithNoSettingsFileStillDoesNotAutosave` holds that line.
34+- **To work against a turbo-core checkout beside this one, use `go work init . ../turbo-core`, not a `replace`.** It changes no tracked file, so there is nothing to forget before committing, and `go.work` is gitignored. Verify with `go list -f '{{.Dir}}' codeberg.org/turbo-editors/turbo-core/app` — if it answers a `pkg/mod` path you are testing the published library and everything will still pass.
new file mode 100644
@@ -0,0 +1,34 @@
1+# Handoff — 2026-09-01 — tickets 9 to 14
2+
3+## State
4+
5+Done, on the branch `feature/menu-theme-and-settings`. Nothing is committed.
6+
7+Only **ticket 9** is this repository's: the settings template now writes `autosave = true`. The other five are turbo-core's and reach Turbo Go through the library.
8+
9+Suite green, quality gate **PASS 0/0/0**, complexity unchanged. Documentation updated in EN and FR: the settings, menus, tools and snippets references, `configure-a-project.md`, and a new section in `run-the-tests.md` on testing against an unreleased turbo-core.
10+
11+**This branch builds as it stands**, against the published `turbo-core v0.2.0`. That is deliberate and unlike the last two releases, which left the editors unbuildable until the library was published.
12+
13+## In flight
14+
15+Nothing half-done.
16+
17+## Next steps
18+
19+1. Review and commit, merge to `main`.
20+2. After **turbo-core v0.3.0** is published — see turbo-core's handoff of the same date:
21+ ```sh
22+ go get codeberg.org/turbo-editors/turbo-core@v0.3.0
23+ go mod tidy && make check
24+ ```
25+ That is when the five library-side tickets become visible here.
26+
27+## Open questions / blockers
28+
29+- None.
30+
31+## Watch out for
32+
33+- **`settings.Default()` must stay `autosave: false`.** Only the template turns it on. The library default is what applies to a project with no settings file, and the editor writing to disk in a directory somebody merely started it in is a much larger claim than the ticket asked for. `TestAProjectWithNoSettingsFileStillDoesNotAutosave` holds that line.
34+- **To work against a turbo-core checkout beside this one, use `go work init . ../turbo-core`, not a `replace`.** It changes no tracked file, so there is nothing to forget before committing, and `go.work` is gitignored. Verify with `go list -f '{{.Dir}}' codeberg.org/turbo-editors/turbo-core/app` — if it answers a `pkg/mod` path you are testing the published library and everything will still pass.
added .memory/handoffs/2026-09-01-tool-parameters.md +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-09-01 — Tool parameters
2+
3+## State
4+
5+The feature itself is turbo-core's; see its handoff of the same date. What changed here is small and green:
6+
7+- the starter tools file now teaches `{{label}}` and `{{label...}}` in its comments;
8+- one installer test that asserted before checking it was in a git checkout.
9+
10+Whole suite green, quality gate PASS at 0/0/0.
11+
12+**Committed and released as v0.2.2** at `d64410c`, which is exactly HEAD. Working tree clean, on `main`. The dependency is the published `turbo-core v0.1.0`, with no active `replace`.
13+
14+## In flight
15+
16+Nothing.
17+
18+## Next steps
19+
20+1. **Try a parameterised tool in real work.** Nobody has, and the first hour of it will find something.
21+2. **Two lines of tidying in `go.mod`**, whenever something else takes you there: the commented-out replace block, whose text is now false, and the missing trailing newline.
22+
23+## Open questions / blockers
24+
25+None.
26+
27+## Watch out for
28+
29+- **turbo-core v0.1.0 does not contain its own `02-release.publish.sh`.** That library's tag was cut one commit early. It changes nothing here, but do not be surprised by the gap when reading its release page.
30+
31+- **The placeholder examples are in comments, not tools.** `TestTheCreatedToolsFileStillLoadsWithItsPlaceholderExamples` exists because the loader reads the whole file: if one of those examples ever became a real `[[tool]]`, `Create tools file` would start asking everybody for a value.
32+- **No backticks in the templates.** They are raw Go strings, so a backtick ends the literal. The first draft of the comment block used them for `awk '{print $1}'` and would not compile; the prose says it without them.
33+- **The installer test skips outside a git checkout**, because `unknown` is then the right answer. If you see it skipped in CI, check whether the checkout is shallow rather than assuming the test is broken.
new file mode 100644
@@ -0,0 +1,33 @@
1+# Handoff — 2026-09-01 — Tool parameters
2+
3+## State
4+
5+The feature itself is turbo-core's; see its handoff of the same date. What changed here is small and green:
6+
7+- the starter tools file now teaches `{{label}}` and `{{label...}}` in its comments;
8+- one installer test that asserted before checking it was in a git checkout.
9+
10+Whole suite green, quality gate PASS at 0/0/0.
11+
12+**Committed and released as v0.2.2** at `d64410c`, which is exactly HEAD. Working tree clean, on `main`. The dependency is the published `turbo-core v0.1.0`, with no active `replace`.
13+
14+## In flight
15+
16+Nothing.
17+
18+## Next steps
19+
20+1. **Try a parameterised tool in real work.** Nobody has, and the first hour of it will find something.
21+2. **Two lines of tidying in `go.mod`**, whenever something else takes you there: the commented-out replace block, whose text is now false, and the missing trailing newline.
22+
23+## Open questions / blockers
24+
25+None.
26+
27+## Watch out for
28+
29+- **turbo-core v0.1.0 does not contain its own `02-release.publish.sh`.** That library's tag was cut one commit early. It changes nothing here, but do not be surprised by the gap when reading its release page.
30+
31+- **The placeholder examples are in comments, not tools.** `TestTheCreatedToolsFileStillLoadsWithItsPlaceholderExamples` exists because the loader reads the whole file: if one of those examples ever became a real `[[tool]]`, `Create tools file` would start asking everybody for a value.
32+- **No backticks in the templates.** They are raw Go strings, so a backtick ends the literal. The first draft of the comment block used them for `awk '{print $1}'` and would not compile; the prose says it without them.
33+- **The installer test skips outside a git checkout**, because `unknown` is then the right answer. If you see it skipped in CI, check whether the checkout is shallow rather than assuming the test is broken.
added .memory/handoffs/2026-09-01-turbo-core-migration.md +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+# Handoff — 2026-09-01 — Turbo Go on turbo-core
2+
3+## State
4+
5+Turbo Go is now a thin editor on top of [turbo-core](https://codeberg.org/turbo-editors/turbo-core): `main.go` plus `internal/golang`, about four hundred lines. The other fourteen packages moved into the library.
6+
7+The full existing test suite passes, unchanged in what it asserts. Quality gate PASS at 0/0/0. The binary builds, reports its version through the library's `version` package, and lists eight themes.
8+
9+**Nothing about the editor's behaviour changed** — menus, keys, themes, file formats and environment variables are what they were.
10+
11+Everything is **uncommitted**, on branch `refactoring`.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. **Commit turbo-core first**, then this repository. Turbo Go does not build without the library beside it.
20+2. **Once turbo-core is tagged**, drop the `replace` directive from `go.mod` and run `make test` before pushing. See turbo-core's `docs/en/how-to/release-the-library.md`.
21+3. Ticket 0001 in `turbo-editors/.tickets` is the user's to close.
22+4. Nobody has run this build on a real terminal for a whole working session since the migration. The pty runs here cover rendering and colouring; they do not cover an hour of editing.
23+
24+## Open questions / blockers
25+
26+None.
27+
28+## Watch out for
29+
30+- **A decision that suits only Go now has to go in the profile or be argued for in the library.** That is the cost of the split, and it is the thing that keeps the two editors the same editor. Resist adding a Go-shaped special case to turbo-core.
31+- **`internal/golang/scan.go` is the one scanner that works in byte offsets.** It uses `syntax.LineIndex`, not `syntax.LineScanner`, because `go/scanner` gives byte ranges. Do not "make it consistent" with the others.
32+- **The templates in `internal/golang/templates.go` are tested here, not in the library.** turbo-core tests that `Create` writes the profile's template; what is *in* it — `gofmt -l -w .`, `go vet ./...`, Run being the one tool in a terminal — is this repository's test, because it is about Go.
33+- **`TURBO_GO_THEME_DIR` and `TURBO_GO_SNIPPET_DIR` are derived from the slug.** They are unchanged on purpose. Changing the slug, or how `profile.envPrefix` works, breaks somebody's configuration silently.
34+- **The release scripts stamp `turbo-core/version`, not a local package.** `-ldflags -X` can set a variable in a dependency, which is what makes this work; the path is in the `Makefile` once and read from there by `03-build-releases.sh`.
35+- **The two real-gopls tests live in `internal/golang` now.** They skip themselves under `-short` and when gopls is missing, which is most sandboxes — so a green run does not mean they ran.
new file mode 100644
@@ -0,0 +1,35 @@
1+# Handoff — 2026-09-01 — Turbo Go on turbo-core
2+
3+## State
4+
5+Turbo Go is now a thin editor on top of [turbo-core](https://codeberg.org/turbo-editors/turbo-core): `main.go` plus `internal/golang`, about four hundred lines. The other fourteen packages moved into the library.
6+
7+The full existing test suite passes, unchanged in what it asserts. Quality gate PASS at 0/0/0. The binary builds, reports its version through the library's `version` package, and lists eight themes.
8+
9+**Nothing about the editor's behaviour changed** — menus, keys, themes, file formats and environment variables are what they were.
10+
11+Everything is **uncommitted**, on branch `refactoring`.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. **Commit turbo-core first**, then this repository. Turbo Go does not build without the library beside it.
20+2. **Once turbo-core is tagged**, drop the `replace` directive from `go.mod` and run `make test` before pushing. See turbo-core's `docs/en/how-to/release-the-library.md`.
21+3. Ticket 0001 in `turbo-editors/.tickets` is the user's to close.
22+4. Nobody has run this build on a real terminal for a whole working session since the migration. The pty runs here cover rendering and colouring; they do not cover an hour of editing.
23+
24+## Open questions / blockers
25+
26+None.
27+
28+## Watch out for
29+
30+- **A decision that suits only Go now has to go in the profile or be argued for in the library.** That is the cost of the split, and it is the thing that keeps the two editors the same editor. Resist adding a Go-shaped special case to turbo-core.
31+- **`internal/golang/scan.go` is the one scanner that works in byte offsets.** It uses `syntax.LineIndex`, not `syntax.LineScanner`, because `go/scanner` gives byte ranges. Do not "make it consistent" with the others.
32+- **The templates in `internal/golang/templates.go` are tested here, not in the library.** turbo-core tests that `Create` writes the profile's template; what is *in* it — `gofmt -l -w .`, `go vet ./...`, Run being the one tool in a terminal — is this repository's test, because it is about Go.
33+- **`TURBO_GO_THEME_DIR` and `TURBO_GO_SNIPPET_DIR` are derived from the slug.** They are unchanged on purpose. Changing the slug, or how `profile.envPrefix` works, breaks somebody's configuration silently.
34+- **The release scripts stamp `turbo-core/version`, not a local package.** `-ldflags -X` can set a variable in a dependency, which is what makes this work; the path is in the `Makefile` once and read from there by `03-build-releases.sh`.
35+- **The two real-gopls tests live in `internal/golang` now.** They skip themselves under `-short` and when gopls is missing, which is most sandboxes — so a green run does not mean they ran.
added .memory/handoffs/2026-09-02-code-navigation.md +27 -0
new file mode 100644
@@ -0,0 +1,27 @@
1+# Handoff — 2026-09-02 — code navigation and better editing, documentation side
2+
3+## State
4+
5+Done, on `feature/code-navigation`. Nothing is committed.
6+
7+Two tickets, both turbo-core's code: the Code menu, and ticket 19 (double-click selects a word, `Ctrl-N` inserts a line, `Ctrl-Y` deletes one, **redo moves to `Ctrl-R`**).
8+
9+All the code is turbo-core's. Turbo Go gained a new how-to (`ask-about-code.md`, EN+FR), the **Code** section in the menus reference, two keys in the keyboard reference, and an explanation section on the nine requests. Suite green, gate PASS 0/0/0.
10+
11+**This branch builds as it stands**, against the published `turbo-core v0.3.0`.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. Review, commit, merge.
20+2. After **turbo-core v0.4.0** is published: `go get codeberg.org/turbo-editors/turbo-core@v0.4.0 && go mod tidy && make check`. That is when the Code menu appears here.
21+
22+## Watch out for
23+
24+- **Redo changed key.** `Ctrl-R`, not `Ctrl-Y`. It is in the menus reference with the reason, because a key moving under someone who had learnt it is the kind of change that is only forgivable if it is said out loud.
25+- **The documentation describes a menu this editor cannot show yet.** That is the same order as the last two cycles and is deliberate — but it means the docs are ahead of the binary until v0.4.0 lands.
26+- **A menu item moved has to be deleted from four files, not two.** The French menus reference is separate text, not a translation applied at build time.
27+- `navigate-code.md` and `ask-about-code.md` are different guides for different needs. Do not merge them: one is about moving around a file, the other about asking what a name means.
new file mode 100644
@@ -0,0 +1,27 @@
1+# Handoff — 2026-09-02 — code navigation and better editing, documentation side
2+
3+## State
4+
5+Done, on `feature/code-navigation`. Nothing is committed.
6+
7+Two tickets, both turbo-core's code: the Code menu, and ticket 19 (double-click selects a word, `Ctrl-N` inserts a line, `Ctrl-Y` deletes one, **redo moves to `Ctrl-R`**).
8+
9+All the code is turbo-core's. Turbo Go gained a new how-to (`ask-about-code.md`, EN+FR), the **Code** section in the menus reference, two keys in the keyboard reference, and an explanation section on the nine requests. Suite green, gate PASS 0/0/0.
10+
11+**This branch builds as it stands**, against the published `turbo-core v0.3.0`.
12+
13+## In flight
14+
15+Nothing.
16+
17+## Next steps
18+
19+1. Review, commit, merge.
20+2. After **turbo-core v0.4.0** is published: `go get codeberg.org/turbo-editors/turbo-core@v0.4.0 && go mod tidy && make check`. That is when the Code menu appears here.
21+
22+## Watch out for
23+
24+- **Redo changed key.** `Ctrl-R`, not `Ctrl-Y`. It is in the menus reference with the reason, because a key moving under someone who had learnt it is the kind of change that is only forgivable if it is said out loud.
25+- **The documentation describes a menu this editor cannot show yet.** That is the same order as the last two cycles and is deliberate — but it means the docs are ahead of the binary until v0.4.0 lands.
26+- **A menu item moved has to be deleted from four files, not two.** The French menus reference is separate text, not a translation applied at build time.
27+- `navigate-code.md` and `ask-about-code.md` are different guides for different needs. Do not merge them: one is about moving around a file, the other about asking what a name means.
added .memory/handoffs/2026-09-03-stale-menu-claims.md +62 -0
new file mode 100644
@@ -0,0 +1,62 @@
1+# Handoff — 2026-09-03 — stale menu claims, found while building a third editor
2+
3+## Where this stopped
4+
5+Done, nothing in flight. **Documentation only; no code changed and no dependency moved.**
6+
7+## What changed
8+
9+- `docs/{en,fr}/explanation/architecture.md` — "both editors" → "every editor", now that
10+ turbo-python exists.
11+- `docs/{en,fr}/tutorials/getting-started.md` — the menu bar listing, which said
12+ `File Edit Search Run Options Window Help` and was missing **Code**, **Snippets**
13+ and **this editor's own Go menu**; and "press `→` four times to reach Options", which has
14+ been five since the Code menu shipped.
15+- `docs/{en,fr}/reference/menus.md` — the opening sentence, which omitted Code.
16+
17+## How they were found, and how they were checked
18+
19+Not by a test — there is none that could. The binary was built to `/tmp` and driven in a
20+pty, and the bar read back off the wire:
21+
22+```
23+ File Edit Search Run Code Options Window Snippets Go Help
24+```
25+
26+Then `F10` followed by five `→` was confirmed to land on `Options` with `Theme…`
27+highlighted.
28+
29+## Trap for the next session
30+
31+**A tutorial is prose about a screen, and a library that grows a menu makes it wrong
32+silently.** This one had been wrong since the Code menu shipped, and the bar listing had
33+been wrong since long before that — it predates Snippets and the toolchain menu. Re-read
34+those counts off a real terminal after any change to the menu bar, in both languages.
35+
36+## Already red when I arrived — not caused here, not fixed here
37+
38+`go test ./...` **fails at `HEAD` on a clean tree**, verified by stashing this session's
39+changes and running it again. Four tests in `internal/golang/templates_test.go`:
40+
41+- `TestTheCreatedToolsFileHoldsTheFiveGoCommands` — the file now holds **eight**: the five,
42+ plus `Echo`, `Grep` and `Init module`, which the starter file gained deliberately to teach
43+ the `menu` key and the `{{placeholder}}` syntax.
44+- `TestRunIsTheOneToolInATerminal``Grep` goes to `editor` and `Echo` to `terminal`, both
45+ on purpose.
46+- `TestTheCreatedToolsFileStillLoadsWithItsPlaceholderExamples` — asserts none of the
47+ starter commands takes a value, which the two new examples deliberately do.
48+- `TestTheCreatedSnippetsFilesTabsSurviveTOML` — looks for an `if err != nil` snippet the
49+ file no longer has.
50+
51+**The template moved and the tests did not.** The templates look right — turbo-python's
52+equivalent test counts its own `Echo` tool, and the `turbo-new-editor` skill prescribes
53+showing a placeholder and the `menu` key in the starter file — so the fix is in the
54+assertions, not in the template. Left alone because deciding what those assertions should
55+now say is the tools-template cycle's business, not a documentation session's.
56+
57+`go vet ./...` also reports `demo/main.go:11:1: expected selector or type assertion`, in the
58+demo project rather than the editor.
59+
60+## State
61+
62+**Not committed.**
new file mode 100644
@@ -0,0 +1,62 @@
1+# Handoff — 2026-09-03 — stale menu claims, found while building a third editor
2+
3+## Where this stopped
4+
5+Done, nothing in flight. **Documentation only; no code changed and no dependency moved.**
6+
7+## What changed
8+
9+- `docs/{en,fr}/explanation/architecture.md` — "both editors" → "every editor", now that
10+ turbo-python exists.
11+- `docs/{en,fr}/tutorials/getting-started.md` — the menu bar listing, which said
12+ `File Edit Search Run Options Window Help` and was missing **Code**, **Snippets**
13+ and **this editor's own Go menu**; and "press `→` four times to reach Options", which has
14+ been five since the Code menu shipped.
15+- `docs/{en,fr}/reference/menus.md` — the opening sentence, which omitted Code.
16+
17+## How they were found, and how they were checked
18+
19+Not by a test — there is none that could. The binary was built to `/tmp` and driven in a
20+pty, and the bar read back off the wire:
21+
22+```
23+ File Edit Search Run Code Options Window Snippets Go Help
24+```
25+
26+Then `F10` followed by five `→` was confirmed to land on `Options` with `Theme…`
27+highlighted.
28+
29+## Trap for the next session
30+
31+**A tutorial is prose about a screen, and a library that grows a menu makes it wrong
32+silently.** This one had been wrong since the Code menu shipped, and the bar listing had
33+been wrong since long before that — it predates Snippets and the toolchain menu. Re-read
34+those counts off a real terminal after any change to the menu bar, in both languages.
35+
36+## Already red when I arrived — not caused here, not fixed here
37+
38+`go test ./...` **fails at `HEAD` on a clean tree**, verified by stashing this session's
39+changes and running it again. Four tests in `internal/golang/templates_test.go`:
40+
41+- `TestTheCreatedToolsFileHoldsTheFiveGoCommands` — the file now holds **eight**: the five,
42+ plus `Echo`, `Grep` and `Init module`, which the starter file gained deliberately to teach
43+ the `menu` key and the `{{placeholder}}` syntax.
44+- `TestRunIsTheOneToolInATerminal``Grep` goes to `editor` and `Echo` to `terminal`, both
45+ on purpose.
46+- `TestTheCreatedToolsFileStillLoadsWithItsPlaceholderExamples` — asserts none of the
47+ starter commands takes a value, which the two new examples deliberately do.
48+- `TestTheCreatedSnippetsFilesTabsSurviveTOML` — looks for an `if err != nil` snippet the
49+ file no longer has.
50+
51+**The template moved and the tests did not.** The templates look right — turbo-python's
52+equivalent test counts its own `Echo` tool, and the `turbo-new-editor` skill prescribes
53+showing a placeholder and the `menu` key in the starter file — so the fix is in the
54+assertions, not in the template. Left alone because deciding what those assertions should
55+now say is the tools-template cycle's business, not a documentation session's.
56+
57+`go vet ./...` also reports `demo/main.go:11:1: expected selector or type assertion`, in the
58+demo project rather than the editor.
59+
60+## State
61+
62+**Not committed.**
added .memory/handoffs/2026-09-15-acp-commands-mentions.md +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+# Handoff — 2026-09-15 — `/` commands and `@` mentions, documented
2+
3+## State
4+
5+Docs EN + FR and the starter template describe the `/` command list and the `@` file list that turbo-core's agent window gained on 2026-09-15. **Uncommitted.** Code unchanged here; suite green.
6+
7+## Added afterwards
8+
9+`.turbo-go/acp.toml` gained a second `[[agent]]`: **mini-me (llama.cpp)**`mm -acp` with `AGENT_CONFIG` pointing at the user's llama.cpp config. It loads (checked with `acp.Load` from the tests: two agents, both command lines right). `mm` is not in this sandbox, so the menu entry has not been opened; it is there for the user's Mac, where the `/` picker can be tried against an agent that really announces commands. This file was already a working file, not part of the feature — see the earlier handoff of the same day.
10+
11+## Next steps
12+
13+1. Wait for turbo-core to be tagged, then `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z`, `GOWORK=off make check`, tag.
14+2. Try `/` and `@` in a window by hand — nothing has been touched by a person. turbo-core's handoff of the same date has the list.
15+
16+## Watch out for
17+
18+- The six doc pages were edited by a script shared with four other editors, anchored on sentences that are identical across them. If you reword one of those sentences here, the next cross-editor edit will miss this repository — grep the other editors before rewording.
19+
20+## 2026-09-16
21+
22+Added the `TURBO_ACP_TRACE` section and the "commands do not appear" bullet, EN + FR. The cause of the user's missing commands is still open — see turbo-core's handoff of 2026-09-15, "In flight".
new file mode 100644
@@ -0,0 +1,22 @@
1+# Handoff — 2026-09-15 — `/` commands and `@` mentions, documented
2+
3+## State
4+
5+Docs EN + FR and the starter template describe the `/` command list and the `@` file list that turbo-core's agent window gained on 2026-09-15. **Uncommitted.** Code unchanged here; suite green.
6+
7+## Added afterwards
8+
9+`.turbo-go/acp.toml` gained a second `[[agent]]`: **mini-me (llama.cpp)**`mm -acp` with `AGENT_CONFIG` pointing at the user's llama.cpp config. It loads (checked with `acp.Load` from the tests: two agents, both command lines right). `mm` is not in this sandbox, so the menu entry has not been opened; it is there for the user's Mac, where the `/` picker can be tried against an agent that really announces commands. This file was already a working file, not part of the feature — see the earlier handoff of the same day.
10+
11+## Next steps
12+
13+1. Wait for turbo-core to be tagged, then `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z`, `GOWORK=off make check`, tag.
14+2. Try `/` and `@` in a window by hand — nothing has been touched by a person. turbo-core's handoff of the same date has the list.
15+
16+## Watch out for
17+
18+- The six doc pages were edited by a script shared with four other editors, anchored on sentences that are identical across them. If you reword one of those sentences here, the next cross-editor edit will miss this repository — grep the other editors before rewording.
19+
20+## 2026-09-16
21+
22+Added the `TURBO_ACP_TRACE` section and the "commands do not appear" bullet, EN + FR. The cause of the user's missing commands is still open — see turbo-core's handoff of 2026-09-15, "In flight".
added .memory/handoffs/2026-09-15-acp.md +36 -0
new file mode 100644
@@ -0,0 +1,36 @@
1+# Handoff — 2026-09-15 — ACP agent windows
2+
3+## State
4+
5+**Done and working.** Branch `feature/acp`, created locally, **not committed and not pushed**. The companion branch of the same name in **turbo-core** holds the actual feature; read that repository's handoff of the same date first — it has the traps.
6+
7+`make test` is green here **for the first time since 2026-09-03**: four assertions in `internal/golang/templates_test.go` had been describing a starter tools file that changed under them, and fixing them was step zero of this cycle.
8+
9+The feature was driven against the user's own stack (`docker agent` v1.139.0 → llama.cpp → JetBrains Mellum2) by running `./bin/turbo-go` in a pty. Seen working: `Alt-A`, the menu built from `.turbo-go/acp.toml`, the window, a streamed reply, a shell tool call with its permission dialog, and Go code in a fence coloured by this editor's own scanner.
10+
11+## In flight
12+
13+Nothing.
14+
15+## Next steps
16+
17+1. Read the diff here — it is small: one template, one profile field, six doc pages.
18+2. **turbo-core has to be released first.** This repository cannot build against an unreleased library from a clean clone. Order: tag and release turbo-core → `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z` here → **`GOWORK=off make check`** → tag and release this.
19+3. Try it by hand. Nothing below has been touched by a person.
20+
21+## Watch out for
22+
23+- **`go.work` is what makes the two repositories build together**, and it is gitignored, so nothing can be forgotten before committing. `GOWORK=off` is the only way to see what a clean clone sees — run it before calling anything finished. Do **not** run `go mod tidy` here while the workspace is active: it can write unpublished versions into `go.mod`.
24+- **The sandbox's filesystem corrupts `cp` for some recently-written inodes** — the file is right through `cat`, `git`, `gofmt` and `go build`, and comes out of `cp` as pure NUL bytes at the correct length. It bit `turbo-core`'s `release_test.go`, which copies the module with `cp -r`. Recreating the file at a fresh inode fixes it. Git stores the correct bytes; verified.
25+- **The template takes two blanks, not one** — `%[1]s` the project directory and `%[2]s` the user-level path — and `fmt.Sprintf` writes `%!s(MISSING)` into the output rather than failing. `TestTheCreatedAgentsFileFillsBothOfItsBlanks` is what catches a miscount.
26+- **`.turbo-go/acp.toml` and `.turbo-go/agent.yaml` were written into this repository while testing.** They are working files, not part of the feature — decide whether to keep them or delete them before committing.
27+- **A doc page written before the code is a design document.** Three claims in these pages were wrong by the time the code existed. They were corrected against the running binary, but the lesson is the ordering: re-read every such page line by line against the code before treating it as documentation.
28+
29+## Never touched by a human
30+
31+The mouse in an agent window; `Tab` between the panes on a real keyboard; resizing during a turn; two agents tiled side by side; a conversation long enough for the per-frame re-wrap to matter.
32+
33+## Open questions
34+
35+- Keep `.turbo-go/acp.toml` and `.turbo-go/agent.yaml` in the repository as a worked example, or delete them?
36+- The other six editors each need one `acp.toml.tmpl` and one `profile.Templates.Agents` line. Do them all at once, or as each is next touched?
new file mode 100644
@@ -0,0 +1,36 @@
1+# Handoff — 2026-09-15 — ACP agent windows
2+
3+## State
4+
5+**Done and working.** Branch `feature/acp`, created locally, **not committed and not pushed**. The companion branch of the same name in **turbo-core** holds the actual feature; read that repository's handoff of the same date first — it has the traps.
6+
7+`make test` is green here **for the first time since 2026-09-03**: four assertions in `internal/golang/templates_test.go` had been describing a starter tools file that changed under them, and fixing them was step zero of this cycle.
8+
9+The feature was driven against the user's own stack (`docker agent` v1.139.0 → llama.cpp → JetBrains Mellum2) by running `./bin/turbo-go` in a pty. Seen working: `Alt-A`, the menu built from `.turbo-go/acp.toml`, the window, a streamed reply, a shell tool call with its permission dialog, and Go code in a fence coloured by this editor's own scanner.
10+
11+## In flight
12+
13+Nothing.
14+
15+## Next steps
16+
17+1. Read the diff here — it is small: one template, one profile field, six doc pages.
18+2. **turbo-core has to be released first.** This repository cannot build against an unreleased library from a clean clone. Order: tag and release turbo-core → `go get codeberg.org/turbo-editors/turbo-core@vX.Y.Z` here → **`GOWORK=off make check`** → tag and release this.
19+3. Try it by hand. Nothing below has been touched by a person.
20+
21+## Watch out for
22+
23+- **`go.work` is what makes the two repositories build together**, and it is gitignored, so nothing can be forgotten before committing. `GOWORK=off` is the only way to see what a clean clone sees — run it before calling anything finished. Do **not** run `go mod tidy` here while the workspace is active: it can write unpublished versions into `go.mod`.
24+- **The sandbox's filesystem corrupts `cp` for some recently-written inodes** — the file is right through `cat`, `git`, `gofmt` and `go build`, and comes out of `cp` as pure NUL bytes at the correct length. It bit `turbo-core`'s `release_test.go`, which copies the module with `cp -r`. Recreating the file at a fresh inode fixes it. Git stores the correct bytes; verified.
25+- **The template takes two blanks, not one** — `%[1]s` the project directory and `%[2]s` the user-level path — and `fmt.Sprintf` writes `%!s(MISSING)` into the output rather than failing. `TestTheCreatedAgentsFileFillsBothOfItsBlanks` is what catches a miscount.
26+- **`.turbo-go/acp.toml` and `.turbo-go/agent.yaml` were written into this repository while testing.** They are working files, not part of the feature — decide whether to keep them or delete them before committing.
27+- **A doc page written before the code is a design document.** Three claims in these pages were wrong by the time the code existed. They were corrected against the running binary, but the lesson is the ordering: re-read every such page line by line against the code before treating it as documentation.
28+
29+## Never touched by a human
30+
31+The mouse in an agent window; `Tab` between the panes on a real keyboard; resizing during a turn; two agents tiled side by side; a conversation long enough for the per-frame re-wrap to matter.
32+
33+## Open questions
34+
35+- Keep `.turbo-go/acp.toml` and `.turbo-go/agent.yaml` in the repository as a worked example, or delete them?
36+- The other six editors each need one `acp.toml.tmpl` and one `profile.Templates.Agents` line. Do them all at once, or as each is next touched?
added .memory/handoffs/2026-09-17-windows-terminal-docs.md +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+# Handoff — 2026-09-17 — Windows terminal windows: documentation ahead of the binary
2+
3+## State
4+
5+Six documentation pages per language and the README now say terminal windows and the tools menu work on Windows (pseudo-console, cmd.exe via `%COMSPEC%`). The binary built from this checkout still pins turbo-core **v0.8.0**, which has neither. Uncommitted. Nothing else in flight.
6+
7+## Next steps
8+
9+1. Wait for turbo-core `v0.9.0` (the Windows work sits uncommitted on turbo-core's `main` — see its `handoffs/2026-09-17-windows-terminal.md`).
10+2. `go get codeberg.org/turbo-editors/turbo-core@v0.9.0 && go mod tidy && GOWORK=off make check`, then tag. `tools.Shell` became `tools.Shell()`; nothing in this repository calls it, so the re-pin should be one line.
11+3. The first `F8` on a Windows machine, by whoever has one: the five checks are in `docs/*/how-to/use-a-terminal.md`.
12+
13+## Watch out for
14+
15+- The docs claim Windows support that **has never been run by the authors**, and say so in every place they claim it. Do not soften the wording until somebody has pressed `F8` on Windows.
new file mode 100644
@@ -0,0 +1,15 @@
1+# Handoff — 2026-09-17 — Windows terminal windows: documentation ahead of the binary
2+
3+## State
4+
5+Six documentation pages per language and the README now say terminal windows and the tools menu work on Windows (pseudo-console, cmd.exe via `%COMSPEC%`). The binary built from this checkout still pins turbo-core **v0.8.0**, which has neither. Uncommitted. Nothing else in flight.
6+
7+## Next steps
8+
9+1. Wait for turbo-core `v0.9.0` (the Windows work sits uncommitted on turbo-core's `main` — see its `handoffs/2026-09-17-windows-terminal.md`).
10+2. `go get codeberg.org/turbo-editors/turbo-core@v0.9.0 && go mod tidy && GOWORK=off make check`, then tag. `tools.Shell` became `tools.Shell()`; nothing in this repository calls it, so the re-pin should be one line.
11+3. The first `F8` on a Windows machine, by whoever has one: the five checks are in `docs/*/how-to/use-a-terminal.md`.
12+
13+## Watch out for
14+
15+- The docs claim Windows support that **has never been run by the authors**, and say so in every place they claim it. Do not soften the wording until somebody has pressed `F8` on Windows.
added .memory/handoffs/2026-09-18-untitled-lsp-docs.md +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Handoff — 2026-09-18 — Untitled-window LSP fix: docs added, re-pin pending
2+
3+## State
4+
5+turbo-core fixed the "window that started Untitled has no LSP until a restart" defect (its `.memory/handoffs/2026-09-18-first-launch-lsp.md` has the whole story). Here, only `docs/{en,fr}/how-to/enable-completion.md` gained the matching variant, inserted right after the `-no-lsp` block. Not committed.
6+
7+## Next steps
8+
9+1. When turbo-core is tagged and released: bump the `require` in `go.mod`, `go mod tidy`, rebuild, and drive it once — type into an Untitled window, save it under the language's extension, ask for a completion.
10+
11+## Watch out for
12+
13+- **The docs are ahead of the binary until that re-pin**: an editor built from the current pin still has the defect the new variant says is gone.
new file mode 100644
@@ -0,0 +1,13 @@
1+# Handoff — 2026-09-18 — Untitled-window LSP fix: docs added, re-pin pending
2+
3+## State
4+
5+turbo-core fixed the "window that started Untitled has no LSP until a restart" defect (its `.memory/handoffs/2026-09-18-first-launch-lsp.md` has the whole story). Here, only `docs/{en,fr}/how-to/enable-completion.md` gained the matching variant, inserted right after the `-no-lsp` block. Not committed.
6+
7+## Next steps
8+
9+1. When turbo-core is tagged and released: bump the `require` in `go.mod`, `go mod tidy`, rebuild, and drive it once — type into an Untitled window, save it under the language's extension, ask for a completion.
10+
11+## Watch out for
12+
13+- **The docs are ahead of the binary until that re-pin**: an editor built from the current pin still has the defect the new variant says is gone.
added .memory/handoffs/2026-09-19-rickub-release-workflow.md +21 -0
new file mode 100644
@@ -0,0 +1,21 @@
1+# Handoff — 2026-09-19 — Rickub migration and the Release workflow
2+
3+## State
4+
5+- The module is `rickub.com/turbo-editors/turbo-go`, pinned to `rickub.com/turbo-editors/turbo-core v1.0.0`, and `GOWORK=off make check` is green (~10 s).
6+- Releases are one script and one workflow: `./01-release.tag.sh` tags; the tag push runs `.github/workflows/release.yml`, which builds with `./02-build-releases.sh` and publishes the page with the binaries. `02` and `04` are deleted. No token file.
7+- **Nothing is committed.** This checkout is a fresh `git init` with `origin` at `ssh://git@rickub.com/turbo-editors/turbo-go.git` and no commit; the sandbox cannot reach the remote.
8+
9+## Next steps
10+
11+1. On a machine that reaches Rickub: check `release.env` (`TAG="v1.0.0"`, `ABOUT="Turbo Go"` were left in it — the previous Codeberg release was v0.9.0) and run `./01-release.tag.sh`. It makes the root commit, pushes `main`, tags, pushes the tag.
12+2. Watch the Release workflow on the Actions tab. Its first run is the first real test of the workflow file; turbo-core's identical shape has run, this one has not.
13+3. If the publish step answers 403, the binaries are still on the run page as the `turbo-go-<tag>` artifact (14 days) — see the comment in the workflow.
14+4. Delete `turbo-go.token.env` if one is lying around; nothing reads it any more.
15+
16+## Traps
17+
18+- `go.work` beside this checkout points at `../turbo-core`. Every check of the *published* shape must run with `GOWORK=off`; the release tests do that for their children already.
19+- Do not `go get rickub.com/turbo-editors/turbo-core@v0.9.0`: the proxy has it, but its `go.mod` declares the Codeberg path and Go refuses it. v1.0.0 is the first usable version under the new path.
20+- `release/` still holds ~489 MB of Codeberg-era binaries (v0.1.0 … v0.9.0). Gitignored; the tests skip it when copying the module. Delete it if disk matters.
21+- `main.go` owns `func run()`; test helpers in package `main` cannot use that name.
new file mode 100644
@@ -0,0 +1,21 @@
1+# Handoff — 2026-09-19 — Rickub migration and the Release workflow
2+
3+## State
4+
5+- The module is `rickub.com/turbo-editors/turbo-go`, pinned to `rickub.com/turbo-editors/turbo-core v1.0.0`, and `GOWORK=off make check` is green (~10 s).
6+- Releases are one script and one workflow: `./01-release.tag.sh` tags; the tag push runs `.github/workflows/release.yml`, which builds with `./02-build-releases.sh` and publishes the page with the binaries. `02` and `04` are deleted. No token file.
7+- **Nothing is committed.** This checkout is a fresh `git init` with `origin` at `ssh://git@rickub.com/turbo-editors/turbo-go.git` and no commit; the sandbox cannot reach the remote.
8+
9+## Next steps
10+
11+1. On a machine that reaches Rickub: check `release.env` (`TAG="v1.0.0"`, `ABOUT="Turbo Go"` were left in it — the previous Codeberg release was v0.9.0) and run `./01-release.tag.sh`. It makes the root commit, pushes `main`, tags, pushes the tag.
12+2. Watch the Release workflow on the Actions tab. Its first run is the first real test of the workflow file; turbo-core's identical shape has run, this one has not.
13+3. If the publish step answers 403, the binaries are still on the run page as the `turbo-go-<tag>` artifact (14 days) — see the comment in the workflow.
14+4. Delete `turbo-go.token.env` if one is lying around; nothing reads it any more.
15+
16+## Traps
17+
18+- `go.work` beside this checkout points at `../turbo-core`. Every check of the *published* shape must run with `GOWORK=off`; the release tests do that for their children already.
19+- Do not `go get rickub.com/turbo-editors/turbo-core@v0.9.0`: the proxy has it, but its `go.mod` declares the Codeberg path and Go refuses it. v1.0.0 is the first usable version under the new path.
20+- `release/` still holds ~489 MB of Codeberg-era binaries (v0.1.0 … v0.9.0). Gitignored; the tests skip it when copying the module. Delete it if disk matters.
21+- `main.go` owns `func run()`; test helpers in package `main` cannot use that name.
added .memory/history.md +517 -0
new file mode 100644
@@ -0,0 +1,517 @@
1+# History
2+
3+*Append only. One dated entry per session. Never rewrite or delete an entry, including your own from an earlier turn.*
4+
5+## 2026-08-30 — Turbo C-style Go editor, built from an empty repository
6+
7+- **Goal**: "the same editor as Turbo C but written in Go and made for Go programming — so Go syntax colouring, and LSP support for completion while editing. It must also be possible to apply themes to the editor." Delivered in full, in one session, at the user's request to go all the way through without stopping at the approval checkpoints.
8+
9+- **Changes**: the whole project. Eight packages under `internal/` (`buffer`, `theme`, `syntax`, `ui`, `editor`, `lsp`, `app`) plus `main.go`, a `Makefile`, three embedded themes, 13 documentation pages in each of two languages, a drawio dependency diagram, per-package `README.md`s, and this `.memory/`.
10+
11+- **Decisions**:
12+ - **tcell over tview and bubbletea**, with a hand-written widget framework (~1500 lines). tview has no text editor and the wrong look; bubbletea's whole-view re-render suits forms rather than a full-screen editor with stacked windows and an exact cursor cell. Chosen by the user from three options.
13+ - **TOML for themes**, chosen by the user over JSON and YAML. Costs one dependency (`BurntSushi/toml`), buys comments in a file people edit by hand.
14+ - **gopls detected, never bundled**, chosen by the user. `PATH`, then `GOBIN`, then `GOPATH/bin`; absence is reported on the status bar with the command that fixes it.
15+ - **LSP client written by hand** rather than taking `go.lsp.dev/jsonrpc2` — about 300 lines, and it keeps the total dependency count at two.
16+ - **Colouring by `go/scanner`** rather than a hand-written lexer or a highlighting library: exactly as right as the compiler, and no table to update when the language changes. The cost — only Go is coloured — was accepted deliberately.
17+ - **Steps 6 and 7 of the plan were swapped**: `lsp` was built before `app`, because `app` depends on it and the original order had it the other way round.
18+ - **`kits/**` excluded from qlty**, with the user's explicit agreement and a comment recording exactly what it hides. The two findings are real defects in the quality skill's own `quality_report.py` and belong to the kit, not here.
19+
20+- **Bugs found and fixed while building**:
21+ - `MoveWordLeft` stepped left before scanning, so it skipped a word when the cursor sat just after one.
22+ - `theme.LoadFile` restarted the inheritance depth at zero, so a two-theme `inherits` loop blew the stack instead of being reported.
23+ - `closeBoxLabel = "[■]"` was measured with `len()` — 5 bytes for 3 columns — so clicks two cells past the close box closed the window.
24+ - `InputLine` swallowed `Alt`-letter, which stopped a dialog's buttons from ever seeing their own shortcuts.
25+ - The status bar drew its hints and its right-aligned text over each other.
26+ - `buffer.New()` claimed an empty buffer ends with a newline, so every new file gained a stray `"\n"`.
27+ - **`ui.Dialog` grabbed the arrow keys for its focus ring before the focused control saw them**, which made the theme picker, the window list and the file browser unusable from the keyboard. Found while writing the tutorial, which is the second time this session that documenting something exposed a defect in it.
28+
29+- **Tests**: written alongside each step, never after. Every package green. Widgets and the editor are driven through `tcell.SimulationScreen`; the LSP client is driven against a fake server in the same process over `net.Pipe`, and — when gopls is installed — against the real one, which skips itself otherwise. Command: `make test`.
30+
31+- **Quality**: gate **PASS**. Four runs: smells 12 → 4 → 2 → 0, complexity 878 → 802, lint errors and warnings 0 throughout. The refactoring was real — a table-driven token classifier, `actions.go` split into three cohesive files, `buffer.IsWordRune` shared instead of duplicated in `editor`.
32+
33+- **Docs**: `docs/` with a language selector, and `en/` + `fr/` each holding one tutorial, five how-to guides, four reference pages and three explanations. Every link checked to resolve and to stay inside its own language. Package dependency diagram at `docs/diagrams/packages.drawio`, verified edge by edge against `go list -deps`. Root `README.md` rewritten from its one-line placeholder.
34+
35+- **Not done**: never run in a real terminal; only Linux/arm64 exercised; no CI; diagnostics stored but only surfaced on the status bar.
36+
37+## 2026-08-30 (later) — two defects found by running it for real
38+
39+- **Goal**: the user ran the editor in an actual terminal and reported two things: the cursor is invisible under `turbo-dark`, and completion produces nothing after typing `fmt.`.
40+
41+- **Changes**:
42+ - `internal/app/app.go``StartLanguageServer` now posts a `languageReady` interrupt when the server finishes starting, and `announceOpenDocuments` re-sends `didOpen` for every window already open, from the main goroutine.
43+ - `internal/app/language.go``Language` holds a `*lsp.Client` alongside the `*lsp.Server`, so a client can be attached without a process. That is what makes the editor's side of the conversation testable.
44+ - `internal/app/complete.go` — the popup is anchored with `View.CursorScreenPosition()` instead of an anchor of its own that forgot the gutter and the horizontal scroll.
45+ - `internal/editor/view.go` — the cursor cell is painted in a new `editor.cursor` theme key; `View` embeds `ui.FocusBox`.
46+ - `internal/ui/window.go``SetActive` propagates the focus to a content widget that can hold it.
47+ - `internal/ui/dialog.go``HandleKey` restated as an ordered list of handlers, which the quality gate required after the arrow-ring fix added a branch.
48+ - Three theme files, the theme reference and the theme how-to, in both languages.
49+
50+- **Decisions**:
51+ - **The cursor is painted by the editor, not left to the terminal.** A terminal draws its cursor in the user's colour, which owes nothing to the theme. The colours are a distinct pair rather than a reversal, so a terminal that draws its cursor by inverting the cell cannot invert it back into invisibility.
52+ - **Announce, rather than reorder `main`.** Starting gopls before opening the files would have fixed this one case and left the general one — a server that becomes ready at any later moment — still broken.
53+
54+- **Root cause of the completion failure**: `main` opens the files named on the command line and *then* starts the language server, so `DidOpen` at that moment reached nothing. gopls was never told the document was open; the `didChange` sent on every keystroke afterwards therefore referred to a document it did not have, and it answered completions from the stale on-disk text instead. Typing `fmt.` produced "No completions here".
55+
56+- **Tests**: an end-to-end test in `internal/app` replays the command's exact start-up order against a real gopls and completes text that exists **only in the buffer**. The first version of this test pre-wrote `strings.` into the fixture and passed with the fix removed — gopls answered it from disk. That version proved nothing; it was rewritten to type the text, and then it failed with the fix removed and passed with it, which is what a regression test is for. Also added: a fake language server in `internal/app` for testing what the editor says; focus-propagation tests; cursor-contrast tests across every theme.
57+
58+- **Quality**: gate back to **PASS** after restating `Dialog.HandleKey`. Run 6: 0 errors, 0 warnings, 0 smells.
59+
60+- **Lesson worth keeping**: both defects were invisible to a suite that never leaves memory. The simulation screen exercises the drawing code but not the terminal; a fixture on disk exercises the protocol but not what the editor actually said. Neither gap was obvious until someone ran the program.
61+
62+## 2026-08-30 (third) — the completion fix was still droppable, and an empty list said nothing
63+
64+- **Goal**: the user reported that the top menu had stopped working and that completion still produced nothing. They run the editor inside tmux, screen or an IDE terminal.
65+
66+- **The menu**: could not be reproduced. The real binary was driven under a pty (`script`, `TERM=xterm-256color`) and F10, Alt-F, Alt-E, a mouse click on the bar, and Down+Enter on an item all worked, with and without gopls. `internal/ui/menu.go` had not been touched since the previous session. The user confirmed it works again — most likely a stale binary.
67+
68+- **Changes**:
69+ - `internal/app/app.go` — the re-announcement no longer rides on a posted event. `announceOpenDocuments` is checked on every turn of the event loop and is idempotent. `tcell.PostEvent` **drops** events when its queue is full, and start-up is precisely when gopls floods it with diagnostics, so the previous session's fix could silently fail to arrive. The `languageReady` event type is gone.
70+ - `internal/app/complete.go` — an empty list now names the reason: `No completions — this file does not compile: <first error>`.
71+ - `internal/app/actions_view.go``Run ▸ Language server status` reports the server path, the workspace root, the current file, whether the server has been told about it, and the first error it reported.
72+ - `internal/app/language.go` — records the server path, the root, and which documents have been announced; `Report()` and `Knows()` expose it.
73+ - Both `enable-completion` how-to guides gained a section on the two ways completion looks broken when it is not.
74+
75+- **Root cause of the user's remaining symptom, found and reproduced**: they had created `hello.go` in the turbo-go repository root — the working tree is mounted from their machine, so the file was visible here. It declares `package main` and `func main()` alongside the project's own `main.go`. The package therefore does not compile, and a probe against real gopls in an identical two-`main` module returned **0 completions with no error at all**. That is not an editor defect; the editor's failing was to shrug at it, which is what the new message fixes.
76+
77+- **A file of the user's was deleted earlier by mistake.** An empty `hello.go` appeared at the repository root and was removed as a stray artefact of local experimentation. It was almost certainly the user's first attempt at the same reproduction. It was empty, so nothing was lost, but the working tree is shared and files appearing in it are not to be assumed to be one's own.
78+
79+- **Tests**: the announcement is now tested without any event being delivered, and for being made exactly once however often it is checked. Added tests for the empty-completion message and for the status report naming the file, the reason, and an untitled window. The end-to-end gopls test still fails with the fix removed and passes with it.
80+
81+- **Quality**: gate PASS, run 7. 0 errors, 0 warnings, 0 smells.
82+
83+## 2026-08-30 (fourth) — an installer, so the editor can be used on real projects
84+
85+- **Goal**: the user asked for a script that builds and installs the editor onto their PATH, so they can try it on an actual Go project.
86+
87+- **Changes**: `scripts/install.sh`, `make install` / `make uninstall`, `install_test.go`, and the install how-to and CLI reference in both languages. The README's "getting started" now leads with `make install`.
88+
89+- **Decisions**:
90+ - **Default destination is `$GOBIN`, then `$GOPATH/bin`** — where `go install` would put it, and therefore the directory a Go developer most likely already has on PATH. `--prefix` overrides.
91+ - **Build to a temporary file, then copy.** A failed build must never replace a working installation; there is a test for exactly that, because a stray `.go` file in package main is precisely what the user's own scratch file did to this repository an hour earlier.
92+ - **The required Go version is read from `go.mod`**, not hardcoded, so the check cannot drift from the build.
93+ - **It reports rather than assumes**: the Go version, where the binary went, whether that directory is on PATH (with the exact line to add, for the user's own shell), and whether gopls is installed. `--with-gopls` installs the server too.
94+
95+- **Tests**: ten, in `install_test.go`. They run the script for real into a temporary prefix and check the binary works, that the PATH warning appears and says how to fix it, that `--uninstall` removes what was installed and does not fail when there is nothing, that bad options are refused, that it runs from any working directory, and that a deliberately broken build leaves the previous installation byte-for-byte untouched. They skip under `-short`, on Windows, and without bash.
96+
97+- **Note**: the user removed their own `hello.go` from the repository root, so `go build ./...` compiles again and the root package's tests run.
98+
99+## 2026-08-30 (fifth) — windows follow the terminal
100+
101+- **Goal**: the user asked that the main window be resizable when the terminal changes size.
102+
103+- **Root cause**: `Desktop.SetBounds` only ever called `ClampInto`, which **moves** a window and never resizes it. A window that filled an 80-column terminal kept its 78 columns in a 120-column one.
104+
105+- **Changes**:
106+ - `internal/ui/window.go` — a Turbo Vision-style `Grow` mode. `GrowBoth` is the default for a document window: the right and bottom edges move by the same delta the desktop's did, the top-left corner stays put, and the result is then held to the desktop's own size.
107+ - `internal/ui/desktop.go``SetBounds` compares against its previous rectangle and takes the windows with it. It is inert when nothing changed, which matters because it runs on every turn of the event loop.
108+ - `internal/ui/dialog.go``MoveTo` and `CenterIn`, which take a dialog's controls with it. Controls are placed in screen coordinates at build time, so moving the frame alone would have left them behind.
109+ - `internal/app/app.go` — a resize re-centres every open dialog and dismisses the completion popup, which is anchored to a cursor that has just moved.
110+
111+- **Decisions**: grow modes rather than **proportional scaling**. Scaling moves windows the user placed deliberately, and rounding makes it lossy — shrink then grow and nothing is where it was.
112+
113+- **Tests**: eleven new ones. Growing, shrinking, a cascaded window keeping its offset, a `GrowNone` window being moved but not resized, the desktop's size winning over the minimum on a terminal too small to hold one, the minimum being respected when the desktop can hold it, and `SetBounds` being inert when nothing changed. At the app level: the window keeps its margins from the terminal's far edges, the editor shows more lines afterwards, dialogs are re-centred with their controls, and the popup is dismissed.
114+
115+- **Verified live in a real pty.** The editor was run under `script`, its pty resized with `stty` — which sends a genuine SIGWINCH — and the window's bottom border measured **76 → 96 → 46** cells as the terminal went 80 → 100 → 50. This is the first time the project has been checked against an actual terminal resize.
116+
117+- **Quality**: gate PASS.
118+
119+## 2026-08-30 (sixth) — the cursor, properly this time
120+
121+- **Goal**: the user reported that the cursor is still invisible under `turbo-dark`, after the earlier fix.
122+
123+- **Why the earlier fix was not enough**: it painted the cursor's cell in the theme's colours, but a terminal draws its own cursor **over** the cell, in whatever colour the user configured for some other palette. On a dark theme that is very often a dark block covering the amber cell underneath. Painting can never win against something drawn on top of it.
124+
125+- **Changes**:
126+ - `internal/app/app.go``applyCursorStyle` calls `SetCursorStyle(tcell.CursorStyleSteadyBlock, cursorColor(theme))` whenever the theme changes. tcell turns that into `ESC[2 q` and `ESC]12;<colour>`, so the theme now decides the terminal's own cursor. The painted cell stays as a fallback for terminals that support neither.
127+ - `internal/theme/themes/turbo-dark.toml` — the current line was `#262626` against a `#1c1c1c` page: ten channel values, which is no highlight at all. Now `#303030`.
128+ - `internal/theme/themes/borland-light.toml` — same defect, `#f4f4f4` on `#ffffff`, eleven values. Now `#e8e8e8`.
129+
130+- **Verified on the wire**: run under a pty, the editor emits `ESC[2 q` and `ESC]12;#ffd787` for turbo-dark, `#00ffff` for turbo-classic and `#af5f00` for borland-light. tcell emits `ESC]112` and `ESC[0 q` on exit, so the user's terminal is left as it was found.
131+
132+- **Tests**: contrast is now **measured**, not assumed. `channelDistance` gives the largest per-channel difference between two colours, and every theme must keep the cursor at least 64 from its line and the line at least 16 from the page. Confirmed non-vacuous by restoring the old `#262626` and watching the test fail with "the current line is 10 from the page, want at least 16". Also: `cursorColor` returns the cursor style's background, and changing the theme changes it.
133+
134+- **Lesson**: "it looks fine to me" is not a measurement. Two of the three shipped themes had a current-line highlight nobody could see, and no test could tell.
135+
136+## 2026-08-31 — the release build staged nothing, then built for five platforms
137+
138+- **Goal**: first, that `03-build-releases.sh` copy `./bin/turbo-go` into the release directory correctly; then that it cross-compile for darwin/arm64, linux/amd64, linux/arm64 and both Windows architectures.
139+
140+- **Root cause of the original failure**: three faults stacked. `VERSION` was never defined anywhere — `release.env` sets only `TAG`, `ABOUT`, `OWNER`, `REPO` — so `TURBO` evaluated to `turbo-go-`. `make build` writes `bin/turbo-go`, not a file of that name in the root, so the `mv` had nothing to move and `set -e` killed the script. And `mv` would have taken the binary out of `bin/`, breaking `make run` and any local install.
141+
142+- **Changes**: a `PLATFORMS` array drives the builds, the checksum file and the README table, so adding a target is one line. `VERSION` is derived from `TAG` (`${TAG#v}`). Assets are named `turbo-go-<version>-<goos>-<goarch>`, with `.exe` on Windows — with five downloads the platform has to be in the name. Cross-compiles run with `CGO_ENABLED=0`, which is safe because tcell and toml are both pure Go, and `-trimpath`, which keeps the build machine's paths out of a binary that goes to strangers. The host build still runs first, as the fastest way to find a compile error and the only binary this machine can run to check the version against `TAG` — which is what `release.env`'s own comment always said the script should do. `SHA256SUMS` covers every platform in one file, since that is what `sha256sum -c` reads.
143+
144+- **Verified by running it**: five binaries staged; `go version -m` reports the right `GOOS`/`GOARCH` for each and the magic bytes are Mach-O, ELF and PE as they should be; `sha256sum -c` passes on all five; the host binary runs; no `/home/agent` path survives `-trimpath`; `TAG=v9.9.9` is refused against a binary reporting `0.1.0`; and adding a sixth platform propagated to the build, the README table and the checksums before being reverted.
145+
146+- **Reported, not changed** (out of the scope asked for, twice): `04-release.upload-binaries.sh` globs `*.vsix`, left over from the VS Code extension template these scripts came from. It uploads `SHA256SUMS` and none of the five binaries the checksums are *for*. Replacing `"${RELEASES_DIR}"/*.vsix` with `"${RELEASES_DIR}"/turbo-go-*` would fix it. The stale `.vsix` and `package.json` comments in `04` and `release.env` are from the same template.
147+
148+## 2026-08-31 (later) — the upload script, adapted to a Go release
149+
150+- **Goal**: the user asked that `04-release.upload-binaries.sh` be fixed and adapted, after two rounds of flagging that it uploaded nothing.
151+
152+- **Root cause**: the loop globbed `"${RELEASES_DIR}"/*.vsix`, left over from the VS Code extension template these scripts came from. It attached `SHA256SUMS` and none of the five binaries the checksums were *for*.
153+
154+- **A supposition that was wrong, and checked before acting**: the upload used `--data-binary` with `application/octet-stream`, and Gitea's documented parameter for this endpoint is a `multipart/form-data` file field. Reading Codeberg's own swagger showed the endpoint `consumes` **both**, so the existing mechanism was correct and was left alone. Nearly rewrote something that was not broken.
155+
156+- **Changes**: the glob now picks up `turbo-go-*` and then `SHA256SUMS`, in that order, so an interrupted run never leaves checksums on a release with nothing to check. `jq` reads the release id and the attached assets instead of `grep -o '"id":[0-9]*' | head -1`. HTTP statuses are told apart: 401/403 says the token was refused, 404 says to run `02` — previously a bad token was reported as a missing release, which sends you to fix the wrong thing. Assets already attached are listed up front and replaced only with permission, so a run that failed halfway can simply be repeated. And `--dry-run` resolves the release and prints exactly what would be sent, without sending it.
157+
158+- **A bug the dry run found in itself**: `read -p` returns non-zero at end of input, so under `set -e` the script died at the README prompt when stdin was not a terminal. Both prompts now go through a `confirm` helper that answers no by itself when there is no terminal — a publishing script must not take silence for consent, nor die on the end of its input.
159+
160+- **Verified against the live API**, read-only: the release id resolves (11905813), the six assets are listed in order, an unknown option is refused, missing artefacts are reported, a deliberately invalid token gives "Codeberg refused the token (HTTP 401)", and a tag with no release gives the 404 message. `release.env` and `turbo-go.token.env` were restored after each. **Not verified**: the POST and DELETE themselves, because running them would publish artefacts on the user's behalf. That is theirs to run.
161+
162+## 2026-08-31 — Terminal windows (ticket 0007)
163+
164+- **Goal**: "je voudrais avoir la possibilité de créer des fenêtres qui soient des terminaux (pour lancer des commandes shell)", then "vas au bout du bout, puis crée le ticket pour la version pour Windows". Options chosen up front: a **real terminal (pty + VT emulator)** rather than captured command output; **several terminals**, each starting in the active file's directory; **Linux and macOS first**, with Windows showing a clear "not supported yet" message.
165+- **Changes**: new `internal/terminal` package (16 files) — `Session` over `/dev/ptmx` with build-tagged `pty_linux.go` / `pty_darwin.go` / `pty_other.go`, a `Parser` state machine over CSI/OSC/ESC, a `Screen` with scrollback and an alternate-screen aside, `Encode` for keys, and a `View` widget. Wired into `app` through a new `terminals.go`, plus edits to `app.go` (the `terminals` map, `keyLayers()`, title refresh, `F8`), `menus.go` (`Window ▸ New terminal`) and `actions_file.go` (`editorViewOf`, `closeWindow`, `Quit`). Two new theme keys, `terminal.text` and `terminal.cursor`, in `keys.go` and all three theme files.
166+- **Decisions**: a **real pseudo-terminal**, because a captured pipe loses colour, paging, `isatty` and `Ctrl-C`, and nothing interactive works at all — that is most of what a terminal is for. The **emulator is hand-written and partial** rather than a third dependency: what a shell, `go test`, `git`, `less`, `htop` and `vim` need is a bounded list, about six hundred lines, and a general-purpose library brings character sets, mouse protocols and sixel that would all need keeping alive. The **key routing was inverted for a focused terminal** — it outranks the editor's global shortcuts, keeping only the function keys, `Alt-X` and `Alt-0``Alt-9` — because a shell and an editor both want `Ctrl-C`, `Ctrl-W` and `Ctrl-F`, and without the reserved handful there is no way out of a full-screen program. Rejected: reserving fewer keys (no escape from `vim`), reserving more (readline becomes unusable), and asking for confirmation when closing a terminal (it holds a process, not unsaved work).
167+- **Tests**: 12 new tests in `internal/app/terminals_test.go` plus the package's own suites; `internal/terminal` at 95.7 %, `internal/app` up from ~71 % to 82.8 %. Run with `make test`, or `go test ./... -race`. Two test defects were found and fixed rather than accepted: a test that matched the pty's own echo of the command line instead of the shell's output, and a flaky drawing test racing the shell's startup — which, once made deterministic, turned out to have been sampling the cursor cell rather than the text.
168+- **Quality**: PASS. Four `return-statements` smells appeared (`handleKey`, `applySGR`, `applyAttribute`, `extendedColor`) and were refactored away by turning three switch-tables into actual tables and the routing chain into a list of layers. 0 errors, 0 warnings, 0 smells, complexity 1023.
169+- **Docs**: three new pages per language — `how-to/use-a-terminal.md`, `reference/terminal.md`, `explanation/terminal-windows.md` — and updates to both `README.md` indexes, `reference/keyboard.md`, `reference/menus.md`, `reference/themes.md`, `how-to/write-a-theme.md` and `explanation/architecture.md`. New `internal/terminal/README.md`; updates to `internal/app/README.md` (whose file table was also stale) and `internal/theme/README.md`. `docs/diagrams/packages.drawio` gained the `terminal` and `golang.org/x/sys/unix` nodes and five edges, and was then checked against `go list` programmatically — it matches edge for edge.
170+- **Also**: created ticket `0015` for the Windows/ConPTY port. Ticket `0007` was left `open` — closing it is the user's call. Nothing was committed.
171+
172+## 2026-08-31 — Project settings, autosave and TOML colouring (ticket 0002)
173+
174+- **Goal**: "enregistrer les paramètres du projet dans un dossier `.turbo-go` dans un fichier `settings.toml`" — the theme, a statement that files are saved automatically, the autosave implementation itself, TOML syntax colouring, loading the file if it exists, and a menu entry that creates a pre-initialised one. Then "va au bout du bout". Options chosen up front: autosave **after a pause in typing**; the theme written back **only when the file already exists**; the file looked for in **the working directory only**, with `-theme` winning over it; and **nothing beyond theme and autosave** in the file for now.
175+- **Changes**: new `internal/settings` package (`settings.go`, `create.go`, `rewrite.go`, `README.md`, tests). `internal/syntax` gained a `Language` dimension — `Highlight(lang, src)`, `LanguageOf(path)`, `NewCache(Language)`, `SetLanguage` replacing `SupportsPath`/`SetEnabled` — plus `toml.go`, a hand-written TOML scanner. `internal/app` gained `autosave.go` and `project.go`, an `autosave` and `settingsPath` field, an injectable clock, `saveDueDocuments` in the `Run` loop, `UseSettings`, and two Options menu items. `main.go` reads the file and resolves the theme. `internal/editor` updated for the new `syntax` API.
176+- **Decisions**: the settings directory is **not searched for upwards** — a module has a real boundary, "the project" does not, and a walk makes a file three directories up change your colours silently. `.turbo-go/` is **created only from the menu**, never as a side effect of picking a theme, because that would put a directory into someone's repository for trying a colour; this is also what makes the write-back rule one sentence. `SetTheme` **rewrites one key in place** rather than re-encoding, since the file exists to be hand-edited and is mostly comments — losing them on a first theme change would be a silent deletion of someone's writing. Autosave waits for a **pause in typing**: a fixed interval writes mid-edit, and on-focus-change leaves disk an hour behind screen. Rejected along the way: per-window autosave deadlines (unobservable gain, more state), and new `syntax.toml*` theme keys (every existing user theme would have stopped colouring TOML). `ui.Menu` has no nested submenus, so the two entries are flat under Options rather than the submenu asked for — said so rather than building nesting nobody requested.
177+- **Tests**: 20 in `internal/settings`, 24 for the TOML scanner, 18 for autosave (with an injected clock, so nothing waits), 10 for the project-settings menu items, 7 in `main`. Run with `make test`, or `go test ./... -race`. Also verified end to end against the real binary in a pty: the theme coming from `settings.toml`, `-theme` overriding it, and autosave writing a file **from the idle timer alone** with the process killed before any quit path could run — plus a control run with no settings file, which left the file untouched.
178+- **Two defects found in my own new code**: the TOML scanner patched `spans[len-1].Start` after an emit that had dropped an empty span, corrupting the *previous* span (a test caught it); and the first colouring test asserted through a live shell, which is the class of trap already recorded from the terminal session.
179+- **Quality**: PASS. Four smells appeared (`writeFile` and `tomlWordClass` many-returns, two complex binary expressions) and were refactored away by splitting functions and naming the character sets. 0 errors, 0 warnings, 0 smells, complexity 1150.
180+- **Docs**: three new pages per language — `how-to/configure-a-project.md`, `reference/project-settings.md`, `explanation/project-settings.md` — and updates to both indexes, `reference/cli.md`, `reference/menus.md`, `explanation/architecture.md` and `explanation/colouring-and-completion.md`. New `internal/settings/README.md`; `internal/syntax/README.md` and `internal/app/README.md` brought back in sync. `docs/diagrams/packages.drawio` gained `settings` and three edges, and was re-checked against `go list` — it matches edge for edge.
181+- **Context**: the terminal work from earlier the same day was merged to `main` by the user as PR #1 during this session; this work sits on `feature/project-settings`, uncommitted.
182+
183+## 2026-08-31 — Window frame boxes: [x] to close, [■] to maximise
184+
185+- **Goal**: "ce bouton `[■]` pour le moment sert a fermer la fenetre, il faudrait le changer par `[x]` et ajouter un bouton sur la droite `[■]` pour maximiser la fenetre — il faudra mettre le readme a jour, ainsi que la doc". Then "va au bout du bout". One option chosen up front: the maximise box **toggles**, and its **symbol changes with the state** (`[■]``[▬]`).
186+- **Changes**: `internal/ui/window.go``closeBoxLabel` is now `[x]`; new `maximizeBoxLabel`/`restoreBoxLabel`/`boxWidth`/`maximizeOffset`/`numberOffset`; `OnMaximize func()`; `Maximized()`, `Maximize(area)`, `Restore()`; `followDesktop` and `place` split out of the resize path; the window number moved from `W-4` to `W-6` and the title's reserved margin from 10 to 11 columns. `internal/ui/desktop.go``Maximize` became **`ToggleMaximize`**, `Add` wires `OnMaximize`, `Tile`/`Cascade` now use `place`. `internal/app/actions_view.go``MaximizeWindow` toggles.
187+- **Decisions**: the box **shows its action, not the window's state** — a fixed symbol is ambiguous exactly when it matters, since you can see the window is large but not what pressing the box would do. `Desktop.Add` wires `OnMaximize` rather than `app`, because the desktop is the only thing that knows the area to fill; a window not on a desktop draws **no box** rather than a dead one. `Desktop.Maximize` was **renamed** rather than left in place: keeping the name for a toggle would be a lie, and there was one caller. **Window ▸ Maximise toggles too** — a menu and a button disagreeing about "maximise" is a bug people report. Rejected: a one-way maximise (the second press does nothing visible), and a fixed `[■]` in both states.
188+- **Two holes found and closed on the way**: a maximised window kept a **stale restore rectangle** across a terminal resize, so shrinking the terminal then restoring would put the window partly off screen (`followDesktop` now carries it); and `Tile`/`Cascade` left a window calling itself maximised, so its box offered to restore to a rectangle that no longer meant anything (`place` clears it).
189+- **Tests**: 14 new in `internal/ui/maximize_test.go`, including a property test over widths 16…60 × four title lengths. `internal/ui` at 93.0 %. Run with `make test`, or `go test ./... -race`.
190+- **A test that was worthless until it was fixed**: the margin test asserted only that the close box, number and maximise box were intact after drawing — which is true whatever the margin, because `drawNumber` runs *after* `drawTitleBar` and simply repaints over the title. Verified by putting the wrong margin back: the test passed. It now asserts on the cell **beside** each piece of furniture, and with the wrong margin it fails on `"…" sits against the number`.
191+- **Verified end to end** by rendering the real binary through the project's own VT emulator (`internal/terminal`): the frame reads `╔═[x]═ main.go ═1═[■]╗`, Window ▸ Maximise fills the terminal and flips the box to `[▬]`, and a second use restores the previous size and symbol.
192+- **Quality**: PASS first time. 0 errors, 0 warnings, 0 smells, complexity 1156.
193+- **Docs**: `internal/ui/README.md` (API table, Turbo Vision details, the new methods) and the root `README.md`'s ASCII screenshot. In both languages: `reference/keyboard.md` (three mouse rows), `reference/menus.md` (Maximise is a toggle), `how-to/use-a-terminal.md` (which told people to click `[■]` to close), and a new section in `explanation/design-decisions.md`. No package was added or rewired, so `docs/diagrams/packages.drawio` is unchanged — re-checked against `go list` and still matching.
194+- **Context**: project settings were merged to `main` as PR #2 before this session; this work sits on `feature/windows-buttons`, uncommitted.
195+
196+## 2026-08-31 — Bug fix: OK did nothing in the Open dialog
197+
198+- **Goal**: user report — "quand on ouvre un fichier, dans la popup le bouton OK ne semble pas fonctionner (click souris ou focus et entree)".
199+- **Diagnosis**: `FileDialog` never wired `ListBox.OnSelect`. Highlighting a file therefore never reached the **Name** field, `Path()` returned `""`, and `confirm()` — which only closed when the path was non-empty — did nothing at all. Both routes the user tried went through `confirm()`, which is why both appeared dead. `OnSelect` itself was fine: implemented, fired by `setSelected`, and covered by its own test in `internal/ui`. It simply had no caller.
200+- **Changes**: `internal/app/dialogs.go``f.list.OnSelect = f.showSelection`; new `showSelection`, which writes the highlighted entry's name into the field and clears it for the parent entry; `confirm()` falls back to `choose()` when the field is empty, so OK is never dead; `parentEntry` extracted, replacing two copies of `".." + string(filepath.Separator)`.
201+- **Decisions**: the field **mirrors the highlight** rather than OK reading the list behind the user's back — a fallback nobody can see would let Save As write to a filename that was never shown. `../` clears the field instead of putting `..` in a box labelled "Name:", and OK then falls back to the highlight, which browses up. Construction is safe without a special case: `SetItems` calls `setSelected(0)`, which does not fire when the selection is already 0, so Save As keeps the name it opened with.
202+- **Tests**: 11 new in `internal/app/dialogs_test.go`, three of them end-to-end through the app — clicking **OK** with the mouse, `Tab` then `Enter`, and `Alt-O`. All eight behavioural ones were confirmed to fail against the original code before the fix went in. `internal/app` 84.3 % → 84.7 %.
203+- **A wrong diagnosis I had to correct mid-session**: the mouse-click test kept failing after the fix and I reported a second bug. It was my own test — `strings.Index` on a drawn row returns a **byte** offset, and a row full of `░` and `║` at three bytes each put the click about thirty columns right of the button. The helper now counts runes. There was only ever one bug.
204+- **Verified end to end** by driving the real binary through a pty and rendering it with the project's own VT emulator: `F3`, `↓`, `↓` fills the Name field with the highlighted entry, and `Tab` `Enter` opens it in a second window.
205+- **Quality**: PASS. 0 errors, 0 warnings, 0 smells, complexity 1160.
206+- **Docs**: a new "The Open and Save As box" subsection in `reference/keyboard.md` in both languages, and a paragraph in `internal/app/README.md`. No package added or rewired; the drawio diagram is unchanged and re-verified against `go list` (27 edges each side).
207+
208+## 2026-08-31 — Project tree window (ticket 0003)
209+
210+- **Goal**: "une fenêtre qui affiche un treeview du projet en cours avec possibilté de sélectionner un fichier et l'ouvrir", then "va au bout du bout". Options chosen up front: rooted at the **working directory** (the `.turbo-go` rule, not the `go.mod` walk); **hide `.git` only**; an **ordinary window**, not a docked panel; refreshed by a **key and after a save**, with no filesystem watching.
211+- **Changes**: new `internal/filetree` package — `tree.go` (the model: `Node`, `Tree`, lazy expansion, sorted listing, `Refresh`), `view.go` / `view_draw.go` / `view_events.go` (the `ui.Widget`), `README.md`, tests. Four `tree.*` keys in `internal/theme/keys.go`, all three shipped themes and the completeness test. New `internal/app/tree.go`, plus `treeWindow`/`treeView` fields, the `F9` shortcut, `Window ▸ Project tree`, a branch in `closeWindow`, and `refreshTree()` on both save paths.
212+- **Decisions**: a **window, not a panel** — a docked strip would mean `Desktop` growing reserved edges that `fitInto`, the grow modes, maximise, tile and cascade all have to respect, which is a change to the foundation of the interface for one widget; as a window it got F6, Alt-digits, `[x]`, `[■]` and Tile for free and `ui` did not change at all. **One tree at a time**, because the root is fixed at start-up and a second view would have nothing to distinguish it. **`.git` hidden and nothing else** — copying the Open dialog's hide-every-dot-entry rule would have made `.turbo-go/settings.toml` unreachable from the editor's own file browser. **No filesystem watching**: `fsnotify` would be a third dependency for a feature whose failure mode is a stale line, so the editor refreshes at the moments it can be sure of. Rejected: rooting at the `go.mod` (unpredictable in a monorepo, and the root would depend on a file three directories away), and respecting `.gitignore` (wants a pattern engine that is a feature in itself).
213+- **A theme decision forced by measurement**: the tree was going to borrow `list.*` and needed keys of its own instead. `list.selected` is coloured against a *dialog* — turbo-classic makes it white on navy while `window.body` is navy — so a tree in a window would have highlighted its selected row in the colour underneath it. Checked before writing the keys, not guessed; a test now holds every shipped theme to 64 channel values between `tree.text` and `tree.selected`, and it was confirmed to fail when `tree.selected` is set back to navy.
214+- **Tests**: 41 in `internal/filetree` (94.7 %) and 10 in `internal/app`; `internal/app` 84.7 % → 84.6 % on a larger base. Run with `make test`, or `go test ./... -race`.
215+- **Verified end to end** by rendering the real binary through the project's own VT emulator: `F9` lists the project with `.git` hidden and `.turbo-go`/`.gitignore` shown, directories first; three `→` presses nest two levels with the right markers; `Enter` opens `internal/app/app.go` into a third window with its content.
216+- **Quality**: PASS first time. 0 errors, 0 warnings, 0 smells, complexity 1228.
217+- **Docs**: three new pages per language — `how-to/browse-a-project.md`, `reference/project-tree.md`, `explanation/project-tree.md` — and updates to both indexes, `reference/keyboard.md`, `reference/menus.md`, `reference/themes.md`, `how-to/write-a-theme.md` and `explanation/architecture.md`. New `internal/filetree/README.md`; `internal/app/README.md` and `internal/theme/README.md` brought back in sync. `docs/diagrams/packages.drawio` gained `filetree` and four edges, re-checked against `go list` — 31 edges each side.
218+
219+## 2026-08-31 — Markdown, JavaScript, HTML and shell colouring (tickets 0009, 0013, 0014)
220+
221+- **Goal**: "ajoute le support des syntaxes markdown, javascript, html et bash", then "va au bout du bout". Options chosen up front: **five new classes** (heading, tag, attribute, emphasis, link) rather than reusing the twelve; **readable depth** rather than the hard tail; a Markdown fence **not** coloured in its announced language; recognition by **extension plus shebang**.
222+- **Changes**: five `Class` values and five `KeySyntax*` keys, set in all three shipped themes and in the completeness test. New `internal/syntax/scanner.go` — a shared `lineScanner` plus `scanLines`, `takeQuoted`, the block-comment helpers and the rune predicates — and the **TOML scanner ported onto it**. New `markdown.go`, `markdown_inline.go`, `javascript.go`, `html.go`, `bash.go`. `LanguageOf(path)` became `LanguageOf(path, firstLine)` with shebang detection; the two call sites in `internal/editor` pass `buf.Line(0)`.
223+- **Decisions**: **no general engine** — no pattern language, no grammar format; each scanner is ordinary Go sharing only a line, a position and the spans so far, so adding a language means writing one rather than learning a notation. **A scanner guesses nothing**: JavaScript regex literals, shell heredocs, JavaScript inside `<script>` and the language of a Markdown fence are all absent, each because recognising it needs more than one line holds and a wrong guess is louder than no guess. **Five new classes** because a heading is not a keyword and a tag is not one either; the cost — third-party themes falling back to `default` — was accepted and is documented. The shared scanner **touched working TOML code**, which was flagged before starting; its 24 tests were the net and stayed green throughout.
224+- **Two defects in my own new code**, both caught by tests: `finishTemplate` coloured nothing because a helper had already run the position to the end of the line and `emit` drops empty spans — the same trap the TOML scanner produced once before, in a different shape; and the shell scanner split `-eu` into an operator and a word, so every option in every script was arithmetic.
225+- **One defect no test could catch**: `syntax.link` was lime in `turbo-classic`, the exact colour of `syntax.string`, so a Markdown link and an inline `code` span were indistinguishable. Found by rendering the real editor through the project's own VT emulator and reading back the foreground colour of every run.
226+- **A weakness found in an existing test**: the theme completeness test only constrains `turbo-classic`, because the other two inherit from it and inheritance is resolved at parse time. Deleting a key from a child passes; from the base, it fails. Recorded rather than changed.
227+- **Tests**: 180 in `internal/syntax` (96.0 %), covering each language's constructs, its multi-line carries, its stated omissions, and that every span stays inside its line. Run with `make test`, or `go test ./... -race`.
228+- **Verified end to end** by rendering the real binary through the project's own VT emulator on one file per language, reading back the foreground of each run: Markdown headings, emphasis, code and links; JavaScript keywords, template literals, builtins and hex numbers; HTML tags, attributes, entities and comments; shell builtins, options, and expansions inside double-quoted strings.
229+- **Quality**: PASS after one round. Four smells appeared (`Highlight` and `Language.String` many-returns, `markdown.go` file complexity, an `html.go` binary expression) and were refactored away with two dispatch tables, a file split and a named character set. 0 errors, 0 warnings, 0 smells, complexity 1389.
230+- **Docs**: a new `reference/languages.md` in both languages, giving each scanner's exact boundary; rewritten "the other five languages" and a new "five classes Go has nothing to say about" section in `explanation/colouring-and-completion.md`; updates to both indexes, `reference/themes.md` and `how-to/write-a-theme.md`. `internal/syntax/README.md` rewritten for the new surface. No package added, so `docs/diagrams/packages.drawio` is unchanged and still matches `go list` (31 edges each side).
231+
232+## 2026-08-31 — Snippets, and one level of submenus in the menu bar (ticket 0006)
233+
234+- **Goal**: "un système de snippets qui seraient dans un fichier toml dans ./turbo-go, on aura un menu principal Snippets dont les sous menus seront construits à partir du contenu de snippets.toml — le snippet sélectionné est copié au niveau du fichier ouvert à l'endroit du curseur", then mid-work: "tu genereras un fichier de snippets par defaut si ils n'existent pas a partir du menu", then "va au bout du bout". Options chosen up front: `.turbo-go/snippets.toml` **plus a user-level file**; **real nested submenus**, not flat items; a **flat `[[snippet]]` list** with a `group` field and a `languages` filter; insertion **re-indented** to the cursor's column.
235+- **Changes**: `ui.MenuItem.Items` and `ui.Menu.OnOpen`, with the submenu's state, geometry, drawing, keyboard and mouse; `menu.go` split into `menu.go` / `menu_draw.go` / `menu_events.go` / `submenu.go`. New `internal/snippets` package (`snippets.go`, `create.go`, `README.md`, tests). `editor.InsertSnippet` plus `indentContinuationLines` and `leadingWhitespace`. New `internal/app/snippets.go` and the `Snippets` menu on the bar.
236+- **Decisions**: **one level of nesting**, because the format is groups → snippets and a general depth would mean replacing the bar's two indices with a path in the widget every dialog depends on. **`Menu.OnOpen`** rather than rebuilding the bar each loop turn: the contents depend on a file that changes and on the front window, so there is no start-up moment at which they exist, and OnOpen runs at exactly the moment they are about to be seen. **Two files, project wins on a clash**, mirroring how `-theme` beats a project setting which beats the built-in default. **Re-indented insertion**, because an `if err != nil` is inserted inside something by definition and a feature whose output needs fixing every time saves nobody anything. **An unreadable file is a greyed line in the menu**, not silence, because silence looks exactly like having no snippets and sends you to create a file you already have. Rejected: flat items with greyed group captions (thirty snippets give a menu taller than the terminal), and placeholders/tab stops (a second feature with its own state).
237+- **Two defects found by driving the real binary**, neither of which any test caught: **`Alt-S` opened Search, not Snippets** — both labels claimed S and the bar answers the first match, so the new menu was unreachable from the keyboard; the label is now `S~n~ippets` and `TestNoTwoMenusShareAHotKey` holds the line. And a submenu **wider than the terminal** could not be made to fit by flipping it left, so the width is capped and long labels are clipped.
238+- **Tests**: 19 for submenus in `internal/ui` (93.3 %), 19 in `internal/snippets` (83.8 %), 12 for insertion in `internal/editor` (96.2 %), 16 in `internal/app` (84.8 %). Run with `make test`, or `go test ./... -race`.
239+- **Verified end to end** by rendering the real binary through the project's own VT emulator: `Alt-N` opens the menu, `Enter` on **Create snippets file** writes and opens `.turbo-go/snippets.toml`, `Alt-N` then `→` opens the **Go** submenu (which flipped to the *left* for want of room), and `Enter` on **table test** inserted a four-line snippet correctly indented on the tab of the line it landed on.
240+- **Quality**: PASS after two rounds. One smell — `menu.go` file complexity, 46 before this work and 69 after — was fixed by splitting the file the way `terminal` and `filetree` are already split, first pulling out `submenu.go` (69 → 56, still over) and then `menu_draw.go` and `menu_events.go`. 0 errors, 0 warnings, 0 smells, complexity 1480.
241+- **Docs**: three new pages per language — `how-to/use-snippets.md`, `reference/snippets.md`, `explanation/snippets.md` — plus a Snippets section in `reference/menus.md`, submenu keys in `reference/keyboard.md`, and updates to both indexes and `explanation/architecture.md`. New `internal/snippets/README.md`; `internal/ui/README.md` and `internal/editor/README.md` brought back in sync. `docs/diagrams/packages.drawio` gained `snippets` and three edges — including `app → syntax`, which the programmatic check against `go list` caught and I had missed.
242+
243+## 2026-08-31 — The Go menu: format, lint, build, test, run (ticket 0017, partly)
244+
245+- **Goal**: "ajouter les commandes go qui permettent de lancer un formatage, le lint, le build, le lancement de tests, le run", then "va au bout du bout". Ticket 0017 asked for more than the message — a regenerable TOML file of commands — and the user chose that: a `tools.toml` **initialised with the five**. Other options chosen up front: output **in a terminal window**; the commands `gofmt -l -w .`, `go vet ./...`, `go build ./...`, `go test ./...`, `go run .` over the whole module; a **new `Go` menu** on `Alt-G`.
246+- **Changes**: `terminal.Options.Args` to run one command rather than a shell, and `terminal.ViewOptions` carrying `Name`/`OnChange`/`OnExit`. New `internal/tools` (`tools.go`, `create.go`, `README.md`, tests) and `internal/projectfile` (`projectfile.go`, `README.md`, tests). `Buffer.Reload` plus `ErrModified`. New `internal/app/gotools.go` with the `Go` menu, `RunTool`, and `reloadAfterTools`; `createProjectFile` extracted in `project.go`. `settings`, `snippets` and `tools` all now write through `projectfile`.
247+- **Decisions**: the five commands are **the starter file's contents, not code**`go vet` is the default only because it ships with the toolchain, and a project with a `Makefile` wants `make check`; changing one is editing a file. **No user-level tools file** (unlike snippets): a global one would offer `go build` in a Rust repository. **A terminal window, not a captured pane**, because a pipe costs `go test`'s colours, `go build`'s paging, `go run`'s keyboard and `Ctrl-C`. **Files a command rewrote are re-read, unless modified**: `Format` rewrites the file in front, and without this the next `F2` writes the unformatted version back over gofmt's work — but a modified buffer is left alone and named, because the edit and the formatter genuinely disagree and the editor is not in a position to decide. Rejected: hardwiring five items (wrong within a week), and splitting an argv instead of `sh -c` (would mean inventing quoting rules for a hand-written string).
248+- **A data race that predated this work and that this work exposed.** `terminal.NewView` starts the reading goroutine, and both callers then assigned `OnChange`/`OnExit`. `-race` never caught it across the whole terminal and snippets features, because a shell takes longer to produce its first output than an assignment takes to run; `sh -c "echo x"` closed that window and the detector fired immediately. Fixed structurally with `ViewOptions` rather than with a mutex, so the race is impossible rather than guarded.
249+- **A second defect found by driving the binary**: a finished command's window could not be closed with `Ctrl-W`. The view consumed every key and wrote it to the dead shell, where the write failed silently and the key was consumed anyway — the mouse was the only way out. A finished view now takes only the scrolling keys.
250+- **Tests**: 4 for `Args`/`Name` and 3 for `Exited` in `internal/terminal` (95.9 %), 12 in `internal/tools` (93.1 %), 7 in `internal/projectfile` (75.0 %), 8 for `Buffer.Reload` (95.8 %), 10 in `internal/app` (85.0 %). Run with `make test`, or `go test ./... -race`.
251+- **Quality**: PASS after one round. Four smells, all real duplication: `CreateTools`/`CreateSnippets` were the same dance (extracted as `createProjectFile`, which `CreateProjectSettings` now uses too), and the atomic TOML write existed in **three** copies (extracted as `internal/projectfile`; `internal/buffer`'s was left alone because it preserves an existing file's mode, which is a different operation). Complexity went **down**, 1528 → 1513. 0 errors, 0 warnings, 0 smells.
252+- **Verified end to end** by rendering the real binary through the project's own VT emulator: `Alt-G` on a project with no tools file offers only `Create tools file`; `Enter` writes and opens it; `Alt-G` then shows the five; `T` runs `go test ./...` in a window titled with the command; and `F` on a deliberately misformatted file ran `gofmt -l -w .`, after which `Ctrl-W` closed the command window and the editor showed the **reformatted** file — the reload working.
253+- **Docs**: three new pages per language — `how-to/run-go-commands.md`, `reference/go-tools.md`, `explanation/go-tools.md` — plus a Go section in `reference/menus.md`, `Alt-G` in `reference/keyboard.md`, a "after the program has gone" section in `reference/terminal.md`, and updates to both indexes and `explanation/architecture.md`. New `internal/tools/README.md` and `internal/projectfile/README.md`; `internal/terminal`, `internal/buffer` and `internal/app` READMEs brought back in sync. `docs/diagrams/packages.drawio` gained `tools` and `projectfile` with five edges, re-checked against `go list` (39 edges each side).
254+
255+## 2026-08-31 — Go tools: configurable output, and a popup by default
256+
257+- **Goal**: "finalement je préfère pour les tools go que la sortie ne soit pas dans un terminal mais dans une popup / pour les autres tools il faudra prévoir de définir le type de sortie: popup, terminal, editeur". A revision of the uncommitted work from earlier the same session, not a layer on it. Options chosen up front: the popup **opens immediately and fills in** (modal, Escape stops the command); the **exit code always in the title** with `(no output)` for a silent success; `editor` means **an ordinary editable window**; and `Run` stays `terminal` in the starter file while the other four are `popup`.
258+- **Changes**: `tools.Output` with `OutputPopup`/`OutputTerminal`/`OutputEditor`, `Tool.Where()`, validation that refuses an unknown value, and the starter file naming `output` on all five. New `internal/tools/run.go``Start`, `Run`, `Lines`, `Done`, `Dropped`, `Stop` — plus build-tagged `group_unix.go`/`group_other.go`. `NewOutputDialog` in `internal/app/dialogs.go`. `internal/app/gotools.go` reworked into `runInTerminal` / `runCaptured`, with `toolRun`, `refreshRunningTool`, `finishRun` and `openOutputInEditor`. `App.tick` extracted from the `Run` loop.
259+- **Decisions**: **three destinations because none is right for everything** — a terminal for interactive or long commands, a popup for run-read-dismiss, an editor window for output to work through. The popup opens **immediately** because one appearing three seconds later swallows whatever was being typed then; it is modal, which is a real cost on a slow build, named in the docs, and answered by `output = "terminal"` on that tool. An **unknown `output` is refused, not corrected**: `"termnial"` falling back silently would look as though it worked. The **exit code is always in the title** because `go build ./...` succeeding is silent and a blank dialog cannot be told from one that never started.
260+- **A defect found by a flaky test, then reproduced deliberately**: `Stop()` killed only the shell, and a grandchild inheriting the output pipe left the reading goroutine blocked until it ended — 20 seconds in the suite, and for `go test ./...` it would be every test binary spawned. Fixed by killing the **process group**, with `cmd.WaitDelay` as the backstop. Verified: 11 ms after the fix, never before it.
261+- **Two vacuous tests caught and fixed**, both the same shape — the test driving the thing under test. `waitForLoopTurn` called `a.reloadAfterTools()` directly, so the test passed with that step deleted from the event loop; `App.tick` was extracted and the helper now takes a whole loop turn. And the process-group test killed the shell before it had forked; it now waits for the child to print. Both were confirmed by breaking the code they cover.
262+- **Tests**: 26 in `internal/tools` (95.0 %), 11 more in `internal/app` (85.7 %). Run with `make test`, or `go test ./... -race`.
263+- **Verified end to end** through the project's own VT emulator: `go build ./... — ok` with `(no output)`; `go vet ./... — exit 1` showing `main.go:6:2: unreachable code`; `gofmt -l -w . — ok` listing the reformatted file.
264+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1543.
265+- **Docs**: the three `go-tools` pages written earlier in the session were **revised**, not appended to — they described a terminal as the only destination. Plus the Go line in `reference/menus.md` in both languages, and `internal/tools/README.md` and `internal/app/README.md` brought back in sync. No package added, so the diagram is unchanged and still matches `go list` (39 edges each side).
266+
267+## 2026-08-31 — Fix: a reinstall produced a binary that would not run on macOS
268+
269+- **Goal**: user report — `scripts/install.sh` printing `✗ the installed binary does not run` after a clean build, on macOS (`/Users/k33g/go/bin`).
270+- **Diagnosis**: the installer used `cp "$STAGING/$BINARY" "$TARGET"`, which opens the destination with `O_TRUNC` and writes in place — **the inode is reused**. macOS caches a binary's code signature against its inode, so new bytes in the old inode leave the cached signature describing something else and the kernel refuses to execute the result. It fails on *reinstall*, not on a first install, which matches a user who had been running the editor all day. Cross-compiling and `go vet` for `darwin/arm64` and `darwin/amd64` were both clean, ruling out the code.
271+- **Changes**: `scripts/install.sh` now copies to `.turbo-go.incoming.$$` **inside `$prefix`** and `mv -f`s it over the target, so the name gets a fresh inode and the install is atomic besides; the `EXIT` trap cleans the temporary. And the verification captures the binary's own stderr and prints it: `the installed binary does not run` on its own tells nobody anything they can act on.
272+- **Decisions**: the temporary must live in `$prefix` rather than in `$STAGING`, because a rename only works within one filesystem — the same reasoning `internal/buffer` and `internal/projectfile` already follow. Rejected: `install -m 0755` (does an in-place write on some platforms, so it would not fix it), and `rm` then `cp` (leaves a window with no binary on the PATH).
273+- **Tests**: 3 added to `install_test.go` — the reinstall gives the file a **new inode**, the reinstalled binary runs, and the installer surfaces the binary's own error. The first two were confirmed failing against the `cp` version before the fix. `install_test.go` is now 13 tests.
274+- **Verified end to end** on Linux: inode 529596 → 529598 across a reinstall of a binary that had been executed in between, no temporary left in the prefix, and the error path shown to carry the system's message.
275+- **Not verified on macOS**, and said so plainly to the user: this sandbox is Linux, so the diagnosis rests on the symptom matching a known failure mode rather than on a reproduction. The improved error message is what makes a wrong diagnosis recoverable.
276+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1543 — unchanged, the fix is in a shell script.
277+- **Docs**: a new "When something goes wrong" section in `how-to/install.md` in both languages, covering the three failures the installer can report, and the script's own header comment brought back in line with what it does.
278+
279+## 2026-08-31 — Terminal output that never appeared, and tools in menus of their own
280+
281+- **Goal**: two user reports in one message. `output = "terminal"` with `command = "echo 'TADA'"` showed only the title and no output; and "je voudrais pouvoir faire la différence entre les tools go et d'autres tools qui iraient dans un menu tools". Options chosen up front for the second: a **free-form `menu` key** on the tool (absent → Go, arbitrary names, menus in the order their first tool appears), in the **single** `tools.toml`, with the stated cost that hot keys for created menus must be assigned without clashing with the nine fixed ones.
282+- **Changes (the bug)**: `internal/terminal/screen_resize.go``rowsToDrop(previous, height, cursorRow)` replaces `max(len(previous)-height, 0)`.
283+- **Changes (the feature)**: `tools.Tool.Menu` with `MenuName()`, `List.In(menu)` and `List.MenuNames()`; `DefaultMenu`; the starter file documents the key. `ui.MenuBar.SetMenus`. New `internal/app/toolmenus.go``allMenus`, `toolMenus`, `toolMenu`, `takenHotKeys`, `hotKeyLabel`, `fileStamp`, `stampOf`, `toolsFileStamp`, `refreshToolMenus` — with `refreshToolMenus` added to `App.tick` and `App.toolsStamp` to the struct. `commandItems` took a menu name. `buildMenus` now delegates the order to `allMenus`.
284+- **The bug, diagnosed**: a terminal window is created at the default 80×24 and the first `layout()` resizes it to its frame (about 76×20). `Screen.Resize` dropped `previous-height` rows **from the top** into scrollback, so a single line of output at row 0 went with them while blank rows stayed below. Whether the output arrived before or after that resize decided whether it showed — which is exactly the intermittence reported. It now drops only as many rows as the cursor actually needs. The two tests were written first and confirmed failing; the pre-existing `TestShrinkingKeepsTheNewestLinesAndRemembersTheRest` still passes unchanged.
285+- **Decisions**: a **free-form name, not a fixed second `Tools` menu** — a Tools menu holding `docker compose up`, `psql` and a deploy script is as undifferentiated as a Go menu holding them, and rejecting the fixed menu also rejects the second file (`menus.toml`) that would have to agree with the first about which tools exist. **`Go` stays fixed** on the bar: it holds `Create tools file`, which has to be reachable in a project that has no tools file — the very project that needs it. **Hot keys are assigned, not read**: the file's author cannot know which letters are free, and a clash is silent (the bar answers the first match; the second menu draws normally and never opens) — the `Snippets`/`Search` bug from earlier in this project, made permanent. Tildes in a name are honoured **only when the letter is free**; refusing the file instead would break a working tools file the day a release adds a menu. **A `stat` per loop turn, not a parse**: `Menu.OnOpen` cannot cover a menu that does not exist yet, and parsing on every keystroke is work done for nothing.
286+- **Tests**: 7 in `internal/tools`, 2 in `internal/ui`, 16 in `internal/app` (new `toolmenus_test.go`), 2 in `internal/terminal`. Run with `make test`, or `go test ./... -race`.
287+- **Two test expectations were wrong, and the code was right** — worth recording because both are the mechanism working: `Format` gets `For~m~at`, not `F~o~rmat`, because `o` is Options'; and a menu written `T~o~ols` loses its `o` for the same reason, so the case was retested with `Doc~k~er`.
288+- **Verified end to end** by rendering the real binary through the project's own VT emulator: the bar reads `… Snippets Go Tools Docker Help` in file order; `Alt-T` drops down Echo and Date with no `Create tools file`; `Alt-T Enter` opens a terminal window titled `echo 'TADA'` **showing TADA**; `Alt-D Enter` gives the popup `echo docker — ok` showing `docker`; and `Alt-G` still holds Build and `Create tools file`.
289+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1543 → 1567.
290+- **Docs**: `reference/go-tools.md` gained the `menu` key, a Go-menu section and a "Menus a tool asks for" section with the hot-key rules; `how-to/run-go-commands.md` gained "Put a tool in a menu of its own" and two variants; `explanation/go-tools.md` gained three sections (why a tool names its menu, why the hot key is not the file's, why the bar is rebuilt from a stat); `reference/menus.md` gained "Project menus"; `reference/keyboard.md` a note — all in **both languages**. `internal/tools`, `internal/app` and `internal/ui` READMEs and the root README brought back in sync. No package added, so the diagram is unchanged and still matches `go list`.
291+- **Then, on request**: `demo/.turbo-go/tools.toml` brought up to date — **regenerated from the `template` constant** in `internal/tools/create.go` rather than hand-edited, so it cannot drift from the generator again, with the user's `~E~cho` tool appended under `menu = "Tools"`. Its header had still been describing a terminal as the only output destination, two revisions after that stopped being true.
292+- **Merged by the user** as PR #7 (`88a4c38`), `feature/go-format-lint` into `main`, who also closed tickets 0004 and 0017.
293+
294+## 2026-08-31 — The version in the About box comes from the build
295+
296+- **Goal**: "je voudrais que lorsque l'on fait une release, la version apparaisse dans la fenêtre about" — ticket 0010. The stated request was already half true: About *did* show a version. It showed `0.1.0` from `const Version` in `internal/app`, on a checkout fourteen commits past `v0.1.0`. Said so up front and treated the real goal as making the number true. Options chosen: **ldflags plus BuildInfo with a fallback**; a non-release build shows `git describe` output; About also carries the **commit** and the **build date** (not the Go version); **no `make release` target** — stamping only.
297+- **Changes**: new `internal/version``Info{Number, Commit, Built}`, `Current`, `String`, `BuiltAt`, and a `resolve` split out so every case is testable, plus `isPseudoVersion`. `app.Version` deleted; `Name` kept. `aboutText(info, themeName)` extracted as a pure function in `actions_view.go`. `main.go` prints `version.Current()`. `Makefile` gained `VERSION`/`COMMIT`/`BUILT`/`LDFLAGS`, a stamped `build`, and a `version` target. `scripts/install.sh` stamps the same way, with a path for a checkout git cannot describe.
298+- **Two discoveries that changed the design mid-way, both from running the thing rather than reasoning about it.** Go 1.26 does **not** report `(devel)` for a plain `go build .` in a checkout: it reports a **pseudo-version**, `0.1.1-0.20260831165958-88a4c3859bf3+dirty`. Unreadable in a dialog, and its `0.1.1` is a patch release that does not exist — so pseudo-versions are recognised and reported as `devel`. Then the first recogniser was wrong: the character before the timestamp is a **dot**, not a dash, whenever a base tag precedes the commit (`-0.` / `-pre.0.`). Caught by writing the three forms into a test and watching three of four fail.
299+- **Decisions**: `unknown` rather than a fallback constant, because a plausible-looking version nobody set is the exact defect being removed. `vcs.time` deliberately unused — it is the commit's timestamp, so "Built" would be false on every binary. About **omits a line whose fact is empty**; `go install …@v0.2.0` records a version and no VCS information at all. `resolve` takes its four inputs as arguments because a test binary cannot be built into having linker stamps.
300+- **Tests**: 17 in `internal/version` (98.3 %), 3 for `aboutText` in `internal/app` (86.3 %), 3 in `install_test.go` (now 16). The existing About test only counted modals; it was left in place and joined by tests that read the text. Run with `make test`, or `go test ./... -race`.
301+- **Verified end to end** through the project's own VT emulator, in both states: a build stamped `v0.2.0` shows `Turbo Go 0.2.0` with `Commit: 88a4c38` and `Built: 2026-08-31 18:04 UTC`; an unstamped build shows `Turbo Go devel-dirty` with the commit line and **no** `Built:` line and no gap where it would have been. `scripts/install.sh` reports `Turbo Go 0.1.0-14-g88a4c38-dirty (88a4c38, built …) → /tmp/tgbin/turbo-go`.
302+- **Quality**: PASS after one round. One real smell — a four-term boolean in `isPseudoVersion` — fixed by splitting out `hasPseudoTail` and `withoutBuildMetadata`, which reads better than what the linter complained about. 0 errors, 0 warnings, 0 smells, complexity 1567 → 1592.
303+- **Docs**: two new pages per language — `reference/versioning.md` and `how-to/make-a-release.md` — plus a section in `explanation/design-decisions.md`, the `-version` row in `reference/cli.md`, the About row in `reference/menus.md`, a note in `how-to/install.md`, and both indexes. New `internal/version/README.md`; `internal/app/README.md` and the root README brought back in sync. `docs/diagrams/packages.drawio` gained the `version` node with two edges, re-checked against `go list`: 41 drawn = 29 internal + 12 third-party, none missing, none stale.
304+
305+## 2026-08-31 — Fix: the release script, broken by removing the version constant
306+
307+- **Goal**: user report — `./03-build-releases.sh` failing with `❌ v0.2.0 does not match the binary, which reports 2026-08-31T18:59:13Z)`. Their own release tooling, broken by the version work merged as PR #8 earlier the same day.
308+- **Two defects, and the one they saw was the smaller.** The script read the version with `awk '{print $NF}'`, which took the last field of `Turbo Go 0.2.0 (7f8b36a, built 2026-08-31T18:59:13Z)` — a timestamp. But the cross-compile loop called `go build -trimpath` with **no `-ldflags` at all**, so all five downloadable binaries would have reported `devel` while the release page announced v0.2.0. Before the constant was removed it travelled into cross-builds; afterwards nothing did, and only the host binary was stamped — so nothing but a hand check would ever have caught it. Reproduced both before changing anything.
309+- **Changes**: `Makefile` gained an `ldflags` target that prints `$(LDFLAGS)`, so the stamp is defined once. `03-build-releases.sh` reads it, passes it to every cross-compile, and runs the staged binary for the host platform before declaring the release built. Its version check was replaced by three plain ones — `git describe --tags --exact-match` equals `TAG`, `git diff --quiet HEAD` is clean, and `grep -F` finds the version in the binary — none of which parses prose. The stale hint "Update Version in internal/app/app.go" named a constant that no longer exists and is gone.
310+- **Decisions**: the script asks **git** rather than the binary wherever it can, because `-version` is written for a person and has already changed shape once. `grep -F` rather than a field, for the one thing only the binary knows. A **dirty-tree check** was added, not asked for: `git describe --dirty` would otherwise stamp `v0.2.0-dirty` into binaries staged in a directory named `v0.2.0`, which is the same class of mismatch the script exists to prevent. Rejected: adding a machine-readable `-version-number` flag — new public surface, two languages of documentation, for a problem `grep -F` solves.
311+- **Tests**: new `release_test.go`, 5 tests — the cross-compile carries `-ldflags`, the flags come from the Makefile and are not respelt, both git checks are present, no field is read out of `-version`, and `make ldflags` really does stamp a binary that then reports what `git describe` says. The first failed before the fix.
312+- **Verified end to end** in a throwaway clone, so no tag was created in the user's repository: a full run stages five binaries and reports `✅ turbo-go-0.2.0-linux-arm64 reports 0.2.0`; and each guard was made to fire — untagged HEAD, HEAD tagged `v0.9.9` against `TAG=v0.2.0`, and a dirty tree — each with an actionable message.
313+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged; the fix is in a Makefile and a shell script.
314+- **Docs**: `reference/versioning.md` gained `make ldflags` and a note that `-version` is not a machine interface; `how-to/make-a-release.md` gained the numbered-scripts section and the three checks — both languages. `internal/version/README.md` and `.memory/summary.md` record why a cross-compile is the case that hides this.
315+
316+## 2026-08-31 — Fix: 01-release.tag.sh pushed a stale tag in silence
317+
318+- **Goal**: user report — `./03-build-releases.sh` refusing with `❌ HEAD carries no tag, so nothing built here can report v0.2.0`, then "fixe moi ca". The refusal was correct; the message was not, and the cause was in a different script.
319+- **Diagnosis**: `01-release.tag.sh` had **no `set -e`**. Their first run tagged `v0.2.0` at `7f8b36a`. The second run's `git add . && git commit` created `78ea819`, then `git tag -a v0.2.0` failed with "already exists" — ignored — and the `git push origin "${TAG}"` after it pushed the **old** tag. `git describe` then read `v0.2.0-1-g78ea819` and `03` refused. So the release builder was reporting a fault three steps upstream of itself.
320+- **Changes**: `01-release.tag.sh` gained `set -euo pipefail`; a `tagExists` check covering **both** the local ref and `git ls-remote origin` (a tag deleted locally after a failed attempt still exists on the remote, and a fresh one at another commit is then rejected); a guard so that having nothing to commit is not a failure, which `set -e` would otherwise have made one; and the tag now goes on **after** the push, so a rejected push leaves no stray tag. `03-build-releases.sh` gained a three-way diagnosis — HEAD tagged something else, the tag exists but HEAD has moved N commits past it, or no such tag — each naming the fix.
321+- **Decisions**: the remote is consulted for the tag as well as the local ref, because the state the user was actually in (local tag deleted, remote tag possibly still there) is invisible locally. No commit SHA is printed for a remote tag: `ls-remote` returns the *tag object* for an annotated tag, and printing it as the commit sends the reader after a SHA that does not exist. **`02-release.publish.sh` was left alone and reported instead** — it has the same missing `set -e`, but its `read -r -d '' DATA` idiom always exits non-zero, so adding one naively would kill the script at its first line; and it POSTs to Codeberg, which is not mine to change unasked.
322+- **Tests**: 5 more in `release_test.go` (now 10) — `01` stops on failure, checks both refs, survives an empty commit, tags only after pushing, and `03` can say how far HEAD is past the tag.
323+- **Verified end to end** in throwaway clones, one with a local bare remote, so no tag was created in the user's repository: `03` was made to print each of its three diagnoses, and `01` was run on the happy path (tagged HEAD, pushed), then again to see it refuse a tag now present only on origin.
324+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged; the fix is in two shell scripts.
325+- **Docs**: a "When the scripts refuse" table in `how-to/make-a-release.md`, both languages, one row per message with its cause and its fix.
326+
327+## 2026-08-31 — Simplify: a release build stamps the tag, and stops checking
328+
329+- **Goal**: user, after the release builder refused a third time — "fais quelque chose de plus simple, tu build comme avant avec le tag de release". A correction of my own two previous turns, not a new feature.
330+- **What was wrong with my design**: I had `03-build-releases.sh` stamp `make ldflags` (derived from `git describe`) and then *verify* that it agreed with `TAG` — HEAD tagged exactly, tree clean, binary reporting the version. Three gates, each defensible on its own, and together they blocked a release for conditions that were not actually errors. `git describe` answers "where is HEAD", which is a different question from "what release is this", so the gates existed only to reconcile an answer I should not have been asking for.
331+- **Changes**: `03-build-releases.sh` now stamps `TAG` directly — `make ldflags VERSION="${TAG}"` and `make build VERSION="${TAG}"` — and the three gates are gone. The Makefile needed nothing: a command-line `VERSION=` already overrides the `:=` default. The one check kept is the staged binary for this machine reporting the version, which proves the artefact rather than the intent.
332+- **Decisions**: the release **is** `${TAG}`, so the binaries say `${TAG}`; the whole class of "describe disagrees with TAG" stops existing rather than being detected. Building no longer requires the tag to exist — only `02-release.publish.sh` does, which is the step that genuinely needs it. `01-release.tag.sh`'s guards were **kept**: they are about not pushing the wrong tag, and they block no build.
333+- **Tests**: two removed with the behaviour they covered — deliberately, at the user's decision, not to make anything pass — and two added: the script stamps `VERSION="${TAG}"` for both the host build and the cross-compiles, and `make ldflags VERSION=v9.9.9` really does produce a binary reporting 9.9.9. `release_test.go` is 9 tests.
334+- **Verified end to end** in a throwaway clone with **no tag at all and a dirty tree** — the situation that had been refused — five binaries staged and `✅ turbo-go-0.2.0-linux-arm64 reports 0.2.0`.
335+- **Quality**: PASS. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged.
336+- **Docs**: the "When the scripts refuse" table removed along with the refusals; `how-to/make-a-release.md` and `reference/versioning.md` rewritten around the override, both languages.
337+- **Lesson worth keeping**: three turns were spent adding checks to reconcile two sources of truth, when the fix was to have one. The user saw it before I did.
338+
339+## 2026-08-31 — Three themes: cappuccino, cobalt, monochrome
340+
341+- **Goal**: `/methodical-dev` — "ajouter un theme cappucino, un theme cobalt, un theme monochrome" (ticket 0012). Options chosen up front: cappuccino **dark** (espresso, not cream); monochrome **pure grey**, no phosphor tint; cobalt **faithful to the recognised palette**; and yes to closing the silent-inheritance trap.
342+- **Changes**: three new files under `internal/theme/themes/``cappuccino.toml`, `cobalt.toml`, `monochrome.toml` — each stating all 67 style keys. No Go code changed: themes are embedded by `//go:embed themes/*.toml`, so adding one is adding a file. Three new tests.
343+- **A measurement changed the design of a test I had already promised.** The plan was "every colour legible on its own background". Probing the six themes first showed the weakest pairs are all *deliberately* faint furniture — scrollbar trough at 20, desktop, shadow, inactive frame, disabled entry, line-number gutter, between 20 and 70 in every theme including the two oldest. A blanket rule would have flagged six correct keys. Narrowed to the keys whose job is to be read, the measured floor is **80** (turbo-dark's `syntax.comment`), so the threshold is **64**: a quarter of the range, 16 below the present, a guard against regression rather than a description of today.
344+- **Decisions**: shipped themes state their palette in full, **user themes may still inherit** — the rule is about what the project is answerable for, not about how a theme should be written, and the how-to still recommends inheriting (now with the advice to inherit from a theme whose ground matches yours). Monochrome distinguishes syntax by weight and slant rather than hue, which is what makes it useful on a projector and to a reader who cannot separate the red from the green. Cobalt's accents were left as loud as the palette is known for rather than muted into house style: a theme called Cobalt that is not that blue is a different theme with a borrowed name.
345+- **Tests**: `TestEveryEmbeddedThemeSetsEveryKeyItself` in `internal/theme`; `TestEveryThemeKeepsItsTextReadable` and `TestEveryThemeTellsAdjacentSyntaxClassesApart` in `internal/editor`, beside the two cursor rules that were already there. **All three were falsified before being trusted** — a deleted `syntax.tag`, a `#2a2a2a` comment, and a cobalt link set to the string green, each producing the expected failure. The five existing theme tests now run over six themes.
346+- **Verified end to end** through the project's own VT emulator: each new theme renders the editor intact, and the real Theme dialog was opened to confirm the list is six entries alphabetically — which **broke the tutorial**, whose "press ↓ to move to turbo-dark" had become five presses. Fixed in both languages with the list written out.
347+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged, the themes are data.
348+- **Docs**: a "themes that ship" table in `reference/themes.md`, the embedded list, a new step 5 "check it stays readable" and the inherit-from-a-similar-ground advice in `how-to/write-a-theme.md`, the tutorial's theme step, `internal/theme/README.md`, and the root README — both languages throughout. The root README's docs index was also brought back in sync: it had been missing `make-a-release` and `versioning` since the previous session. No package added, so the diagram is unchanged and still parses.
349+
350+## 2026-09-01 — Migrated onto turbo-core, and a second editor exists
351+
352+- **Goal**: ticket 0001 in the `turbo-editors` parent — extract the code shared with a future Turbo Rust into a versioned library, and build that second editor. Options chosen by the user before implementation: the library holds `app` too, so an editor is a command, a profile and a scanner; the language scanner lives in its own editor; the editors depend on the library with `require` plus a committed `replace`; per-editor configuration directories rather than a shared `.turbo/`; the Rust toolchain menu is `Rus~t~` on Alt-T.
353+- **Changes here**: `internal/*` deleted — fourteen packages moved to `codeberg.org/turbo-editors/turbo-core` and made public. New `internal/golang`: the profile, the Go scanner (recovered from `internal/syntax/scan.go` and ported onto the library's exported `Class`, `LineIndex` and `Register`), and the three starter templates. `main.go` rewritten around `golang.Profile()`; `moduleRoot`/`projectRoot` replaced by `app.ProjectRoot`. `Makefile` and `scripts/install.sh` stamp `turbo-core/version` instead of `internal/version`.
354+- **Nothing about the editor's behaviour changed.** The menus, the keys, the themes, the file formats and the environment variables are what they were. `TURBO_GO_THEME_DIR` still works, deliberately: it is derived from the profile's slug precisely so a released name is preserved.
355+- **Decisions**: the Go scanner stays here rather than in the library, so that "what does this editor register?" is the first question about a new editor — a `.rs` file therefore opens as plain text here. `golang.Register()` is called from `main` explicitly, not from an `init`, so the fact is a line somebody can read. The `replace` is committed rather than hidden in a gitignored `go.work`, so it is visible in the diff and so three repositories side by side build with nothing published.
356+- **Tests**: the whole existing suite passes unchanged. Twelve Go-scanner tests came back here from the library, along with the three templates' content tests and both real-gopls tests — the ones that are about *Go*, and that the library has no language server of its own to run. New `internal/golang/editor_test.go` builds a whole Turbo Go on a simulated terminal through the library's public API and checks that Register was called, that the profile reached the menu bar, and that a `.go` file comes out coloured; a bug where `main` forgot to register Go would pass every test in turbo-core.
357+- **One pre-existing test was fragile and is fixed.** `TestTheMakefileHandsOutTheFlagsThatStampABuild` asserted the stamped binary did not say `devel`, which is only true in a checkout that has tags. It now compares against `make version`, so it holds in a fresh clone too — and the comparison strips the leading `v`, because `internal/version` does.
358+- **Quality**: PASS. 0 errors, 0 warnings, 0 smells, complexity 1592 → 37. The fall is the code moving, not anything being simplified; turbo-core carries 1584 of it.
359+- **Docs**: `explanation/architecture.md` rewritten in both languages around the split, with a table of what moved where; `colouring-and-completion.md` updated to say which scanners are shared; every `internal/version` path corrected in `reference/versioning.md`, `how-to/make-a-release.md` and `how-to/run-the-tests.md`; the root README given a "Where the code is" section; `docs/diagrams/packages.drawio` regenerated from `go list` and verified against it edge for edge.
360+- **A pre-existing documentation defect was found by driving the real binary**, and fixed: the tutorial said "press ↓ five times to reach turbo-dark" when the Theme dialog has always opened *on the current theme*, so it was one press. Both languages now say one, and say why.
361+- **Two themes were added** at the user's request during the same session — `catppuccin-frappe` and `catppuccin-latte`, in turbo-core — which is what made the tutorial's arrow count worth checking rather than merely updating.
362+
363+## 2026-09-01 — Tool parameters, from turbo-core
364+
365+- **Goal**: part of the same request as turbo-core's entry of this date — a tool whose command needs a value must be able to ask for it. The feature is the library's; what changed here is the starter file people are given.
366+- **Changes**: `internal/golang/templates.go` — the tools template's comments now teach `{{label}}` and `{{label...}}`, with an example for THISgolang and the warning about single braces. `install_test.go` — one test asserted before checking whether it was in a git checkout at all, so it failed in a tree with no `.git` where `unknown` is the correct answer.
367+- **Decisions**: the examples go in the **comments**, not as a sixth tool. The five starter commands are what a project runs before it commits; `go mod init` is a different kind of thing, and adding it would change what `Create tools file` gives everybody in order to demonstrate a syntax.
368+- **Tests**: 2 in `internal/golang/templates_test.go` — the created file teaches the syntax, and none of the five starter commands accidentally became parameterised by the prose around them.
369+- **Quality**: PASS. 0/0/0, complexity 37 — unchanged; the change is comments and a test.
370+- **Docs**: a section in `reference/go-tools.md`, one in `how-to/run-go-commands.md` and one in `explanation/go-tools.md`, both languages.
371+
372+## 2026-09-01 — Released as v0.2.2
373+
374+- **Goal**: the user committed and released everything and asked for the record to be brought up to date. This entry is what was verified, not what was intended.
375+- **Verified from the repository and the Codeberg API**: **v0.2.2** at `d64410c`, which is exactly HEAD, with a release page. Working tree clean, on `main`. The tag is the fourth for this editor and the first since it moved onto the library.
376+- **The dependency is the published library**: `require codeberg.org/turbo-editors/turbo-core v0.1.0` with no active `replace`, and a `go.sum` whose checksum matches sum.golang.org. A clean clone now builds without turbo-core beside it, which is what the whole extraction was for.
377+- **One wart, left alone deliberately**: the old replace block is commented out rather than deleted, and its comment still says "drop it once the version above is tagged and published" — which is done. It sits inside a released commit, so it was written down rather than changed.
378+- **Nothing was built or changed in this entry** — no code, no tests, no docs. The suite and the gate were last measured at the previous entry and are unchanged.
379+
380+## 2026-09-01 — Dockerfile, compose, YAML and XML colouring (ticket 8)
381+
382+- **Goal**: ticket 8 — "add syntax for Dockerfile, compose file, yaml, xml". The scanners themselves belong in turbo-core; this repository's part was to use them and to say so.
383+- **Changes**: `internal/golang/templates.go` — the snippets template's `languages` comment now lists the nine names this editor knows. `go.mod` requires `turbo-core v0.2.0`. Documentation: the YAML, XML and Dockerfile sections in `docs/{en,fr}/reference/languages.md` with the recognition and class tables brought up to date, and the language counts corrected in the architecture and colouring explanations, both READMEs, and the snippets references.
384+- **Decisions**: none taken here — the three that matter (a compose file is just YAML, XML gets its own scanner for CDATA's sake, `Filenames` matches the stem) were taken in turbo-core and are recorded there.
385+- **Tests**: `TestTheCreatedSnippetsFileListsEveryLanguageTheEditorKnows` iterates `syntax.Registered()` rather than a hardcoded list, so the template cannot fall behind the registry again. Falsified by removing a name from the template.
386+- **A stale claim found while sweeping**: the reference said themes were "the three shipped themes" when eight ship, and turbo-rust's English snippets reference listed `go` where it meant `rust`. Both fixed.
387+- **Quality**: PASS, 0 errors / 0 warnings / 0 smells, complexity unchanged.
388+- **Verified in a real pty**: a `Dockerfile`, a `compose.yaml` and a `pom.xml` opened in the built binary and coloured, with a CDATA section's contents arriving as a string rather than as markup.
389+- **Blocked on a release**: this branch does not build until turbo-core v0.2.0 is tagged and published.
390+
391+## 2026-09-01 — The build checks the version it stamped
392+
393+- **Goal**: the user asked that the build verify it really embeds the right version number.
394+- **Changes**: new `scripts/check-version.sh`, called by `make build` after linking, by `scripts/install.sh` on the staged binary **before** the install, and by `03-build-releases.sh` on the one asset this machine can run. The release script's own `grep -qF` check was replaced by it.
395+- **The failure it catches**: a linker stamp is a string, and a wrong one is not an error. `-X` naming a symbol that does not exist links happily and stamps nothing; the binary then reports whatever Go build info says — `devel`, on a binary attached to a release. Reproduced by hand: `make build LDFLAGS="-X '….version.stampX=v9.9.9'"` linked cleanly and reported `0.2.2+dirty`, and now fails the build.
396+- **Decisions**: the version comparison is an **equality**, not a search — `0.2.0` is a substring of `10.2.0` and of a commit hash that contains it, and a stamp that is nearly right is the case worth catching. The check runs **before** the install, so a binary that cannot name itself never replaces one that can. With no version to expect — a build outside a git checkout — the only claim left is that the number is not `unknown`.
397+- **Tests**: 8 in a new `version_check_test.go`, driving the script against binaries built for the purpose. Three were falsified: the wiring in the Makefile, the ordering in the installer, and the substring case.
398+- **Verified for real**: `make build`, `scripts/install.sh --prefix $(mktemp -d)`, and a deliberately misspelt `-X`.
399+- **Docs**: a "Checked at build time" section in `docs/{en,fr}/reference/versioning.md`.
400+- **Quality**: PASS 0/0/0, complexity unchanged.
401+
402+## 2026-09-01 — Tickets 9 to 14: autosave on in a created settings file
403+
404+- **Goal**: tickets 9–14. Only ticket 9 is editor-side; the other five are turbo-core's and reach Turbo Go through the library.
405+- **Changes**: `internal/golang/templates.go` — the settings template now writes `autosave = true`, with the reason in the comment above it. `.gitignore` gained `go.work`.
406+- **Decision**: the template, **not** `settings.Default()`. A project that has created a settings file has said what it wants, and the file is the visible, editable place to say otherwise. Turning the library default on would mean the editor writing to disk in any directory it is started in, which is a different and much larger claim; the user was asked and chose the narrower one.
407+- **Tests**: `TestTheCreatedSettingsFileTurnsAutosaveOn` loads the created file rather than grepping it, and `TestAProjectWithNoSettingsFileStillDoesNotAutosave` holds the other half of the decision. The first was falsified by putting `false` back.
408+- **Docs**: the settings reference gained a "When a change takes effect" section; the menus reference now states the enabled condition of all six create/open items; the tools and snippets references gained their `Open …` rows and lost "a project that already has one is opened unchanged"; `configure-a-project.md` was rewritten around autosave already being on; `run-the-tests.md` gained a section on testing against an unreleased turbo-core with `go work`. EN and FR throughout.
409+- **Quality**: PASS 0/0/0, complexity unchanged.
410+- **Verified in a real pty**: all six menu items flipping between available and greyed, and the created settings file holding `autosave = true`.
411+- **Note**: this branch builds and passes against the published `turbo-core v0.2.0`. The other five tickets only become visible once turbo-core v0.3.0 is released and the `require` here is bumped.
412+
413+## 2026-09-02 — Code navigation: documentation only
414+
415+- **Goal**: the Code menu and the eight questions it puts to the language server. All the code is turbo-core's; Turbo Go changes only by describing it.
416+- **Changes**: a new `docs/{en,fr}/how-to/ask-about-code.md`; the **Code** section in the menus reference, with Describe symbol and Go to definition removed from Run and Search; `Shift-F12` and `Ctrl-T` in the keyboard reference; a "Nine questions, one connection" section in the colouring-and-completion explanation. EN and FR throughout.
417+- **Decision**: a **separate** guide rather than an extension of `navigate-code.md`. That page answers "how do I get to the piece of code I am looking for" — searching, line numbers, windows. This one answers "what does this name mean" — a different need, so a different page, with the old one linking to it.
418+- **Docs traps met**: the new guide was first written *over* `navigate-code.md` and had to be restored from git. And the two moved menu items had to be deleted from Run and Search in **four** files, not two — the French tables are separate text.
419+- **Quality**: PASS 0/0/0, complexity unchanged.
420+- **Note**: this branch builds against the published `turbo-core v0.3.0`. Nothing here needs v0.4.0 to compile; the menu it documents appears once that is released and the `require` is bumped.
421+- **Follow-up the same day**: the user asked whether the LSP features were documented for users. They were — `how-to/ask-about-code.md`, EN and FR, both editors — but the neighbouring `enable-completion.md` still had a "what else the server gives you" section listing three keys and no mention of the Code menu, Problems, or the gutter marks. Fixed in all four files. That is the "adapting is not substituting" trap from the `turbo-new-editor` skill, met on a page I had not thought to re-read: **a new feature makes its neighbours stale, and the neighbours are where a user already is.**
422+
423+## 2026-09-02 — Ticket 19: better code editing, documentation only
424+
425+- **Goal**: ticket 19 — double-click to select a word, insert line, delete line. All the code is turbo-core's; this repository documents it.
426+- **Changes**: the keyboard and menus references in EN and FR, and a "Select and edit whole lines" section in `how-to/navigate-code.md`.
427+- **The one thing to notice**: **redo is `Ctrl-R` now, not `Ctrl-Y`**`Ctrl-Y` deletes a line, as it did in Turbo C. That is a key changing under people who had learnt it, so it is stated in the menus reference rather than only in the table of keys.
428+- **Quality**: PASS 0/0/0, complexity unchanged.
429+
430+## 2026-09-02 — Starter templates moved out of the source into embedded files
431+
432+- **Goal**: the user asked for the three starter templates to live in three files in `internal/golang/` and be embedded into the binary, instead of Go constants in `templates.go`. Extended to both editors at their choice.
433+- **Changes**: `settings.toml.tmpl`, `snippets.toml.tmpl` and `tools.toml.tmpl` beside the code; `templates.go` reduced to three `//go:embed` declarations. `profile.Templates` is unchanged — it takes strings, and an embedded variable is one, so turbo-core needed nothing.
434+- **Decisions**: **`.tmpl`, not `.toml`**, put to the user with the measurement behind it — `settings.toml.tmpl` holds `theme = %q`, which `tomllib` rejects, so naming it `settings.toml` would be a claim it cannot meet: a linter would reject it and the editor would colour it as TOML and draw it as broken. The snippets and tools templates *are* valid TOML (their verbs sit in comments), but all three take the suffix so the set is consistent. **The user accepted that the editor will not colour `.tmpl` files.**
435+- **Method**: the constants were **evaluated, not cut out of the source** — each is a concatenation of a raw string with a quoted one, because a raw string cannot contain the backtick in `\`turbo-go -list-themes\``. A throwaway test wrote the three files from the constants themselves, then was deleted.
436+- **A guard added for a risk this refactoring created**: the format verbs no longer sit next to the `profile.Templates` contract that documents them, so three tests now count the verbs per file, check none is empty, and fill each template asserting no `%!` marker comes out — Go writes `%!q(MISSING)` into the output rather than failing, so a wrong count produces a starter file that is written, opened, and wrong. All three falsified.
437+- **A verification that went stale under me.** I compared the six new files against HEAD byte for byte and they matched — and then `turbo-go/internal/golang/snippets.toml.tmpl` was overwritten with the contents of the playground's own `bin/.turbo-go/snippets.toml`, which a test caught. I could not attribute the overwrite. Restored from HEAD's evaluated constants and re-verified **after** the last step rather than in the middle. The lesson is the ordering: verify at the end, not when convenient.
438+- **Quality**: PASS 0/0/0 in both, complexity unchanged.
439+- **Docs**: turbo-core's `how-to/write-the-starter-files.md` gained a section on keeping them in files, in EN and FR; both architecture explanations list the new files; the `turbo-new-editor` skill's step 3 now prescribes this shape.
440+
441+## 2026-09-03 — Family count corrected, and three stale documentation claims found by a pty run
442+
443+- **Documentation only; no code changed.** `turbo-python` joined the family, so `docs/{en,fr}/explanation/architecture.md`'s "both editors use them unchanged" and "a change to a menu now affects both editors at once" became false. Changed to "every editor".
444+- **Three claims were stale because the library grew a Code menu, and nothing noticed.** Driving this editor's own binary in a pty gives the bar as `File Edit Search Run Code Options Window Snippets Go Help`. The tutorial listed `File Edit Search Run Options Window Help` — missing Code, Snippets **and this editor's own Go menu** — and told the reader to press `→` **four** times to reach Options, which has been five since the Code menu shipped. `reference/menus.md`'s opening sentence omitted Code as well. Fixed in EN and FR.
445+- **The lesson, and it is not this editor's alone**: a library that grows a menu makes every editor's tutorial wrong in a way no test sees, because a tutorial is prose about a screen. The counts are worth re-reading off a terminal after any change to the bar.
446+- **This repository's suite was already red at `HEAD`** — four tests in `internal/golang/templates_test.go` still assert a five-tool starter file that deliberately grew to eight. Verified pre-existing by stashing and re-running; not caused here and not fixed here. Detail in the handoff.
447+- Not committed.
448+
449+## 2026-09-09 (later) — the theme list gained three entries
450+
451+- **Goal**: none of its own. turbo-core gained `monochrome-light`, `darcula` and `intellij-light`, and renamed `monochrome` to `monochrome-dark`; this repository's documentation had to follow. Eleven themes ship now.
452+- **Changes**: `docs/{en,fr}/reference/themes.md` — the embedded list, three new table rows, and a new "a name a theme used to answer to" section saying `monochrome` still loads; `docs/{en,fr}/how-to/write-a-theme.md` — the inherit-from advice and the shipped-theme count; `README.md`'s themes bullet. No code change.
453+- **History was left alone**: "comments were the dimmest colour in six of the eight shipped themes" in `write-a-theme.md` is a true sentence about when that rule was written.
454+- **Not yet true of the binary.** This repository pins turbo-core v0.4.2, which ships eight themes. The documentation is ahead until turbo-core is tagged and the `go.mod` here is bumped — see turbo-core's handoff of the same date.
455+
456+## 2026-09-15 — ACP agent windows, and four stale tests fixed on the way in
457+
458+- **Goal**: the user asked for Agent Client Protocol support — an agent window with a typing area and a rendered conversation, code coloured, several agents configured in TOML in `acp.toml`, one window per agent. The library owns the window, the menu and the event loop, so the feature itself went into turbo-core; see that repository's history for the same date.
459+- **What is in *this* repository**: `internal/golang/acp.toml.tmpl`, embedded beside the other three starter files and wired into `profile.Templates.Agents`. That is all the code. Plus six documentation pages (EN + FR: a how-to, a reference, an explanation) and their index entries.
460+- **Step zero was fixing a suite that had been red at `HEAD` since 2026-09-03.** Four assertions in `templates_test.go` still described a five-tool starter file that had deliberately grown to eight. The template was right and the tests had drifted, exactly as that handoff predicted. `…HoldsTheFiveGoCommands` now names all eight and fails on a ninth it does not know about; `TestRunIsTheOneToolInATerminal` became `TestEachToolGoesWhereItsOwnOutputBelongs` with the real map; the tabs test targets `main` rather than an `if err != nil` snippet that no longer exists; and `…StillLoadsWithItsPlaceholderExamples` was inverted — two starter commands now take a value on purpose, and what it checks is that the braces in the file's *comments* did not become tools. All four were falsified before being accepted.
461+- **Decisions**: the starter file's example agent is `docker agent`, because that is what a Go developer is most likely to already have; the template takes two blanks (the project directory, and the user-level path a comment names) and a test fills it and asserts no `%!` marker comes out, since Go writes `%!s(MISSING)` into the output rather than failing.
462+- **Tests**: 4 new in `internal/golang`, 4 corrected. `make test` green — for the first time since 2026-09-03.
463+- **Quality**: PASS 0/0/0, complexity 37, unchanged.
464+- **Docs**: `docs/{en,fr}/how-to/talk-to-an-agent.md`, `reference/acp.md`, `explanation/agent-windows.md`, both `README.md` indexes. The drawio diagram was checked against `go list` and needed no change — this repository's import graph did not move.
465+- **The documentation was written before the code, at the user's request**, with a status banner on every page saying so. Two of its claims were false by the time the code existed (`syntax.error` is not a class — `diagnostic.error` is the key; and the output cap is per entry rather than 10 000 lines), and the refusal messages it quoted were not the ones the loader emits. All corrected against the running code before the banners came off.
466+- Not committed.
467+
468+## 2026-09-15 (later) — the spinner and copying, documented
469+
470+- **Documentation only in this repository**; the code is turbo-core's. The user asked for a spinner beside *thinking* and for a way to copy text out of a conversation, and both landed in the library.
471+- **Changes**: `docs/{en,fr}/how-to/talk-to-an-agent.md` gained "Take something out of the conversation" — the key table, what `Ctrl-C` copies with nothing selected, and the two clipboards. `reference/acp.md` gained per-pane key tables, a Copying section, the `editor.selection` row and a paragraph on the spinner. `explanation/agent-windows.md` gained three sections: why copying goes to two clipboards, why copying with nothing selected takes a whole block, and why the spinner is drawn from the clock.
472+- **Verified against the running binary**, not written from the code: the spinner was captured turning through ten distinct frames in a pty, and the copy was checked by base64-decoding the OSC 52 payload off the wire — which is how the editor's own defect (the speaker's label copied with the code) was found.
473+- **Quality**: PASS 0/0/0, unchanged.
474+- Not committed.
475+
476+## 2026-09-15 (night) — slash commands and `@` mentions, documented
477+
478+- **Documentation and the starter file only in this repository**; the code is turbo-core's (see its `.memory/` of the same date). The user asked for the ACP changes that let an agent's commands be discovered the way Zed discovers them, then for `@` as a file selector.
479+- **Changes**: `docs/{en,fr}/reference/acp.md` — seven key rows for the list, a **Commands and mentions** section, the `session/prompt` and `available_commands_update` rows, the Limits bullet; `docs/{en,fr}/how-to/talk-to-an-agent.md` — "Use the agent's own commands" and "Point the agent at a file"; `docs/{en,fr}/explanation/agent-windows.md` — the "left out" bullet narrowed to images, two sections appended; the embedded `acp.toml.tmpl` — two key lines. Applied by one script across the five editors with an exactly-once anchor check.
480+- **Tests**: `go test ./...` green (the template change is a comment; the tests that read the created file still pass).
481+- **Ahead of the binary**: this repository pins turbo-core v0.7.0, which has none of this. The pages are true once turbo-core is tagged and the pin moved.
482+- Not committed.
483+- **Later the same night**: `.turbo-go/acp.toml` gained the user's `mini-me (llama.cpp)` agent (`mm -acp`, `AGENT_CONFIG` env). Loads as two agents; not opened here, `mm` lives on the user's Mac.
484+
485+## 2026-09-16 — the trace variable and a troubleshooting bullet, documented
486+
487+- turbo-core gained `TURBO_ACP_TRACE=<file>` and an "update this editor could not read" line in Agent status, because the user saw no `/` commands from their own agent and nothing on screen could say why. Documented here EN + FR: a bullet in the how-to's Variants, a section in `reference/acp.md`. Docs only; not committed.
488+
489+## 2026-09-17 — documentation: terminal windows and tools on Windows
490+
491+- **Asked**: nothing of this repository directly. turbo-core gained pseudo-console (ConPTY) terminal windows and a per-platform tools shell (cmd.exe on Windows); the pages here that said "Linux and macOS" or `/bin/sh -c` went false the moment that landed.
492+- **Changes** (EN + FR): `README.md` (terminal windows: Linux, macOS and Windows), `docs/*/reference/terminal.md` (shell row, controlling-terminal row, platform table, error row), `docs/*/explanation/terminal-windows.md` (the Windows section rewritten: a pseudo-console and why it is a file of its own, built and vetted but not yet run), `docs/*/how-to/use-a-terminal.md` (`%COMSPEC%`, the five things to try first on Windows), `docs/*/reference/go-tools.md` (shell row, cmd.exe's globs and `;`, error row), `docs/*/explanation/go-tools.md` (`cmd.exe /S /C`). Applied by one script across the six editors, one anchor per file.
493+- **Not changed**: code, `go.mod`. **The documentation is ahead of the binary** until turbo-core is tagged (v0.9.0) and re-pinned here; the feature has never been run on Windows by anyone.
494+- **Tests**: none affected — documentation only.
495+
496+## 2026-09-18 — the "Untitled window has no LSP" fix: docs variant added, fix is turbo-core's
497+
498+- **Origin**: the defect was reported against this editor — "first launch: no LSP; save, quit, relaunch: works", the window having started Untitled — and diagnosed then fixed in turbo-core, where the save path lives.
499+- **Asked**: propagate to every editor the fix made in turbo-core the same day — saving now announces a document the server does not know (a window that started Untitled gets LSP from its first save), and Save As under a new name closes the old document. See turbo-core's `.memory/history.md` of 2026-09-18 for the defect and the fix.
500+- **Changes here**: `docs/{en,fr}/how-to/enable-completion.md` gain one variant — completion in a window that started without a name works from its first save, no relaunch needed. No code in this repository is involved.
501+- **Quality**: gate not re-run — a Markdown-only change; the gate measures the Go code.
502+- **Docs ahead of the binary**: the behaviour arrives only when turbo-core is tagged and this editor re-pinned. Not committed.
503+
504+## 2026-09-19 — moved to Rickub: turbo-core v1.0.0 re-pinned, releases published by a workflow
505+
506+- **Asked**: turbo-core had been moved to `rickub.com` and published as v1.0.0 with a Release workflow; do the same migration here — add the GitHub Action, update `01-release.tag.sh` and if need be `02`, drop `04` which the workflow makes unnecessary, and keep `03-build-releases.sh` runnable by hand.
507+- **Changes, module**: `codeberg.org/turbo-editors``rickub.com/turbo-editors` in `go.mod`, every `.go` file, `Makefile` (`VERSION_PKG`), `scripts/install.sh`, `README.md`, `docs/{en,fr}` (the two turbo-core deep links also went from Codeberg's `src/branch/main/` to `blob/main/`). `require rickub.com/turbo-editors/turbo-core v1.0.0`; `go mod tidy` with `GOWORK=off` rewrote `go.sum` from the proxy. The proxy also lists a `rickub.com/…/turbo-core v0.9.0`, but its `go.mod` still declares the Codeberg path, so v1.0.0 is the first version this module *can* require.
508+- **Changes, release tooling**: `.github/workflows/release.yml` (new, modelled on turbo-core's: tag push `v*`, `contents: write`, `go test` with `TURBO_GO_RELEASING=1`, `./03-build-releases.sh "${GITHUB_REF_NAME}"`, notes from the tag message + `go install` line + docs at the tag + checksums, run artifact, `softprops/action-gh-release@v2` attaching `turbo-go-*`, `SHA256SUMS`, `README.md`). `01-release.tag.sh` rewritten on turbo-core's: requires `release.env`, validates `TAG`, runs `make check` under `TURBO_GO_RELEASING=1`, refuses a taken tag (bump, never move), refuses a `replace`, pushes the current branch (not a hardcoded `main`) before tagging, no token file. `03-build-releases.sh`: tag from `$1` with `release.env` optional and `ABOUT` defaulting to `Turbo Go ${TAG}`, tag format check, `replace` check, `rm -rf release/${TAG}` before building, README gains the `go install` line, the closing hint no longer points at 04. **`02-release.publish.sh` and `04-release.upload-binaries.sh` deleted.** `release.env` comments rewritten; `OWNER`/`REPO` dropped (nothing reads them).
509+- **Tests**: `release_test.go` — the push assertion follows the new `git push origin "$(git rev-parse …)"`; new: `01` runs `make check`, refuses a `replace`, `go.mod` has none, no script reads a token and 02/04 are gone, `01` **run for real** twice against a throwaway bare remote (publishes; refuses the second time with "already exists"), `03` takes the tag from the command line and refuses `v0.o.0` (run for real, script alone in an empty dir), `03` no longer hands off to 04, and seven workflow assertions (trigger, `contents: write`, uses `./03-build-releases.sh`, attaches with `fail_on_unmatched_files`, links docs at the tag, no `secrets.`, sets `TURBO_GO_RELEASING`). The helper had to be `runOrFail`: `main.go` already owns `run`. Copy of the module for the throwaway clone leaves out `.git`, `bin`, `release` (489 MB of old binaries), `kits`, `demo`, `*.env` and `go.work*`; children run with `GOWORK=off` so the clone builds against the published library. Suite green in ~10 s with `GOWORK=off`.
510+- **Verified by hand**: `GOWORK=off ./03-build-releases.sh v0.0.1-test` → five binaries, host binary reports `0.0.1-test`, `sha256sum -c SHA256SUMS` all OK, README as expected; directory removed afterwards. `01` in a hand-made throwaway clone: `make check` ran (fmt, vet, test), root commit, push, tag on the bare remote.
511+- **Not done**: nothing committed or pushed — the repository has no commit yet and `origin` is unreachable from this sandbox (no SSH). The workflow has not run on Rickub: it is written against the same platform facts turbo-core's is, and turbo-core's has run. `docs/{en,fr}/reference/versioning.md` still describes `03` accurately and was left alone.
512+
513+## 2026-09-19 (later) — `03-build-releases.sh` renamed `02-build-releases.sh`
514+
515+- **Asked**: rename the build script, now that `02` and `04` are gone and the numbering had a hole.
516+- **Changes**: `git mv`-equivalent rename; every reference rewritten — `01-release.tag.sh`, `.github/workflows/release.yml`, `Makefile` (the `ldflags` target's comment), `release_test.go`, `release.env`, `docs/{en,fr}/how-to/make-a-release.md`, `docs/{en,fr}/reference/versioning.md`, `.memory/summary.md`. Older history entries and handoffs keep the old name, as history does.
517+- **Tests**: `GOWORK=off make check` green.
new file mode 100644
@@ -0,0 +1,517 @@
1+# History
2+
3+*Append only. One dated entry per session. Never rewrite or delete an entry, including your own from an earlier turn.*
4+
5+## 2026-08-30 — Turbo C-style Go editor, built from an empty repository
6+
7+- **Goal**: "the same editor as Turbo C but written in Go and made for Go programming — so Go syntax colouring, and LSP support for completion while editing. It must also be possible to apply themes to the editor." Delivered in full, in one session, at the user's request to go all the way through without stopping at the approval checkpoints.
8+
9+- **Changes**: the whole project. Eight packages under `internal/` (`buffer`, `theme`, `syntax`, `ui`, `editor`, `lsp`, `app`) plus `main.go`, a `Makefile`, three embedded themes, 13 documentation pages in each of two languages, a drawio dependency diagram, per-package `README.md`s, and this `.memory/`.
10+
11+- **Decisions**:
12+ - **tcell over tview and bubbletea**, with a hand-written widget framework (~1500 lines). tview has no text editor and the wrong look; bubbletea's whole-view re-render suits forms rather than a full-screen editor with stacked windows and an exact cursor cell. Chosen by the user from three options.
13+ - **TOML for themes**, chosen by the user over JSON and YAML. Costs one dependency (`BurntSushi/toml`), buys comments in a file people edit by hand.
14+ - **gopls detected, never bundled**, chosen by the user. `PATH`, then `GOBIN`, then `GOPATH/bin`; absence is reported on the status bar with the command that fixes it.
15+ - **LSP client written by hand** rather than taking `go.lsp.dev/jsonrpc2` — about 300 lines, and it keeps the total dependency count at two.
16+ - **Colouring by `go/scanner`** rather than a hand-written lexer or a highlighting library: exactly as right as the compiler, and no table to update when the language changes. The cost — only Go is coloured — was accepted deliberately.
17+ - **Steps 6 and 7 of the plan were swapped**: `lsp` was built before `app`, because `app` depends on it and the original order had it the other way round.
18+ - **`kits/**` excluded from qlty**, with the user's explicit agreement and a comment recording exactly what it hides. The two findings are real defects in the quality skill's own `quality_report.py` and belong to the kit, not here.
19+
20+- **Bugs found and fixed while building**:
21+ - `MoveWordLeft` stepped left before scanning, so it skipped a word when the cursor sat just after one.
22+ - `theme.LoadFile` restarted the inheritance depth at zero, so a two-theme `inherits` loop blew the stack instead of being reported.
23+ - `closeBoxLabel = "[■]"` was measured with `len()` — 5 bytes for 3 columns — so clicks two cells past the close box closed the window.
24+ - `InputLine` swallowed `Alt`-letter, which stopped a dialog's buttons from ever seeing their own shortcuts.
25+ - The status bar drew its hints and its right-aligned text over each other.
26+ - `buffer.New()` claimed an empty buffer ends with a newline, so every new file gained a stray `"\n"`.
27+ - **`ui.Dialog` grabbed the arrow keys for its focus ring before the focused control saw them**, which made the theme picker, the window list and the file browser unusable from the keyboard. Found while writing the tutorial, which is the second time this session that documenting something exposed a defect in it.
28+
29+- **Tests**: written alongside each step, never after. Every package green. Widgets and the editor are driven through `tcell.SimulationScreen`; the LSP client is driven against a fake server in the same process over `net.Pipe`, and — when gopls is installed — against the real one, which skips itself otherwise. Command: `make test`.
30+
31+- **Quality**: gate **PASS**. Four runs: smells 12 → 4 → 2 → 0, complexity 878 → 802, lint errors and warnings 0 throughout. The refactoring was real — a table-driven token classifier, `actions.go` split into three cohesive files, `buffer.IsWordRune` shared instead of duplicated in `editor`.
32+
33+- **Docs**: `docs/` with a language selector, and `en/` + `fr/` each holding one tutorial, five how-to guides, four reference pages and three explanations. Every link checked to resolve and to stay inside its own language. Package dependency diagram at `docs/diagrams/packages.drawio`, verified edge by edge against `go list -deps`. Root `README.md` rewritten from its one-line placeholder.
34+
35+- **Not done**: never run in a real terminal; only Linux/arm64 exercised; no CI; diagnostics stored but only surfaced on the status bar.
36+
37+## 2026-08-30 (later) — two defects found by running it for real
38+
39+- **Goal**: the user ran the editor in an actual terminal and reported two things: the cursor is invisible under `turbo-dark`, and completion produces nothing after typing `fmt.`.
40+
41+- **Changes**:
42+ - `internal/app/app.go``StartLanguageServer` now posts a `languageReady` interrupt when the server finishes starting, and `announceOpenDocuments` re-sends `didOpen` for every window already open, from the main goroutine.
43+ - `internal/app/language.go``Language` holds a `*lsp.Client` alongside the `*lsp.Server`, so a client can be attached without a process. That is what makes the editor's side of the conversation testable.
44+ - `internal/app/complete.go` — the popup is anchored with `View.CursorScreenPosition()` instead of an anchor of its own that forgot the gutter and the horizontal scroll.
45+ - `internal/editor/view.go` — the cursor cell is painted in a new `editor.cursor` theme key; `View` embeds `ui.FocusBox`.
46+ - `internal/ui/window.go``SetActive` propagates the focus to a content widget that can hold it.
47+ - `internal/ui/dialog.go``HandleKey` restated as an ordered list of handlers, which the quality gate required after the arrow-ring fix added a branch.
48+ - Three theme files, the theme reference and the theme how-to, in both languages.
49+
50+- **Decisions**:
51+ - **The cursor is painted by the editor, not left to the terminal.** A terminal draws its cursor in the user's colour, which owes nothing to the theme. The colours are a distinct pair rather than a reversal, so a terminal that draws its cursor by inverting the cell cannot invert it back into invisibility.
52+ - **Announce, rather than reorder `main`.** Starting gopls before opening the files would have fixed this one case and left the general one — a server that becomes ready at any later moment — still broken.
53+
54+- **Root cause of the completion failure**: `main` opens the files named on the command line and *then* starts the language server, so `DidOpen` at that moment reached nothing. gopls was never told the document was open; the `didChange` sent on every keystroke afterwards therefore referred to a document it did not have, and it answered completions from the stale on-disk text instead. Typing `fmt.` produced "No completions here".
55+
56+- **Tests**: an end-to-end test in `internal/app` replays the command's exact start-up order against a real gopls and completes text that exists **only in the buffer**. The first version of this test pre-wrote `strings.` into the fixture and passed with the fix removed — gopls answered it from disk. That version proved nothing; it was rewritten to type the text, and then it failed with the fix removed and passed with it, which is what a regression test is for. Also added: a fake language server in `internal/app` for testing what the editor says; focus-propagation tests; cursor-contrast tests across every theme.
57+
58+- **Quality**: gate back to **PASS** after restating `Dialog.HandleKey`. Run 6: 0 errors, 0 warnings, 0 smells.
59+
60+- **Lesson worth keeping**: both defects were invisible to a suite that never leaves memory. The simulation screen exercises the drawing code but not the terminal; a fixture on disk exercises the protocol but not what the editor actually said. Neither gap was obvious until someone ran the program.
61+
62+## 2026-08-30 (third) — the completion fix was still droppable, and an empty list said nothing
63+
64+- **Goal**: the user reported that the top menu had stopped working and that completion still produced nothing. They run the editor inside tmux, screen or an IDE terminal.
65+
66+- **The menu**: could not be reproduced. The real binary was driven under a pty (`script`, `TERM=xterm-256color`) and F10, Alt-F, Alt-E, a mouse click on the bar, and Down+Enter on an item all worked, with and without gopls. `internal/ui/menu.go` had not been touched since the previous session. The user confirmed it works again — most likely a stale binary.
67+
68+- **Changes**:
69+ - `internal/app/app.go` — the re-announcement no longer rides on a posted event. `announceOpenDocuments` is checked on every turn of the event loop and is idempotent. `tcell.PostEvent` **drops** events when its queue is full, and start-up is precisely when gopls floods it with diagnostics, so the previous session's fix could silently fail to arrive. The `languageReady` event type is gone.
70+ - `internal/app/complete.go` — an empty list now names the reason: `No completions — this file does not compile: <first error>`.
71+ - `internal/app/actions_view.go``Run ▸ Language server status` reports the server path, the workspace root, the current file, whether the server has been told about it, and the first error it reported.
72+ - `internal/app/language.go` — records the server path, the root, and which documents have been announced; `Report()` and `Knows()` expose it.
73+ - Both `enable-completion` how-to guides gained a section on the two ways completion looks broken when it is not.
74+
75+- **Root cause of the user's remaining symptom, found and reproduced**: they had created `hello.go` in the turbo-go repository root — the working tree is mounted from their machine, so the file was visible here. It declares `package main` and `func main()` alongside the project's own `main.go`. The package therefore does not compile, and a probe against real gopls in an identical two-`main` module returned **0 completions with no error at all**. That is not an editor defect; the editor's failing was to shrug at it, which is what the new message fixes.
76+
77+- **A file of the user's was deleted earlier by mistake.** An empty `hello.go` appeared at the repository root and was removed as a stray artefact of local experimentation. It was almost certainly the user's first attempt at the same reproduction. It was empty, so nothing was lost, but the working tree is shared and files appearing in it are not to be assumed to be one's own.
78+
79+- **Tests**: the announcement is now tested without any event being delivered, and for being made exactly once however often it is checked. Added tests for the empty-completion message and for the status report naming the file, the reason, and an untitled window. The end-to-end gopls test still fails with the fix removed and passes with it.
80+
81+- **Quality**: gate PASS, run 7. 0 errors, 0 warnings, 0 smells.
82+
83+## 2026-08-30 (fourth) — an installer, so the editor can be used on real projects
84+
85+- **Goal**: the user asked for a script that builds and installs the editor onto their PATH, so they can try it on an actual Go project.
86+
87+- **Changes**: `scripts/install.sh`, `make install` / `make uninstall`, `install_test.go`, and the install how-to and CLI reference in both languages. The README's "getting started" now leads with `make install`.
88+
89+- **Decisions**:
90+ - **Default destination is `$GOBIN`, then `$GOPATH/bin`** — where `go install` would put it, and therefore the directory a Go developer most likely already has on PATH. `--prefix` overrides.
91+ - **Build to a temporary file, then copy.** A failed build must never replace a working installation; there is a test for exactly that, because a stray `.go` file in package main is precisely what the user's own scratch file did to this repository an hour earlier.
92+ - **The required Go version is read from `go.mod`**, not hardcoded, so the check cannot drift from the build.
93+ - **It reports rather than assumes**: the Go version, where the binary went, whether that directory is on PATH (with the exact line to add, for the user's own shell), and whether gopls is installed. `--with-gopls` installs the server too.
94+
95+- **Tests**: ten, in `install_test.go`. They run the script for real into a temporary prefix and check the binary works, that the PATH warning appears and says how to fix it, that `--uninstall` removes what was installed and does not fail when there is nothing, that bad options are refused, that it runs from any working directory, and that a deliberately broken build leaves the previous installation byte-for-byte untouched. They skip under `-short`, on Windows, and without bash.
96+
97+- **Note**: the user removed their own `hello.go` from the repository root, so `go build ./...` compiles again and the root package's tests run.
98+
99+## 2026-08-30 (fifth) — windows follow the terminal
100+
101+- **Goal**: the user asked that the main window be resizable when the terminal changes size.
102+
103+- **Root cause**: `Desktop.SetBounds` only ever called `ClampInto`, which **moves** a window and never resizes it. A window that filled an 80-column terminal kept its 78 columns in a 120-column one.
104+
105+- **Changes**:
106+ - `internal/ui/window.go` — a Turbo Vision-style `Grow` mode. `GrowBoth` is the default for a document window: the right and bottom edges move by the same delta the desktop's did, the top-left corner stays put, and the result is then held to the desktop's own size.
107+ - `internal/ui/desktop.go``SetBounds` compares against its previous rectangle and takes the windows with it. It is inert when nothing changed, which matters because it runs on every turn of the event loop.
108+ - `internal/ui/dialog.go``MoveTo` and `CenterIn`, which take a dialog's controls with it. Controls are placed in screen coordinates at build time, so moving the frame alone would have left them behind.
109+ - `internal/app/app.go` — a resize re-centres every open dialog and dismisses the completion popup, which is anchored to a cursor that has just moved.
110+
111+- **Decisions**: grow modes rather than **proportional scaling**. Scaling moves windows the user placed deliberately, and rounding makes it lossy — shrink then grow and nothing is where it was.
112+
113+- **Tests**: eleven new ones. Growing, shrinking, a cascaded window keeping its offset, a `GrowNone` window being moved but not resized, the desktop's size winning over the minimum on a terminal too small to hold one, the minimum being respected when the desktop can hold it, and `SetBounds` being inert when nothing changed. At the app level: the window keeps its margins from the terminal's far edges, the editor shows more lines afterwards, dialogs are re-centred with their controls, and the popup is dismissed.
114+
115+- **Verified live in a real pty.** The editor was run under `script`, its pty resized with `stty` — which sends a genuine SIGWINCH — and the window's bottom border measured **76 → 96 → 46** cells as the terminal went 80 → 100 → 50. This is the first time the project has been checked against an actual terminal resize.
116+
117+- **Quality**: gate PASS.
118+
119+## 2026-08-30 (sixth) — the cursor, properly this time
120+
121+- **Goal**: the user reported that the cursor is still invisible under `turbo-dark`, after the earlier fix.
122+
123+- **Why the earlier fix was not enough**: it painted the cursor's cell in the theme's colours, but a terminal draws its own cursor **over** the cell, in whatever colour the user configured for some other palette. On a dark theme that is very often a dark block covering the amber cell underneath. Painting can never win against something drawn on top of it.
124+
125+- **Changes**:
126+ - `internal/app/app.go``applyCursorStyle` calls `SetCursorStyle(tcell.CursorStyleSteadyBlock, cursorColor(theme))` whenever the theme changes. tcell turns that into `ESC[2 q` and `ESC]12;<colour>`, so the theme now decides the terminal's own cursor. The painted cell stays as a fallback for terminals that support neither.
127+ - `internal/theme/themes/turbo-dark.toml` — the current line was `#262626` against a `#1c1c1c` page: ten channel values, which is no highlight at all. Now `#303030`.
128+ - `internal/theme/themes/borland-light.toml` — same defect, `#f4f4f4` on `#ffffff`, eleven values. Now `#e8e8e8`.
129+
130+- **Verified on the wire**: run under a pty, the editor emits `ESC[2 q` and `ESC]12;#ffd787` for turbo-dark, `#00ffff` for turbo-classic and `#af5f00` for borland-light. tcell emits `ESC]112` and `ESC[0 q` on exit, so the user's terminal is left as it was found.
131+
132+- **Tests**: contrast is now **measured**, not assumed. `channelDistance` gives the largest per-channel difference between two colours, and every theme must keep the cursor at least 64 from its line and the line at least 16 from the page. Confirmed non-vacuous by restoring the old `#262626` and watching the test fail with "the current line is 10 from the page, want at least 16". Also: `cursorColor` returns the cursor style's background, and changing the theme changes it.
133+
134+- **Lesson**: "it looks fine to me" is not a measurement. Two of the three shipped themes had a current-line highlight nobody could see, and no test could tell.
135+
136+## 2026-08-31 — the release build staged nothing, then built for five platforms
137+
138+- **Goal**: first, that `03-build-releases.sh` copy `./bin/turbo-go` into the release directory correctly; then that it cross-compile for darwin/arm64, linux/amd64, linux/arm64 and both Windows architectures.
139+
140+- **Root cause of the original failure**: three faults stacked. `VERSION` was never defined anywhere — `release.env` sets only `TAG`, `ABOUT`, `OWNER`, `REPO` — so `TURBO` evaluated to `turbo-go-`. `make build` writes `bin/turbo-go`, not a file of that name in the root, so the `mv` had nothing to move and `set -e` killed the script. And `mv` would have taken the binary out of `bin/`, breaking `make run` and any local install.
141+
142+- **Changes**: a `PLATFORMS` array drives the builds, the checksum file and the README table, so adding a target is one line. `VERSION` is derived from `TAG` (`${TAG#v}`). Assets are named `turbo-go-<version>-<goos>-<goarch>`, with `.exe` on Windows — with five downloads the platform has to be in the name. Cross-compiles run with `CGO_ENABLED=0`, which is safe because tcell and toml are both pure Go, and `-trimpath`, which keeps the build machine's paths out of a binary that goes to strangers. The host build still runs first, as the fastest way to find a compile error and the only binary this machine can run to check the version against `TAG` — which is what `release.env`'s own comment always said the script should do. `SHA256SUMS` covers every platform in one file, since that is what `sha256sum -c` reads.
143+
144+- **Verified by running it**: five binaries staged; `go version -m` reports the right `GOOS`/`GOARCH` for each and the magic bytes are Mach-O, ELF and PE as they should be; `sha256sum -c` passes on all five; the host binary runs; no `/home/agent` path survives `-trimpath`; `TAG=v9.9.9` is refused against a binary reporting `0.1.0`; and adding a sixth platform propagated to the build, the README table and the checksums before being reverted.
145+
146+- **Reported, not changed** (out of the scope asked for, twice): `04-release.upload-binaries.sh` globs `*.vsix`, left over from the VS Code extension template these scripts came from. It uploads `SHA256SUMS` and none of the five binaries the checksums are *for*. Replacing `"${RELEASES_DIR}"/*.vsix` with `"${RELEASES_DIR}"/turbo-go-*` would fix it. The stale `.vsix` and `package.json` comments in `04` and `release.env` are from the same template.
147+
148+## 2026-08-31 (later) — the upload script, adapted to a Go release
149+
150+- **Goal**: the user asked that `04-release.upload-binaries.sh` be fixed and adapted, after two rounds of flagging that it uploaded nothing.
151+
152+- **Root cause**: the loop globbed `"${RELEASES_DIR}"/*.vsix`, left over from the VS Code extension template these scripts came from. It attached `SHA256SUMS` and none of the five binaries the checksums were *for*.
153+
154+- **A supposition that was wrong, and checked before acting**: the upload used `--data-binary` with `application/octet-stream`, and Gitea's documented parameter for this endpoint is a `multipart/form-data` file field. Reading Codeberg's own swagger showed the endpoint `consumes` **both**, so the existing mechanism was correct and was left alone. Nearly rewrote something that was not broken.
155+
156+- **Changes**: the glob now picks up `turbo-go-*` and then `SHA256SUMS`, in that order, so an interrupted run never leaves checksums on a release with nothing to check. `jq` reads the release id and the attached assets instead of `grep -o '"id":[0-9]*' | head -1`. HTTP statuses are told apart: 401/403 says the token was refused, 404 says to run `02` — previously a bad token was reported as a missing release, which sends you to fix the wrong thing. Assets already attached are listed up front and replaced only with permission, so a run that failed halfway can simply be repeated. And `--dry-run` resolves the release and prints exactly what would be sent, without sending it.
157+
158+- **A bug the dry run found in itself**: `read -p` returns non-zero at end of input, so under `set -e` the script died at the README prompt when stdin was not a terminal. Both prompts now go through a `confirm` helper that answers no by itself when there is no terminal — a publishing script must not take silence for consent, nor die on the end of its input.
159+
160+- **Verified against the live API**, read-only: the release id resolves (11905813), the six assets are listed in order, an unknown option is refused, missing artefacts are reported, a deliberately invalid token gives "Codeberg refused the token (HTTP 401)", and a tag with no release gives the 404 message. `release.env` and `turbo-go.token.env` were restored after each. **Not verified**: the POST and DELETE themselves, because running them would publish artefacts on the user's behalf. That is theirs to run.
161+
162+## 2026-08-31 — Terminal windows (ticket 0007)
163+
164+- **Goal**: "je voudrais avoir la possibilité de créer des fenêtres qui soient des terminaux (pour lancer des commandes shell)", then "vas au bout du bout, puis crée le ticket pour la version pour Windows". Options chosen up front: a **real terminal (pty + VT emulator)** rather than captured command output; **several terminals**, each starting in the active file's directory; **Linux and macOS first**, with Windows showing a clear "not supported yet" message.
165+- **Changes**: new `internal/terminal` package (16 files) — `Session` over `/dev/ptmx` with build-tagged `pty_linux.go` / `pty_darwin.go` / `pty_other.go`, a `Parser` state machine over CSI/OSC/ESC, a `Screen` with scrollback and an alternate-screen aside, `Encode` for keys, and a `View` widget. Wired into `app` through a new `terminals.go`, plus edits to `app.go` (the `terminals` map, `keyLayers()`, title refresh, `F8`), `menus.go` (`Window ▸ New terminal`) and `actions_file.go` (`editorViewOf`, `closeWindow`, `Quit`). Two new theme keys, `terminal.text` and `terminal.cursor`, in `keys.go` and all three theme files.
166+- **Decisions**: a **real pseudo-terminal**, because a captured pipe loses colour, paging, `isatty` and `Ctrl-C`, and nothing interactive works at all — that is most of what a terminal is for. The **emulator is hand-written and partial** rather than a third dependency: what a shell, `go test`, `git`, `less`, `htop` and `vim` need is a bounded list, about six hundred lines, and a general-purpose library brings character sets, mouse protocols and sixel that would all need keeping alive. The **key routing was inverted for a focused terminal** — it outranks the editor's global shortcuts, keeping only the function keys, `Alt-X` and `Alt-0``Alt-9` — because a shell and an editor both want `Ctrl-C`, `Ctrl-W` and `Ctrl-F`, and without the reserved handful there is no way out of a full-screen program. Rejected: reserving fewer keys (no escape from `vim`), reserving more (readline becomes unusable), and asking for confirmation when closing a terminal (it holds a process, not unsaved work).
167+- **Tests**: 12 new tests in `internal/app/terminals_test.go` plus the package's own suites; `internal/terminal` at 95.7 %, `internal/app` up from ~71 % to 82.8 %. Run with `make test`, or `go test ./... -race`. Two test defects were found and fixed rather than accepted: a test that matched the pty's own echo of the command line instead of the shell's output, and a flaky drawing test racing the shell's startup — which, once made deterministic, turned out to have been sampling the cursor cell rather than the text.
168+- **Quality**: PASS. Four `return-statements` smells appeared (`handleKey`, `applySGR`, `applyAttribute`, `extendedColor`) and were refactored away by turning three switch-tables into actual tables and the routing chain into a list of layers. 0 errors, 0 warnings, 0 smells, complexity 1023.
169+- **Docs**: three new pages per language — `how-to/use-a-terminal.md`, `reference/terminal.md`, `explanation/terminal-windows.md` — and updates to both `README.md` indexes, `reference/keyboard.md`, `reference/menus.md`, `reference/themes.md`, `how-to/write-a-theme.md` and `explanation/architecture.md`. New `internal/terminal/README.md`; updates to `internal/app/README.md` (whose file table was also stale) and `internal/theme/README.md`. `docs/diagrams/packages.drawio` gained the `terminal` and `golang.org/x/sys/unix` nodes and five edges, and was then checked against `go list` programmatically — it matches edge for edge.
170+- **Also**: created ticket `0015` for the Windows/ConPTY port. Ticket `0007` was left `open` — closing it is the user's call. Nothing was committed.
171+
172+## 2026-08-31 — Project settings, autosave and TOML colouring (ticket 0002)
173+
174+- **Goal**: "enregistrer les paramètres du projet dans un dossier `.turbo-go` dans un fichier `settings.toml`" — the theme, a statement that files are saved automatically, the autosave implementation itself, TOML syntax colouring, loading the file if it exists, and a menu entry that creates a pre-initialised one. Then "va au bout du bout". Options chosen up front: autosave **after a pause in typing**; the theme written back **only when the file already exists**; the file looked for in **the working directory only**, with `-theme` winning over it; and **nothing beyond theme and autosave** in the file for now.
175+- **Changes**: new `internal/settings` package (`settings.go`, `create.go`, `rewrite.go`, `README.md`, tests). `internal/syntax` gained a `Language` dimension — `Highlight(lang, src)`, `LanguageOf(path)`, `NewCache(Language)`, `SetLanguage` replacing `SupportsPath`/`SetEnabled` — plus `toml.go`, a hand-written TOML scanner. `internal/app` gained `autosave.go` and `project.go`, an `autosave` and `settingsPath` field, an injectable clock, `saveDueDocuments` in the `Run` loop, `UseSettings`, and two Options menu items. `main.go` reads the file and resolves the theme. `internal/editor` updated for the new `syntax` API.
176+- **Decisions**: the settings directory is **not searched for upwards** — a module has a real boundary, "the project" does not, and a walk makes a file three directories up change your colours silently. `.turbo-go/` is **created only from the menu**, never as a side effect of picking a theme, because that would put a directory into someone's repository for trying a colour; this is also what makes the write-back rule one sentence. `SetTheme` **rewrites one key in place** rather than re-encoding, since the file exists to be hand-edited and is mostly comments — losing them on a first theme change would be a silent deletion of someone's writing. Autosave waits for a **pause in typing**: a fixed interval writes mid-edit, and on-focus-change leaves disk an hour behind screen. Rejected along the way: per-window autosave deadlines (unobservable gain, more state), and new `syntax.toml*` theme keys (every existing user theme would have stopped colouring TOML). `ui.Menu` has no nested submenus, so the two entries are flat under Options rather than the submenu asked for — said so rather than building nesting nobody requested.
177+- **Tests**: 20 in `internal/settings`, 24 for the TOML scanner, 18 for autosave (with an injected clock, so nothing waits), 10 for the project-settings menu items, 7 in `main`. Run with `make test`, or `go test ./... -race`. Also verified end to end against the real binary in a pty: the theme coming from `settings.toml`, `-theme` overriding it, and autosave writing a file **from the idle timer alone** with the process killed before any quit path could run — plus a control run with no settings file, which left the file untouched.
178+- **Two defects found in my own new code**: the TOML scanner patched `spans[len-1].Start` after an emit that had dropped an empty span, corrupting the *previous* span (a test caught it); and the first colouring test asserted through a live shell, which is the class of trap already recorded from the terminal session.
179+- **Quality**: PASS. Four smells appeared (`writeFile` and `tomlWordClass` many-returns, two complex binary expressions) and were refactored away by splitting functions and naming the character sets. 0 errors, 0 warnings, 0 smells, complexity 1150.
180+- **Docs**: three new pages per language — `how-to/configure-a-project.md`, `reference/project-settings.md`, `explanation/project-settings.md` — and updates to both indexes, `reference/cli.md`, `reference/menus.md`, `explanation/architecture.md` and `explanation/colouring-and-completion.md`. New `internal/settings/README.md`; `internal/syntax/README.md` and `internal/app/README.md` brought back in sync. `docs/diagrams/packages.drawio` gained `settings` and three edges, and was re-checked against `go list` — it matches edge for edge.
181+- **Context**: the terminal work from earlier the same day was merged to `main` by the user as PR #1 during this session; this work sits on `feature/project-settings`, uncommitted.
182+
183+## 2026-08-31 — Window frame boxes: [x] to close, [■] to maximise
184+
185+- **Goal**: "ce bouton `[■]` pour le moment sert a fermer la fenetre, il faudrait le changer par `[x]` et ajouter un bouton sur la droite `[■]` pour maximiser la fenetre — il faudra mettre le readme a jour, ainsi que la doc". Then "va au bout du bout". One option chosen up front: the maximise box **toggles**, and its **symbol changes with the state** (`[■]``[▬]`).
186+- **Changes**: `internal/ui/window.go``closeBoxLabel` is now `[x]`; new `maximizeBoxLabel`/`restoreBoxLabel`/`boxWidth`/`maximizeOffset`/`numberOffset`; `OnMaximize func()`; `Maximized()`, `Maximize(area)`, `Restore()`; `followDesktop` and `place` split out of the resize path; the window number moved from `W-4` to `W-6` and the title's reserved margin from 10 to 11 columns. `internal/ui/desktop.go``Maximize` became **`ToggleMaximize`**, `Add` wires `OnMaximize`, `Tile`/`Cascade` now use `place`. `internal/app/actions_view.go``MaximizeWindow` toggles.
187+- **Decisions**: the box **shows its action, not the window's state** — a fixed symbol is ambiguous exactly when it matters, since you can see the window is large but not what pressing the box would do. `Desktop.Add` wires `OnMaximize` rather than `app`, because the desktop is the only thing that knows the area to fill; a window not on a desktop draws **no box** rather than a dead one. `Desktop.Maximize` was **renamed** rather than left in place: keeping the name for a toggle would be a lie, and there was one caller. **Window ▸ Maximise toggles too** — a menu and a button disagreeing about "maximise" is a bug people report. Rejected: a one-way maximise (the second press does nothing visible), and a fixed `[■]` in both states.
188+- **Two holes found and closed on the way**: a maximised window kept a **stale restore rectangle** across a terminal resize, so shrinking the terminal then restoring would put the window partly off screen (`followDesktop` now carries it); and `Tile`/`Cascade` left a window calling itself maximised, so its box offered to restore to a rectangle that no longer meant anything (`place` clears it).
189+- **Tests**: 14 new in `internal/ui/maximize_test.go`, including a property test over widths 16…60 × four title lengths. `internal/ui` at 93.0 %. Run with `make test`, or `go test ./... -race`.
190+- **A test that was worthless until it was fixed**: the margin test asserted only that the close box, number and maximise box were intact after drawing — which is true whatever the margin, because `drawNumber` runs *after* `drawTitleBar` and simply repaints over the title. Verified by putting the wrong margin back: the test passed. It now asserts on the cell **beside** each piece of furniture, and with the wrong margin it fails on `"…" sits against the number`.
191+- **Verified end to end** by rendering the real binary through the project's own VT emulator (`internal/terminal`): the frame reads `╔═[x]═ main.go ═1═[■]╗`, Window ▸ Maximise fills the terminal and flips the box to `[▬]`, and a second use restores the previous size and symbol.
192+- **Quality**: PASS first time. 0 errors, 0 warnings, 0 smells, complexity 1156.
193+- **Docs**: `internal/ui/README.md` (API table, Turbo Vision details, the new methods) and the root `README.md`'s ASCII screenshot. In both languages: `reference/keyboard.md` (three mouse rows), `reference/menus.md` (Maximise is a toggle), `how-to/use-a-terminal.md` (which told people to click `[■]` to close), and a new section in `explanation/design-decisions.md`. No package was added or rewired, so `docs/diagrams/packages.drawio` is unchanged — re-checked against `go list` and still matching.
194+- **Context**: project settings were merged to `main` as PR #2 before this session; this work sits on `feature/windows-buttons`, uncommitted.
195+
196+## 2026-08-31 — Bug fix: OK did nothing in the Open dialog
197+
198+- **Goal**: user report — "quand on ouvre un fichier, dans la popup le bouton OK ne semble pas fonctionner (click souris ou focus et entree)".
199+- **Diagnosis**: `FileDialog` never wired `ListBox.OnSelect`. Highlighting a file therefore never reached the **Name** field, `Path()` returned `""`, and `confirm()` — which only closed when the path was non-empty — did nothing at all. Both routes the user tried went through `confirm()`, which is why both appeared dead. `OnSelect` itself was fine: implemented, fired by `setSelected`, and covered by its own test in `internal/ui`. It simply had no caller.
200+- **Changes**: `internal/app/dialogs.go``f.list.OnSelect = f.showSelection`; new `showSelection`, which writes the highlighted entry's name into the field and clears it for the parent entry; `confirm()` falls back to `choose()` when the field is empty, so OK is never dead; `parentEntry` extracted, replacing two copies of `".." + string(filepath.Separator)`.
201+- **Decisions**: the field **mirrors the highlight** rather than OK reading the list behind the user's back — a fallback nobody can see would let Save As write to a filename that was never shown. `../` clears the field instead of putting `..` in a box labelled "Name:", and OK then falls back to the highlight, which browses up. Construction is safe without a special case: `SetItems` calls `setSelected(0)`, which does not fire when the selection is already 0, so Save As keeps the name it opened with.
202+- **Tests**: 11 new in `internal/app/dialogs_test.go`, three of them end-to-end through the app — clicking **OK** with the mouse, `Tab` then `Enter`, and `Alt-O`. All eight behavioural ones were confirmed to fail against the original code before the fix went in. `internal/app` 84.3 % → 84.7 %.
203+- **A wrong diagnosis I had to correct mid-session**: the mouse-click test kept failing after the fix and I reported a second bug. It was my own test — `strings.Index` on a drawn row returns a **byte** offset, and a row full of `░` and `║` at three bytes each put the click about thirty columns right of the button. The helper now counts runes. There was only ever one bug.
204+- **Verified end to end** by driving the real binary through a pty and rendering it with the project's own VT emulator: `F3`, `↓`, `↓` fills the Name field with the highlighted entry, and `Tab` `Enter` opens it in a second window.
205+- **Quality**: PASS. 0 errors, 0 warnings, 0 smells, complexity 1160.
206+- **Docs**: a new "The Open and Save As box" subsection in `reference/keyboard.md` in both languages, and a paragraph in `internal/app/README.md`. No package added or rewired; the drawio diagram is unchanged and re-verified against `go list` (27 edges each side).
207+
208+## 2026-08-31 — Project tree window (ticket 0003)
209+
210+- **Goal**: "une fenêtre qui affiche un treeview du projet en cours avec possibilté de sélectionner un fichier et l'ouvrir", then "va au bout du bout". Options chosen up front: rooted at the **working directory** (the `.turbo-go` rule, not the `go.mod` walk); **hide `.git` only**; an **ordinary window**, not a docked panel; refreshed by a **key and after a save**, with no filesystem watching.
211+- **Changes**: new `internal/filetree` package — `tree.go` (the model: `Node`, `Tree`, lazy expansion, sorted listing, `Refresh`), `view.go` / `view_draw.go` / `view_events.go` (the `ui.Widget`), `README.md`, tests. Four `tree.*` keys in `internal/theme/keys.go`, all three shipped themes and the completeness test. New `internal/app/tree.go`, plus `treeWindow`/`treeView` fields, the `F9` shortcut, `Window ▸ Project tree`, a branch in `closeWindow`, and `refreshTree()` on both save paths.
212+- **Decisions**: a **window, not a panel** — a docked strip would mean `Desktop` growing reserved edges that `fitInto`, the grow modes, maximise, tile and cascade all have to respect, which is a change to the foundation of the interface for one widget; as a window it got F6, Alt-digits, `[x]`, `[■]` and Tile for free and `ui` did not change at all. **One tree at a time**, because the root is fixed at start-up and a second view would have nothing to distinguish it. **`.git` hidden and nothing else** — copying the Open dialog's hide-every-dot-entry rule would have made `.turbo-go/settings.toml` unreachable from the editor's own file browser. **No filesystem watching**: `fsnotify` would be a third dependency for a feature whose failure mode is a stale line, so the editor refreshes at the moments it can be sure of. Rejected: rooting at the `go.mod` (unpredictable in a monorepo, and the root would depend on a file three directories away), and respecting `.gitignore` (wants a pattern engine that is a feature in itself).
213+- **A theme decision forced by measurement**: the tree was going to borrow `list.*` and needed keys of its own instead. `list.selected` is coloured against a *dialog* — turbo-classic makes it white on navy while `window.body` is navy — so a tree in a window would have highlighted its selected row in the colour underneath it. Checked before writing the keys, not guessed; a test now holds every shipped theme to 64 channel values between `tree.text` and `tree.selected`, and it was confirmed to fail when `tree.selected` is set back to navy.
214+- **Tests**: 41 in `internal/filetree` (94.7 %) and 10 in `internal/app`; `internal/app` 84.7 % → 84.6 % on a larger base. Run with `make test`, or `go test ./... -race`.
215+- **Verified end to end** by rendering the real binary through the project's own VT emulator: `F9` lists the project with `.git` hidden and `.turbo-go`/`.gitignore` shown, directories first; three `→` presses nest two levels with the right markers; `Enter` opens `internal/app/app.go` into a third window with its content.
216+- **Quality**: PASS first time. 0 errors, 0 warnings, 0 smells, complexity 1228.
217+- **Docs**: three new pages per language — `how-to/browse-a-project.md`, `reference/project-tree.md`, `explanation/project-tree.md` — and updates to both indexes, `reference/keyboard.md`, `reference/menus.md`, `reference/themes.md`, `how-to/write-a-theme.md` and `explanation/architecture.md`. New `internal/filetree/README.md`; `internal/app/README.md` and `internal/theme/README.md` brought back in sync. `docs/diagrams/packages.drawio` gained `filetree` and four edges, re-checked against `go list` — 31 edges each side.
218+
219+## 2026-08-31 — Markdown, JavaScript, HTML and shell colouring (tickets 0009, 0013, 0014)
220+
221+- **Goal**: "ajoute le support des syntaxes markdown, javascript, html et bash", then "va au bout du bout". Options chosen up front: **five new classes** (heading, tag, attribute, emphasis, link) rather than reusing the twelve; **readable depth** rather than the hard tail; a Markdown fence **not** coloured in its announced language; recognition by **extension plus shebang**.
222+- **Changes**: five `Class` values and five `KeySyntax*` keys, set in all three shipped themes and in the completeness test. New `internal/syntax/scanner.go` — a shared `lineScanner` plus `scanLines`, `takeQuoted`, the block-comment helpers and the rune predicates — and the **TOML scanner ported onto it**. New `markdown.go`, `markdown_inline.go`, `javascript.go`, `html.go`, `bash.go`. `LanguageOf(path)` became `LanguageOf(path, firstLine)` with shebang detection; the two call sites in `internal/editor` pass `buf.Line(0)`.
223+- **Decisions**: **no general engine** — no pattern language, no grammar format; each scanner is ordinary Go sharing only a line, a position and the spans so far, so adding a language means writing one rather than learning a notation. **A scanner guesses nothing**: JavaScript regex literals, shell heredocs, JavaScript inside `<script>` and the language of a Markdown fence are all absent, each because recognising it needs more than one line holds and a wrong guess is louder than no guess. **Five new classes** because a heading is not a keyword and a tag is not one either; the cost — third-party themes falling back to `default` — was accepted and is documented. The shared scanner **touched working TOML code**, which was flagged before starting; its 24 tests were the net and stayed green throughout.
224+- **Two defects in my own new code**, both caught by tests: `finishTemplate` coloured nothing because a helper had already run the position to the end of the line and `emit` drops empty spans — the same trap the TOML scanner produced once before, in a different shape; and the shell scanner split `-eu` into an operator and a word, so every option in every script was arithmetic.
225+- **One defect no test could catch**: `syntax.link` was lime in `turbo-classic`, the exact colour of `syntax.string`, so a Markdown link and an inline `code` span were indistinguishable. Found by rendering the real editor through the project's own VT emulator and reading back the foreground colour of every run.
226+- **A weakness found in an existing test**: the theme completeness test only constrains `turbo-classic`, because the other two inherit from it and inheritance is resolved at parse time. Deleting a key from a child passes; from the base, it fails. Recorded rather than changed.
227+- **Tests**: 180 in `internal/syntax` (96.0 %), covering each language's constructs, its multi-line carries, its stated omissions, and that every span stays inside its line. Run with `make test`, or `go test ./... -race`.
228+- **Verified end to end** by rendering the real binary through the project's own VT emulator on one file per language, reading back the foreground of each run: Markdown headings, emphasis, code and links; JavaScript keywords, template literals, builtins and hex numbers; HTML tags, attributes, entities and comments; shell builtins, options, and expansions inside double-quoted strings.
229+- **Quality**: PASS after one round. Four smells appeared (`Highlight` and `Language.String` many-returns, `markdown.go` file complexity, an `html.go` binary expression) and were refactored away with two dispatch tables, a file split and a named character set. 0 errors, 0 warnings, 0 smells, complexity 1389.
230+- **Docs**: a new `reference/languages.md` in both languages, giving each scanner's exact boundary; rewritten "the other five languages" and a new "five classes Go has nothing to say about" section in `explanation/colouring-and-completion.md`; updates to both indexes, `reference/themes.md` and `how-to/write-a-theme.md`. `internal/syntax/README.md` rewritten for the new surface. No package added, so `docs/diagrams/packages.drawio` is unchanged and still matches `go list` (31 edges each side).
231+
232+## 2026-08-31 — Snippets, and one level of submenus in the menu bar (ticket 0006)
233+
234+- **Goal**: "un système de snippets qui seraient dans un fichier toml dans ./turbo-go, on aura un menu principal Snippets dont les sous menus seront construits à partir du contenu de snippets.toml — le snippet sélectionné est copié au niveau du fichier ouvert à l'endroit du curseur", then mid-work: "tu genereras un fichier de snippets par defaut si ils n'existent pas a partir du menu", then "va au bout du bout". Options chosen up front: `.turbo-go/snippets.toml` **plus a user-level file**; **real nested submenus**, not flat items; a **flat `[[snippet]]` list** with a `group` field and a `languages` filter; insertion **re-indented** to the cursor's column.
235+- **Changes**: `ui.MenuItem.Items` and `ui.Menu.OnOpen`, with the submenu's state, geometry, drawing, keyboard and mouse; `menu.go` split into `menu.go` / `menu_draw.go` / `menu_events.go` / `submenu.go`. New `internal/snippets` package (`snippets.go`, `create.go`, `README.md`, tests). `editor.InsertSnippet` plus `indentContinuationLines` and `leadingWhitespace`. New `internal/app/snippets.go` and the `Snippets` menu on the bar.
236+- **Decisions**: **one level of nesting**, because the format is groups → snippets and a general depth would mean replacing the bar's two indices with a path in the widget every dialog depends on. **`Menu.OnOpen`** rather than rebuilding the bar each loop turn: the contents depend on a file that changes and on the front window, so there is no start-up moment at which they exist, and OnOpen runs at exactly the moment they are about to be seen. **Two files, project wins on a clash**, mirroring how `-theme` beats a project setting which beats the built-in default. **Re-indented insertion**, because an `if err != nil` is inserted inside something by definition and a feature whose output needs fixing every time saves nobody anything. **An unreadable file is a greyed line in the menu**, not silence, because silence looks exactly like having no snippets and sends you to create a file you already have. Rejected: flat items with greyed group captions (thirty snippets give a menu taller than the terminal), and placeholders/tab stops (a second feature with its own state).
237+- **Two defects found by driving the real binary**, neither of which any test caught: **`Alt-S` opened Search, not Snippets** — both labels claimed S and the bar answers the first match, so the new menu was unreachable from the keyboard; the label is now `S~n~ippets` and `TestNoTwoMenusShareAHotKey` holds the line. And a submenu **wider than the terminal** could not be made to fit by flipping it left, so the width is capped and long labels are clipped.
238+- **Tests**: 19 for submenus in `internal/ui` (93.3 %), 19 in `internal/snippets` (83.8 %), 12 for insertion in `internal/editor` (96.2 %), 16 in `internal/app` (84.8 %). Run with `make test`, or `go test ./... -race`.
239+- **Verified end to end** by rendering the real binary through the project's own VT emulator: `Alt-N` opens the menu, `Enter` on **Create snippets file** writes and opens `.turbo-go/snippets.toml`, `Alt-N` then `→` opens the **Go** submenu (which flipped to the *left* for want of room), and `Enter` on **table test** inserted a four-line snippet correctly indented on the tab of the line it landed on.
240+- **Quality**: PASS after two rounds. One smell — `menu.go` file complexity, 46 before this work and 69 after — was fixed by splitting the file the way `terminal` and `filetree` are already split, first pulling out `submenu.go` (69 → 56, still over) and then `menu_draw.go` and `menu_events.go`. 0 errors, 0 warnings, 0 smells, complexity 1480.
241+- **Docs**: three new pages per language — `how-to/use-snippets.md`, `reference/snippets.md`, `explanation/snippets.md` — plus a Snippets section in `reference/menus.md`, submenu keys in `reference/keyboard.md`, and updates to both indexes and `explanation/architecture.md`. New `internal/snippets/README.md`; `internal/ui/README.md` and `internal/editor/README.md` brought back in sync. `docs/diagrams/packages.drawio` gained `snippets` and three edges — including `app → syntax`, which the programmatic check against `go list` caught and I had missed.
242+
243+## 2026-08-31 — The Go menu: format, lint, build, test, run (ticket 0017, partly)
244+
245+- **Goal**: "ajouter les commandes go qui permettent de lancer un formatage, le lint, le build, le lancement de tests, le run", then "va au bout du bout". Ticket 0017 asked for more than the message — a regenerable TOML file of commands — and the user chose that: a `tools.toml` **initialised with the five**. Other options chosen up front: output **in a terminal window**; the commands `gofmt -l -w .`, `go vet ./...`, `go build ./...`, `go test ./...`, `go run .` over the whole module; a **new `Go` menu** on `Alt-G`.
246+- **Changes**: `terminal.Options.Args` to run one command rather than a shell, and `terminal.ViewOptions` carrying `Name`/`OnChange`/`OnExit`. New `internal/tools` (`tools.go`, `create.go`, `README.md`, tests) and `internal/projectfile` (`projectfile.go`, `README.md`, tests). `Buffer.Reload` plus `ErrModified`. New `internal/app/gotools.go` with the `Go` menu, `RunTool`, and `reloadAfterTools`; `createProjectFile` extracted in `project.go`. `settings`, `snippets` and `tools` all now write through `projectfile`.
247+- **Decisions**: the five commands are **the starter file's contents, not code**`go vet` is the default only because it ships with the toolchain, and a project with a `Makefile` wants `make check`; changing one is editing a file. **No user-level tools file** (unlike snippets): a global one would offer `go build` in a Rust repository. **A terminal window, not a captured pane**, because a pipe costs `go test`'s colours, `go build`'s paging, `go run`'s keyboard and `Ctrl-C`. **Files a command rewrote are re-read, unless modified**: `Format` rewrites the file in front, and without this the next `F2` writes the unformatted version back over gofmt's work — but a modified buffer is left alone and named, because the edit and the formatter genuinely disagree and the editor is not in a position to decide. Rejected: hardwiring five items (wrong within a week), and splitting an argv instead of `sh -c` (would mean inventing quoting rules for a hand-written string).
248+- **A data race that predated this work and that this work exposed.** `terminal.NewView` starts the reading goroutine, and both callers then assigned `OnChange`/`OnExit`. `-race` never caught it across the whole terminal and snippets features, because a shell takes longer to produce its first output than an assignment takes to run; `sh -c "echo x"` closed that window and the detector fired immediately. Fixed structurally with `ViewOptions` rather than with a mutex, so the race is impossible rather than guarded.
249+- **A second defect found by driving the binary**: a finished command's window could not be closed with `Ctrl-W`. The view consumed every key and wrote it to the dead shell, where the write failed silently and the key was consumed anyway — the mouse was the only way out. A finished view now takes only the scrolling keys.
250+- **Tests**: 4 for `Args`/`Name` and 3 for `Exited` in `internal/terminal` (95.9 %), 12 in `internal/tools` (93.1 %), 7 in `internal/projectfile` (75.0 %), 8 for `Buffer.Reload` (95.8 %), 10 in `internal/app` (85.0 %). Run with `make test`, or `go test ./... -race`.
251+- **Quality**: PASS after one round. Four smells, all real duplication: `CreateTools`/`CreateSnippets` were the same dance (extracted as `createProjectFile`, which `CreateProjectSettings` now uses too), and the atomic TOML write existed in **three** copies (extracted as `internal/projectfile`; `internal/buffer`'s was left alone because it preserves an existing file's mode, which is a different operation). Complexity went **down**, 1528 → 1513. 0 errors, 0 warnings, 0 smells.
252+- **Verified end to end** by rendering the real binary through the project's own VT emulator: `Alt-G` on a project with no tools file offers only `Create tools file`; `Enter` writes and opens it; `Alt-G` then shows the five; `T` runs `go test ./...` in a window titled with the command; and `F` on a deliberately misformatted file ran `gofmt -l -w .`, after which `Ctrl-W` closed the command window and the editor showed the **reformatted** file — the reload working.
253+- **Docs**: three new pages per language — `how-to/run-go-commands.md`, `reference/go-tools.md`, `explanation/go-tools.md` — plus a Go section in `reference/menus.md`, `Alt-G` in `reference/keyboard.md`, a "after the program has gone" section in `reference/terminal.md`, and updates to both indexes and `explanation/architecture.md`. New `internal/tools/README.md` and `internal/projectfile/README.md`; `internal/terminal`, `internal/buffer` and `internal/app` READMEs brought back in sync. `docs/diagrams/packages.drawio` gained `tools` and `projectfile` with five edges, re-checked against `go list` (39 edges each side).
254+
255+## 2026-08-31 — Go tools: configurable output, and a popup by default
256+
257+- **Goal**: "finalement je préfère pour les tools go que la sortie ne soit pas dans un terminal mais dans une popup / pour les autres tools il faudra prévoir de définir le type de sortie: popup, terminal, editeur". A revision of the uncommitted work from earlier the same session, not a layer on it. Options chosen up front: the popup **opens immediately and fills in** (modal, Escape stops the command); the **exit code always in the title** with `(no output)` for a silent success; `editor` means **an ordinary editable window**; and `Run` stays `terminal` in the starter file while the other four are `popup`.
258+- **Changes**: `tools.Output` with `OutputPopup`/`OutputTerminal`/`OutputEditor`, `Tool.Where()`, validation that refuses an unknown value, and the starter file naming `output` on all five. New `internal/tools/run.go``Start`, `Run`, `Lines`, `Done`, `Dropped`, `Stop` — plus build-tagged `group_unix.go`/`group_other.go`. `NewOutputDialog` in `internal/app/dialogs.go`. `internal/app/gotools.go` reworked into `runInTerminal` / `runCaptured`, with `toolRun`, `refreshRunningTool`, `finishRun` and `openOutputInEditor`. `App.tick` extracted from the `Run` loop.
259+- **Decisions**: **three destinations because none is right for everything** — a terminal for interactive or long commands, a popup for run-read-dismiss, an editor window for output to work through. The popup opens **immediately** because one appearing three seconds later swallows whatever was being typed then; it is modal, which is a real cost on a slow build, named in the docs, and answered by `output = "terminal"` on that tool. An **unknown `output` is refused, not corrected**: `"termnial"` falling back silently would look as though it worked. The **exit code is always in the title** because `go build ./...` succeeding is silent and a blank dialog cannot be told from one that never started.
260+- **A defect found by a flaky test, then reproduced deliberately**: `Stop()` killed only the shell, and a grandchild inheriting the output pipe left the reading goroutine blocked until it ended — 20 seconds in the suite, and for `go test ./...` it would be every test binary spawned. Fixed by killing the **process group**, with `cmd.WaitDelay` as the backstop. Verified: 11 ms after the fix, never before it.
261+- **Two vacuous tests caught and fixed**, both the same shape — the test driving the thing under test. `waitForLoopTurn` called `a.reloadAfterTools()` directly, so the test passed with that step deleted from the event loop; `App.tick` was extracted and the helper now takes a whole loop turn. And the process-group test killed the shell before it had forked; it now waits for the child to print. Both were confirmed by breaking the code they cover.
262+- **Tests**: 26 in `internal/tools` (95.0 %), 11 more in `internal/app` (85.7 %). Run with `make test`, or `go test ./... -race`.
263+- **Verified end to end** through the project's own VT emulator: `go build ./... — ok` with `(no output)`; `go vet ./... — exit 1` showing `main.go:6:2: unreachable code`; `gofmt -l -w . — ok` listing the reformatted file.
264+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1543.
265+- **Docs**: the three `go-tools` pages written earlier in the session were **revised**, not appended to — they described a terminal as the only destination. Plus the Go line in `reference/menus.md` in both languages, and `internal/tools/README.md` and `internal/app/README.md` brought back in sync. No package added, so the diagram is unchanged and still matches `go list` (39 edges each side).
266+
267+## 2026-08-31 — Fix: a reinstall produced a binary that would not run on macOS
268+
269+- **Goal**: user report — `scripts/install.sh` printing `✗ the installed binary does not run` after a clean build, on macOS (`/Users/k33g/go/bin`).
270+- **Diagnosis**: the installer used `cp "$STAGING/$BINARY" "$TARGET"`, which opens the destination with `O_TRUNC` and writes in place — **the inode is reused**. macOS caches a binary's code signature against its inode, so new bytes in the old inode leave the cached signature describing something else and the kernel refuses to execute the result. It fails on *reinstall*, not on a first install, which matches a user who had been running the editor all day. Cross-compiling and `go vet` for `darwin/arm64` and `darwin/amd64` were both clean, ruling out the code.
271+- **Changes**: `scripts/install.sh` now copies to `.turbo-go.incoming.$$` **inside `$prefix`** and `mv -f`s it over the target, so the name gets a fresh inode and the install is atomic besides; the `EXIT` trap cleans the temporary. And the verification captures the binary's own stderr and prints it: `the installed binary does not run` on its own tells nobody anything they can act on.
272+- **Decisions**: the temporary must live in `$prefix` rather than in `$STAGING`, because a rename only works within one filesystem — the same reasoning `internal/buffer` and `internal/projectfile` already follow. Rejected: `install -m 0755` (does an in-place write on some platforms, so it would not fix it), and `rm` then `cp` (leaves a window with no binary on the PATH).
273+- **Tests**: 3 added to `install_test.go` — the reinstall gives the file a **new inode**, the reinstalled binary runs, and the installer surfaces the binary's own error. The first two were confirmed failing against the `cp` version before the fix. `install_test.go` is now 13 tests.
274+- **Verified end to end** on Linux: inode 529596 → 529598 across a reinstall of a binary that had been executed in between, no temporary left in the prefix, and the error path shown to carry the system's message.
275+- **Not verified on macOS**, and said so plainly to the user: this sandbox is Linux, so the diagnosis rests on the symptom matching a known failure mode rather than on a reproduction. The improved error message is what makes a wrong diagnosis recoverable.
276+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1543 — unchanged, the fix is in a shell script.
277+- **Docs**: a new "When something goes wrong" section in `how-to/install.md` in both languages, covering the three failures the installer can report, and the script's own header comment brought back in line with what it does.
278+
279+## 2026-08-31 — Terminal output that never appeared, and tools in menus of their own
280+
281+- **Goal**: two user reports in one message. `output = "terminal"` with `command = "echo 'TADA'"` showed only the title and no output; and "je voudrais pouvoir faire la différence entre les tools go et d'autres tools qui iraient dans un menu tools". Options chosen up front for the second: a **free-form `menu` key** on the tool (absent → Go, arbitrary names, menus in the order their first tool appears), in the **single** `tools.toml`, with the stated cost that hot keys for created menus must be assigned without clashing with the nine fixed ones.
282+- **Changes (the bug)**: `internal/terminal/screen_resize.go``rowsToDrop(previous, height, cursorRow)` replaces `max(len(previous)-height, 0)`.
283+- **Changes (the feature)**: `tools.Tool.Menu` with `MenuName()`, `List.In(menu)` and `List.MenuNames()`; `DefaultMenu`; the starter file documents the key. `ui.MenuBar.SetMenus`. New `internal/app/toolmenus.go``allMenus`, `toolMenus`, `toolMenu`, `takenHotKeys`, `hotKeyLabel`, `fileStamp`, `stampOf`, `toolsFileStamp`, `refreshToolMenus` — with `refreshToolMenus` added to `App.tick` and `App.toolsStamp` to the struct. `commandItems` took a menu name. `buildMenus` now delegates the order to `allMenus`.
284+- **The bug, diagnosed**: a terminal window is created at the default 80×24 and the first `layout()` resizes it to its frame (about 76×20). `Screen.Resize` dropped `previous-height` rows **from the top** into scrollback, so a single line of output at row 0 went with them while blank rows stayed below. Whether the output arrived before or after that resize decided whether it showed — which is exactly the intermittence reported. It now drops only as many rows as the cursor actually needs. The two tests were written first and confirmed failing; the pre-existing `TestShrinkingKeepsTheNewestLinesAndRemembersTheRest` still passes unchanged.
285+- **Decisions**: a **free-form name, not a fixed second `Tools` menu** — a Tools menu holding `docker compose up`, `psql` and a deploy script is as undifferentiated as a Go menu holding them, and rejecting the fixed menu also rejects the second file (`menus.toml`) that would have to agree with the first about which tools exist. **`Go` stays fixed** on the bar: it holds `Create tools file`, which has to be reachable in a project that has no tools file — the very project that needs it. **Hot keys are assigned, not read**: the file's author cannot know which letters are free, and a clash is silent (the bar answers the first match; the second menu draws normally and never opens) — the `Snippets`/`Search` bug from earlier in this project, made permanent. Tildes in a name are honoured **only when the letter is free**; refusing the file instead would break a working tools file the day a release adds a menu. **A `stat` per loop turn, not a parse**: `Menu.OnOpen` cannot cover a menu that does not exist yet, and parsing on every keystroke is work done for nothing.
286+- **Tests**: 7 in `internal/tools`, 2 in `internal/ui`, 16 in `internal/app` (new `toolmenus_test.go`), 2 in `internal/terminal`. Run with `make test`, or `go test ./... -race`.
287+- **Two test expectations were wrong, and the code was right** — worth recording because both are the mechanism working: `Format` gets `For~m~at`, not `F~o~rmat`, because `o` is Options'; and a menu written `T~o~ols` loses its `o` for the same reason, so the case was retested with `Doc~k~er`.
288+- **Verified end to end** by rendering the real binary through the project's own VT emulator: the bar reads `… Snippets Go Tools Docker Help` in file order; `Alt-T` drops down Echo and Date with no `Create tools file`; `Alt-T Enter` opens a terminal window titled `echo 'TADA'` **showing TADA**; `Alt-D Enter` gives the popup `echo docker — ok` showing `docker`; and `Alt-G` still holds Build and `Create tools file`.
289+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1543 → 1567.
290+- **Docs**: `reference/go-tools.md` gained the `menu` key, a Go-menu section and a "Menus a tool asks for" section with the hot-key rules; `how-to/run-go-commands.md` gained "Put a tool in a menu of its own" and two variants; `explanation/go-tools.md` gained three sections (why a tool names its menu, why the hot key is not the file's, why the bar is rebuilt from a stat); `reference/menus.md` gained "Project menus"; `reference/keyboard.md` a note — all in **both languages**. `internal/tools`, `internal/app` and `internal/ui` READMEs and the root README brought back in sync. No package added, so the diagram is unchanged and still matches `go list`.
291+- **Then, on request**: `demo/.turbo-go/tools.toml` brought up to date — **regenerated from the `template` constant** in `internal/tools/create.go` rather than hand-edited, so it cannot drift from the generator again, with the user's `~E~cho` tool appended under `menu = "Tools"`. Its header had still been describing a terminal as the only output destination, two revisions after that stopped being true.
292+- **Merged by the user** as PR #7 (`88a4c38`), `feature/go-format-lint` into `main`, who also closed tickets 0004 and 0017.
293+
294+## 2026-08-31 — The version in the About box comes from the build
295+
296+- **Goal**: "je voudrais que lorsque l'on fait une release, la version apparaisse dans la fenêtre about" — ticket 0010. The stated request was already half true: About *did* show a version. It showed `0.1.0` from `const Version` in `internal/app`, on a checkout fourteen commits past `v0.1.0`. Said so up front and treated the real goal as making the number true. Options chosen: **ldflags plus BuildInfo with a fallback**; a non-release build shows `git describe` output; About also carries the **commit** and the **build date** (not the Go version); **no `make release` target** — stamping only.
297+- **Changes**: new `internal/version``Info{Number, Commit, Built}`, `Current`, `String`, `BuiltAt`, and a `resolve` split out so every case is testable, plus `isPseudoVersion`. `app.Version` deleted; `Name` kept. `aboutText(info, themeName)` extracted as a pure function in `actions_view.go`. `main.go` prints `version.Current()`. `Makefile` gained `VERSION`/`COMMIT`/`BUILT`/`LDFLAGS`, a stamped `build`, and a `version` target. `scripts/install.sh` stamps the same way, with a path for a checkout git cannot describe.
298+- **Two discoveries that changed the design mid-way, both from running the thing rather than reasoning about it.** Go 1.26 does **not** report `(devel)` for a plain `go build .` in a checkout: it reports a **pseudo-version**, `0.1.1-0.20260831165958-88a4c3859bf3+dirty`. Unreadable in a dialog, and its `0.1.1` is a patch release that does not exist — so pseudo-versions are recognised and reported as `devel`. Then the first recogniser was wrong: the character before the timestamp is a **dot**, not a dash, whenever a base tag precedes the commit (`-0.` / `-pre.0.`). Caught by writing the three forms into a test and watching three of four fail.
299+- **Decisions**: `unknown` rather than a fallback constant, because a plausible-looking version nobody set is the exact defect being removed. `vcs.time` deliberately unused — it is the commit's timestamp, so "Built" would be false on every binary. About **omits a line whose fact is empty**; `go install …@v0.2.0` records a version and no VCS information at all. `resolve` takes its four inputs as arguments because a test binary cannot be built into having linker stamps.
300+- **Tests**: 17 in `internal/version` (98.3 %), 3 for `aboutText` in `internal/app` (86.3 %), 3 in `install_test.go` (now 16). The existing About test only counted modals; it was left in place and joined by tests that read the text. Run with `make test`, or `go test ./... -race`.
301+- **Verified end to end** through the project's own VT emulator, in both states: a build stamped `v0.2.0` shows `Turbo Go 0.2.0` with `Commit: 88a4c38` and `Built: 2026-08-31 18:04 UTC`; an unstamped build shows `Turbo Go devel-dirty` with the commit line and **no** `Built:` line and no gap where it would have been. `scripts/install.sh` reports `Turbo Go 0.1.0-14-g88a4c38-dirty (88a4c38, built …) → /tmp/tgbin/turbo-go`.
302+- **Quality**: PASS after one round. One real smell — a four-term boolean in `isPseudoVersion` — fixed by splitting out `hasPseudoTail` and `withoutBuildMetadata`, which reads better than what the linter complained about. 0 errors, 0 warnings, 0 smells, complexity 1567 → 1592.
303+- **Docs**: two new pages per language — `reference/versioning.md` and `how-to/make-a-release.md` — plus a section in `explanation/design-decisions.md`, the `-version` row in `reference/cli.md`, the About row in `reference/menus.md`, a note in `how-to/install.md`, and both indexes. New `internal/version/README.md`; `internal/app/README.md` and the root README brought back in sync. `docs/diagrams/packages.drawio` gained the `version` node with two edges, re-checked against `go list`: 41 drawn = 29 internal + 12 third-party, none missing, none stale.
304+
305+## 2026-08-31 — Fix: the release script, broken by removing the version constant
306+
307+- **Goal**: user report — `./03-build-releases.sh` failing with `❌ v0.2.0 does not match the binary, which reports 2026-08-31T18:59:13Z)`. Their own release tooling, broken by the version work merged as PR #8 earlier the same day.
308+- **Two defects, and the one they saw was the smaller.** The script read the version with `awk '{print $NF}'`, which took the last field of `Turbo Go 0.2.0 (7f8b36a, built 2026-08-31T18:59:13Z)` — a timestamp. But the cross-compile loop called `go build -trimpath` with **no `-ldflags` at all**, so all five downloadable binaries would have reported `devel` while the release page announced v0.2.0. Before the constant was removed it travelled into cross-builds; afterwards nothing did, and only the host binary was stamped — so nothing but a hand check would ever have caught it. Reproduced both before changing anything.
309+- **Changes**: `Makefile` gained an `ldflags` target that prints `$(LDFLAGS)`, so the stamp is defined once. `03-build-releases.sh` reads it, passes it to every cross-compile, and runs the staged binary for the host platform before declaring the release built. Its version check was replaced by three plain ones — `git describe --tags --exact-match` equals `TAG`, `git diff --quiet HEAD` is clean, and `grep -F` finds the version in the binary — none of which parses prose. The stale hint "Update Version in internal/app/app.go" named a constant that no longer exists and is gone.
310+- **Decisions**: the script asks **git** rather than the binary wherever it can, because `-version` is written for a person and has already changed shape once. `grep -F` rather than a field, for the one thing only the binary knows. A **dirty-tree check** was added, not asked for: `git describe --dirty` would otherwise stamp `v0.2.0-dirty` into binaries staged in a directory named `v0.2.0`, which is the same class of mismatch the script exists to prevent. Rejected: adding a machine-readable `-version-number` flag — new public surface, two languages of documentation, for a problem `grep -F` solves.
311+- **Tests**: new `release_test.go`, 5 tests — the cross-compile carries `-ldflags`, the flags come from the Makefile and are not respelt, both git checks are present, no field is read out of `-version`, and `make ldflags` really does stamp a binary that then reports what `git describe` says. The first failed before the fix.
312+- **Verified end to end** in a throwaway clone, so no tag was created in the user's repository: a full run stages five binaries and reports `✅ turbo-go-0.2.0-linux-arm64 reports 0.2.0`; and each guard was made to fire — untagged HEAD, HEAD tagged `v0.9.9` against `TAG=v0.2.0`, and a dirty tree — each with an actionable message.
313+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged; the fix is in a Makefile and a shell script.
314+- **Docs**: `reference/versioning.md` gained `make ldflags` and a note that `-version` is not a machine interface; `how-to/make-a-release.md` gained the numbered-scripts section and the three checks — both languages. `internal/version/README.md` and `.memory/summary.md` record why a cross-compile is the case that hides this.
315+
316+## 2026-08-31 — Fix: 01-release.tag.sh pushed a stale tag in silence
317+
318+- **Goal**: user report — `./03-build-releases.sh` refusing with `❌ HEAD carries no tag, so nothing built here can report v0.2.0`, then "fixe moi ca". The refusal was correct; the message was not, and the cause was in a different script.
319+- **Diagnosis**: `01-release.tag.sh` had **no `set -e`**. Their first run tagged `v0.2.0` at `7f8b36a`. The second run's `git add . && git commit` created `78ea819`, then `git tag -a v0.2.0` failed with "already exists" — ignored — and the `git push origin "${TAG}"` after it pushed the **old** tag. `git describe` then read `v0.2.0-1-g78ea819` and `03` refused. So the release builder was reporting a fault three steps upstream of itself.
320+- **Changes**: `01-release.tag.sh` gained `set -euo pipefail`; a `tagExists` check covering **both** the local ref and `git ls-remote origin` (a tag deleted locally after a failed attempt still exists on the remote, and a fresh one at another commit is then rejected); a guard so that having nothing to commit is not a failure, which `set -e` would otherwise have made one; and the tag now goes on **after** the push, so a rejected push leaves no stray tag. `03-build-releases.sh` gained a three-way diagnosis — HEAD tagged something else, the tag exists but HEAD has moved N commits past it, or no such tag — each naming the fix.
321+- **Decisions**: the remote is consulted for the tag as well as the local ref, because the state the user was actually in (local tag deleted, remote tag possibly still there) is invisible locally. No commit SHA is printed for a remote tag: `ls-remote` returns the *tag object* for an annotated tag, and printing it as the commit sends the reader after a SHA that does not exist. **`02-release.publish.sh` was left alone and reported instead** — it has the same missing `set -e`, but its `read -r -d '' DATA` idiom always exits non-zero, so adding one naively would kill the script at its first line; and it POSTs to Codeberg, which is not mine to change unasked.
322+- **Tests**: 5 more in `release_test.go` (now 10) — `01` stops on failure, checks both refs, survives an empty commit, tags only after pushing, and `03` can say how far HEAD is past the tag.
323+- **Verified end to end** in throwaway clones, one with a local bare remote, so no tag was created in the user's repository: `03` was made to print each of its three diagnoses, and `01` was run on the happy path (tagged HEAD, pushed), then again to see it refuse a tag now present only on origin.
324+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged; the fix is in two shell scripts.
325+- **Docs**: a "When the scripts refuse" table in `how-to/make-a-release.md`, both languages, one row per message with its cause and its fix.
326+
327+## 2026-08-31 — Simplify: a release build stamps the tag, and stops checking
328+
329+- **Goal**: user, after the release builder refused a third time — "fais quelque chose de plus simple, tu build comme avant avec le tag de release". A correction of my own two previous turns, not a new feature.
330+- **What was wrong with my design**: I had `03-build-releases.sh` stamp `make ldflags` (derived from `git describe`) and then *verify* that it agreed with `TAG` — HEAD tagged exactly, tree clean, binary reporting the version. Three gates, each defensible on its own, and together they blocked a release for conditions that were not actually errors. `git describe` answers "where is HEAD", which is a different question from "what release is this", so the gates existed only to reconcile an answer I should not have been asking for.
331+- **Changes**: `03-build-releases.sh` now stamps `TAG` directly — `make ldflags VERSION="${TAG}"` and `make build VERSION="${TAG}"` — and the three gates are gone. The Makefile needed nothing: a command-line `VERSION=` already overrides the `:=` default. The one check kept is the staged binary for this machine reporting the version, which proves the artefact rather than the intent.
332+- **Decisions**: the release **is** `${TAG}`, so the binaries say `${TAG}`; the whole class of "describe disagrees with TAG" stops existing rather than being detected. Building no longer requires the tag to exist — only `02-release.publish.sh` does, which is the step that genuinely needs it. `01-release.tag.sh`'s guards were **kept**: they are about not pushing the wrong tag, and they block no build.
333+- **Tests**: two removed with the behaviour they covered — deliberately, at the user's decision, not to make anything pass — and two added: the script stamps `VERSION="${TAG}"` for both the host build and the cross-compiles, and `make ldflags VERSION=v9.9.9` really does produce a binary reporting 9.9.9. `release_test.go` is 9 tests.
334+- **Verified end to end** in a throwaway clone with **no tag at all and a dirty tree** — the situation that had been refused — five binaries staged and `✅ turbo-go-0.2.0-linux-arm64 reports 0.2.0`.
335+- **Quality**: PASS. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged.
336+- **Docs**: the "When the scripts refuse" table removed along with the refusals; `how-to/make-a-release.md` and `reference/versioning.md` rewritten around the override, both languages.
337+- **Lesson worth keeping**: three turns were spent adding checks to reconcile two sources of truth, when the fix was to have one. The user saw it before I did.
338+
339+## 2026-08-31 — Three themes: cappuccino, cobalt, monochrome
340+
341+- **Goal**: `/methodical-dev` — "ajouter un theme cappucino, un theme cobalt, un theme monochrome" (ticket 0012). Options chosen up front: cappuccino **dark** (espresso, not cream); monochrome **pure grey**, no phosphor tint; cobalt **faithful to the recognised palette**; and yes to closing the silent-inheritance trap.
342+- **Changes**: three new files under `internal/theme/themes/``cappuccino.toml`, `cobalt.toml`, `monochrome.toml` — each stating all 67 style keys. No Go code changed: themes are embedded by `//go:embed themes/*.toml`, so adding one is adding a file. Three new tests.
343+- **A measurement changed the design of a test I had already promised.** The plan was "every colour legible on its own background". Probing the six themes first showed the weakest pairs are all *deliberately* faint furniture — scrollbar trough at 20, desktop, shadow, inactive frame, disabled entry, line-number gutter, between 20 and 70 in every theme including the two oldest. A blanket rule would have flagged six correct keys. Narrowed to the keys whose job is to be read, the measured floor is **80** (turbo-dark's `syntax.comment`), so the threshold is **64**: a quarter of the range, 16 below the present, a guard against regression rather than a description of today.
344+- **Decisions**: shipped themes state their palette in full, **user themes may still inherit** — the rule is about what the project is answerable for, not about how a theme should be written, and the how-to still recommends inheriting (now with the advice to inherit from a theme whose ground matches yours). Monochrome distinguishes syntax by weight and slant rather than hue, which is what makes it useful on a projector and to a reader who cannot separate the red from the green. Cobalt's accents were left as loud as the palette is known for rather than muted into house style: a theme called Cobalt that is not that blue is a different theme with a borrowed name.
345+- **Tests**: `TestEveryEmbeddedThemeSetsEveryKeyItself` in `internal/theme`; `TestEveryThemeKeepsItsTextReadable` and `TestEveryThemeTellsAdjacentSyntaxClassesApart` in `internal/editor`, beside the two cursor rules that were already there. **All three were falsified before being trusted** — a deleted `syntax.tag`, a `#2a2a2a` comment, and a cobalt link set to the string green, each producing the expected failure. The five existing theme tests now run over six themes.
346+- **Verified end to end** through the project's own VT emulator: each new theme renders the editor intact, and the real Theme dialog was opened to confirm the list is six entries alphabetically — which **broke the tutorial**, whose "press ↓ to move to turbo-dark" had become five presses. Fixed in both languages with the list written out.
347+- **Quality**: PASS, no refactoring needed. 0 errors, 0 warnings, 0 smells, complexity 1592 — unchanged, the themes are data.
348+- **Docs**: a "themes that ship" table in `reference/themes.md`, the embedded list, a new step 5 "check it stays readable" and the inherit-from-a-similar-ground advice in `how-to/write-a-theme.md`, the tutorial's theme step, `internal/theme/README.md`, and the root README — both languages throughout. The root README's docs index was also brought back in sync: it had been missing `make-a-release` and `versioning` since the previous session. No package added, so the diagram is unchanged and still parses.
349+
350+## 2026-09-01 — Migrated onto turbo-core, and a second editor exists
351+
352+- **Goal**: ticket 0001 in the `turbo-editors` parent — extract the code shared with a future Turbo Rust into a versioned library, and build that second editor. Options chosen by the user before implementation: the library holds `app` too, so an editor is a command, a profile and a scanner; the language scanner lives in its own editor; the editors depend on the library with `require` plus a committed `replace`; per-editor configuration directories rather than a shared `.turbo/`; the Rust toolchain menu is `Rus~t~` on Alt-T.
353+- **Changes here**: `internal/*` deleted — fourteen packages moved to `codeberg.org/turbo-editors/turbo-core` and made public. New `internal/golang`: the profile, the Go scanner (recovered from `internal/syntax/scan.go` and ported onto the library's exported `Class`, `LineIndex` and `Register`), and the three starter templates. `main.go` rewritten around `golang.Profile()`; `moduleRoot`/`projectRoot` replaced by `app.ProjectRoot`. `Makefile` and `scripts/install.sh` stamp `turbo-core/version` instead of `internal/version`.
354+- **Nothing about the editor's behaviour changed.** The menus, the keys, the themes, the file formats and the environment variables are what they were. `TURBO_GO_THEME_DIR` still works, deliberately: it is derived from the profile's slug precisely so a released name is preserved.
355+- **Decisions**: the Go scanner stays here rather than in the library, so that "what does this editor register?" is the first question about a new editor — a `.rs` file therefore opens as plain text here. `golang.Register()` is called from `main` explicitly, not from an `init`, so the fact is a line somebody can read. The `replace` is committed rather than hidden in a gitignored `go.work`, so it is visible in the diff and so three repositories side by side build with nothing published.
356+- **Tests**: the whole existing suite passes unchanged. Twelve Go-scanner tests came back here from the library, along with the three templates' content tests and both real-gopls tests — the ones that are about *Go*, and that the library has no language server of its own to run. New `internal/golang/editor_test.go` builds a whole Turbo Go on a simulated terminal through the library's public API and checks that Register was called, that the profile reached the menu bar, and that a `.go` file comes out coloured; a bug where `main` forgot to register Go would pass every test in turbo-core.
357+- **One pre-existing test was fragile and is fixed.** `TestTheMakefileHandsOutTheFlagsThatStampABuild` asserted the stamped binary did not say `devel`, which is only true in a checkout that has tags. It now compares against `make version`, so it holds in a fresh clone too — and the comparison strips the leading `v`, because `internal/version` does.
358+- **Quality**: PASS. 0 errors, 0 warnings, 0 smells, complexity 1592 → 37. The fall is the code moving, not anything being simplified; turbo-core carries 1584 of it.
359+- **Docs**: `explanation/architecture.md` rewritten in both languages around the split, with a table of what moved where; `colouring-and-completion.md` updated to say which scanners are shared; every `internal/version` path corrected in `reference/versioning.md`, `how-to/make-a-release.md` and `how-to/run-the-tests.md`; the root README given a "Where the code is" section; `docs/diagrams/packages.drawio` regenerated from `go list` and verified against it edge for edge.
360+- **A pre-existing documentation defect was found by driving the real binary**, and fixed: the tutorial said "press ↓ five times to reach turbo-dark" when the Theme dialog has always opened *on the current theme*, so it was one press. Both languages now say one, and say why.
361+- **Two themes were added** at the user's request during the same session — `catppuccin-frappe` and `catppuccin-latte`, in turbo-core — which is what made the tutorial's arrow count worth checking rather than merely updating.
362+
363+## 2026-09-01 — Tool parameters, from turbo-core
364+
365+- **Goal**: part of the same request as turbo-core's entry of this date — a tool whose command needs a value must be able to ask for it. The feature is the library's; what changed here is the starter file people are given.
366+- **Changes**: `internal/golang/templates.go` — the tools template's comments now teach `{{label}}` and `{{label...}}`, with an example for THISgolang and the warning about single braces. `install_test.go` — one test asserted before checking whether it was in a git checkout at all, so it failed in a tree with no `.git` where `unknown` is the correct answer.
367+- **Decisions**: the examples go in the **comments**, not as a sixth tool. The five starter commands are what a project runs before it commits; `go mod init` is a different kind of thing, and adding it would change what `Create tools file` gives everybody in order to demonstrate a syntax.
368+- **Tests**: 2 in `internal/golang/templates_test.go` — the created file teaches the syntax, and none of the five starter commands accidentally became parameterised by the prose around them.
369+- **Quality**: PASS. 0/0/0, complexity 37 — unchanged; the change is comments and a test.
370+- **Docs**: a section in `reference/go-tools.md`, one in `how-to/run-go-commands.md` and one in `explanation/go-tools.md`, both languages.
371+
372+## 2026-09-01 — Released as v0.2.2
373+
374+- **Goal**: the user committed and released everything and asked for the record to be brought up to date. This entry is what was verified, not what was intended.
375+- **Verified from the repository and the Codeberg API**: **v0.2.2** at `d64410c`, which is exactly HEAD, with a release page. Working tree clean, on `main`. The tag is the fourth for this editor and the first since it moved onto the library.
376+- **The dependency is the published library**: `require codeberg.org/turbo-editors/turbo-core v0.1.0` with no active `replace`, and a `go.sum` whose checksum matches sum.golang.org. A clean clone now builds without turbo-core beside it, which is what the whole extraction was for.
377+- **One wart, left alone deliberately**: the old replace block is commented out rather than deleted, and its comment still says "drop it once the version above is tagged and published" — which is done. It sits inside a released commit, so it was written down rather than changed.
378+- **Nothing was built or changed in this entry** — no code, no tests, no docs. The suite and the gate were last measured at the previous entry and are unchanged.
379+
380+## 2026-09-01 — Dockerfile, compose, YAML and XML colouring (ticket 8)
381+
382+- **Goal**: ticket 8 — "add syntax for Dockerfile, compose file, yaml, xml". The scanners themselves belong in turbo-core; this repository's part was to use them and to say so.
383+- **Changes**: `internal/golang/templates.go` — the snippets template's `languages` comment now lists the nine names this editor knows. `go.mod` requires `turbo-core v0.2.0`. Documentation: the YAML, XML and Dockerfile sections in `docs/{en,fr}/reference/languages.md` with the recognition and class tables brought up to date, and the language counts corrected in the architecture and colouring explanations, both READMEs, and the snippets references.
384+- **Decisions**: none taken here — the three that matter (a compose file is just YAML, XML gets its own scanner for CDATA's sake, `Filenames` matches the stem) were taken in turbo-core and are recorded there.
385+- **Tests**: `TestTheCreatedSnippetsFileListsEveryLanguageTheEditorKnows` iterates `syntax.Registered()` rather than a hardcoded list, so the template cannot fall behind the registry again. Falsified by removing a name from the template.
386+- **A stale claim found while sweeping**: the reference said themes were "the three shipped themes" when eight ship, and turbo-rust's English snippets reference listed `go` where it meant `rust`. Both fixed.
387+- **Quality**: PASS, 0 errors / 0 warnings / 0 smells, complexity unchanged.
388+- **Verified in a real pty**: a `Dockerfile`, a `compose.yaml` and a `pom.xml` opened in the built binary and coloured, with a CDATA section's contents arriving as a string rather than as markup.
389+- **Blocked on a release**: this branch does not build until turbo-core v0.2.0 is tagged and published.
390+
391+## 2026-09-01 — The build checks the version it stamped
392+
393+- **Goal**: the user asked that the build verify it really embeds the right version number.
394+- **Changes**: new `scripts/check-version.sh`, called by `make build` after linking, by `scripts/install.sh` on the staged binary **before** the install, and by `03-build-releases.sh` on the one asset this machine can run. The release script's own `grep -qF` check was replaced by it.
395+- **The failure it catches**: a linker stamp is a string, and a wrong one is not an error. `-X` naming a symbol that does not exist links happily and stamps nothing; the binary then reports whatever Go build info says — `devel`, on a binary attached to a release. Reproduced by hand: `make build LDFLAGS="-X '….version.stampX=v9.9.9'"` linked cleanly and reported `0.2.2+dirty`, and now fails the build.
396+- **Decisions**: the version comparison is an **equality**, not a search — `0.2.0` is a substring of `10.2.0` and of a commit hash that contains it, and a stamp that is nearly right is the case worth catching. The check runs **before** the install, so a binary that cannot name itself never replaces one that can. With no version to expect — a build outside a git checkout — the only claim left is that the number is not `unknown`.
397+- **Tests**: 8 in a new `version_check_test.go`, driving the script against binaries built for the purpose. Three were falsified: the wiring in the Makefile, the ordering in the installer, and the substring case.
398+- **Verified for real**: `make build`, `scripts/install.sh --prefix $(mktemp -d)`, and a deliberately misspelt `-X`.
399+- **Docs**: a "Checked at build time" section in `docs/{en,fr}/reference/versioning.md`.
400+- **Quality**: PASS 0/0/0, complexity unchanged.
401+
402+## 2026-09-01 — Tickets 9 to 14: autosave on in a created settings file
403+
404+- **Goal**: tickets 9–14. Only ticket 9 is editor-side; the other five are turbo-core's and reach Turbo Go through the library.
405+- **Changes**: `internal/golang/templates.go` — the settings template now writes `autosave = true`, with the reason in the comment above it. `.gitignore` gained `go.work`.
406+- **Decision**: the template, **not** `settings.Default()`. A project that has created a settings file has said what it wants, and the file is the visible, editable place to say otherwise. Turning the library default on would mean the editor writing to disk in any directory it is started in, which is a different and much larger claim; the user was asked and chose the narrower one.
407+- **Tests**: `TestTheCreatedSettingsFileTurnsAutosaveOn` loads the created file rather than grepping it, and `TestAProjectWithNoSettingsFileStillDoesNotAutosave` holds the other half of the decision. The first was falsified by putting `false` back.
408+- **Docs**: the settings reference gained a "When a change takes effect" section; the menus reference now states the enabled condition of all six create/open items; the tools and snippets references gained their `Open …` rows and lost "a project that already has one is opened unchanged"; `configure-a-project.md` was rewritten around autosave already being on; `run-the-tests.md` gained a section on testing against an unreleased turbo-core with `go work`. EN and FR throughout.
409+- **Quality**: PASS 0/0/0, complexity unchanged.
410+- **Verified in a real pty**: all six menu items flipping between available and greyed, and the created settings file holding `autosave = true`.
411+- **Note**: this branch builds and passes against the published `turbo-core v0.2.0`. The other five tickets only become visible once turbo-core v0.3.0 is released and the `require` here is bumped.
412+
413+## 2026-09-02 — Code navigation: documentation only
414+
415+- **Goal**: the Code menu and the eight questions it puts to the language server. All the code is turbo-core's; Turbo Go changes only by describing it.
416+- **Changes**: a new `docs/{en,fr}/how-to/ask-about-code.md`; the **Code** section in the menus reference, with Describe symbol and Go to definition removed from Run and Search; `Shift-F12` and `Ctrl-T` in the keyboard reference; a "Nine questions, one connection" section in the colouring-and-completion explanation. EN and FR throughout.
417+- **Decision**: a **separate** guide rather than an extension of `navigate-code.md`. That page answers "how do I get to the piece of code I am looking for" — searching, line numbers, windows. This one answers "what does this name mean" — a different need, so a different page, with the old one linking to it.
418+- **Docs traps met**: the new guide was first written *over* `navigate-code.md` and had to be restored from git. And the two moved menu items had to be deleted from Run and Search in **four** files, not two — the French tables are separate text.
419+- **Quality**: PASS 0/0/0, complexity unchanged.
420+- **Note**: this branch builds against the published `turbo-core v0.3.0`. Nothing here needs v0.4.0 to compile; the menu it documents appears once that is released and the `require` is bumped.
421+- **Follow-up the same day**: the user asked whether the LSP features were documented for users. They were — `how-to/ask-about-code.md`, EN and FR, both editors — but the neighbouring `enable-completion.md` still had a "what else the server gives you" section listing three keys and no mention of the Code menu, Problems, or the gutter marks. Fixed in all four files. That is the "adapting is not substituting" trap from the `turbo-new-editor` skill, met on a page I had not thought to re-read: **a new feature makes its neighbours stale, and the neighbours are where a user already is.**
422+
423+## 2026-09-02 — Ticket 19: better code editing, documentation only
424+
425+- **Goal**: ticket 19 — double-click to select a word, insert line, delete line. All the code is turbo-core's; this repository documents it.
426+- **Changes**: the keyboard and menus references in EN and FR, and a "Select and edit whole lines" section in `how-to/navigate-code.md`.
427+- **The one thing to notice**: **redo is `Ctrl-R` now, not `Ctrl-Y`**`Ctrl-Y` deletes a line, as it did in Turbo C. That is a key changing under people who had learnt it, so it is stated in the menus reference rather than only in the table of keys.
428+- **Quality**: PASS 0/0/0, complexity unchanged.
429+
430+## 2026-09-02 — Starter templates moved out of the source into embedded files
431+
432+- **Goal**: the user asked for the three starter templates to live in three files in `internal/golang/` and be embedded into the binary, instead of Go constants in `templates.go`. Extended to both editors at their choice.
433+- **Changes**: `settings.toml.tmpl`, `snippets.toml.tmpl` and `tools.toml.tmpl` beside the code; `templates.go` reduced to three `//go:embed` declarations. `profile.Templates` is unchanged — it takes strings, and an embedded variable is one, so turbo-core needed nothing.
434+- **Decisions**: **`.tmpl`, not `.toml`**, put to the user with the measurement behind it — `settings.toml.tmpl` holds `theme = %q`, which `tomllib` rejects, so naming it `settings.toml` would be a claim it cannot meet: a linter would reject it and the editor would colour it as TOML and draw it as broken. The snippets and tools templates *are* valid TOML (their verbs sit in comments), but all three take the suffix so the set is consistent. **The user accepted that the editor will not colour `.tmpl` files.**
435+- **Method**: the constants were **evaluated, not cut out of the source** — each is a concatenation of a raw string with a quoted one, because a raw string cannot contain the backtick in `\`turbo-go -list-themes\``. A throwaway test wrote the three files from the constants themselves, then was deleted.
436+- **A guard added for a risk this refactoring created**: the format verbs no longer sit next to the `profile.Templates` contract that documents them, so three tests now count the verbs per file, check none is empty, and fill each template asserting no `%!` marker comes out — Go writes `%!q(MISSING)` into the output rather than failing, so a wrong count produces a starter file that is written, opened, and wrong. All three falsified.
437+- **A verification that went stale under me.** I compared the six new files against HEAD byte for byte and they matched — and then `turbo-go/internal/golang/snippets.toml.tmpl` was overwritten with the contents of the playground's own `bin/.turbo-go/snippets.toml`, which a test caught. I could not attribute the overwrite. Restored from HEAD's evaluated constants and re-verified **after** the last step rather than in the middle. The lesson is the ordering: verify at the end, not when convenient.
438+- **Quality**: PASS 0/0/0 in both, complexity unchanged.
439+- **Docs**: turbo-core's `how-to/write-the-starter-files.md` gained a section on keeping them in files, in EN and FR; both architecture explanations list the new files; the `turbo-new-editor` skill's step 3 now prescribes this shape.
440+
441+## 2026-09-03 — Family count corrected, and three stale documentation claims found by a pty run
442+
443+- **Documentation only; no code changed.** `turbo-python` joined the family, so `docs/{en,fr}/explanation/architecture.md`'s "both editors use them unchanged" and "a change to a menu now affects both editors at once" became false. Changed to "every editor".
444+- **Three claims were stale because the library grew a Code menu, and nothing noticed.** Driving this editor's own binary in a pty gives the bar as `File Edit Search Run Code Options Window Snippets Go Help`. The tutorial listed `File Edit Search Run Options Window Help` — missing Code, Snippets **and this editor's own Go menu** — and told the reader to press `→` **four** times to reach Options, which has been five since the Code menu shipped. `reference/menus.md`'s opening sentence omitted Code as well. Fixed in EN and FR.
445+- **The lesson, and it is not this editor's alone**: a library that grows a menu makes every editor's tutorial wrong in a way no test sees, because a tutorial is prose about a screen. The counts are worth re-reading off a terminal after any change to the bar.
446+- **This repository's suite was already red at `HEAD`** — four tests in `internal/golang/templates_test.go` still assert a five-tool starter file that deliberately grew to eight. Verified pre-existing by stashing and re-running; not caused here and not fixed here. Detail in the handoff.
447+- Not committed.
448+
449+## 2026-09-09 (later) — the theme list gained three entries
450+
451+- **Goal**: none of its own. turbo-core gained `monochrome-light`, `darcula` and `intellij-light`, and renamed `monochrome` to `monochrome-dark`; this repository's documentation had to follow. Eleven themes ship now.
452+- **Changes**: `docs/{en,fr}/reference/themes.md` — the embedded list, three new table rows, and a new "a name a theme used to answer to" section saying `monochrome` still loads; `docs/{en,fr}/how-to/write-a-theme.md` — the inherit-from advice and the shipped-theme count; `README.md`'s themes bullet. No code change.
453+- **History was left alone**: "comments were the dimmest colour in six of the eight shipped themes" in `write-a-theme.md` is a true sentence about when that rule was written.
454+- **Not yet true of the binary.** This repository pins turbo-core v0.4.2, which ships eight themes. The documentation is ahead until turbo-core is tagged and the `go.mod` here is bumped — see turbo-core's handoff of the same date.
455+
456+## 2026-09-15 — ACP agent windows, and four stale tests fixed on the way in
457+
458+- **Goal**: the user asked for Agent Client Protocol support — an agent window with a typing area and a rendered conversation, code coloured, several agents configured in TOML in `acp.toml`, one window per agent. The library owns the window, the menu and the event loop, so the feature itself went into turbo-core; see that repository's history for the same date.
459+- **What is in *this* repository**: `internal/golang/acp.toml.tmpl`, embedded beside the other three starter files and wired into `profile.Templates.Agents`. That is all the code. Plus six documentation pages (EN + FR: a how-to, a reference, an explanation) and their index entries.
460+- **Step zero was fixing a suite that had been red at `HEAD` since 2026-09-03.** Four assertions in `templates_test.go` still described a five-tool starter file that had deliberately grown to eight. The template was right and the tests had drifted, exactly as that handoff predicted. `…HoldsTheFiveGoCommands` now names all eight and fails on a ninth it does not know about; `TestRunIsTheOneToolInATerminal` became `TestEachToolGoesWhereItsOwnOutputBelongs` with the real map; the tabs test targets `main` rather than an `if err != nil` snippet that no longer exists; and `…StillLoadsWithItsPlaceholderExamples` was inverted — two starter commands now take a value on purpose, and what it checks is that the braces in the file's *comments* did not become tools. All four were falsified before being accepted.
461+- **Decisions**: the starter file's example agent is `docker agent`, because that is what a Go developer is most likely to already have; the template takes two blanks (the project directory, and the user-level path a comment names) and a test fills it and asserts no `%!` marker comes out, since Go writes `%!s(MISSING)` into the output rather than failing.
462+- **Tests**: 4 new in `internal/golang`, 4 corrected. `make test` green — for the first time since 2026-09-03.
463+- **Quality**: PASS 0/0/0, complexity 37, unchanged.
464+- **Docs**: `docs/{en,fr}/how-to/talk-to-an-agent.md`, `reference/acp.md`, `explanation/agent-windows.md`, both `README.md` indexes. The drawio diagram was checked against `go list` and needed no change — this repository's import graph did not move.
465+- **The documentation was written before the code, at the user's request**, with a status banner on every page saying so. Two of its claims were false by the time the code existed (`syntax.error` is not a class — `diagnostic.error` is the key; and the output cap is per entry rather than 10 000 lines), and the refusal messages it quoted were not the ones the loader emits. All corrected against the running code before the banners came off.
466+- Not committed.
467+
468+## 2026-09-15 (later) — the spinner and copying, documented
469+
470+- **Documentation only in this repository**; the code is turbo-core's. The user asked for a spinner beside *thinking* and for a way to copy text out of a conversation, and both landed in the library.
471+- **Changes**: `docs/{en,fr}/how-to/talk-to-an-agent.md` gained "Take something out of the conversation" — the key table, what `Ctrl-C` copies with nothing selected, and the two clipboards. `reference/acp.md` gained per-pane key tables, a Copying section, the `editor.selection` row and a paragraph on the spinner. `explanation/agent-windows.md` gained three sections: why copying goes to two clipboards, why copying with nothing selected takes a whole block, and why the spinner is drawn from the clock.
472+- **Verified against the running binary**, not written from the code: the spinner was captured turning through ten distinct frames in a pty, and the copy was checked by base64-decoding the OSC 52 payload off the wire — which is how the editor's own defect (the speaker's label copied with the code) was found.
473+- **Quality**: PASS 0/0/0, unchanged.
474+- Not committed.
475+
476+## 2026-09-15 (night) — slash commands and `@` mentions, documented
477+
478+- **Documentation and the starter file only in this repository**; the code is turbo-core's (see its `.memory/` of the same date). The user asked for the ACP changes that let an agent's commands be discovered the way Zed discovers them, then for `@` as a file selector.
479+- **Changes**: `docs/{en,fr}/reference/acp.md` — seven key rows for the list, a **Commands and mentions** section, the `session/prompt` and `available_commands_update` rows, the Limits bullet; `docs/{en,fr}/how-to/talk-to-an-agent.md` — "Use the agent's own commands" and "Point the agent at a file"; `docs/{en,fr}/explanation/agent-windows.md` — the "left out" bullet narrowed to images, two sections appended; the embedded `acp.toml.tmpl` — two key lines. Applied by one script across the five editors with an exactly-once anchor check.
480+- **Tests**: `go test ./...` green (the template change is a comment; the tests that read the created file still pass).
481+- **Ahead of the binary**: this repository pins turbo-core v0.7.0, which has none of this. The pages are true once turbo-core is tagged and the pin moved.
482+- Not committed.
483+- **Later the same night**: `.turbo-go/acp.toml` gained the user's `mini-me (llama.cpp)` agent (`mm -acp`, `AGENT_CONFIG` env). Loads as two agents; not opened here, `mm` lives on the user's Mac.
484+
485+## 2026-09-16 — the trace variable and a troubleshooting bullet, documented
486+
487+- turbo-core gained `TURBO_ACP_TRACE=<file>` and an "update this editor could not read" line in Agent status, because the user saw no `/` commands from their own agent and nothing on screen could say why. Documented here EN + FR: a bullet in the how-to's Variants, a section in `reference/acp.md`. Docs only; not committed.
488+
489+## 2026-09-17 — documentation: terminal windows and tools on Windows
490+
491+- **Asked**: nothing of this repository directly. turbo-core gained pseudo-console (ConPTY) terminal windows and a per-platform tools shell (cmd.exe on Windows); the pages here that said "Linux and macOS" or `/bin/sh -c` went false the moment that landed.
492+- **Changes** (EN + FR): `README.md` (terminal windows: Linux, macOS and Windows), `docs/*/reference/terminal.md` (shell row, controlling-terminal row, platform table, error row), `docs/*/explanation/terminal-windows.md` (the Windows section rewritten: a pseudo-console and why it is a file of its own, built and vetted but not yet run), `docs/*/how-to/use-a-terminal.md` (`%COMSPEC%`, the five things to try first on Windows), `docs/*/reference/go-tools.md` (shell row, cmd.exe's globs and `;`, error row), `docs/*/explanation/go-tools.md` (`cmd.exe /S /C`). Applied by one script across the six editors, one anchor per file.
493+- **Not changed**: code, `go.mod`. **The documentation is ahead of the binary** until turbo-core is tagged (v0.9.0) and re-pinned here; the feature has never been run on Windows by anyone.
494+- **Tests**: none affected — documentation only.
495+
496+## 2026-09-18 — the "Untitled window has no LSP" fix: docs variant added, fix is turbo-core's
497+
498+- **Origin**: the defect was reported against this editor — "first launch: no LSP; save, quit, relaunch: works", the window having started Untitled — and diagnosed then fixed in turbo-core, where the save path lives.
499+- **Asked**: propagate to every editor the fix made in turbo-core the same day — saving now announces a document the server does not know (a window that started Untitled gets LSP from its first save), and Save As under a new name closes the old document. See turbo-core's `.memory/history.md` of 2026-09-18 for the defect and the fix.
500+- **Changes here**: `docs/{en,fr}/how-to/enable-completion.md` gain one variant — completion in a window that started without a name works from its first save, no relaunch needed. No code in this repository is involved.
501+- **Quality**: gate not re-run — a Markdown-only change; the gate measures the Go code.
502+- **Docs ahead of the binary**: the behaviour arrives only when turbo-core is tagged and this editor re-pinned. Not committed.
503+
504+## 2026-09-19 — moved to Rickub: turbo-core v1.0.0 re-pinned, releases published by a workflow
505+
506+- **Asked**: turbo-core had been moved to `rickub.com` and published as v1.0.0 with a Release workflow; do the same migration here — add the GitHub Action, update `01-release.tag.sh` and if need be `02`, drop `04` which the workflow makes unnecessary, and keep `03-build-releases.sh` runnable by hand.
507+- **Changes, module**: `codeberg.org/turbo-editors``rickub.com/turbo-editors` in `go.mod`, every `.go` file, `Makefile` (`VERSION_PKG`), `scripts/install.sh`, `README.md`, `docs/{en,fr}` (the two turbo-core deep links also went from Codeberg's `src/branch/main/` to `blob/main/`). `require rickub.com/turbo-editors/turbo-core v1.0.0`; `go mod tidy` with `GOWORK=off` rewrote `go.sum` from the proxy. The proxy also lists a `rickub.com/…/turbo-core v0.9.0`, but its `go.mod` still declares the Codeberg path, so v1.0.0 is the first version this module *can* require.
508+- **Changes, release tooling**: `.github/workflows/release.yml` (new, modelled on turbo-core's: tag push `v*`, `contents: write`, `go test` with `TURBO_GO_RELEASING=1`, `./03-build-releases.sh "${GITHUB_REF_NAME}"`, notes from the tag message + `go install` line + docs at the tag + checksums, run artifact, `softprops/action-gh-release@v2` attaching `turbo-go-*`, `SHA256SUMS`, `README.md`). `01-release.tag.sh` rewritten on turbo-core's: requires `release.env`, validates `TAG`, runs `make check` under `TURBO_GO_RELEASING=1`, refuses a taken tag (bump, never move), refuses a `replace`, pushes the current branch (not a hardcoded `main`) before tagging, no token file. `03-build-releases.sh`: tag from `$1` with `release.env` optional and `ABOUT` defaulting to `Turbo Go ${TAG}`, tag format check, `replace` check, `rm -rf release/${TAG}` before building, README gains the `go install` line, the closing hint no longer points at 04. **`02-release.publish.sh` and `04-release.upload-binaries.sh` deleted.** `release.env` comments rewritten; `OWNER`/`REPO` dropped (nothing reads them).
509+- **Tests**: `release_test.go` — the push assertion follows the new `git push origin "$(git rev-parse …)"`; new: `01` runs `make check`, refuses a `replace`, `go.mod` has none, no script reads a token and 02/04 are gone, `01` **run for real** twice against a throwaway bare remote (publishes; refuses the second time with "already exists"), `03` takes the tag from the command line and refuses `v0.o.0` (run for real, script alone in an empty dir), `03` no longer hands off to 04, and seven workflow assertions (trigger, `contents: write`, uses `./03-build-releases.sh`, attaches with `fail_on_unmatched_files`, links docs at the tag, no `secrets.`, sets `TURBO_GO_RELEASING`). The helper had to be `runOrFail`: `main.go` already owns `run`. Copy of the module for the throwaway clone leaves out `.git`, `bin`, `release` (489 MB of old binaries), `kits`, `demo`, `*.env` and `go.work*`; children run with `GOWORK=off` so the clone builds against the published library. Suite green in ~10 s with `GOWORK=off`.
510+- **Verified by hand**: `GOWORK=off ./03-build-releases.sh v0.0.1-test` → five binaries, host binary reports `0.0.1-test`, `sha256sum -c SHA256SUMS` all OK, README as expected; directory removed afterwards. `01` in a hand-made throwaway clone: `make check` ran (fmt, vet, test), root commit, push, tag on the bare remote.
511+- **Not done**: nothing committed or pushed — the repository has no commit yet and `origin` is unreachable from this sandbox (no SSH). The workflow has not run on Rickub: it is written against the same platform facts turbo-core's is, and turbo-core's has run. `docs/{en,fr}/reference/versioning.md` still describes `03` accurately and was left alone.
512+
513+## 2026-09-19 (later) — `03-build-releases.sh` renamed `02-build-releases.sh`
514+
515+- **Asked**: rename the build script, now that `02` and `04` are gone and the numbering had a hole.
516+- **Changes**: `git mv`-equivalent rename; every reference rewritten — `01-release.tag.sh`, `.github/workflows/release.yml`, `Makefile` (the `ldflags` target's comment), `release_test.go`, `release.env`, `docs/{en,fr}/how-to/make-a-release.md`, `docs/{en,fr}/reference/versioning.md`, `.memory/summary.md`. Older history entries and handoffs keep the old name, as history does.
517+- **Tests**: `GOWORK=off make check` green.
added .memory/summary.md +209 -0
new file mode 100644
@@ -0,0 +1,209 @@
1+# turbo-go — project summary
2+
3+*A snapshot of the present. No history here — that is `history.md`.*
4+
5+## What this is
6+
7+A Turbo C-style editor for Go, written in Go: a full-screen terminal IDE with a menu bar, movable overlapping windows, modal dialogs, mouse support, Go syntax colouring, loadable TOML themes, completion from `gopls`, terminal windows running a real shell, per-project settings, a project tree, snippets, the go toolchain a menu away, and windows onto coding agents speaking the Agent Client Protocol.
8+
9+**Since 2026-09-01 it is a thin editor on top of [turbo-core](https://rickub.com/turbo-editors/turbo-core)**, the library every Turbo editor shares. What is in this repository is `main.go` and `internal/golang` — about four hundred lines. The other fourteen packages moved into the library, unchanged in behaviour.
10+
11+Module path `rickub.com/turbo-editors/turbo-go`. Go 1.26.5. Remote: `ssh://git@rickub.com/turbo-editors/turbo-go.git`.
12+
13+## Architecture
14+
15+Two packages here; everything else is the library.
16+
17+```
18+main → {turbo-core/app, turbo-core/profile, turbo-core/settings, turbo-core/theme,
19+ turbo-core/version, internal/golang, tcell}
20+internal/golang → {turbo-core/profile, turbo-core/syntax}
21+```
22+
23+| Package | What it holds |
24+| --- | --- |
25+| `main` | Flags, the terminal, and the wiring: register Go, build the profile, read the project's settings, hand them to `app.New`, start gopls in the module root, run the loop |
26+| `internal/golang` | The whole of what makes this Turbo Go: the profile (`golang.go`), the Go scanner on top of `go/scanner` (`scan.go`), and the four starter files a project gets (`templates.go`) |
27+
28+turbo-core holds `app`, `buffer`, `editor`, `filetree`, `lsp`, `profile`, `projectfile`, `settings`, `snippets`, `syntax`, `terminal`, `theme`, `tools`, `ui` and `version`. Its own `.memory/summary.md` is the place to read about them.
29+
30+`docs/diagrams/packages.drawio` is generated from `go list` and verified against it edge for edge.
31+
32+## Decisions in force
33+
34+*The decisions below were made while this was a single program. Almost all of them are now enforced in turbo-core, where the code lives; they are kept here because this is where they were made and why they were made is recorded nowhere else. The ones about **this editor** come first.*
35+
36+- **This editor is a command, a profile and a scanner.** Everything else is turbo-core. `golang.Profile()` is the entire answer to "what makes this Turbo Go?" — the name, the slug, the `~G~o` menu, `go.mod` as the root marker, gopls with `serve`, and the three starter templates. Rejected: forking the editor for each language, which is two copies of eleven thousand lines drifting within a month.
37+- **The Go scanner stays here, not in the library.** turbo-core colours the eight languages every editor meets whatever it is for — TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles, shell. The language that *defines* an editor is registered by that editor, which is why a `.rs` file opens as plain text here. It is also the scanner least like the others: it goes through `go/scanner` and byte offsets, where every other one works a line at a time.
38+- **`golang.Register()` is called from `main`, explicitly**, rather than from an `init` function, so that "this editor knows Go" is a line somebody can read.
39+- **The environment variable names did not change.** `TURBO_GO_THEME_DIR` and `TURBO_GO_SNIPPET_DIR` are derived from the profile's slug precisely so a user who set one against a released binary is not broken by a refactoring.
40+- **The version is a property of the build, not of the source.** There is **no version constant**: `internal/version` takes the number from the linker's stamp (`git describe --tags --dirty`, set by the Makefile and `scripts/install.sh`), then from `runtime/debug.ReadBuildInfo()`, then reports `unknown`. `unknown` is deliberately not a number — the failure being designed against is a plausible-looking version nobody set, which is exactly what `const Version = "0.1.0"` had become fourteen commits after somebody wrote it. Rejected: a `make release` target — releasing is three git commands and wrapping them hides which one failed.
41+- **Anything that ships must be stamped explicitly.** Removing the version constant moved a cost that used to be invisible: an unstamped build used to carry the last number somebody typed, and now carries `devel`. In a **cross-compile** nothing else notices — the host binary is right while the five downloads are not. `02-build-releases.sh` therefore stamps every platform, from `make ldflags` rather than repeating the `-X` paths, and runs the staged binary for its own machine before declaring the release built.
42+- **A release build stamps `TAG`, not `git describe`.** `02-build-releases.sh` overrides the Makefile's version — `make ldflags VERSION="${TAG}"` — so the binaries report what the release announces, by construction. An earlier attempt made the script *verify* that `git describe` agreed with `TAG` (exact-match tag, clean tree, correct report) and the user rejected it: the checks blocked the build for conditions that stamping the tag directly makes impossible. Building no longer needs the tag to exist at all; only `02-release.publish.sh` does. **Do not reintroduce those gates.**
43+- **`-version` is written for a person, and scripts must not parse fields out of it.** `awk '{print $NF}'` read the build timestamp and failed a release the day the line grew a parenthetical. The release script now asks git directly (`git describe --tags --exact-match`, `git diff --quiet HEAD`) and uses `grep -F` for the binary, so its checks do not depend on the shape of the sentence.
44+- **Two limits of the Go build system shape that design.** It does **not read git tags**, so a plain `go build .` can never report `0.1.0-14-g88a4c38`; it reports `devel` plus the commit, and the docs say so. And what it reports for such a build is a **pseudo-version** (`v0.1.1-0.20260831165958-88a4c3859bf3`), shown as `devel` instead because its `0.1.1` is a patch release that does not exist. `vcs.time` is deliberately unused: it is the *commit's* timestamp, so labelling it "Built" would be false on every binary.
45+- **The About box omits a line whose fact is empty** rather than showing a blank one. A binary from `go install …@v0.2.0` knows its version and nothing else, and `Commit:` with nothing after it says only that the editor failed to fill it in. `aboutText(info, themeName)` is pure, so the box's text is tested without opening one.
46+- **Eleven themes ship, and each states its whole palette.** `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino` (espresso brown), `catppuccin-frappe` and `catppuccin-latte` (the published palettes unchanged), `cobalt` (the recognised Cobalt palette, accents left loud) and the two monochromes (no hue at all, one on ink and one on paper). They live in turbo-core now. `Defines` is satisfied by inheritance, so a theme omitting a key silently shows a colour Turbo Classic chose for its navy background — unreadable on espresso or on black, and invisible to the completeness test. `TestEveryEmbeddedThemeSetsEveryKeyItself` closes that for shipped themes only; a **user** theme may still inherit, which is what `inherits` is for.
47+- **Five rules hold a theme to being readable**, four of them arithmetic in `internal/editor` where the colour maths already lives: the cursor is ≥64 from its line and never a plain reversal of it, the current line is ≥16 from the page, and text meant to be read is ≥64 from its background. That last one **exempts the furniture** — desktop, shadow, scrollbar trough, inactive frame, disabled entry, gutter — which sits between 20 and 70 in every theme *by design*; a blanket rule would have flagged six correct keys in the two oldest themes. The floor 64 was chosen against a measured floor of 80 (turbo-dark's `syntax.comment`), so it catches a regression rather than the present.
48+- **The fifth rule is the one no measurement finds**: two syntax classes a reader meets side by side must not be drawn identically. `turbo-classic` once painted `syntax.link` the same lime as `syntax.string`. Classes deliberately alike — string/char, constant/number, type/tag — are not grouped, so the test stays silent about them. `monochrome` passes it with no hue, using bold, italic and underline.
49+- **A tool's command can ask for values.** A `{{label}}` in it opens a box before the command runs; the value is shell-quoted unless the label ends in `...`. The feature is turbo-core's — see its summary — and what belongs to this editor is the starter file's comments, which teach the syntax without adding a sixth tool.
50+- **The go toolchain is data, not code.** `.turbo-go/tools.toml` holds the commands; the five Go defaults (`gofmt -l -w .`, `go vet ./...`, `go build ./...`, `go test ./...`, `go run .`) are the *contents of the starter file* that `Go ▸ Create tools file` writes, not compiled-in behaviour. `go vet` is the default linter only because it ships with the toolchain. Commands go to `sh -c`, so one entry can be a sequence. There is **no user-level tools file**, unlike snippets: a project's tools belong to its own toolchain, and a global one would offer `go build` in a Rust repository.
51+- **Which menu a tool is in is the tool's choice too**, from a free-form `menu` key; absent means `Go`. A name nothing else uses simply creates a menu, between Go and Help, in the order the names first appear in the file. There is **no list of allowed names**, because a list would be a list of somebody else's projects. Rejected: a fixed second `Tools` menu (only moves the problem — a Tools menu holding Docker, psql and a deploy script is just as undifferentiated) and a separate `menus.toml` (two files that have to agree about which tools exist). `Go` stays **fixed** on the bar rather than becoming another name from the file, because it holds `Create tools file`, which has to be reachable in a project that has none.
52+- **Hot keys for those menus are assigned by the editor, never read from the file.** The author of a tools file cannot know which letters are free, and a clash is **silent** — the bar answers the first menu matching a key, so the second draws normally and simply never opens. That trap already sprang once here (`Snippets` vs `Search`, with every test passing). `hotKeyLabel` marks the first letter of the name nothing else claims; tildes written into the name are kept when the letter is free and **dropped when it is not**, because refusing the file instead would break a working tools file the day a release adds a menu. Every letter taken means no hot key at all, which `F10` and the mouse still reach.
53+- **The menu bar is rebuilt from a `stat`, not from a parse.** `Menu.OnOpen` refills one menu's items; the *set* of menus belongs to the bar, and a menu that does not exist yet has no `OnOpen` to call. `App.toolsStamp` holds the tools file's size and modification time, and one `stat` per turn of the loop decides whether to call `ui.MenuBar.SetMenus`. The stamp is taken *before* the bar is built, so a file written between the two is picked up next turn rather than missed.
54+- **Where a command's output goes is the tool's choice**, from an `output` key: `popup` (the default), `terminal`, `editor`. An unknown value is **refused, not corrected**`"termnial"` falling back silently would look as though it worked while sending the output elsewhere. Four of the five defaults are `popup`; `Run` is `terminal`, and is the worked example of why the key exists: a popup cannot answer a program that reads the keyboard, nor be stopped with `Ctrl-C`.
55+- **The popup opens immediately and fills in**, rather than appearing when the command ends. A dialog arriving three seconds later swallows whatever was being typed at that moment. It is modal, which is a real cost on a slow build and is documented; Escape closes it *and* stops the command, which is the only way to interrupt one whose output is not in a terminal.
56+- **The exit code is always in the popup's title**, and a finished command that printed nothing shows `(no output)`. `go build ./...` succeeding is silent, and a blank dialog with a neutral title cannot be told from one whose command has not started. While still running the body stays blank — "(no output)" is a verdict.
57+- **`tools.Start` runs a command without a pty**, merging stderr into stdout in write order, capped at 10000 lines with `Dropped()` reporting the loss. Its `onLine` callback is a **parameter, not a field**, because it starts the goroutine that calls it — the same race `terminal.ViewOptions` was created to fix.
58+- **The Code menu is turbo-core's, and so are its eight questions.** Describe symbol and Go to definition moved into it from Run and Search; their keys did not change. This repository documents the menu and owns none of it — as with everything else the two editors share, a change to it is a `/methodical-dev` cycle in turbo-core.
59+- **The settings file a project creates turns autosave on.** A project that has gone to the trouble of having one has said what it wants, and the file is the visible, editable place to say otherwise. `settings.Default()` — what applies with no settings file at all — stays **off**: the editor must not write to disk in a directory somebody merely started it in. Two different statements, set in two different places on purpose.
60+- **A workspace, not a `replace`, is how to build against an unreleased turbo-core.** `go work init . ../turbo-core` changes no tracked file, so there is nothing to forget before committing; `go.work` is gitignored in all three repositories. The commented-out `replace` at the bottom of `go.mod` still works and is documented as the older way, with its hazard named.
61+- **The build runs the binary it just built and checks it names the right version.** `scripts/check-version.sh` is called by `make build`, by `scripts/install.sh` before the install, and by `02-build-releases.sh`. A linker stamp is a string and a wrong one is not an error — `-X` naming a symbol that does not exist links happily and stamps nothing — so nothing but running the binary catches it. The comparison is an **equality**: `0.2.0` is a substring of `10.2.0`.
62+- **The installer replaces the binary by rename, never by `cp` over it.** macOS caches a binary's code signature against its **inode**; writing new bytes into the existing inode leaves the cached signature describing something else and the kernel refuses to execute a binary that built and installed cleanly. `cp` writes in place, so a *reinstall* failed while a first install worked. The temporary must sit in `$prefix`, because a rename only works within one filesystem. The install is atomic as a result, which is the same reasoning `internal/buffer` and `internal/projectfile` already follow.
63+- **Stopping a command kills its whole process group**, not just the shell. A grandchild inherits the output pipe, so killing only the shell leaves the reading goroutine blocked until *that* ends — for `go test ./...` that is every test binary it spawned. `cmd.WaitDelay` is the backstop for anything that escapes the group.
64+- **`App.tick` is the event loop's turn, extracted so a test can take one.** Everything in it is state-driven; tests call `tick`, never an individual step, or removing that step from the loop would leave them passing.
65+- **A finished terminal view takes only the scrolling keys.** It used to consume every key and write it to a dead shell, where the write failed silently and the key was consumed anyway — so `Ctrl-W` could never close a finished window and the mouse was the only way out.
66+- **Files a command rewrote are re-read, unless they have unsaved changes.** `Format` rewrites the file in front, and without this the next `F2` would write the unformatted version back over gofmt's work. A modified buffer is left alone and named on the status bar: the edit and the formatter genuinely disagree, and the editor is not in a position to decide. `Buffer.Reload` refuses over unsaved work by returning `ErrModified`, keeps the cursor (clamped), and discards the undo history.
67+- **`terminal.ViewOptions` gives the callbacks *before* the goroutines start.** They were assignable fields, and `NewView` starts the goroutine that reads them — a data race that hid for a whole feature because a shell takes longer to produce output than an assignment takes to run. It surfaced the moment a command finished immediately.
68+- **`ui.Menu` has one level of submenus**, via `MenuItem.Items`, and `Menu.OnOpen` refills a menu just before it drops down. One level because the only nested menu in the editor — snippets grouped by kind — is one level, and a general depth would mean replacing the bar's two indices with a path in the widget every dialog depends on. `OnOpen` exists because a menu built from a file, filtered by the front window, has no start-up moment at which its contents exist.
69+- **A submenu panel flips left *and* is capped to the screen width.** Flipping alone cannot fit a panel wider than the terminal; long labels are clipped by the painter instead, because a frame with no right-hand edge looks broken in a way a truncated label does not.
70+- **No two menus may share a hot key.** The bar answers the first match it finds, so a duplicate silently makes one menu unreachable. Snippets is `Alt-N`, not `Alt-S`, because Search already owns S — and `TestNoTwoMenusShareAHotKey` in `internal/app` is what holds it.
71+- **Snippets come from two files, and the project's wins.** The user's `<config>/turbo-go/snippets.toml` is read first, then `<project>/.turbo-go/snippets.toml`; where a `group` **and** `name` clash the project's replaces it, being the more specific statement. A missing file is fine; a present-but-unreadable one is an error shown as a greyed line in the menu, because a silent drop looks exactly like having no snippets.
72+- **A snippet is re-indented on insertion**, and it is one undo step. `editor.InsertSnippet` copies the current line's own whitespace prefix onto every line after the first — verbatim insertion restarts a multi-line body at column zero, which is wrong everywhere an `if err != nil` actually goes. Blank lines in a body stay blank, so no trailing whitespace lands in the next diff. Placeholders and tab stops were deliberately left out.
73+- **The project tree is a window, not a docked panel.** A panel would mean `Desktop` growing a notion of reserved edges, and `fitInto`, the grow modes, maximising, tiling and cascading all having to respect them — a change to the foundation of the interface for one widget. As an ordinary window it gets F6, Alt-digits, `[x]`, `[■]` and Tile for free, and nothing in `ui` had to change.
74+- **There is at most one project tree.** The root is fixed at start-up, so a second view of it would have nothing to distinguish it. `F9` on an open tree raises it, the way opening an already-open file does.
75+- **The tree hides `.git` and nothing else** — deliberately *not* the Open dialog's rule of hiding every dot-entry. `.turbo-go/settings.toml` is a file the editor asks people to edit, and `.gitignore` and `.qlty/` belong to the project too. Respecting `.gitignore` as well was turned down for now: it needs a pattern engine (negation, `**`, anchoring) that is a feature in its own right.
76+- **The tree does not watch the filesystem.** That would be `fsnotify`, a third dependency, for a feature whose failure mode is a stale line in a list. It re-reads on a save (the one moment the editor knows) and on `F5` / `Ctrl-R` (the moment only the user knows). `Refresh` re-reads only directories that were actually opened, so it costs what is on screen.
77+- **The project is the working directory.** `.turbo-go/settings.toml` **and the project tree** are both rooted in `os.Getwd()` alone, with **no walk up** the way `go.mod` is found. A module has a real boundary; "the project" does not — it is where you chose to start. A walk would also make a file three directories up change your colours silently. Cost, accepted: starting the editor from `internal/app` means the project's theme does not apply.
78+- **Theme precedence is flag > project file > built-in default.** `-theme`'s flag default is `""` rather than `theme.DefaultName` precisely so that "was it given?" is still answerable in `main.themeName`.
79+- **`.turbo-go/` is created only by Options ▸ Create project settings**, never as a side effect. Writing it the first time someone picks a theme would put a directory into their repository for trying a colour. That is also what makes the write-back rule one sentence: the theme is written when the file exists, and not otherwise.
80+- **`settings.SetTheme` rewrites one key in place, it never re-encodes the file.** Marshalling the struct back would be four lines and would delete every comment — in a file that exists to be hand-edited, and whose created form is mostly comments. This is why TOML colouring exists at all.
81+- **Autosave is state checked at the top of the event loop, nudged by a `time.AfterFunc`.** Third instance of the same rule (see the re-announcement below and the terminal's redraws): `PostEvent` drops what does not fit, so the timer may only *cause a turn*, never decide. A failed save clears the deadline **before** writing, so a read-only file is retried once per edit rather than forever, and reports on the status bar rather than in a modal that would return every two seconds.
82+- **One autosave deadline for the whole editor**, not one per window: "you stopped typing" is a single event, and a per-window deadline would save the file you moved away from at a different moment for no observable gain.
83+- **The tree needed theme keys of its own; the terminal's reasoning does not apply, but the outcome is the same.** `list.selected` is coloured against a *dialog* — in turbo-classic it is white on navy while `window.body` is navy, so a tree borrowing it would have highlighted its selected row in the colour underneath it. `tree.text`, `tree.directory`, `tree.selected` and `tree.unfocused` exist for that, and a test holds every shipped theme to 64 channel values between the first and the third.
84+- **Six languages, six hand-written scanners, and no general engine.** Go goes through `go/scanner`; TOML, Markdown, JavaScript, HTML and shell each have a file of ordinary Go sharing only `lineScanner` (a line in runes, a position, the spans so far). There is no pattern language and no grammar format on purpose: adding a language means writing one beside the others rather than learning a notation.
85+- **A scanner guesses nothing.** Where a construct cannot be recognised from what one line holds, it is left alone rather than approximated — a highlighter that is wrong is worse than one that is quiet. Deliberately absent, each for a stated reason: **JavaScript regex literals** (telling `/x/g` from a division needs the previous token's type; a wrong guess strings the rest of the line), **shell heredocs**, **JavaScript inside `<script>`**, and **the language of a Markdown fence**. The boundaries are written down in `docs/*/reference/languages.md`.
86+- **TOML, JavaScript and shell add no theme keys**; the markup languages needed five. `syntax.heading`, `syntax.tag`, `syntax.attribute`, `syntax.emphasis` and `syntax.link` have no Go equivalent — a heading is not a keyword, and a theme wanting quiet headings with loud keywords could not say so otherwise. Third-party themes that set none of them fall back to `default`: readable, undifferentiated.
87+- **A file is recognised by extension, then by shebang.** `LanguageOf(path, firstLine)`. The extension always wins; a file with none is a shell script when its first line names `sh`, `bash`, `zsh`, `dash` or `ksh`. That is what colours `configure` and a git hook.
88+- **Two direct dependencies only**: `tcell/v2` and `BurntSushi/toml`. `golang.org/x/sys` is now also direct, for the pseudo-terminal ioctls — it was already in the graph, indirect via tcell, so nothing new entered `go.sum`. The tokeniser, the JSON-RPC client, the LSP framing and the VT/ANSI emulator are hand-written on purpose. Do not add another without saying why.
89+- **The terminal emulator is deliberately partial.** It implements what a shell, `go test`, `git`, `less`, `htop` and `vim` need — movement, erase, insert/delete, scroll region, SGR in all three colour depths, alternate screen, DECAWM, DECTCEM, DECCKM — and nothing else. Mouse reporting, bracketed paste, character sets, sixel and the DEC status reports are absent; a program asking for one gets silence rather than corruption. The boundary is written down in `docs/*/reference/terminal.md`; keep it true if you extend the parser.
90+- **A focused terminal outranks the editor's global shortcuts.** `App.keyLayers()` is the routing chain, and a terminal sits *above* the shortcuts — a shell needs `Ctrl-C`, `Ctrl-W` and `Ctrl-F`, all of which the editor would otherwise take. `editorOwnedKey` reserves only the function keys, `Alt-X` and `Alt-0``Alt-9`, which are the way out of a full-screen program. The cost, accepted knowingly: `F1``F12` never reach a program inside a terminal, so `htop`'s function-key menu is unreachable.
91+- **Closing a terminal asks nothing.** A terminal holds a running process, not unsaved work. `Quit` closes every terminal, because a window is the only handle on those shells.
92+- **Terminal redraws are on a 16 ms ticker, not per chunk of output.** Same reasoning as the re-announcement below: `PostEvent` drops what does not fit, and a build is exactly when the queue is fullest. A dropped tick cannot strand anything.
93+- **Widget bounds are absolute screen coordinates.** Hit-testing is a rectangle test; containers place children in screen space. `Painter.Sub` takes absolute coordinates while drawing calls take local ones — that asymmetry is deliberate and documented.
94+- **Every text mutation goes through `buffer.ReplaceRange`.** Undo history, modified flag, revision counter and cursor are maintained there and nowhere else.
95+- **Undo merges runs** of typing and of backspaces into one step. Cursor movement ends a run; typing and deleting never merge.
96+- **A new buffer's text is `""`, not `"\n"`.** A file gets its trailing newline when the user presses Enter. Line endings and the trailing newline of a file that was read are preserved byte for byte.
97+- **An unknown colour in a theme is a load error**, not a silent fallback.
98+- **The language server is optional by construction**: `app.Language` is a no-op when nothing is connected, so no other code checks for it.
99+- **The cursor is marked twice.** `editor.cursor`'s background is sent to the terminal through `SetCursorStyle(SteadyBlock, colour)` — DECSCUSR plus OSC 12 — and the cell underneath is painted in the same style as a fallback. Painting alone is not enough: the terminal draws its cursor *over* the cell in the user's own colour, so on a dark theme it covers whatever is beneath it. The colours must be a **distinct pair**, never a reversal of the line, and tests hold every theme to a minimum channel distance: 64 for the cursor against its line, 16 for the line against the page. Only the active window shows one.
100+- **A window tells its content whether it is focused** (`Window.SetActive``Focusable.SetFocused`), which is what stops every open window drawing a cursor.
101+- **The frame carries two boxes, and each says what pressing it will do.** `[x]` at the left closes; `[■]` at the right fills the desktop and then reads `[▬]`. A fixed symbol would be ambiguous exactly when it matters — you can see the window is large, not whether the box will enlarge it further or put it back. `Desktop.ToggleMaximize` is the single path, used by both the box and **Window ▸ Maximise**, so the two cannot disagree. A window not on a desktop has no `OnMaximize` and draws **no box** rather than a dead one.
102+- **A maximised window carries its restore rectangle through a terminal resize** (`Window.followDesktop`), and `Tile`/`Cascade` clear the maximised flag (`Window.place`). Without the first, shrinking the terminal leaves a window restoring to somewhere unreachable; without the second, a tiled window offers to restore to a rectangle that means nothing.
103+- **The re-announcement is state-driven, never event-driven.** `App.announceOpenDocuments` runs on every turn of the event loop and does nothing until the server is ready. A posted event would not do: tcell's queue is bounded, `PostEvent` drops what does not fit, and start-up — when gopls publishes diagnostics for the whole module — is when it is fullest. Correctness must not depend on a message allowed to go missing.
104+- **Documents already open are re-announced once the language server is ready.** `main` opens files *before* starting gopls, so the first `didOpen` reaches nothing. Without the second announcement, `didChange` arrives for a document the server was never told was open, gopls ignores it, and completion answers from the stale on-disk text.
105+- **Windows follow the terminal, they do not scale with it.** `ui.Window` has a Turbo Vision-style grow mode; a document window follows the desktop's right and bottom edges, so its far corner moves by exactly the delta the terminal's did and its top-left corner stays put. Proportional scaling was rejected: it moves windows the user placed on purpose, and rounding makes shrink-then-grow lossy. No window may exceed the desktop's own size.
106+- **Colour contrast is measured, not eyeballed.** `channelDistance` in `internal/editor` is the yardstick; turbo-dark once highlighted the cursor's line ten channel values from the page, which is no highlight at all.
107+- **`Dialog.MoveTo` / `CenterIn` move a dialog's controls with it.** Controls are placed in screen coordinates when the dialog is built, so moving the frame alone leaves them behind. A resize re-centres open dialogs and dismisses the completion popup, which is anchored to a cursor that has moved.
108+- **Upward communication is by function field** (`OnChange`, `OnCursorMove`, `OnCompletionRequest`, …), not by interface.
109+- **Dialogs are asynchronous**: `pushModal(dialog, onClose)`, settled after each event. There is no nested event loop.
110+- **In a dialog, arrows reach the focused control before they move the focus.** Reversing this makes every list box unusable by keyboard — it was a real bug, fixed and covered by tests.
111+- **In the Open / Save As box, the Name field mirrors the list highlight.** `ListBox.OnSelect` writes the highlighted entry into the field, and `confirm` falls back to the highlight when the field is empty. Without the wiring the two controls are independent and **OK does nothing at all** on a freshly opened dialog: the user has highlighted a file, the field is still empty, `Path()` returns `""`, and the button looks broken. Reported by the user, fixed 2026-08-31.
112+- **Agent windows are turbo-core's, and what belongs here is the starter file.** `acp.toml.tmpl` is the fourth embedded template, and `profile.Templates.Agents` is the whole of turbo-go's contribution to the feature — the example agent is `docker agent serve acp .turbo-go/agent.yaml`, which is a choice about what a Go developer is likely to have installed, not about the protocol. Every other editor gets agent windows by writing a starter file of its own and nothing else. The reasoning, and why it could not have been built here, is in `docs/*/explanation/agent-windows.md`.
113+- **The starter agents file teaches the window's keyboard as well as the format.** `Enter`, `Alt-Enter`, `Tab`, `Esc` and `Ctrl-W` are all in its comments, because a file the editor hands you is the one document a user is guaranteed to see.
114+
115+## Build, test, run
116+
117+```bash
118+make install # build + install onto PATH (scripts/install.sh)
119+make uninstall # remove it again
120+make build # → bin/turbo-go
121+make test # the whole suite; the single documented command
122+make check # fmt + vet + test — what a commit should pass
123+make run FILE=main.go
124+go test -short ./... # skips the test that starts a real gopls
125+go test ./internal/golang/
126+```
127+
128+Quality gate, separate from the tests:
129+
130+```bash
131+python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .
132+```
133+
134+Reports land in `.quality/`; exit code 0 = pass.
135+
136+## State as of 2026-09-01
137+
138+- **Migrated onto turbo-core.** `internal/*` is gone; `main.go` and `internal/golang` remain. The full existing test suite passes unchanged, plus the two real-gopls tests that came back here with the language they are about.
139+- **Quality gate: PASS.** 0/0/0, complexity 37 — the number fell from 1592 because the code moved, not because anything was simplified.
140+- **Released as v0.2.2** at `d64410c`, which is exactly HEAD, with a release page on Codeberg. The first release built on the library.
141+- **Depends on `turbo-core v0.2.0`**, with no active `replace`. On `main` that is v0.1.0 and builds from the module proxy. On `feature/more-syntaxes` the `require` names **v0.2.0, which is not published yet**: that branch does not build until turbo-core is tagged and released, and `go.sum` has no entry for it. The old replace block is still there, commented out, as the documented way to develop across the three repositories.
142+- **Nothing about the editor's behaviour changed.** The menus, the keys, the themes, the file formats and the environment variables are what they were.
143+
144+## State as of 2026-08-31 (second session)
145+
146+- **Feature-complete against the original request**, plus terminal windows. Editor, Go colouring, themes, LSP completion and `F8` shell windows all implemented.
147+- **Terminal windows (ticket 0007) implemented on 2026-08-31**, and **merged into `main`** by the user as PR #1. New `internal/terminal` package — pseudo-terminal, VT/ANSI emulator, view widget — wired into `app` as `Window ▸ New terminal` / `F8`.
148+- **Project settings (ticket 0002) implemented on 2026-08-31** and **merged into `main`** as PR #2. New `internal/settings` package; TOML colouring in `internal/syntax`; autosave in `internal/app`; `Options ▸ Create project settings` and `Options ▸ Project settings…`. Both tickets have since been closed by the user.
149+- **Run in a real terminal once**, by the user, which found two defects — an invisible cursor under `turbo-dark`, and completion returning nothing. Both fixed and covered; see the handoff of the same date.
150+- **Project tree (ticket 0003) implemented on 2026-08-31** and **merged into `main`** as PR #4. New `internal/filetree` package; `Window ▸ Project tree` / `F9`.
151+- **Window frame boxes** (`[x]` closes, `[■]`/`[▬]` maximises) and the **Open-dialog OK fix** were merged as PR #3.
152+- **Markdown, JavaScript, HTML and shell colouring (tickets 0009, 0013, 0014) implemented on 2026-08-31** and **merged into `main`** as PR #5.
153+- **Snippets (ticket 0006) implemented on 2026-08-31** and **merged into `main`** as PR #6.
154+- **The Go menu (ticket 0017) implemented on 2026-08-31** and **merged into `main`** as PR #7, from `feature/go-format-lint`. New `internal/tools` and `internal/projectfile`; `terminal.Options.Args` and `terminal.ViewOptions`; `Buffer.Reload`; a `Go` menu on `Alt-G`; three output destinations; a `menu` key putting tools into menus of their own; the `Screen.Resize` fix. The user **closed ticket 0017** on 2026-08-31 without the `go mod init + touch main.go` idea it also mentioned, so that idea is settled rather than outstanding.
155+- **Tests**: every package green, and green under `-race`. Coverage — version 98.3 %, editor 96.2 %, syntax 96.0 %, terminal 95.9 %, buffer 95.8 %, tools 95.7 %, filetree 94.7 %, theme 94.3 %, settings 93.8 %, ui 93.5 %, snippets 88.9 %, lsp 87.0 %, app 86.3 %, projectfile 75.0 %, main 31.7 %.
156+- **Quality gate: PASS.** 0 lint errors, 0 warnings, 0 code smells, total complexity 1592.
157+- **Project settings verified end to end against the real binary in a pty**: `settings.toml` picking `turbo-dark` (seen as `ESC]12;#ffd787` on the wire), `-theme turbo-classic` overriding it, and autosave writing a file **from the idle timer alone** — the editor was killed without ever quitting cleanly, so no close-or-quit path could have written it. The control run, with no settings file, left the file untouched.
158+- **Verified against a real gopls 0.23.0**, twice over: `internal/lsp` drives the protocol directly, and `internal/app` replays the command's own start-up order and completes text that exists **only in the buffer** — which is the only version of that test that can fail.
159+- **Docs**: 31 pages × EN + FR under `docs/`, plus a per-package `README.md` and the drawio diagram. The diagram was checked against `go list` programmatically and matches edge for edge.
160+- **Moved from Codeberg to Rickub on 2026-09-19.** Module path `rickub.com/turbo-editors/turbo-go`, depends on `rickub.com/turbo-editors/turbo-core v1.0.0` (the first turbo-core version published under that path; `v0.9.0` on the proxy still declares the Codeberg path and cannot be required as `rickub.com/…`). Every import, the Makefile's `VERSION_PKG`, `scripts/install.sh`, the README and the docs say `rickub.com`; `go.sum` verified against the proxy; `GOWORK=off make check` green. The repository on this side is a fresh `git init` with `origin` at `ssh://git@rickub.com/turbo-editors/turbo-go.git` and **no commit yet**; `01-release.tag.sh` makes the first one.
161+- **Releases are cut by one script and one workflow (since 2026-09-19).** `01-release.tag.sh` runs `make check`, refuses a tag taken locally or on origin, refuses a `replace` in `go.mod`, commits, pushes the branch, then tags and pushes the tag. That push starts `.github/workflows/release.yml`, which runs the suite, builds with `02-build-releases.sh` and publishes the release page with the binaries using the job's own `GITHUB_TOKEN`. `02-release.publish.sh` and `04-release.upload-binaries.sh` are gone — no personal token, no `turbo-go.token.env`. `release.env` (untracked) holds only `TAG` and `ABOUT`. **`01` had no `set -e`** once, so a `git tag` refusing an existing tag was skipped in silence and the following `git push` pushed the *old* tag — a release cut from a commit nobody meant; that guard is still there, and `release_test.go` now runs `01` for real against a throwaway clone.
162+- **`scripts/install.sh`** builds and installs onto the user's PATH, reporting the Go version, the destination, whether it is on PATH, and whether gopls is there. Thirteen tests in `install_test.go` drive it for real, including that a failed build leaves an existing installation untouched and that a reinstall **replaces** the binary rather than writing into it.
163+
164+## Traps worth knowing
165+
166+- **LSP columns are UTF-16 code units**, editor columns are runes. Everything crossing that boundary goes through `RuneToUTF16` / `UTF16ToRune`. On ASCII the two agree, so a mistake here survives testing until a file has an accent in it.
167+- **gopls asks its client questions.** It requests `workspace/configuration` during start-up and waits for the reply; a client that ignores server-to-client requests hangs with no error. `Client.handleRequest` answers them.
168+- **An empty completion list is usually a package that does not compile.** gopls answers nothing at all — no error — for a package it cannot load. `App.noCompletionsReason` and `App.languageReport` turn that into a sentence naming the problem.
169+- **A test whose fixture already contains the text being completed proves nothing.** gopls answers from disk for anything it has not been told is open, so such a test passes whether or not the editor said a word. The text must be *typed* into the buffer.
170+- **`len()` on a string with box-drawing characters counts bytes.** `"[■]"` is 5 bytes and 3 columns — that was a real bug in the close box, and the same characters are now the maximise box. Count runes for anything that becomes a screen width; `boxWidth` and a test hold the two in step.
171+- **A `ListBox` callback that nobody wires is invisible.** `OnSelect` existed, worked, and was covered by its own test in `internal/ui` — and `FileDialog` never set it, so the field and the list drifted apart with every test still green. A callback with no caller is not a feature.
172+- **`strings.Index` on a drawn screen row gives a byte offset, not a column.** A row is full of `░` and `║` at three bytes each, so clicking at that offset lands about thirty columns right of the target. This cost a wrong diagnosis: a test failure that looked like a second bug in the app was the test's own arithmetic.
173+- **`drawNumber` runs after `drawTitleBar`, so the number always wins.** A test that only checks the furniture survived is therefore vacuous — it would pass with the title margin off by one, because the number is simply repainted over the title. What breaks is the *title*, which loses a character and reads `main.g7`. Assert on the cell **beside** the furniture, not on the furniture.
174+- **A pseudo-terminal echoes the command line.** A test that types `echo red` and waits for `red` passes *before* the shell has run anything. Wait for something only the output can produce — a colour, or a string the typed line spells differently (`echo turbo''-go-works``turbo-go-works`).
175+- **Drawing tests must not race the shell's startup output.** A test asserting on a screen cell while a live shell writes to it passes or fails by luck; one such test hid a real fault for a whole session. `terminal.newOfflineView` builds a `View` with no session behind it — `Draw` needs none — and is deterministic.
176+- **The terminal cursor is drawn over cell (0,0) of a fresh screen.** A drawing test that samples the top-left cell is sampling the cursor, not the text.
177+- **A settings key absent is not the same as a key set to its default.** `settings.file` uses pointer fields for exactly this: `autosave = false` written out on purpose and no `autosave` line must not be the same statement, or a future default flip would silently change existing projects.
178+- **`emit` dropping empty spans makes "patch the span afterwards" unsafe.** In the TOML scanner, fixing up `spans[len-1].Start` after an emit that produced nothing rewrote the *previous* span instead. It bit a second time in the JavaScript scanner, differently: `finishTemplate` called a helper that ran the position to the end of the line, then asked `takeRest` to colour "what is left", which was nothing — a whole line of a template literal came back uncoloured. **Pass a span's start in as a parameter.** Never derive it from the scanner's position after a helper has moved it.
179+- **A test that drives the step under test proves nothing.** Twice now: `waitForLoopTurn` called `a.reloadAfterTools()` itself, so the test passed with that step removed from the event loop — fixed by extracting `App.tick` and having tests take a whole loop turn. And `TestStopEndsWhatTheCommandStartedToo` killed the shell before it had forked, so it passed without the process-group fix — fixed by waiting for the child to print. **Verify a new test by breaking the code it covers.**
180+- **A goroutine started by a constructor may not have its callbacks assigned afterwards.** `terminal.NewView` started the reading goroutine and callers then set `OnChange`/`OnExit` — a data race that existed from the terminal feature onward and that `-race` never caught, because a shell takes longer to produce its first output than the assignment takes to run. It surfaced only when a command finished immediately (`sh -c "echo x"`). The fix is `ViewOptions`: everything the goroutines read is set before they start. **When adding a constructor that starts a goroutine, take its callbacks as parameters.**
181+- **Two menus sharing a hot key make one of them unreachable, silently.** `handleClosedKey` returns on the first match. `Alt-S` opened Search rather than Snippets and every test passed; it was found by driving the real binary. `TestNoTwoMenusShareAHotKey` now covers it.
182+- **The theme completeness test only really constrains `turbo-classic`.** `turbo-dark` and `borland-light` both `inherits = "turbo-classic"`, and inheritance is resolved at parse time into the theme's own map, so `Defines` is true for an inherited key. Deleting a key from a child theme passes; deleting it from `turbo-classic` fails. Check against the base theme when verifying that test.
183+- **Colours that clash are only visible on a screen.** `syntax.link` was set to lime in `turbo-classic` — the exact colour of `syntax.string`, making a Markdown link indistinguishable from an inline `code` span. Every test passed. It was caught by rendering the editor through the project's own VT emulator and reading back the foreground of each run.
184+- **`tcell.KeyCtrlC` is 67, not 3.** tcell reports control bytes as `KeyCtrlSpace + b`, and `KeyCtrlSpace` is 64. Code that tests `key < 0x20` to spot a control key silently matches nothing.
185+- **`.qlty/qlty.toml` excludes `kits/**`** with a comment saying exactly what that hides. It hides two real defects in the kit's own `quality_report.py`, which belong to the kit. Do not widen the pattern.
186+
187+## Release tooling
188+
189+`01-release.tag.sh` and `02-build-releases.sh` in the repository root, plus `.github/workflows/release.yml`, modelled on turbo-core's. `release.env` carries `TAG` and `ABOUT` and is gitignored (`*.env`); there is no token file any more.
190+
191+`01` tags; the tag push starts the workflow. It exports `TURBO_GO_RELEASING=1` before `make check` so the tests that run `01` against a throwaway clone do not recurse; the workflow sets the same variable for its `go test` step. `02-build-releases.sh` cross-compiles for the platforms in its own `PLATFORMS` array and writes `release/${TAG}/` (binaries, `SHA256SUMS`, `README.md`); adding a target is one line and the checksums and README follow. It takes the tag as its first argument (what CI does, having no `release.env`), validates it as `vX.Y.Z[-pre]`, refuses a `replace` in `go.mod`, and starts from an empty `release/${TAG}/`. The workflow attaches `turbo-go-*`, `SHA256SUMS` and `README.md` with `softprops/action-gh-release@v2`, `fail_on_unmatched_files: true`, and writes release notes from the tag's message with the docs linked at that tag. Rickub's release API takes only the job's `GITHUB_TOKEN` (a personal token is refused), and its dispatch API fires every dispatchable workflow of a ref — hence `contents: write`, no `secrets.`, and no `workflow_dispatch`.
192+
193+## Not yet established
194+
195+- **Agent windows work, and nobody has typed into one by hand.** The whole path was driven against a real `docker agent` v1.139.0 and a real llama.cpp (JetBrains Mellum2) by running the binary in a pty: the `Alt-A` menu, the window, a streamed reply, a shell tool call, the permission dialog answered, and a ```go fence coloured span by span read back off the wire. The mouse, `Tab` between the panes, resizing mid-turn and two agents side by side are all untried. Detail in the handoff of 2026-09-15.
196+
197+
198+- **Barely run in a real terminal.** A few sessions by the user, plus scripted pty runs here. **Verified on a real pty**: the menu bar via F10, Alt-F, Alt-E and the mouse; and live resize via SIGWINCH, where the window's frame measured 78, then 98, then 48 cells as the terminal went 80 → 100 → 50. Also verified on the wire: the cursor escapes, `ESC[2 q` and `ESC]12;<colour>`, and tcell's restoration of both on exit. Still **unverified**: mouse dragging and corner-resizing, and the light theme on an actual terminal emulator.
199+- **A reinstall on macOS was broken until 2026-08-31**, and the diagnosis was made from the symptom rather than reproduced: this sandbox is Linux. If `the installed binary does not run` ever returns, the installer now prints the system's own message above it — read that before theorising.
200+- **Windows and macOS are untested.** The code paths exist (drive letters in `lsp.PathToURI`, `os.UserConfigDir`) but have only been exercised on Linux/arm64. `internal/terminal/pty_darwin.go` in particular **compiles and passes `go vet` but has never been run** — its `TIOCPTYGRANT` / `TIOCPTYUNLK` / `TIOCPTYGNAME` path is unverified. Terminal windows are **not implemented at all on Windows**: `pty_other.go` returns `ErrUnsupported` and `F8` says so; ticket 0015 tracks the ConPTY port.
201+- **No performance measurement.** The syntax cache means a full re-scan per change rather than per redraw, but nothing has been profiled. Behaviour on a file of tens of thousands of lines is unknown.
202+- **Diagnostics are stored but barely shown.** `Language.Diagnostics` keeps them per file and the status bar shows the first error; there is no marker in the gutter or under the offending text.
203+- **No CI.** There is no pipeline configuration in the repository.
204+- **Terminal windows have never been used in a real terminal.** Everything about them was verified against a real pty here — a shell really runs, `ls` really lists — but nobody has yet opened one inside `turbo-go` on a physical terminal and run `vim` or `htop` in it.
205+- **The project tree has never been driven by a human.** It was verified by rendering the real binary through the project's own VT emulator — `F9` lists the project with `.git` hidden, `→` nests two levels, `Enter` opens a file into a third window — but nobody has yet clicked a row or scrolled it with a real mouse.
206+- **`.tickets/` holds 20 issues**, `0002``0021`, most with an empty `body`. Twelve are closed (`0002`, `0003`, `0004`, `0006`, `0007`, `0009`, `0013`, `0014`, `0017`, `0019`, `0020`, `0021`); eight remain open: `0005` wasm plugins, `0008` a mini agent view, `0010` a version number in About (**implemented**), `0011` a website, `0012` more themes (**implemented**), `0015` Windows support for terminal windows, `0016` no shadow on tiled windows, `0018` a core library extracted from Turbo Go. `0010` (a version number in About) was **implemented on 2026-08-31** and is the user's to close. **The user opens and closes these; do not edit them.** The schema also carries an `epic:` field on some tickets — `.tickets/epics.yaml` lists the epics.
207+- **Autosave has never been used for a whole working session.** It was verified end to end in a pty, but nobody has yet spent an hour editing with it on, which is where a save at an unwanted moment would show up.
208+- **The window boxes have not been clicked by a human.** Both were verified by rendering the real binary through the project's own VT emulator — the frame reads `[x] … 1═[■]`, Window ▸ Maximise flips it to `[▬]` and fills the terminal, and a second use restores it — but nobody has yet pressed either box with an actual mouse.
209+- **`ui.Menu` has no nested submenus.** `MenuItem` has no `Items` field, so the project-settings entries are two flat items under Options rather than the submenu originally asked for. Adding nesting is a `ui` change nobody has asked for yet.
new file mode 100644
@@ -0,0 +1,209 @@
1+# turbo-go — project summary
2+
3+*A snapshot of the present. No history here — that is `history.md`.*
4+
5+## What this is
6+
7+A Turbo C-style editor for Go, written in Go: a full-screen terminal IDE with a menu bar, movable overlapping windows, modal dialogs, mouse support, Go syntax colouring, loadable TOML themes, completion from `gopls`, terminal windows running a real shell, per-project settings, a project tree, snippets, the go toolchain a menu away, and windows onto coding agents speaking the Agent Client Protocol.
8+
9+**Since 2026-09-01 it is a thin editor on top of [turbo-core](https://rickub.com/turbo-editors/turbo-core)**, the library every Turbo editor shares. What is in this repository is `main.go` and `internal/golang` — about four hundred lines. The other fourteen packages moved into the library, unchanged in behaviour.
10+
11+Module path `rickub.com/turbo-editors/turbo-go`. Go 1.26.5. Remote: `ssh://git@rickub.com/turbo-editors/turbo-go.git`.
12+
13+## Architecture
14+
15+Two packages here; everything else is the library.
16+
17+```
18+main → {turbo-core/app, turbo-core/profile, turbo-core/settings, turbo-core/theme,
19+ turbo-core/version, internal/golang, tcell}
20+internal/golang → {turbo-core/profile, turbo-core/syntax}
21+```
22+
23+| Package | What it holds |
24+| --- | --- |
25+| `main` | Flags, the terminal, and the wiring: register Go, build the profile, read the project's settings, hand them to `app.New`, start gopls in the module root, run the loop |
26+| `internal/golang` | The whole of what makes this Turbo Go: the profile (`golang.go`), the Go scanner on top of `go/scanner` (`scan.go`), and the four starter files a project gets (`templates.go`) |
27+
28+turbo-core holds `app`, `buffer`, `editor`, `filetree`, `lsp`, `profile`, `projectfile`, `settings`, `snippets`, `syntax`, `terminal`, `theme`, `tools`, `ui` and `version`. Its own `.memory/summary.md` is the place to read about them.
29+
30+`docs/diagrams/packages.drawio` is generated from `go list` and verified against it edge for edge.
31+
32+## Decisions in force
33+
34+*The decisions below were made while this was a single program. Almost all of them are now enforced in turbo-core, where the code lives; they are kept here because this is where they were made and why they were made is recorded nowhere else. The ones about **this editor** come first.*
35+
36+- **This editor is a command, a profile and a scanner.** Everything else is turbo-core. `golang.Profile()` is the entire answer to "what makes this Turbo Go?" — the name, the slug, the `~G~o` menu, `go.mod` as the root marker, gopls with `serve`, and the three starter templates. Rejected: forking the editor for each language, which is two copies of eleven thousand lines drifting within a month.
37+- **The Go scanner stays here, not in the library.** turbo-core colours the eight languages every editor meets whatever it is for — TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles, shell. The language that *defines* an editor is registered by that editor, which is why a `.rs` file opens as plain text here. It is also the scanner least like the others: it goes through `go/scanner` and byte offsets, where every other one works a line at a time.
38+- **`golang.Register()` is called from `main`, explicitly**, rather than from an `init` function, so that "this editor knows Go" is a line somebody can read.
39+- **The environment variable names did not change.** `TURBO_GO_THEME_DIR` and `TURBO_GO_SNIPPET_DIR` are derived from the profile's slug precisely so a user who set one against a released binary is not broken by a refactoring.
40+- **The version is a property of the build, not of the source.** There is **no version constant**: `internal/version` takes the number from the linker's stamp (`git describe --tags --dirty`, set by the Makefile and `scripts/install.sh`), then from `runtime/debug.ReadBuildInfo()`, then reports `unknown`. `unknown` is deliberately not a number — the failure being designed against is a plausible-looking version nobody set, which is exactly what `const Version = "0.1.0"` had become fourteen commits after somebody wrote it. Rejected: a `make release` target — releasing is three git commands and wrapping them hides which one failed.
41+- **Anything that ships must be stamped explicitly.** Removing the version constant moved a cost that used to be invisible: an unstamped build used to carry the last number somebody typed, and now carries `devel`. In a **cross-compile** nothing else notices — the host binary is right while the five downloads are not. `02-build-releases.sh` therefore stamps every platform, from `make ldflags` rather than repeating the `-X` paths, and runs the staged binary for its own machine before declaring the release built.
42+- **A release build stamps `TAG`, not `git describe`.** `02-build-releases.sh` overrides the Makefile's version — `make ldflags VERSION="${TAG}"` — so the binaries report what the release announces, by construction. An earlier attempt made the script *verify* that `git describe` agreed with `TAG` (exact-match tag, clean tree, correct report) and the user rejected it: the checks blocked the build for conditions that stamping the tag directly makes impossible. Building no longer needs the tag to exist at all; only `02-release.publish.sh` does. **Do not reintroduce those gates.**
43+- **`-version` is written for a person, and scripts must not parse fields out of it.** `awk '{print $NF}'` read the build timestamp and failed a release the day the line grew a parenthetical. The release script now asks git directly (`git describe --tags --exact-match`, `git diff --quiet HEAD`) and uses `grep -F` for the binary, so its checks do not depend on the shape of the sentence.
44+- **Two limits of the Go build system shape that design.** It does **not read git tags**, so a plain `go build .` can never report `0.1.0-14-g88a4c38`; it reports `devel` plus the commit, and the docs say so. And what it reports for such a build is a **pseudo-version** (`v0.1.1-0.20260831165958-88a4c3859bf3`), shown as `devel` instead because its `0.1.1` is a patch release that does not exist. `vcs.time` is deliberately unused: it is the *commit's* timestamp, so labelling it "Built" would be false on every binary.
45+- **The About box omits a line whose fact is empty** rather than showing a blank one. A binary from `go install …@v0.2.0` knows its version and nothing else, and `Commit:` with nothing after it says only that the editor failed to fill it in. `aboutText(info, themeName)` is pure, so the box's text is tested without opening one.
46+- **Eleven themes ship, and each states its whole palette.** `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino` (espresso brown), `catppuccin-frappe` and `catppuccin-latte` (the published palettes unchanged), `cobalt` (the recognised Cobalt palette, accents left loud) and the two monochromes (no hue at all, one on ink and one on paper). They live in turbo-core now. `Defines` is satisfied by inheritance, so a theme omitting a key silently shows a colour Turbo Classic chose for its navy background — unreadable on espresso or on black, and invisible to the completeness test. `TestEveryEmbeddedThemeSetsEveryKeyItself` closes that for shipped themes only; a **user** theme may still inherit, which is what `inherits` is for.
47+- **Five rules hold a theme to being readable**, four of them arithmetic in `internal/editor` where the colour maths already lives: the cursor is ≥64 from its line and never a plain reversal of it, the current line is ≥16 from the page, and text meant to be read is ≥64 from its background. That last one **exempts the furniture** — desktop, shadow, scrollbar trough, inactive frame, disabled entry, gutter — which sits between 20 and 70 in every theme *by design*; a blanket rule would have flagged six correct keys in the two oldest themes. The floor 64 was chosen against a measured floor of 80 (turbo-dark's `syntax.comment`), so it catches a regression rather than the present.
48+- **The fifth rule is the one no measurement finds**: two syntax classes a reader meets side by side must not be drawn identically. `turbo-classic` once painted `syntax.link` the same lime as `syntax.string`. Classes deliberately alike — string/char, constant/number, type/tag — are not grouped, so the test stays silent about them. `monochrome` passes it with no hue, using bold, italic and underline.
49+- **A tool's command can ask for values.** A `{{label}}` in it opens a box before the command runs; the value is shell-quoted unless the label ends in `...`. The feature is turbo-core's — see its summary — and what belongs to this editor is the starter file's comments, which teach the syntax without adding a sixth tool.
50+- **The go toolchain is data, not code.** `.turbo-go/tools.toml` holds the commands; the five Go defaults (`gofmt -l -w .`, `go vet ./...`, `go build ./...`, `go test ./...`, `go run .`) are the *contents of the starter file* that `Go ▸ Create tools file` writes, not compiled-in behaviour. `go vet` is the default linter only because it ships with the toolchain. Commands go to `sh -c`, so one entry can be a sequence. There is **no user-level tools file**, unlike snippets: a project's tools belong to its own toolchain, and a global one would offer `go build` in a Rust repository.
51+- **Which menu a tool is in is the tool's choice too**, from a free-form `menu` key; absent means `Go`. A name nothing else uses simply creates a menu, between Go and Help, in the order the names first appear in the file. There is **no list of allowed names**, because a list would be a list of somebody else's projects. Rejected: a fixed second `Tools` menu (only moves the problem — a Tools menu holding Docker, psql and a deploy script is just as undifferentiated) and a separate `menus.toml` (two files that have to agree about which tools exist). `Go` stays **fixed** on the bar rather than becoming another name from the file, because it holds `Create tools file`, which has to be reachable in a project that has none.
52+- **Hot keys for those menus are assigned by the editor, never read from the file.** The author of a tools file cannot know which letters are free, and a clash is **silent** — the bar answers the first menu matching a key, so the second draws normally and simply never opens. That trap already sprang once here (`Snippets` vs `Search`, with every test passing). `hotKeyLabel` marks the first letter of the name nothing else claims; tildes written into the name are kept when the letter is free and **dropped when it is not**, because refusing the file instead would break a working tools file the day a release adds a menu. Every letter taken means no hot key at all, which `F10` and the mouse still reach.
53+- **The menu bar is rebuilt from a `stat`, not from a parse.** `Menu.OnOpen` refills one menu's items; the *set* of menus belongs to the bar, and a menu that does not exist yet has no `OnOpen` to call. `App.toolsStamp` holds the tools file's size and modification time, and one `stat` per turn of the loop decides whether to call `ui.MenuBar.SetMenus`. The stamp is taken *before* the bar is built, so a file written between the two is picked up next turn rather than missed.
54+- **Where a command's output goes is the tool's choice**, from an `output` key: `popup` (the default), `terminal`, `editor`. An unknown value is **refused, not corrected**`"termnial"` falling back silently would look as though it worked while sending the output elsewhere. Four of the five defaults are `popup`; `Run` is `terminal`, and is the worked example of why the key exists: a popup cannot answer a program that reads the keyboard, nor be stopped with `Ctrl-C`.
55+- **The popup opens immediately and fills in**, rather than appearing when the command ends. A dialog arriving three seconds later swallows whatever was being typed at that moment. It is modal, which is a real cost on a slow build and is documented; Escape closes it *and* stops the command, which is the only way to interrupt one whose output is not in a terminal.
56+- **The exit code is always in the popup's title**, and a finished command that printed nothing shows `(no output)`. `go build ./...` succeeding is silent, and a blank dialog with a neutral title cannot be told from one whose command has not started. While still running the body stays blank — "(no output)" is a verdict.
57+- **`tools.Start` runs a command without a pty**, merging stderr into stdout in write order, capped at 10000 lines with `Dropped()` reporting the loss. Its `onLine` callback is a **parameter, not a field**, because it starts the goroutine that calls it — the same race `terminal.ViewOptions` was created to fix.
58+- **The Code menu is turbo-core's, and so are its eight questions.** Describe symbol and Go to definition moved into it from Run and Search; their keys did not change. This repository documents the menu and owns none of it — as with everything else the two editors share, a change to it is a `/methodical-dev` cycle in turbo-core.
59+- **The settings file a project creates turns autosave on.** A project that has gone to the trouble of having one has said what it wants, and the file is the visible, editable place to say otherwise. `settings.Default()` — what applies with no settings file at all — stays **off**: the editor must not write to disk in a directory somebody merely started it in. Two different statements, set in two different places on purpose.
60+- **A workspace, not a `replace`, is how to build against an unreleased turbo-core.** `go work init . ../turbo-core` changes no tracked file, so there is nothing to forget before committing; `go.work` is gitignored in all three repositories. The commented-out `replace` at the bottom of `go.mod` still works and is documented as the older way, with its hazard named.
61+- **The build runs the binary it just built and checks it names the right version.** `scripts/check-version.sh` is called by `make build`, by `scripts/install.sh` before the install, and by `02-build-releases.sh`. A linker stamp is a string and a wrong one is not an error — `-X` naming a symbol that does not exist links happily and stamps nothing — so nothing but running the binary catches it. The comparison is an **equality**: `0.2.0` is a substring of `10.2.0`.
62+- **The installer replaces the binary by rename, never by `cp` over it.** macOS caches a binary's code signature against its **inode**; writing new bytes into the existing inode leaves the cached signature describing something else and the kernel refuses to execute a binary that built and installed cleanly. `cp` writes in place, so a *reinstall* failed while a first install worked. The temporary must sit in `$prefix`, because a rename only works within one filesystem. The install is atomic as a result, which is the same reasoning `internal/buffer` and `internal/projectfile` already follow.
63+- **Stopping a command kills its whole process group**, not just the shell. A grandchild inherits the output pipe, so killing only the shell leaves the reading goroutine blocked until *that* ends — for `go test ./...` that is every test binary it spawned. `cmd.WaitDelay` is the backstop for anything that escapes the group.
64+- **`App.tick` is the event loop's turn, extracted so a test can take one.** Everything in it is state-driven; tests call `tick`, never an individual step, or removing that step from the loop would leave them passing.
65+- **A finished terminal view takes only the scrolling keys.** It used to consume every key and write it to a dead shell, where the write failed silently and the key was consumed anyway — so `Ctrl-W` could never close a finished window and the mouse was the only way out.
66+- **Files a command rewrote are re-read, unless they have unsaved changes.** `Format` rewrites the file in front, and without this the next `F2` would write the unformatted version back over gofmt's work. A modified buffer is left alone and named on the status bar: the edit and the formatter genuinely disagree, and the editor is not in a position to decide. `Buffer.Reload` refuses over unsaved work by returning `ErrModified`, keeps the cursor (clamped), and discards the undo history.
67+- **`terminal.ViewOptions` gives the callbacks *before* the goroutines start.** They were assignable fields, and `NewView` starts the goroutine that reads them — a data race that hid for a whole feature because a shell takes longer to produce output than an assignment takes to run. It surfaced the moment a command finished immediately.
68+- **`ui.Menu` has one level of submenus**, via `MenuItem.Items`, and `Menu.OnOpen` refills a menu just before it drops down. One level because the only nested menu in the editor — snippets grouped by kind — is one level, and a general depth would mean replacing the bar's two indices with a path in the widget every dialog depends on. `OnOpen` exists because a menu built from a file, filtered by the front window, has no start-up moment at which its contents exist.
69+- **A submenu panel flips left *and* is capped to the screen width.** Flipping alone cannot fit a panel wider than the terminal; long labels are clipped by the painter instead, because a frame with no right-hand edge looks broken in a way a truncated label does not.
70+- **No two menus may share a hot key.** The bar answers the first match it finds, so a duplicate silently makes one menu unreachable. Snippets is `Alt-N`, not `Alt-S`, because Search already owns S — and `TestNoTwoMenusShareAHotKey` in `internal/app` is what holds it.
71+- **Snippets come from two files, and the project's wins.** The user's `<config>/turbo-go/snippets.toml` is read first, then `<project>/.turbo-go/snippets.toml`; where a `group` **and** `name` clash the project's replaces it, being the more specific statement. A missing file is fine; a present-but-unreadable one is an error shown as a greyed line in the menu, because a silent drop looks exactly like having no snippets.
72+- **A snippet is re-indented on insertion**, and it is one undo step. `editor.InsertSnippet` copies the current line's own whitespace prefix onto every line after the first — verbatim insertion restarts a multi-line body at column zero, which is wrong everywhere an `if err != nil` actually goes. Blank lines in a body stay blank, so no trailing whitespace lands in the next diff. Placeholders and tab stops were deliberately left out.
73+- **The project tree is a window, not a docked panel.** A panel would mean `Desktop` growing a notion of reserved edges, and `fitInto`, the grow modes, maximising, tiling and cascading all having to respect them — a change to the foundation of the interface for one widget. As an ordinary window it gets F6, Alt-digits, `[x]`, `[■]` and Tile for free, and nothing in `ui` had to change.
74+- **There is at most one project tree.** The root is fixed at start-up, so a second view of it would have nothing to distinguish it. `F9` on an open tree raises it, the way opening an already-open file does.
75+- **The tree hides `.git` and nothing else** — deliberately *not* the Open dialog's rule of hiding every dot-entry. `.turbo-go/settings.toml` is a file the editor asks people to edit, and `.gitignore` and `.qlty/` belong to the project too. Respecting `.gitignore` as well was turned down for now: it needs a pattern engine (negation, `**`, anchoring) that is a feature in its own right.
76+- **The tree does not watch the filesystem.** That would be `fsnotify`, a third dependency, for a feature whose failure mode is a stale line in a list. It re-reads on a save (the one moment the editor knows) and on `F5` / `Ctrl-R` (the moment only the user knows). `Refresh` re-reads only directories that were actually opened, so it costs what is on screen.
77+- **The project is the working directory.** `.turbo-go/settings.toml` **and the project tree** are both rooted in `os.Getwd()` alone, with **no walk up** the way `go.mod` is found. A module has a real boundary; "the project" does not — it is where you chose to start. A walk would also make a file three directories up change your colours silently. Cost, accepted: starting the editor from `internal/app` means the project's theme does not apply.
78+- **Theme precedence is flag > project file > built-in default.** `-theme`'s flag default is `""` rather than `theme.DefaultName` precisely so that "was it given?" is still answerable in `main.themeName`.
79+- **`.turbo-go/` is created only by Options ▸ Create project settings**, never as a side effect. Writing it the first time someone picks a theme would put a directory into their repository for trying a colour. That is also what makes the write-back rule one sentence: the theme is written when the file exists, and not otherwise.
80+- **`settings.SetTheme` rewrites one key in place, it never re-encodes the file.** Marshalling the struct back would be four lines and would delete every comment — in a file that exists to be hand-edited, and whose created form is mostly comments. This is why TOML colouring exists at all.
81+- **Autosave is state checked at the top of the event loop, nudged by a `time.AfterFunc`.** Third instance of the same rule (see the re-announcement below and the terminal's redraws): `PostEvent` drops what does not fit, so the timer may only *cause a turn*, never decide. A failed save clears the deadline **before** writing, so a read-only file is retried once per edit rather than forever, and reports on the status bar rather than in a modal that would return every two seconds.
82+- **One autosave deadline for the whole editor**, not one per window: "you stopped typing" is a single event, and a per-window deadline would save the file you moved away from at a different moment for no observable gain.
83+- **The tree needed theme keys of its own; the terminal's reasoning does not apply, but the outcome is the same.** `list.selected` is coloured against a *dialog* — in turbo-classic it is white on navy while `window.body` is navy, so a tree borrowing it would have highlighted its selected row in the colour underneath it. `tree.text`, `tree.directory`, `tree.selected` and `tree.unfocused` exist for that, and a test holds every shipped theme to 64 channel values between the first and the third.
84+- **Six languages, six hand-written scanners, and no general engine.** Go goes through `go/scanner`; TOML, Markdown, JavaScript, HTML and shell each have a file of ordinary Go sharing only `lineScanner` (a line in runes, a position, the spans so far). There is no pattern language and no grammar format on purpose: adding a language means writing one beside the others rather than learning a notation.
85+- **A scanner guesses nothing.** Where a construct cannot be recognised from what one line holds, it is left alone rather than approximated — a highlighter that is wrong is worse than one that is quiet. Deliberately absent, each for a stated reason: **JavaScript regex literals** (telling `/x/g` from a division needs the previous token's type; a wrong guess strings the rest of the line), **shell heredocs**, **JavaScript inside `<script>`**, and **the language of a Markdown fence**. The boundaries are written down in `docs/*/reference/languages.md`.
86+- **TOML, JavaScript and shell add no theme keys**; the markup languages needed five. `syntax.heading`, `syntax.tag`, `syntax.attribute`, `syntax.emphasis` and `syntax.link` have no Go equivalent — a heading is not a keyword, and a theme wanting quiet headings with loud keywords could not say so otherwise. Third-party themes that set none of them fall back to `default`: readable, undifferentiated.
87+- **A file is recognised by extension, then by shebang.** `LanguageOf(path, firstLine)`. The extension always wins; a file with none is a shell script when its first line names `sh`, `bash`, `zsh`, `dash` or `ksh`. That is what colours `configure` and a git hook.
88+- **Two direct dependencies only**: `tcell/v2` and `BurntSushi/toml`. `golang.org/x/sys` is now also direct, for the pseudo-terminal ioctls — it was already in the graph, indirect via tcell, so nothing new entered `go.sum`. The tokeniser, the JSON-RPC client, the LSP framing and the VT/ANSI emulator are hand-written on purpose. Do not add another without saying why.
89+- **The terminal emulator is deliberately partial.** It implements what a shell, `go test`, `git`, `less`, `htop` and `vim` need — movement, erase, insert/delete, scroll region, SGR in all three colour depths, alternate screen, DECAWM, DECTCEM, DECCKM — and nothing else. Mouse reporting, bracketed paste, character sets, sixel and the DEC status reports are absent; a program asking for one gets silence rather than corruption. The boundary is written down in `docs/*/reference/terminal.md`; keep it true if you extend the parser.
90+- **A focused terminal outranks the editor's global shortcuts.** `App.keyLayers()` is the routing chain, and a terminal sits *above* the shortcuts — a shell needs `Ctrl-C`, `Ctrl-W` and `Ctrl-F`, all of which the editor would otherwise take. `editorOwnedKey` reserves only the function keys, `Alt-X` and `Alt-0``Alt-9`, which are the way out of a full-screen program. The cost, accepted knowingly: `F1``F12` never reach a program inside a terminal, so `htop`'s function-key menu is unreachable.
91+- **Closing a terminal asks nothing.** A terminal holds a running process, not unsaved work. `Quit` closes every terminal, because a window is the only handle on those shells.
92+- **Terminal redraws are on a 16 ms ticker, not per chunk of output.** Same reasoning as the re-announcement below: `PostEvent` drops what does not fit, and a build is exactly when the queue is fullest. A dropped tick cannot strand anything.
93+- **Widget bounds are absolute screen coordinates.** Hit-testing is a rectangle test; containers place children in screen space. `Painter.Sub` takes absolute coordinates while drawing calls take local ones — that asymmetry is deliberate and documented.
94+- **Every text mutation goes through `buffer.ReplaceRange`.** Undo history, modified flag, revision counter and cursor are maintained there and nowhere else.
95+- **Undo merges runs** of typing and of backspaces into one step. Cursor movement ends a run; typing and deleting never merge.
96+- **A new buffer's text is `""`, not `"\n"`.** A file gets its trailing newline when the user presses Enter. Line endings and the trailing newline of a file that was read are preserved byte for byte.
97+- **An unknown colour in a theme is a load error**, not a silent fallback.
98+- **The language server is optional by construction**: `app.Language` is a no-op when nothing is connected, so no other code checks for it.
99+- **The cursor is marked twice.** `editor.cursor`'s background is sent to the terminal through `SetCursorStyle(SteadyBlock, colour)` — DECSCUSR plus OSC 12 — and the cell underneath is painted in the same style as a fallback. Painting alone is not enough: the terminal draws its cursor *over* the cell in the user's own colour, so on a dark theme it covers whatever is beneath it. The colours must be a **distinct pair**, never a reversal of the line, and tests hold every theme to a minimum channel distance: 64 for the cursor against its line, 16 for the line against the page. Only the active window shows one.
100+- **A window tells its content whether it is focused** (`Window.SetActive``Focusable.SetFocused`), which is what stops every open window drawing a cursor.
101+- **The frame carries two boxes, and each says what pressing it will do.** `[x]` at the left closes; `[■]` at the right fills the desktop and then reads `[▬]`. A fixed symbol would be ambiguous exactly when it matters — you can see the window is large, not whether the box will enlarge it further or put it back. `Desktop.ToggleMaximize` is the single path, used by both the box and **Window ▸ Maximise**, so the two cannot disagree. A window not on a desktop has no `OnMaximize` and draws **no box** rather than a dead one.
102+- **A maximised window carries its restore rectangle through a terminal resize** (`Window.followDesktop`), and `Tile`/`Cascade` clear the maximised flag (`Window.place`). Without the first, shrinking the terminal leaves a window restoring to somewhere unreachable; without the second, a tiled window offers to restore to a rectangle that means nothing.
103+- **The re-announcement is state-driven, never event-driven.** `App.announceOpenDocuments` runs on every turn of the event loop and does nothing until the server is ready. A posted event would not do: tcell's queue is bounded, `PostEvent` drops what does not fit, and start-up — when gopls publishes diagnostics for the whole module — is when it is fullest. Correctness must not depend on a message allowed to go missing.
104+- **Documents already open are re-announced once the language server is ready.** `main` opens files *before* starting gopls, so the first `didOpen` reaches nothing. Without the second announcement, `didChange` arrives for a document the server was never told was open, gopls ignores it, and completion answers from the stale on-disk text.
105+- **Windows follow the terminal, they do not scale with it.** `ui.Window` has a Turbo Vision-style grow mode; a document window follows the desktop's right and bottom edges, so its far corner moves by exactly the delta the terminal's did and its top-left corner stays put. Proportional scaling was rejected: it moves windows the user placed on purpose, and rounding makes shrink-then-grow lossy. No window may exceed the desktop's own size.
106+- **Colour contrast is measured, not eyeballed.** `channelDistance` in `internal/editor` is the yardstick; turbo-dark once highlighted the cursor's line ten channel values from the page, which is no highlight at all.
107+- **`Dialog.MoveTo` / `CenterIn` move a dialog's controls with it.** Controls are placed in screen coordinates when the dialog is built, so moving the frame alone leaves them behind. A resize re-centres open dialogs and dismisses the completion popup, which is anchored to a cursor that has moved.
108+- **Upward communication is by function field** (`OnChange`, `OnCursorMove`, `OnCompletionRequest`, …), not by interface.
109+- **Dialogs are asynchronous**: `pushModal(dialog, onClose)`, settled after each event. There is no nested event loop.
110+- **In a dialog, arrows reach the focused control before they move the focus.** Reversing this makes every list box unusable by keyboard — it was a real bug, fixed and covered by tests.
111+- **In the Open / Save As box, the Name field mirrors the list highlight.** `ListBox.OnSelect` writes the highlighted entry into the field, and `confirm` falls back to the highlight when the field is empty. Without the wiring the two controls are independent and **OK does nothing at all** on a freshly opened dialog: the user has highlighted a file, the field is still empty, `Path()` returns `""`, and the button looks broken. Reported by the user, fixed 2026-08-31.
112+- **Agent windows are turbo-core's, and what belongs here is the starter file.** `acp.toml.tmpl` is the fourth embedded template, and `profile.Templates.Agents` is the whole of turbo-go's contribution to the feature — the example agent is `docker agent serve acp .turbo-go/agent.yaml`, which is a choice about what a Go developer is likely to have installed, not about the protocol. Every other editor gets agent windows by writing a starter file of its own and nothing else. The reasoning, and why it could not have been built here, is in `docs/*/explanation/agent-windows.md`.
113+- **The starter agents file teaches the window's keyboard as well as the format.** `Enter`, `Alt-Enter`, `Tab`, `Esc` and `Ctrl-W` are all in its comments, because a file the editor hands you is the one document a user is guaranteed to see.
114+
115+## Build, test, run
116+
117+```bash
118+make install # build + install onto PATH (scripts/install.sh)
119+make uninstall # remove it again
120+make build # → bin/turbo-go
121+make test # the whole suite; the single documented command
122+make check # fmt + vet + test — what a commit should pass
123+make run FILE=main.go
124+go test -short ./... # skips the test that starts a real gopls
125+go test ./internal/golang/
126+```
127+
128+Quality gate, separate from the tests:
129+
130+```bash
131+python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .
132+```
133+
134+Reports land in `.quality/`; exit code 0 = pass.
135+
136+## State as of 2026-09-01
137+
138+- **Migrated onto turbo-core.** `internal/*` is gone; `main.go` and `internal/golang` remain. The full existing test suite passes unchanged, plus the two real-gopls tests that came back here with the language they are about.
139+- **Quality gate: PASS.** 0/0/0, complexity 37 — the number fell from 1592 because the code moved, not because anything was simplified.
140+- **Released as v0.2.2** at `d64410c`, which is exactly HEAD, with a release page on Codeberg. The first release built on the library.
141+- **Depends on `turbo-core v0.2.0`**, with no active `replace`. On `main` that is v0.1.0 and builds from the module proxy. On `feature/more-syntaxes` the `require` names **v0.2.0, which is not published yet**: that branch does not build until turbo-core is tagged and released, and `go.sum` has no entry for it. The old replace block is still there, commented out, as the documented way to develop across the three repositories.
142+- **Nothing about the editor's behaviour changed.** The menus, the keys, the themes, the file formats and the environment variables are what they were.
143+
144+## State as of 2026-08-31 (second session)
145+
146+- **Feature-complete against the original request**, plus terminal windows. Editor, Go colouring, themes, LSP completion and `F8` shell windows all implemented.
147+- **Terminal windows (ticket 0007) implemented on 2026-08-31**, and **merged into `main`** by the user as PR #1. New `internal/terminal` package — pseudo-terminal, VT/ANSI emulator, view widget — wired into `app` as `Window ▸ New terminal` / `F8`.
148+- **Project settings (ticket 0002) implemented on 2026-08-31** and **merged into `main`** as PR #2. New `internal/settings` package; TOML colouring in `internal/syntax`; autosave in `internal/app`; `Options ▸ Create project settings` and `Options ▸ Project settings…`. Both tickets have since been closed by the user.
149+- **Run in a real terminal once**, by the user, which found two defects — an invisible cursor under `turbo-dark`, and completion returning nothing. Both fixed and covered; see the handoff of the same date.
150+- **Project tree (ticket 0003) implemented on 2026-08-31** and **merged into `main`** as PR #4. New `internal/filetree` package; `Window ▸ Project tree` / `F9`.
151+- **Window frame boxes** (`[x]` closes, `[■]`/`[▬]` maximises) and the **Open-dialog OK fix** were merged as PR #3.
152+- **Markdown, JavaScript, HTML and shell colouring (tickets 0009, 0013, 0014) implemented on 2026-08-31** and **merged into `main`** as PR #5.
153+- **Snippets (ticket 0006) implemented on 2026-08-31** and **merged into `main`** as PR #6.
154+- **The Go menu (ticket 0017) implemented on 2026-08-31** and **merged into `main`** as PR #7, from `feature/go-format-lint`. New `internal/tools` and `internal/projectfile`; `terminal.Options.Args` and `terminal.ViewOptions`; `Buffer.Reload`; a `Go` menu on `Alt-G`; three output destinations; a `menu` key putting tools into menus of their own; the `Screen.Resize` fix. The user **closed ticket 0017** on 2026-08-31 without the `go mod init + touch main.go` idea it also mentioned, so that idea is settled rather than outstanding.
155+- **Tests**: every package green, and green under `-race`. Coverage — version 98.3 %, editor 96.2 %, syntax 96.0 %, terminal 95.9 %, buffer 95.8 %, tools 95.7 %, filetree 94.7 %, theme 94.3 %, settings 93.8 %, ui 93.5 %, snippets 88.9 %, lsp 87.0 %, app 86.3 %, projectfile 75.0 %, main 31.7 %.
156+- **Quality gate: PASS.** 0 lint errors, 0 warnings, 0 code smells, total complexity 1592.
157+- **Project settings verified end to end against the real binary in a pty**: `settings.toml` picking `turbo-dark` (seen as `ESC]12;#ffd787` on the wire), `-theme turbo-classic` overriding it, and autosave writing a file **from the idle timer alone** — the editor was killed without ever quitting cleanly, so no close-or-quit path could have written it. The control run, with no settings file, left the file untouched.
158+- **Verified against a real gopls 0.23.0**, twice over: `internal/lsp` drives the protocol directly, and `internal/app` replays the command's own start-up order and completes text that exists **only in the buffer** — which is the only version of that test that can fail.
159+- **Docs**: 31 pages × EN + FR under `docs/`, plus a per-package `README.md` and the drawio diagram. The diagram was checked against `go list` programmatically and matches edge for edge.
160+- **Moved from Codeberg to Rickub on 2026-09-19.** Module path `rickub.com/turbo-editors/turbo-go`, depends on `rickub.com/turbo-editors/turbo-core v1.0.0` (the first turbo-core version published under that path; `v0.9.0` on the proxy still declares the Codeberg path and cannot be required as `rickub.com/…`). Every import, the Makefile's `VERSION_PKG`, `scripts/install.sh`, the README and the docs say `rickub.com`; `go.sum` verified against the proxy; `GOWORK=off make check` green. The repository on this side is a fresh `git init` with `origin` at `ssh://git@rickub.com/turbo-editors/turbo-go.git` and **no commit yet**; `01-release.tag.sh` makes the first one.
161+- **Releases are cut by one script and one workflow (since 2026-09-19).** `01-release.tag.sh` runs `make check`, refuses a tag taken locally or on origin, refuses a `replace` in `go.mod`, commits, pushes the branch, then tags and pushes the tag. That push starts `.github/workflows/release.yml`, which runs the suite, builds with `02-build-releases.sh` and publishes the release page with the binaries using the job's own `GITHUB_TOKEN`. `02-release.publish.sh` and `04-release.upload-binaries.sh` are gone — no personal token, no `turbo-go.token.env`. `release.env` (untracked) holds only `TAG` and `ABOUT`. **`01` had no `set -e`** once, so a `git tag` refusing an existing tag was skipped in silence and the following `git push` pushed the *old* tag — a release cut from a commit nobody meant; that guard is still there, and `release_test.go` now runs `01` for real against a throwaway clone.
162+- **`scripts/install.sh`** builds and installs onto the user's PATH, reporting the Go version, the destination, whether it is on PATH, and whether gopls is there. Thirteen tests in `install_test.go` drive it for real, including that a failed build leaves an existing installation untouched and that a reinstall **replaces** the binary rather than writing into it.
163+
164+## Traps worth knowing
165+
166+- **LSP columns are UTF-16 code units**, editor columns are runes. Everything crossing that boundary goes through `RuneToUTF16` / `UTF16ToRune`. On ASCII the two agree, so a mistake here survives testing until a file has an accent in it.
167+- **gopls asks its client questions.** It requests `workspace/configuration` during start-up and waits for the reply; a client that ignores server-to-client requests hangs with no error. `Client.handleRequest` answers them.
168+- **An empty completion list is usually a package that does not compile.** gopls answers nothing at all — no error — for a package it cannot load. `App.noCompletionsReason` and `App.languageReport` turn that into a sentence naming the problem.
169+- **A test whose fixture already contains the text being completed proves nothing.** gopls answers from disk for anything it has not been told is open, so such a test passes whether or not the editor said a word. The text must be *typed* into the buffer.
170+- **`len()` on a string with box-drawing characters counts bytes.** `"[■]"` is 5 bytes and 3 columns — that was a real bug in the close box, and the same characters are now the maximise box. Count runes for anything that becomes a screen width; `boxWidth` and a test hold the two in step.
171+- **A `ListBox` callback that nobody wires is invisible.** `OnSelect` existed, worked, and was covered by its own test in `internal/ui` — and `FileDialog` never set it, so the field and the list drifted apart with every test still green. A callback with no caller is not a feature.
172+- **`strings.Index` on a drawn screen row gives a byte offset, not a column.** A row is full of `░` and `║` at three bytes each, so clicking at that offset lands about thirty columns right of the target. This cost a wrong diagnosis: a test failure that looked like a second bug in the app was the test's own arithmetic.
173+- **`drawNumber` runs after `drawTitleBar`, so the number always wins.** A test that only checks the furniture survived is therefore vacuous — it would pass with the title margin off by one, because the number is simply repainted over the title. What breaks is the *title*, which loses a character and reads `main.g7`. Assert on the cell **beside** the furniture, not on the furniture.
174+- **A pseudo-terminal echoes the command line.** A test that types `echo red` and waits for `red` passes *before* the shell has run anything. Wait for something only the output can produce — a colour, or a string the typed line spells differently (`echo turbo''-go-works``turbo-go-works`).
175+- **Drawing tests must not race the shell's startup output.** A test asserting on a screen cell while a live shell writes to it passes or fails by luck; one such test hid a real fault for a whole session. `terminal.newOfflineView` builds a `View` with no session behind it — `Draw` needs none — and is deterministic.
176+- **The terminal cursor is drawn over cell (0,0) of a fresh screen.** A drawing test that samples the top-left cell is sampling the cursor, not the text.
177+- **A settings key absent is not the same as a key set to its default.** `settings.file` uses pointer fields for exactly this: `autosave = false` written out on purpose and no `autosave` line must not be the same statement, or a future default flip would silently change existing projects.
178+- **`emit` dropping empty spans makes "patch the span afterwards" unsafe.** In the TOML scanner, fixing up `spans[len-1].Start` after an emit that produced nothing rewrote the *previous* span instead. It bit a second time in the JavaScript scanner, differently: `finishTemplate` called a helper that ran the position to the end of the line, then asked `takeRest` to colour "what is left", which was nothing — a whole line of a template literal came back uncoloured. **Pass a span's start in as a parameter.** Never derive it from the scanner's position after a helper has moved it.
179+- **A test that drives the step under test proves nothing.** Twice now: `waitForLoopTurn` called `a.reloadAfterTools()` itself, so the test passed with that step removed from the event loop — fixed by extracting `App.tick` and having tests take a whole loop turn. And `TestStopEndsWhatTheCommandStartedToo` killed the shell before it had forked, so it passed without the process-group fix — fixed by waiting for the child to print. **Verify a new test by breaking the code it covers.**
180+- **A goroutine started by a constructor may not have its callbacks assigned afterwards.** `terminal.NewView` started the reading goroutine and callers then set `OnChange`/`OnExit` — a data race that existed from the terminal feature onward and that `-race` never caught, because a shell takes longer to produce its first output than the assignment takes to run. It surfaced only when a command finished immediately (`sh -c "echo x"`). The fix is `ViewOptions`: everything the goroutines read is set before they start. **When adding a constructor that starts a goroutine, take its callbacks as parameters.**
181+- **Two menus sharing a hot key make one of them unreachable, silently.** `handleClosedKey` returns on the first match. `Alt-S` opened Search rather than Snippets and every test passed; it was found by driving the real binary. `TestNoTwoMenusShareAHotKey` now covers it.
182+- **The theme completeness test only really constrains `turbo-classic`.** `turbo-dark` and `borland-light` both `inherits = "turbo-classic"`, and inheritance is resolved at parse time into the theme's own map, so `Defines` is true for an inherited key. Deleting a key from a child theme passes; deleting it from `turbo-classic` fails. Check against the base theme when verifying that test.
183+- **Colours that clash are only visible on a screen.** `syntax.link` was set to lime in `turbo-classic` — the exact colour of `syntax.string`, making a Markdown link indistinguishable from an inline `code` span. Every test passed. It was caught by rendering the editor through the project's own VT emulator and reading back the foreground of each run.
184+- **`tcell.KeyCtrlC` is 67, not 3.** tcell reports control bytes as `KeyCtrlSpace + b`, and `KeyCtrlSpace` is 64. Code that tests `key < 0x20` to spot a control key silently matches nothing.
185+- **`.qlty/qlty.toml` excludes `kits/**`** with a comment saying exactly what that hides. It hides two real defects in the kit's own `quality_report.py`, which belong to the kit. Do not widen the pattern.
186+
187+## Release tooling
188+
189+`01-release.tag.sh` and `02-build-releases.sh` in the repository root, plus `.github/workflows/release.yml`, modelled on turbo-core's. `release.env` carries `TAG` and `ABOUT` and is gitignored (`*.env`); there is no token file any more.
190+
191+`01` tags; the tag push starts the workflow. It exports `TURBO_GO_RELEASING=1` before `make check` so the tests that run `01` against a throwaway clone do not recurse; the workflow sets the same variable for its `go test` step. `02-build-releases.sh` cross-compiles for the platforms in its own `PLATFORMS` array and writes `release/${TAG}/` (binaries, `SHA256SUMS`, `README.md`); adding a target is one line and the checksums and README follow. It takes the tag as its first argument (what CI does, having no `release.env`), validates it as `vX.Y.Z[-pre]`, refuses a `replace` in `go.mod`, and starts from an empty `release/${TAG}/`. The workflow attaches `turbo-go-*`, `SHA256SUMS` and `README.md` with `softprops/action-gh-release@v2`, `fail_on_unmatched_files: true`, and writes release notes from the tag's message with the docs linked at that tag. Rickub's release API takes only the job's `GITHUB_TOKEN` (a personal token is refused), and its dispatch API fires every dispatchable workflow of a ref — hence `contents: write`, no `secrets.`, and no `workflow_dispatch`.
192+
193+## Not yet established
194+
195+- **Agent windows work, and nobody has typed into one by hand.** The whole path was driven against a real `docker agent` v1.139.0 and a real llama.cpp (JetBrains Mellum2) by running the binary in a pty: the `Alt-A` menu, the window, a streamed reply, a shell tool call, the permission dialog answered, and a ```go fence coloured span by span read back off the wire. The mouse, `Tab` between the panes, resizing mid-turn and two agents side by side are all untried. Detail in the handoff of 2026-09-15.
196+
197+
198+- **Barely run in a real terminal.** A few sessions by the user, plus scripted pty runs here. **Verified on a real pty**: the menu bar via F10, Alt-F, Alt-E and the mouse; and live resize via SIGWINCH, where the window's frame measured 78, then 98, then 48 cells as the terminal went 80 → 100 → 50. Also verified on the wire: the cursor escapes, `ESC[2 q` and `ESC]12;<colour>`, and tcell's restoration of both on exit. Still **unverified**: mouse dragging and corner-resizing, and the light theme on an actual terminal emulator.
199+- **A reinstall on macOS was broken until 2026-08-31**, and the diagnosis was made from the symptom rather than reproduced: this sandbox is Linux. If `the installed binary does not run` ever returns, the installer now prints the system's own message above it — read that before theorising.
200+- **Windows and macOS are untested.** The code paths exist (drive letters in `lsp.PathToURI`, `os.UserConfigDir`) but have only been exercised on Linux/arm64. `internal/terminal/pty_darwin.go` in particular **compiles and passes `go vet` but has never been run** — its `TIOCPTYGRANT` / `TIOCPTYUNLK` / `TIOCPTYGNAME` path is unverified. Terminal windows are **not implemented at all on Windows**: `pty_other.go` returns `ErrUnsupported` and `F8` says so; ticket 0015 tracks the ConPTY port.
201+- **No performance measurement.** The syntax cache means a full re-scan per change rather than per redraw, but nothing has been profiled. Behaviour on a file of tens of thousands of lines is unknown.
202+- **Diagnostics are stored but barely shown.** `Language.Diagnostics` keeps them per file and the status bar shows the first error; there is no marker in the gutter or under the offending text.
203+- **No CI.** There is no pipeline configuration in the repository.
204+- **Terminal windows have never been used in a real terminal.** Everything about them was verified against a real pty here — a shell really runs, `ls` really lists — but nobody has yet opened one inside `turbo-go` on a physical terminal and run `vim` or `htop` in it.
205+- **The project tree has never been driven by a human.** It was verified by rendering the real binary through the project's own VT emulator — `F9` lists the project with `.git` hidden, `→` nests two levels, `Enter` opens a file into a third window — but nobody has yet clicked a row or scrolled it with a real mouse.
206+- **`.tickets/` holds 20 issues**, `0002``0021`, most with an empty `body`. Twelve are closed (`0002`, `0003`, `0004`, `0006`, `0007`, `0009`, `0013`, `0014`, `0017`, `0019`, `0020`, `0021`); eight remain open: `0005` wasm plugins, `0008` a mini agent view, `0010` a version number in About (**implemented**), `0011` a website, `0012` more themes (**implemented**), `0015` Windows support for terminal windows, `0016` no shadow on tiled windows, `0018` a core library extracted from Turbo Go. `0010` (a version number in About) was **implemented on 2026-08-31** and is the user's to close. **The user opens and closes these; do not edit them.** The schema also carries an `epic:` field on some tickets — `.tickets/epics.yaml` lists the epics.
207+- **Autosave has never been used for a whole working session.** It was verified end to end in a pty, but nobody has yet spent an hour editing with it on, which is where a save at an unwanted moment would show up.
208+- **The window boxes have not been clicked by a human.** Both were verified by rendering the real binary through the project's own VT emulator — the frame reads `[x] … 1═[■]`, Window ▸ Maximise flips it to `[▬]` and fills the terminal, and a second use restores it — but nobody has yet pressed either box with an actual mouse.
209+- **`ui.Menu` has no nested submenus.** `MenuItem` has no `Items` field, so the project-settings entries are two flat items under Options rather than the submenu originally asked for. Adding nesting is a `ui` change nobody has asked for yet.
added .qlty/.gitignore +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+*
2+!configs
3+!configs/**
4+!hooks
5+!hooks/**
6+!qlty.toml
7+!.gitignore
new file mode 100644
@@ -0,0 +1,7 @@
1+*
2+!configs
3+!configs/**
4+!hooks
5+!hooks/**
6+!qlty.toml
7+!.gitignore
added .qlty/qlty.toml +75 -0
new file mode 100644
@@ -0,0 +1,75 @@
1+# This file was automatically generated by `qlty init`.
2+# You can modify it to suit your needs.
3+# We recommend you to commit this file to your repository.
4+#
5+# This configuration is used by both Qlty CLI and Qlty Cloud.
6+#
7+# Qlty CLI -- Code quality toolkit for developers
8+# Qlty Cloud -- Fully automated Code Health Platform
9+#
10+# Try Qlty Cloud: https://qlty.sh
11+#
12+# For a guide to configuration, visit https://qlty.sh/d/config
13+# Or for a full reference, visit https://qlty.sh/d/qlty-toml
14+config_version = "0"
15+
16+exclude_patterns = [
17+ "*_min.*",
18+ "*-min.*",
19+ "*.min.*",
20+ "**/.yarn/**",
21+ "**/*.d.ts",
22+ "**/assets/**",
23+ "**/bower_components/**",
24+ "**/build/**",
25+ "**/cache/**",
26+ "**/config/**",
27+ "**/db/**",
28+ "**/deps/**",
29+ "**/dist/**",
30+ "**/extern/**",
31+ "**/external/**",
32+ "**/generated/**",
33+ "**/Godeps/**",
34+ "**/gradlew/**",
35+ "**/mvnw/**",
36+ "**/node_modules/**",
37+ "**/protos/**",
38+ "**/seed/**",
39+ "**/target/**",
40+ "**/templates/**",
41+ "**/testdata/**",
42+ "**/vendor/**",
43+
44+ # The vendored kit sources, which are installed into ~/.claude rather than
45+ # built here. quality_report.py is the script that *produces* this
46+ # measurement, so leaving it in scope means measuring the instrument with
47+ # itself. What this pattern hides, as of qlty 0.639.0:
48+ # qlty:function-parameters build_report has 7 parameters
49+ # qlty:file-complexity quality_report.py, total complexity 73
50+ # Both are real defects, and both belong to the kit: they are to be fixed at
51+ # the kit's own source, not silenced here for good.
52+ "kits/**",
53+]
54+
55+test_patterns = [
56+ "**/test/**",
57+ "**/spec/**",
58+ "**/*.test.*",
59+ "**/*.spec.*",
60+ "**/*_test.*",
61+ "**/*_spec.*",
62+ "**/test_*.*",
63+ "**/spec_*.*",
64+]
65+
66+[smells]
67+mode = "comment"
68+
69+[[source]]
70+name = "default"
71+default = true
72+
73+
74+[[plugin]]
75+name = "trufflehog"
new file mode 100644
@@ -0,0 +1,75 @@
1+# This file was automatically generated by `qlty init`.
2+# You can modify it to suit your needs.
3+# We recommend you to commit this file to your repository.
4+#
5+# This configuration is used by both Qlty CLI and Qlty Cloud.
6+#
7+# Qlty CLI -- Code quality toolkit for developers
8+# Qlty Cloud -- Fully automated Code Health Platform
9+#
10+# Try Qlty Cloud: https://qlty.sh
11+#
12+# For a guide to configuration, visit https://qlty.sh/d/config
13+# Or for a full reference, visit https://qlty.sh/d/qlty-toml
14+config_version = "0"
15+
16+exclude_patterns = [
17+ "*_min.*",
18+ "*-min.*",
19+ "*.min.*",
20+ "**/.yarn/**",
21+ "**/*.d.ts",
22+ "**/assets/**",
23+ "**/bower_components/**",
24+ "**/build/**",
25+ "**/cache/**",
26+ "**/config/**",
27+ "**/db/**",
28+ "**/deps/**",
29+ "**/dist/**",
30+ "**/extern/**",
31+ "**/external/**",
32+ "**/generated/**",
33+ "**/Godeps/**",
34+ "**/gradlew/**",
35+ "**/mvnw/**",
36+ "**/node_modules/**",
37+ "**/protos/**",
38+ "**/seed/**",
39+ "**/target/**",
40+ "**/templates/**",
41+ "**/testdata/**",
42+ "**/vendor/**",
43+
44+ # The vendored kit sources, which are installed into ~/.claude rather than
45+ # built here. quality_report.py is the script that *produces* this
46+ # measurement, so leaving it in scope means measuring the instrument with
47+ # itself. What this pattern hides, as of qlty 0.639.0:
48+ # qlty:function-parameters build_report has 7 parameters
49+ # qlty:file-complexity quality_report.py, total complexity 73
50+ # Both are real defects, and both belong to the kit: they are to be fixed at
51+ # the kit's own source, not silenced here for good.
52+ "kits/**",
53+]
54+
55+test_patterns = [
56+ "**/test/**",
57+ "**/spec/**",
58+ "**/*.test.*",
59+ "**/*.spec.*",
60+ "**/*_test.*",
61+ "**/*_spec.*",
62+ "**/test_*.*",
63+ "**/spec_*.*",
64+]
65+
66+[smells]
67+mode = "comment"
68+
69+[[source]]
70+name = "default"
71+default = true
72+
73+
74+[[plugin]]
75+name = "trufflehog"
added .quality/history.jsonl +56 -0
new file mode 100644
@@ -0,0 +1,56 @@
1+{"branch": "main", "breaches": ["code smells: 12 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 74, "complex": 878, "cyclo": 1958, "fields": 227, "funcs": 616, "lcom": 0, "lines": 8779, "loc": 6026}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 1, "smells": 12, "timestamp": "2026-08-30T16:41:01Z"}
2+{"branch": "main", "breaches": ["code smells: 4 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 74, "complex": 882, "cyclo": 1952, "fields": 227, "funcs": 624, "lcom": 0, "lines": 8871, "loc": 6073}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 2, "smells": 4, "timestamp": "2026-08-30T16:43:18Z"}
3+{"branch": "main", "breaches": ["code smells: 2 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 74, "complex": 875, "cyclo": 1937, "fields": 227, "funcs": 623, "lcom": 0, "lines": 8871, "loc": 6069}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 3, "smells": 2, "timestamp": "2026-08-30T16:43:39Z"}
4+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 74, "complex": 802, "cyclo": 1854, "fields": 227, "funcs": 596, "lcom": 0, "lines": 8408, "loc": 5707}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 4, "smells": 0, "timestamp": "2026-08-30T16:45:20Z"}
5+{"branch": "main", "breaches": ["code smells: 1 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 75, "complex": 810, "cyclo": 1861, "fields": 228, "funcs": 601, "lcom": 0, "lines": 8512, "loc": 5762}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 5, "smells": 1, "timestamp": "2026-08-30T18:00:37Z"}
6+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 75, "complex": 812, "cyclo": 1858, "fields": 228, "funcs": 602, "lcom": 0, "lines": 8524, "loc": 5765}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 6, "smells": 0, "timestamp": "2026-08-30T18:01:05Z"}
7+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 75, "complex": 823, "cyclo": 1879, "fields": 236, "funcs": 608, "lcom": 0, "lines": 8638, "loc": 5846}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 7, "smells": 0, "timestamp": "2026-08-30T18:18:11Z"}
8+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 75, "complex": 823, "cyclo": 1878, "fields": 236, "funcs": 607, "lcom": 0, "lines": 8631, "loc": 5841}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 8, "smells": 0, "timestamp": "2026-08-30T18:30:37Z"}
9+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 76, "complex": 832, "cyclo": 1900, "fields": 237, "funcs": 614, "lcom": 0, "lines": 8744, "loc": 5896}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 9, "smells": 0, "timestamp": "2026-08-30T18:42:53Z"}
10+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 76, "complex": 833, "cyclo": 1900, "fields": 237, "funcs": 616, "lcom": 0, "lines": 8768, "loc": 5903}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 10, "smells": 0, "timestamp": "2026-08-30T18:50:41Z"}
11+{"branch": "main", "breaches": [], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 76, "complex": 833, "cyclo": 1900, "fields": 237, "funcs": 616, "lcom": 0, "lines": 8768, "loc": 5904}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 11, "smells": 0, "timestamp": "2026-08-31T03:11:37Z"}
12+{"branch": "feature/terminal", "breaches": ["code smells: 4 (max 0)"], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 88, "complex": 1022, "cyclo": 2453, "fields": 289, "funcs": 760, "lcom": 0, "lines": 11123, "loc": 7405}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 12, "smells": 4, "timestamp": "2026-08-31T04:04:25Z"}
13+{"branch": "feature/terminal", "breaches": [], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 88, "complex": 1023, "cyclo": 2421, "fields": 293, "funcs": 767, "lcom": 0, "lines": 11184, "loc": 7421}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 13, "smells": 0, "timestamp": "2026-08-31T04:05:54Z"}
14+{"branch": "feature/terminal", "breaches": [], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 88, "complex": 1023, "cyclo": 2421, "fields": 293, "funcs": 767, "lcom": 0, "lines": 11184, "loc": 7421}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 14, "smells": 0, "timestamp": "2026-08-31T04:08:32Z"}
15+{"branch": "feature/project-settings", "breaches": ["code smells: 4 (max 0)"], "commit": "a0373dd", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 94, "complex": 1148, "cyclo": 2725, "fields": 308, "funcs": 824, "lcom": 0, "lines": 12305, "loc": 8107}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 15, "smells": 4, "timestamp": "2026-08-31T04:51:55Z"}
16+{"branch": "feature/project-settings", "breaches": [], "commit": "a0373dd", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 94, "complex": 1150, "cyclo": 2706, "fields": 308, "funcs": 829, "lcom": 0, "lines": 12340, "loc": 8119}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 16, "smells": 0, "timestamp": "2026-08-31T04:52:40Z"}
17+{"branch": "feature/windows-buttons", "breaches": [], "commit": "0ef19ec", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 94, "complex": 1156, "cyclo": 2730, "fields": 311, "funcs": 837, "lcom": 0, "lines": 12462, "loc": 8175}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 17, "smells": 0, "timestamp": "2026-08-31T05:21:16Z"}
18+{"branch": "feature/windows-buttons", "breaches": [], "commit": "0ef19ec", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 94, "complex": 1160, "cyclo": 2735, "fields": 311, "funcs": 838, "lcom": 0, "lines": 12499, "loc": 8190}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 18, "smells": 0, "timestamp": "2026-08-31T06:04:14Z"}
19+{"branch": "feature/treeview-window", "breaches": [], "commit": "70106fc", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 98, "complex": 1228, "cyclo": 2870, "fields": 326, "funcs": 881, "lcom": 0, "lines": 13198, "loc": 8617}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 19, "smells": 0, "timestamp": "2026-08-31T06:24:42Z"}
20+{"branch": "feature/new-syntaxes", "breaches": ["code smells: 4 (max 0)"], "commit": "386992e", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 103, "complex": 1388, "cyclo": 3241, "fields": 327, "funcs": 946, "lcom": 0, "lines": 14349, "loc": 9394}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 20, "smells": 4, "timestamp": "2026-08-31T07:13:43Z"}
21+{"branch": "feature/new-syntaxes", "breaches": [], "commit": "386992e", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 103, "complex": 1389, "cyclo": 3225, "fields": 327, "funcs": 946, "lcom": 0, "lines": 14359, "loc": 9389}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 21, "smells": 0, "timestamp": "2026-08-31T07:14:19Z"}
22+{"branch": "feature/snippets", "breaches": ["code smells: 1 (max 0)"], "commit": "2bdbb88", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 107, "complex": 1475, "cyclo": 3388, "fields": 337, "funcs": 988, "lcom": 0, "lines": 15188, "loc": 9911}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 22, "smells": 1, "timestamp": "2026-08-31T10:44:47Z"}
23+{"branch": "feature/snippets", "breaches": ["code smells: 1 (max 0)"], "commit": "2bdbb88", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 107, "complex": 1478, "cyclo": 3389, "fields": 337, "funcs": 988, "lcom": 0, "lines": 15198, "loc": 9916}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 23, "smells": 1, "timestamp": "2026-08-31T10:45:25Z"}
24+{"branch": "feature/snippets", "breaches": [], "commit": "2bdbb88", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 107, "complex": 1480, "cyclo": 3391, "fields": 337, "funcs": 988, "lcom": 0, "lines": 15208, "loc": 9918}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 24, "smells": 0, "timestamp": "2026-08-31T10:46:01Z"}
25+{"branch": "feature/go-format-lint", "breaches": ["code smells: 4 (max 0)"], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 111, "complex": 1528, "cyclo": 3484, "fields": 347, "funcs": 1010, "lcom": 0, "lines": 15800, "loc": 10252}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 25, "smells": 4, "timestamp": "2026-08-31T11:44:41Z"}
26+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 111, "complex": 1513, "cyclo": 3446, "fields": 347, "funcs": 1007, "lcom": 0, "lines": 15748, "loc": 10178}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 26, "smells": 0, "timestamp": "2026-08-31T11:46:21Z"}
27+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 111, "complex": 1513, "cyclo": 3446, "fields": 347, "funcs": 1007, "lcom": 0, "lines": 15748, "loc": 10178}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 27, "smells": 0, "timestamp": "2026-08-31T11:46:54Z"}
28+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1542, "cyclo": 3501, "fields": 361, "funcs": 1029, "lcom": 0, "lines": 16203, "loc": 10423}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 28, "smells": 0, "timestamp": "2026-08-31T14:07:09Z"}
29+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1543, "cyclo": 3506, "fields": 361, "funcs": 1033, "lcom": 0, "lines": 16263, "loc": 10447}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 29, "smells": 0, "timestamp": "2026-08-31T14:13:39Z"}
30+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1543, "cyclo": 3506, "fields": 361, "funcs": 1033, "lcom": 0, "lines": 16263, "loc": 10447}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 30, "smells": 0, "timestamp": "2026-08-31T16:12:06Z"}
31+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1543, "cyclo": 3506, "fields": 361, "funcs": 1033, "lcom": 0, "lines": 16262, "loc": 10447}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 31, "smells": 0, "timestamp": "2026-08-31T16:19:51Z"}
32+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 115, "complex": 1567, "cyclo": 3542, "fields": 366, "funcs": 1046, "lcom": 0, "lines": 16517, "loc": 10588}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 32, "smells": 0, "timestamp": "2026-08-31T16:45:33Z"}
33+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 115, "complex": 1567, "cyclo": 3542, "fields": 366, "funcs": 1046, "lcom": 0, "lines": 16517, "loc": 10588}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 33, "smells": 0, "timestamp": "2026-08-31T16:51:21Z"}
34+{"branch": "feature/about-version", "breaches": ["code smells: 1 (max 0)"], "commit": "88a4c38", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 116, "complex": 1591, "cyclo": 3625, "fields": 369, "funcs": 1061, "lcom": 0, "lines": 16769, "loc": 10721}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 34, "smells": 1, "timestamp": "2026-08-31T18:49:59Z"}
35+{"branch": "feature/about-version", "breaches": [], "commit": "88a4c38", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 35, "smells": 0, "timestamp": "2026-08-31T18:50:25Z"}
36+{"branch": "feature/about-version", "breaches": [], "commit": "88a4c38", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 36, "smells": 0, "timestamp": "2026-08-31T18:55:02Z"}
37+{"branch": "main", "breaches": [], "commit": "7f8b36a", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 37, "smells": 0, "timestamp": "2026-08-31T19:03:59Z"}
38+{"branch": "main", "breaches": [], "commit": "78ea819", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 38, "smells": 0, "timestamp": "2026-08-31T19:10:14Z"}
39+{"branch": "main", "breaches": [], "commit": "78ea819", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 39, "smells": 0, "timestamp": "2026-08-31T19:15:48Z"}
40+{"branch": "feature/theme-cappucino", "breaches": [], "commit": "9505d1d", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 40, "smells": 0, "timestamp": "2026-08-31T19:34:10Z"}
41+{"branch": "refactoring", "breaches": [], "commit": "209fed2", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 693, "loc": 461}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 41, "smells": 0, "timestamp": "2026-09-01T04:14:21Z"}
42+{"branch": "refactoring", "breaches": [], "commit": "209fed2", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 693, "loc": 461}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 42, "smells": 0, "timestamp": "2026-09-01T04:54:46Z"}
43+{"branch": "refactoring", "breaches": [], "commit": "209fed2", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 693, "loc": 461}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 43, "smells": 0, "timestamp": "2026-09-01T05:02:13Z"}
44+{"branch": "feature/tool-parameters", "breaches": [], "commit": "7273452", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 44, "smells": 0, "timestamp": "2026-09-01T06:36:40Z"}
45+{"branch": "feature/tool-parameters", "breaches": [], "commit": "7273452", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 45, "smells": 0, "timestamp": "2026-09-01T06:43:58Z"}
46+{"branch": "feature/tool-parameters", "breaches": [], "commit": "7273452", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 46, "smells": 0, "timestamp": "2026-09-01T07:03:55Z"}
47+{"branch": "feature/more-syntaxes", "breaches": [], "commit": "d64410c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 47, "smells": 0, "timestamp": "2026-09-01T12:04:38Z"}
48+{"branch": "feature/more-syntaxes", "breaches": [], "commit": "d64410c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 48, "smells": 0, "timestamp": "2026-09-01T12:14:57Z"}
49+{"branch": "feature/menu-theme-and-settings", "breaches": [], "commit": "4ebf977", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 714, "loc": 482}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 49, "smells": 0, "timestamp": "2026-09-01T15:31:09Z"}
50+{"branch": "feature/menu-theme-and-settings", "breaches": [], "commit": "4ebf977", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 714, "loc": 482}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 50, "smells": 0, "timestamp": "2026-09-01T16:09:49Z"}
51+{"branch": "feature/code-navigation", "breaches": [], "commit": "c348e02", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 715, "loc": 483}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 51, "smells": 0, "timestamp": "2026-09-02T05:02:09Z"}
52+{"branch": "feature/code-navigation", "breaches": [], "commit": "c348e02", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 715, "loc": 483}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 52, "smells": 0, "timestamp": "2026-09-02T06:13:01Z"}
53+{"branch": "main", "breaches": [], "commit": "16da3cb", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 573, "loc": 343}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 53, "smells": 0, "timestamp": "2026-09-02T19:13:51Z"}
54+{"branch": "main", "breaches": [], "commit": "16da3cb", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 573, "loc": 343}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 54, "smells": 0, "timestamp": "2026-09-02T19:18:21Z"}
55+{"branch": "feature/acp", "breaches": [], "commit": "cb86146", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 584, "loc": 345}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 55, "smells": 0, "timestamp": "2026-09-15T09:35:03Z"}
56+{"branch": "feature/acp", "breaches": [], "commit": "cb86146", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 584, "loc": 345}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 56, "smells": 0, "timestamp": "2026-09-15T16:57:10Z"}
new file mode 100644
@@ -0,0 +1,56 @@
1+{"branch": "main", "breaches": ["code smells: 12 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 74, "complex": 878, "cyclo": 1958, "fields": 227, "funcs": 616, "lcom": 0, "lines": 8779, "loc": 6026}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 1, "smells": 12, "timestamp": "2026-08-30T16:41:01Z"}
2+{"branch": "main", "breaches": ["code smells: 4 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 74, "complex": 882, "cyclo": 1952, "fields": 227, "funcs": 624, "lcom": 0, "lines": 8871, "loc": 6073}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 2, "smells": 4, "timestamp": "2026-08-30T16:43:18Z"}
3+{"branch": "main", "breaches": ["code smells: 2 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 74, "complex": 875, "cyclo": 1937, "fields": 227, "funcs": 623, "lcom": 0, "lines": 8871, "loc": 6069}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 3, "smells": 2, "timestamp": "2026-08-30T16:43:39Z"}
4+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 74, "complex": 802, "cyclo": 1854, "fields": 227, "funcs": 596, "lcom": 0, "lines": 8408, "loc": 5707}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 4, "smells": 0, "timestamp": "2026-08-30T16:45:20Z"}
5+{"branch": "main", "breaches": ["code smells: 1 (max 0)"], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 75, "complex": 810, "cyclo": 1861, "fields": 228, "funcs": 601, "lcom": 0, "lines": 8512, "loc": 5762}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 5, "smells": 1, "timestamp": "2026-08-30T18:00:37Z"}
6+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 75, "complex": 812, "cyclo": 1858, "fields": 228, "funcs": 602, "lcom": 0, "lines": 8524, "loc": 5765}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 6, "smells": 0, "timestamp": "2026-08-30T18:01:05Z"}
7+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 75, "complex": 823, "cyclo": 1879, "fields": 236, "funcs": 608, "lcom": 0, "lines": 8638, "loc": 5846}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 7, "smells": 0, "timestamp": "2026-08-30T18:18:11Z"}
8+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 75, "complex": 823, "cyclo": 1878, "fields": 236, "funcs": 607, "lcom": 0, "lines": 8631, "loc": 5841}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 8, "smells": 0, "timestamp": "2026-08-30T18:30:37Z"}
9+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 76, "complex": 832, "cyclo": 1900, "fields": 237, "funcs": 614, "lcom": 0, "lines": 8744, "loc": 5896}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 9, "smells": 0, "timestamp": "2026-08-30T18:42:53Z"}
10+{"branch": "main", "breaches": [], "commit": "a0b64c0", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 76, "complex": 833, "cyclo": 1900, "fields": 237, "funcs": 616, "lcom": 0, "lines": 8768, "loc": 5903}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 10, "smells": 0, "timestamp": "2026-08-30T18:50:41Z"}
11+{"branch": "main", "breaches": [], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 76, "complex": 833, "cyclo": 1900, "fields": 237, "funcs": 616, "lcom": 0, "lines": 8768, "loc": 5904}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 11, "smells": 0, "timestamp": "2026-08-31T03:11:37Z"}
12+{"branch": "feature/terminal", "breaches": ["code smells: 4 (max 0)"], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 88, "complex": 1022, "cyclo": 2453, "fields": 289, "funcs": 760, "lcom": 0, "lines": 11123, "loc": 7405}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 12, "smells": 4, "timestamp": "2026-08-31T04:04:25Z"}
13+{"branch": "feature/terminal", "breaches": [], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 88, "complex": 1023, "cyclo": 2421, "fields": 293, "funcs": 767, "lcom": 0, "lines": 11184, "loc": 7421}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 13, "smells": 0, "timestamp": "2026-08-31T04:05:54Z"}
14+{"branch": "feature/terminal", "breaches": [], "commit": "31ec686", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 88, "complex": 1023, "cyclo": 2421, "fields": 293, "funcs": 767, "lcom": 0, "lines": 11184, "loc": 7421}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 14, "smells": 0, "timestamp": "2026-08-31T04:08:32Z"}
15+{"branch": "feature/project-settings", "breaches": ["code smells: 4 (max 0)"], "commit": "a0373dd", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 94, "complex": 1148, "cyclo": 2725, "fields": 308, "funcs": 824, "lcom": 0, "lines": 12305, "loc": 8107}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 15, "smells": 4, "timestamp": "2026-08-31T04:51:55Z"}
16+{"branch": "feature/project-settings", "breaches": [], "commit": "a0373dd", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 94, "complex": 1150, "cyclo": 2706, "fields": 308, "funcs": 829, "lcom": 0, "lines": 12340, "loc": 8119}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 16, "smells": 0, "timestamp": "2026-08-31T04:52:40Z"}
17+{"branch": "feature/windows-buttons", "breaches": [], "commit": "0ef19ec", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 94, "complex": 1156, "cyclo": 2730, "fields": 311, "funcs": 837, "lcom": 0, "lines": 12462, "loc": 8175}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 17, "smells": 0, "timestamp": "2026-08-31T05:21:16Z"}
18+{"branch": "feature/windows-buttons", "breaches": [], "commit": "0ef19ec", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 94, "complex": 1160, "cyclo": 2735, "fields": 311, "funcs": 838, "lcom": 0, "lines": 12499, "loc": 8190}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 18, "smells": 0, "timestamp": "2026-08-31T06:04:14Z"}
19+{"branch": "feature/treeview-window", "breaches": [], "commit": "70106fc", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 98, "complex": 1228, "cyclo": 2870, "fields": 326, "funcs": 881, "lcom": 0, "lines": 13198, "loc": 8617}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 19, "smells": 0, "timestamp": "2026-08-31T06:24:42Z"}
20+{"branch": "feature/new-syntaxes", "breaches": ["code smells: 4 (max 0)"], "commit": "386992e", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 103, "complex": 1388, "cyclo": 3241, "fields": 327, "funcs": 946, "lcom": 0, "lines": 14349, "loc": 9394}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 20, "smells": 4, "timestamp": "2026-08-31T07:13:43Z"}
21+{"branch": "feature/new-syntaxes", "breaches": [], "commit": "386992e", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 103, "complex": 1389, "cyclo": 3225, "fields": 327, "funcs": 946, "lcom": 0, "lines": 14359, "loc": 9389}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 21, "smells": 0, "timestamp": "2026-08-31T07:14:19Z"}
22+{"branch": "feature/snippets", "breaches": ["code smells: 1 (max 0)"], "commit": "2bdbb88", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 107, "complex": 1475, "cyclo": 3388, "fields": 337, "funcs": 988, "lcom": 0, "lines": 15188, "loc": 9911}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 22, "smells": 1, "timestamp": "2026-08-31T10:44:47Z"}
23+{"branch": "feature/snippets", "breaches": ["code smells: 1 (max 0)"], "commit": "2bdbb88", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 107, "complex": 1478, "cyclo": 3389, "fields": 337, "funcs": 988, "lcom": 0, "lines": 15198, "loc": 9916}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 23, "smells": 1, "timestamp": "2026-08-31T10:45:25Z"}
24+{"branch": "feature/snippets", "breaches": [], "commit": "2bdbb88", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 107, "complex": 1480, "cyclo": 3391, "fields": 337, "funcs": 988, "lcom": 0, "lines": 15208, "loc": 9918}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 24, "smells": 0, "timestamp": "2026-08-31T10:46:01Z"}
25+{"branch": "feature/go-format-lint", "breaches": ["code smells: 4 (max 0)"], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 111, "complex": 1528, "cyclo": 3484, "fields": 347, "funcs": 1010, "lcom": 0, "lines": 15800, "loc": 10252}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 25, "smells": 4, "timestamp": "2026-08-31T11:44:41Z"}
26+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 111, "complex": 1513, "cyclo": 3446, "fields": 347, "funcs": 1007, "lcom": 0, "lines": 15748, "loc": 10178}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 26, "smells": 0, "timestamp": "2026-08-31T11:46:21Z"}
27+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 111, "complex": 1513, "cyclo": 3446, "fields": 347, "funcs": 1007, "lcom": 0, "lines": 15748, "loc": 10178}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 27, "smells": 0, "timestamp": "2026-08-31T11:46:54Z"}
28+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1542, "cyclo": 3501, "fields": 361, "funcs": 1029, "lcom": 0, "lines": 16203, "loc": 10423}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 28, "smells": 0, "timestamp": "2026-08-31T14:07:09Z"}
29+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1543, "cyclo": 3506, "fields": 361, "funcs": 1033, "lcom": 0, "lines": 16263, "loc": 10447}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 29, "smells": 0, "timestamp": "2026-08-31T14:13:39Z"}
30+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1543, "cyclo": 3506, "fields": 361, "funcs": 1033, "lcom": 0, "lines": 16263, "loc": 10447}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 30, "smells": 0, "timestamp": "2026-08-31T16:12:06Z"}
31+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 114, "complex": 1543, "cyclo": 3506, "fields": 361, "funcs": 1033, "lcom": 0, "lines": 16262, "loc": 10447}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 31, "smells": 0, "timestamp": "2026-08-31T16:19:51Z"}
32+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 115, "complex": 1567, "cyclo": 3542, "fields": 366, "funcs": 1046, "lcom": 0, "lines": 16517, "loc": 10588}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 32, "smells": 0, "timestamp": "2026-08-31T16:45:33Z"}
33+{"branch": "feature/go-format-lint", "breaches": [], "commit": "d44a159", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 115, "complex": 1567, "cyclo": 3542, "fields": 366, "funcs": 1046, "lcom": 0, "lines": 16517, "loc": 10588}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 33, "smells": 0, "timestamp": "2026-08-31T16:51:21Z"}
34+{"branch": "feature/about-version", "breaches": ["code smells: 1 (max 0)"], "commit": "88a4c38", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": false, "metrics": {"classes": 116, "complex": 1591, "cyclo": 3625, "fields": 369, "funcs": 1061, "lcom": 0, "lines": 16769, "loc": 10721}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 34, "smells": 1, "timestamp": "2026-08-31T18:49:59Z"}
35+{"branch": "feature/about-version", "breaches": [], "commit": "88a4c38", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 35, "smells": 0, "timestamp": "2026-08-31T18:50:25Z"}
36+{"branch": "feature/about-version", "breaches": [], "commit": "88a4c38", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 36, "smells": 0, "timestamp": "2026-08-31T18:55:02Z"}
37+{"branch": "main", "breaches": [], "commit": "7f8b36a", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 37, "smells": 0, "timestamp": "2026-08-31T19:03:59Z"}
38+{"branch": "main", "breaches": [], "commit": "78ea819", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 38, "smells": 0, "timestamp": "2026-08-31T19:10:14Z"}
39+{"branch": "main", "breaches": [], "commit": "78ea819", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 39, "smells": 0, "timestamp": "2026-08-31T19:15:48Z"}
40+{"branch": "feature/theme-cappucino", "breaches": [], "commit": "9505d1d", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 116, "complex": 1592, "cyclo": 3625, "fields": 369, "funcs": 1063, "lcom": 0, "lines": 16783, "loc": 10730}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 40, "smells": 0, "timestamp": "2026-08-31T19:34:10Z"}
41+{"branch": "refactoring", "breaches": [], "commit": "209fed2", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 693, "loc": 461}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 41, "smells": 0, "timestamp": "2026-09-01T04:14:21Z"}
42+{"branch": "refactoring", "breaches": [], "commit": "209fed2", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 693, "loc": 461}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 42, "smells": 0, "timestamp": "2026-09-01T04:54:46Z"}
43+{"branch": "refactoring", "breaches": [], "commit": "209fed2", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 693, "loc": 461}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 43, "smells": 0, "timestamp": "2026-09-01T05:02:13Z"}
44+{"branch": "feature/tool-parameters", "breaches": [], "commit": "7273452", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 44, "smells": 0, "timestamp": "2026-09-01T06:36:40Z"}
45+{"branch": "feature/tool-parameters", "breaches": [], "commit": "7273452", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 45, "smells": 0, "timestamp": "2026-09-01T06:43:58Z"}
46+{"branch": "feature/tool-parameters", "breaches": [], "commit": "7273452", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 46, "smells": 0, "timestamp": "2026-09-01T07:03:55Z"}
47+{"branch": "feature/more-syntaxes", "breaches": [], "commit": "d64410c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 47, "smells": 0, "timestamp": "2026-09-01T12:04:38Z"}
48+{"branch": "feature/more-syntaxes", "breaches": [], "commit": "d64410c", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 712, "loc": 480}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 48, "smells": 0, "timestamp": "2026-09-01T12:14:57Z"}
49+{"branch": "feature/menu-theme-and-settings", "breaches": [], "commit": "4ebf977", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 714, "loc": 482}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 49, "smells": 0, "timestamp": "2026-09-01T15:31:09Z"}
50+{"branch": "feature/menu-theme-and-settings", "breaches": [], "commit": "4ebf977", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 714, "loc": 482}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 50, "smells": 0, "timestamp": "2026-09-01T16:09:49Z"}
51+{"branch": "feature/code-navigation", "breaches": [], "commit": "c348e02", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 715, "loc": 483}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 51, "smells": 0, "timestamp": "2026-09-02T05:02:09Z"}
52+{"branch": "feature/code-navigation", "breaches": [], "commit": "c348e02", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 81, "fields": 9, "funcs": 24, "lcom": 0, "lines": 715, "loc": 483}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 52, "smells": 0, "timestamp": "2026-09-02T06:13:01Z"}
53+{"branch": "main", "breaches": [], "commit": "16da3cb", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 573, "loc": 343}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 53, "smells": 0, "timestamp": "2026-09-02T19:13:51Z"}
54+{"branch": "main", "breaches": [], "commit": "16da3cb", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 573, "loc": 343}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 54, "smells": 0, "timestamp": "2026-09-02T19:18:21Z"}
55+{"branch": "feature/acp", "breaches": [], "commit": "cb86146", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 584, "loc": 345}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 55, "smells": 0, "timestamp": "2026-09-15T09:35:03Z"}
56+{"branch": "feature/acp", "breaches": [], "commit": "cb86146", "counts": {}, "gate": {"max_error": 0, "max_smells": 0, "max_warning": 0}, "gate_passed": true, "metrics": {"classes": 2, "complex": 37, "cyclo": 79, "fields": 9, "funcs": 24, "lcom": 0, "lines": 584, "loc": 345}, "qlty_version": "qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)", "run": 56, "smells": 0, "timestamp": "2026-09-15T16:57:10Z"}
added .quality/report-20260830T164101Z.md +80 -0
new file mode 100644
@@ -0,0 +1,80 @@
1+# Quality report — 2026-08-30T16:41:01Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #1 (first recorded run)
7+
8+## Gate violations
9+
10+- code smells: 12 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **12** (vs previous: —)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/lsp/conn.go | 198 | Function with many returns (count = 6): Call |
31+| qlty:return-statements | internal/lsp/framing.go | 62 | Function with many returns (count = 6): readContentLength |
32+| qlty:file-complexity | internal/app/actions.go | 1 | High total complexity (count = 74) |
33+| qlty:boolean-logic | internal/editor/view.go | 407 | Complex binary expression |
34+| qlty:boolean-logic | internal/editor/view.go | 407 | Complex binary expression |
35+| qlty:return-statements | internal/editor/events.go | 43 | Function with many returns (count = 9): movementFor |
36+| qlty:return-statements | internal/editor/events.go | 110 | Function with many returns (count = 7): handleEditing |
37+| qlty:return-statements | internal/syntax/scan.go | 73 | Function with many returns (count = 9): classOf |
38+| qlty:return-statements | internal/syntax/scan.go | 97 | Function with many returns (count = 6): identifierClass |
39+| qlty:return-statements | internal/ui/dialog.go | 158 | Function with many returns (count = 7): HandleKey |
40+| qlty:function-parameters | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 365 | Function with many parameters (count = 7): build_report |
41+| qlty:file-complexity | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 1 | High total complexity (count = 73) |
42+
43+## Metrics (`qlty metrics`)
44+
45+| metric | total | vs previous |
46+|---|---|---|
47+| funcs | 616 | — |
48+| classes | 74 | — |
49+| fields | 227 | — |
50+| cyclo | 1958 | — |
51+| complex | 878 | — |
52+| lcom | 0 | — |
53+| lines | 8779 | — |
54+| loc | 6026 | — |
55+
56+### Most complex files
57+
58+| file | complex | cyclo | loc |
59+|---|---|---|---|
60+| internal/app/actions.go | 74 | 121 | 393 |
61+| kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 73 | 83 | 362 |
62+| internal/ui/menu.go | 46 | 102 | 255 |
63+| internal/editor/view.go | 45 | 122 | 316 |
64+| internal/theme/load.go | 45 | 74 | 220 |
65+| internal/lsp/conn.go | 39 | 58 | 235 |
66+| internal/ui/dialog.go | 35 | 62 | 151 |
67+| internal/app/app.go | 32 | 88 | 267 |
68+| internal/app/completion.go | 30 | 74 | 186 |
69+| internal/lsp/client.go | 30 | 61 | 260 |
70+| internal/ui/controls.go | 30 | 69 | 216 |
71+| internal/ui/window.go | 24 | 75 | 171 |
72+| internal/ui/desktop.go | 23 | 60 | 130 |
73+| internal/app/dialogs.go | 22 | 80 | 247 |
74+| internal/buffer/cursor.go | 21 | 54 | 108 |
75+
76+## Trend
77+
78+| run | timestamp | error | warning | smells | complex | gate |
79+|---|---|---|---|---|---|---|
80+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
new file mode 100644
@@ -0,0 +1,80 @@
1+# Quality report — 2026-08-30T16:41:01Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #1 (first recorded run)
7+
8+## Gate violations
9+
10+- code smells: 12 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **12** (vs previous: —)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/lsp/conn.go | 198 | Function with many returns (count = 6): Call |
31+| qlty:return-statements | internal/lsp/framing.go | 62 | Function with many returns (count = 6): readContentLength |
32+| qlty:file-complexity | internal/app/actions.go | 1 | High total complexity (count = 74) |
33+| qlty:boolean-logic | internal/editor/view.go | 407 | Complex binary expression |
34+| qlty:boolean-logic | internal/editor/view.go | 407 | Complex binary expression |
35+| qlty:return-statements | internal/editor/events.go | 43 | Function with many returns (count = 9): movementFor |
36+| qlty:return-statements | internal/editor/events.go | 110 | Function with many returns (count = 7): handleEditing |
37+| qlty:return-statements | internal/syntax/scan.go | 73 | Function with many returns (count = 9): classOf |
38+| qlty:return-statements | internal/syntax/scan.go | 97 | Function with many returns (count = 6): identifierClass |
39+| qlty:return-statements | internal/ui/dialog.go | 158 | Function with many returns (count = 7): HandleKey |
40+| qlty:function-parameters | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 365 | Function with many parameters (count = 7): build_report |
41+| qlty:file-complexity | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 1 | High total complexity (count = 73) |
42+
43+## Metrics (`qlty metrics`)
44+
45+| metric | total | vs previous |
46+|---|---|---|
47+| funcs | 616 | — |
48+| classes | 74 | — |
49+| fields | 227 | — |
50+| cyclo | 1958 | — |
51+| complex | 878 | — |
52+| lcom | 0 | — |
53+| lines | 8779 | — |
54+| loc | 6026 | — |
55+
56+### Most complex files
57+
58+| file | complex | cyclo | loc |
59+|---|---|---|---|
60+| internal/app/actions.go | 74 | 121 | 393 |
61+| kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 73 | 83 | 362 |
62+| internal/ui/menu.go | 46 | 102 | 255 |
63+| internal/editor/view.go | 45 | 122 | 316 |
64+| internal/theme/load.go | 45 | 74 | 220 |
65+| internal/lsp/conn.go | 39 | 58 | 235 |
66+| internal/ui/dialog.go | 35 | 62 | 151 |
67+| internal/app/app.go | 32 | 88 | 267 |
68+| internal/app/completion.go | 30 | 74 | 186 |
69+| internal/lsp/client.go | 30 | 61 | 260 |
70+| internal/ui/controls.go | 30 | 69 | 216 |
71+| internal/ui/window.go | 24 | 75 | 171 |
72+| internal/ui/desktop.go | 23 | 60 | 130 |
73+| internal/app/dialogs.go | 22 | 80 | 247 |
74+| internal/buffer/cursor.go | 21 | 54 | 108 |
75+
76+## Trend
77+
78+| run | timestamp | error | warning | smells | complex | gate |
79+|---|---|---|---|---|---|---|
80+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
added .quality/report-20260830T164318Z.md +73 -0
new file mode 100644
@@ -0,0 +1,73 @@
1+# Quality report — 2026-08-30T16:43:18Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #2 (previous: 2026-08-30T16:41:01Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: -8)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:boolean-logic | internal/editor/view.go | 416 | Complex binary expression |
31+| qlty:boolean-logic | internal/editor/view.go | 416 | Complex binary expression |
32+| qlty:function-parameters | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 365 | Function with many parameters (count = 7): build_report |
33+| qlty:file-complexity | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 1 | High total complexity (count = 73) |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 624 | +8 |
40+| classes | 74 | ±0 |
41+| fields | 227 | ±0 |
42+| cyclo | 1952 | -6 |
43+| complex | 882 | +4 |
44+| lcom | 0 | ±0 |
45+| lines | 8871 | +92 |
46+| loc | 6073 | +47 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 73 | 83 | 362 |
53+| internal/editor/view.go | 46 | 122 | 322 |
54+| internal/ui/menu.go | 46 | 102 | 255 |
55+| internal/theme/load.go | 45 | 74 | 220 |
56+| internal/app/actions_file.go | 39 | 64 | 195 |
57+| internal/lsp/conn.go | 39 | 58 | 238 |
58+| internal/ui/dialog.go | 33 | 64 | 166 |
59+| internal/app/app.go | 32 | 88 | 267 |
60+| internal/app/completion.go | 30 | 74 | 186 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/ui/controls.go | 30 | 69 | 216 |
63+| internal/ui/window.go | 24 | 75 | 171 |
64+| internal/app/actions_edit.go | 23 | 39 | 137 |
65+| internal/ui/desktop.go | 23 | 60 | 130 |
66+| internal/app/dialogs.go | 22 | 80 | 247 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
73+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
new file mode 100644
@@ -0,0 +1,73 @@
1+# Quality report — 2026-08-30T16:43:18Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #2 (previous: 2026-08-30T16:41:01Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: -8)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:boolean-logic | internal/editor/view.go | 416 | Complex binary expression |
31+| qlty:boolean-logic | internal/editor/view.go | 416 | Complex binary expression |
32+| qlty:function-parameters | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 365 | Function with many parameters (count = 7): build_report |
33+| qlty:file-complexity | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 1 | High total complexity (count = 73) |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 624 | +8 |
40+| classes | 74 | ±0 |
41+| fields | 227 | ±0 |
42+| cyclo | 1952 | -6 |
43+| complex | 882 | +4 |
44+| lcom | 0 | ±0 |
45+| lines | 8871 | +92 |
46+| loc | 6073 | +47 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 73 | 83 | 362 |
53+| internal/editor/view.go | 46 | 122 | 322 |
54+| internal/ui/menu.go | 46 | 102 | 255 |
55+| internal/theme/load.go | 45 | 74 | 220 |
56+| internal/app/actions_file.go | 39 | 64 | 195 |
57+| internal/lsp/conn.go | 39 | 58 | 238 |
58+| internal/ui/dialog.go | 33 | 64 | 166 |
59+| internal/app/app.go | 32 | 88 | 267 |
60+| internal/app/completion.go | 30 | 74 | 186 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/ui/controls.go | 30 | 69 | 216 |
63+| internal/ui/window.go | 24 | 75 | 171 |
64+| internal/app/actions_edit.go | 23 | 39 | 137 |
65+| internal/ui/desktop.go | 23 | 60 | 130 |
66+| internal/app/dialogs.go | 22 | 80 | 247 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
73+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
added .quality/report-20260830T164339Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-30T16:43:39Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #3 (previous: 2026-08-30T16:43:18Z)
7+
8+## Gate violations
9+
10+- code smells: 2 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **2** (vs previous: -2)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:function-parameters | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 365 | Function with many parameters (count = 7): build_report |
31+| qlty:file-complexity | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 1 | High total complexity (count = 73) |
32+
33+## Metrics (`qlty metrics`)
34+
35+| metric | total | vs previous |
36+|---|---|---|
37+| funcs | 623 | -1 |
38+| classes | 74 | ±0 |
39+| fields | 227 | ±0 |
40+| cyclo | 1937 | -15 |
41+| complex | 875 | -7 |
42+| lcom | 0 | ±0 |
43+| lines | 8871 | ±0 |
44+| loc | 6069 | -4 |
45+
46+### Most complex files
47+
48+| file | complex | cyclo | loc |
49+|---|---|---|---|
50+| kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 73 | 83 | 362 |
51+| internal/ui/menu.go | 46 | 102 | 255 |
52+| internal/theme/load.go | 45 | 74 | 220 |
53+| internal/app/actions_file.go | 39 | 64 | 195 |
54+| internal/editor/view.go | 39 | 107 | 318 |
55+| internal/lsp/conn.go | 39 | 58 | 238 |
56+| internal/ui/dialog.go | 33 | 64 | 166 |
57+| internal/app/app.go | 32 | 88 | 267 |
58+| internal/app/completion.go | 30 | 74 | 186 |
59+| internal/lsp/client.go | 30 | 61 | 260 |
60+| internal/ui/controls.go | 30 | 69 | 216 |
61+| internal/ui/window.go | 24 | 75 | 171 |
62+| internal/app/actions_edit.go | 23 | 39 | 137 |
63+| internal/ui/desktop.go | 23 | 60 | 130 |
64+| internal/app/dialogs.go | 22 | 80 | 247 |
65+
66+## Trend
67+
68+| run | timestamp | error | warning | smells | complex | gate |
69+|---|---|---|---|---|---|---|
70+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
71+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
72+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-30T16:43:39Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #3 (previous: 2026-08-30T16:43:18Z)
7+
8+## Gate violations
9+
10+- code smells: 2 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **2** (vs previous: -2)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:function-parameters | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 365 | Function with many parameters (count = 7): build_report |
31+| qlty:file-complexity | kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 1 | High total complexity (count = 73) |
32+
33+## Metrics (`qlty metrics`)
34+
35+| metric | total | vs previous |
36+|---|---|---|
37+| funcs | 623 | -1 |
38+| classes | 74 | ±0 |
39+| fields | 227 | ±0 |
40+| cyclo | 1937 | -15 |
41+| complex | 875 | -7 |
42+| lcom | 0 | ±0 |
43+| lines | 8871 | ±0 |
44+| loc | 6069 | -4 |
45+
46+### Most complex files
47+
48+| file | complex | cyclo | loc |
49+|---|---|---|---|
50+| kits/dev-toolkit/files/home/.claude/skills/quality/scripts/quality_report.py | 73 | 83 | 362 |
51+| internal/ui/menu.go | 46 | 102 | 255 |
52+| internal/theme/load.go | 45 | 74 | 220 |
53+| internal/app/actions_file.go | 39 | 64 | 195 |
54+| internal/editor/view.go | 39 | 107 | 318 |
55+| internal/lsp/conn.go | 39 | 58 | 238 |
56+| internal/ui/dialog.go | 33 | 64 | 166 |
57+| internal/app/app.go | 32 | 88 | 267 |
58+| internal/app/completion.go | 30 | 74 | 186 |
59+| internal/lsp/client.go | 30 | 61 | 260 |
60+| internal/ui/controls.go | 30 | 69 | 216 |
61+| internal/ui/window.go | 24 | 75 | 171 |
62+| internal/app/actions_edit.go | 23 | 39 | 137 |
63+| internal/ui/desktop.go | 23 | 60 | 130 |
64+| internal/app/dialogs.go | 22 | 80 | 247 |
65+
66+## Trend
67+
68+| run | timestamp | error | warning | smells | complex | gate |
69+|---|---|---|---|---|---|---|
70+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
71+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
72+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
added .quality/report-20260830T164520Z.md +66 -0
new file mode 100644
@@ -0,0 +1,66 @@
1+# Quality report — 2026-08-30T16:45:20Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #4 (previous: 2026-08-30T16:43:39Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -2)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 596 | -27 |
31+| classes | 74 | ±0 |
32+| fields | 227 | ±0 |
33+| cyclo | 1854 | -83 |
34+| complex | 802 | -73 |
35+| lcom | 0 | ±0 |
36+| lines | 8408 | -463 |
37+| loc | 5707 | -362 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 39 | 64 | 195 |
46+| internal/editor/view.go | 39 | 107 | 318 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/ui/dialog.go | 33 | 64 | 166 |
49+| internal/app/app.go | 32 | 88 | 267 |
50+| internal/app/completion.go | 30 | 74 | 186 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 24 | 75 | 171 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
new file mode 100644
@@ -0,0 +1,66 @@
1+# Quality report — 2026-08-30T16:45:20Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #4 (previous: 2026-08-30T16:43:39Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -2)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 596 | -27 |
31+| classes | 74 | ±0 |
32+| fields | 227 | ±0 |
33+| cyclo | 1854 | -83 |
34+| complex | 802 | -73 |
35+| lcom | 0 | ±0 |
36+| lines | 8408 | -463 |
37+| loc | 5707 | -362 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 39 | 64 | 195 |
46+| internal/editor/view.go | 39 | 107 | 318 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/ui/dialog.go | 33 | 64 | 166 |
49+| internal/app/app.go | 32 | 88 | 267 |
50+| internal/app/completion.go | 30 | 74 | 186 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 24 | 75 | 171 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
added .quality/report-20260830T180037Z.md +73 -0
new file mode 100644
@@ -0,0 +1,73 @@
1+# Quality report — 2026-08-30T18:00:37Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #5 (previous: 2026-08-30T16:45:20Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/ui/dialog.go | 162 | Function with many returns (count = 6): HandleKey |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 601 | +5 |
37+| classes | 75 | +1 |
38+| fields | 228 | +1 |
39+| cyclo | 1861 | +7 |
40+| complex | 810 | +8 |
41+| lcom | 0 | ±0 |
42+| lines | 8512 | +104 |
43+| loc | 5762 | +55 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/ui/menu.go | 46 | 102 | 255 |
50+| internal/theme/load.go | 45 | 74 | 220 |
51+| internal/editor/view.go | 40 | 110 | 333 |
52+| internal/app/actions_file.go | 39 | 64 | 195 |
53+| internal/lsp/conn.go | 39 | 58 | 238 |
54+| internal/app/app.go | 37 | 92 | 284 |
55+| internal/ui/dialog.go | 34 | 67 | 179 |
56+| internal/app/completion.go | 31 | 75 | 193 |
57+| internal/lsp/client.go | 30 | 61 | 260 |
58+| internal/ui/controls.go | 30 | 69 | 216 |
59+| internal/ui/window.go | 25 | 76 | 176 |
60+| internal/app/actions_edit.go | 23 | 39 | 137 |
61+| internal/ui/desktop.go | 23 | 60 | 130 |
62+| internal/app/dialogs.go | 22 | 80 | 247 |
63+| internal/buffer/cursor.go | 21 | 54 | 108 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
70+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
71+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
72+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
73+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
new file mode 100644
@@ -0,0 +1,73 @@
1+# Quality report — 2026-08-30T18:00:37Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #5 (previous: 2026-08-30T16:45:20Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/ui/dialog.go | 162 | Function with many returns (count = 6): HandleKey |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 601 | +5 |
37+| classes | 75 | +1 |
38+| fields | 228 | +1 |
39+| cyclo | 1861 | +7 |
40+| complex | 810 | +8 |
41+| lcom | 0 | ±0 |
42+| lines | 8512 | +104 |
43+| loc | 5762 | +55 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/ui/menu.go | 46 | 102 | 255 |
50+| internal/theme/load.go | 45 | 74 | 220 |
51+| internal/editor/view.go | 40 | 110 | 333 |
52+| internal/app/actions_file.go | 39 | 64 | 195 |
53+| internal/lsp/conn.go | 39 | 58 | 238 |
54+| internal/app/app.go | 37 | 92 | 284 |
55+| internal/ui/dialog.go | 34 | 67 | 179 |
56+| internal/app/completion.go | 31 | 75 | 193 |
57+| internal/lsp/client.go | 30 | 61 | 260 |
58+| internal/ui/controls.go | 30 | 69 | 216 |
59+| internal/ui/window.go | 25 | 76 | 176 |
60+| internal/app/actions_edit.go | 23 | 39 | 137 |
61+| internal/ui/desktop.go | 23 | 60 | 130 |
62+| internal/app/dialogs.go | 22 | 80 | 247 |
63+| internal/buffer/cursor.go | 21 | 54 | 108 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
70+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
71+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
72+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
73+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
added .quality/report-20260830T180105Z.md +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+# Quality report — 2026-08-30T18:01:05Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #6 (previous: 2026-08-30T18:00:37Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 602 | +1 |
31+| classes | 75 | ±0 |
32+| fields | 228 | ±0 |
33+| cyclo | 1858 | -3 |
34+| complex | 812 | +2 |
35+| lcom | 0 | ±0 |
36+| lines | 8524 | +12 |
37+| loc | 5765 | +3 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 37 | 92 | 284 |
49+| internal/ui/dialog.go | 36 | 64 | 182 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 25 | 76 | 176 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
new file mode 100644
@@ -0,0 +1,68 @@
1+# Quality report — 2026-08-30T18:01:05Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #6 (previous: 2026-08-30T18:00:37Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 602 | +1 |
31+| classes | 75 | ±0 |
32+| fields | 228 | ±0 |
33+| cyclo | 1858 | -3 |
34+| complex | 812 | +2 |
35+| lcom | 0 | ±0 |
36+| lines | 8524 | +12 |
37+| loc | 5765 | +3 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 37 | 92 | 284 |
49+| internal/ui/dialog.go | 36 | 64 | 182 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 25 | 76 | 176 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
added .quality/report-20260830T181811Z.md +69 -0
new file mode 100644
@@ -0,0 +1,69 @@
1+# Quality report — 2026-08-30T18:18:11Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #7 (previous: 2026-08-30T18:01:05Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 608 | +6 |
31+| classes | 75 | ±0 |
32+| fields | 236 | +8 |
33+| cyclo | 1879 | +21 |
34+| complex | 823 | +11 |
35+| lcom | 0 | ±0 |
36+| lines | 8638 | +114 |
37+| loc | 5846 | +81 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 37 | 92 | 283 |
49+| internal/ui/dialog.go | 36 | 64 | 182 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 25 | 76 | 176 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
new file mode 100644
@@ -0,0 +1,69 @@
1+# Quality report — 2026-08-30T18:18:11Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #7 (previous: 2026-08-30T18:01:05Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 608 | +6 |
31+| classes | 75 | ±0 |
32+| fields | 236 | +8 |
33+| cyclo | 1879 | +21 |
34+| complex | 823 | +11 |
35+| lcom | 0 | ±0 |
36+| lines | 8638 | +114 |
37+| loc | 5846 | +81 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 37 | 92 | 283 |
49+| internal/ui/dialog.go | 36 | 64 | 182 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 25 | 76 | 176 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
added .quality/report-20260830T183037Z.md +70 -0
new file mode 100644
@@ -0,0 +1,70 @@
1+# Quality report — 2026-08-30T18:30:37Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #8 (previous: 2026-08-30T18:18:11Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 607 | -1 |
31+| classes | 75 | ±0 |
32+| fields | 236 | ±0 |
33+| cyclo | 1878 | -1 |
34+| complex | 823 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 8631 | -7 |
37+| loc | 5841 | -5 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 37 | 92 | 283 |
49+| internal/ui/dialog.go | 36 | 64 | 182 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 25 | 76 | 176 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
70+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
new file mode 100644
@@ -0,0 +1,70 @@
1+# Quality report — 2026-08-30T18:30:37Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #8 (previous: 2026-08-30T18:18:11Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 607 | -1 |
31+| classes | 75 | ±0 |
32+| fields | 236 | ±0 |
33+| cyclo | 1878 | -1 |
34+| complex | 823 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 8631 | -7 |
37+| loc | 5841 | -5 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 37 | 92 | 283 |
49+| internal/ui/dialog.go | 36 | 64 | 182 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 25 | 76 | 176 |
54+| internal/app/actions_edit.go | 23 | 39 | 137 |
55+| internal/ui/desktop.go | 23 | 60 | 130 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
70+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
added .quality/report-20260830T184253Z.md +71 -0
new file mode 100644
@@ -0,0 +1,71 @@
1+# Quality report — 2026-08-30T18:42:53Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #9 (previous: 2026-08-30T18:30:37Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 614 | +7 |
31+| classes | 76 | +1 |
32+| fields | 237 | +1 |
33+| cyclo | 1900 | +22 |
34+| complex | 832 | +9 |
35+| lcom | 0 | ±0 |
36+| lines | 8744 | +113 |
37+| loc | 5896 | +55 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 38 | 93 | 290 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
70+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
71+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
new file mode 100644
@@ -0,0 +1,71 @@
1+# Quality report — 2026-08-30T18:42:53Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #9 (previous: 2026-08-30T18:30:37Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 614 | +7 |
31+| classes | 76 | +1 |
32+| fields | 237 | +1 |
33+| cyclo | 1900 | +22 |
34+| complex | 832 | +9 |
35+| lcom | 0 | ±0 |
36+| lines | 8744 | +113 |
37+| loc | 5896 | +55 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/app/app.go | 38 | 93 | 290 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
70+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
71+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
added .quality/report-20260830T185041Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-30T18:50:41Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #10 (previous: 2026-08-30T18:42:53Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 616 | +2 |
31+| classes | 76 | ±0 |
32+| fields | 237 | ±0 |
33+| cyclo | 1900 | ±0 |
34+| complex | 833 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 8768 | +24 |
37+| loc | 5903 | +7 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/app/app.go | 39 | 93 | 297 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
70+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
71+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
72+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-30T18:50:41Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0b64c0` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #10 (previous: 2026-08-30T18:42:53Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 616 | +2 |
31+| classes | 76 | ±0 |
32+| fields | 237 | ±0 |
33+| cyclo | 1900 | ±0 |
34+| complex | 833 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 8768 | +24 |
37+| loc | 5903 | +7 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/app/app.go | 39 | 93 | 297 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 1 | 2026-08-30T16:41:01Z | 0 | 0 | 12 | 878 | FAIL |
64+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
65+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
66+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
67+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
68+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
69+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
70+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
71+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
72+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
added .quality/report-20260831T031137Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T03:11:37Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `31ec686` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #11 (previous: 2026-08-30T18:50:41Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 616 | ±0 |
31+| classes | 76 | ±0 |
32+| fields | 237 | ±0 |
33+| cyclo | 1900 | ±0 |
34+| complex | 833 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 8768 | ±0 |
37+| loc | 5904 | +1 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/app/app.go | 39 | 93 | 297 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
64+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
65+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
66+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
67+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
68+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
69+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
70+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
71+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
72+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T03:11:37Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `31ec686` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #11 (previous: 2026-08-30T18:50:41Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 616 | ±0 |
31+| classes | 76 | ±0 |
32+| fields | 237 | ±0 |
33+| cyclo | 1900 | ±0 |
34+| complex | 833 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 8768 | ±0 |
37+| loc | 5904 | +1 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/editor/view.go | 40 | 110 | 333 |
46+| internal/app/actions_file.go | 39 | 64 | 195 |
47+| internal/app/app.go | 39 | 93 | 297 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 2 | 2026-08-30T16:43:18Z | 0 | 0 | 4 | 882 | FAIL |
64+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
65+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
66+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
67+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
68+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
69+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
70+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
71+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
72+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
added .quality/report-20260831T040425Z.md +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T04:04:25Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `31ec686` on `feature/terminal`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #12 (previous: 2026-08-31T03:11:37Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/app/app.go | 290 | Function with many returns (count = 6): handleKey |
31+| qlty:return-statements | internal/terminal/parser_sgr.go | 37 | Function with many returns (count = 7): applySGR |
32+| qlty:return-statements | internal/terminal/parser_sgr.go | 59 | Function with many returns (count = 17): applyAttribute |
33+| qlty:return-statements | internal/terminal/parser_sgr.go | 114 | Function with many returns (count = 6): extendedColor |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 760 | +144 |
40+| classes | 88 | +12 |
41+| fields | 289 | +52 |
42+| cyclo | 2453 | +553 |
43+| complex | 1022 | +189 |
44+| lcom | 0 | ±0 |
45+| lines | 11123 | +2355 |
46+| loc | 7405 | +1501 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/ui/menu.go | 46 | 102 | 255 |
53+| internal/app/app.go | 45 | 117 | 322 |
54+| internal/theme/load.go | 45 | 74 | 220 |
55+| internal/app/actions_file.go | 40 | 65 | 200 |
56+| internal/editor/view.go | 40 | 110 | 333 |
57+| internal/lsp/conn.go | 39 | 58 | 238 |
58+| internal/ui/dialog.go | 38 | 71 | 197 |
59+| internal/app/completion.go | 31 | 75 | 193 |
60+| internal/lsp/client.go | 30 | 61 | 260 |
61+| internal/ui/controls.go | 30 | 69 | 216 |
62+| internal/ui/window.go | 30 | 87 | 200 |
63+| internal/ui/desktop.go | 24 | 62 | 134 |
64+| internal/app/actions_edit.go | 23 | 39 | 137 |
65+| internal/app/dialogs.go | 22 | 80 | 247 |
66+| internal/buffer/cursor.go | 21 | 54 | 108 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
73+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
74+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
75+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
76+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
77+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
78+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
79+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
80+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
81+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T04:04:25Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `31ec686` on `feature/terminal`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #12 (previous: 2026-08-31T03:11:37Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/app/app.go | 290 | Function with many returns (count = 6): handleKey |
31+| qlty:return-statements | internal/terminal/parser_sgr.go | 37 | Function with many returns (count = 7): applySGR |
32+| qlty:return-statements | internal/terminal/parser_sgr.go | 59 | Function with many returns (count = 17): applyAttribute |
33+| qlty:return-statements | internal/terminal/parser_sgr.go | 114 | Function with many returns (count = 6): extendedColor |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 760 | +144 |
40+| classes | 88 | +12 |
41+| fields | 289 | +52 |
42+| cyclo | 2453 | +553 |
43+| complex | 1022 | +189 |
44+| lcom | 0 | ±0 |
45+| lines | 11123 | +2355 |
46+| loc | 7405 | +1501 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/ui/menu.go | 46 | 102 | 255 |
53+| internal/app/app.go | 45 | 117 | 322 |
54+| internal/theme/load.go | 45 | 74 | 220 |
55+| internal/app/actions_file.go | 40 | 65 | 200 |
56+| internal/editor/view.go | 40 | 110 | 333 |
57+| internal/lsp/conn.go | 39 | 58 | 238 |
58+| internal/ui/dialog.go | 38 | 71 | 197 |
59+| internal/app/completion.go | 31 | 75 | 193 |
60+| internal/lsp/client.go | 30 | 61 | 260 |
61+| internal/ui/controls.go | 30 | 69 | 216 |
62+| internal/ui/window.go | 30 | 87 | 200 |
63+| internal/ui/desktop.go | 24 | 62 | 134 |
64+| internal/app/actions_edit.go | 23 | 39 | 137 |
65+| internal/app/dialogs.go | 22 | 80 | 247 |
66+| internal/buffer/cursor.go | 21 | 54 | 108 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 3 | 2026-08-30T16:43:39Z | 0 | 0 | 2 | 875 | FAIL |
73+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
74+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
75+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
76+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
77+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
78+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
79+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
80+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
81+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
added .quality/report-20260831T040554Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T04:05:54Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `31ec686` on `feature/terminal`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #13 (previous: 2026-08-31T04:04:25Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 767 | +7 |
31+| classes | 88 | ±0 |
32+| fields | 293 | +4 |
33+| cyclo | 2421 | -32 |
34+| complex | 1023 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 11184 | +61 |
37+| loc | 7421 | +16 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 331 |
46+| internal/app/actions_file.go | 40 | 65 | 200 |
47+| internal/editor/view.go | 40 | 110 | 333 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
64+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
65+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
66+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
67+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
68+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
69+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
70+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
71+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
72+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T04:05:54Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `31ec686` on `feature/terminal`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #13 (previous: 2026-08-31T04:04:25Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 767 | +7 |
31+| classes | 88 | ±0 |
32+| fields | 293 | +4 |
33+| cyclo | 2421 | -32 |
34+| complex | 1023 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 11184 | +61 |
37+| loc | 7421 | +16 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 331 |
46+| internal/app/actions_file.go | 40 | 65 | 200 |
47+| internal/editor/view.go | 40 | 110 | 333 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 4 | 2026-08-30T16:45:20Z | 0 | 0 | 0 | 802 | PASS |
64+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
65+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
66+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
67+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
68+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
69+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
70+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
71+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
72+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
added .quality/report-20260831T040832Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T04:08:32Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `31ec686` on `feature/terminal`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #14 (previous: 2026-08-31T04:05:54Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 767 | ±0 |
31+| classes | 88 | ±0 |
32+| fields | 293 | ±0 |
33+| cyclo | 2421 | ±0 |
34+| complex | 1023 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 11184 | ±0 |
37+| loc | 7421 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 331 |
46+| internal/app/actions_file.go | 40 | 65 | 200 |
47+| internal/editor/view.go | 40 | 110 | 333 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
64+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
65+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
66+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
67+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
68+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
69+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
70+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
71+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
72+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T04:08:32Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `31ec686` on `feature/terminal`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #14 (previous: 2026-08-31T04:05:54Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 767 | ±0 |
31+| classes | 88 | ±0 |
32+| fields | 293 | ±0 |
33+| cyclo | 2421 | ±0 |
34+| complex | 1023 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 11184 | ±0 |
37+| loc | 7421 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 331 |
46+| internal/app/actions_file.go | 40 | 65 | 200 |
47+| internal/editor/view.go | 40 | 110 | 333 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/app/completion.go | 31 | 75 | 193 |
51+| internal/lsp/client.go | 30 | 61 | 260 |
52+| internal/ui/controls.go | 30 | 69 | 216 |
53+| internal/ui/window.go | 30 | 87 | 200 |
54+| internal/ui/desktop.go | 24 | 62 | 134 |
55+| internal/app/actions_edit.go | 23 | 39 | 137 |
56+| internal/app/dialogs.go | 22 | 80 | 247 |
57+| internal/buffer/cursor.go | 21 | 54 | 108 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 5 | 2026-08-30T18:00:37Z | 0 | 0 | 1 | 810 | FAIL |
64+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
65+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
66+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
67+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
68+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
69+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
70+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
71+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
72+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
added .quality/report-20260831T045155Z.md +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T04:51:55Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0373dd` on `feature/project-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #15 (previous: 2026-08-31T04:08:32Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/settings/create.go | 92 | Function with many returns (count = 6): writeFile |
31+| qlty:return-statements | internal/syntax/toml.go | 215 | Function with many returns (count = 6): tomlWordClass |
32+| qlty:boolean-logic | internal/syntax/toml.go | 69 | Complex binary expression |
33+| qlty:boolean-logic | internal/syntax/toml.go | 250 | Complex binary expression |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 824 | +57 |
40+| classes | 94 | +6 |
41+| fields | 308 | +15 |
42+| cyclo | 2725 | +304 |
43+| complex | 1148 | +125 |
44+| lcom | 0 | ±0 |
45+| lines | 12305 | +1121 |
46+| loc | 8107 | +686 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/ui/menu.go | 46 | 102 | 255 |
53+| internal/theme/load.go | 45 | 74 | 220 |
54+| internal/app/app.go | 43 | 114 | 344 |
55+| internal/app/actions_file.go | 42 | 67 | 210 |
56+| internal/syntax/toml.go | 41 | 124 | 188 |
57+| internal/editor/view.go | 40 | 110 | 333 |
58+| internal/lsp/conn.go | 39 | 58 | 238 |
59+| internal/ui/dialog.go | 38 | 71 | 197 |
60+| internal/settings/rewrite.go | 32 | 64 | 121 |
61+| internal/app/completion.go | 31 | 75 | 193 |
62+| internal/lsp/client.go | 30 | 61 | 260 |
63+| internal/ui/controls.go | 30 | 69 | 216 |
64+| internal/ui/window.go | 30 | 87 | 200 |
65+| main.go | 25 | 43 | 152 |
66+| internal/ui/desktop.go | 24 | 62 | 134 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
73+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
74+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
75+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
76+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
77+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
78+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
79+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
80+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
81+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T04:51:55Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `a0373dd` on `feature/project-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #15 (previous: 2026-08-31T04:08:32Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/settings/create.go | 92 | Function with many returns (count = 6): writeFile |
31+| qlty:return-statements | internal/syntax/toml.go | 215 | Function with many returns (count = 6): tomlWordClass |
32+| qlty:boolean-logic | internal/syntax/toml.go | 69 | Complex binary expression |
33+| qlty:boolean-logic | internal/syntax/toml.go | 250 | Complex binary expression |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 824 | +57 |
40+| classes | 94 | +6 |
41+| fields | 308 | +15 |
42+| cyclo | 2725 | +304 |
43+| complex | 1148 | +125 |
44+| lcom | 0 | ±0 |
45+| lines | 12305 | +1121 |
46+| loc | 8107 | +686 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/ui/menu.go | 46 | 102 | 255 |
53+| internal/theme/load.go | 45 | 74 | 220 |
54+| internal/app/app.go | 43 | 114 | 344 |
55+| internal/app/actions_file.go | 42 | 67 | 210 |
56+| internal/syntax/toml.go | 41 | 124 | 188 |
57+| internal/editor/view.go | 40 | 110 | 333 |
58+| internal/lsp/conn.go | 39 | 58 | 238 |
59+| internal/ui/dialog.go | 38 | 71 | 197 |
60+| internal/settings/rewrite.go | 32 | 64 | 121 |
61+| internal/app/completion.go | 31 | 75 | 193 |
62+| internal/lsp/client.go | 30 | 61 | 260 |
63+| internal/ui/controls.go | 30 | 69 | 216 |
64+| internal/ui/window.go | 30 | 87 | 200 |
65+| main.go | 25 | 43 | 152 |
66+| internal/ui/desktop.go | 24 | 62 | 134 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 6 | 2026-08-30T18:01:05Z | 0 | 0 | 0 | 812 | PASS |
73+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
74+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
75+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
76+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
77+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
78+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
79+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
80+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
81+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
added .quality/report-20260831T045240Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T04:52:40Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0373dd` on `feature/project-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #16 (previous: 2026-08-31T04:51:55Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 829 | +5 |
31+| classes | 94 | ±0 |
32+| fields | 308 | ±0 |
33+| cyclo | 2706 | -19 |
34+| complex | 1150 | +2 |
35+| lcom | 0 | ±0 |
36+| lines | 12340 | +35 |
37+| loc | 8119 | +12 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 344 |
46+| internal/syntax/toml.go | 43 | 105 | 194 |
47+| internal/app/actions_file.go | 42 | 67 | 210 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/settings/rewrite.go | 32 | 64 | 121 |
52+| internal/app/completion.go | 31 | 75 | 193 |
53+| internal/lsp/client.go | 30 | 61 | 260 |
54+| internal/ui/controls.go | 30 | 69 | 216 |
55+| internal/ui/window.go | 30 | 87 | 200 |
56+| main.go | 25 | 43 | 152 |
57+| internal/ui/desktop.go | 24 | 62 | 134 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
64+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
65+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
66+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
67+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
68+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
69+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
70+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
71+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
72+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T04:52:40Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `a0373dd` on `feature/project-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #16 (previous: 2026-08-31T04:51:55Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 829 | +5 |
31+| classes | 94 | ±0 |
32+| fields | 308 | ±0 |
33+| cyclo | 2706 | -19 |
34+| complex | 1150 | +2 |
35+| lcom | 0 | ±0 |
36+| lines | 12340 | +35 |
37+| loc | 8119 | +12 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 344 |
46+| internal/syntax/toml.go | 43 | 105 | 194 |
47+| internal/app/actions_file.go | 42 | 67 | 210 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/settings/rewrite.go | 32 | 64 | 121 |
52+| internal/app/completion.go | 31 | 75 | 193 |
53+| internal/lsp/client.go | 30 | 61 | 260 |
54+| internal/ui/controls.go | 30 | 69 | 216 |
55+| internal/ui/window.go | 30 | 87 | 200 |
56+| main.go | 25 | 43 | 152 |
57+| internal/ui/desktop.go | 24 | 62 | 134 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 7 | 2026-08-30T18:18:11Z | 0 | 0 | 0 | 823 | PASS |
64+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
65+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
66+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
67+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
68+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
69+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
70+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
71+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
72+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
added .quality/report-20260831T052116Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T05:21:16Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `0ef19ec` on `feature/windows-buttons`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #17 (previous: 2026-08-31T04:52:40Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 837 | +8 |
31+| classes | 94 | ±0 |
32+| fields | 311 | +3 |
33+| cyclo | 2730 | +24 |
34+| complex | 1156 | +6 |
35+| lcom | 0 | ±0 |
36+| lines | 12462 | +122 |
37+| loc | 8175 | +56 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 344 |
46+| internal/syntax/toml.go | 43 | 105 | 194 |
47+| internal/app/actions_file.go | 42 | 67 | 210 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/ui/controls.go | 30 | 69 | 216 |
56+| internal/ui/desktop.go | 25 | 63 | 139 |
57+| main.go | 25 | 43 | 152 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
64+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
65+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
66+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
67+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
68+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
69+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
70+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
71+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
72+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T05:21:16Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `0ef19ec` on `feature/windows-buttons`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #17 (previous: 2026-08-31T04:52:40Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 837 | +8 |
31+| classes | 94 | ±0 |
32+| fields | 311 | +3 |
33+| cyclo | 2730 | +24 |
34+| complex | 1156 | +6 |
35+| lcom | 0 | ±0 |
36+| lines | 12462 | +122 |
37+| loc | 8175 | +56 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 344 |
46+| internal/syntax/toml.go | 43 | 105 | 194 |
47+| internal/app/actions_file.go | 42 | 67 | 210 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/ui/controls.go | 30 | 69 | 216 |
56+| internal/ui/desktop.go | 25 | 63 | 139 |
57+| main.go | 25 | 43 | 152 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 8 | 2026-08-30T18:30:37Z | 0 | 0 | 0 | 823 | PASS |
64+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
65+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
66+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
67+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
68+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
69+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
70+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
71+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
72+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
added .quality/report-20260831T060414Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T06:04:14Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `0ef19ec` on `feature/windows-buttons`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #18 (previous: 2026-08-31T05:21:16Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 838 | +1 |
31+| classes | 94 | ±0 |
32+| fields | 311 | ±0 |
33+| cyclo | 2735 | +5 |
34+| complex | 1160 | +4 |
35+| lcom | 0 | ±0 |
36+| lines | 12499 | +37 |
37+| loc | 8190 | +15 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 344 |
46+| internal/syntax/toml.go | 43 | 105 | 194 |
47+| internal/app/actions_file.go | 42 | 67 | 210 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/ui/controls.go | 30 | 69 | 216 |
56+| internal/app/dialogs.go | 26 | 85 | 262 |
57+| internal/ui/desktop.go | 25 | 63 | 139 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
64+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
65+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
66+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
67+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
68+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
69+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
70+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
71+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
72+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T06:04:14Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `0ef19ec` on `feature/windows-buttons`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #18 (previous: 2026-08-31T05:21:16Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 838 | +1 |
31+| classes | 94 | ±0 |
32+| fields | 311 | ±0 |
33+| cyclo | 2735 | +5 |
34+| complex | 1160 | +4 |
35+| lcom | 0 | ±0 |
36+| lines | 12499 | +37 |
37+| loc | 8190 | +15 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/app.go | 43 | 114 | 344 |
46+| internal/syntax/toml.go | 43 | 105 | 194 |
47+| internal/app/actions_file.go | 42 | 67 | 210 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/ui/controls.go | 30 | 69 | 216 |
56+| internal/app/dialogs.go | 26 | 85 | 262 |
57+| internal/ui/desktop.go | 25 | 63 | 139 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 9 | 2026-08-30T18:42:53Z | 0 | 0 | 0 | 832 | PASS |
64+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
65+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
66+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
67+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
68+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
69+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
70+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
71+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
72+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
added .quality/report-20260831T062442Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T06:24:42Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `70106fc` on `feature/treeview-window`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #19 (previous: 2026-08-31T06:04:14Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 881 | +43 |
31+| classes | 98 | +4 |
32+| fields | 326 | +15 |
33+| cyclo | 2870 | +135 |
34+| complex | 1228 | +68 |
35+| lcom | 0 | ±0 |
36+| lines | 13198 | +699 |
37+| loc | 8617 | +427 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 349 |
47+| internal/syntax/toml.go | 43 | 105 | 194 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/ui/controls.go | 30 | 69 | 216 |
56+| internal/app/dialogs.go | 26 | 85 | 262 |
57+| internal/ui/desktop.go | 25 | 63 | 139 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
64+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
65+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
66+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
67+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
68+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
69+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
70+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
71+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
72+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T06:24:42Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `70106fc` on `feature/treeview-window`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #19 (previous: 2026-08-31T06:04:14Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 881 | +43 |
31+| classes | 98 | +4 |
32+| fields | 326 | +15 |
33+| cyclo | 2870 | +135 |
34+| complex | 1228 | +68 |
35+| lcom | 0 | ±0 |
36+| lines | 13198 | +699 |
37+| loc | 8617 | +427 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 349 |
47+| internal/syntax/toml.go | 43 | 105 | 194 |
48+| internal/editor/view.go | 40 | 110 | 333 |
49+| internal/lsp/conn.go | 39 | 58 | 238 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/ui/controls.go | 30 | 69 | 216 |
56+| internal/app/dialogs.go | 26 | 85 | 262 |
57+| internal/ui/desktop.go | 25 | 63 | 139 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 10 | 2026-08-30T18:50:41Z | 0 | 0 | 0 | 833 | PASS |
64+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
65+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
66+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
67+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
68+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
69+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
70+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
71+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
72+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
added .quality/report-20260831T071343Z.md +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T07:13:43Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `386992e` on `feature/new-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #20 (previous: 2026-08-31T06:24:42Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/syntax/highlight.go | 21 | Function with many returns (count = 7): Highlight |
31+| qlty:file-complexity | internal/syntax/markdown.go | 1 | High total complexity (count = 53) |
32+| qlty:return-statements | internal/syntax/language.go | 110 | Function with many returns (count = 7): String |
33+| qlty:boolean-logic | internal/syntax/html.go | 134 | Complex binary expression |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 946 | +65 |
40+| classes | 103 | +5 |
41+| fields | 327 | +1 |
42+| cyclo | 3241 | +371 |
43+| complex | 1388 | +160 |
44+| lcom | 0 | ±0 |
45+| lines | 14349 | +1151 |
46+| loc | 9394 | +777 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/syntax/markdown.go | 53 | 128 | 191 |
53+| internal/ui/menu.go | 46 | 102 | 255 |
54+| internal/theme/load.go | 45 | 74 | 220 |
55+| internal/app/actions_file.go | 43 | 68 | 215 |
56+| internal/app/app.go | 43 | 116 | 349 |
57+| internal/editor/view.go | 40 | 110 | 333 |
58+| internal/lsp/conn.go | 39 | 58 | 238 |
59+| internal/syntax/toml.go | 38 | 92 | 179 |
60+| internal/ui/dialog.go | 38 | 71 | 197 |
61+| internal/ui/window.go | 35 | 110 | 251 |
62+| internal/settings/rewrite.go | 32 | 64 | 121 |
63+| internal/app/completion.go | 31 | 75 | 193 |
64+| internal/lsp/client.go | 30 | 61 | 260 |
65+| internal/syntax/scanner.go | 30 | 67 | 137 |
66+| internal/ui/controls.go | 30 | 69 | 216 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
73+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
74+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
75+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
76+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
77+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
78+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
79+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
80+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
81+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T07:13:43Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `386992e` on `feature/new-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #20 (previous: 2026-08-31T06:24:42Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:return-statements | internal/syntax/highlight.go | 21 | Function with many returns (count = 7): Highlight |
31+| qlty:file-complexity | internal/syntax/markdown.go | 1 | High total complexity (count = 53) |
32+| qlty:return-statements | internal/syntax/language.go | 110 | Function with many returns (count = 7): String |
33+| qlty:boolean-logic | internal/syntax/html.go | 134 | Complex binary expression |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 946 | +65 |
40+| classes | 103 | +5 |
41+| fields | 327 | +1 |
42+| cyclo | 3241 | +371 |
43+| complex | 1388 | +160 |
44+| lcom | 0 | ±0 |
45+| lines | 14349 | +1151 |
46+| loc | 9394 | +777 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/syntax/markdown.go | 53 | 128 | 191 |
53+| internal/ui/menu.go | 46 | 102 | 255 |
54+| internal/theme/load.go | 45 | 74 | 220 |
55+| internal/app/actions_file.go | 43 | 68 | 215 |
56+| internal/app/app.go | 43 | 116 | 349 |
57+| internal/editor/view.go | 40 | 110 | 333 |
58+| internal/lsp/conn.go | 39 | 58 | 238 |
59+| internal/syntax/toml.go | 38 | 92 | 179 |
60+| internal/ui/dialog.go | 38 | 71 | 197 |
61+| internal/ui/window.go | 35 | 110 | 251 |
62+| internal/settings/rewrite.go | 32 | 64 | 121 |
63+| internal/app/completion.go | 31 | 75 | 193 |
64+| internal/lsp/client.go | 30 | 61 | 260 |
65+| internal/syntax/scanner.go | 30 | 67 | 137 |
66+| internal/ui/controls.go | 30 | 69 | 216 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 11 | 2026-08-31T03:11:37Z | 0 | 0 | 0 | 833 | PASS |
73+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
74+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
75+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
76+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
77+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
78+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
79+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
80+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
81+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
added .quality/report-20260831T071419Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T07:14:19Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `386992e` on `feature/new-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #21 (previous: 2026-08-31T07:13:43Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 946 | ±0 |
31+| classes | 103 | ±0 |
32+| fields | 327 | ±0 |
33+| cyclo | 3225 | -16 |
34+| complex | 1389 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 14359 | +10 |
37+| loc | 9389 | -5 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 349 |
47+| internal/editor/view.go | 40 | 110 | 333 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/syntax/toml.go | 38 | 92 | 179 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/syntax/markdown.go | 30 | 69 | 110 |
56+| internal/syntax/scanner.go | 30 | 67 | 137 |
57+| internal/ui/controls.go | 30 | 69 | 216 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
64+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
65+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
66+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
67+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
68+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
69+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
70+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
71+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
72+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T07:14:19Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `386992e` on `feature/new-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #21 (previous: 2026-08-31T07:13:43Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 946 | ±0 |
31+| classes | 103 | ±0 |
32+| fields | 327 | ±0 |
33+| cyclo | 3225 | -16 |
34+| complex | 1389 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 14359 | +10 |
37+| loc | 9389 | -5 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/ui/menu.go | 46 | 102 | 255 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 349 |
47+| internal/editor/view.go | 40 | 110 | 333 |
48+| internal/lsp/conn.go | 39 | 58 | 238 |
49+| internal/syntax/toml.go | 38 | 92 | 179 |
50+| internal/ui/dialog.go | 38 | 71 | 197 |
51+| internal/ui/window.go | 35 | 110 | 251 |
52+| internal/settings/rewrite.go | 32 | 64 | 121 |
53+| internal/app/completion.go | 31 | 75 | 193 |
54+| internal/lsp/client.go | 30 | 61 | 260 |
55+| internal/syntax/markdown.go | 30 | 69 | 110 |
56+| internal/syntax/scanner.go | 30 | 67 | 137 |
57+| internal/ui/controls.go | 30 | 69 | 216 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 12 | 2026-08-31T04:04:25Z | 0 | 0 | 4 | 1022 | FAIL |
64+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
65+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
66+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
67+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
68+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
69+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
70+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
71+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
72+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
added .quality/report-20260831T104447Z.md +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+# Quality report — 2026-08-31T10:44:47Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2bdbb88` on `feature/snippets`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #22 (previous: 2026-08-31T07:14:19Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:file-complexity | internal/ui/menu.go | 1 | High total complexity (count = 69) |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 988 | +42 |
37+| classes | 107 | +4 |
38+| fields | 337 | +10 |
39+| cyclo | 3388 | +163 |
40+| complex | 1475 | +86 |
41+| lcom | 0 | ±0 |
42+| lines | 15188 | +829 |
43+| loc | 9911 | +522 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/ui/menu.go | 69 | 166 | 398 |
50+| internal/editor/view.go | 48 | 124 | 361 |
51+| internal/theme/load.go | 45 | 74 | 220 |
52+| internal/app/actions_file.go | 43 | 68 | 215 |
53+| internal/app/app.go | 43 | 116 | 349 |
54+| internal/lsp/conn.go | 39 | 58 | 238 |
55+| internal/syntax/toml.go | 38 | 92 | 179 |
56+| internal/ui/dialog.go | 38 | 71 | 197 |
57+| internal/snippets/snippets.go | 35 | 41 | 140 |
58+| internal/ui/window.go | 35 | 110 | 251 |
59+| internal/settings/rewrite.go | 32 | 64 | 121 |
60+| internal/app/completion.go | 31 | 75 | 193 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/syntax/markdown.go | 30 | 69 | 110 |
63+| internal/syntax/scanner.go | 30 | 67 | 137 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
70+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
71+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
72+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
73+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
74+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
75+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
76+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
77+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
78+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
new file mode 100644
@@ -0,0 +1,78 @@
1+# Quality report — 2026-08-31T10:44:47Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2bdbb88` on `feature/snippets`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #22 (previous: 2026-08-31T07:14:19Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:file-complexity | internal/ui/menu.go | 1 | High total complexity (count = 69) |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 988 | +42 |
37+| classes | 107 | +4 |
38+| fields | 337 | +10 |
39+| cyclo | 3388 | +163 |
40+| complex | 1475 | +86 |
41+| lcom | 0 | ±0 |
42+| lines | 15188 | +829 |
43+| loc | 9911 | +522 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/ui/menu.go | 69 | 166 | 398 |
50+| internal/editor/view.go | 48 | 124 | 361 |
51+| internal/theme/load.go | 45 | 74 | 220 |
52+| internal/app/actions_file.go | 43 | 68 | 215 |
53+| internal/app/app.go | 43 | 116 | 349 |
54+| internal/lsp/conn.go | 39 | 58 | 238 |
55+| internal/syntax/toml.go | 38 | 92 | 179 |
56+| internal/ui/dialog.go | 38 | 71 | 197 |
57+| internal/snippets/snippets.go | 35 | 41 | 140 |
58+| internal/ui/window.go | 35 | 110 | 251 |
59+| internal/settings/rewrite.go | 32 | 64 | 121 |
60+| internal/app/completion.go | 31 | 75 | 193 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/syntax/markdown.go | 30 | 69 | 110 |
63+| internal/syntax/scanner.go | 30 | 67 | 137 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 13 | 2026-08-31T04:05:54Z | 0 | 0 | 0 | 1023 | PASS |
70+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
71+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
72+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
73+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
74+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
75+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
76+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
77+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
78+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
added .quality/report-20260831T104525Z.md +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+# Quality report — 2026-08-31T10:45:25Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2bdbb88` on `feature/snippets`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #23 (previous: 2026-08-31T10:44:47Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: ±0)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:file-complexity | internal/ui/menu.go | 1 | High total complexity (count = 56) |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 988 | ±0 |
37+| classes | 107 | ±0 |
38+| fields | 337 | ±0 |
39+| cyclo | 3389 | +1 |
40+| complex | 1478 | +3 |
41+| lcom | 0 | ±0 |
42+| lines | 15198 | +10 |
43+| loc | 9916 | +5 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/ui/menu.go | 56 | 118 | 293 |
50+| internal/editor/view.go | 48 | 124 | 361 |
51+| internal/theme/load.go | 45 | 74 | 220 |
52+| internal/app/actions_file.go | 43 | 68 | 215 |
53+| internal/app/app.go | 43 | 116 | 349 |
54+| internal/lsp/conn.go | 39 | 58 | 238 |
55+| internal/syntax/toml.go | 38 | 92 | 179 |
56+| internal/ui/dialog.go | 38 | 71 | 197 |
57+| internal/snippets/snippets.go | 35 | 41 | 140 |
58+| internal/ui/window.go | 35 | 110 | 251 |
59+| internal/settings/rewrite.go | 32 | 64 | 121 |
60+| internal/app/completion.go | 31 | 75 | 193 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/syntax/markdown.go | 30 | 69 | 110 |
63+| internal/syntax/scanner.go | 30 | 67 | 137 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
70+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
71+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
72+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
73+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
74+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
75+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
76+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
77+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
78+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
new file mode 100644
@@ -0,0 +1,78 @@
1+# Quality report — 2026-08-31T10:45:25Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `2bdbb88` on `feature/snippets`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #23 (previous: 2026-08-31T10:44:47Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: ±0)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:file-complexity | internal/ui/menu.go | 1 | High total complexity (count = 56) |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 988 | ±0 |
37+| classes | 107 | ±0 |
38+| fields | 337 | ±0 |
39+| cyclo | 3389 | +1 |
40+| complex | 1478 | +3 |
41+| lcom | 0 | ±0 |
42+| lines | 15198 | +10 |
43+| loc | 9916 | +5 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/ui/menu.go | 56 | 118 | 293 |
50+| internal/editor/view.go | 48 | 124 | 361 |
51+| internal/theme/load.go | 45 | 74 | 220 |
52+| internal/app/actions_file.go | 43 | 68 | 215 |
53+| internal/app/app.go | 43 | 116 | 349 |
54+| internal/lsp/conn.go | 39 | 58 | 238 |
55+| internal/syntax/toml.go | 38 | 92 | 179 |
56+| internal/ui/dialog.go | 38 | 71 | 197 |
57+| internal/snippets/snippets.go | 35 | 41 | 140 |
58+| internal/ui/window.go | 35 | 110 | 251 |
59+| internal/settings/rewrite.go | 32 | 64 | 121 |
60+| internal/app/completion.go | 31 | 75 | 193 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/syntax/markdown.go | 30 | 69 | 110 |
63+| internal/syntax/scanner.go | 30 | 67 | 137 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 14 | 2026-08-31T04:08:32Z | 0 | 0 | 0 | 1023 | PASS |
70+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
71+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
72+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
73+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
74+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
75+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
76+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
77+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
78+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
added .quality/report-20260831T104601Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T10:46:01Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2bdbb88` on `feature/snippets`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #24 (previous: 2026-08-31T10:45:25Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 988 | ±0 |
31+| classes | 107 | ±0 |
32+| fields | 337 | ±0 |
33+| cyclo | 3391 | +2 |
34+| complex | 1480 | +2 |
35+| lcom | 0 | ±0 |
36+| lines | 15208 | +10 |
37+| loc | 9918 | +2 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 349 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
64+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
65+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
66+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
67+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
68+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
69+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
70+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
71+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
72+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T10:46:01Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `2bdbb88` on `feature/snippets`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #24 (previous: 2026-08-31T10:45:25Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 988 | ±0 |
31+| classes | 107 | ±0 |
32+| fields | 337 | ±0 |
33+| cyclo | 3391 | +2 |
34+| complex | 1480 | +2 |
35+| lcom | 0 | ±0 |
36+| lines | 15208 | +10 |
37+| loc | 9918 | +2 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 349 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 15 | 2026-08-31T04:51:55Z | 0 | 0 | 4 | 1148 | FAIL |
64+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
65+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
66+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
67+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
68+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
69+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
70+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
71+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
72+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
added .quality/report-20260831T114441Z.md +81 -0
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T11:44:41Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #25 (previous: 2026-08-31T10:46:01Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:similar-code | internal/app/gotools.go | 202 | Found 19 lines of similar code in 2 locations (mass = 117) |
31+| qlty:similar-code | internal/app/snippets.go | 134 | Found 19 lines of similar code in 2 locations (mass = 117) |
32+| qlty:similar-code | internal/snippets/create.go | 135 | Found 20 lines of similar code in 2 locations (mass = 124) |
33+| qlty:similar-code | internal/tools/create.go | 88 | Found 20 lines of similar code in 2 locations (mass = 124) |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 1010 | +22 |
40+| classes | 111 | +4 |
41+| fields | 347 | +10 |
42+| cyclo | 3484 | +93 |
43+| complex | 1528 | +48 |
44+| lcom | 0 | ±0 |
45+| lines | 15800 | +592 |
46+| loc | 10252 | +334 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/editor/view.go | 48 | 124 | 361 |
53+| internal/theme/load.go | 45 | 74 | 220 |
54+| internal/app/actions_file.go | 43 | 68 | 215 |
55+| internal/app/app.go | 43 | 116 | 352 |
56+| internal/lsp/conn.go | 39 | 58 | 238 |
57+| internal/syntax/toml.go | 38 | 92 | 179 |
58+| internal/ui/dialog.go | 38 | 71 | 197 |
59+| internal/ui/menu_events.go | 38 | 66 | 145 |
60+| internal/snippets/snippets.go | 35 | 41 | 140 |
61+| internal/ui/window.go | 35 | 110 | 251 |
62+| internal/settings/rewrite.go | 32 | 64 | 121 |
63+| internal/app/completion.go | 31 | 75 | 193 |
64+| internal/lsp/client.go | 30 | 61 | 260 |
65+| internal/syntax/markdown.go | 30 | 69 | 110 |
66+| internal/syntax/scanner.go | 30 | 67 | 137 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
73+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
74+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
75+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
76+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
77+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
78+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
79+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
80+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
81+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
new file mode 100644
@@ -0,0 +1,81 @@
1+# Quality report — 2026-08-31T11:44:41Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #25 (previous: 2026-08-31T10:46:01Z)
7+
8+## Gate violations
9+
10+- code smells: 4 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **4** (vs previous: +4)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:similar-code | internal/app/gotools.go | 202 | Found 19 lines of similar code in 2 locations (mass = 117) |
31+| qlty:similar-code | internal/app/snippets.go | 134 | Found 19 lines of similar code in 2 locations (mass = 117) |
32+| qlty:similar-code | internal/snippets/create.go | 135 | Found 20 lines of similar code in 2 locations (mass = 124) |
33+| qlty:similar-code | internal/tools/create.go | 88 | Found 20 lines of similar code in 2 locations (mass = 124) |
34+
35+## Metrics (`qlty metrics`)
36+
37+| metric | total | vs previous |
38+|---|---|---|
39+| funcs | 1010 | +22 |
40+| classes | 111 | +4 |
41+| fields | 347 | +10 |
42+| cyclo | 3484 | +93 |
43+| complex | 1528 | +48 |
44+| lcom | 0 | ±0 |
45+| lines | 15800 | +592 |
46+| loc | 10252 | +334 |
47+
48+### Most complex files
49+
50+| file | complex | cyclo | loc |
51+|---|---|---|---|
52+| internal/editor/view.go | 48 | 124 | 361 |
53+| internal/theme/load.go | 45 | 74 | 220 |
54+| internal/app/actions_file.go | 43 | 68 | 215 |
55+| internal/app/app.go | 43 | 116 | 352 |
56+| internal/lsp/conn.go | 39 | 58 | 238 |
57+| internal/syntax/toml.go | 38 | 92 | 179 |
58+| internal/ui/dialog.go | 38 | 71 | 197 |
59+| internal/ui/menu_events.go | 38 | 66 | 145 |
60+| internal/snippets/snippets.go | 35 | 41 | 140 |
61+| internal/ui/window.go | 35 | 110 | 251 |
62+| internal/settings/rewrite.go | 32 | 64 | 121 |
63+| internal/app/completion.go | 31 | 75 | 193 |
64+| internal/lsp/client.go | 30 | 61 | 260 |
65+| internal/syntax/markdown.go | 30 | 69 | 110 |
66+| internal/syntax/scanner.go | 30 | 67 | 137 |
67+
68+## Trend
69+
70+| run | timestamp | error | warning | smells | complex | gate |
71+|---|---|---|---|---|---|---|
72+| 16 | 2026-08-31T04:52:40Z | 0 | 0 | 0 | 1150 | PASS |
73+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
74+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
75+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
76+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
77+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
78+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
79+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
80+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
81+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
added .quality/report-20260831T114621Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T11:46:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #26 (previous: 2026-08-31T11:44:41Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1007 | -3 |
31+| classes | 111 | ±0 |
32+| fields | 347 | ±0 |
33+| cyclo | 3446 | -38 |
34+| complex | 1513 | -15 |
35+| lcom | 0 | ±0 |
36+| lines | 15748 | -52 |
37+| loc | 10178 | -74 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 352 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
64+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
65+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
66+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
67+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
68+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
69+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
70+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
71+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
72+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T11:46:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #26 (previous: 2026-08-31T11:44:41Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -4)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1007 | -3 |
31+| classes | 111 | ±0 |
32+| fields | 347 | ±0 |
33+| cyclo | 3446 | -38 |
34+| complex | 1513 | -15 |
35+| lcom | 0 | ±0 |
36+| lines | 15748 | -52 |
37+| loc | 10178 | -74 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 352 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 17 | 2026-08-31T05:21:16Z | 0 | 0 | 0 | 1156 | PASS |
64+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
65+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
66+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
67+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
68+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
69+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
70+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
71+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
72+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
added .quality/report-20260831T114654Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T11:46:54Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #27 (previous: 2026-08-31T11:46:21Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1007 | ±0 |
31+| classes | 111 | ±0 |
32+| fields | 347 | ±0 |
33+| cyclo | 3446 | ±0 |
34+| complex | 1513 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 15748 | ±0 |
37+| loc | 10178 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 352 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
64+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
65+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
66+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
67+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
68+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
69+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
70+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
71+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
72+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T11:46:54Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #27 (previous: 2026-08-31T11:46:21Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1007 | ±0 |
31+| classes | 111 | ±0 |
32+| fields | 347 | ±0 |
33+| cyclo | 3446 | ±0 |
34+| complex | 1513 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 15748 | ±0 |
37+| loc | 10178 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 352 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 18 | 2026-08-31T06:04:14Z | 0 | 0 | 0 | 1160 | PASS |
64+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
65+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
66+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
67+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
68+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
69+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
70+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
71+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
72+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
added .quality/report-20260831T140709Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T14:07:09Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #28 (previous: 2026-08-31T11:46:54Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1029 | +22 |
31+| classes | 114 | +3 |
32+| fields | 361 | +14 |
33+| cyclo | 3501 | +55 |
34+| complex | 1542 | +29 |
35+| lcom | 0 | ±0 |
36+| lines | 16203 | +455 |
37+| loc | 10423 | +245 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
64+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
65+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
66+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
67+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
68+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
69+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
70+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
71+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
72+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T14:07:09Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #28 (previous: 2026-08-31T11:46:54Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1029 | +22 |
31+| classes | 114 | +3 |
32+| fields | 361 | +14 |
33+| cyclo | 3501 | +55 |
34+| complex | 1542 | +29 |
35+| lcom | 0 | ±0 |
36+| lines | 16203 | +455 |
37+| loc | 10423 | +245 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 19 | 2026-08-31T06:24:42Z | 0 | 0 | 0 | 1228 | PASS |
64+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
65+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
66+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
67+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
68+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
69+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
70+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
71+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
72+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
added .quality/report-20260831T141339Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T14:13:39Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #29 (previous: 2026-08-31T14:07:09Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1033 | +4 |
31+| classes | 114 | ±0 |
32+| fields | 361 | ±0 |
33+| cyclo | 3506 | +5 |
34+| complex | 1543 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 16263 | +60 |
37+| loc | 10447 | +24 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
64+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
65+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
66+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
67+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
68+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
69+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
70+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
71+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
72+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T14:13:39Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #29 (previous: 2026-08-31T14:07:09Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1033 | +4 |
31+| classes | 114 | ±0 |
32+| fields | 361 | ±0 |
33+| cyclo | 3506 | +5 |
34+| complex | 1543 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 16263 | +60 |
37+| loc | 10447 | +24 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 20 | 2026-08-31T07:13:43Z | 0 | 0 | 4 | 1388 | FAIL |
64+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
65+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
66+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
67+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
68+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
69+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
70+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
71+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
72+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
added .quality/report-20260831T161206Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:12:06Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #30 (previous: 2026-08-31T14:13:39Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1033 | ±0 |
31+| classes | 114 | ±0 |
32+| fields | 361 | ±0 |
33+| cyclo | 3506 | ±0 |
34+| complex | 1543 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16263 | ±0 |
37+| loc | 10447 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
64+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
65+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
66+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
67+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
68+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
69+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
70+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
71+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
72+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:12:06Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #30 (previous: 2026-08-31T14:13:39Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1033 | ±0 |
31+| classes | 114 | ±0 |
32+| fields | 361 | ±0 |
33+| cyclo | 3506 | ±0 |
34+| complex | 1543 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16263 | ±0 |
37+| loc | 10447 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 21 | 2026-08-31T07:14:19Z | 0 | 0 | 0 | 1389 | PASS |
64+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
65+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
66+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
67+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
68+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
69+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
70+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
71+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
72+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
added .quality/report-20260831T161951Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:19:51Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #31 (previous: 2026-08-31T16:12:06Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1033 | ±0 |
31+| classes | 114 | ±0 |
32+| fields | 361 | ±0 |
33+| cyclo | 3506 | ±0 |
34+| complex | 1543 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16262 | -1 |
37+| loc | 10447 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
64+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
65+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
66+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
67+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
68+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
69+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
70+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
71+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
72+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:19:51Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #31 (previous: 2026-08-31T16:12:06Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1033 | ±0 |
31+| classes | 114 | ±0 |
32+| fields | 361 | ±0 |
33+| cyclo | 3506 | ±0 |
34+| complex | 1543 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16262 | -1 |
37+| loc | 10447 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 22 | 2026-08-31T10:44:47Z | 0 | 0 | 1 | 1475 | FAIL |
64+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
65+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
66+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
67+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
68+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
69+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
70+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
71+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
72+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
added .quality/report-20260831T164533Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:45:33Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #32 (previous: 2026-08-31T16:19:51Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1046 | +13 |
31+| classes | 115 | +1 |
32+| fields | 366 | +5 |
33+| cyclo | 3542 | +36 |
34+| complex | 1567 | +24 |
35+| lcom | 0 | ±0 |
36+| lines | 16517 | +255 |
37+| loc | 10588 | +141 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 360 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
64+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
65+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
66+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
67+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
68+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
69+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
70+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
71+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
72+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:45:33Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #32 (previous: 2026-08-31T16:19:51Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1046 | +13 |
31+| classes | 115 | +1 |
32+| fields | 366 | +5 |
33+| cyclo | 3542 | +36 |
34+| complex | 1567 | +24 |
35+| lcom | 0 | ±0 |
36+| lines | 16517 | +255 |
37+| loc | 10588 | +141 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 360 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 23 | 2026-08-31T10:45:25Z | 0 | 0 | 1 | 1478 | FAIL |
64+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
65+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
66+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
67+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
68+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
69+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
70+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
71+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
72+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
added .quality/report-20260831T165121Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:51:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #33 (previous: 2026-08-31T16:45:33Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1046 | ±0 |
31+| classes | 115 | ±0 |
32+| fields | 366 | ±0 |
33+| cyclo | 3542 | ±0 |
34+| complex | 1567 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16517 | ±0 |
37+| loc | 10588 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 360 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
64+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
65+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
66+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
67+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
68+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
69+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
70+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
71+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
72+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T16:51:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d44a159` on `feature/go-format-lint`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #33 (previous: 2026-08-31T16:45:33Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1046 | ±0 |
31+| classes | 115 | ±0 |
32+| fields | 366 | ±0 |
33+| cyclo | 3542 | ±0 |
34+| complex | 1567 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16517 | ±0 |
37+| loc | 10588 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 360 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 24 | 2026-08-31T10:46:01Z | 0 | 0 | 0 | 1480 | PASS |
64+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
65+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
66+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
67+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
68+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
69+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
70+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
71+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
72+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
added .quality/report-20260831T184959Z.md +78 -0
new file mode 100644
@@ -0,0 +1,78 @@
1+# Quality report — 2026-08-31T18:49:59Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `88a4c38` on `feature/about-version`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #34 (previous: 2026-08-31T16:51:21Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:boolean-logic | internal/version/version.go | 162 | Complex binary expression |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 1061 | +15 |
37+| classes | 116 | +1 |
38+| fields | 369 | +3 |
39+| cyclo | 3625 | +83 |
40+| complex | 1591 | +24 |
41+| lcom | 0 | ±0 |
42+| lines | 16769 | +252 |
43+| loc | 10721 | +133 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/editor/view.go | 48 | 124 | 361 |
50+| internal/theme/load.go | 45 | 74 | 220 |
51+| internal/app/actions_file.go | 43 | 68 | 215 |
52+| internal/app/app.go | 43 | 116 | 357 |
53+| internal/lsp/conn.go | 39 | 58 | 238 |
54+| internal/syntax/toml.go | 38 | 92 | 179 |
55+| internal/ui/dialog.go | 38 | 71 | 197 |
56+| internal/ui/menu_events.go | 38 | 66 | 145 |
57+| internal/snippets/snippets.go | 35 | 41 | 140 |
58+| internal/ui/window.go | 35 | 110 | 251 |
59+| internal/settings/rewrite.go | 32 | 64 | 121 |
60+| internal/app/completion.go | 31 | 75 | 193 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/syntax/markdown.go | 30 | 69 | 110 |
63+| internal/syntax/scanner.go | 30 | 67 | 137 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
70+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
71+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
72+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
73+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
74+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
75+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
76+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
77+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
78+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
new file mode 100644
@@ -0,0 +1,78 @@
1+# Quality report — 2026-08-31T18:49:59Z
2+
3+- **Gate**: ❌ **FAIL**
4+- **Commit**: `88a4c38` on `feature/about-version`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #34 (previous: 2026-08-31T16:51:21Z)
7+
8+## Gate violations
9+
10+- code smells: 1 (max 0)
11+
12+## Lint issues (`qlty check`)
13+
14+_none_
15+
16+### Top rules
17+
18+_none_
19+
20+### Most affected files
21+
22+_none_
23+
24+## Code smells (`qlty smells`)
25+
26+Total: **1** (vs previous: +1)
27+
28+| smell | file | line | detail |
29+|---|---|---|---|
30+| qlty:boolean-logic | internal/version/version.go | 162 | Complex binary expression |
31+
32+## Metrics (`qlty metrics`)
33+
34+| metric | total | vs previous |
35+|---|---|---|
36+| funcs | 1061 | +15 |
37+| classes | 116 | +1 |
38+| fields | 369 | +3 |
39+| cyclo | 3625 | +83 |
40+| complex | 1591 | +24 |
41+| lcom | 0 | ±0 |
42+| lines | 16769 | +252 |
43+| loc | 10721 | +133 |
44+
45+### Most complex files
46+
47+| file | complex | cyclo | loc |
48+|---|---|---|---|
49+| internal/editor/view.go | 48 | 124 | 361 |
50+| internal/theme/load.go | 45 | 74 | 220 |
51+| internal/app/actions_file.go | 43 | 68 | 215 |
52+| internal/app/app.go | 43 | 116 | 357 |
53+| internal/lsp/conn.go | 39 | 58 | 238 |
54+| internal/syntax/toml.go | 38 | 92 | 179 |
55+| internal/ui/dialog.go | 38 | 71 | 197 |
56+| internal/ui/menu_events.go | 38 | 66 | 145 |
57+| internal/snippets/snippets.go | 35 | 41 | 140 |
58+| internal/ui/window.go | 35 | 110 | 251 |
59+| internal/settings/rewrite.go | 32 | 64 | 121 |
60+| internal/app/completion.go | 31 | 75 | 193 |
61+| internal/lsp/client.go | 30 | 61 | 260 |
62+| internal/syntax/markdown.go | 30 | 69 | 110 |
63+| internal/syntax/scanner.go | 30 | 67 | 137 |
64+
65+## Trend
66+
67+| run | timestamp | error | warning | smells | complex | gate |
68+|---|---|---|---|---|---|---|
69+| 25 | 2026-08-31T11:44:41Z | 0 | 0 | 4 | 1528 | FAIL |
70+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
71+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
72+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
73+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
74+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
75+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
76+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
77+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
78+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
added .quality/report-20260831T185025Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T18:50:25Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `88a4c38` on `feature/about-version`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #35 (previous: 2026-08-31T18:49:59Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | +2 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | +14 |
37+| loc | 10730 | +9 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
64+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
65+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
66+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
67+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
68+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
69+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
70+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
71+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
72+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T18:50:25Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `88a4c38` on `feature/about-version`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #35 (previous: 2026-08-31T18:49:59Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: -1)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | +2 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | +1 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | +14 |
37+| loc | 10730 | +9 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 26 | 2026-08-31T11:46:21Z | 0 | 0 | 0 | 1513 | PASS |
64+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
65+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
66+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
67+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
68+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
69+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
70+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
71+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
72+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
added .quality/report-20260831T185502Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T18:55:02Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `88a4c38` on `feature/about-version`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #36 (previous: 2026-08-31T18:50:25Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
64+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
65+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
66+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
67+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
68+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
69+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
70+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
71+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
72+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T18:55:02Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `88a4c38` on `feature/about-version`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #36 (previous: 2026-08-31T18:50:25Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 27 | 2026-08-31T11:46:54Z | 0 | 0 | 0 | 1513 | PASS |
64+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
65+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
66+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
67+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
68+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
69+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
70+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
71+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
72+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
added .quality/report-20260831T190359Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:03:59Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7f8b36a` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #37 (previous: 2026-08-31T18:55:02Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
64+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
65+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
66+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
67+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
68+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
69+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
70+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
71+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
72+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:03:59Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7f8b36a` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #37 (previous: 2026-08-31T18:55:02Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 28 | 2026-08-31T14:07:09Z | 0 | 0 | 0 | 1542 | PASS |
64+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
65+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
66+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
67+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
68+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
69+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
70+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
71+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
72+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
added .quality/report-20260831T191014Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:10:14Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `78ea819` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #38 (previous: 2026-08-31T19:03:59Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
64+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
65+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
66+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
67+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
68+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
69+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
70+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
71+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
72+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:10:14Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `78ea819` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #38 (previous: 2026-08-31T19:03:59Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 29 | 2026-08-31T14:13:39Z | 0 | 0 | 0 | 1543 | PASS |
64+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
65+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
66+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
67+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
68+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
69+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
70+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
71+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
72+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
added .quality/report-20260831T191548Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:15:48Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `78ea819` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #39 (previous: 2026-08-31T19:10:14Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
64+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
65+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
66+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
67+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
68+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
69+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
70+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
71+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
72+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:15:48Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `78ea819` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #39 (previous: 2026-08-31T19:10:14Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 30 | 2026-08-31T16:12:06Z | 0 | 0 | 0 | 1543 | PASS |
64+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
65+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
66+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
67+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
68+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
69+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
70+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
71+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
72+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
added .quality/report-20260831T193410Z.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:34:10Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `9505d1d` on `feature/theme-cappucino`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #40 (previous: 2026-08-31T19:15:48Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
64+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
65+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
66+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
67+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
68+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
69+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
70+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
71+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
72+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
new file mode 100644
@@ -0,0 +1,72 @@
1+# Quality report — 2026-08-31T19:34:10Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `9505d1d` on `feature/theme-cappucino`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #40 (previous: 2026-08-31T19:15:48Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 1063 | ±0 |
31+| classes | 116 | ±0 |
32+| fields | 369 | ±0 |
33+| cyclo | 3625 | ±0 |
34+| complex | 1592 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 16783 | ±0 |
37+| loc | 10730 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/editor/view.go | 48 | 124 | 361 |
44+| internal/theme/load.go | 45 | 74 | 220 |
45+| internal/app/actions_file.go | 43 | 68 | 215 |
46+| internal/app/app.go | 43 | 116 | 357 |
47+| internal/lsp/conn.go | 39 | 58 | 238 |
48+| internal/syntax/toml.go | 38 | 92 | 179 |
49+| internal/ui/dialog.go | 38 | 71 | 197 |
50+| internal/ui/menu_events.go | 38 | 66 | 145 |
51+| internal/snippets/snippets.go | 35 | 41 | 140 |
52+| internal/ui/window.go | 35 | 110 | 251 |
53+| internal/settings/rewrite.go | 32 | 64 | 121 |
54+| internal/app/completion.go | 31 | 75 | 193 |
55+| internal/lsp/client.go | 30 | 61 | 260 |
56+| internal/syntax/markdown.go | 30 | 69 | 110 |
57+| internal/syntax/scanner.go | 30 | 67 | 137 |
58+
59+## Trend
60+
61+| run | timestamp | error | warning | smells | complex | gate |
62+|---|---|---|---|---|---|---|
63+| 31 | 2026-08-31T16:19:51Z | 0 | 0 | 0 | 1543 | PASS |
64+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
65+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
66+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
67+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
68+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
69+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
70+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
71+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
72+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
added .quality/report-20260901T041421Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T04:14:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `209fed2` on `refactoring`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #41 (previous: 2026-08-31T19:34:10Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | -1039 |
31+| classes | 2 | -114 |
32+| fields | 9 | -360 |
33+| cyclo | 81 | -3544 |
34+| complex | 37 | -1555 |
35+| lcom | 0 | ±0 |
36+| lines | 693 | -16090 |
37+| loc | 461 | -10269 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 6 |
48+| internal/golang/templates.go | 0 | 3 | 125 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
55+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
56+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
57+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
58+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
59+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
60+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
61+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
62+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
63+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T04:14:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `209fed2` on `refactoring`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #41 (previous: 2026-08-31T19:34:10Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | -1039 |
31+| classes | 2 | -114 |
32+| fields | 9 | -360 |
33+| cyclo | 81 | -3544 |
34+| complex | 37 | -1555 |
35+| lcom | 0 | ±0 |
36+| lines | 693 | -16090 |
37+| loc | 461 | -10269 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 6 |
48+| internal/golang/templates.go | 0 | 3 | 125 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 32 | 2026-08-31T16:45:33Z | 0 | 0 | 0 | 1567 | PASS |
55+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
56+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
57+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
58+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
59+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
60+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
61+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
62+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
63+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T045446Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T04:54:46Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `209fed2` on `refactoring`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #42 (previous: 2026-09-01T04:14:21Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 693 | ±0 |
37+| loc | 461 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 6 |
48+| internal/golang/templates.go | 0 | 3 | 125 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
55+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
56+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
57+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
58+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
59+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
60+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
61+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
62+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
63+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T04:54:46Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `209fed2` on `refactoring`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #42 (previous: 2026-09-01T04:14:21Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 693 | ±0 |
37+| loc | 461 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 6 |
48+| internal/golang/templates.go | 0 | 3 | 125 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 33 | 2026-08-31T16:51:21Z | 0 | 0 | 0 | 1567 | PASS |
55+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
56+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
57+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
58+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
59+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
60+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
61+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
62+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
63+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T050213Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T05:02:13Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `209fed2` on `refactoring`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #43 (previous: 2026-09-01T04:54:46Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 693 | ±0 |
37+| loc | 461 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 6 |
48+| internal/golang/templates.go | 0 | 3 | 125 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
55+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
56+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
57+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
58+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
59+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
60+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
61+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
62+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
63+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T05:02:13Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `209fed2` on `refactoring`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #43 (previous: 2026-09-01T04:54:46Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 693 | ±0 |
37+| loc | 461 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 6 |
48+| internal/golang/templates.go | 0 | 3 | 125 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 34 | 2026-08-31T18:49:59Z | 0 | 0 | 1 | 1591 | FAIL |
55+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
56+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
57+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
58+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
59+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
60+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
61+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
62+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
63+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T063640Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T06:36:40Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7273452` on `feature/tool-parameters`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #44 (previous: 2026-09-01T05:02:13Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | +19 |
37+| loc | 480 | +19 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
55+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
56+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
57+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
58+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
59+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
60+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
61+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
62+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
63+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T06:36:40Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7273452` on `feature/tool-parameters`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #44 (previous: 2026-09-01T05:02:13Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | +19 |
37+| loc | 480 | +19 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 35 | 2026-08-31T18:50:25Z | 0 | 0 | 0 | 1592 | PASS |
55+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
56+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
57+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
58+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
59+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
60+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
61+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
62+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
63+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T064358Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T06:43:58Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7273452` on `feature/tool-parameters`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #45 (previous: 2026-09-01T06:36:40Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
55+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
56+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
57+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
58+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
59+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
60+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
61+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
62+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
63+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T06:43:58Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7273452` on `feature/tool-parameters`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #45 (previous: 2026-09-01T06:36:40Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 36 | 2026-08-31T18:55:02Z | 0 | 0 | 0 | 1592 | PASS |
55+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
56+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
57+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
58+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
59+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
60+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
61+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
62+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
63+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T070355Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T07:03:55Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7273452` on `feature/tool-parameters`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #46 (previous: 2026-09-01T06:43:58Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
55+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
56+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
57+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
58+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
59+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
60+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
61+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
62+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
63+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T07:03:55Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `7273452` on `feature/tool-parameters`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #46 (previous: 2026-09-01T06:43:58Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 37 | 2026-08-31T19:03:59Z | 0 | 0 | 0 | 1592 | PASS |
55+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
56+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
57+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
58+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
59+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
60+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
61+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
62+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
63+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T120438Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T12:04:38Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d64410c` on `feature/more-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #47 (previous: 2026-09-01T07:03:55Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
55+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
56+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
57+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
58+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
59+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
60+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
61+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
62+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
63+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T12:04:38Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d64410c` on `feature/more-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #47 (previous: 2026-09-01T07:03:55Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 38 | 2026-08-31T19:10:14Z | 0 | 0 | 0 | 1592 | PASS |
55+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
56+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
57+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
58+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
59+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
60+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
61+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
62+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
63+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T121457Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T12:14:57Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d64410c` on `feature/more-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #48 (previous: 2026-09-01T12:04:38Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
55+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
56+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
57+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
58+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
59+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
60+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
61+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
62+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
63+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T12:14:57Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `d64410c` on `feature/more-syntaxes`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #48 (previous: 2026-09-01T12:04:38Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 712 | ±0 |
37+| loc | 480 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 143 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 39 | 2026-08-31T19:15:48Z | 0 | 0 | 0 | 1592 | PASS |
55+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
56+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
57+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
58+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
59+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
60+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
61+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
62+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
63+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T153109Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T15:31:09Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4ebf977` on `feature/menu-theme-and-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #49 (previous: 2026-09-01T12:14:57Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 714 | +2 |
37+| loc | 482 | +2 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
55+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
56+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
57+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
58+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
59+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
60+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
61+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
62+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
63+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T15:31:09Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4ebf977` on `feature/menu-theme-and-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #49 (previous: 2026-09-01T12:14:57Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 714 | +2 |
37+| loc | 482 | +2 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 40 | 2026-08-31T19:34:10Z | 0 | 0 | 0 | 1592 | PASS |
55+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
56+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
57+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
58+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
59+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
60+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
61+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
62+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
63+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260901T160949Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T16:09:49Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4ebf977` on `feature/menu-theme-and-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #50 (previous: 2026-09-01T15:31:09Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 714 | ±0 |
37+| loc | 482 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
55+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
56+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
57+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
58+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
59+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
60+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
61+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
62+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
63+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-01T16:09:49Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `4ebf977` on `feature/menu-theme-and-settings`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #50 (previous: 2026-09-01T15:31:09Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 714 | ±0 |
37+| loc | 482 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 7 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 41 | 2026-09-01T04:14:21Z | 0 | 0 | 0 | 37 | PASS |
55+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
56+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
57+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
58+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
59+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
60+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
61+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
62+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
63+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260902T050209Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T05:02:09Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `c348e02` on `feature/code-navigation`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #51 (previous: 2026-09-01T16:09:49Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 715 | +1 |
37+| loc | 483 | +1 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
55+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
56+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
57+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
58+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
59+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
60+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
61+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
62+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
63+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T05:02:09Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `c348e02` on `feature/code-navigation`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #51 (previous: 2026-09-01T16:09:49Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 715 | +1 |
37+| loc | 483 | +1 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 42 | 2026-09-01T04:54:46Z | 0 | 0 | 0 | 37 | PASS |
55+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
56+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
57+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
58+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
59+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
60+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
61+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
62+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
63+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260902T061301Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T06:13:01Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `c348e02` on `feature/code-navigation`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #52 (previous: 2026-09-02T05:02:09Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 715 | ±0 |
37+| loc | 483 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
55+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
56+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
57+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
58+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
59+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
60+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
61+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
62+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
63+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T06:13:01Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `c348e02` on `feature/code-navigation`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #52 (previous: 2026-09-02T05:02:09Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 81 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 715 | ±0 |
37+| loc | 483 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 3 | 145 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 43 | 2026-09-01T05:02:13Z | 0 | 0 | 0 | 37 | PASS |
55+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
56+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
57+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
58+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
59+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
60+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
61+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
62+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
63+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260902T191351Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T19:13:51Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `16da3cb` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #53 (previous: 2026-09-02T06:13:01Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | -2 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 573 | -142 |
37+| loc | 343 | -140 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 5 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
55+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
56+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
57+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
58+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
59+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
60+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
61+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
62+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
63+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T19:13:51Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `16da3cb` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #53 (previous: 2026-09-02T06:13:01Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | -2 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 573 | -142 |
37+| loc | 343 | -140 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 5 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 44 | 2026-09-01T06:36:40Z | 0 | 0 | 0 | 37 | PASS |
55+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
56+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
57+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
58+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
59+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
60+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
61+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
62+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
63+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260902T191821Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T19:18:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `16da3cb` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #54 (previous: 2026-09-02T19:13:51Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 573 | ±0 |
37+| loc | 343 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 5 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
55+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
56+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
57+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
58+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
59+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
60+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
61+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
62+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
63+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-02T19:18:21Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `16da3cb` on `main`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #54 (previous: 2026-09-02T19:13:51Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 573 | ±0 |
37+| loc | 343 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 65 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 5 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 45 | 2026-09-01T06:43:58Z | 0 | 0 | 0 | 37 | PASS |
55+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
56+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
57+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
58+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
59+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
60+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
61+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
62+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
63+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260915T093503Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-15T09:35:03Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `cb86146` on `feature/acp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #55 (previous: 2026-09-02T19:18:21Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 584 | +11 |
37+| loc | 345 | +2 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 66 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 6 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
55+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
56+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
57+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
58+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
59+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
60+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
61+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
62+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
63+| 55 | 2026-09-15T09:35:03Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-15T09:35:03Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `cb86146` on `feature/acp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #55 (previous: 2026-09-02T19:18:21Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 584 | +11 |
37+| loc | 345 | +2 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 66 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 6 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 46 | 2026-09-01T07:03:55Z | 0 | 0 | 0 | 37 | PASS |
55+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
56+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
57+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
58+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
59+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
60+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
61+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
62+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
63+| 55 | 2026-09-15T09:35:03Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-20260915T165710Z.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-15T16:57:10Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `cb86146` on `feature/acp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #56 (previous: 2026-09-15T09:35:03Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 584 | ±0 |
37+| loc | 345 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 66 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 6 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
55+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
56+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
57+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
58+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
59+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
60+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
61+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
62+| 55 | 2026-09-15T09:35:03Z | 0 | 0 | 0 | 37 | PASS |
63+| 56 | 2026-09-15T16:57:10Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-15T16:57:10Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `cb86146` on `feature/acp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #56 (previous: 2026-09-15T09:35:03Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 584 | ±0 |
37+| loc | 345 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 66 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 6 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
55+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
56+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
57+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
58+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
59+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
60+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
61+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
62+| 55 | 2026-09-15T09:35:03Z | 0 | 0 | 0 | 37 | PASS |
63+| 56 | 2026-09-15T16:57:10Z | 0 | 0 | 0 | 37 | PASS |
added .quality/report-latest.md +63 -0
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-15T16:57:10Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `cb86146` on `feature/acp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #56 (previous: 2026-09-15T09:35:03Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 584 | ±0 |
37+| loc | 345 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 66 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 6 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
55+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
56+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
57+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
58+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
59+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
60+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
61+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
62+| 55 | 2026-09-15T09:35:03Z | 0 | 0 | 0 | 37 | PASS |
63+| 56 | 2026-09-15T16:57:10Z | 0 | 0 | 0 | 37 | PASS |
new file mode 100644
@@ -0,0 +1,63 @@
1+# Quality report — 2026-09-15T16:57:10Z
2+
3+- **Gate**: ✅ **PASS**
4+- **Commit**: `cb86146` on `feature/acp`
5+- **qlty**: qlty 0.639.0 linux-arm64 (d9801f1 2026-07-23)
6+- **Run**: #56 (previous: 2026-09-15T09:35:03Z)
7+
8+## Lint issues (`qlty check`)
9+
10+_none_
11+
12+### Top rules
13+
14+_none_
15+
16+### Most affected files
17+
18+_none_
19+
20+## Code smells (`qlty smells`)
21+
22+Total: **0** (vs previous: ±0)
23+
24+_none_
25+
26+## Metrics (`qlty metrics`)
27+
28+| metric | total | vs previous |
29+|---|---|---|
30+| funcs | 24 | ±0 |
31+| classes | 2 | ±0 |
32+| fields | 9 | ±0 |
33+| cyclo | 79 | ±0 |
34+| complex | 37 | ±0 |
35+| lcom | 0 | ±0 |
36+| lines | 584 | ±0 |
37+| loc | 345 | ±0 |
38+
39+### Most complex files
40+
41+| file | complex | cyclo | loc |
42+|---|---|---|---|
43+| internal/golang/scan.go | 17 | 36 | 129 |
44+| main.go | 16 | 32 | 132 |
45+| internal/golang/golang.go | 4 | 8 | 66 |
46+| demo/index.js | 0 | 1 | 4 |
47+| demo/main.go | 0 | 1 | 8 |
48+| internal/golang/templates.go | 0 | 1 | 6 |
49+
50+## Trend
51+
52+| run | timestamp | error | warning | smells | complex | gate |
53+|---|---|---|---|---|---|---|
54+| 47 | 2026-09-01T12:04:38Z | 0 | 0 | 0 | 37 | PASS |
55+| 48 | 2026-09-01T12:14:57Z | 0 | 0 | 0 | 37 | PASS |
56+| 49 | 2026-09-01T15:31:09Z | 0 | 0 | 0 | 37 | PASS |
57+| 50 | 2026-09-01T16:09:49Z | 0 | 0 | 0 | 37 | PASS |
58+| 51 | 2026-09-02T05:02:09Z | 0 | 0 | 0 | 37 | PASS |
59+| 52 | 2026-09-02T06:13:01Z | 0 | 0 | 0 | 37 | PASS |
60+| 53 | 2026-09-02T19:13:51Z | 0 | 0 | 0 | 37 | PASS |
61+| 54 | 2026-09-02T19:18:21Z | 0 | 0 | 0 | 37 | PASS |
62+| 55 | 2026-09-15T09:35:03Z | 0 | 0 | 0 | 37 | PASS |
63+| 56 | 2026-09-15T16:57:10Z | 0 | 0 | 0 | 37 | PASS |
added .tickets/config.yaml +2 -0
new file mode 100644
@@ -0,0 +1,2 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+version: 1
new file mode 100644
@@ -0,0 +1,2 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+version: 1
added .tickets/epics.yaml +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+epics:
3+ - title: Improve GUI
4+ state: open
5+ color: "#5319e7"
6+ - title: Editor
7+ state: open
8+ color: "#d4c5f9"
9+ - title: Golang features
10+ state: open
11+ color: "#1d76db"
12+ - title: Portability
13+ state: open
14+ color: "#0e8a16"
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+epics:
3+ - title: Improve GUI
4+ state: open
5+ color: "#5319e7"
6+ - title: Editor
7+ state: open
8+ color: "#d4c5f9"
9+ - title: Golang features
10+ state: open
11+ color: "#1d76db"
12+ - title: Portability
13+ state: open
14+ color: "#0e8a16"
added .tickets/issues/0002-project-settings.yaml +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 2
3+title: project settings
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T18:37:58.737Z
9+updatedAt: 2026-08-31T05:06:59.703Z
10+labels: []
11+epic: Editor
12+body: |
13+ save project settings in .turbo-go directory
14+
15+ - theme
16+ - auto save
17+tasks: []
18+comments: []
new file mode 100644
@@ -0,0 +1,18 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 2
3+title: project settings
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T18:37:58.737Z
9+updatedAt: 2026-08-31T05:06:59.703Z
10+labels: []
11+epic: Editor
12+body: |
13+ save project settings in .turbo-go directory
14+
15+ - theme
16+ - auto save
17+tasks: []
18+comments: []
added .tickets/issues/0003-add-a-tree-view-windows-for-project.yaml +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 3
3+title: add a tree view windows (for project)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:30:19.115Z
9+updatedAt: 2026-08-31T06:35:29.699Z
10+labels:
11+ - priority::urgent
12+epic: Improve GUI
13+body: ""
14+tasks: []
15+comments: []
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 3
3+title: add a tree view windows (for project)
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:30:19.115Z
9+updatedAt: 2026-08-31T06:35:29.699Z
10+labels:
11+ - priority::urgent
12+epic: Improve GUI
13+body: ""
14+tasks: []
15+comments: []
added .tickets/issues/0004-define-what-is-a-project-a-golang-project.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 4
3+title: define what is a project - a golang project
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:30:34.731Z
9+updatedAt: 2026-08-31T17:05:26.301Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 4
3+title: define what is a project - a golang project
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:30:34.731Z
9+updatedAt: 2026-08-31T17:05:26.301Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0005-wasm-plugins-extension.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 5
3+title: wasm plugins / extension
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:31:35.309Z
9+updatedAt: 2026-09-02T03:50:06.813Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 5
3+title: wasm plugins / extension
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:31:35.309Z
9+updatedAt: 2026-09-02T03:50:06.813Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0006-snippets.yaml +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 6
3+title: snippets
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:31:48.999Z
9+updatedAt: 2026-08-31T11:28:45.512Z
10+labels:
11+ - priority::urgent
12+epic: Editor
13+body: ""
14+tasks: []
15+comments: []
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 6
3+title: snippets
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:31:48.999Z
9+updatedAt: 2026-08-31T11:28:45.512Z
10+labels:
11+ - priority::urgent
12+epic: Editor
13+body: ""
14+tasks: []
15+comments: []
added .tickets/issues/0007-add-a-terminal-window.yaml +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 7
3+title: add a terminal window
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:40:15.917Z
9+updatedAt: 2026-08-31T04:59:02.859Z
10+labels:
11+ - priority::high
12+epic: Editor
13+body: ""
14+tasks: []
15+comments: []
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 7
3+title: add a terminal window
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:40:15.917Z
9+updatedAt: 2026-08-31T04:59:02.859Z
10+labels:
11+ - priority::high
12+epic: Editor
13+body: ""
14+tasks: []
15+comments: []
added .tickets/issues/0008-add-a-mini-agent-view.yaml +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 8
3+title: add a mini agent view
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:41:17.968Z
9+updatedAt: 2026-09-02T03:50:14.588Z
10+labels:
11+ - priority::low
12+body: ""
13+tasks: []
14+comments: []
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 8
3+title: add a mini agent view
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:41:17.968Z
9+updatedAt: 2026-09-02T03:50:14.588Z
10+labels:
11+ - priority::low
12+body: ""
13+tasks: []
14+comments: []
added .tickets/issues/0009-add-markdown-syntax.yaml +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 9
3+title: add markdown syntax
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:43:41.818Z
9+updatedAt: 2026-08-31T10:20:36.567Z
10+labels: []
11+epic: Editor
12+body: ""
13+tasks: []
14+comments: []
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 9
3+title: add markdown syntax
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-30T19:43:41.818Z
9+updatedAt: 2026-08-31T10:20:36.567Z
10+labels: []
11+epic: Editor
12+body: ""
13+tasks: []
14+comments: []
added .tickets/issues/0010-add-version-number-to-about.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 10
3+title: add version number to about
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:09:36.186Z
9+updatedAt: 2026-08-31T19:21:02.216Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 10
3+title: add version number to about
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:09:36.186Z
9+updatedAt: 2026-08-31T19:21:02.216Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0011-create-a-website-for-turbo-go.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 11
3+title: create a website for turbo-go
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:10:12.609Z
9+updatedAt: 2026-08-31T03:10:12.609Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 11
3+title: create a website for turbo-go
4+state: open
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:10:12.609Z
9+updatedAt: 2026-08-31T03:10:12.609Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0012-add-more-themes.yaml +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 12
3+title: add more themes
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:22:47.784Z
9+updatedAt: 2026-09-02T03:50:38.737Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
new file mode 100644
@@ -0,0 +1,13 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 12
3+title: add more themes
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:22:47.784Z
9+updatedAt: 2026-09-02T03:50:38.737Z
10+labels: []
11+body: ""
12+tasks: []
13+comments: []
added .tickets/issues/0013-add-javascript-syntax.yaml +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 13
3+title: add javascript syntax
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:24:38.059Z
9+updatedAt: 2026-08-31T10:20:47.149Z
10+labels: []
11+epic: Editor
12+body: ""
13+tasks: []
14+comments: []
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 13
3+title: add javascript syntax
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:24:38.059Z
9+updatedAt: 2026-08-31T10:20:47.149Z
10+labels: []
11+epic: Editor
12+body: ""
13+tasks: []
14+comments: []
added .tickets/issues/0014-add-html-syntax.yaml +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 14
3+title: add html syntax
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:24:58.752Z
9+updatedAt: 2026-08-31T10:21:01.982Z
10+labels: []
11+epic: Editor
12+body: ""
13+tasks: []
14+comments: []
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 14
3+title: add html syntax
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T03:24:58.752Z
9+updatedAt: 2026-08-31T10:21:01.982Z
10+labels: []
11+epic: Editor
12+body: ""
13+tasks: []
14+comments: []
added .tickets/issues/0015-windows-support-for-terminal-windows.yaml +84 -0
new file mode 100644
@@ -0,0 +1,84 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 15
3+title: windows support for terminal windows
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:40:00.000Z
9+updatedAt: 2026-09-02T03:51:59.496Z
10+labels:
11+ - priority::medium
12+body: |
13+ Terminal windows (#7) ship for Linux and macOS only. On Windows, `F8` opens a
14+ message saying they are not supported yet and changes nothing else.
15+
16+ Everything above the pseudo-terminal is already portable. `Parser`, `Screen`,
17+ `Encode` and `View` are pure Go with no platform assumptions, and the whole
18+ VT/ANSI emulator is tested without a process behind it. What is missing is one
19+ file.
20+
21+ ## The platform surface
22+
23+ `internal/terminal` isolates the whole of it in three build-tagged files, each
24+ implementing the same three functions:
25+
26+ func openPTY() (master, slave *os.File, err error)
27+ func setWinsize(f *os.File, width, height int) error
28+ func childAttributes() *syscall.SysProcAttr
29+
30+ | File | Platform |
31+ | --- | --- |
32+ | `pty_linux.go` | `TIOCSPTLCK`, `TIOCGPTN` -> `/dev/pts/N` |
33+ | `pty_darwin.go` | `TIOCPTYGRANT`, `TIOCPTYUNLK`, `TIOCPTYGNAME` |
34+ | `pty_other.go` | returns `ErrUnsupported` |
35+
36+ Windows means adding `pty_windows.go` and narrowing `pty_other.go`'s build tag.
37+ No other file should need to change.
38+
39+ ## Why it is not a fourth ioctl
40+
41+ Windows has no `/dev/ptmx`. The equivalent is **ConPTY**, available from
42+ Windows 10 1809, and it does not fit the same shape:
43+
44+ - `CreatePseudoConsole` takes two pipe handles and returns an `HPCON`, rather
45+ than handing back a master and a slave file.
46+ - The child is started with `CreateProcess` and an attribute list carrying
47+ `PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE_HANDLE` — not `os/exec`'s
48+ `SysProcAttr{Setsid, Setctty}`. `exec.Cmd` may not be usable directly.
49+ - Resizing is `ResizePseudoConsole(hpcon, COORD)`, not an ioctl.
50+ - There is no controlling terminal and no `SIGWINCH`: job control has no
51+ equivalent, and `Ctrl-C` reaches the child through the console rather than
52+ through a process group.
53+
54+ So the contract above may need widening — most likely `Session` gaining a
55+ platform-provided `resize` and `close` rather than `setWinsize` alone. Keep
56+ the change inside `internal/terminal`; `internal/app` should not learn that
57+ Windows exists.
58+
59+ ## Which shell
60+
61+ `shellOrDefault` falls back to `/bin/sh`, which is meaningless there. Windows
62+ needs its own default — `%COMSPEC%`, or PowerShell — decided before the port,
63+ because it is the first thing a user notices.
64+
65+ ## Notes
66+
67+ - `golang.org/x/sys/windows` carries the ConPTY bindings, and is already in
68+ the module graph as `golang.org/x/sys`. No new dependency.
69+ - The emulator sends `TERM=xterm-256color`; ConPTY emits VT sequences, so the
70+ existing parser should be close to sufficient. Expect differences in how
71+ the alternate screen and the cursor-visibility modes are used.
72+ - This cannot be developed or tested in the current sandbox: it is Linux, and
73+ no Windows machine is reachable from it. Whoever takes it needs Windows.
74+
75+ ## Checklist
76+
77+ - [ ] decide the default shell on Windows (%COMSPEC% or PowerShell)
78+ - [ ] add `pty_windows.go` using `CreatePseudoConsole` / `ResizePseudoConsole`
79+ - [ ] narrow the build tag on `pty_other.go`
80+ - [ ] widen the platform contract if `exec.Cmd` cannot carry the HPCON
81+ - [ ] run the `internal/terminal` suite on Windows and unskip what passes
82+ - [ ] update `docs/*/reference/terminal.md`, which currently says Windows is unsupported
83+tasks: []
84+comments: []
new file mode 100644
@@ -0,0 +1,84 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 15
3+title: windows support for terminal windows
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:40:00.000Z
9+updatedAt: 2026-09-02T03:51:59.496Z
10+labels:
11+ - priority::medium
12+body: |
13+ Terminal windows (#7) ship for Linux and macOS only. On Windows, `F8` opens a
14+ message saying they are not supported yet and changes nothing else.
15+
16+ Everything above the pseudo-terminal is already portable. `Parser`, `Screen`,
17+ `Encode` and `View` are pure Go with no platform assumptions, and the whole
18+ VT/ANSI emulator is tested without a process behind it. What is missing is one
19+ file.
20+
21+ ## The platform surface
22+
23+ `internal/terminal` isolates the whole of it in three build-tagged files, each
24+ implementing the same three functions:
25+
26+ func openPTY() (master, slave *os.File, err error)
27+ func setWinsize(f *os.File, width, height int) error
28+ func childAttributes() *syscall.SysProcAttr
29+
30+ | File | Platform |
31+ | --- | --- |
32+ | `pty_linux.go` | `TIOCSPTLCK`, `TIOCGPTN` -> `/dev/pts/N` |
33+ | `pty_darwin.go` | `TIOCPTYGRANT`, `TIOCPTYUNLK`, `TIOCPTYGNAME` |
34+ | `pty_other.go` | returns `ErrUnsupported` |
35+
36+ Windows means adding `pty_windows.go` and narrowing `pty_other.go`'s build tag.
37+ No other file should need to change.
38+
39+ ## Why it is not a fourth ioctl
40+
41+ Windows has no `/dev/ptmx`. The equivalent is **ConPTY**, available from
42+ Windows 10 1809, and it does not fit the same shape:
43+
44+ - `CreatePseudoConsole` takes two pipe handles and returns an `HPCON`, rather
45+ than handing back a master and a slave file.
46+ - The child is started with `CreateProcess` and an attribute list carrying
47+ `PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE_HANDLE` — not `os/exec`'s
48+ `SysProcAttr{Setsid, Setctty}`. `exec.Cmd` may not be usable directly.
49+ - Resizing is `ResizePseudoConsole(hpcon, COORD)`, not an ioctl.
50+ - There is no controlling terminal and no `SIGWINCH`: job control has no
51+ equivalent, and `Ctrl-C` reaches the child through the console rather than
52+ through a process group.
53+
54+ So the contract above may need widening — most likely `Session` gaining a
55+ platform-provided `resize` and `close` rather than `setWinsize` alone. Keep
56+ the change inside `internal/terminal`; `internal/app` should not learn that
57+ Windows exists.
58+
59+ ## Which shell
60+
61+ `shellOrDefault` falls back to `/bin/sh`, which is meaningless there. Windows
62+ needs its own default — `%COMSPEC%`, or PowerShell — decided before the port,
63+ because it is the first thing a user notices.
64+
65+ ## Notes
66+
67+ - `golang.org/x/sys/windows` carries the ConPTY bindings, and is already in
68+ the module graph as `golang.org/x/sys`. No new dependency.
69+ - The emulator sends `TERM=xterm-256color`; ConPTY emits VT sequences, so the
70+ existing parser should be close to sufficient. Expect differences in how
71+ the alternate screen and the cursor-visibility modes are used.
72+ - This cannot be developed or tested in the current sandbox: it is Linux, and
73+ no Windows machine is reachable from it. Whoever takes it needs Windows.
74+
75+ ## Checklist
76+
77+ - [ ] decide the default shell on Windows (%COMSPEC% or PowerShell)
78+ - [ ] add `pty_windows.go` using `CreatePseudoConsole` / `ResizePseudoConsole`
79+ - [ ] narrow the build tag on `pty_other.go`
80+ - [ ] widen the platform contract if `exec.Cmd` cannot carry the HPCON
81+ - [ ] run the `internal/terminal` suite on Windows and unskip what passes
82+ - [ ] update `docs/*/reference/terminal.md`, which currently says Windows is unsupported
83+tasks: []
84+comments: []
added .tickets/issues/0016-when-windows-are-tiled-remove-window-shadow.yaml +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 16
3+title: When windows are tiled, remove window shadow
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:24:29.967Z
9+updatedAt: 2026-09-02T03:53:02.390Z
10+labels:
11+ - priority::low
12+ - kind::bug
13+epic: Improve GUI
14+body: |
15+ Make the test with a terminal window: when it has the focus, the shadow bites the other window
16+tasks: []
17+comments: []
new file mode 100644
@@ -0,0 +1,17 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 16
3+title: When windows are tiled, remove window shadow
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:24:29.967Z
9+updatedAt: 2026-09-02T03:53:02.390Z
10+labels:
11+ - priority::low
12+ - kind::bug
13+epic: Improve GUI
14+body: |
15+ Make the test with a terminal window: when it has the focus, the shadow bites the other window
16+tasks: []
17+comments: []
added .tickets/issues/0017-add-golang-tools-menu.yaml +31 -0
new file mode 100644
@@ -0,0 +1,31 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 17
3+title: Add (golang) tools menu
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:37:38.703Z
9+updatedAt: 2026-08-31T17:00:51.269Z
10+labels:
11+ - priority::urgent
12+epic: Golang features
13+body: |
14+ A Golang specific menu with some useful commands:
15+ - go mod init + touch main.go
16+
17+ See comments for ideas
18+tasks: []
19+comments:
20+ - id: 1
21+ author:
22+ name: k33g
23+ email: ph.charriere@gmail.com
24+ createdAt: 2026-08-31T04:39:30.864Z
25+ updatedAt: 2026-08-31T04:40:59.162Z
26+ body: |
27+ Define tools command in a toml file
28+
29+ if the file is automatically initialized: add golang default commands
30+
31+ it's possbile to re-generate the tools
new file mode 100644
@@ -0,0 +1,31 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 17
3+title: Add (golang) tools menu
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:37:38.703Z
9+updatedAt: 2026-08-31T17:00:51.269Z
10+labels:
11+ - priority::urgent
12+epic: Golang features
13+body: |
14+ A Golang specific menu with some useful commands:
15+ - go mod init + touch main.go
16+
17+ See comments for ideas
18+tasks: []
19+comments:
20+ - id: 1
21+ author:
22+ name: k33g
23+ email: ph.charriere@gmail.com
24+ createdAt: 2026-08-31T04:39:30.864Z
25+ updatedAt: 2026-08-31T04:40:59.162Z
26+ body: |
27+ Define tools command in a toml file
28+
29+ if the file is automatically initialized: add golang default commands
30+
31+ it's possbile to re-generate the tools
added .tickets/issues/0018-create-a-core-library-from-turbo-go.yaml +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 18
3+title: Create a core library from Turbo Go
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:51:55.353Z
9+updatedAt: 2026-09-02T03:52:08.669Z
10+labels: []
11+epic: Portability
12+body: |
13+ Goal: to be able to create other Turbo Editors
14+tasks: []
15+comments: []
new file mode 100644
@@ -0,0 +1,15 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 18
3+title: Create a core library from Turbo Go
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T04:51:55.353Z
9+updatedAt: 2026-09-02T03:52:08.669Z
10+labels: []
11+epic: Portability
12+body: |
13+ Goal: to be able to create other Turbo Editors
14+tasks: []
15+comments: []
added .tickets/issues/0019-add-a-visual-indicator-to-sho-that-the-file-is-s.yaml +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 19
3+title: Add a visual indicator to sho that the file is saved
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T05:02:53.539Z
9+updatedAt: 2026-08-31T10:21:21.699Z
10+labels: []
11+epic: Improve GUI
12+body: ""
13+tasks: []
14+comments: []
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 19
3+title: Add a visual indicator to sho that the file is saved
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T05:02:53.539Z
9+updatedAt: 2026-08-31T10:21:21.699Z
10+labels: []
11+epic: Improve GUI
12+body: ""
13+tasks: []
14+comments: []
added .tickets/issues/0020-the-button-to-close-the-window-should-be-x.yaml +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 20
3+title: The button to close the window should be [x]
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T05:03:36.231Z
9+updatedAt: 2026-08-31T06:01:37.776Z
10+labels: []
11+epic: Improve GUI
12+body: ""
13+tasks: []
14+comments: []
new file mode 100644
@@ -0,0 +1,14 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 20
3+title: The button to close the window should be [x]
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T05:03:36.231Z
9+updatedAt: 2026-08-31T06:01:37.776Z
10+labels: []
11+epic: Improve GUI
12+body: ""
13+tasks: []
14+comments: []
added .tickets/issues/0021-add-a-button-to-maximize-the-window.yaml +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 21
3+title: Add a button to maximize the window
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T05:04:34.636Z
9+updatedAt: 2026-08-31T06:01:29.443Z
10+labels: []
11+epic: Improve GUI
12+related:
13+ - 20
14+body: ""
15+tasks: []
16+comments: []
new file mode 100644
@@ -0,0 +1,16 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+id: 21
3+title: Add a button to maximize the window
4+state: closed
5+author:
6+ name: k33g
7+ email: ph.charriere@gmail.com
8+createdAt: 2026-08-31T05:04:34.636Z
9+updatedAt: 2026-08-31T06:01:29.443Z
10+labels: []
11+epic: Improve GUI
12+related:
13+ - 20
14+body: ""
15+tasks: []
16+comments: []
added .tickets/labels.yaml +38 -0
new file mode 100644
@@ -0,0 +1,38 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+labels:
3+ - name: bug
4+ color: "#d73a4a"
5+ description: Something is not working
6+ - name: documentation
7+ color: "#0075ca"
8+ description: Improvements or additions to documentation
9+ - name: enhancement
10+ color: "#a2eeef"
11+ description: New feature or request
12+ - name: question
13+ color: "#d876e3"
14+ description: Further information is requested
15+ - name: wontfix
16+ color: "#ffffff"
17+ description: This will not be worked on
18+ - name: priority::low
19+ color: "#c5def5"
20+ description: Can wait
21+ - name: priority::medium
22+ color: "#fbca04"
23+ description: Normal priority
24+ - name: priority::high
25+ color: "#d93f0b"
26+ description: Should be picked up next
27+ - name: priority::urgent
28+ color: "#b60205"
29+ description: Drop everything
30+ - name: kind::bug
31+ color: "#d73a4a"
32+ description: Something is broken
33+ - name: kind::feature
34+ color: "#0e8a16"
35+ description: Something new
36+ - name: kind::chore
37+ color: "#bfdadc"
38+ description: Maintenance, no visible change
new file mode 100644
@@ -0,0 +1,38 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+labels:
3+ - name: bug
4+ color: "#d73a4a"
5+ description: Something is not working
6+ - name: documentation
7+ color: "#0075ca"
8+ description: Improvements or additions to documentation
9+ - name: enhancement
10+ color: "#a2eeef"
11+ description: New feature or request
12+ - name: question
13+ color: "#d876e3"
14+ description: Further information is requested
15+ - name: wontfix
16+ color: "#ffffff"
17+ description: This will not be worked on
18+ - name: priority::low
19+ color: "#c5def5"
20+ description: Can wait
21+ - name: priority::medium
22+ color: "#fbca04"
23+ description: Normal priority
24+ - name: priority::high
25+ color: "#d93f0b"
26+ description: Should be picked up next
27+ - name: priority::urgent
28+ color: "#b60205"
29+ description: Drop everything
30+ - name: kind::bug
31+ color: "#d73a4a"
32+ description: Something is broken
33+ - name: kind::feature
34+ color: "#0e8a16"
35+ description: Something new
36+ - name: kind::chore
37+ color: "#bfdadc"
38+ description: Maintenance, no visible change
added .tickets/milestones.yaml +2 -0
new file mode 100644
@@ -0,0 +1,2 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+milestones: []
new file mode 100644
@@ -0,0 +1,2 @@
1+# Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2+milestones: []
added .turbo-go/acp.toml +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+[[agent]]
2+name = "Bob (llama.cpp)"
3+command = "docker"
4+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
5+env = { TELEMETRY_ENABLED = "false" }
6+
7+# The user's own agent, the one Zed runs as "mini-me": it speaks the protocol
8+# on stdin/stdout when started with -acp, and announces slash commands the
9+# editor lists when / is typed at the start of the box.
10+[[agent]]
11+name = "mini-me (llama.cpp)"
12+command = "mm"
13+args = ["-acp"]
14+env = { AGENT_CONFIG = ".turbo-go/agent.llamacpp.yaml" }
15+#env = { AGENT_CONFIG = "/Users/k33g/kDrive/Rickub/bots-garden/mini-me/agent.llamacpp.yaml" }
new file mode 100644
@@ -0,0 +1,15 @@
1+[[agent]]
2+name = "Bob (llama.cpp)"
3+command = "docker"
4+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
5+env = { TELEMETRY_ENABLED = "false" }
6+
7+# The user's own agent, the one Zed runs as "mini-me": it speaks the protocol
8+# on stdin/stdout when started with -acp, and announces slash commands the
9+# editor lists when / is typed at the start of the box.
10+[[agent]]
11+name = "mini-me (llama.cpp)"
12+command = "mm"
13+args = ["-acp"]
14+env = { AGENT_CONFIG = ".turbo-go/agent.llamacpp.yaml" }
15+#env = { AGENT_CONFIG = "/Users/k33g/kDrive/Rickub/bots-garden/mini-me/agent.llamacpp.yaml" }
added .turbo-go/agent.llamacpp.yaml +155 -0
new file mode 100644
@@ -0,0 +1,155 @@
1+# Same agent, served by llama.cpp's `llama-server` instead of Docker Model Runner.
2+#
3+# llama-server -hf poolside/Laguna-XS-2.1-GGUF:Q4_K_M \
4+# -a poolside/Laguna-XS-2.1-GGUF:Q4_K_M --jinja -c 32768 --port 8080
5+# ./bob agent.llamacpp.yaml # from demo/, so that skills/ is found
6+#
7+# `-hf <user>/<repo>:<quant>` downloads the GGUF from Hugging Face (the quant tag
8+# is optional and defaults to Q4_K_M; here the repo has exactly one such file,
9+# Laguna-XS-2.1-Q4_K_M.gguf, 20.3 GB). `-a` gives the served model the SAME name
10+# as the `model:` key below: llama-server routes requests on the `model` field,
11+# and without an alias it exposes the file's name on /v1/models, not the repo's.
12+# `--jinja` is not optional: without it llama-server refuses the `tools`
13+# parameter, and this agent is nothing but tool calls. The agent recognises that
14+# refusal and says so in one line instead of printing the server's stack.
15+# `-c` is the context the server SERVES (the model accepts up to 262,144); it is
16+# what shows up under `ctx:` at start-up, read from GET /props.
17+# Laguna XS 2.1 is a 33B MoE with 3B active parameters, built for coding; its
18+# card says the chat template does tool calling under --jinja, and that it
19+# needs a recent llama.cpp (the card points at PR #25165 — check it is merged
20+# in your build, or build that branch).
21+
22+provider: llamacpp
23+
24+# Must match what GET /v1/models returns — the `-a` alias above. llama-server
25+# routes on this field; a name it does not serve is an error, not a fallback.
26+#model: poolside/Laguna-XS-2.1-GGUF:Q4_K_M
27+#model: unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M
28+model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
29+
30+# llama-server's default is 127.0.0.1:8080; the OpenAI routes sit under /v1.
31+# Leave it out to get exactly this value.
32+baseUrl: http://127.0.0.1:8080/v1
33+
34+# No fallback here: llama-server is wherever you started it.
35+fallback: ""
36+
37+# Only when llama-server was started with --api-key; unset otherwise.
38+# apiKeyEnv: LLAMA_API_KEY
39+
40+# The built-in file tools: read_file, write_file, edit_file. This is the switch
41+# this part exists for. `true`: the model edits files through tools it can SEE
42+# in its tool list. `false`: the agent is part 09 again — bash and read_skill —
43+# and edits files through the `edit` CLI if it is on the PATH. Same binary,
44+# same prompts, two set-ups: measured side by side, that is the comparison
45+# (in part 07, `read_skill` as a tool was loaded 3/3 times where a catalogue
46+# in the prompt plus `cat` managed 1/11). Paths are relative to the current
47+# directory and are not confined to it.
48+editTools: true
49+
50+# 0 = read the served size from /props. Set it when the server hides /props
51+# (a reverse proxy) or when you know better.
52+contextWindow: 0
53+
54+maxOutput: 16000
55+maxTurns: 40
56+skillsDir: skills
57+previewLines: 20
58+displayCommands: true
59+
60+system: |
61+ Your name is Bob.
62+ You are a coding agent working in a terminal.
63+ You have a "bash" tool to run shell commands.
64+ Use it to explore files, run tests, inspect the repository, etc.
65+ Chain several commands if needed, then answer clearly in English.
66+
67+ A request often mixes things you answer from yourself ("say hello") with
68+ things only a command can answer ("list the files"). Handle every part, in
69+ the order asked, and run a command for each part that needs one.
70+ Never state the contents of a file, the output of a command, or the state of
71+ the repository unless a command in THIS answer returned it. What you did not
72+ read, you do not know: run the command instead of recalling it.
73+
74+ SKILLS
75+ You have a second tool, `read_skill`. Its description lists the procedures
76+ available for this project — one per kind of task.
77+
78+ Any request to DO something to a Go project is a skill, not a shell command
79+ you invent. Match the request against that list, call `read_skill` FIRST,
80+ before any bash command, and then follow what it says step by step.
81+
82+ FILE EDITING
83+ You have three tools for files: `read_file`, `edit_file` and `write_file`.
84+ They are how a file gets read and changed here: each change is exact,
85+ checked before it is written, and comes back as a diff with line numbers.
86+ bash is for running things — building, testing, listing, searching.
87+
88+ - Read before you write: call `read_file` on the file (numbered=true when
89+ you need line numbers). You cannot target text you have not seen; never
90+ rely on what you think you remember about a file.
91+ - To change an existing file, call `edit_file` with one or more {old, new}
92+ pairs. `old` is copied from the file character for character — same
93+ spaces, same indentation, same line breaks — and appears exactly once:
94+ add the surrounding lines until it is unique. Several pairs are applied
95+ together, against the original file. An empty `new` deletes the text.
96+ - Call `write_file` only to create a file, or to rewrite one entirely and
97+ on purpose. On an existing file it replaces everything, including what
98+ you did not intend to touch.
99+ - Read the diff the tool returns: it says exactly what changed and on which
100+ line. If `edit_file` refuses — text not found, ambiguous, overlapping
101+ edits — read the file again and fix `old`. Do not fall back to
102+ `write_file` to force the change through.
103+ - After editing code, run the narrowest check with bash: the formatter, the
104+ compiler, or the test covering that file.
105+
106+ RULES
107+ - Keep everything the file already does, unless the user asked to remove it.
108+ - Touch only the files the request is about. Do not add tests, files or
109+ features that were not asked for.
110+ - Never run a git command unless the user says git, commit or push.
111+ - Never move, rename or delete a file unless the user asked for it.
112+ - Then answer in English, in a few lines.
113+ - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`)
114+ to understand the options, then run the command.
115+
116+ BACKGROUND JOBS
117+ Never let a command block the answer. Anything that serves, watches or runs
118+ long goes to the background, with BOTH streams redirected and its pid kept:
119+
120+ nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid
121+
122+ Redirecting only stdout still blocks until the process exits. Read the
123+ `bg-jobs` skill before you wait on, inspect or stop such a job — each has a
124+ rule you cannot guess. Stop every job you started before you finish, and say
125+ which ones you left running.
126+
127+# Same sampling as the DMR file. llama-server honours `parallel_tool_calls`
128+# (off by default on its side too) and `max_tokens`.
129+sampling:
130+ temperature: 0.0
131+ parallel_tool_calls: false
132+ top_p: 0.9
133+ max_tokens: 4096
134+
135+# llama-server processes the whole prompt before the first token; on a laptop a
136+# 32k context can take a while. Same watchdog as DMR, raise it if it fires.
137+watchdogTimeout: 30s
138+
139+# Context compression (from part 08). ON here, because this is the one set-up
140+# where the agent knows the window without being told: `contextWindow: 0`
141+# above means "read n_ctx from /props", and that number is what `threshold`
142+# applies to — so the banner's `ctx: 32768 (/props)` and the trigger agree by
143+# construction. With a 32k window and a 33B MoE, the whole history is
144+# re-read at every turn; compressing at 75 % keeps the prefill — and the
145+# watchdog — inside the 30 s above. Set `enabled: false` to get the exact
146+# part-07 behaviour back; `/compact` still works.
147+context:
148+ enabled: true
149+ threshold: 75
150+ # Kept as a net for a reverse proxy that hides /props: then the window is
151+ # unknown and only this can trigger.
152+ maxMessages: 80
153+ keepLastTurns: 3
154+ summaryMaxTokens: 1200
155+ showStats: true
new file mode 100644
@@ -0,0 +1,155 @@
1+# Same agent, served by llama.cpp's `llama-server` instead of Docker Model Runner.
2+#
3+# llama-server -hf poolside/Laguna-XS-2.1-GGUF:Q4_K_M \
4+# -a poolside/Laguna-XS-2.1-GGUF:Q4_K_M --jinja -c 32768 --port 8080
5+# ./bob agent.llamacpp.yaml # from demo/, so that skills/ is found
6+#
7+# `-hf <user>/<repo>:<quant>` downloads the GGUF from Hugging Face (the quant tag
8+# is optional and defaults to Q4_K_M; here the repo has exactly one such file,
9+# Laguna-XS-2.1-Q4_K_M.gguf, 20.3 GB). `-a` gives the served model the SAME name
10+# as the `model:` key below: llama-server routes requests on the `model` field,
11+# and without an alias it exposes the file's name on /v1/models, not the repo's.
12+# `--jinja` is not optional: without it llama-server refuses the `tools`
13+# parameter, and this agent is nothing but tool calls. The agent recognises that
14+# refusal and says so in one line instead of printing the server's stack.
15+# `-c` is the context the server SERVES (the model accepts up to 262,144); it is
16+# what shows up under `ctx:` at start-up, read from GET /props.
17+# Laguna XS 2.1 is a 33B MoE with 3B active parameters, built for coding; its
18+# card says the chat template does tool calling under --jinja, and that it
19+# needs a recent llama.cpp (the card points at PR #25165 — check it is merged
20+# in your build, or build that branch).
21+
22+provider: llamacpp
23+
24+# Must match what GET /v1/models returns — the `-a` alias above. llama-server
25+# routes on this field; a name it does not serve is an error, not a fallback.
26+#model: poolside/Laguna-XS-2.1-GGUF:Q4_K_M
27+#model: unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M
28+model: jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M
29+
30+# llama-server's default is 127.0.0.1:8080; the OpenAI routes sit under /v1.
31+# Leave it out to get exactly this value.
32+baseUrl: http://127.0.0.1:8080/v1
33+
34+# No fallback here: llama-server is wherever you started it.
35+fallback: ""
36+
37+# Only when llama-server was started with --api-key; unset otherwise.
38+# apiKeyEnv: LLAMA_API_KEY
39+
40+# The built-in file tools: read_file, write_file, edit_file. This is the switch
41+# this part exists for. `true`: the model edits files through tools it can SEE
42+# in its tool list. `false`: the agent is part 09 again — bash and read_skill —
43+# and edits files through the `edit` CLI if it is on the PATH. Same binary,
44+# same prompts, two set-ups: measured side by side, that is the comparison
45+# (in part 07, `read_skill` as a tool was loaded 3/3 times where a catalogue
46+# in the prompt plus `cat` managed 1/11). Paths are relative to the current
47+# directory and are not confined to it.
48+editTools: true
49+
50+# 0 = read the served size from /props. Set it when the server hides /props
51+# (a reverse proxy) or when you know better.
52+contextWindow: 0
53+
54+maxOutput: 16000
55+maxTurns: 40
56+skillsDir: skills
57+previewLines: 20
58+displayCommands: true
59+
60+system: |
61+ Your name is Bob.
62+ You are a coding agent working in a terminal.
63+ You have a "bash" tool to run shell commands.
64+ Use it to explore files, run tests, inspect the repository, etc.
65+ Chain several commands if needed, then answer clearly in English.
66+
67+ A request often mixes things you answer from yourself ("say hello") with
68+ things only a command can answer ("list the files"). Handle every part, in
69+ the order asked, and run a command for each part that needs one.
70+ Never state the contents of a file, the output of a command, or the state of
71+ the repository unless a command in THIS answer returned it. What you did not
72+ read, you do not know: run the command instead of recalling it.
73+
74+ SKILLS
75+ You have a second tool, `read_skill`. Its description lists the procedures
76+ available for this project — one per kind of task.
77+
78+ Any request to DO something to a Go project is a skill, not a shell command
79+ you invent. Match the request against that list, call `read_skill` FIRST,
80+ before any bash command, and then follow what it says step by step.
81+
82+ FILE EDITING
83+ You have three tools for files: `read_file`, `edit_file` and `write_file`.
84+ They are how a file gets read and changed here: each change is exact,
85+ checked before it is written, and comes back as a diff with line numbers.
86+ bash is for running things — building, testing, listing, searching.
87+
88+ - Read before you write: call `read_file` on the file (numbered=true when
89+ you need line numbers). You cannot target text you have not seen; never
90+ rely on what you think you remember about a file.
91+ - To change an existing file, call `edit_file` with one or more {old, new}
92+ pairs. `old` is copied from the file character for character — same
93+ spaces, same indentation, same line breaks — and appears exactly once:
94+ add the surrounding lines until it is unique. Several pairs are applied
95+ together, against the original file. An empty `new` deletes the text.
96+ - Call `write_file` only to create a file, or to rewrite one entirely and
97+ on purpose. On an existing file it replaces everything, including what
98+ you did not intend to touch.
99+ - Read the diff the tool returns: it says exactly what changed and on which
100+ line. If `edit_file` refuses — text not found, ambiguous, overlapping
101+ edits — read the file again and fix `old`. Do not fall back to
102+ `write_file` to force the change through.
103+ - After editing code, run the narrowest check with bash: the formatter, the
104+ compiler, or the test covering that file.
105+
106+ RULES
107+ - Keep everything the file already does, unless the user asked to remove it.
108+ - Touch only the files the request is about. Do not add tests, files or
109+ features that were not asked for.
110+ - Never run a git command unless the user says git, commit or push.
111+ - Never move, rename or delete a file unless the user asked for it.
112+ - Then answer in English, in a few lines.
113+ - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`)
114+ to understand the options, then run the command.
115+
116+ BACKGROUND JOBS
117+ Never let a command block the answer. Anything that serves, watches or runs
118+ long goes to the background, with BOTH streams redirected and its pid kept:
119+
120+ nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid
121+
122+ Redirecting only stdout still blocks until the process exits. Read the
123+ `bg-jobs` skill before you wait on, inspect or stop such a job — each has a
124+ rule you cannot guess. Stop every job you started before you finish, and say
125+ which ones you left running.
126+
127+# Same sampling as the DMR file. llama-server honours `parallel_tool_calls`
128+# (off by default on its side too) and `max_tokens`.
129+sampling:
130+ temperature: 0.0
131+ parallel_tool_calls: false
132+ top_p: 0.9
133+ max_tokens: 4096
134+
135+# llama-server processes the whole prompt before the first token; on a laptop a
136+# 32k context can take a while. Same watchdog as DMR, raise it if it fires.
137+watchdogTimeout: 30s
138+
139+# Context compression (from part 08). ON here, because this is the one set-up
140+# where the agent knows the window without being told: `contextWindow: 0`
141+# above means "read n_ctx from /props", and that number is what `threshold`
142+# applies to — so the banner's `ctx: 32768 (/props)` and the trigger agree by
143+# construction. With a 32k window and a 33B MoE, the whole history is
144+# re-read at every turn; compressing at 75 % keeps the prefill — and the
145+# watchdog — inside the 30 s above. Set `enabled: false` to get the exact
146+# part-07 behaviour back; `/compact` still works.
147+context:
148+ enabled: true
149+ threshold: 75
150+ # Kept as a net for a reverse proxy that hides /props: then the window is
151+ # unknown and only this can trigger.
152+ maxMessages: 80
153+ keepLastTurns: 3
154+ summaryMaxTokens: 1200
155+ showStats: true
added .turbo-go/agent.yaml +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml
2+providers:
3+ llamacpp:
4+ api_type: openai_chatcompletions
5+ base_url: http://host.docker.internal:8080/v1
6+
7+models:
8+ mellum2:
9+ provider: llamacpp
10+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
11+ #max_tokens: 8192
12+ temperature: 0.7
13+ provider_opts:
14+ context_size: 262144
15+
16+agents:
17+ root:
18+ model: mellum2
19+ description: A helpful AI assistant running on a local llama.cpp server
20+ instruction: |
21+ You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks.
22+ Be helpful, accurate, and concise in your responses.
23+ You have access to the local filesystem and shell: use these tools
24+ welcome_message: |
25+ 🤖 Local Assistant propulsed by **llama.cpp** 🦙
26+
27+ toolsets:
28+ - type: filesystem
29+ - type: shell
new file mode 100644
@@ -0,0 +1,29 @@
1+# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml
2+providers:
3+ llamacpp:
4+ api_type: openai_chatcompletions
5+ base_url: http://host.docker.internal:8080/v1
6+
7+models:
8+ mellum2:
9+ provider: llamacpp
10+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
11+ #max_tokens: 8192
12+ temperature: 0.7
13+ provider_opts:
14+ context_size: 262144
15+
16+agents:
17+ root:
18+ model: mellum2
19+ description: A helpful AI assistant running on a local llama.cpp server
20+ instruction: |
21+ You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks.
22+ Be helpful, accurate, and concise in your responses.
23+ You have access to the local filesystem and shell: use these tools
24+ welcome_message: |
25+ 🤖 Local Assistant propulsed by **llama.cpp** 🦙
26+
27+ toolsets:
28+ - type: filesystem
29+ - type: shell
added .vscode/extensions.json +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+{
2+ "recommendations": [
3+ "ms-azuretools.vscode-docker",
4+ "pkief.material-icon-theme",
5+ "pkief.material-product-icons",
6+ "aaron-bond.better-comments",
7+ "bierner.markdown-mermaid",
8+ ]
9+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,9 @@
1+{
2+ "recommendations": [
3+ "ms-azuretools.vscode-docker",
4+ "pkief.material-icon-theme",
5+ "pkief.material-product-icons",
6+ "aaron-bond.better-comments",
7+ "bierner.markdown-mermaid",
8+ ]
9+}
\ No newline at end of file\ No newline at end of file
added .vscode/settings.json +90 -0
new file mode 100644
@@ -0,0 +1,90 @@
1+{
2+ "workbench.iconTheme": "material-icon-theme",
3+ "workbench.colorTheme": "GitHub Light Colorblind (Beta)",
4+ "editor.fontSize": 14,
5+ "terminal.integrated.fontSize": 14,
6+ "editor.insertSpaces": true,
7+ "editor.tabSize": 4,
8+ "editor.detectIndentation": true,
9+ "files.autoSave": "afterDelay",
10+ "files.autoSaveDelay": 1000,
11+ // "editor.defaultFormatter": "esbenp.prettier-vscode",
12+ "editor.formatOnSave": true,
13+ "workbench.tree.indent": 20,
14+ //"workbench.activityBar.location": "top",
15+ "workbench.editor.showTabs": "multiple",
16+ "window.zoomLevel": 0.0,
17+ "[markdown]": {
18+ "editor.unicodeHighlight.ambiguousCharacters": false,
19+ "editor.unicodeHighlight.invisibleCharacters": false,
20+ "diffEditor.ignoreTrimWhitespace": false,
21+ "editor.fontWeight": "normal",
22+ "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",
23+ "editor.fontSize": 14,
24+ "editor.wordWrap": "on",
25+ "editor.quickSuggestions": {
26+ "comments": "off",
27+ "strings": "off",
28+ "other": "off"
29+ }
30+ },
31+ "markdown.preview.fontSize": 14,
32+ // "workbench.editorAssociations": {
33+ // "*.md": "vscode.markdown.preview.editor"
34+ // },
35+ "markdown.marp.html": "all",
36+ "[dockerfile]": {
37+ "editor.fontSize": 14
38+ },
39+ "[dockercompose]": {
40+ "editor.fontSize": 14
41+ },
42+ "[json]": {
43+ "editor.fontSize": 14
44+ },
45+ "[yaml]": {
46+ "editor.fontSize": 14
47+ },
48+ "[go]": {
49+ "editor.fontSize": 14,
50+ "editor.defaultFormatter": "golang.go",
51+ "editor.codeActionsOnSave": {
52+ "source.organizeImports": "explicit"
53+ }
54+ },
55+ "go.lintTool": "golangci-lint",
56+ "go.lintOnSave": "package",
57+ "go.formatTool": "goimports",
58+ "go.useLanguageServer": true,
59+ "gopls": {
60+ "ui.semanticTokens": true,
61+ "ui.completion.usePlaceholders": true
62+ },
63+
64+ "workbench.colorCustomizations": {
65+ "activityBar.activeBackground": "#ffffff",
66+ "activityBar.background": "#ffffff",
67+ "activityBar.foreground": "#15202b",
68+ "activityBar.inactiveForeground": "#15202b99",
69+ "activityBarBadge.background": "#df9f9f",
70+ "activityBarBadge.foreground": "#15202b",
71+ "commandCenter.border": "#15202b99",
72+ "sash.hoverBorder": "#ffffff",
73+ "statusBar.background": "#d2fad8",
74+ "statusBar.foreground": "#15202b",
75+ "statusBarItem.hoverBackground": "#a4f5b0",
76+ "statusBarItem.remoteBackground": "#9c8cf2",
77+ "statusBarItem.remoteForeground": "#15202b",
78+ "titleBar.activeBackground": "#d2fad8",
79+ "titleBar.activeForeground": "#15202b",
80+ "titleBar.inactiveBackground": "#d2fad899",
81+ "titleBar.inactiveForeground": "#15202b99",
82+ "activityBarTop.activeBackground": "#ffffff",
83+ "activityBarTop.background": "#ffffff",
84+ "activityBarTop.foreground": "#15202b",
85+ "activityBarTop.inactiveForeground": "#15202b99",
86+ "commandCenter.foreground": "#15202b",
87+ "statusBar.debuggingBackground": "#d2fad8",
88+ "statusBar.debuggingForeground": "#15202b"
89+ }
90+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,90 @@
1+{
2+ "workbench.iconTheme": "material-icon-theme",
3+ "workbench.colorTheme": "GitHub Light Colorblind (Beta)",
4+ "editor.fontSize": 14,
5+ "terminal.integrated.fontSize": 14,
6+ "editor.insertSpaces": true,
7+ "editor.tabSize": 4,
8+ "editor.detectIndentation": true,
9+ "files.autoSave": "afterDelay",
10+ "files.autoSaveDelay": 1000,
11+ // "editor.defaultFormatter": "esbenp.prettier-vscode",
12+ "editor.formatOnSave": true,
13+ "workbench.tree.indent": 20,
14+ //"workbench.activityBar.location": "top",
15+ "workbench.editor.showTabs": "multiple",
16+ "window.zoomLevel": 0.0,
17+ "[markdown]": {
18+ "editor.unicodeHighlight.ambiguousCharacters": false,
19+ "editor.unicodeHighlight.invisibleCharacters": false,
20+ "diffEditor.ignoreTrimWhitespace": false,
21+ "editor.fontWeight": "normal",
22+ "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace",
23+ "editor.fontSize": 14,
24+ "editor.wordWrap": "on",
25+ "editor.quickSuggestions": {
26+ "comments": "off",
27+ "strings": "off",
28+ "other": "off"
29+ }
30+ },
31+ "markdown.preview.fontSize": 14,
32+ // "workbench.editorAssociations": {
33+ // "*.md": "vscode.markdown.preview.editor"
34+ // },
35+ "markdown.marp.html": "all",
36+ "[dockerfile]": {
37+ "editor.fontSize": 14
38+ },
39+ "[dockercompose]": {
40+ "editor.fontSize": 14
41+ },
42+ "[json]": {
43+ "editor.fontSize": 14
44+ },
45+ "[yaml]": {
46+ "editor.fontSize": 14
47+ },
48+ "[go]": {
49+ "editor.fontSize": 14,
50+ "editor.defaultFormatter": "golang.go",
51+ "editor.codeActionsOnSave": {
52+ "source.organizeImports": "explicit"
53+ }
54+ },
55+ "go.lintTool": "golangci-lint",
56+ "go.lintOnSave": "package",
57+ "go.formatTool": "goimports",
58+ "go.useLanguageServer": true,
59+ "gopls": {
60+ "ui.semanticTokens": true,
61+ "ui.completion.usePlaceholders": true
62+ },
63+
64+ "workbench.colorCustomizations": {
65+ "activityBar.activeBackground": "#ffffff",
66+ "activityBar.background": "#ffffff",
67+ "activityBar.foreground": "#15202b",
68+ "activityBar.inactiveForeground": "#15202b99",
69+ "activityBarBadge.background": "#df9f9f",
70+ "activityBarBadge.foreground": "#15202b",
71+ "commandCenter.border": "#15202b99",
72+ "sash.hoverBorder": "#ffffff",
73+ "statusBar.background": "#d2fad8",
74+ "statusBar.foreground": "#15202b",
75+ "statusBarItem.hoverBackground": "#a4f5b0",
76+ "statusBarItem.remoteBackground": "#9c8cf2",
77+ "statusBarItem.remoteForeground": "#15202b",
78+ "titleBar.activeBackground": "#d2fad8",
79+ "titleBar.activeForeground": "#15202b",
80+ "titleBar.inactiveBackground": "#d2fad899",
81+ "titleBar.inactiveForeground": "#15202b99",
82+ "activityBarTop.activeBackground": "#ffffff",
83+ "activityBarTop.background": "#ffffff",
84+ "activityBarTop.foreground": "#15202b",
85+ "activityBarTop.inactiveForeground": "#15202b99",
86+ "commandCenter.foreground": "#15202b",
87+ "statusBar.debuggingBackground": "#d2fad8",
88+ "statusBar.debuggingForeground": "#15202b"
89+ }
90+}
\ No newline at end of file\ No newline at end of file
added 01-release.tag.sh +115 -0
new file mode 100755
@@ -0,0 +1,115 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Releasing turbo-go is tagging it. Pushing the tag starts the Release workflow,
4+which builds the binaries and publishes the release page with them.
5+
6+1. Set TAG and ABOUT in release.env
7+2. Run this script: ./01-release.tag.sh (make check, commit, push, tag, push the tag)
8+3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts
9+ it; it cross-compiles the binaries with ./02-build-releases.sh and publishes
10+ the release page with them, using the job's own token. No personal token is
11+ needed, and nothing else has to be run by hand.
12+COMMENT
13+
14+# Without this, a failing step is ignored and the next one runs anyway. That is
15+# not theoretical: `git tag` refusing a tag that already existed was skipped in
16+# silence, and the `git push` below then pushed the OLD tag — so a release was
17+# cut from a commit nobody meant, and 02 was left to notice.
18+set -euo pipefail
19+
20+if [ ! -f release.env ]; then
21+ echo "❌ release.env is missing"
22+ echo "💡 Create it with the version you are publishing:"
23+ echo ' TAG=v1.0.0'
24+ echo ' ABOUT="Turbo Go"'
25+ exit 1
26+fi
27+
28+set -o allexport
29+# shellcheck source=/dev/null
30+source release.env
31+set +o allexport
32+
33+echo "Releasing turbo-go ${TAG}: ${ABOUT}"
34+
35+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and worse than
36+# a typo: the workflow only starts on v*, and the module proxy will not serve
37+# `go install …@TAG` for a tag it cannot read as a version.
38+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
39+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
40+ exit 1
41+fi
42+
43+# The whole suite has to pass before a version exists that people will
44+# download. A tag the proxy has cached is the wrong place to find out.
45+#
46+# The marker matters: the suite includes tests that run *this script* against a
47+# throwaway clone, and without it `make check` would run those, which would run
48+# this script, which would run `make check`. They skip themselves when they see
49+# it. Exported, so it reaches the test binary through make and go test.
50+export TURBO_GO_RELEASING=1
51+echo "→ make check"
52+make --no-print-directory check
53+
54+# tagExists reports whether TAG is already taken, here or on the remote. The
55+# remote matters on its own: a tag deleted locally after a failed attempt still
56+# exists there, and pushing a new one at a different commit is rejected.
57+tagExists() {
58+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
59+ printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")"
60+ return 0
61+ fi
62+ if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then
63+ return 1 # the remote is unreachable; the push below will say so
64+ fi
65+ if [ -n "${remote}" ]; then
66+ # No commit is named here on purpose: for an annotated tag ls-remote
67+ # gives the tag object, not the commit, and printing that as if it were
68+ # the commit sends the reader looking for a SHA they will never find.
69+ printf 'on origin\n'
70+ return 0
71+ fi
72+ return 1
73+}
74+
75+if where="$(tagExists)"; then
76+ echo "${TAG} already exists ${where}"
77+ echo "💡 A published version is not yours to move: the module proxy caches"
78+ echo " what it fetched for \`go install\`, and the release page already"
79+ echo " carries binaries with that number. Bump TAG in release.env instead."
80+ exit 1
81+fi
82+
83+# The editor must not be published with a replace directive in it: the module
84+# proxy serves the go.mod as written, and `go install …@TAG` would then look
85+# for turbo-core in a directory that does not exist on the installer's machine.
86+if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then
87+ echo "❌ go.mod has a replace directive, which a published module must not"
88+ grep -nE '^[[:space:]]*replace[[:space:]]' go.mod
89+ exit 1
90+fi
91+
92+find . -name '.DS_Store' -type f -delete
93+
94+git add .
95+
96+# Nothing to commit is not a failure — the work may already be committed — but
97+# under `set -e` a plain `git commit` would stop the release right here.
98+if git diff --cached --quiet; then
99+ echo "Nothing to commit; releasing what is already on HEAD"
100+else
101+ git commit -m "📦 ${ABOUT}"
102+fi
103+
104+git push origin "$(git rev-parse --abbrev-ref HEAD)"
105+
106+# The tag goes on after the push, so a rejected push never leaves a tag behind
107+# pointing at a commit the remote has never seen.
108+git tag -a "${TAG}" -m "${ABOUT}"
109+git push origin "${TAG}"
110+
111+echo "✅ turbo-go ${TAG} published"
112+echo "💡 The tag push started the Release workflow; it builds the binaries and"
113+echo " creates the release page. Watch it on the repository's Actions tab."
114+echo "💡 To see what it will build without publishing anything:"
115+echo " ./02-build-releases.sh ${TAG}"
new file mode 100755
@@ -0,0 +1,115 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Releasing turbo-go is tagging it. Pushing the tag starts the Release workflow,
4+which builds the binaries and publishes the release page with them.
5+
6+1. Set TAG and ABOUT in release.env
7+2. Run this script: ./01-release.tag.sh (make check, commit, push, tag, push the tag)
8+3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts
9+ it; it cross-compiles the binaries with ./02-build-releases.sh and publishes
10+ the release page with them, using the job's own token. No personal token is
11+ needed, and nothing else has to be run by hand.
12+COMMENT
13+
14+# Without this, a failing step is ignored and the next one runs anyway. That is
15+# not theoretical: `git tag` refusing a tag that already existed was skipped in
16+# silence, and the `git push` below then pushed the OLD tag — so a release was
17+# cut from a commit nobody meant, and 02 was left to notice.
18+set -euo pipefail
19+
20+if [ ! -f release.env ]; then
21+ echo "❌ release.env is missing"
22+ echo "💡 Create it with the version you are publishing:"
23+ echo ' TAG=v1.0.0'
24+ echo ' ABOUT="Turbo Go"'
25+ exit 1
26+fi
27+
28+set -o allexport
29+# shellcheck source=/dev/null
30+source release.env
31+set +o allexport
32+
33+echo "Releasing turbo-go ${TAG}: ${ABOUT}"
34+
35+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and worse than
36+# a typo: the workflow only starts on v*, and the module proxy will not serve
37+# `go install …@TAG` for a tag it cannot read as a version.
38+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
39+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
40+ exit 1
41+fi
42+
43+# The whole suite has to pass before a version exists that people will
44+# download. A tag the proxy has cached is the wrong place to find out.
45+#
46+# The marker matters: the suite includes tests that run *this script* against a
47+# throwaway clone, and without it `make check` would run those, which would run
48+# this script, which would run `make check`. They skip themselves when they see
49+# it. Exported, so it reaches the test binary through make and go test.
50+export TURBO_GO_RELEASING=1
51+echo "→ make check"
52+make --no-print-directory check
53+
54+# tagExists reports whether TAG is already taken, here or on the remote. The
55+# remote matters on its own: a tag deleted locally after a failed attempt still
56+# exists there, and pushing a new one at a different commit is rejected.
57+tagExists() {
58+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
59+ printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")"
60+ return 0
61+ fi
62+ if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then
63+ return 1 # the remote is unreachable; the push below will say so
64+ fi
65+ if [ -n "${remote}" ]; then
66+ # No commit is named here on purpose: for an annotated tag ls-remote
67+ # gives the tag object, not the commit, and printing that as if it were
68+ # the commit sends the reader looking for a SHA they will never find.
69+ printf 'on origin\n'
70+ return 0
71+ fi
72+ return 1
73+}
74+
75+if where="$(tagExists)"; then
76+ echo "${TAG} already exists ${where}"
77+ echo "💡 A published version is not yours to move: the module proxy caches"
78+ echo " what it fetched for \`go install\`, and the release page already"
79+ echo " carries binaries with that number. Bump TAG in release.env instead."
80+ exit 1
81+fi
82+
83+# The editor must not be published with a replace directive in it: the module
84+# proxy serves the go.mod as written, and `go install …@TAG` would then look
85+# for turbo-core in a directory that does not exist on the installer's machine.
86+if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then
87+ echo "❌ go.mod has a replace directive, which a published module must not"
88+ grep -nE '^[[:space:]]*replace[[:space:]]' go.mod
89+ exit 1
90+fi
91+
92+find . -name '.DS_Store' -type f -delete
93+
94+git add .
95+
96+# Nothing to commit is not a failure — the work may already be committed — but
97+# under `set -e` a plain `git commit` would stop the release right here.
98+if git diff --cached --quiet; then
99+ echo "Nothing to commit; releasing what is already on HEAD"
100+else
101+ git commit -m "📦 ${ABOUT}"
102+fi
103+
104+git push origin "$(git rev-parse --abbrev-ref HEAD)"
105+
106+# The tag goes on after the push, so a rejected push never leaves a tag behind
107+# pointing at a commit the remote has never seen.
108+git tag -a "${TAG}" -m "${ABOUT}"
109+git push origin "${TAG}"
110+
111+echo "✅ turbo-go ${TAG} published"
112+echo "💡 The tag push started the Release workflow; it builds the binaries and"
113+echo " creates the release page. Watch it on the repository's Actions tab."
114+echo "💡 To see what it will build without publishing anything:"
115+echo " ./02-build-releases.sh ${TAG}"
added 02-build-releases.sh +209 -0
new file mode 100755
@@ -0,0 +1,209 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Build the release binaries and stage them under release/${TAG}/
4+
5+Usage:
6+ ./02-build-releases.sh # TAG and ABOUT come from release.env
7+ ./02-build-releases.sh v1.0.0 # override the tag for this run (what CI does)
8+
9+This is the script the Release workflow runs when ./01-release.tag.sh pushes a
10+tag; the workflow then attaches everything staged here to the release page.
11+Nothing here publishes anything, so it is also the way to see what a release
12+will contain before cutting it, or to build the binaries by hand.
13+
14+Only the Go toolchain and git are needed. The same command works on a laptop
15+and in a Rickub CI job.
16+
17+What ends up in release/${TAG}/:
18+ turbo-go-<version>-<os>-<arch>[.exe] one binary per platform in PLATFORMS
19+ SHA256SUMS checksums of every binary
20+ README.md the downloads, how to run and verify them
21+COMMENT
22+
23+set -euo pipefail
24+
25+# release.env carries TAG ("v1.0.0") and ABOUT (the one-line description). It
26+# is git-ignored (*.env), so a CI job does not have it: there the tag comes
27+# from the command line and ABOUT from the environment, or defaults to the
28+# tag. A tag given on the command line always wins, so a test build never
29+# edits the file.
30+if [ -f release.env ]; then
31+ # shellcheck source=/dev/null
32+ source release.env
33+fi
34+TAG="${1:-${TAG:-}}"
35+ABOUT="${ABOUT:-Turbo Go ${TAG}}"
36+
37+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and for a Go
38+# module it is worse than a typo: the proxy will not serve a tag it cannot read
39+# as a version, so `go install` would fail on a release that built perfectly.
40+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
41+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
42+ exit 1
43+fi
44+
45+# 01 refuses this too, but CI runs *this* script on a tag that is already
46+# pushed, without ever running 01. The proxy serves go.mod as written, so a
47+# published editor carrying a replace tells `go install` to look for turbo-core
48+# in a directory that does not exist on the installer's machine.
49+if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then
50+ echo "❌ go.mod has a replace directive, which a published module must not"
51+ grep -nE '^[[:space:]]*replace[[:space:]]' go.mod
52+ exit 1
53+fi
54+
55+# The platforms a release is built for. Add or remove a line and everything
56+# below follows: the builds, the checksums and the README.
57+PLATFORMS=(
58+ "darwin/arm64"
59+ "linux/amd64"
60+ "linux/arm64"
61+ "windows/amd64"
62+ "windows/arm64"
63+)
64+
65+echo "🚀 Building Turbo Go ${TAG}${ABOUT}"
66+echo "🐹 $(go version)"
67+
68+RELEASES_DIR="release/${TAG}"
69+
70+# The tag is "v1.0.0"; the assets carry the bare version, "1.0.0".
71+VERSION="${TAG#v}"
72+
73+# Where the Makefile puts the binary for this machine.
74+BUILT="bin/turbo-go"
75+
76+# The release is ${TAG}, so ${TAG} is what every binary here reports. The
77+# Makefile's own default comes from `git describe`, which answers a different
78+# question — where HEAD is — and disagrees the moment you commit after tagging.
79+# Overriding VERSION keeps the -X paths defined in one place all the same.
80+LDFLAGS="$(make --no-print-directory ldflags VERSION="${TAG}")"
81+
82+# A fresh directory, so a binary left by an earlier run for a platform since
83+# removed from PLATFORMS cannot end up on the release page.
84+rm -rf "${RELEASES_DIR}"
85+mkdir -p "${RELEASES_DIR}"
86+
87+# The host build comes first: it is the quickest way to find a compile error,
88+# before spending five cross-compiles on it.
89+make build VERSION="${TAG}"
90+
91+if [ ! -f "${BUILT}" ]; then
92+ echo "❌ make build produced no ${BUILT}"
93+ exit 1
94+fi
95+
96+# assetName returns what the binary for a platform is called once staged.
97+# Windows executables carry .exe, or Windows will not run them.
98+assetName() {
99+ local goos=$1 goarch=$2
100+ local name="turbo-go-${VERSION}-${goos}-${goarch}"
101+
102+ if [ "${goos}" = "windows" ]; then
103+ name="${name}.exe"
104+ fi
105+ printf '%s\n' "${name}"
106+}
107+
108+echo ""
109+echo "🔨 Cross-compiling for ${#PLATFORMS[@]} platforms..."
110+
111+for platform in "${PLATFORMS[@]}"; do
112+ goos="${platform%/*}"
113+ goarch="${platform#*/}"
114+ asset="$(assetName "${goos}" "${goarch}")"
115+
116+ # CGO_ENABLED=0 because there is nothing to link against on the other side
117+ # of a cross-compile, and this project needs no C at all: tcell and toml
118+ # are both pure Go.
119+ #
120+ # -trimpath keeps the paths of this machine out of a binary that goes to
121+ # strangers.
122+ #
123+ # -ldflags is what makes a downloaded binary agree with the release it came
124+ # from. Without it the Go build system names the build itself, and every
125+ # asset here would report "devel" while the release page says ${TAG}.
126+ if ! CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \
127+ go build -trimpath -ldflags "${LDFLAGS}" -o "${RELEASES_DIR}/${asset}" .; then
128+ echo "${platform}"
129+ exit 1
130+ fi
131+ echo "${asset}"
132+done
133+
134+# The staged asset for this machine is the only one that can be run here, and
135+# running it is the only proof that what ships carries the version rather than
136+# that the flags looked right.
137+# The number has to *equal* the tag, not merely appear in the output: "0.2.0"
138+# is a substring of "10.2.0" and of a commit hash that happens to contain it,
139+# and a stamp that is nearly right is the failure worth catching.
140+HOST_ASSET="$(assetName "$(go env GOOS)" "$(go env GOARCH)")"
141+if [ -x "${RELEASES_DIR}/${HOST_ASSET}" ]; then
142+ if ! reported="$(scripts/check-version.sh "${RELEASES_DIR}/${HOST_ASSET}" "${TAG}")"; then
143+ exit 1
144+ fi
145+ echo "${HOST_ASSET} reports ${reported}"
146+fi
147+
148+# checksum runs whichever of the two tools this machine has: sha256sum on
149+# Linux, shasum on macOS.
150+checksum() {
151+ if command -v sha256sum >/dev/null 2>&1; then
152+ sha256sum "$@"
153+ else
154+ shasum -a 256 "$@"
155+ fi
156+}
157+
158+# The workflow attaches SHA256SUMS beside the binaries, so it is written here.
159+# One file covers every platform, which is what "sha256sum -c" expects to
160+# read, and the names carry no directory so it works next to the downloads.
161+(cd "${RELEASES_DIR}" && checksum turbo-go-"${VERSION}"-* >SHA256SUMS)
162+echo " ✅ SHA256SUMS"
163+
164+# downloadTable lists the platforms as a Markdown table, so the README grows
165+# and shrinks with PLATFORMS rather than repeating it by hand.
166+downloadTable() {
167+ printf '| Platform | Download |\n|---|---|\n'
168+ for platform in "${PLATFORMS[@]}"; do
169+ local goos="${platform%/*}" goarch="${platform#*/}"
170+ printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
171+ done
172+}
173+
174+cat >"${RELEASES_DIR}/README.md" <<EOM
175+# Turbo Go ${TAG}
176+
177+${ABOUT}
178+
179+Built with $(go env GOVERSION). No runtime dependencies; \`gopls\` is optional and
180+only completion needs it.
181+
182+$(downloadTable)
183+
184+## Running it
185+
186+ chmod +x turbo-go-${VERSION}-<platform>
187+ ./turbo-go-${VERSION}-<platform> main.go
188+
189+On macOS, an unsigned download is quarantined until you say otherwise:
190+
191+ xattr -d com.apple.quarantine turbo-go-${VERSION}-darwin-arm64
192+
193+## Installing from the module proxy instead
194+
195+ go install $(go list -m)@${TAG}
196+
197+## Verifying the download
198+
199+ sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS
200+
201+EOM
202+echo " ✅ README.md"
203+
204+echo ""
205+echo "✨ Build complete!"
206+ls -lh "${RELEASES_DIR}"
207+echo ""
208+echo "💡 Nothing was published. The Release workflow runs this same script when"
209+echo " ./01-release.tag.sh pushes ${TAG}, and attaches release/${TAG}/ to the release page."
new file mode 100755
@@ -0,0 +1,209 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Build the release binaries and stage them under release/${TAG}/
4+
5+Usage:
6+ ./02-build-releases.sh # TAG and ABOUT come from release.env
7+ ./02-build-releases.sh v1.0.0 # override the tag for this run (what CI does)
8+
9+This is the script the Release workflow runs when ./01-release.tag.sh pushes a
10+tag; the workflow then attaches everything staged here to the release page.
11+Nothing here publishes anything, so it is also the way to see what a release
12+will contain before cutting it, or to build the binaries by hand.
13+
14+Only the Go toolchain and git are needed. The same command works on a laptop
15+and in a Rickub CI job.
16+
17+What ends up in release/${TAG}/:
18+ turbo-go-<version>-<os>-<arch>[.exe] one binary per platform in PLATFORMS
19+ SHA256SUMS checksums of every binary
20+ README.md the downloads, how to run and verify them
21+COMMENT
22+
23+set -euo pipefail
24+
25+# release.env carries TAG ("v1.0.0") and ABOUT (the one-line description). It
26+# is git-ignored (*.env), so a CI job does not have it: there the tag comes
27+# from the command line and ABOUT from the environment, or defaults to the
28+# tag. A tag given on the command line always wins, so a test build never
29+# edits the file.
30+if [ -f release.env ]; then
31+ # shellcheck source=/dev/null
32+ source release.env
33+fi
34+TAG="${1:-${TAG:-}}"
35+ABOUT="${ABOUT:-Turbo Go ${TAG}}"
36+
37+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — and for a Go
38+# module it is worse than a typo: the proxy will not serve a tag it cannot read
39+# as a version, so `go install` would fail on a release that built perfectly.
40+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
41+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
42+ exit 1
43+fi
44+
45+# 01 refuses this too, but CI runs *this* script on a tag that is already
46+# pushed, without ever running 01. The proxy serves go.mod as written, so a
47+# published editor carrying a replace tells `go install` to look for turbo-core
48+# in a directory that does not exist on the installer's machine.
49+if grep -qE '^[[:space:]]*replace[[:space:]]' go.mod; then
50+ echo "❌ go.mod has a replace directive, which a published module must not"
51+ grep -nE '^[[:space:]]*replace[[:space:]]' go.mod
52+ exit 1
53+fi
54+
55+# The platforms a release is built for. Add or remove a line and everything
56+# below follows: the builds, the checksums and the README.
57+PLATFORMS=(
58+ "darwin/arm64"
59+ "linux/amd64"
60+ "linux/arm64"
61+ "windows/amd64"
62+ "windows/arm64"
63+)
64+
65+echo "🚀 Building Turbo Go ${TAG}${ABOUT}"
66+echo "🐹 $(go version)"
67+
68+RELEASES_DIR="release/${TAG}"
69+
70+# The tag is "v1.0.0"; the assets carry the bare version, "1.0.0".
71+VERSION="${TAG#v}"
72+
73+# Where the Makefile puts the binary for this machine.
74+BUILT="bin/turbo-go"
75+
76+# The release is ${TAG}, so ${TAG} is what every binary here reports. The
77+# Makefile's own default comes from `git describe`, which answers a different
78+# question — where HEAD is — and disagrees the moment you commit after tagging.
79+# Overriding VERSION keeps the -X paths defined in one place all the same.
80+LDFLAGS="$(make --no-print-directory ldflags VERSION="${TAG}")"
81+
82+# A fresh directory, so a binary left by an earlier run for a platform since
83+# removed from PLATFORMS cannot end up on the release page.
84+rm -rf "${RELEASES_DIR}"
85+mkdir -p "${RELEASES_DIR}"
86+
87+# The host build comes first: it is the quickest way to find a compile error,
88+# before spending five cross-compiles on it.
89+make build VERSION="${TAG}"
90+
91+if [ ! -f "${BUILT}" ]; then
92+ echo "❌ make build produced no ${BUILT}"
93+ exit 1
94+fi
95+
96+# assetName returns what the binary for a platform is called once staged.
97+# Windows executables carry .exe, or Windows will not run them.
98+assetName() {
99+ local goos=$1 goarch=$2
100+ local name="turbo-go-${VERSION}-${goos}-${goarch}"
101+
102+ if [ "${goos}" = "windows" ]; then
103+ name="${name}.exe"
104+ fi
105+ printf '%s\n' "${name}"
106+}
107+
108+echo ""
109+echo "🔨 Cross-compiling for ${#PLATFORMS[@]} platforms..."
110+
111+for platform in "${PLATFORMS[@]}"; do
112+ goos="${platform%/*}"
113+ goarch="${platform#*/}"
114+ asset="$(assetName "${goos}" "${goarch}")"
115+
116+ # CGO_ENABLED=0 because there is nothing to link against on the other side
117+ # of a cross-compile, and this project needs no C at all: tcell and toml
118+ # are both pure Go.
119+ #
120+ # -trimpath keeps the paths of this machine out of a binary that goes to
121+ # strangers.
122+ #
123+ # -ldflags is what makes a downloaded binary agree with the release it came
124+ # from. Without it the Go build system names the build itself, and every
125+ # asset here would report "devel" while the release page says ${TAG}.
126+ if ! CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \
127+ go build -trimpath -ldflags "${LDFLAGS}" -o "${RELEASES_DIR}/${asset}" .; then
128+ echo "${platform}"
129+ exit 1
130+ fi
131+ echo "${asset}"
132+done
133+
134+# The staged asset for this machine is the only one that can be run here, and
135+# running it is the only proof that what ships carries the version rather than
136+# that the flags looked right.
137+# The number has to *equal* the tag, not merely appear in the output: "0.2.0"
138+# is a substring of "10.2.0" and of a commit hash that happens to contain it,
139+# and a stamp that is nearly right is the failure worth catching.
140+HOST_ASSET="$(assetName "$(go env GOOS)" "$(go env GOARCH)")"
141+if [ -x "${RELEASES_DIR}/${HOST_ASSET}" ]; then
142+ if ! reported="$(scripts/check-version.sh "${RELEASES_DIR}/${HOST_ASSET}" "${TAG}")"; then
143+ exit 1
144+ fi
145+ echo "${HOST_ASSET} reports ${reported}"
146+fi
147+
148+# checksum runs whichever of the two tools this machine has: sha256sum on
149+# Linux, shasum on macOS.
150+checksum() {
151+ if command -v sha256sum >/dev/null 2>&1; then
152+ sha256sum "$@"
153+ else
154+ shasum -a 256 "$@"
155+ fi
156+}
157+
158+# The workflow attaches SHA256SUMS beside the binaries, so it is written here.
159+# One file covers every platform, which is what "sha256sum -c" expects to
160+# read, and the names carry no directory so it works next to the downloads.
161+(cd "${RELEASES_DIR}" && checksum turbo-go-"${VERSION}"-* >SHA256SUMS)
162+echo " ✅ SHA256SUMS"
163+
164+# downloadTable lists the platforms as a Markdown table, so the README grows
165+# and shrinks with PLATFORMS rather than repeating it by hand.
166+downloadTable() {
167+ printf '| Platform | Download |\n|---|---|\n'
168+ for platform in "${PLATFORMS[@]}"; do
169+ local goos="${platform%/*}" goarch="${platform#*/}"
170+ printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
171+ done
172+}
173+
174+cat >"${RELEASES_DIR}/README.md" <<EOM
175+# Turbo Go ${TAG}
176+
177+${ABOUT}
178+
179+Built with $(go env GOVERSION). No runtime dependencies; \`gopls\` is optional and
180+only completion needs it.
181+
182+$(downloadTable)
183+
184+## Running it
185+
186+ chmod +x turbo-go-${VERSION}-<platform>
187+ ./turbo-go-${VERSION}-<platform> main.go
188+
189+On macOS, an unsigned download is quarantined until you say otherwise:
190+
191+ xattr -d com.apple.quarantine turbo-go-${VERSION}-darwin-arm64
192+
193+## Installing from the module proxy instead
194+
195+ go install $(go list -m)@${TAG}
196+
197+## Verifying the download
198+
199+ sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS
200+
201+EOM
202+echo " ✅ README.md"
203+
204+echo ""
205+echo "✨ Build complete!"
206+ls -lh "${RELEASES_DIR}"
207+echo ""
208+echo "💡 Nothing was published. The Release workflow runs this same script when"
209+echo " ./01-release.tag.sh pushes ${TAG}, and attaches release/${TAG}/ to the release page."
added LICENSE +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+MIT License
2+
3+Copyright (c) 2026 turbo-editors
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6+associated documentation files (the "Software"), to deal in the Software without restriction, including
7+without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9+following conditions:
10+
11+The above copyright notice and this permission notice shall be included in all copies or substantial
12+portions of the Software.
13+
14+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15+LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16+EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18+USE OR OTHER DEALINGS IN THE SOFTWARE.
new file mode 100644
@@ -0,0 +1,18 @@
1+MIT License
2+
3+Copyright (c) 2026 turbo-editors
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6+associated documentation files (the "Software"), to deal in the Software without restriction, including
7+without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9+following conditions:
10+
11+The above copyright notice and this permission notice shall be included in all copies or substantial
12+portions of the Software.
13+
14+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15+LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16+EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18+USE OR OTHER DEALINGS IN THE SOFTWARE.
added Makefile +75 -0
new file mode 100644
@@ -0,0 +1,75 @@
1+BINARY := turbo-go
2+BUILD_DIR := bin
3+
4+# The version the binary reports is stamped in by the linker, so that a release
5+# cannot ship an About box still naming the previous one. git describe gives
6+# the last tag, how far past it this is, and the commit; a checkout with no
7+# tags, or no git at all, falls back to "devel".
8+VERSION_PKG := rickub.com/turbo-editors/turbo-core/version
9+VERSION := $(shell git describe --tags --dirty 2>/dev/null || echo devel)
10+COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
11+BUILT := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
12+LDFLAGS := -X '$(VERSION_PKG).stamp=$(VERSION)' \
13+ -X '$(VERSION_PKG).commit=$(COMMIT)' \
14+ -X '$(VERSION_PKG).built=$(BUILT)'
15+
16+.DEFAULT_GOAL := help
17+
18+## help: list the available targets
19+help:
20+ @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## //'
21+
22+## test: run the whole test suite
23+test:
24+ go test ./...
25+
26+## test-verbose: run the whole test suite, naming every test
27+test-verbose:
28+ go test -v ./...
29+
30+## cover: run the tests and report statement coverage per package
31+cover:
32+ go test -cover ./...
33+
34+## build: compile the editor into bin/turbo-go, then check it reports its version
35+build:
36+ go build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY) .
37+ @scripts/check-version.sh $(BUILD_DIR)/$(BINARY) "$(VERSION)" "$(COMMIT)"
38+
39+## version: print the version this checkout would build
40+version:
41+ @echo "$(VERSION) ($(COMMIT))"
42+
43+## ldflags: print the linker flags a stamped build uses
44+## (02-build-releases.sh reads this, so the stamp is defined once)
45+ldflags:
46+ @printf '%s\n' "$(LDFLAGS)"
47+
48+## install: build and install turbo-go where your shell can find it
49+install:
50+ @scripts/install.sh
51+
52+## uninstall: remove an installed turbo-go
53+uninstall:
54+ @scripts/install.sh --uninstall
55+
56+## run: build and start the editor (make run FILE=main.go)
57+run: build
58+ ./$(BUILD_DIR)/$(BINARY) $(FILE)
59+
60+## fmt: format every Go file in place
61+fmt:
62+ go fmt ./...
63+
64+## vet: run the standard Go static checks
65+vet:
66+ go vet ./...
67+
68+## check: format, vet and test — what to run before committing
69+check: fmt vet test
70+
71+## clean: remove build artefacts
72+clean:
73+ rm -rf $(BUILD_DIR)
74+
75+.PHONY: help test test-verbose cover build version ldflags install uninstall run fmt vet check clean
new file mode 100644
@@ -0,0 +1,75 @@
1+BINARY := turbo-go
2+BUILD_DIR := bin
3+
4+# The version the binary reports is stamped in by the linker, so that a release
5+# cannot ship an About box still naming the previous one. git describe gives
6+# the last tag, how far past it this is, and the commit; a checkout with no
7+# tags, or no git at all, falls back to "devel".
8+VERSION_PKG := rickub.com/turbo-editors/turbo-core/version
9+VERSION := $(shell git describe --tags --dirty 2>/dev/null || echo devel)
10+COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
11+BUILT := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
12+LDFLAGS := -X '$(VERSION_PKG).stamp=$(VERSION)' \
13+ -X '$(VERSION_PKG).commit=$(COMMIT)' \
14+ -X '$(VERSION_PKG).built=$(BUILT)'
15+
16+.DEFAULT_GOAL := help
17+
18+## help: list the available targets
19+help:
20+ @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## //'
21+
22+## test: run the whole test suite
23+test:
24+ go test ./...
25+
26+## test-verbose: run the whole test suite, naming every test
27+test-verbose:
28+ go test -v ./...
29+
30+## cover: run the tests and report statement coverage per package
31+cover:
32+ go test -cover ./...
33+
34+## build: compile the editor into bin/turbo-go, then check it reports its version
35+build:
36+ go build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY) .
37+ @scripts/check-version.sh $(BUILD_DIR)/$(BINARY) "$(VERSION)" "$(COMMIT)"
38+
39+## version: print the version this checkout would build
40+version:
41+ @echo "$(VERSION) ($(COMMIT))"
42+
43+## ldflags: print the linker flags a stamped build uses
44+## (02-build-releases.sh reads this, so the stamp is defined once)
45+ldflags:
46+ @printf '%s\n' "$(LDFLAGS)"
47+
48+## install: build and install turbo-go where your shell can find it
49+install:
50+ @scripts/install.sh
51+
52+## uninstall: remove an installed turbo-go
53+uninstall:
54+ @scripts/install.sh --uninstall
55+
56+## run: build and start the editor (make run FILE=main.go)
57+run: build
58+ ./$(BUILD_DIR)/$(BINARY) $(FILE)
59+
60+## fmt: format every Go file in place
61+fmt:
62+ go fmt ./...
63+
64+## vet: run the standard Go static checks
65+vet:
66+ go vet ./...
67+
68+## check: format, vet and test — what to run before committing
69+check: fmt vet test
70+
71+## clean: remove build artefacts
72+clean:
73+ rm -rf $(BUILD_DIR)
74+
75+.PHONY: help test test-verbose cover build version ldflags install uninstall run fmt vet check clean
added README.md +113 -0
new file mode 100644
@@ -0,0 +1,113 @@
1+# turbo-go
2+
3+A Turbo C-style editor for Go, written in Go.
4+
5+Built on **[turbo-core](https://rickub.com/turbo-editors/turbo-core)**, the library every Turbo editor shares. What is in this repository is the command, the profile that says this editor is for Go, and the Go scanner — about four hundred lines. Everything else lives in the library.
6+
7+A full-screen terminal IDE with the Borland furniture - a menu bar with hot keys, movable windows that cast shadows, modal dialogs, a clickable status bar — and the things a Go editor needs today: syntax colouring from the compiler's own tokeniser, loadable colour themes, completion from `gopls`, shell windows, per-project settings, a project tree, snippets, and the go toolchain a menu away.
8+
9+```
10+ File Edit Search Run Options Window Help
11+╔═[x]═════════════════════════════ greeter.go ══════════════════════════════1═[■]╗
12+║ 1 package main ▲║
13+║ 2 ▓║
14+║ 3 import "fmt" ░║
15+║ 4 ░║
16+║ 5 // Greeter says hello to whoever asks. ░║
17+║ 6 type Greeter struct { ░║
18+║ 7 Name string ░║
19+║ 8 Count int ░║
20+║ 9 } ▼║
21+║◄▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░►║
22+╚════════════════════════════════════════════════════════════════════════════════╝
23+ F1 Describe F2 Save F3 Open F6 Window F7 Next F10 Menu 1:1 LSP: ready
24+```
25+
26+## Getting started
27+
28+```bash
29+make install
30+```
31+
32+That builds the editor, puts it where your shell looks for commands, and reports what it found — the Go version, where the binary went, whether that directory is on your `PATH`, and whether `gopls` is installed. Then, from any Go project:
33+
34+```bash
35+turbo-go main.go
36+```
37+
38+To build without installing, `make build` leaves the binary in `bin/turbo-go`. From the module proxy instead of a checkout: `go install rickub.com/turbo-editors/turbo-go@latest`.
39+
40+For completion, install the Go language server as well — the editor works without it, and says so on the status bar:
41+
42+```bash
43+go install golang.org/x/tools/gopls@latest
44+```
45+
46+The [tutorial](docs/en/tutorials/getting-started.md) walks through a first session in about ten minutes.
47+
48+## Features
49+
50+- **Every build knows what it is** — `turbo-go -version` and **Help ▸ About** name the version, the commit and the build date, stamped in by the linker from `git describe` rather than read from a constant somebody forgot to bump
51+- **Turbo Vision interface** — menu bar with `Alt`-letter hot keys, overlapping movable and resizable windows, modal dialogs, mouse support throughout
52+- **Syntax colouring for nine languages** — Go through `go/scanner`, so it is exactly as right as the compiler, plus TOML, Markdown, JavaScript, HTML and shell scripts from turbo-core, each with a small scanner of its own
53+- **Themes** in TOML, eleven embedded — Borland navy, dark grey, paper white, espresso, Catppuccin Frappé and Latte, cobalt, Darcula and IntelliJ Light, and a hueless monochrome in both polarities — and any number of your own, with inheritance between files and between style keys. Every shipped theme is held to its contrast by tests
54+- **Per-project settings** in `.turbo-go/settings.toml` — pin a theme, turn on automatic saving — created from a menu item and never by itself
55+- **Completion, hover and go-to-definition** from `gopls`, entirely optional
56+- **Terminal windows** — `F8` opens a real shell in a window, with its own VT/ANSI emulator, scrollback and job control (Linux, macOS and Windows)
57+- **Project tree** — `F9` shows the project's files in a window; walk it with the arrows and press Enter to open one
58+- **Snippets** — a `Snippets` menu built from `.turbo-go/snippets.toml`, grouped into submenus and filtered by the file you are in; the chosen text is inserted at the cursor, re-indented to match
59+- **The go toolchain a menu away** — `Alt-G` runs format, vet, build, test and run from `.turbo-go/tools.toml`, each showing its output where the tool asked: a popup that fills in as it goes, a terminal window, or an editing window to search. Files the command rewrote are re-read for you, and a tool naming a `menu` of its own gets that menu on the bar
60+- **Editing** with word movement, block indent, a shared clipboard, and undo that merges a run of typing into one step
61+- **Faithful files** — line endings and the trailing newline are preserved, and saving is atomic
62+- **Automatic saving**, off by default, writing a short while after you stop typing
63+
64+## Commands
65+
66+| Command | What it does |
67+| --- | --- |
68+| `make install` | Build and install onto your `PATH` |
69+| `make build` | Compile into `bin/turbo-go` |
70+| `make test` | Run the whole test suite |
71+| `make check` | `fmt`, `vet`, then the tests — what a commit should pass |
72+| `make run FILE=x.go` | Build and start the editor on a file |
73+| `make help` | List every target |
74+
75+```bash
76+turbo-go [-theme name] [-no-lsp] [file...]
77+turbo-go -list-themes
78+```
79+
80+## Documentation
81+
82+Full documentation in **[English](docs/en/)** and **[French](docs/fr/)**, organised by the [Diátaxis](https://diataxis.fr) method:
83+
84+| | |
85+| --- | --- |
86+| **Tutorial** | [Your first file in Turbo Go](docs/en/tutorials/getting-started.md) |
87+| **How-to** | [install](docs/en/how-to/install.md) · [run the tests](docs/en/how-to/run-the-tests.md) · [enable completion](docs/en/how-to/enable-completion.md) · [write a theme](docs/en/how-to/write-a-theme.md) · [move around a file](docs/en/how-to/navigate-code.md) · [use a terminal](docs/en/how-to/use-a-terminal.md) · [configure a project](docs/en/how-to/configure-a-project.md) · [browse a project](docs/en/how-to/browse-a-project.md) · [use snippets](docs/en/how-to/use-snippets.md) · [run go commands](docs/en/how-to/run-go-commands.md) · [make a release](docs/en/how-to/make-a-release.md) |
88+| **Reference** | [command line](docs/en/reference/cli.md) · [keyboard](docs/en/reference/keyboard.md) · [menus](docs/en/reference/menus.md) · [theme format](docs/en/reference/themes.md) · [terminal windows](docs/en/reference/terminal.md) · [project settings](docs/en/reference/project-settings.md) · [project tree](docs/en/reference/project-tree.md) · [languages](docs/en/reference/languages.md) · [snippets](docs/en/reference/snippets.md) · [go tools](docs/en/reference/go-tools.md) · [the version number](docs/en/reference/versioning.md) |
89+| **Explanation** | [architecture](docs/en/explanation/architecture.md) · [design decisions](docs/en/explanation/design-decisions.md) · [colouring and completion](docs/en/explanation/colouring-and-completion.md) · [terminal windows](docs/en/explanation/terminal-windows.md) · [project settings](docs/en/explanation/project-settings.md) · [project tree](docs/en/explanation/project-tree.md) · [snippets](docs/en/explanation/snippets.md) · [go tools](docs/en/explanation/go-tools.md) |
90+
91+The library's packages each carry their own `README.md` beside the code, in [turbo-core](https://rickub.com/turbo-editors/turbo-core).
92+
93+## Where the code is
94+
95+| | |
96+| --- | --- |
97+| `main.go` | flags, the terminal, the wiring |
98+| `internal/golang` | the profile, the Go scanner, the three starter files |
99+| everything else | [turbo-core](https://rickub.com/turbo-editors/turbo-core) |
100+
101+The dependency graph is drawn in [`docs/diagrams/packages.drawio`](docs/diagrams/packages.drawio), checked against `go list`.
102+
103+## Design in one line
104+
105+Two dependencies — `tcell/v2` and `BurntSushi/toml` — and everything else from the standard library, including the tokeniser and the Language Server Protocol client. Both come through turbo-core; this repository adds none of its own. The [design decisions](docs/en/explanation/design-decisions.md) page explains why.
106+
107+## Requirements
108+
109+Go 1.26 or later. A terminal with mouse reporting, which is all of them. `gopls` is optional.
110+
111+## Licence
112+
113+See [LICENSE](LICENSE).
new file mode 100644
@@ -0,0 +1,113 @@
1+# turbo-go
2+
3+A Turbo C-style editor for Go, written in Go.
4+
5+Built on **[turbo-core](https://rickub.com/turbo-editors/turbo-core)**, the library every Turbo editor shares. What is in this repository is the command, the profile that says this editor is for Go, and the Go scanner — about four hundred lines. Everything else lives in the library.
6+
7+A full-screen terminal IDE with the Borland furniture - a menu bar with hot keys, movable windows that cast shadows, modal dialogs, a clickable status bar — and the things a Go editor needs today: syntax colouring from the compiler's own tokeniser, loadable colour themes, completion from `gopls`, shell windows, per-project settings, a project tree, snippets, and the go toolchain a menu away.
8+
9+```
10+ File Edit Search Run Options Window Help
11+╔═[x]═════════════════════════════ greeter.go ══════════════════════════════1═[■]╗
12+║ 1 package main ▲║
13+║ 2 ▓║
14+║ 3 import "fmt" ░║
15+║ 4 ░║
16+║ 5 // Greeter says hello to whoever asks. ░║
17+║ 6 type Greeter struct { ░║
18+║ 7 Name string ░║
19+║ 8 Count int ░║
20+║ 9 } ▼║
21+║◄▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░►║
22+╚════════════════════════════════════════════════════════════════════════════════╝
23+ F1 Describe F2 Save F3 Open F6 Window F7 Next F10 Menu 1:1 LSP: ready
24+```
25+
26+## Getting started
27+
28+```bash
29+make install
30+```
31+
32+That builds the editor, puts it where your shell looks for commands, and reports what it found — the Go version, where the binary went, whether that directory is on your `PATH`, and whether `gopls` is installed. Then, from any Go project:
33+
34+```bash
35+turbo-go main.go
36+```
37+
38+To build without installing, `make build` leaves the binary in `bin/turbo-go`. From the module proxy instead of a checkout: `go install rickub.com/turbo-editors/turbo-go@latest`.
39+
40+For completion, install the Go language server as well — the editor works without it, and says so on the status bar:
41+
42+```bash
43+go install golang.org/x/tools/gopls@latest
44+```
45+
46+The [tutorial](docs/en/tutorials/getting-started.md) walks through a first session in about ten minutes.
47+
48+## Features
49+
50+- **Every build knows what it is** — `turbo-go -version` and **Help ▸ About** name the version, the commit and the build date, stamped in by the linker from `git describe` rather than read from a constant somebody forgot to bump
51+- **Turbo Vision interface** — menu bar with `Alt`-letter hot keys, overlapping movable and resizable windows, modal dialogs, mouse support throughout
52+- **Syntax colouring for nine languages** — Go through `go/scanner`, so it is exactly as right as the compiler, plus TOML, Markdown, JavaScript, HTML and shell scripts from turbo-core, each with a small scanner of its own
53+- **Themes** in TOML, eleven embedded — Borland navy, dark grey, paper white, espresso, Catppuccin Frappé and Latte, cobalt, Darcula and IntelliJ Light, and a hueless monochrome in both polarities — and any number of your own, with inheritance between files and between style keys. Every shipped theme is held to its contrast by tests
54+- **Per-project settings** in `.turbo-go/settings.toml` — pin a theme, turn on automatic saving — created from a menu item and never by itself
55+- **Completion, hover and go-to-definition** from `gopls`, entirely optional
56+- **Terminal windows** — `F8` opens a real shell in a window, with its own VT/ANSI emulator, scrollback and job control (Linux, macOS and Windows)
57+- **Project tree** — `F9` shows the project's files in a window; walk it with the arrows and press Enter to open one
58+- **Snippets** — a `Snippets` menu built from `.turbo-go/snippets.toml`, grouped into submenus and filtered by the file you are in; the chosen text is inserted at the cursor, re-indented to match
59+- **The go toolchain a menu away** — `Alt-G` runs format, vet, build, test and run from `.turbo-go/tools.toml`, each showing its output where the tool asked: a popup that fills in as it goes, a terminal window, or an editing window to search. Files the command rewrote are re-read for you, and a tool naming a `menu` of its own gets that menu on the bar
60+- **Editing** with word movement, block indent, a shared clipboard, and undo that merges a run of typing into one step
61+- **Faithful files** — line endings and the trailing newline are preserved, and saving is atomic
62+- **Automatic saving**, off by default, writing a short while after you stop typing
63+
64+## Commands
65+
66+| Command | What it does |
67+| --- | --- |
68+| `make install` | Build and install onto your `PATH` |
69+| `make build` | Compile into `bin/turbo-go` |
70+| `make test` | Run the whole test suite |
71+| `make check` | `fmt`, `vet`, then the tests — what a commit should pass |
72+| `make run FILE=x.go` | Build and start the editor on a file |
73+| `make help` | List every target |
74+
75+```bash
76+turbo-go [-theme name] [-no-lsp] [file...]
77+turbo-go -list-themes
78+```
79+
80+## Documentation
81+
82+Full documentation in **[English](docs/en/)** and **[French](docs/fr/)**, organised by the [Diátaxis](https://diataxis.fr) method:
83+
84+| | |
85+| --- | --- |
86+| **Tutorial** | [Your first file in Turbo Go](docs/en/tutorials/getting-started.md) |
87+| **How-to** | [install](docs/en/how-to/install.md) · [run the tests](docs/en/how-to/run-the-tests.md) · [enable completion](docs/en/how-to/enable-completion.md) · [write a theme](docs/en/how-to/write-a-theme.md) · [move around a file](docs/en/how-to/navigate-code.md) · [use a terminal](docs/en/how-to/use-a-terminal.md) · [configure a project](docs/en/how-to/configure-a-project.md) · [browse a project](docs/en/how-to/browse-a-project.md) · [use snippets](docs/en/how-to/use-snippets.md) · [run go commands](docs/en/how-to/run-go-commands.md) · [make a release](docs/en/how-to/make-a-release.md) |
88+| **Reference** | [command line](docs/en/reference/cli.md) · [keyboard](docs/en/reference/keyboard.md) · [menus](docs/en/reference/menus.md) · [theme format](docs/en/reference/themes.md) · [terminal windows](docs/en/reference/terminal.md) · [project settings](docs/en/reference/project-settings.md) · [project tree](docs/en/reference/project-tree.md) · [languages](docs/en/reference/languages.md) · [snippets](docs/en/reference/snippets.md) · [go tools](docs/en/reference/go-tools.md) · [the version number](docs/en/reference/versioning.md) |
89+| **Explanation** | [architecture](docs/en/explanation/architecture.md) · [design decisions](docs/en/explanation/design-decisions.md) · [colouring and completion](docs/en/explanation/colouring-and-completion.md) · [terminal windows](docs/en/explanation/terminal-windows.md) · [project settings](docs/en/explanation/project-settings.md) · [project tree](docs/en/explanation/project-tree.md) · [snippets](docs/en/explanation/snippets.md) · [go tools](docs/en/explanation/go-tools.md) |
90+
91+The library's packages each carry their own `README.md` beside the code, in [turbo-core](https://rickub.com/turbo-editors/turbo-core).
92+
93+## Where the code is
94+
95+| | |
96+| --- | --- |
97+| `main.go` | flags, the terminal, the wiring |
98+| `internal/golang` | the profile, the Go scanner, the three starter files |
99+| everything else | [turbo-core](https://rickub.com/turbo-editors/turbo-core) |
100+
101+The dependency graph is drawn in [`docs/diagrams/packages.drawio`](docs/diagrams/packages.drawio), checked against `go list`.
102+
103+## Design in one line
104+
105+Two dependencies — `tcell/v2` and `BurntSushi/toml` — and everything else from the standard library, including the tokeniser and the Language Server Protocol client. Both come through turbo-core; this repository adds none of its own. The [design decisions](docs/en/explanation/design-decisions.md) page explains why.
106+
107+## Requirements
108+
109+Go 1.26 or later. A terminal with mouse reporting, which is all of them. `gopls` is optional.
110+
111+## Licence
112+
113+See [LICENSE](LICENSE).
added acp-agent/agent.yaml +29 -0
new file mode 100644
@@ -0,0 +1,29 @@
1+# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml
2+providers:
3+ llamacpp:
4+ api_type: openai_chatcompletions
5+ base_url: http://localhost:8080/v1
6+
7+models:
8+ mellum2:
9+ provider: llamacpp
10+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
11+ #max_tokens: 8192
12+ temperature: 0.7
13+ provider_opts:
14+ context_size: 262144
15+
16+agents:
17+ root:
18+ model: mellum2
19+ description: A helpful AI assistant running on a local llama.cpp server
20+ instruction: |
21+ You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks.
22+ Be helpful, accurate, and concise in your responses.
23+ You have access to the local filesystem and shell: use these tools
24+ welcome_message: |
25+ 🤖 Local Assistant propulsed by **llama.cpp** 🦙
26+
27+ toolsets:
28+ - type: filesystem
29+ - type: shell
new file mode 100644
@@ -0,0 +1,29 @@
1+# /Users/k33g/CodeBerg/turbo-editors/turbo-go/acp-agent/agent.yaml
2+providers:
3+ llamacpp:
4+ api_type: openai_chatcompletions
5+ base_url: http://localhost:8080/v1
6+
7+models:
8+ mellum2:
9+ provider: llamacpp
10+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
11+ #max_tokens: 8192
12+ temperature: 0.7
13+ provider_opts:
14+ context_size: 262144
15+
16+agents:
17+ root:
18+ model: mellum2
19+ description: A helpful AI assistant running on a local llama.cpp server
20+ instruction: |
21+ You name is Bob 🤓, you are a knowledgeable code assistant that helps users with various tasks.
22+ Be helpful, accurate, and concise in your responses.
23+ You have access to the local filesystem and shell: use these tools
24+ welcome_message: |
25+ 🤖 Local Assistant propulsed by **llama.cpp** 🦙
26+
27+ toolsets:
28+ - type: filesystem
29+ - type: shell
added acp-agent/how-to-test.md +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+cd turbo-go
2+go work init . ../turbo-core
3+make build && ./bin/turbo-go
new file mode 100644
@@ -0,0 +1,3 @@
1+cd turbo-go
2+go work init . ../turbo-core
3+make build && ./bin/turbo-go
added demo/.gitignore +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+hello
\ No newline at end of file
new file mode 100644
@@ -0,0 +1 @@
1+hello
\ No newline at end of file\ No newline at end of file
added demo/.turbo-go/settings.toml +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+# turbo-go project settings.
2+#
3+# These apply to everyone who opens this project in turbo-go. Delete this file
4+# and the editor falls back to its own defaults.
5+
6+[editor]
7+
8+# The colour theme to start in. `turbo-go -list-themes` lists them all.
9+# A -theme flag on the command line overrides this.
10+theme = "catppuccin-frappe"
11+
12+# Write modified files by themselves, a short while after you stop typing.
13+autosave = true
14+
15+# How long that while is. Any Go duration: "500ms", "2s", "1m".
16+autosave_delay = "2s"
new file mode 100644
@@ -0,0 +1,16 @@
1+# turbo-go project settings.
2+#
3+# These apply to everyone who opens this project in turbo-go. Delete this file
4+# and the editor falls back to its own defaults.
5+
6+[editor]
7+
8+# The colour theme to start in. `turbo-go -list-themes` lists them all.
9+# A -theme flag on the command line overrides this.
10+theme = "catppuccin-frappe"
11+
12+# Write modified files by themselves, a short while after you stop typing.
13+autosave = true
14+
15+# How long that while is. Any Go duration: "500ms", "2s", "1m".
16+autosave_delay = "2s"
added demo/.turbo-go/snippets.toml +60 -0
new file mode 100644
@@ -0,0 +1,60 @@
1+# turbo-go snippets.
2+#
3+# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a
4+# group appear together in a submenu of that name; one with no group goes into
5+# General. A snippet is inserted at the cursor, and every line after the
6+# first is indented to match the line you inserted it on.
7+#
8+# languages restricts a snippet to files of those kinds, by the names the
9+# editor uses: go, toml, markdown, javascript, html, bash. Leave it out and the
10+# snippet is offered everywhere.
11+#
12+# Your own snippets, shared across every project, go in:
13+# /Users/k33g/Library/Application Support/turbo-go/snippets.toml
14+
15+[[snippet]]
16+name = "if err != nil"
17+group = "Go"
18+languages = ["go"]
19+body = """
20+if err != nil {
21+\treturn err
22+}"""
23+
24+[[snippet]]
25+name = "table test"
26+group = "Go"
27+languages = ["go"]
28+body = """
29+for _, test := range tests {
30+\tt.Run(test.name, func(t *testing.T) {
31+\t})
32+}"""
33+
34+[[snippet]]
35+name = "strict mode"
36+group = "Shell"
37+languages = ["bash"]
38+body = "set -euo pipefail"
39+
40+[[snippet]]
41+name = "details block"
42+group = "Markdown"
43+languages = ["markdown"]
44+body = """
45+<details>
46+<summary></summary>
47+
48+</details>"""
49+
50+[[snippet]]
51+name = "TODO"
52+body = "TODO: "
53+
54+[[snippet]]
55+name = "Hello World"
56+body = """Hello
57+World
58+With Bob Morane
59+"""
60+
new file mode 100644
@@ -0,0 +1,60 @@
1+# turbo-go snippets.
2+#
3+# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a
4+# group appear together in a submenu of that name; one with no group goes into
5+# General. A snippet is inserted at the cursor, and every line after the
6+# first is indented to match the line you inserted it on.
7+#
8+# languages restricts a snippet to files of those kinds, by the names the
9+# editor uses: go, toml, markdown, javascript, html, bash. Leave it out and the
10+# snippet is offered everywhere.
11+#
12+# Your own snippets, shared across every project, go in:
13+# /Users/k33g/Library/Application Support/turbo-go/snippets.toml
14+
15+[[snippet]]
16+name = "if err != nil"
17+group = "Go"
18+languages = ["go"]
19+body = """
20+if err != nil {
21+\treturn err
22+}"""
23+
24+[[snippet]]
25+name = "table test"
26+group = "Go"
27+languages = ["go"]
28+body = """
29+for _, test := range tests {
30+\tt.Run(test.name, func(t *testing.T) {
31+\t})
32+}"""
33+
34+[[snippet]]
35+name = "strict mode"
36+group = "Shell"
37+languages = ["bash"]
38+body = "set -euo pipefail"
39+
40+[[snippet]]
41+name = "details block"
42+group = "Markdown"
43+languages = ["markdown"]
44+body = """
45+<details>
46+<summary></summary>
47+
48+</details>"""
49+
50+[[snippet]]
51+name = "TODO"
52+body = "TODO: "
53+
54+[[snippet]]
55+name = "Hello World"
56+body = """Hello
57+World
58+With Bob Morane
59+"""
60+
added demo/.turbo-go/tools.toml +64 -0
new file mode 100644
@@ -0,0 +1,64 @@
1+# turbo-go tools.
2+#
3+# Each [[tool]] becomes one line of the Go menu, in the order they appear here.
4+# name is what the menu shows; a letter between tildes is its hot key, and no
5+# two tools should claim the same one.
6+#
7+# command goes to "sh -c", so pipes, globs and && work: one entry can be a
8+# whole sequence.
9+#
10+# menu says which menu it appears in. Leave it out and the tool goes into the Go
11+# menu; name anything else and that menu is created for you, in the order the
12+# names first appear here. A tool that has nothing to do with Go belongs in one
13+# of your own:
14+#
15+# [[tool]]
16+# name = "~E~cho"
17+# command = "echo TADA"
18+# menu = "Tools"
19+#
20+# output says where what the command prints goes:
21+# popup a dialog that fills in as it runs, and says the exit code (default)
22+# terminal a terminal window, for anything that reads the keyboard or runs long
23+# editor an editing window once it has finished, to search with Ctrl-F
24+#
25+# Commands run in the directory the editor was started in, which is why ./...
26+# means the whole module when you start from the project root.
27+
28+[[tool]]
29+name = "~F~ormat"
30+command = "gofmt -l -w ."
31+output = "popup"
32+
33+[[tool]]
34+name = "~L~int"
35+command = "go vet ./..."
36+output = "popup"
37+
38+[[tool]]
39+name = "~B~uild"
40+command = "go build ./..."
41+output = "popup"
42+
43+[[tool]]
44+name = "~T~est"
45+command = "go test ./..."
46+output = "popup"
47+
48+[[tool]]
49+name = "~R~un"
50+command = "go run ."
51+# A terminal, not a popup: a program that reads the keyboard has to be able to
52+# be answered, and one that runs long has to be able to be interrupted.
53+output = "terminal"
54+
55+# A tool that has nothing to do with Go, in a menu of its own. This one puts a
56+# "Tools" menu on the bar, on Alt-T, between Go and Help.
57+[[tool]]
58+name = "~E~cho"
59+command = """
60+echo 'TADA'
61+echo 'QWERTY'
62+"""
63+output = "terminal"
64+menu = "Tools"
new file mode 100644
@@ -0,0 +1,64 @@
1+# turbo-go tools.
2+#
3+# Each [[tool]] becomes one line of the Go menu, in the order they appear here.
4+# name is what the menu shows; a letter between tildes is its hot key, and no
5+# two tools should claim the same one.
6+#
7+# command goes to "sh -c", so pipes, globs and && work: one entry can be a
8+# whole sequence.
9+#
10+# menu says which menu it appears in. Leave it out and the tool goes into the Go
11+# menu; name anything else and that menu is created for you, in the order the
12+# names first appear here. A tool that has nothing to do with Go belongs in one
13+# of your own:
14+#
15+# [[tool]]
16+# name = "~E~cho"
17+# command = "echo TADA"
18+# menu = "Tools"
19+#
20+# output says where what the command prints goes:
21+# popup a dialog that fills in as it runs, and says the exit code (default)
22+# terminal a terminal window, for anything that reads the keyboard or runs long
23+# editor an editing window once it has finished, to search with Ctrl-F
24+#
25+# Commands run in the directory the editor was started in, which is why ./...
26+# means the whole module when you start from the project root.
27+
28+[[tool]]
29+name = "~F~ormat"
30+command = "gofmt -l -w ."
31+output = "popup"
32+
33+[[tool]]
34+name = "~L~int"
35+command = "go vet ./..."
36+output = "popup"
37+
38+[[tool]]
39+name = "~B~uild"
40+command = "go build ./..."
41+output = "popup"
42+
43+[[tool]]
44+name = "~T~est"
45+command = "go test ./..."
46+output = "popup"
47+
48+[[tool]]
49+name = "~R~un"
50+command = "go run ."
51+# A terminal, not a popup: a program that reads the keyboard has to be able to
52+# be answered, and one that runs long has to be able to be interrupted.
53+output = "terminal"
54+
55+# A tool that has nothing to do with Go, in a menu of its own. This one puts a
56+# "Tools" menu on the bar, on Alt-T, between Go and Help.
57+[[tool]]
58+name = "~E~cho"
59+command = """
60+echo 'TADA'
61+echo 'QWERTY'
62+"""
63+output = "terminal"
64+menu = "Tools"
added demo/README.md +12 -0
new file mode 100644
@@ -0,0 +1,12 @@
1+## This a test
2+
3+**Hello**
4+
5+I'm bob
6+
7+```golang
8+// this is a remark
9+
10+```
11+
12+
new file mode 100644
@@ -0,0 +1,12 @@
1+## This a test
2+
3+**Hello**
4+
5+I'm bob
6+
7+```golang
8+// this is a remark
9+
10+```
11+
12+
added demo/go.mod +3 -0
new file mode 100644
@@ -0,0 +1,3 @@
1+module hello
2+
3+go 1.26.5
new file mode 100644
@@ -0,0 +1,3 @@
1+module hello
2+
3+go 1.26.5
added demo/index.html +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+<body id="yolo">
2+ this is a test...
3+ hello world 😄
4+</body>
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,4 @@
1+<body id="yolo">
2+ this is a test...
3+ hello world 😄
4+</body>
\ No newline at end of file\ No newline at end of file
added demo/index.js +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+// hello
2+
3+console.log("hello I'm Bob")
4+
5+function hello(name) {
6+ // this is a function
7+ return `Hello ${name}`
8+
9+}
\ No newline at end of file
new file mode 100644
@@ -0,0 +1,9 @@
1+// hello
2+
3+console.log("hello I'm Bob")
4+
5+function hello(name) {
6+ // this is a function
7+ return `Hello ${name}`
8+
9+}
\ No newline at end of file\ No newline at end of file
added demo/main.go +13 -0
new file mode 100644
@@ -0,0 +1,13 @@
1+package main
2+
3+import "fmt"
4+
5+func main() {
6+
7+ fmt.Println("hello")
8+ fmt.Println("world 😉 ")
9+ fmt.Println("I'm Bob")
10+ fmt.Println("I'm Philippe")
11+
12+}
13+
new file mode 100644
@@ -0,0 +1,13 @@
1+package main
2+
3+import "fmt"
4+
5+func main() {
6+
7+ fmt.Println("hello")
8+ fmt.Println("world 😉 ")
9+ fmt.Println("I'm Bob")
10+ fmt.Println("I'm Philippe")
11+
12+}
13+
added docs/README.md +10 -0
new file mode 100644
@@ -0,0 +1,10 @@
1+# Turbo Go — documentation
2+
3+Choose your language / Choisissez votre langue :
4+
5+- 🇬🇧 **[English](en/)**
6+- 🇫🇷 **[Français](fr/)**
7+
8+Both are complete and organised the same way, following the [Diátaxis](https://diataxis.fr) method: tutorials, how-to guides, reference, explanation.
9+
10+The [package dependency diagram](diagrams/packages.drawio) is language-neutral and shared by both.
new file mode 100644
@@ -0,0 +1,10 @@
1+# Turbo Go — documentation
2+
3+Choose your language / Choisissez votre langue :
4+
5+- 🇬🇧 **[English](en/)**
6+- 🇫🇷 **[Français](fr/)**
7+
8+Both are complete and organised the same way, following the [Diátaxis](https://diataxis.fr) method: tutorials, how-to guides, reference, explanation.
9+
10+The [package dependency diagram](diagrams/packages.drawio) is language-neutral and shared by both.
added docs/diagrams/packages.drawio +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+<mxfile host="turbo-go" modified="" agent="generated from go list -deps">
2+ <diagram name="packages" id="packages">
3+ <mxGraphModel dx="1400" dy="900" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
4+ <root>
5+ <mxCell id="0"/>
6+ <mxCell id="1" parent="0"/>
7+ <mxCell id="2" value="&lt;b&gt;main&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:10px&#x27;&gt;flags, terminal, wiring&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="0" width="190" height="54" as="geometry"/></mxCell>
8+ <mxCell id="3" value="&lt;b&gt;internal/golang&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:10px&#x27;&gt;the profile and the Go scanner&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="130" width="190" height="54" as="geometry"/></mxCell>
9+ <mxCell id="4" value="&lt;b&gt;app&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-519" y="260" width="190" height="54" as="geometry"/></mxCell>
10+ <mxCell id="5" value="&lt;b&gt;profile&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-307" y="260" width="190" height="54" as="geometry"/></mxCell>
11+ <mxCell id="6" value="&lt;b&gt;settings&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="260" width="190" height="54" as="geometry"/></mxCell>
12+ <mxCell id="7" value="&lt;b&gt;syntax&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="117" y="260" width="190" height="54" as="geometry"/></mxCell>
13+ <mxCell id="8" value="&lt;b&gt;theme&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="329" y="260" width="190" height="54" as="geometry"/></mxCell>
14+ <mxCell id="9" value="&lt;b&gt;version&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="390" width="190" height="54" as="geometry"/></mxCell>
15+ <mxCell id="10" value="tcell/v2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;dashed=1;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="520" width="190" height="54" as="geometry"/></mxCell>
16+ <mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="5"><mxGeometry relative="1" as="geometry"/></mxCell>
17+ <mxCell id="e12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="7"><mxGeometry relative="1" as="geometry"/></mxCell>
18+ <mxCell id="e13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="3"><mxGeometry relative="1" as="geometry"/></mxCell>
19+ <mxCell id="e14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="10"><mxGeometry relative="1" as="geometry"/></mxCell>
20+ <mxCell id="e15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="4"><mxGeometry relative="1" as="geometry"/></mxCell>
21+ <mxCell id="e16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="5"><mxGeometry relative="1" as="geometry"/></mxCell>
22+ <mxCell id="e17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="6"><mxGeometry relative="1" as="geometry"/></mxCell>
23+ <mxCell id="e18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="8"><mxGeometry relative="1" as="geometry"/></mxCell>
24+ <mxCell id="e19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="9"><mxGeometry relative="1" as="geometry"/></mxCell>
25+ </root>
26+ </mxGraphModel>
27+ </diagram>
28+</mxfile>
new file mode 100644
@@ -0,0 +1,28 @@
1+<mxfile host="turbo-go" modified="" agent="generated from go list -deps">
2+ <diagram name="packages" id="packages">
3+ <mxGraphModel dx="1400" dy="900" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
4+ <root>
5+ <mxCell id="0"/>
6+ <mxCell id="1" parent="0"/>
7+ <mxCell id="2" value="&lt;b&gt;main&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:10px&#x27;&gt;flags, terminal, wiring&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="0" width="190" height="54" as="geometry"/></mxCell>
8+ <mxCell id="3" value="&lt;b&gt;internal/golang&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:10px&#x27;&gt;the profile and the Go scanner&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" vertex="1" parent="1"><mxGeometry x="-95" y="130" width="190" height="54" as="geometry"/></mxCell>
9+ <mxCell id="4" value="&lt;b&gt;app&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-519" y="260" width="190" height="54" as="geometry"/></mxCell>
10+ <mxCell id="5" value="&lt;b&gt;profile&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-307" y="260" width="190" height="54" as="geometry"/></mxCell>
11+ <mxCell id="6" value="&lt;b&gt;settings&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="260" width="190" height="54" as="geometry"/></mxCell>
12+ <mxCell id="7" value="&lt;b&gt;syntax&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="117" y="260" width="190" height="54" as="geometry"/></mxCell>
13+ <mxCell id="8" value="&lt;b&gt;theme&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="329" y="260" width="190" height="54" as="geometry"/></mxCell>
14+ <mxCell id="9" value="&lt;b&gt;version&lt;/b&gt;&lt;br/&gt;&lt;font style=&#x27;font-size:9px&#x27;&gt;turbo-core&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="390" width="190" height="54" as="geometry"/></mxCell>
15+ <mxCell id="10" value="tcell/v2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;dashed=1;fontSize=11;" vertex="1" parent="1"><mxGeometry x="-95" y="520" width="190" height="54" as="geometry"/></mxCell>
16+ <mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="5"><mxGeometry relative="1" as="geometry"/></mxCell>
17+ <mxCell id="e12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="3" target="7"><mxGeometry relative="1" as="geometry"/></mxCell>
18+ <mxCell id="e13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="3"><mxGeometry relative="1" as="geometry"/></mxCell>
19+ <mxCell id="e14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="10"><mxGeometry relative="1" as="geometry"/></mxCell>
20+ <mxCell id="e15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="4"><mxGeometry relative="1" as="geometry"/></mxCell>
21+ <mxCell id="e16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="5"><mxGeometry relative="1" as="geometry"/></mxCell>
22+ <mxCell id="e17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="6"><mxGeometry relative="1" as="geometry"/></mxCell>
23+ <mxCell id="e18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="8"><mxGeometry relative="1" as="geometry"/></mxCell>
24+ <mxCell id="e19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="2" target="9"><mxGeometry relative="1" as="geometry"/></mxCell>
25+ </root>
26+ </mxGraphModel>
27+ </diagram>
28+</mxfile>
added docs/en/README.md +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+# Turbo Go — documentation
2+
3+Turbo Go is a Turbo C-style editor for Go: a full-screen terminal IDE with menus, movable windows, syntax colouring for nine languages, themes, completion from `gopls`, shell windows, per-project settings, a project tree, snippets, and the go toolchain a menu away.
4+
5+This documentation follows the [Diátaxis](https://diataxis.fr) method. Four kinds of page, four different needs — go to the one that matches what you want right now.
6+
7+| I want to… | Go to |
8+| --- | --- |
9+| **learn** the editor by using it | [Tutorials](tutorials/) |
10+| **do** something specific | [How-to guides](how-to/) |
11+| **look up** an exact detail | [Reference](reference/) |
12+| **understand** how and why it works | [Explanation](explanation/) |
13+
14+## Tutorials — learning by doing
15+
16+- [Your first file in Turbo Go](tutorials/getting-started.md) — build, open the editor, type a Go program, colour it, save it and run it.
17+
18+## How-to guides — recipes for a task
19+
20+- [How to install and build Turbo Go](how-to/install.md)
21+- [How to run the tests](how-to/run-the-tests.md)
22+- [How to enable Go completion](how-to/enable-completion.md)
23+- [How to write your own theme](how-to/write-a-theme.md)
24+- [How to move around a file](how-to/navigate-code.md)
25+- [How to ask what the code means](how-to/ask-about-code.md)
26+- [How to run shell commands without leaving the editor](how-to/use-a-terminal.md)
27+- [How to give a project its own settings](how-to/configure-a-project.md)
28+- [How to browse a project and open files from a tree](how-to/browse-a-project.md)
29+- [How to insert snippets from a menu](how-to/use-snippets.md)
30+- [How to run go commands from the editor](how-to/run-go-commands.md)
31+- [How to talk to a coding agent from the editor](how-to/talk-to-an-agent.md)
32+- [How to make a release](how-to/make-a-release.md)
33+
34+## Reference — the exact details
35+
36+- [Command line](reference/cli.md)
37+- [Keyboard](reference/keyboard.md)
38+- [Menus](reference/menus.md)
39+- [Theme file format](reference/themes.md)
40+- [Terminal windows](reference/terminal.md)
41+- [Project settings](reference/project-settings.md)
42+- [Project tree](reference/project-tree.md)
43+- [Languages coloured](reference/languages.md)
44+- [Snippets](reference/snippets.md)
45+- [Go tools](reference/go-tools.md)
46+- [Agents and ACP](reference/acp.md)
47+- [The version number](reference/versioning.md)
48+
49+## Explanation — understanding
50+
51+- [Architecture](explanation/architecture.md)
52+- [Design decisions](explanation/design-decisions.md)
53+- [Colouring and completion](explanation/colouring-and-completion.md)
54+- [Terminal windows](explanation/terminal-windows.md)
55+- [Project settings](explanation/project-settings.md)
56+- [Project tree](explanation/project-tree.md)
57+- [Snippets](explanation/snippets.md)
58+- [Go tools](explanation/go-tools.md)
59+- [Agent windows](explanation/agent-windows.md)
new file mode 100644
@@ -0,0 +1,59 @@
1+# Turbo Go — documentation
2+
3+Turbo Go is a Turbo C-style editor for Go: a full-screen terminal IDE with menus, movable windows, syntax colouring for nine languages, themes, completion from `gopls`, shell windows, per-project settings, a project tree, snippets, and the go toolchain a menu away.
4+
5+This documentation follows the [Diátaxis](https://diataxis.fr) method. Four kinds of page, four different needs — go to the one that matches what you want right now.
6+
7+| I want to… | Go to |
8+| --- | --- |
9+| **learn** the editor by using it | [Tutorials](tutorials/) |
10+| **do** something specific | [How-to guides](how-to/) |
11+| **look up** an exact detail | [Reference](reference/) |
12+| **understand** how and why it works | [Explanation](explanation/) |
13+
14+## Tutorials — learning by doing
15+
16+- [Your first file in Turbo Go](tutorials/getting-started.md) — build, open the editor, type a Go program, colour it, save it and run it.
17+
18+## How-to guides — recipes for a task
19+
20+- [How to install and build Turbo Go](how-to/install.md)
21+- [How to run the tests](how-to/run-the-tests.md)
22+- [How to enable Go completion](how-to/enable-completion.md)
23+- [How to write your own theme](how-to/write-a-theme.md)
24+- [How to move around a file](how-to/navigate-code.md)
25+- [How to ask what the code means](how-to/ask-about-code.md)
26+- [How to run shell commands without leaving the editor](how-to/use-a-terminal.md)
27+- [How to give a project its own settings](how-to/configure-a-project.md)
28+- [How to browse a project and open files from a tree](how-to/browse-a-project.md)
29+- [How to insert snippets from a menu](how-to/use-snippets.md)
30+- [How to run go commands from the editor](how-to/run-go-commands.md)
31+- [How to talk to a coding agent from the editor](how-to/talk-to-an-agent.md)
32+- [How to make a release](how-to/make-a-release.md)
33+
34+## Reference — the exact details
35+
36+- [Command line](reference/cli.md)
37+- [Keyboard](reference/keyboard.md)
38+- [Menus](reference/menus.md)
39+- [Theme file format](reference/themes.md)
40+- [Terminal windows](reference/terminal.md)
41+- [Project settings](reference/project-settings.md)
42+- [Project tree](reference/project-tree.md)
43+- [Languages coloured](reference/languages.md)
44+- [Snippets](reference/snippets.md)
45+- [Go tools](reference/go-tools.md)
46+- [Agents and ACP](reference/acp.md)
47+- [The version number](reference/versioning.md)
48+
49+## Explanation — understanding
50+
51+- [Architecture](explanation/architecture.md)
52+- [Design decisions](explanation/design-decisions.md)
53+- [Colouring and completion](explanation/colouring-and-completion.md)
54+- [Terminal windows](explanation/terminal-windows.md)
55+- [Project settings](explanation/project-settings.md)
56+- [Project tree](explanation/project-tree.md)
57+- [Snippets](explanation/snippets.md)
58+- [Go tools](explanation/go-tools.md)
59+- [Agent windows](explanation/agent-windows.md)
added docs/en/explanation/agent-windows.md +114 -0
new file mode 100644
@@ -0,0 +1,114 @@
1+# Agent windows
2+
3+This page is about why talking to an agent is shaped the way it is. For how to do it, see [How to talk to a coding agent](../how-to/talk-to-an-agent.md); for the exact keys and file format, [Agents and ACP](../reference/acp.md).
4+
5+## Why a protocol rather than a provider
6+
7+An editor that wanted to offer a chat window had two ways to get one. It could speak to model providers directly — an HTTP client per provider, a set of API keys to store, a tool-calling loop to write, and a new one of each every time somebody wants a provider the editor has never heard of. Or it could speak one protocol to whatever program the user already trusts to do that work.
8+
9+The [Agent Client Protocol](https://agentclientprotocol.com) is the second. The agent is a child process; the editor sends it prompts and draws what comes back. The editor holds no API key, knows no provider, and implements no tool-calling loop — and the same code talks to `docker agent` against a local llama.cpp, to a cloud agent, or to something you wrote this afternoon.
10+
11+It also means the editor is not the place a new model lands. Support for one is a line in *your* agent's configuration file, which is a file this editor does not read.
12+
13+## Why this lives in turbo-core
14+
15+Turbo Go is [a command, a profile and a scanner](architecture.md); everything else is the library every Turbo editor shares. An agent window is a window, a menu, a modal dialog and a turn of the event loop — all four of which belong to `turbo-core/app`. Building it here would have meant adding a general "let an editor add a window and a menu from outside" seam to the library and then using it exactly once.
16+
17+So the protocol client, the conversation model and the window are `turbo-core/acp`, beside `terminal` and `filetree`, which are the same shape. What Turbo Go contributes is the starter `acp.toml` it offers to write — the one part of this that is about Go projects. Turbo Rust and Turbo Python get agent windows by writing a starter file of their own, and nothing else.
18+
19+## Why a window, not a panel
20+
21+The same reasoning the [project tree](project-tree.md) settled. A docked panel would mean the desktop growing a notion of reserved edges, and `fitInto`, the grow modes, maximising, tiling and cascading all having to respect them — a change to the foundation of the interface for one widget. As an ordinary window an agent gets `F6`, `Alt`-digits, `[x]`, `[■]` and Tile for free.
22+
23+It also makes "several agents at once" fall out rather than being designed: two windows are two processes and two conversations, and Tile puts a fast local model beside a careful slow one. A panel would have had to grow tabs to do that.
24+
25+## Why one process per window, started when the window opens
26+
27+An agent is a conversation, and a conversation has a beginning. Starting the process with the window means the agent's working directory, its environment and its session all belong to that window, and closing it is an unambiguous end — the same bargain [terminal windows](terminal-windows.md) make, and for the same reason: what the window holds is a running process, not unsaved work, so closing it asks nothing.
28+
29+The alternative — one long-lived agent multiplexed across several windows — would have meant the editor deciding which window a `session/update` belonged to, and what to do with a window whose session had gone away while the process lived on. Two processes are cheaper than that bookkeeping.
30+
31+## Why the permission dialog is opened from the event loop, not from the message
32+
33+`session/request_permission` arrives on the connection's reading goroutine, and the answer comes from a dialog the user has to look at. The reply therefore cannot be made where the request is handled, and the dialog cannot be opened there either: everything that draws belongs to the main goroutine.
34+
35+So the request is *recorded*, and the event loop notices it on its next turn and opens the dialog. This is the fourth time this project has reached the same conclusion — [autosave](project-settings.md), the language server's re-announcement, and the terminal's redraws are the others — and the reason is always the same: `PostEvent` is allowed to drop what does not fit, so an event may cause a turn of the loop but must never be the only thing that carries a fact.
36+
37+That is why the JSON-RPC layer had to learn to answer a request *later*. It is also the whole of why `jsonrpc` was extracted out of `lsp`: a language server's questions can all be answered on the spot, and an agent's cannot.
38+
39+## Why the agent is offered the buffer rather than the file
40+
41+When the agent reads a file you have open and have not saved, it is given the text you can see, not the text on disk. The alternative is an agent that reviews the version you have just moved past, which is wrong precisely when you are most likely to be asking — you changed something and want to know about the change.
42+
43+The cost is that the agent sees text that no other tool can see, so an answer quoting a line number may not match what `go build` says. That is accepted: the same is already true of completion, which has answered from the buffer since the editor learnt to talk to `gopls`.
44+
45+Writes go the same way, into the buffer, marked modified. An agent that edits a file leaves the change in front of you, undoable with `Ctrl-Z` and unsaved until you press `F2`. An agent quietly rewriting a file under a window you have open would be the worst possible version of this feature.
46+
47+## Why the colours are the syntax classes, and not new theme keys
48+
49+The [project tree](project-tree.md) needed theme keys of its own, because it would otherwise have borrowed `list.selected`, a colour chosen against a *dialog* background, and drawn its selected row in the colour underneath it. Nothing like that is true here: an agent window's body is `window.body`, which is what the syntax classes are already chosen against and already tested against for contrast.
50+
51+So a speaker's name is drawn in the keyword style, a thought in the comment style, a tool call in the type style, and code in whatever its own scanner says. Eleven themes therefore colour agent windows correctly without being touched, and a theme somebody wrote last year does too.
52+
53+What is given up is expressiveness: a theme cannot make thoughts quiet without also making comments quiet, because they are the same key. If that turns out to matter in use, `agent.*` keys can be added later — the contrast rules and the completeness test are the cost, and they are worth paying only if somebody wants the distinction.
54+
55+## Why the transcript is a model the window merely draws
56+
57+The agent sends tokens: `"I"`, `" found"`, `" agent"`, `".yaml"`. A window that appended each one to a list of lines would be a window that could not reflow, could not tell prose from a fenced code block, and could not be tested without a live agent.
58+
59+So the conversation is a value — `acp.Transcript` — that coalesces chunks into entries, folds each `tool_call_update` onto the `tool_call` its id matches, and hands the window a list of blocks that are either prose or code-in-a-named-language. It knows nothing about a terminal, which is what lets it be tested by calling functions and comparing values, the same organising rule `buffer`, `lsp` and `syntax` follow.
60+
61+It is also what makes the drawing tests deterministic. The project has been bitten before by tests that asserted on a screen while a live process wrote to it, and that hid a real fault for a whole session; a window drawn from a fixed transcript cannot race anything.
62+
63+## What was deliberately left out
64+
65+- **Session resume.** `session/load` exists, and using it would mean deciding where conversations are stored, how long they are kept, and what happens when the project moved. That is a feature in its own right.
66+- **Authentication.** An agent that needs a login is told to log in with its own CLI. Storing a credential is a responsibility this editor has so far avoided entirely, and one protocol method is not a good reason to start.
67+- **The terminal capability.** An agent can already have a shell through its own toolsets, as `docker agent` does. Advertising `terminal` would mean the editor running commands on the agent's behalf and owning the output — the tools menu already does that, better, for commands *you* chose.
68+- **Images in prompts.** The editor has text and files to send, and a terminal to draw in.
69+
70+## See also
71+
72+- [Architecture](architecture.md) — what is here and what is in the library
73+- [Terminal windows](terminal-windows.md) — the other window holding a live process
74+- [Project tree](project-tree.md) — where the window-not-a-panel argument was first made
75+
76+## Why copying goes to two clipboards
77+
78+"Copy this so I can use it elsewhere" usually means *elsewhere entirely* — another window, a browser, a message to a colleague. A clipboard that only worked inside this editor would answer the smaller half of the request, and the half you were least likely to be asking about.
79+
80+So a copy goes to both: the editor's own, which `Shift-Ins` pastes from, and the system's, reached by asking the terminal through OSC 52. Nothing verifies the second, because there is nothing to verify — the sequence has no reply, a terminal may refuse it for security, and some need it turned on. A message promising something that did not happen would be worse than one that stays quiet, so the status bar says only how many lines were copied, which is true either way.
81+
82+## Why copying with nothing selected copies a whole block
83+
84+The thing somebody wants out of a conversation is almost always a code block. Making them select it first — six keystrokes, or a drag they have to aim — is work the editor already has the information to do for them: it laid the conversation out, so it knows exactly where that block starts and ends.
85+
86+So the lines carry a **region**: one fenced code block, one passage of prose, one tool call's output. With nothing selected, `Ctrl-C` copies the region the cursor is on. A speaker's label and a tool call's heading are furniture and get regions of their own, which is what keeps `‣ Bob (llama.cpp)` out of a block pasted into a source file.
87+
88+That last part was not designed; it was found. The first version copied the label along with the code, and it was caught by copying from the real binary and reading the OSC 52 payload back off the wire.
89+
90+## Why the spinner is drawn from the clock
91+
92+An agent thinking for twenty seconds sends nothing at all, and a window that looked frozen would be indistinguishable from one that was. The spinner is the cheapest possible answer to "is this still working?".
93+
94+It is a function of the time — `Spinner(now)` — rather than a counter something increments. Nothing has to be reset when a turn begins, two windows thinking at once turn in step, and a test can assert on a frame without waiting for one, which is the same reason `editor.View` and `app.App` both take an injectable clock.
95+
96+Drawing from the clock means something else has to *cause* the redraw, so a session running a turn wakes the event loop at the spinner's own rate. That is allowed to be a ticker precisely because a dropped tick cannot strand anything: it asks for a turn of the loop and never carries a fact — the rule this project has now reached five times.
97+
98+The window's **title** deliberately does not animate. It is also what the window list and the `Alt`-digit menu show, and a name that changed eight times a second would make both of them flicker for no gain.
99+
100+## Why commands are a popup in the box, and not a menu
101+
102+An agent's commands arrive over the wire as a list — `available_commands_update` — and may change during the session. A menu built from them would have to be rebuilt on every update, would sit far from where the command is typed, and would still have to end by putting `/web ` into the box, because that is the only thing the protocol lets a client send: a command is a text prompt the agent recognises by its first word.
103+
104+So the list opens where the text is, on the character that starts a command, and closes when the word is complete. It is the same shape as the completion popup over a file, for the same reason: what you are choosing is what you are typing. Using `/` and `@` rather than keys of the editor's own is deliberate — they are the characters Zed uses, so an agent's own documentation is true here without a translation table.
105+
106+`Enter` has two meanings on the list, ordered by how finished the word is: it completes an unfinished one, and sends a finished one. The alternative — `Enter` always completes, a second `Enter` sends — costs a keystroke on every command and gains nothing, because a word that already reads exactly as a command has nothing left to complete.
107+
108+## Why a mention carries the file, when it can
109+
110+The protocol offers two ways to name a file in a prompt: a `resource_link`, which is a URI the agent fetches for itself, and an embedded `resource`, which is the URI *and the text*. The specification calls the second "the preferred way to include context", and the reason is the same one that makes `fs/read_text_file` answer from the buffer: the editor knows things about the file that the disk does not. An agent following a link to a file you have edited and not saved reads the version you have just moved past, which is wrong precisely when you are most likely to be asking.
111+
112+So the editor sends the text when the agent declared `promptCapabilities.embeddedContext`, read through the same path `fs/read_text_file` uses, and a link otherwise — never nothing. A file that cannot be read goes as a link too, so the agent is at least told which file was meant.
113+
114+The mention replaces the name in the text rather than travelling beside it. Sending `explain @main.go` as the text *and* an attachment would give the agent the name twice and leave it to match them; putting the block where the name was gives it the file where the sentence needs it. The conversation, on the other hand, keeps the line as typed: that is what you said, and the window is a record of the conversation, not of the wire.
new file mode 100644
@@ -0,0 +1,114 @@
1+# Agent windows
2+
3+This page is about why talking to an agent is shaped the way it is. For how to do it, see [How to talk to a coding agent](../how-to/talk-to-an-agent.md); for the exact keys and file format, [Agents and ACP](../reference/acp.md).
4+
5+## Why a protocol rather than a provider
6+
7+An editor that wanted to offer a chat window had two ways to get one. It could speak to model providers directly — an HTTP client per provider, a set of API keys to store, a tool-calling loop to write, and a new one of each every time somebody wants a provider the editor has never heard of. Or it could speak one protocol to whatever program the user already trusts to do that work.
8+
9+The [Agent Client Protocol](https://agentclientprotocol.com) is the second. The agent is a child process; the editor sends it prompts and draws what comes back. The editor holds no API key, knows no provider, and implements no tool-calling loop — and the same code talks to `docker agent` against a local llama.cpp, to a cloud agent, or to something you wrote this afternoon.
10+
11+It also means the editor is not the place a new model lands. Support for one is a line in *your* agent's configuration file, which is a file this editor does not read.
12+
13+## Why this lives in turbo-core
14+
15+Turbo Go is [a command, a profile and a scanner](architecture.md); everything else is the library every Turbo editor shares. An agent window is a window, a menu, a modal dialog and a turn of the event loop — all four of which belong to `turbo-core/app`. Building it here would have meant adding a general "let an editor add a window and a menu from outside" seam to the library and then using it exactly once.
16+
17+So the protocol client, the conversation model and the window are `turbo-core/acp`, beside `terminal` and `filetree`, which are the same shape. What Turbo Go contributes is the starter `acp.toml` it offers to write — the one part of this that is about Go projects. Turbo Rust and Turbo Python get agent windows by writing a starter file of their own, and nothing else.
18+
19+## Why a window, not a panel
20+
21+The same reasoning the [project tree](project-tree.md) settled. A docked panel would mean the desktop growing a notion of reserved edges, and `fitInto`, the grow modes, maximising, tiling and cascading all having to respect them — a change to the foundation of the interface for one widget. As an ordinary window an agent gets `F6`, `Alt`-digits, `[x]`, `[■]` and Tile for free.
22+
23+It also makes "several agents at once" fall out rather than being designed: two windows are two processes and two conversations, and Tile puts a fast local model beside a careful slow one. A panel would have had to grow tabs to do that.
24+
25+## Why one process per window, started when the window opens
26+
27+An agent is a conversation, and a conversation has a beginning. Starting the process with the window means the agent's working directory, its environment and its session all belong to that window, and closing it is an unambiguous end — the same bargain [terminal windows](terminal-windows.md) make, and for the same reason: what the window holds is a running process, not unsaved work, so closing it asks nothing.
28+
29+The alternative — one long-lived agent multiplexed across several windows — would have meant the editor deciding which window a `session/update` belonged to, and what to do with a window whose session had gone away while the process lived on. Two processes are cheaper than that bookkeeping.
30+
31+## Why the permission dialog is opened from the event loop, not from the message
32+
33+`session/request_permission` arrives on the connection's reading goroutine, and the answer comes from a dialog the user has to look at. The reply therefore cannot be made where the request is handled, and the dialog cannot be opened there either: everything that draws belongs to the main goroutine.
34+
35+So the request is *recorded*, and the event loop notices it on its next turn and opens the dialog. This is the fourth time this project has reached the same conclusion — [autosave](project-settings.md), the language server's re-announcement, and the terminal's redraws are the others — and the reason is always the same: `PostEvent` is allowed to drop what does not fit, so an event may cause a turn of the loop but must never be the only thing that carries a fact.
36+
37+That is why the JSON-RPC layer had to learn to answer a request *later*. It is also the whole of why `jsonrpc` was extracted out of `lsp`: a language server's questions can all be answered on the spot, and an agent's cannot.
38+
39+## Why the agent is offered the buffer rather than the file
40+
41+When the agent reads a file you have open and have not saved, it is given the text you can see, not the text on disk. The alternative is an agent that reviews the version you have just moved past, which is wrong precisely when you are most likely to be asking — you changed something and want to know about the change.
42+
43+The cost is that the agent sees text that no other tool can see, so an answer quoting a line number may not match what `go build` says. That is accepted: the same is already true of completion, which has answered from the buffer since the editor learnt to talk to `gopls`.
44+
45+Writes go the same way, into the buffer, marked modified. An agent that edits a file leaves the change in front of you, undoable with `Ctrl-Z` and unsaved until you press `F2`. An agent quietly rewriting a file under a window you have open would be the worst possible version of this feature.
46+
47+## Why the colours are the syntax classes, and not new theme keys
48+
49+The [project tree](project-tree.md) needed theme keys of its own, because it would otherwise have borrowed `list.selected`, a colour chosen against a *dialog* background, and drawn its selected row in the colour underneath it. Nothing like that is true here: an agent window's body is `window.body`, which is what the syntax classes are already chosen against and already tested against for contrast.
50+
51+So a speaker's name is drawn in the keyword style, a thought in the comment style, a tool call in the type style, and code in whatever its own scanner says. Eleven themes therefore colour agent windows correctly without being touched, and a theme somebody wrote last year does too.
52+
53+What is given up is expressiveness: a theme cannot make thoughts quiet without also making comments quiet, because they are the same key. If that turns out to matter in use, `agent.*` keys can be added later — the contrast rules and the completeness test are the cost, and they are worth paying only if somebody wants the distinction.
54+
55+## Why the transcript is a model the window merely draws
56+
57+The agent sends tokens: `"I"`, `" found"`, `" agent"`, `".yaml"`. A window that appended each one to a list of lines would be a window that could not reflow, could not tell prose from a fenced code block, and could not be tested without a live agent.
58+
59+So the conversation is a value — `acp.Transcript` — that coalesces chunks into entries, folds each `tool_call_update` onto the `tool_call` its id matches, and hands the window a list of blocks that are either prose or code-in-a-named-language. It knows nothing about a terminal, which is what lets it be tested by calling functions and comparing values, the same organising rule `buffer`, `lsp` and `syntax` follow.
60+
61+It is also what makes the drawing tests deterministic. The project has been bitten before by tests that asserted on a screen while a live process wrote to it, and that hid a real fault for a whole session; a window drawn from a fixed transcript cannot race anything.
62+
63+## What was deliberately left out
64+
65+- **Session resume.** `session/load` exists, and using it would mean deciding where conversations are stored, how long they are kept, and what happens when the project moved. That is a feature in its own right.
66+- **Authentication.** An agent that needs a login is told to log in with its own CLI. Storing a credential is a responsibility this editor has so far avoided entirely, and one protocol method is not a good reason to start.
67+- **The terminal capability.** An agent can already have a shell through its own toolsets, as `docker agent` does. Advertising `terminal` would mean the editor running commands on the agent's behalf and owning the output — the tools menu already does that, better, for commands *you* chose.
68+- **Images in prompts.** The editor has text and files to send, and a terminal to draw in.
69+
70+## See also
71+
72+- [Architecture](architecture.md) — what is here and what is in the library
73+- [Terminal windows](terminal-windows.md) — the other window holding a live process
74+- [Project tree](project-tree.md) — where the window-not-a-panel argument was first made
75+
76+## Why copying goes to two clipboards
77+
78+"Copy this so I can use it elsewhere" usually means *elsewhere entirely* — another window, a browser, a message to a colleague. A clipboard that only worked inside this editor would answer the smaller half of the request, and the half you were least likely to be asking about.
79+
80+So a copy goes to both: the editor's own, which `Shift-Ins` pastes from, and the system's, reached by asking the terminal through OSC 52. Nothing verifies the second, because there is nothing to verify — the sequence has no reply, a terminal may refuse it for security, and some need it turned on. A message promising something that did not happen would be worse than one that stays quiet, so the status bar says only how many lines were copied, which is true either way.
81+
82+## Why copying with nothing selected copies a whole block
83+
84+The thing somebody wants out of a conversation is almost always a code block. Making them select it first — six keystrokes, or a drag they have to aim — is work the editor already has the information to do for them: it laid the conversation out, so it knows exactly where that block starts and ends.
85+
86+So the lines carry a **region**: one fenced code block, one passage of prose, one tool call's output. With nothing selected, `Ctrl-C` copies the region the cursor is on. A speaker's label and a tool call's heading are furniture and get regions of their own, which is what keeps `‣ Bob (llama.cpp)` out of a block pasted into a source file.
87+
88+That last part was not designed; it was found. The first version copied the label along with the code, and it was caught by copying from the real binary and reading the OSC 52 payload back off the wire.
89+
90+## Why the spinner is drawn from the clock
91+
92+An agent thinking for twenty seconds sends nothing at all, and a window that looked frozen would be indistinguishable from one that was. The spinner is the cheapest possible answer to "is this still working?".
93+
94+It is a function of the time — `Spinner(now)` — rather than a counter something increments. Nothing has to be reset when a turn begins, two windows thinking at once turn in step, and a test can assert on a frame without waiting for one, which is the same reason `editor.View` and `app.App` both take an injectable clock.
95+
96+Drawing from the clock means something else has to *cause* the redraw, so a session running a turn wakes the event loop at the spinner's own rate. That is allowed to be a ticker precisely because a dropped tick cannot strand anything: it asks for a turn of the loop and never carries a fact — the rule this project has now reached five times.
97+
98+The window's **title** deliberately does not animate. It is also what the window list and the `Alt`-digit menu show, and a name that changed eight times a second would make both of them flicker for no gain.
99+
100+## Why commands are a popup in the box, and not a menu
101+
102+An agent's commands arrive over the wire as a list — `available_commands_update` — and may change during the session. A menu built from them would have to be rebuilt on every update, would sit far from where the command is typed, and would still have to end by putting `/web ` into the box, because that is the only thing the protocol lets a client send: a command is a text prompt the agent recognises by its first word.
103+
104+So the list opens where the text is, on the character that starts a command, and closes when the word is complete. It is the same shape as the completion popup over a file, for the same reason: what you are choosing is what you are typing. Using `/` and `@` rather than keys of the editor's own is deliberate — they are the characters Zed uses, so an agent's own documentation is true here without a translation table.
105+
106+`Enter` has two meanings on the list, ordered by how finished the word is: it completes an unfinished one, and sends a finished one. The alternative — `Enter` always completes, a second `Enter` sends — costs a keystroke on every command and gains nothing, because a word that already reads exactly as a command has nothing left to complete.
107+
108+## Why a mention carries the file, when it can
109+
110+The protocol offers two ways to name a file in a prompt: a `resource_link`, which is a URI the agent fetches for itself, and an embedded `resource`, which is the URI *and the text*. The specification calls the second "the preferred way to include context", and the reason is the same one that makes `fs/read_text_file` answer from the buffer: the editor knows things about the file that the disk does not. An agent following a link to a file you have edited and not saved reads the version you have just moved past, which is wrong precisely when you are most likely to be asking.
111+
112+So the editor sends the text when the agent declared `promptCapabilities.embeddedContext`, read through the same path `fs/read_text_file` uses, and a link otherwise — never nothing. A file that cannot be read goes as a link too, so the agent is at least told which file was meant.
113+
114+The mention replaces the name in the text rather than travelling beside it. Sending `explain @main.go` as the text *and* an attachment would give the agent the name twice and leave it to match them; putting the block where the name was gives it the file where the sentence needs it. The conversation, on the other hand, keeps the line as typed: that is what you said, and the window is a record of the conversation, not of the wire.
added docs/en/explanation/architecture.md +85 -0
new file mode 100644
@@ -0,0 +1,85 @@
1+# Architecture — explanation
2+
3+## What is this about?
4+
5+Turbo Go is a command, a profile and a scanner. Everything else — the editing widget, the windows, the menus, the dialogs, the themes, the terminal emulator, the file tree, the LSP client — is [turbo-core](https://rickub.com/turbo-editors/turbo-core), the library every Turbo editor is built on.
6+
7+That was not always true. Turbo Go was a single program of about eleven and a half thousand lines in fourteen packages until a second editor was wanted, at which point the fourteen moved into a library and one stayed here. This page is about the split that resulted.
8+
9+## What is in this repository
10+
11+```
12+main.go flags, the terminal, and the wiring
13+internal/golang the whole of what makes this Turbo Go
14+ golang.go the profile: name, menu, server, root marker
15+ scan.go the Go scanner, on top of go/scanner
16+ templates.go three //go:embed declarations
17+ *.toml.tmpl the three starter files a project gets, embedded
18+```
19+
20+About four hundred lines. There is no `internal/app`, no `internal/ui`, no `internal/buffer` — those exist once, in the library, and every editor built on it uses them unchanged.
21+
22+## What `main` does
23+
24+Six things, in this order:
25+
26+1. Parses the flags.
27+2. Calls `golang.Register()`, which teaches the library to colour `.go` files.
28+3. Builds `golang.Profile()` — the value that says this editor is Turbo Go.
29+4. Reads `.turbo-go/settings.toml` from the working directory, if there is one.
30+5. Opens the terminal and hands the screen, the theme name and the profile to `app.New`.
31+6. Starts gopls in the module root, and runs the event loop.
32+
33+That is the whole command. Every decision it makes — which theme wins, which files to open, whether to start a language server — is about *this run*, not about Go.
34+
35+## The profile is the seam
36+
37+```go
38+profile.Profile{
39+ Name: "Turbo Go",
40+ Slug: "turbo-go",
41+ Language: "Go",
42+ ToolsMenu: "~G~o",
43+ RootMarkers: []string{"go.mod"},
44+ Server: profile.Server{Command: "gopls", Args: []string{"serve"}, },
45+ Templates: profile.Templates{Settings: , Snippets: , Tools: },
46+}
47+```
48+
49+Everything that used to be a hardcoded `"turbo-go"`, `"gopls"` or `"go.mod"` somewhere in eleven thousand lines is one field here. The library reads them; nothing in the library knows what any of them mean.
50+
51+`Slug` carries more than it looks. The binary is `turbo-go`, the project directory is `.turbo-go`, the user's own configuration lives in `~/.config/turbo-go`, and the environment variables that override it are `TURBO_GO_THEME_DIR` and `TURBO_GO_SNIPPET_DIR` — all derived from that one word. Those names are unchanged by the refactoring, deliberately: somebody who set `TURBO_GO_THEME_DIR` did so against a released binary.
52+
53+## Why the Go scanner is here and not in the library
54+
55+turbo-core colours eight languages itself: TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell. Those are the ones every editor meets whatever it is for — a project's configuration is TOML or YAML, its documentation is Markdown, its scripts are shell, its image build a Dockerfile.
56+
57+Go is not one of them. The language that *defines* an editor is registered by that editor, which is why a `.rs` file opens as plain text here and a `.go` file opens as plain text in Turbo Rust.
58+
59+The Go scanner is also the one that is *least* like the others. Every language in the library is scanned a line at a time with `syntax.LineScanner`; Go goes through `go/scanner`, the lexer the Go toolchain itself uses, and converts its byte offsets with `syntax.LineIndex`. That the library supports both shapes is because of this scanner.
60+
61+## What moved, and what did not
62+
63+| Was | Is |
64+| --- | --- |
65+| `internal/buffer`, `internal/ui`, `internal/editor`, … | `turbo-core/buffer`, `turbo-core/ui`, `turbo-core/editor`, … |
66+| `internal/syntax` — six languages | `turbo-core/syntax` — eight, plus a registry; Go lives here |
67+| `internal/app` with a `Name` constant | `turbo-core/app` taking a `profile.Profile` |
68+| `internal/lsp` hardcoding gopls | `turbo-core/lsp` taking a `profile.Server` |
69+| `moduleRoot` in `main.go` | `app.ProjectRoot(p, files)`, with `go.mod` in the profile |
70+| `settings.DirName = ".turbo-go"` | `p.ProjectDir()` |
71+
72+**Nothing about the editor's behaviour changed.** The menus, the keys, the themes, the file formats and the environment variables are what they were. What changed is where the code lives.
73+
74+## Why a library rather than a fork
75+
76+The alternative to extracting turbo-core was copying Turbo Go and changing the Go bits. It was rejected before it was started: two copies of eleven thousand lines drift within a month, and every fix has to be made twice by somebody who remembers there are two.
77+
78+The cost, accepted: a change to a menu now affects every editor at once, and Turbo Go can no longer make a decision that suits only Go without either putting it in the profile or arguing for it in the library. That is a real constraint, and it is the one that keeps the editors the same editor.
79+
80+## How it relates to the rest
81+
82+- What each of the library's packages does: [turbo-core's package reference](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/en/reference/packages.md)
83+- How the colouring works here: [Colouring and completion](colouring-and-completion.md)
84+- Why the tools menu is data: [Go tools](go-tools.md)
85+- The decisions that outlived the refactoring: [Design decisions](design-decisions.md)
new file mode 100644
@@ -0,0 +1,85 @@
1+# Architecture — explanation
2+
3+## What is this about?
4+
5+Turbo Go is a command, a profile and a scanner. Everything else — the editing widget, the windows, the menus, the dialogs, the themes, the terminal emulator, the file tree, the LSP client — is [turbo-core](https://rickub.com/turbo-editors/turbo-core), the library every Turbo editor is built on.
6+
7+That was not always true. Turbo Go was a single program of about eleven and a half thousand lines in fourteen packages until a second editor was wanted, at which point the fourteen moved into a library and one stayed here. This page is about the split that resulted.
8+
9+## What is in this repository
10+
11+```
12+main.go flags, the terminal, and the wiring
13+internal/golang the whole of what makes this Turbo Go
14+ golang.go the profile: name, menu, server, root marker
15+ scan.go the Go scanner, on top of go/scanner
16+ templates.go three //go:embed declarations
17+ *.toml.tmpl the three starter files a project gets, embedded
18+```
19+
20+About four hundred lines. There is no `internal/app`, no `internal/ui`, no `internal/buffer` — those exist once, in the library, and every editor built on it uses them unchanged.
21+
22+## What `main` does
23+
24+Six things, in this order:
25+
26+1. Parses the flags.
27+2. Calls `golang.Register()`, which teaches the library to colour `.go` files.
28+3. Builds `golang.Profile()` — the value that says this editor is Turbo Go.
29+4. Reads `.turbo-go/settings.toml` from the working directory, if there is one.
30+5. Opens the terminal and hands the screen, the theme name and the profile to `app.New`.
31+6. Starts gopls in the module root, and runs the event loop.
32+
33+That is the whole command. Every decision it makes — which theme wins, which files to open, whether to start a language server — is about *this run*, not about Go.
34+
35+## The profile is the seam
36+
37+```go
38+profile.Profile{
39+ Name: "Turbo Go",
40+ Slug: "turbo-go",
41+ Language: "Go",
42+ ToolsMenu: "~G~o",
43+ RootMarkers: []string{"go.mod"},
44+ Server: profile.Server{Command: "gopls", Args: []string{"serve"}, },
45+ Templates: profile.Templates{Settings: , Snippets: , Tools: },
46+}
47+```
48+
49+Everything that used to be a hardcoded `"turbo-go"`, `"gopls"` or `"go.mod"` somewhere in eleven thousand lines is one field here. The library reads them; nothing in the library knows what any of them mean.
50+
51+`Slug` carries more than it looks. The binary is `turbo-go`, the project directory is `.turbo-go`, the user's own configuration lives in `~/.config/turbo-go`, and the environment variables that override it are `TURBO_GO_THEME_DIR` and `TURBO_GO_SNIPPET_DIR` — all derived from that one word. Those names are unchanged by the refactoring, deliberately: somebody who set `TURBO_GO_THEME_DIR` did so against a released binary.
52+
53+## Why the Go scanner is here and not in the library
54+
55+turbo-core colours eight languages itself: TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell. Those are the ones every editor meets whatever it is for — a project's configuration is TOML or YAML, its documentation is Markdown, its scripts are shell, its image build a Dockerfile.
56+
57+Go is not one of them. The language that *defines* an editor is registered by that editor, which is why a `.rs` file opens as plain text here and a `.go` file opens as plain text in Turbo Rust.
58+
59+The Go scanner is also the one that is *least* like the others. Every language in the library is scanned a line at a time with `syntax.LineScanner`; Go goes through `go/scanner`, the lexer the Go toolchain itself uses, and converts its byte offsets with `syntax.LineIndex`. That the library supports both shapes is because of this scanner.
60+
61+## What moved, and what did not
62+
63+| Was | Is |
64+| --- | --- |
65+| `internal/buffer`, `internal/ui`, `internal/editor`, … | `turbo-core/buffer`, `turbo-core/ui`, `turbo-core/editor`, … |
66+| `internal/syntax` — six languages | `turbo-core/syntax` — eight, plus a registry; Go lives here |
67+| `internal/app` with a `Name` constant | `turbo-core/app` taking a `profile.Profile` |
68+| `internal/lsp` hardcoding gopls | `turbo-core/lsp` taking a `profile.Server` |
69+| `moduleRoot` in `main.go` | `app.ProjectRoot(p, files)`, with `go.mod` in the profile |
70+| `settings.DirName = ".turbo-go"` | `p.ProjectDir()` |
71+
72+**Nothing about the editor's behaviour changed.** The menus, the keys, the themes, the file formats and the environment variables are what they were. What changed is where the code lives.
73+
74+## Why a library rather than a fork
75+
76+The alternative to extracting turbo-core was copying Turbo Go and changing the Go bits. It was rejected before it was started: two copies of eleven thousand lines drift within a month, and every fix has to be made twice by somebody who remembers there are two.
77+
78+The cost, accepted: a change to a menu now affects every editor at once, and Turbo Go can no longer make a decision that suits only Go without either putting it in the profile or arguing for it in the library. That is a real constraint, and it is the one that keeps the editors the same editor.
79+
80+## How it relates to the rest
81+
82+- What each of the library's packages does: [turbo-core's package reference](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/en/reference/packages.md)
83+- How the colouring works here: [Colouring and completion](colouring-and-completion.md)
84+- Why the tools menu is data: [Go tools](go-tools.md)
85+- The decisions that outlived the refactoring: [Design decisions](design-decisions.md)
added docs/en/explanation/colouring-and-completion.md +122 -0
new file mode 100644
@@ -0,0 +1,122 @@
1+# Colouring and completion — explanation
2+
3+## What is this about?
4+
5+The two features that make Turbo Go an editor *for Go* rather than a text editor that happens to be written in it: syntax colouring, and completion from a language server. They work quite differently, and the difference is instructive.
6+
7+## Colouring: the compiler's own tokeniser
8+
9+Turbo Go does not have a syntax definition. It calls `go/scanner` — the lexer the Go toolchain itself uses — and turns the tokens it gets back into coloured spans.
10+
11+This means keywords, literals and operators are recognised **exactly** as the compiler recognises them. Raw string literals, automatic semicolon insertion, `0x_FF` separators, the lot. There is no regular expression to get subtly wrong, and no table to update when the language changes.
12+
13+### Tolerance is the whole point
14+
15+Source under a cursor is syntactically broken most of the time it is being typed. Half a string, an unclosed brace, an identifier that stops mid-word. A highlighter that gives up on invalid input is a highlighter that flickers off exactly when you are looking at it.
16+
17+So the scanner runs in its most forgiving mode and **every syntax error is discarded**. It still returns usable tokens: an unterminated string comes back as a string running to the end of the line, an unclosed `/*` as a comment running to the end of the file. Which is precisely the behaviour you want — the colours stay steady, and they tell you what is wrong.
18+
19+### What the editor adds
20+
21+Three distinctions the scanner does not make, because they are about reading rather than parsing:
22+
23+- an identifier before `(`, or after `func`, is a **function**
24+- an identifier after `type`, `struct` or `interface` is a **type**
25+- brackets, commas, dots and semicolons are **punctuation**, split out from the operators that compute, so a theme can quiet them down
26+
27+Predeclared names — `int`, `error`, `nil`, `len`, `min` — are recognised by name, not by keyword, because they are not keywords: a file may shadow them, and colouring the shadowed one anyway is what every other Go editor does too.
28+
29+### Why it is fast enough to do naively
30+
31+Scanning the whole file on every keystroke sounds wasteful, and would be. It does not happen: the buffer keeps a revision counter, bumped on every change, and the highlighter re-scans only when the number has moved. The editor redraws far more often than the text changes — every cursor move, every scroll — and all of those redraws are free.
32+
33+### The cost of this choice
34+
35+**A language is coloured only if someone wrote a scanner for it.** There is no definition language to write a definition in, so each one is Go code.
36+
37+Eight of them — TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell — live in turbo-core, because every editor built on it meets them whatever language it is for. The Go scanner lives here, in `internal/golang`, and is registered at start-up with `syntax.Register`. That is why a `.rs` file opens as plain text in Turbo Go: this editor registers Go and nothing else.
38+
39+That is a real limitation, and it was accepted deliberately: this is an editor for Go. A generic highlighter would have brought a dependency, a definition format, and a permanent gap between "what the highlighter thinks Go is" and what Go is.
40+
41+### The other eight languages
42+
43+TOML came first, and it earned its scanner by being unavoidable: the editor reads two TOML files — theme files and a project's `.turbo-go/settings.toml` — and both are meant to be edited in the editor itself. Shipping a settings file full of explanatory comments and then showing it in flat grey would have been an odd thing to do.
44+
45+Markdown, JavaScript, HTML and shell followed for a plainer reason: they are what sits beside Go in a Go project. A repository has a `README.md`, some scripts, and often a page or a bit of JavaScript, and an editor that colours only the `.go` files makes you leave it for the rest. YAML, XML and Dockerfiles joined them on the same reasoning: the files a Go project keeps beside its code are now just as likely to be a compose file, a CI workflow or an image build, and a compose file in flat grey is exactly the file you most want the shape of. They are now shared: written once here, and inherited by every editor built on turbo-core.
46+
47+Each is a few hundred lines and they share one small piece of machinery — a line, a position in it, and the spans found so far. What they do **not** share is any attempt at a general engine. There is no pattern language, no grammar format, no table of regular expressions: each scanner is ordinary Go that a reader can follow, and adding one means writing one rather than learning a notation.
48+
49+They stop short in ways the [reference](../reference/languages.md) states outright, and the stopping points were chosen rather than run out of:
50+
51+- **No JavaScript regular expressions.** Telling `/x/g` from a division needs to know whether the previous token could end an expression. A wrong guess colours the rest of the line as a string — a much louder failure than leaving a regex the colour of an operator.
52+- **No shell heredocs.** Following `<<EOF` to its delimiter means carrying an arbitrary word across lines, with the `<<-` and quoted spellings on top, for a construct that is usually a few lines of plain text.
53+- **No JavaScript inside `<script>`.** It means following an element across lines and mapping another scanner's columns back out, and the same argument would then demand CSS.
54+- **No language inside a Markdown fence.** ```` ```go ```` is one colour. Colouring it properly means every scanner has to be reachable from every other, which is the beginning of the general engine this package does not have.
55+
56+The unifying rule is that a scanner **guesses nothing**. Where a construct cannot be recognised without knowing more than one line holds, it is left alone rather than approximated, because a highlighter that is wrong is worse than one that is quiet.
57+
58+### Five classes that Go has nothing to say about
59+
60+The twelve classes the Go tokeniser produces cover the eight other languages almost entirely — a string is a string in all of them. Five things had no home: a Markdown heading, its emphasis and its links, and an HTML tag and attribute.
61+
62+Reusing existing classes was the cheaper option and was taken for TOML, where a table header genuinely reads as a type and a key as an identifier. It does not survive the markup languages: a heading is not a keyword and a tag is not one either, and a theme that wanted headings quiet and keywords loud could not say so. So `syntax.heading`, `syntax.tag`, `syntax.attribute`, `syntax.emphasis` and `syntax.link` exist.
63+
64+The cost is real and falls on themes written elsewhere: one that sets none of them falls back along the dots to `syntax` and then to `default`, so Markdown stays readable but its headings are not distinct. Every shipped theme sets all five, and a test fails if one stops doing so.
65+
66+## Completion: someone else's program
67+
68+Completion works the other way round. Turbo Go knows nothing about Go's type system and does not try to: it asks `gopls`, over the Language Server Protocol, and draws the answer.
69+
70+### Optional, and not by accident
71+
72+The editor is fully usable with no language server. Not degraded — the buffer, the colouring, the themes, the windows, the search, all work exactly the same. Only completion, hover and go-to-definition are missing, and the status bar says so with the single command that fixes it.
73+
74+This is enforced by construction rather than by discipline. `Language` wraps the entire conversation, and with no server every method does nothing at all. There is no `if server != nil` anywhere else in the editor, because there is nothing to check.
75+
76+### The trap in the protocol
77+
78+The protocol counts columns in **UTF-16 code units**. The editor counts them in runes. On ASCII the two are the same number, which is exactly why getting this wrong survives testing — until someone opens a file with an accent in a comment, and every completion after it lands one column off.
79+
80+So every position crossing that boundary is converted, and the conversion is tested with a musical clef, which needs a surrogate pair and therefore counts as two.
81+
82+### The other trap
83+
84+`gopls` asks its client questions. During start-up it requests `workspace/configuration` — and **waits for the reply**. A client that only sends requests and only reads responses never finishes initialising, and hangs with no error at all.
85+
86+So the connection routes server-to-client requests to a handler, and the client answers with empty settings, which means "use your defaults".
87+
88+### Bounded, always
89+
90+Every request has a deadline: three seconds for a completion, thirty for the handshake, because a cold `gopls` has a module graph to load before it can say hello. A server that stops answering slows the editor down and never stops it.
91+
92+A completion that arrives after you have typed three more characters is not a completion, it is an interruption — which is why the request is made synchronously and given up on quickly, rather than being delivered late.
93+
94+## Nine questions, one connection
95+
96+Completion is the loudest thing the language server does and the least revealing. The same connection answers eight more, and they divide into three kinds by what comes back.
97+
98+**Something to read.** `hover` — what is this? — drawn in a box.
99+
100+**Places in the code.** `definition`, `typeDefinition`, `implementation`, `references`. One request each, one answer shape between them, which is why they are one function underneath. A single place is opened; several are offered as a list, because a single answer is the exception rather than the rule — a Go interface has as many definitions as it has implementations, and for a long time this editor took the first and threw the rest away.
101+
102+**Names.** `documentSymbol` for a file's own outline, `workspace/symbol` for a search across the project. The protocol has three shapes for a symbol and the editor wants one, so the flattening is done where the answers arrive rather than where they are drawn.
103+
104+And one thing nobody asks for at all: **`publishDiagnostics` arrives unbidden**, whenever the server has an opinion, for whatever files it has loaded — which are usually more than the one in front of you. That is why Problems lists every file rather than the current one, and why the mark in the gutter appears without anything being pressed.
105+
106+The editor asks for none of this until the server says it is ready, and says which of those it is when a question cannot be answered. "Nothing found" and "I have not finished loading" are the same empty answer and very different news; conflating them is the most confusing way completion has ever failed here, and it would have been inherited by all eight for free.
107+
108+## Two features, two shapes
109+
110+It is worth noticing why these ended up so different.
111+
112+Colouring must be **instant and always right enough**, on text that is usually invalid. That calls for a local, tolerant, cheap answer — and the tokeniser is already in the standard library.
113+
114+Completion must be **occasionally right about the whole program**, including its dependencies. That is a compiler's job, it is expensive, and it is already solved by a program that does nothing else.
115+
116+The first was worth writing. The second was worth asking for.
117+
118+## How it relates to the rest
119+
120+- Where these two live in the codebase: [Architecture](architecture.md)
121+- The dependency policy that shaped both: [Design decisions](design-decisions.md)
122+- Getting completion running: [How to enable Go completion](../how-to/enable-completion.md)
new file mode 100644
@@ -0,0 +1,122 @@
1+# Colouring and completion — explanation
2+
3+## What is this about?
4+
5+The two features that make Turbo Go an editor *for Go* rather than a text editor that happens to be written in it: syntax colouring, and completion from a language server. They work quite differently, and the difference is instructive.
6+
7+## Colouring: the compiler's own tokeniser
8+
9+Turbo Go does not have a syntax definition. It calls `go/scanner` — the lexer the Go toolchain itself uses — and turns the tokens it gets back into coloured spans.
10+
11+This means keywords, literals and operators are recognised **exactly** as the compiler recognises them. Raw string literals, automatic semicolon insertion, `0x_FF` separators, the lot. There is no regular expression to get subtly wrong, and no table to update when the language changes.
12+
13+### Tolerance is the whole point
14+
15+Source under a cursor is syntactically broken most of the time it is being typed. Half a string, an unclosed brace, an identifier that stops mid-word. A highlighter that gives up on invalid input is a highlighter that flickers off exactly when you are looking at it.
16+
17+So the scanner runs in its most forgiving mode and **every syntax error is discarded**. It still returns usable tokens: an unterminated string comes back as a string running to the end of the line, an unclosed `/*` as a comment running to the end of the file. Which is precisely the behaviour you want — the colours stay steady, and they tell you what is wrong.
18+
19+### What the editor adds
20+
21+Three distinctions the scanner does not make, because they are about reading rather than parsing:
22+
23+- an identifier before `(`, or after `func`, is a **function**
24+- an identifier after `type`, `struct` or `interface` is a **type**
25+- brackets, commas, dots and semicolons are **punctuation**, split out from the operators that compute, so a theme can quiet them down
26+
27+Predeclared names — `int`, `error`, `nil`, `len`, `min` — are recognised by name, not by keyword, because they are not keywords: a file may shadow them, and colouring the shadowed one anyway is what every other Go editor does too.
28+
29+### Why it is fast enough to do naively
30+
31+Scanning the whole file on every keystroke sounds wasteful, and would be. It does not happen: the buffer keeps a revision counter, bumped on every change, and the highlighter re-scans only when the number has moved. The editor redraws far more often than the text changes — every cursor move, every scroll — and all of those redraws are free.
32+
33+### The cost of this choice
34+
35+**A language is coloured only if someone wrote a scanner for it.** There is no definition language to write a definition in, so each one is Go code.
36+
37+Eight of them — TOML, YAML, Markdown, JavaScript, HTML, XML, Dockerfiles and shell — live in turbo-core, because every editor built on it meets them whatever language it is for. The Go scanner lives here, in `internal/golang`, and is registered at start-up with `syntax.Register`. That is why a `.rs` file opens as plain text in Turbo Go: this editor registers Go and nothing else.
38+
39+That is a real limitation, and it was accepted deliberately: this is an editor for Go. A generic highlighter would have brought a dependency, a definition format, and a permanent gap between "what the highlighter thinks Go is" and what Go is.
40+
41+### The other eight languages
42+
43+TOML came first, and it earned its scanner by being unavoidable: the editor reads two TOML files — theme files and a project's `.turbo-go/settings.toml` — and both are meant to be edited in the editor itself. Shipping a settings file full of explanatory comments and then showing it in flat grey would have been an odd thing to do.
44+
45+Markdown, JavaScript, HTML and shell followed for a plainer reason: they are what sits beside Go in a Go project. A repository has a `README.md`, some scripts, and often a page or a bit of JavaScript, and an editor that colours only the `.go` files makes you leave it for the rest. YAML, XML and Dockerfiles joined them on the same reasoning: the files a Go project keeps beside its code are now just as likely to be a compose file, a CI workflow or an image build, and a compose file in flat grey is exactly the file you most want the shape of. They are now shared: written once here, and inherited by every editor built on turbo-core.
46+
47+Each is a few hundred lines and they share one small piece of machinery — a line, a position in it, and the spans found so far. What they do **not** share is any attempt at a general engine. There is no pattern language, no grammar format, no table of regular expressions: each scanner is ordinary Go that a reader can follow, and adding one means writing one rather than learning a notation.
48+
49+They stop short in ways the [reference](../reference/languages.md) states outright, and the stopping points were chosen rather than run out of:
50+
51+- **No JavaScript regular expressions.** Telling `/x/g` from a division needs to know whether the previous token could end an expression. A wrong guess colours the rest of the line as a string — a much louder failure than leaving a regex the colour of an operator.
52+- **No shell heredocs.** Following `<<EOF` to its delimiter means carrying an arbitrary word across lines, with the `<<-` and quoted spellings on top, for a construct that is usually a few lines of plain text.
53+- **No JavaScript inside `<script>`.** It means following an element across lines and mapping another scanner's columns back out, and the same argument would then demand CSS.
54+- **No language inside a Markdown fence.** ```` ```go ```` is one colour. Colouring it properly means every scanner has to be reachable from every other, which is the beginning of the general engine this package does not have.
55+
56+The unifying rule is that a scanner **guesses nothing**. Where a construct cannot be recognised without knowing more than one line holds, it is left alone rather than approximated, because a highlighter that is wrong is worse than one that is quiet.
57+
58+### Five classes that Go has nothing to say about
59+
60+The twelve classes the Go tokeniser produces cover the eight other languages almost entirely — a string is a string in all of them. Five things had no home: a Markdown heading, its emphasis and its links, and an HTML tag and attribute.
61+
62+Reusing existing classes was the cheaper option and was taken for TOML, where a table header genuinely reads as a type and a key as an identifier. It does not survive the markup languages: a heading is not a keyword and a tag is not one either, and a theme that wanted headings quiet and keywords loud could not say so. So `syntax.heading`, `syntax.tag`, `syntax.attribute`, `syntax.emphasis` and `syntax.link` exist.
63+
64+The cost is real and falls on themes written elsewhere: one that sets none of them falls back along the dots to `syntax` and then to `default`, so Markdown stays readable but its headings are not distinct. Every shipped theme sets all five, and a test fails if one stops doing so.
65+
66+## Completion: someone else's program
67+
68+Completion works the other way round. Turbo Go knows nothing about Go's type system and does not try to: it asks `gopls`, over the Language Server Protocol, and draws the answer.
69+
70+### Optional, and not by accident
71+
72+The editor is fully usable with no language server. Not degraded — the buffer, the colouring, the themes, the windows, the search, all work exactly the same. Only completion, hover and go-to-definition are missing, and the status bar says so with the single command that fixes it.
73+
74+This is enforced by construction rather than by discipline. `Language` wraps the entire conversation, and with no server every method does nothing at all. There is no `if server != nil` anywhere else in the editor, because there is nothing to check.
75+
76+### The trap in the protocol
77+
78+The protocol counts columns in **UTF-16 code units**. The editor counts them in runes. On ASCII the two are the same number, which is exactly why getting this wrong survives testing — until someone opens a file with an accent in a comment, and every completion after it lands one column off.
79+
80+So every position crossing that boundary is converted, and the conversion is tested with a musical clef, which needs a surrogate pair and therefore counts as two.
81+
82+### The other trap
83+
84+`gopls` asks its client questions. During start-up it requests `workspace/configuration` — and **waits for the reply**. A client that only sends requests and only reads responses never finishes initialising, and hangs with no error at all.
85+
86+So the connection routes server-to-client requests to a handler, and the client answers with empty settings, which means "use your defaults".
87+
88+### Bounded, always
89+
90+Every request has a deadline: three seconds for a completion, thirty for the handshake, because a cold `gopls` has a module graph to load before it can say hello. A server that stops answering slows the editor down and never stops it.
91+
92+A completion that arrives after you have typed three more characters is not a completion, it is an interruption — which is why the request is made synchronously and given up on quickly, rather than being delivered late.
93+
94+## Nine questions, one connection
95+
96+Completion is the loudest thing the language server does and the least revealing. The same connection answers eight more, and they divide into three kinds by what comes back.
97+
98+**Something to read.** `hover` — what is this? — drawn in a box.
99+
100+**Places in the code.** `definition`, `typeDefinition`, `implementation`, `references`. One request each, one answer shape between them, which is why they are one function underneath. A single place is opened; several are offered as a list, because a single answer is the exception rather than the rule — a Go interface has as many definitions as it has implementations, and for a long time this editor took the first and threw the rest away.
101+
102+**Names.** `documentSymbol` for a file's own outline, `workspace/symbol` for a search across the project. The protocol has three shapes for a symbol and the editor wants one, so the flattening is done where the answers arrive rather than where they are drawn.
103+
104+And one thing nobody asks for at all: **`publishDiagnostics` arrives unbidden**, whenever the server has an opinion, for whatever files it has loaded — which are usually more than the one in front of you. That is why Problems lists every file rather than the current one, and why the mark in the gutter appears without anything being pressed.
105+
106+The editor asks for none of this until the server says it is ready, and says which of those it is when a question cannot be answered. "Nothing found" and "I have not finished loading" are the same empty answer and very different news; conflating them is the most confusing way completion has ever failed here, and it would have been inherited by all eight for free.
107+
108+## Two features, two shapes
109+
110+It is worth noticing why these ended up so different.
111+
112+Colouring must be **instant and always right enough**, on text that is usually invalid. That calls for a local, tolerant, cheap answer — and the tokeniser is already in the standard library.
113+
114+Completion must be **occasionally right about the whole program**, including its dependencies. That is a compiler's job, it is expensive, and it is already solved by a program that does nothing else.
115+
116+The first was worth writing. The second was worth asking for.
117+
118+## How it relates to the rest
119+
120+- Where these two live in the codebase: [Architecture](architecture.md)
121+- The dependency policy that shaped both: [Design decisions](design-decisions.md)
122+- Getting completion running: [How to enable Go completion](../how-to/enable-completion.md)
added docs/en/explanation/design-decisions.md +109 -0
new file mode 100644
@@ -0,0 +1,109 @@
1+# Design decisions — explanation
2+
3+## What is this about?
4+
5+The choices that shaped Turbo Go, what the alternatives were, and why they were turned down. This is the page to read before changing something that looks arbitrary.
6+
7+## Two dependencies, and no more
8+
9+Turbo Go depends on `tcell/v2` and `BurntSushi/toml`. Everything else is the standard library — including the tokeniser, the JSON-RPC client, the LSP framing and the file handling.
10+
11+**What was rejected.** `go.lsp.dev/jsonrpc2` would have saved perhaps three hundred lines of turbo-core's `lsp`. `rivo/tview` would have saved rather more of turbo-core's `ui`. A syntax-highlighting library would have brought fifty languages instead of one.
12+
13+**Why.** An editor is a program you keep for years and change often. Every dependency is a piece of it you cannot change, cannot fully test, and have to track. The protocol is simple enough to write down, and writing it down put the whole conversation somewhere a reader can follow. Three hundred lines you understand beat three hundred you inherit.
14+
15+The exception proves the rule: `tcell` is not a convenience, it is the terminal-compatibility database, and reimplementing that would be neither small nor honest work.
16+
17+## The widget framework is hand-written
18+
19+`tview` has widgets. `bubbletea` has an architecture. Neither has what Turbo Vision had: overlapping movable windows with shadows, a menu bar with hot keys, and modal dialogs, all drawn with box characters in sixteen colours.
20+
21+The Elm-style architecture that `bubbletea` uses re-renders the whole view on every message. That model is excellent for a form and awkward for a full-screen editor with windows stacked on top of each other and a cursor that has to be in one exact cell.
22+
23+Writing the framework cost roughly fifteen hundred lines. In exchange the editor looks like Turbo C rather than like a modern TUI wearing a blue background, and every drawing decision is one file away.
24+
25+## Bounds are absolute screen coordinates
26+
27+Every widget's `Bounds()` is where it really is on the terminal, not where it is relative to its parent. Hit-testing a mouse click is then a plain rectangle test, and no event ever needs translating on its way down.
28+
29+**The cost** is that containers place their children in screen space. **The alternative** — relative coordinates with a translation at each hop — moves the arithmetic from layout into event handling, where it is done far more often and is far easier to get wrong. Clipping still composes correctly because a painter intersects its parent's clip, so a child with wrong arithmetic draws nothing rather than drawing over its neighbours.
30+
31+## Every change goes through one function
32+
33+`buffer.ReplaceRange` is the only place the text is modified. Insert, backspace, delete, indent, paste and undo all funnel through it, and it is the only place the undo history, the modified flag, the revision counter and the cursor are maintained.
34+
35+The alternative — each operation maintaining its own bookkeeping — is how undo bugs are born. There is exactly one thing to get right, and it is tested directly.
36+
37+## Windows follow the terminal, they do not scale with it
38+
39+A window has a **grow mode**, which names the desktop edges it follows. A document window follows the right and bottom edges: its top-left corner stays where it is, and its far corner moves by exactly as much as the terminal's did. A window that filled the terminal therefore still fills it, and one you had cascaded keeps its offset.
40+
41+**The alternative was proportional scaling** — multiply every window's rectangle by the ratio of the old and new sizes. It was rejected because it moves windows the user deliberately placed, and because rounding makes it lossy: shrink and grow again and nothing is where it was. Turbo Vision used grow modes, and they are still the right answer.
42+
43+Whatever its grow mode, a window is then held to the desktop's own size. One larger than the desktop that holds it has parts nobody can reach.
44+
45+
46+## A window's boxes say what they will do, not what the window is
47+
48+The frame carries two boxes: `[x]` at the left closes the window, `[■]` at the right fills the desktop.
49+
50+The close box used to be `[■]` — Turbo Vision's own — and it had to move. Two boxes on one frame need to be told apart at a glance, and a filled block reads as "fill the screen" far more readily than as "close". `[x]` is what a close button has meant for thirty years; the block went to the job it actually looks like.
51+
52+The maximise box **changes with the window's state**: `[■]` while there is room to grow, `[▬]` once the window fills the desktop. The alternative was a fixed symbol, and it makes the button ambiguous exactly when you need it — you can see that the window is large, but not whether pressing the box will make it larger still or put it back. A control that shows its *current state* leaves you to work out the action; one that shows its *action* does not.
53+
54+A window with nowhere to maximise into shows **no box at all**, rather than one that does nothing. Only the desktop knows what area a window would fill, so a window that is not on one has nothing to offer.
55+
56+**Window ▸ Maximise is the same toggle**, not a one-way action. A menu item and a button that disagreed about what "maximise" means would be a bug people reported rather than a subtlety they appreciated.
57+
58+## Undo merges runs of typing
59+
60+Typing `func` and pressing Ctrl-Z removes all four letters. So does a run of backspaces. Moving the cursor ends the run, and typing never merges with deleting.
61+
62+Character-by-character undo is what a naive implementation gives you, and it is what Turbo C itself did. It is also what nobody wants any more.
63+
64+## Themes are TOML, with two kinds of inheritance
65+
66+**Between files**, `inherits` takes the parent's resolved styles as the starting point. A theme of your own can therefore be five lines.
67+
68+**Between keys**, along the dots: `syntax.keyword` falls back to `syntax`, and `syntax` to `default`. This happens twice — once at parse time, so an entry setting only `fg` inherits its `bg`, and once at lookup time, so a theme that never mentions `syntax.keyword` still colours keywords.
69+
70+That second one is what makes a partial theme a usable theme, and it is why there is no such thing as a theme that leaves half the screen unpainted.
71+
72+**Why TOML rather than JSON.** Comments. A theme is a file people edit by hand and annotate.
73+
74+**An unknown colour is an error**, not a silent fallback to the terminal default. A typo that quietly repaints half the screen is much harder to find than one that says so at load.
75+
76+## The language server is optional by construction
77+
78+`app.Language` wraps the whole gopls conversation, and when there is no server every method is a no-op rather than an error. Nothing else in the editor asks whether a language server exists.
79+
80+The alternative — checking for `nil` at each of the twenty call sites — is twenty chances to forget. Here, forgetting is impossible: there is nothing to check.
81+
82+This is why `gopls` is not bundled, not downloaded, and not required. It is looked for on `PATH` and in `GOPATH/bin`, and its absence is reported on the status bar with the one command that fixes it.
83+
84+## Saving is atomic, and byte-faithful
85+
86+A save writes to a temporary file in the same directory and renames it over the target, preserving the original's permissions. An interrupted save cannot leave a half-written source file.
87+
88+Separately, the line endings a file was read with and its trailing newline — or lack of one — are remembered, so opening and saving an untouched file reproduces it byte for byte. An editor that silently normalises line endings turns a one-line change into a whole-file diff.
89+
90+## The clipboard is the editor's own
91+
92+A terminal program cannot read the host clipboard portably. Rather than pretend, Turbo Go shares one clipboard between its own windows, which is what Turbo C did.
93+
94+## The version is a property of the build, not of the source
95+
96+The version used to be `const Version = "0.1.0"` in a subdirectory. It was accurate the day it was written and wrong for the fourteen commits after it, because nothing in the process of committing, tagging or installing touches a Go constant. An About box is where somebody looks when they are about to report a bug; a number there that names a release the binary is not is worse than no number, because it is believed.
97+
98+So the number is taken from the build. The linker stamps `git describe --tags --dirty` into turbo-core's `version` package from the Makefile and from the installer, which is what makes `make install` produce an editor that names the commit it came from. When nothing stamped it, the binary asks `runtime/debug.ReadBuildInfo()`, which covers the one path that cannot be stamped: `go install rickub.com/turbo-editors/turbo-go@v0.2.0`, where there is no Makefile in the picture and the Go tool knows the module version. Only when both are silent does it say `unknown` — deliberately not a number, because the whole failure being designed against is a plausible-looking version nobody set.
99+
100+Two things the build system cannot do explain the rest of the design. **It does not read git tags**, so a plain `go build .` can never report `0.1.0-14-g88a4c38` however clever the code is; it reports `devel` plus the commit, and the documentation says so rather than implying that every build is equal. And what it *does* report for such a build is a **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — which is shown as `devel` instead, because its `0.1.1` is a patch release that does not exist and would be read as one.
101+
102+`vcs.time` is deliberately unused. It is the commit's timestamp, and every binary is linked later than the commit it was built from, so labelling it "Built" would be false on all of them. A build date is shown only when a build actually stamped one, which is the same rule the About box follows throughout: **a fact nobody recorded gets no line**, rather than an empty one that reads as a failure to fill it in.
103+
104+Rejected: a `make release` target that tags, builds and pushes. Releasing is three git commands, and wrapping them hides which of them failed; the version stamping is the part that could not be done by hand reliably, and that is the part that was automated.
105+
106+## How it relates to the rest
107+
108+- What the packages are and how they fit: [Architecture](architecture.md)
109+- How the colouring and the completion work: [Colouring and completion](colouring-and-completion.md)
new file mode 100644
@@ -0,0 +1,109 @@
1+# Design decisions — explanation
2+
3+## What is this about?
4+
5+The choices that shaped Turbo Go, what the alternatives were, and why they were turned down. This is the page to read before changing something that looks arbitrary.
6+
7+## Two dependencies, and no more
8+
9+Turbo Go depends on `tcell/v2` and `BurntSushi/toml`. Everything else is the standard library — including the tokeniser, the JSON-RPC client, the LSP framing and the file handling.
10+
11+**What was rejected.** `go.lsp.dev/jsonrpc2` would have saved perhaps three hundred lines of turbo-core's `lsp`. `rivo/tview` would have saved rather more of turbo-core's `ui`. A syntax-highlighting library would have brought fifty languages instead of one.
12+
13+**Why.** An editor is a program you keep for years and change often. Every dependency is a piece of it you cannot change, cannot fully test, and have to track. The protocol is simple enough to write down, and writing it down put the whole conversation somewhere a reader can follow. Three hundred lines you understand beat three hundred you inherit.
14+
15+The exception proves the rule: `tcell` is not a convenience, it is the terminal-compatibility database, and reimplementing that would be neither small nor honest work.
16+
17+## The widget framework is hand-written
18+
19+`tview` has widgets. `bubbletea` has an architecture. Neither has what Turbo Vision had: overlapping movable windows with shadows, a menu bar with hot keys, and modal dialogs, all drawn with box characters in sixteen colours.
20+
21+The Elm-style architecture that `bubbletea` uses re-renders the whole view on every message. That model is excellent for a form and awkward for a full-screen editor with windows stacked on top of each other and a cursor that has to be in one exact cell.
22+
23+Writing the framework cost roughly fifteen hundred lines. In exchange the editor looks like Turbo C rather than like a modern TUI wearing a blue background, and every drawing decision is one file away.
24+
25+## Bounds are absolute screen coordinates
26+
27+Every widget's `Bounds()` is where it really is on the terminal, not where it is relative to its parent. Hit-testing a mouse click is then a plain rectangle test, and no event ever needs translating on its way down.
28+
29+**The cost** is that containers place their children in screen space. **The alternative** — relative coordinates with a translation at each hop — moves the arithmetic from layout into event handling, where it is done far more often and is far easier to get wrong. Clipping still composes correctly because a painter intersects its parent's clip, so a child with wrong arithmetic draws nothing rather than drawing over its neighbours.
30+
31+## Every change goes through one function
32+
33+`buffer.ReplaceRange` is the only place the text is modified. Insert, backspace, delete, indent, paste and undo all funnel through it, and it is the only place the undo history, the modified flag, the revision counter and the cursor are maintained.
34+
35+The alternative — each operation maintaining its own bookkeeping — is how undo bugs are born. There is exactly one thing to get right, and it is tested directly.
36+
37+## Windows follow the terminal, they do not scale with it
38+
39+A window has a **grow mode**, which names the desktop edges it follows. A document window follows the right and bottom edges: its top-left corner stays where it is, and its far corner moves by exactly as much as the terminal's did. A window that filled the terminal therefore still fills it, and one you had cascaded keeps its offset.
40+
41+**The alternative was proportional scaling** — multiply every window's rectangle by the ratio of the old and new sizes. It was rejected because it moves windows the user deliberately placed, and because rounding makes it lossy: shrink and grow again and nothing is where it was. Turbo Vision used grow modes, and they are still the right answer.
42+
43+Whatever its grow mode, a window is then held to the desktop's own size. One larger than the desktop that holds it has parts nobody can reach.
44+
45+
46+## A window's boxes say what they will do, not what the window is
47+
48+The frame carries two boxes: `[x]` at the left closes the window, `[■]` at the right fills the desktop.
49+
50+The close box used to be `[■]` — Turbo Vision's own — and it had to move. Two boxes on one frame need to be told apart at a glance, and a filled block reads as "fill the screen" far more readily than as "close". `[x]` is what a close button has meant for thirty years; the block went to the job it actually looks like.
51+
52+The maximise box **changes with the window's state**: `[■]` while there is room to grow, `[▬]` once the window fills the desktop. The alternative was a fixed symbol, and it makes the button ambiguous exactly when you need it — you can see that the window is large, but not whether pressing the box will make it larger still or put it back. A control that shows its *current state* leaves you to work out the action; one that shows its *action* does not.
53+
54+A window with nowhere to maximise into shows **no box at all**, rather than one that does nothing. Only the desktop knows what area a window would fill, so a window that is not on one has nothing to offer.
55+
56+**Window ▸ Maximise is the same toggle**, not a one-way action. A menu item and a button that disagreed about what "maximise" means would be a bug people reported rather than a subtlety they appreciated.
57+
58+## Undo merges runs of typing
59+
60+Typing `func` and pressing Ctrl-Z removes all four letters. So does a run of backspaces. Moving the cursor ends the run, and typing never merges with deleting.
61+
62+Character-by-character undo is what a naive implementation gives you, and it is what Turbo C itself did. It is also what nobody wants any more.
63+
64+## Themes are TOML, with two kinds of inheritance
65+
66+**Between files**, `inherits` takes the parent's resolved styles as the starting point. A theme of your own can therefore be five lines.
67+
68+**Between keys**, along the dots: `syntax.keyword` falls back to `syntax`, and `syntax` to `default`. This happens twice — once at parse time, so an entry setting only `fg` inherits its `bg`, and once at lookup time, so a theme that never mentions `syntax.keyword` still colours keywords.
69+
70+That second one is what makes a partial theme a usable theme, and it is why there is no such thing as a theme that leaves half the screen unpainted.
71+
72+**Why TOML rather than JSON.** Comments. A theme is a file people edit by hand and annotate.
73+
74+**An unknown colour is an error**, not a silent fallback to the terminal default. A typo that quietly repaints half the screen is much harder to find than one that says so at load.
75+
76+## The language server is optional by construction
77+
78+`app.Language` wraps the whole gopls conversation, and when there is no server every method is a no-op rather than an error. Nothing else in the editor asks whether a language server exists.
79+
80+The alternative — checking for `nil` at each of the twenty call sites — is twenty chances to forget. Here, forgetting is impossible: there is nothing to check.
81+
82+This is why `gopls` is not bundled, not downloaded, and not required. It is looked for on `PATH` and in `GOPATH/bin`, and its absence is reported on the status bar with the one command that fixes it.
83+
84+## Saving is atomic, and byte-faithful
85+
86+A save writes to a temporary file in the same directory and renames it over the target, preserving the original's permissions. An interrupted save cannot leave a half-written source file.
87+
88+Separately, the line endings a file was read with and its trailing newline — or lack of one — are remembered, so opening and saving an untouched file reproduces it byte for byte. An editor that silently normalises line endings turns a one-line change into a whole-file diff.
89+
90+## The clipboard is the editor's own
91+
92+A terminal program cannot read the host clipboard portably. Rather than pretend, Turbo Go shares one clipboard between its own windows, which is what Turbo C did.
93+
94+## The version is a property of the build, not of the source
95+
96+The version used to be `const Version = "0.1.0"` in a subdirectory. It was accurate the day it was written and wrong for the fourteen commits after it, because nothing in the process of committing, tagging or installing touches a Go constant. An About box is where somebody looks when they are about to report a bug; a number there that names a release the binary is not is worse than no number, because it is believed.
97+
98+So the number is taken from the build. The linker stamps `git describe --tags --dirty` into turbo-core's `version` package from the Makefile and from the installer, which is what makes `make install` produce an editor that names the commit it came from. When nothing stamped it, the binary asks `runtime/debug.ReadBuildInfo()`, which covers the one path that cannot be stamped: `go install rickub.com/turbo-editors/turbo-go@v0.2.0`, where there is no Makefile in the picture and the Go tool knows the module version. Only when both are silent does it say `unknown` — deliberately not a number, because the whole failure being designed against is a plausible-looking version nobody set.
99+
100+Two things the build system cannot do explain the rest of the design. **It does not read git tags**, so a plain `go build .` can never report `0.1.0-14-g88a4c38` however clever the code is; it reports `devel` plus the commit, and the documentation says so rather than implying that every build is equal. And what it *does* report for such a build is a **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — which is shown as `devel` instead, because its `0.1.1` is a patch release that does not exist and would be read as one.
101+
102+`vcs.time` is deliberately unused. It is the commit's timestamp, and every binary is linked later than the commit it was built from, so labelling it "Built" would be false on all of them. A build date is shown only when a build actually stamped one, which is the same rule the About box follows throughout: **a fact nobody recorded gets no line**, rather than an empty one that reads as a failure to fill it in.
103+
104+Rejected: a `make release` target that tags, builds and pushes. Releasing is three git commands, and wrapping them hides which of them failed; the version stamping is the part that could not be done by hand reliably, and that is the part that was automated.
105+
106+## How it relates to the rest
107+
108+- What the packages are and how they fit: [Architecture](architecture.md)
109+- How the colouring and the completion work: [Colouring and completion](colouring-and-completion.md)
added docs/en/explanation/go-tools.md +117 -0
new file mode 100644
@@ -0,0 +1,117 @@
1+# Go tools — explanation
2+
3+## What is this about?
4+
5+A **Go** menu whose commands come from a TOML file, each run in a terminal window, and the open files re-read afterwards. This page is about why each of those three is the way it is.
6+
7+## Why the output has three places to go, and a popup by default
8+
9+The first version put every command in a terminal window, and it was the wrong default for four of the five.
10+
11+A terminal is the right answer when the program is *interactive or long*: `go run .` on something that reads standard input has to be answerable, and a build that turns out to take a minute has to be interruptible with `Ctrl-C`. Neither is true of `go vet ./...`, which prints four lines and ends. Giving that a whole window — one you then have to close, on a desktop where windows overlap and are numbered — is more ceremony than the result deserves.
12+
13+A popup is the right answer for a command you run, read and dismiss. It is modal, which is a real cost and is named in the [how-to](../how-to/run-go-commands.md): a `go build` you did not expect to be slow holds the editor until it finishes or you press Escape. That cost was accepted on purpose, because the alternative — a dialog appearing unbidden three seconds later — swallows whatever was being typed at the moment it arrives.
14+
15+So the popup **opens immediately and fills in**. You see progress, nothing surprises you, and Escape both closes it and stops the command, which is the only way to interrupt something whose output is not in a terminal.
16+
17+An editing window is the right answer for output you are going to work through: a long `go test -v`, a coverage report. It is an ordinary buffer, so `Ctrl-F` searches it and `Save as` keeps it. It is filled once the command has ended rather than as it goes, because a buffer growing under the cursor while you search it is the opposite of what that mode is for.
18+
19+None of those three is right for everything, which is why `output` is in the file rather than in the code. `Run` is the worked example: it is the one command in the starter file that says `terminal`, and the comment beside it says why.
20+
21+## Why a terminal window is still there
22+
23+The editor already had one — a real pseudo-terminal with a VT emulator, built for the `F8` windows — so `output = "terminal"` costs one field on its options and buys colours, paging, `Ctrl-C`, keyboard input and scrollback for nothing, because they are the same mechanisms every other terminal uses.
24+
25+The window stays after the command exits, which is the point: the output is what you asked for, and a window that vanished with it would be useless.
26+
27+That needed a fix of its own. A terminal view consumed every key it was given and wrote 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 finished window and the mouse was the only way out. A finished view now takes only the scrolling keys and lets the rest through to the editor.
28+
29+## Why the exit code is always in the title
30+
31+`go build ./...` succeeding prints nothing at all. A popup with an empty body and a neutral title is indistinguishable from one whose command has not started, and the reader is left guessing at the one thing they wanted to know.
32+
33+So the title carries the verdict — `— ok` or `— exit 1` — and an empty body says `(no output)` once the command has ended. While it is still running the body stays blank, because "(no output)" is a verdict and a running command has not reached one.
34+
35+## Why the commands are in a file
36+
37+Five commands hardwired into the editor would have answered the request. They would also have been wrong within a week.
38+
39+`go vet` is the default linter because it ships with the toolchain and is never missing — but plenty of projects want `golangci-lint`. `go run .` assumes the main package is at the root. A project with a `Makefile` wants `make check`. A project that generates code wants `go generate ./...` before anything else. None of that is knowable from here, and all of it is one line in a file.
40+
41+So the five are **defaults, not code**: they are the contents of the starter file that **Go ▸ Create tools file** writes, and changing one is editing a file rather than rebuilding an editor. The file is read every time the menu opens, for the same reason the Snippets menu is: an edit should take effect at once, and the file is often open in the window behind the menu.
42+
43+Commands go to `sh -c``cmd.exe /S /C` on Windows — rather than being split into an argv here. The file is the user's own, so pipes, globs and `&&` are features rather than hazards, and one entry can be `gofmt -l -w . && go vet ./... && go test ./...`. Splitting an argv would mean inventing quoting rules for a string somebody wrote by hand.
44+
45+## Why there is no user-level tools file
46+
47+Snippets are read from two files — yours and the project's — because your snippets are your habits and should follow you between projects.
48+
49+Tools are not like that. They belong to a project's own toolchain: a global tools file would offer `go build ./...` in a Rust repository and `cargo test` in a Go one. The file is per-project, and that is the whole of the rule.
50+
51+## Why a tool may name its own menu
52+
53+A menu called **Go** holding `docker compose up` is a lie about what the menu is. The first tools file anybody writes outgrows Go, because a project's commands are not all about the language it is written in: containers, databases, deploys, a `Makefile` target somebody added in 2019.
54+
55+Two shapes were considered. A **fixed second menu** called Tools — everything Go in Go, everything else in Tools — is one key in the format and no naming problem at all, but it only moves the lie: a Tools menu holding `docker compose up`, `psql`, and a deploy script is just as undifferentiated, and the moment there are ten entries nobody can find one. And a **second file**, `menus.toml`, keeps the tools file simple at the cost of two files that have to agree about which tools exist.
56+
57+So the menu is a **free-form name on the tool**, in the one file: `menu = "Docker"`. A name nothing else uses creates the menu; leaving the key out means Go. There is no list of allowed names, because a list would be a list of somebody else's projects.
58+
59+Go itself stays fixed on the bar rather than becoming just another name from the file. **Go ▸ Create tools file** has to be reachable in a project that has no tools file at all — which is exactly the project that needs it — and a menu that only exists once the file exists cannot offer to write the file.
60+
61+## Why the hot key is not the file's to choose
62+
63+The author of a tools file cannot know which letters are free. They can see `File`, `Edit`, `Search`, `Run`, `Options`, `Window`, `Snippets`, `Go` and `Help` on the bar, but only by counting the underlines, and a project shared between people would then depend on nobody adding a menu that collides.
64+
65+Collisions here are **silent**, which is what makes them worth designing against. The bar answers the first menu whose hot key matches; a second menu claiming the same letter is not an error and draws normally — it simply never opens. That trap has already been sprung once in this editor: `Snippets` and `Search` both wanted `S`, `Snippets` was the unreachable one, and every test passed. The fix then was to move Snippets to `N` by hand. Letting a file name menus makes that a permanent hazard rather than a one-off mistake, so the assignment is done by the editor: the first letter of the name nothing else claims.
66+
67+Tildes written into the name are honoured **when the letter is free**, and quietly overridden when it is not. Refusing the file instead was the alternative, and it is worse: the clash depends on which menus exist, so a tools file that worked would break the day an editor release added a menu. Between a menu on a letter you did not ask for and a menu you cannot open, the first is the smaller loss.
68+
69+When every letter of a name is taken, the menu gets no hot key at all. `F10`, the arrow keys and the mouse still reach it, and the alternative — reaching for a letter that is not in the name — would put an underline under nothing.
70+
71+## Why the bar is rebuilt from a stat
72+
73+`Menu.OnOpen` refills a menu's items just before it drops down, which is how the Go and Snippets menus follow their files without a restart. It cannot help here: the *set* of menus is part of the bar, not part of any one menu, and adding `menu = "Docker"` to the file should put Docker on the bar.
74+
75+Reading and parsing the file on every turn of the event loop would do it, and would also be work done for nothing on every keystroke of a file nobody has edited. So the bar carries the size and modification time of the tools file it was built from, and one `stat` per turn decides whether to rebuild. Editing the file in the window in front of you, saving it, and watching the bar change is the case this is for.
76+
77+## Why open files are re-read, and only some of them
78+
79+`Format` is the first item in the menu and it rewrites files on disk — including the one you are looking at. Without anything further, the editor would sit on a stale copy, and the next `F2` would write your unformatted version back over gofmt's work. That is not a rough edge; it is the feature quietly undoing itself.
80+
81+So when a command finishes, the editor re-reads every open file. The interesting part is which ones it refuses to touch.
82+
83+**A file with unsaved changes is left alone**, and the status bar says how many were skipped. Reloading it would throw away work the user has not saved, which no amount of convenience justifies. And the conflict is genuine: the formatter and the unsaved edit disagree about what the file should say, and the editor is not in a position to decide. Naming it and stopping is the honest outcome — the user can save and re-run, or keep editing and format later.
84+
85+Two smaller decisions inside that:
86+
87+- **The cursor stays where it was**, clamped into whatever the file now holds. A formatter moves lines about; putting the cursor back at the top would lose the reader's place for no reason.
88+- **The undo history is discarded.** Undoing back past a reload would restore text the file no longer has, which is worse than not being able to undo at all.
89+
90+## Why the reload happens on the event loop
91+
92+The command's exit is noticed on the goroutine reading the terminal, which may not touch a buffer or the desktop. So it sets a flag, and the reload runs at the top of the next turn of the event loop.
93+
94+This is the fourth thing in this editor built that way — the language-server announcement, the terminal redraws, the autosave deadline, and now this. The rule they share is worth stating once more: **the wake-up may be lost, so the state must not be.** `PostEvent` drops what does not fit in its queue, so anything that depends on a message arriving is a bug waiting for a busy moment. A flag the loop checks for itself cannot go missing.
95+
96+## Why a command can ask for a value, and why it asks in double braces
97+
98+`go mod init` needs a module path. `cargo new` needs a crate name. `go test -run` needs a pattern. None of those can live in the tools file, because the answer is different every time — and a tool that cannot ask is a tool that has to be edited before each use, which is not a tool.
99+
100+So a `{{label}}` in a command is a value the editor asks for first, in a box titled after the tool.
101+
102+**Single braces were the obvious spelling and are wrong.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary things to put in a tools file, and reading the first as a placeholder turns a working command into a box asking for "print $1". Double braces collide with almost nothing, and the one construct they do collide with — a nested block in awk — is rare enough to be written down rather than designed around.
103+
104+**The value is quoted by default**, because the alternative fails silently. A path with a space in it, substituted raw, becomes two arguments and the command reports something about a file that does not exist. Quoting makes that case work and makes the other case — "put these three flags on the end" — impossible, so `...` inside the braces asks for the value verbatim. Two behaviours, both documented, rather than one that is wrong half the time.
105+
106+**Nothing is remembered on disk.** The box starts from what was typed last time, for the session. Writing it into the project's own directory was considered and rejected: that directory holds what the project decided, and a filter somebody typed while chasing one test is not that. It would also be the first thing in there that changes without anybody editing it.
107+
108+**A file that cannot be parsed is refused when it is read**, not when the tool is chosen. An unclosed `{{` reaching the shell is a command failing with braces in it, which names neither the tool nor the file; refusing at load names both. That is the same rule an unknown `output` value already follows.
109+
110+**The dialog is refused when it will not fit.** A tool asking for more values than the terminal has rows would give a box whose OK button is below the bottom of the screen — answerable only by Escape, which cancels. Saying "this asks for twelve values and nine fit" is worse than nothing only if you would rather find out by trying.
111+
112+## How it relates to the rest
113+
114+- Every key of the file and every rule: [Go tools reference](../reference/go-tools.md)
115+- Using it: [How to run go commands from the editor](../how-to/run-go-commands.md)
116+- The windows `output = "terminal"` uses, and why they are real terminals: [Terminal windows](terminal-windows.md)
117+- The other menu built from a file: [Snippets](snippets.md)
new file mode 100644
@@ -0,0 +1,117 @@
1+# Go tools — explanation
2+
3+## What is this about?
4+
5+A **Go** menu whose commands come from a TOML file, each run in a terminal window, and the open files re-read afterwards. This page is about why each of those three is the way it is.
6+
7+## Why the output has three places to go, and a popup by default
8+
9+The first version put every command in a terminal window, and it was the wrong default for four of the five.
10+
11+A terminal is the right answer when the program is *interactive or long*: `go run .` on something that reads standard input has to be answerable, and a build that turns out to take a minute has to be interruptible with `Ctrl-C`. Neither is true of `go vet ./...`, which prints four lines and ends. Giving that a whole window — one you then have to close, on a desktop where windows overlap and are numbered — is more ceremony than the result deserves.
12+
13+A popup is the right answer for a command you run, read and dismiss. It is modal, which is a real cost and is named in the [how-to](../how-to/run-go-commands.md): a `go build` you did not expect to be slow holds the editor until it finishes or you press Escape. That cost was accepted on purpose, because the alternative — a dialog appearing unbidden three seconds later — swallows whatever was being typed at the moment it arrives.
14+
15+So the popup **opens immediately and fills in**. You see progress, nothing surprises you, and Escape both closes it and stops the command, which is the only way to interrupt something whose output is not in a terminal.
16+
17+An editing window is the right answer for output you are going to work through: a long `go test -v`, a coverage report. It is an ordinary buffer, so `Ctrl-F` searches it and `Save as` keeps it. It is filled once the command has ended rather than as it goes, because a buffer growing under the cursor while you search it is the opposite of what that mode is for.
18+
19+None of those three is right for everything, which is why `output` is in the file rather than in the code. `Run` is the worked example: it is the one command in the starter file that says `terminal`, and the comment beside it says why.
20+
21+## Why a terminal window is still there
22+
23+The editor already had one — a real pseudo-terminal with a VT emulator, built for the `F8` windows — so `output = "terminal"` costs one field on its options and buys colours, paging, `Ctrl-C`, keyboard input and scrollback for nothing, because they are the same mechanisms every other terminal uses.
24+
25+The window stays after the command exits, which is the point: the output is what you asked for, and a window that vanished with it would be useless.
26+
27+That needed a fix of its own. A terminal view consumed every key it was given and wrote 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 finished window and the mouse was the only way out. A finished view now takes only the scrolling keys and lets the rest through to the editor.
28+
29+## Why the exit code is always in the title
30+
31+`go build ./...` succeeding prints nothing at all. A popup with an empty body and a neutral title is indistinguishable from one whose command has not started, and the reader is left guessing at the one thing they wanted to know.
32+
33+So the title carries the verdict — `— ok` or `— exit 1` — and an empty body says `(no output)` once the command has ended. While it is still running the body stays blank, because "(no output)" is a verdict and a running command has not reached one.
34+
35+## Why the commands are in a file
36+
37+Five commands hardwired into the editor would have answered the request. They would also have been wrong within a week.
38+
39+`go vet` is the default linter because it ships with the toolchain and is never missing — but plenty of projects want `golangci-lint`. `go run .` assumes the main package is at the root. A project with a `Makefile` wants `make check`. A project that generates code wants `go generate ./...` before anything else. None of that is knowable from here, and all of it is one line in a file.
40+
41+So the five are **defaults, not code**: they are the contents of the starter file that **Go ▸ Create tools file** writes, and changing one is editing a file rather than rebuilding an editor. The file is read every time the menu opens, for the same reason the Snippets menu is: an edit should take effect at once, and the file is often open in the window behind the menu.
42+
43+Commands go to `sh -c``cmd.exe /S /C` on Windows — rather than being split into an argv here. The file is the user's own, so pipes, globs and `&&` are features rather than hazards, and one entry can be `gofmt -l -w . && go vet ./... && go test ./...`. Splitting an argv would mean inventing quoting rules for a string somebody wrote by hand.
44+
45+## Why there is no user-level tools file
46+
47+Snippets are read from two files — yours and the project's — because your snippets are your habits and should follow you between projects.
48+
49+Tools are not like that. They belong to a project's own toolchain: a global tools file would offer `go build ./...` in a Rust repository and `cargo test` in a Go one. The file is per-project, and that is the whole of the rule.
50+
51+## Why a tool may name its own menu
52+
53+A menu called **Go** holding `docker compose up` is a lie about what the menu is. The first tools file anybody writes outgrows Go, because a project's commands are not all about the language it is written in: containers, databases, deploys, a `Makefile` target somebody added in 2019.
54+
55+Two shapes were considered. A **fixed second menu** called Tools — everything Go in Go, everything else in Tools — is one key in the format and no naming problem at all, but it only moves the lie: a Tools menu holding `docker compose up`, `psql`, and a deploy script is just as undifferentiated, and the moment there are ten entries nobody can find one. And a **second file**, `menus.toml`, keeps the tools file simple at the cost of two files that have to agree about which tools exist.
56+
57+So the menu is a **free-form name on the tool**, in the one file: `menu = "Docker"`. A name nothing else uses creates the menu; leaving the key out means Go. There is no list of allowed names, because a list would be a list of somebody else's projects.
58+
59+Go itself stays fixed on the bar rather than becoming just another name from the file. **Go ▸ Create tools file** has to be reachable in a project that has no tools file at all — which is exactly the project that needs it — and a menu that only exists once the file exists cannot offer to write the file.
60+
61+## Why the hot key is not the file's to choose
62+
63+The author of a tools file cannot know which letters are free. They can see `File`, `Edit`, `Search`, `Run`, `Options`, `Window`, `Snippets`, `Go` and `Help` on the bar, but only by counting the underlines, and a project shared between people would then depend on nobody adding a menu that collides.
64+
65+Collisions here are **silent**, which is what makes them worth designing against. The bar answers the first menu whose hot key matches; a second menu claiming the same letter is not an error and draws normally — it simply never opens. That trap has already been sprung once in this editor: `Snippets` and `Search` both wanted `S`, `Snippets` was the unreachable one, and every test passed. The fix then was to move Snippets to `N` by hand. Letting a file name menus makes that a permanent hazard rather than a one-off mistake, so the assignment is done by the editor: the first letter of the name nothing else claims.
66+
67+Tildes written into the name are honoured **when the letter is free**, and quietly overridden when it is not. Refusing the file instead was the alternative, and it is worse: the clash depends on which menus exist, so a tools file that worked would break the day an editor release added a menu. Between a menu on a letter you did not ask for and a menu you cannot open, the first is the smaller loss.
68+
69+When every letter of a name is taken, the menu gets no hot key at all. `F10`, the arrow keys and the mouse still reach it, and the alternative — reaching for a letter that is not in the name — would put an underline under nothing.
70+
71+## Why the bar is rebuilt from a stat
72+
73+`Menu.OnOpen` refills a menu's items just before it drops down, which is how the Go and Snippets menus follow their files without a restart. It cannot help here: the *set* of menus is part of the bar, not part of any one menu, and adding `menu = "Docker"` to the file should put Docker on the bar.
74+
75+Reading and parsing the file on every turn of the event loop would do it, and would also be work done for nothing on every keystroke of a file nobody has edited. So the bar carries the size and modification time of the tools file it was built from, and one `stat` per turn decides whether to rebuild. Editing the file in the window in front of you, saving it, and watching the bar change is the case this is for.
76+
77+## Why open files are re-read, and only some of them
78+
79+`Format` is the first item in the menu and it rewrites files on disk — including the one you are looking at. Without anything further, the editor would sit on a stale copy, and the next `F2` would write your unformatted version back over gofmt's work. That is not a rough edge; it is the feature quietly undoing itself.
80+
81+So when a command finishes, the editor re-reads every open file. The interesting part is which ones it refuses to touch.
82+
83+**A file with unsaved changes is left alone**, and the status bar says how many were skipped. Reloading it would throw away work the user has not saved, which no amount of convenience justifies. And the conflict is genuine: the formatter and the unsaved edit disagree about what the file should say, and the editor is not in a position to decide. Naming it and stopping is the honest outcome — the user can save and re-run, or keep editing and format later.
84+
85+Two smaller decisions inside that:
86+
87+- **The cursor stays where it was**, clamped into whatever the file now holds. A formatter moves lines about; putting the cursor back at the top would lose the reader's place for no reason.
88+- **The undo history is discarded.** Undoing back past a reload would restore text the file no longer has, which is worse than not being able to undo at all.
89+
90+## Why the reload happens on the event loop
91+
92+The command's exit is noticed on the goroutine reading the terminal, which may not touch a buffer or the desktop. So it sets a flag, and the reload runs at the top of the next turn of the event loop.
93+
94+This is the fourth thing in this editor built that way — the language-server announcement, the terminal redraws, the autosave deadline, and now this. The rule they share is worth stating once more: **the wake-up may be lost, so the state must not be.** `PostEvent` drops what does not fit in its queue, so anything that depends on a message arriving is a bug waiting for a busy moment. A flag the loop checks for itself cannot go missing.
95+
96+## Why a command can ask for a value, and why it asks in double braces
97+
98+`go mod init` needs a module path. `cargo new` needs a crate name. `go test -run` needs a pattern. None of those can live in the tools file, because the answer is different every time — and a tool that cannot ask is a tool that has to be edited before each use, which is not a tool.
99+
100+So a `{{label}}` in a command is a value the editor asks for first, in a box titled after the tool.
101+
102+**Single braces were the obvious spelling and are wrong.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary things to put in a tools file, and reading the first as a placeholder turns a working command into a box asking for "print $1". Double braces collide with almost nothing, and the one construct they do collide with — a nested block in awk — is rare enough to be written down rather than designed around.
103+
104+**The value is quoted by default**, because the alternative fails silently. A path with a space in it, substituted raw, becomes two arguments and the command reports something about a file that does not exist. Quoting makes that case work and makes the other case — "put these three flags on the end" — impossible, so `...` inside the braces asks for the value verbatim. Two behaviours, both documented, rather than one that is wrong half the time.
105+
106+**Nothing is remembered on disk.** The box starts from what was typed last time, for the session. Writing it into the project's own directory was considered and rejected: that directory holds what the project decided, and a filter somebody typed while chasing one test is not that. It would also be the first thing in there that changes without anybody editing it.
107+
108+**A file that cannot be parsed is refused when it is read**, not when the tool is chosen. An unclosed `{{` reaching the shell is a command failing with braces in it, which names neither the tool nor the file; refusing at load names both. That is the same rule an unknown `output` value already follows.
109+
110+**The dialog is refused when it will not fit.** A tool asking for more values than the terminal has rows would give a box whose OK button is below the bottom of the screen — answerable only by Escape, which cancels. Saying "this asks for twelve values and nine fit" is worse than nothing only if you would rather find out by trying.
111+
112+## How it relates to the rest
113+
114+- Every key of the file and every rule: [Go tools reference](../reference/go-tools.md)
115+- Using it: [How to run go commands from the editor](../how-to/run-go-commands.md)
116+- The windows `output = "terminal"` uses, and why they are real terminals: [Terminal windows](terminal-windows.md)
117+- The other menu built from a file: [Snippets](snippets.md)
added docs/en/explanation/project-settings.md +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+# Project settings — explanation
2+
3+## What is this about?
4+
5+A project can keep a `.turbo-go/settings.toml` beside its code, saying which theme to use and whether to save files automatically. This page is about the decisions inside that sentence: why the file is only ever looked for in one place, why creating it is a menu item rather than something that happens by itself, and why automatic saving works the way it does.
6+
7+## Why the directory is not searched for upwards
8+
9+`go.mod` is found by walking up from the file you opened until one turns up, and the language server uses exactly that. The settings file deliberately does not.
10+
11+The walk is right for `go.mod` because a module has a real boundary: the file is either above you or it is not, and being inside a module is a fact about the code. "The project" is not a fact about the code. It is where you decided to start working, and the same directory tree can be several projects depending on what you are doing in it — a monorepo's `services/api` is a project when you are working on the API and part of a larger one when you are not.
12+
13+A walk would also make the setting act at a distance. You open a file, and the editor's colours change because of a file three directories up that you did not know existed. Every explanation of that behaviour has to start with "well, it searches upwards", and the rule you would rather be able to state is the one that is now true: **the project is the directory you started the editor in.**
14+
15+The cost is real and worth naming. Start the editor from a subdirectory and the project's theme does not apply. The answer is to start from the project root, which is where you would run `go build` and `git` anyway.
16+
17+## Why creating the file is a menu item
18+
19+The alternative was tempting: the first time you pick a theme, write `.turbo-go/settings.toml` so the choice sticks. Every editor that stores workspace state does something like it.
20+
21+It was rejected because it puts a directory into someone's repository as a side effect of trying a colour. The user is one `git status` away from a change they did not make, in a project that may not be theirs, possibly in a review. A theme picked to look at for ten seconds should not leave anything behind.
22+
23+So the file is created only by **Options ▸ Create project settings**, and its existence means something: this project has settings on purpose. That is also what makes the write-back rule simple to state — **the theme is written to the file when the file exists, and not otherwise** — with no flag anywhere for "do you want to remember this?".
24+
25+## Why the theme is written in place rather than re-encoded
26+
27+Once the file exists, picking a theme rewrites it. Marshalling the `Settings` struct back to TOML would be four lines and would delete every comment in the file.
28+
29+That matters more here than it usually would, because this file is *meant* to be edited by hand. It is the reason TOML colouring exists in the editor at all; the created file is mostly comments explaining the keys; a team will add comments of their own saying why they chose what they chose. Losing all of it the first time someone tries a different theme would be a silent, surprising deletion of somebody's writing.
30+
31+So the rewrite finds the `theme` line inside the `[editor]` table and changes the value between the `=` and any trailing comment. Everything else in the file comes back byte for byte. It is about forty lines rather than four, and it is the difference between a file you can keep things in and a file that eats them.
32+
33+## Why automatic saving waits for a pause
34+
35+Three triggers were considered.
36+
37+**On a fixed interval** is the simplest and is wrong: it writes in the middle of an edit. Half a renamed identifier reaches disk, a file watcher rebuilds, and a test suite fails on code that never existed as anyone's intention.
38+
39+**On leaving the window** never writes while you work, which sounds safe and means the thing on disk can be an hour behind the thing on screen — precisely when it matters, because the reason to want autosave is usually a tool watching the file.
40+
41+**After a pause in typing** is what both other editors and this one settled on. Two seconds is long enough that a pause for thought is not a write, short enough that a rebuild follows a change closely. A run of typing is one write, not one per keystroke.
42+
43+There is one deadline for the whole editor rather than one per window, because "you stopped typing" is one event. A per-window deadline would save the file you have moved away from at a different moment from the one in front of you, which nobody could observe and which is more state to keep right.
44+
45+## Why the deadline is checked, and only nudged by a timer
46+
47+This is the same trap the language-server announcement and the terminal redraws both hit, and it is worth stating once more because it will come up again.
48+
49+The editor blocks in `PollEvent`. To notice a deadline while nothing is happening, something has to wake it, and the only way to wake it from a timer is `PostEvent` — which **drops** events when its queue is full.
50+
51+So the timer is not what decides. The deadline is state, checked at the top of every turn of the event loop, exactly as `announceOpenDocuments` checks whether the language server is ready. The timer's only job is to make sure a turn happens. A nudge that gets dropped costs a save that is late until the next keystroke or click; a design where the timer did the saving would lose it altogether.
52+
53+## Why a failed automatic save does not open a dialog
54+
55+An autosave nobody asked for should not interrupt with a modal, and a modal that reappears every two seconds because a file is read-only is worse than the problem it reports. It goes on the status bar instead, and the deadline is cleared *before* the write is attempted, so a file that cannot be written is tried once per edit rather than forever.
56+
57+## Why TOML colouring reuses the Go classes
58+
59+Adding `syntax.tomlkey` and friends would have meant every theme — including the ones users have written — silently failing to colour TOML until it was updated.
60+
61+The classes already there fit: a table header names a structure, so it reads as a type; a key names a thing, so it reads as an identifier; `true` and `false` are constants because that is what they are. The result is that every theme that ever worked colours TOML correctly, with no change and no new keys. The scanner is hand-written for the same reason the terminal emulator is — TOML is a small, fully specified language, and it is one file against a third dependency.
62+
63+## How it relates to the rest
64+
65+- The exact keys and their defaults: [Project settings reference](../reference/project-settings.md)
66+- Setting one up: [How to give a project its own settings](../how-to/configure-a-project.md)
67+- The other TOML file the editor reads: [Theme file format](../reference/themes.md)
68+- Where `settings` sits among the packages: [Architecture](architecture.md)
new file mode 100644
@@ -0,0 +1,68 @@
1+# Project settings — explanation
2+
3+## What is this about?
4+
5+A project can keep a `.turbo-go/settings.toml` beside its code, saying which theme to use and whether to save files automatically. This page is about the decisions inside that sentence: why the file is only ever looked for in one place, why creating it is a menu item rather than something that happens by itself, and why automatic saving works the way it does.
6+
7+## Why the directory is not searched for upwards
8+
9+`go.mod` is found by walking up from the file you opened until one turns up, and the language server uses exactly that. The settings file deliberately does not.
10+
11+The walk is right for `go.mod` because a module has a real boundary: the file is either above you or it is not, and being inside a module is a fact about the code. "The project" is not a fact about the code. It is where you decided to start working, and the same directory tree can be several projects depending on what you are doing in it — a monorepo's `services/api` is a project when you are working on the API and part of a larger one when you are not.
12+
13+A walk would also make the setting act at a distance. You open a file, and the editor's colours change because of a file three directories up that you did not know existed. Every explanation of that behaviour has to start with "well, it searches upwards", and the rule you would rather be able to state is the one that is now true: **the project is the directory you started the editor in.**
14+
15+The cost is real and worth naming. Start the editor from a subdirectory and the project's theme does not apply. The answer is to start from the project root, which is where you would run `go build` and `git` anyway.
16+
17+## Why creating the file is a menu item
18+
19+The alternative was tempting: the first time you pick a theme, write `.turbo-go/settings.toml` so the choice sticks. Every editor that stores workspace state does something like it.
20+
21+It was rejected because it puts a directory into someone's repository as a side effect of trying a colour. The user is one `git status` away from a change they did not make, in a project that may not be theirs, possibly in a review. A theme picked to look at for ten seconds should not leave anything behind.
22+
23+So the file is created only by **Options ▸ Create project settings**, and its existence means something: this project has settings on purpose. That is also what makes the write-back rule simple to state — **the theme is written to the file when the file exists, and not otherwise** — with no flag anywhere for "do you want to remember this?".
24+
25+## Why the theme is written in place rather than re-encoded
26+
27+Once the file exists, picking a theme rewrites it. Marshalling the `Settings` struct back to TOML would be four lines and would delete every comment in the file.
28+
29+That matters more here than it usually would, because this file is *meant* to be edited by hand. It is the reason TOML colouring exists in the editor at all; the created file is mostly comments explaining the keys; a team will add comments of their own saying why they chose what they chose. Losing all of it the first time someone tries a different theme would be a silent, surprising deletion of somebody's writing.
30+
31+So the rewrite finds the `theme` line inside the `[editor]` table and changes the value between the `=` and any trailing comment. Everything else in the file comes back byte for byte. It is about forty lines rather than four, and it is the difference between a file you can keep things in and a file that eats them.
32+
33+## Why automatic saving waits for a pause
34+
35+Three triggers were considered.
36+
37+**On a fixed interval** is the simplest and is wrong: it writes in the middle of an edit. Half a renamed identifier reaches disk, a file watcher rebuilds, and a test suite fails on code that never existed as anyone's intention.
38+
39+**On leaving the window** never writes while you work, which sounds safe and means the thing on disk can be an hour behind the thing on screen — precisely when it matters, because the reason to want autosave is usually a tool watching the file.
40+
41+**After a pause in typing** is what both other editors and this one settled on. Two seconds is long enough that a pause for thought is not a write, short enough that a rebuild follows a change closely. A run of typing is one write, not one per keystroke.
42+
43+There is one deadline for the whole editor rather than one per window, because "you stopped typing" is one event. A per-window deadline would save the file you have moved away from at a different moment from the one in front of you, which nobody could observe and which is more state to keep right.
44+
45+## Why the deadline is checked, and only nudged by a timer
46+
47+This is the same trap the language-server announcement and the terminal redraws both hit, and it is worth stating once more because it will come up again.
48+
49+The editor blocks in `PollEvent`. To notice a deadline while nothing is happening, something has to wake it, and the only way to wake it from a timer is `PostEvent` — which **drops** events when its queue is full.
50+
51+So the timer is not what decides. The deadline is state, checked at the top of every turn of the event loop, exactly as `announceOpenDocuments` checks whether the language server is ready. The timer's only job is to make sure a turn happens. A nudge that gets dropped costs a save that is late until the next keystroke or click; a design where the timer did the saving would lose it altogether.
52+
53+## Why a failed automatic save does not open a dialog
54+
55+An autosave nobody asked for should not interrupt with a modal, and a modal that reappears every two seconds because a file is read-only is worse than the problem it reports. It goes on the status bar instead, and the deadline is cleared *before* the write is attempted, so a file that cannot be written is tried once per edit rather than forever.
56+
57+## Why TOML colouring reuses the Go classes
58+
59+Adding `syntax.tomlkey` and friends would have meant every theme — including the ones users have written — silently failing to colour TOML until it was updated.
60+
61+The classes already there fit: a table header names a structure, so it reads as a type; a key names a thing, so it reads as an identifier; `true` and `false` are constants because that is what they are. The result is that every theme that ever worked colours TOML correctly, with no change and no new keys. The scanner is hand-written for the same reason the terminal emulator is — TOML is a small, fully specified language, and it is one file against a third dependency.
62+
63+## How it relates to the rest
64+
65+- The exact keys and their defaults: [Project settings reference](../reference/project-settings.md)
66+- Setting one up: [How to give a project its own settings](../how-to/configure-a-project.md)
67+- The other TOML file the editor reads: [Theme file format](../reference/themes.md)
68+- Where `settings` sits among the packages: [Architecture](architecture.md)
added docs/en/explanation/project-tree.md +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+# Project tree — explanation
2+
3+## What is this about?
4+
5+`F9` opens a window listing the project's files, and pressing `Enter` on one opens it. This page is about the three decisions inside that: where the tree is rooted, why it is a window rather than a panel down the side, and why it does not notice files appearing on its own.
6+
7+## Why the root is the working directory
8+
9+The editor already contains two different answers to "what is the project".
10+
11+The language server walks up from the file you opened until it finds a `go.mod`, because a module has a real boundary — being inside one is a fact about the code, and gopls needs that exact directory to work in. The project settings file does not walk at all: `.turbo-go/settings.toml` is looked for in the working directory and nowhere else.
12+
13+The tree follows the settings file, and it is worth saying why the *other* rule was tempting. Walking up to the `go.mod` would mean that opening a file from anywhere in a project shows the whole project, which is what an explorer usually does. But it also means the tree's root depends on a file three directories away that you may not have thought about, and it stops being predictable the moment a repository holds more than one module — a monorepo would show you whichever module the file you happened to open belongs to.
14+
15+The rule kept is the one that can be said in a sentence and is true everywhere in the editor: **the project is the directory you started the editor in.** It costs something, and the cost is named in the [how-to](../how-to/browse-a-project.md): start from a subdirectory and you get a tree of that subdirectory. The answer is to start from the project root, which is where you would run `go build` and `git` anyway.
16+
17+## Why `.git` is hidden and nothing else is
18+
19+The Open dialog hides every entry beginning with a dot. Copying that here was the obvious thing and would have been wrong.
20+
21+`.turbo-go/settings.toml` is a file this editor asks people to edit — it is why the editor colours TOML at all. `.gitignore` and `.qlty/qlty.toml` are files of the project too. A tree that hid them would make the editor's own configuration unreachable from the editor's own file browser, which is an odd place to end up.
22+
23+`.git` is different in kind rather than in spelling: nothing inside it is meant to be opened by hand, and it holds enough objects to bury everything else in the listing. One name, hidden for a reason that can be stated. Respecting `.gitignore` as well was considered and turned down for now: it would hide `bin/` and `release/`, which is genuinely nicer, and it costs a gitignore pattern engine — negation, `**`, anchoring — that is a feature in its own right rather than a detail of a tree.
24+
25+## Why it is a window, not a panel
26+
27+Every other editor puts its file tree in a fixed strip down the left. That was the alternative, and it was turned down because of what it would have cost the rest of the editor.
28+
29+A docked panel means the desktop is no longer a single rectangle that windows live in. `Desktop` would need a notion of reserved edges; `Window.fitInto` and the grow modes would have to respect them; maximising would mean "the whole desktop except the panel"; tiling and cascading would need to know about it. That is a change to the foundation of the whole interface, for one widget.
30+
31+As an ordinary window, the tree gets everything for free and behaves like everything else: `F6` reaches it, `Alt-2` raises it, `[x]` closes it, `[■]` fills the desktop with it, **Window ▸ Tile** puts it beside your file. Nothing in `ui` had to change. If a docked panel is wanted later, it is a `ui` feature to be designed on its own terms rather than something smuggled in with a file browser.
32+
33+## Why there is only one
34+
35+Two trees on the same project would be two views of one thing with nothing to tell them apart, and the project cannot change while the editor runs — the root is fixed at start-up. So `F9` on an open tree raises it rather than making another, the same way opening a file that is already open raises its window.
36+
37+## Why it does not watch the disk
38+
39+A tree that noticed `go build` producing `bin/` would be better. Doing it properly means watching the filesystem, and in Go that means `fsnotify` — a third dependency, against a project that has kept to two since it started and treats adding one as a decision to be argued for.
40+
41+It is not a small dependency in behaviour either: recursive watches, watch descriptors running out on large trees, and different semantics on every platform, in a feature whose failure mode is a stale line in a list.
42+
43+So the tree re-reads on demand, and the editor picks the moments it can be sure about. Saving a file is one: the editor did it, so it knows. `F5` and `Ctrl-R` are the other, because a build in a terminal window is something only the user knows has finished. Refreshing keeps the shape of the tree and re-reads only the directories that were actually opened, so it costs what is on screen rather than a walk of the project.
44+
45+## Why the tree has theme keys of its own
46+
47+The obvious economy was to draw it with the `list.*` keys — a tree is a list, after all, and it would have meant no new keys for user themes to miss.
48+
49+It does not work, and the reason is worth recording. `list.selected` is coloured to stand out against a **dialog**. In `turbo-classic` it is white on navy, and `window.body` is silver on **navy** — a tree in a window would have highlighted its selected row in exactly the background colour it sits on. The selection would have been invisible in the theme the editor ships as its default.
50+
51+So `tree.text`, `tree.directory`, `tree.selected` and `tree.unfocused` exist, and a test holds every shipped theme to a minimum contrast between the first and the third, in the same way the cursor colours are checked. A user theme that sets none of them falls back along the dots to `default`: a readable tree without the file-and-directory distinction, rather than nothing at all.
52+
53+## How it relates to the rest
54+
55+- Every key and every rule, exactly: [Project tree reference](../reference/project-tree.md)
56+- Using it: [How to browse a project and open files from a tree](../how-to/browse-a-project.md)
57+- The other place "the project" is defined the same way: [Project settings](project-settings.md)
58+- Where `filetree` sits among the packages: [Architecture](architecture.md)
new file mode 100644
@@ -0,0 +1,58 @@
1+# Project tree — explanation
2+
3+## What is this about?
4+
5+`F9` opens a window listing the project's files, and pressing `Enter` on one opens it. This page is about the three decisions inside that: where the tree is rooted, why it is a window rather than a panel down the side, and why it does not notice files appearing on its own.
6+
7+## Why the root is the working directory
8+
9+The editor already contains two different answers to "what is the project".
10+
11+The language server walks up from the file you opened until it finds a `go.mod`, because a module has a real boundary — being inside one is a fact about the code, and gopls needs that exact directory to work in. The project settings file does not walk at all: `.turbo-go/settings.toml` is looked for in the working directory and nowhere else.
12+
13+The tree follows the settings file, and it is worth saying why the *other* rule was tempting. Walking up to the `go.mod` would mean that opening a file from anywhere in a project shows the whole project, which is what an explorer usually does. But it also means the tree's root depends on a file three directories away that you may not have thought about, and it stops being predictable the moment a repository holds more than one module — a monorepo would show you whichever module the file you happened to open belongs to.
14+
15+The rule kept is the one that can be said in a sentence and is true everywhere in the editor: **the project is the directory you started the editor in.** It costs something, and the cost is named in the [how-to](../how-to/browse-a-project.md): start from a subdirectory and you get a tree of that subdirectory. The answer is to start from the project root, which is where you would run `go build` and `git` anyway.
16+
17+## Why `.git` is hidden and nothing else is
18+
19+The Open dialog hides every entry beginning with a dot. Copying that here was the obvious thing and would have been wrong.
20+
21+`.turbo-go/settings.toml` is a file this editor asks people to edit — it is why the editor colours TOML at all. `.gitignore` and `.qlty/qlty.toml` are files of the project too. A tree that hid them would make the editor's own configuration unreachable from the editor's own file browser, which is an odd place to end up.
22+
23+`.git` is different in kind rather than in spelling: nothing inside it is meant to be opened by hand, and it holds enough objects to bury everything else in the listing. One name, hidden for a reason that can be stated. Respecting `.gitignore` as well was considered and turned down for now: it would hide `bin/` and `release/`, which is genuinely nicer, and it costs a gitignore pattern engine — negation, `**`, anchoring — that is a feature in its own right rather than a detail of a tree.
24+
25+## Why it is a window, not a panel
26+
27+Every other editor puts its file tree in a fixed strip down the left. That was the alternative, and it was turned down because of what it would have cost the rest of the editor.
28+
29+A docked panel means the desktop is no longer a single rectangle that windows live in. `Desktop` would need a notion of reserved edges; `Window.fitInto` and the grow modes would have to respect them; maximising would mean "the whole desktop except the panel"; tiling and cascading would need to know about it. That is a change to the foundation of the whole interface, for one widget.
30+
31+As an ordinary window, the tree gets everything for free and behaves like everything else: `F6` reaches it, `Alt-2` raises it, `[x]` closes it, `[■]` fills the desktop with it, **Window ▸ Tile** puts it beside your file. Nothing in `ui` had to change. If a docked panel is wanted later, it is a `ui` feature to be designed on its own terms rather than something smuggled in with a file browser.
32+
33+## Why there is only one
34+
35+Two trees on the same project would be two views of one thing with nothing to tell them apart, and the project cannot change while the editor runs — the root is fixed at start-up. So `F9` on an open tree raises it rather than making another, the same way opening a file that is already open raises its window.
36+
37+## Why it does not watch the disk
38+
39+A tree that noticed `go build` producing `bin/` would be better. Doing it properly means watching the filesystem, and in Go that means `fsnotify` — a third dependency, against a project that has kept to two since it started and treats adding one as a decision to be argued for.
40+
41+It is not a small dependency in behaviour either: recursive watches, watch descriptors running out on large trees, and different semantics on every platform, in a feature whose failure mode is a stale line in a list.
42+
43+So the tree re-reads on demand, and the editor picks the moments it can be sure about. Saving a file is one: the editor did it, so it knows. `F5` and `Ctrl-R` are the other, because a build in a terminal window is something only the user knows has finished. Refreshing keeps the shape of the tree and re-reads only the directories that were actually opened, so it costs what is on screen rather than a walk of the project.
44+
45+## Why the tree has theme keys of its own
46+
47+The obvious economy was to draw it with the `list.*` keys — a tree is a list, after all, and it would have meant no new keys for user themes to miss.
48+
49+It does not work, and the reason is worth recording. `list.selected` is coloured to stand out against a **dialog**. In `turbo-classic` it is white on navy, and `window.body` is silver on **navy** — a tree in a window would have highlighted its selected row in exactly the background colour it sits on. The selection would have been invisible in the theme the editor ships as its default.
50+
51+So `tree.text`, `tree.directory`, `tree.selected` and `tree.unfocused` exist, and a test holds every shipped theme to a minimum contrast between the first and the third, in the same way the cursor colours are checked. A user theme that sets none of them falls back along the dots to `default`: a readable tree without the file-and-directory distinction, rather than nothing at all.
52+
53+## How it relates to the rest
54+
55+- Every key and every rule, exactly: [Project tree reference](../reference/project-tree.md)
56+- Using it: [How to browse a project and open files from a tree](../how-to/browse-a-project.md)
57+- The other place "the project" is defined the same way: [Project settings](project-settings.md)
58+- Where `filetree` sits among the packages: [Architecture](architecture.md)
added docs/en/explanation/snippets.md +62 -0
new file mode 100644
@@ -0,0 +1,62 @@
1+# Snippets — explanation
2+
3+## What is this about?
4+
5+A **Snippets** menu whose contents come from a TOML file, and a chosen snippet dropped into the file you are editing. This page is about the three decisions that shape it: why the menu is rebuilt every time it opens, why the editor grew real submenus for it, and why insertion re-indents.
6+
7+## Why the menu is built at the moment it opens
8+
9+Every other menu in the editor is decided once, in `New()`. This one cannot be, and there are two independent reasons.
10+
11+The first is the file. Snippets live in TOML, and the whole point of that is that you edit it — often in this editor, in the window the **Create snippets file** item just opened for you. A menu built at start-up would show the state of the file when the editor launched, and you would have to restart to see a snippet you had just written. That is the kind of friction that stops people using a feature at all.
12+
13+The second is the front window. The menu is filtered by what you are editing, so it changes when you press `F6`. There is no start-up moment at which the answer exists.
14+
15+So `ui.Menu` grew an `OnOpen` field: a function the bar calls immediately before dropping a menu down, letting its owner refill `Items` first. It is the same upward-communication mechanism as everything else in this codebase — a function field, not an interface — and it runs at exactly the moment the contents are about to be seen and no more often.
16+
17+## Why the editor grew submenus
18+
19+`ui.MenuItem` had no nesting, and adding it was the largest single piece of this work: a second panel to place and draw, arrow keys that mean "further in" and "back out", the pointer opening a branch on hover and closing it on leaving, and a cascade that puts both panels away at once.
20+
21+The alternative was one flat panel with the groups as greyed-out captions between separators. It works, needs nothing new, and falls over on the case the feature is for: a project with thirty snippets gives a menu taller than the terminal. Grouping that only labels rather than folds does not solve the problem it appears to solve.
22+
23+It is deliberately **one level deep**. The format is groups containing snippets — exactly one level — and a general depth would mean replacing the bar's two indices with a path, in the widget every dialog and every menu test already depends on. That is speculative work on the most load-bearing part of the interface.
24+
25+Two details of the submenu are worth naming because they were chosen rather than fallen into:
26+
27+- **Right and left are asymmetric with Escape.** Right opens a branch, or moves to the next menu when the item has none, so it always means "further in" wherever you are. Left steps *out* of a submenu to its parent, while Escape puts the whole menu away — because cancel should mean cancel from anywhere.
28+- **The panel flips left, and is also capped to the screen.** A submenu that would run off the right edge is drawn on the other side of its parent instead. Flipping alone is not enough: a panel wider than the terminal cannot be made to fit by moving it, so the width is capped too and long labels are clipped by the painter. A frame with no right-hand edge looks broken in a way a truncated label does not.
29+
30+## Why insertion re-indents
31+
32+A snippet is text, and the obvious implementation is to insert it. That is right for a one-liner and wrong for everything else, which is most of what people keep in snippets.
33+
34+Dropped in verbatim, a multi-line body restarts at column zero. Inserted inside a function, inside a loop, inside a `switch` — which is where you insert an `if err != nil` — the result is text that no formatter, no compiler and no reader is happy with, and the first thing you do is re-indent it by hand. A feature whose output needs fixing every time is not saving anyone anything.
35+
36+So the lines after the first get the leading whitespace of the line the cursor was on. That copies whatever the file already uses — tabs or spaces, however many — rather than imposing a choice, which matters in a project with a mixed history.
37+
38+Two smaller decisions inside that:
39+
40+- **A blank line in the body stays blank.** Padding it to the indent would put trailing whitespace in, which every formatter then strips — noise in the diff of the very next save.
41+- **It is one undo step.** A snippet is one action to the person who chose it, so `Ctrl-Z` should take all of it back. This falls out of doing the whole insertion in a single `ReplaceRange`, which is the rule the buffer already enforces for every other edit.
42+
43+Placeholders and tab stops — `${1:name}` and moving between them — were considered and left out. They are a second feature with their own state to keep across edits, and the thing being asked for was reusable text.
44+
45+## Why two files, and why the project wins
46+
47+Your own snippets belong to you and should follow you between projects; a project's belong to the project and should arrive with a checkout. Neither is the whole answer, so both are read.
48+
49+Where a name clashes in the same group, the project's replaces yours. It is the more specific of the two statements, and it is the one a team agreed on — the same reason a `-theme` flag beats a project's setting while a project's setting beats the built-in default.
50+
51+## Why an unreadable file is loud
52+
53+A typo in TOML could drop every snippet silently and leave a menu with nothing but **Create snippets file** — which looks exactly like a project that has no snippets, and sends you to create a file you already have.
54+
55+So the menu shows a greyed-out `Cannot read snippets` where the groups would be. It cannot be chosen, it is where you were looking, and the create item is still below it so there is a way forward either way.
56+
57+## How it relates to the rest
58+
59+- Every key and every rule: [Snippets reference](../reference/snippets.md)
60+- Setting them up: [How to insert snippets from a menu](../how-to/use-snippets.md)
61+- The other file in the same directory: [Project settings](project-settings.md)
62+- The language names `languages` uses: [Languages coloured](../reference/languages.md)
new file mode 100644
@@ -0,0 +1,62 @@
1+# Snippets — explanation
2+
3+## What is this about?
4+
5+A **Snippets** menu whose contents come from a TOML file, and a chosen snippet dropped into the file you are editing. This page is about the three decisions that shape it: why the menu is rebuilt every time it opens, why the editor grew real submenus for it, and why insertion re-indents.
6+
7+## Why the menu is built at the moment it opens
8+
9+Every other menu in the editor is decided once, in `New()`. This one cannot be, and there are two independent reasons.
10+
11+The first is the file. Snippets live in TOML, and the whole point of that is that you edit it — often in this editor, in the window the **Create snippets file** item just opened for you. A menu built at start-up would show the state of the file when the editor launched, and you would have to restart to see a snippet you had just written. That is the kind of friction that stops people using a feature at all.
12+
13+The second is the front window. The menu is filtered by what you are editing, so it changes when you press `F6`. There is no start-up moment at which the answer exists.
14+
15+So `ui.Menu` grew an `OnOpen` field: a function the bar calls immediately before dropping a menu down, letting its owner refill `Items` first. It is the same upward-communication mechanism as everything else in this codebase — a function field, not an interface — and it runs at exactly the moment the contents are about to be seen and no more often.
16+
17+## Why the editor grew submenus
18+
19+`ui.MenuItem` had no nesting, and adding it was the largest single piece of this work: a second panel to place and draw, arrow keys that mean "further in" and "back out", the pointer opening a branch on hover and closing it on leaving, and a cascade that puts both panels away at once.
20+
21+The alternative was one flat panel with the groups as greyed-out captions between separators. It works, needs nothing new, and falls over on the case the feature is for: a project with thirty snippets gives a menu taller than the terminal. Grouping that only labels rather than folds does not solve the problem it appears to solve.
22+
23+It is deliberately **one level deep**. The format is groups containing snippets — exactly one level — and a general depth would mean replacing the bar's two indices with a path, in the widget every dialog and every menu test already depends on. That is speculative work on the most load-bearing part of the interface.
24+
25+Two details of the submenu are worth naming because they were chosen rather than fallen into:
26+
27+- **Right and left are asymmetric with Escape.** Right opens a branch, or moves to the next menu when the item has none, so it always means "further in" wherever you are. Left steps *out* of a submenu to its parent, while Escape puts the whole menu away — because cancel should mean cancel from anywhere.
28+- **The panel flips left, and is also capped to the screen.** A submenu that would run off the right edge is drawn on the other side of its parent instead. Flipping alone is not enough: a panel wider than the terminal cannot be made to fit by moving it, so the width is capped too and long labels are clipped by the painter. A frame with no right-hand edge looks broken in a way a truncated label does not.
29+
30+## Why insertion re-indents
31+
32+A snippet is text, and the obvious implementation is to insert it. That is right for a one-liner and wrong for everything else, which is most of what people keep in snippets.
33+
34+Dropped in verbatim, a multi-line body restarts at column zero. Inserted inside a function, inside a loop, inside a `switch` — which is where you insert an `if err != nil` — the result is text that no formatter, no compiler and no reader is happy with, and the first thing you do is re-indent it by hand. A feature whose output needs fixing every time is not saving anyone anything.
35+
36+So the lines after the first get the leading whitespace of the line the cursor was on. That copies whatever the file already uses — tabs or spaces, however many — rather than imposing a choice, which matters in a project with a mixed history.
37+
38+Two smaller decisions inside that:
39+
40+- **A blank line in the body stays blank.** Padding it to the indent would put trailing whitespace in, which every formatter then strips — noise in the diff of the very next save.
41+- **It is one undo step.** A snippet is one action to the person who chose it, so `Ctrl-Z` should take all of it back. This falls out of doing the whole insertion in a single `ReplaceRange`, which is the rule the buffer already enforces for every other edit.
42+
43+Placeholders and tab stops — `${1:name}` and moving between them — were considered and left out. They are a second feature with their own state to keep across edits, and the thing being asked for was reusable text.
44+
45+## Why two files, and why the project wins
46+
47+Your own snippets belong to you and should follow you between projects; a project's belong to the project and should arrive with a checkout. Neither is the whole answer, so both are read.
48+
49+Where a name clashes in the same group, the project's replaces yours. It is the more specific of the two statements, and it is the one a team agreed on — the same reason a `-theme` flag beats a project's setting while a project's setting beats the built-in default.
50+
51+## Why an unreadable file is loud
52+
53+A typo in TOML could drop every snippet silently and leave a menu with nothing but **Create snippets file** — which looks exactly like a project that has no snippets, and sends you to create a file you already have.
54+
55+So the menu shows a greyed-out `Cannot read snippets` where the groups would be. It cannot be chosen, it is where you were looking, and the create item is still below it so there is a way forward either way.
56+
57+## How it relates to the rest
58+
59+- Every key and every rule: [Snippets reference](../reference/snippets.md)
60+- Setting them up: [How to insert snippets from a menu](../how-to/use-snippets.md)
61+- The other file in the same directory: [Project settings](project-settings.md)
62+- The language names `languages` uses: [Languages coloured](../reference/languages.md)
added docs/en/explanation/terminal-windows.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# 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+
9+The 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+
11+A program behaves differently when its output is a pipe rather than a terminal. `go test` drops its colours. `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.
12+
13+So 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+
15+The 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+
19+Go 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+
21+The thing being weighed is not "emulator" against "no emulator" but against *how much* emulator. What a shell, `go test`, `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+
23+Compare 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+
25+So 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+
29+This is the decision with the most consequence for how the editor feels, and the first version got it wrong.
30+
31+The 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+
33+The 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+
37+Those 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+
39+What 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+
43+Closing a modified file asks whether to save it. Closing a terminal does not ask anything at all, and that asymmetry is deliberate.
44+
45+A 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+
47+Leaving 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+
51+The 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+
53+A 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+
55+So 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+
59+Pseudo-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+
61+Three 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+
63+The 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)
new file mode 100644
@@ -0,0 +1,72 @@
1+# 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+
9+The 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+
11+A program behaves differently when its output is a pipe rather than a terminal. `go test` drops its colours. `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.
12+
13+So 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+
15+The 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+
19+Go 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+
21+The thing being weighed is not "emulator" against "no emulator" but against *how much* emulator. What a shell, `go test`, `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+
23+Compare 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+
25+So 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+
29+This is the decision with the most consequence for how the editor feels, and the first version got it wrong.
30+
31+The 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+
33+The 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+
37+Those 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+
39+What 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+
43+Closing a modified file asks whether to save it. Closing a terminal does not ask anything at all, and that asymmetry is deliberate.
44+
45+A 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+
47+Leaving 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+
51+The 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+
53+A 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+
55+So 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+
59+Pseudo-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+
61+Three 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+
63+The 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)
added docs/en/how-to/ask-about-code.md +69 -0
new file mode 100644
@@ -0,0 +1,69 @@
1+# How to ask what the code means
2+
3+This guide shows how to follow a name through a project: where it is declared, what implements it, everywhere it is used, and what is wrong with it. It assumes Turbo Go is installed and a language server is running — the status bar says `LSP: ready` when it is.
4+
5+For moving around a file — searching, jumping to a line, switching windows — see [How to move around a file](navigate-code.md) instead.
6+
7+## Put the cursor on a name
8+
9+Any character of it will do. Every question below asks about the **position of the cursor**, not about a selection, so there is nothing to highlight first.
10+
11+## Ask
12+
13+| To find | Do | Shortcut |
14+| --- | --- | --- |
15+| What it is | **Code ▸ Describe symbol** | `F1` |
16+| Where it is declared | **Code ▸ Go to definition** | `F12` |
17+| Where its *type* is declared | **Code ▸ Go to type definition** | |
18+| What implements it | **Code ▸ Find implementations…** | |
19+| Everywhere it is used | **Code ▸ Find references…** | `Shift-F12` |
20+
21+One answer takes you straight there. Several open a list showing each file, its line, and the text of that line:
22+
23+```
24+Implementations (2)
25+ main.go:7 type French struct{}
26+ main.go:11 type English struct{}
27+```
28+
29+Move with the arrow keys, `Enter` to go, `Esc` to stay where you are.
30+
31+## When nothing comes back
32+
33+Three different things look alike, and the status bar tells them apart:
34+
35+| It says | Meaning |
36+| --- | --- |
37+| `No references found` | The server answered, and there are none |
38+| Anything else, such as `Loading…` | The server has not finished indexing. Wait a moment and ask again. |
39+| `LSP: off` on the status bar | No server is running. See [How to enable completion](enable-completion.md). |
40+
41+The middle one is worth knowing: a server still indexing answers every question with nothing, and that is indistinguishable from a real answer unless the editor says so.
42+
43+## Find something by name instead
44+
45+- **Code ▸ Symbol in file…** lists what the file in front declares, indented, with each symbol's kind — an outline you can walk.
46+- **Code ▸ Symbol in project…** (`Ctrl-T`) asks for a name and searches everywhere. What counts as a match is the server's decision; gopls matches loosely, so a few letters usually do.
47+
48+## See what is wrong
49+
50+**Code ▸ Problems…** lists every problem the server has reported, for **every file it has loaded** — usually more than the one you are editing. Choosing one goes to the line.
51+
52+Lines with a problem carry a mark in the gutter, beside the line number:
53+
54+| Mark | Meaning |
55+| --- | --- |
56+| `×` | An error |
57+| `!` | A warning |
58+| `i` | Information |
59+| `·` | A hint |
60+
61+A line with more than one problem shows the worst of them.
62+
63+**The marks need the line numbers.** They sit in the column that separates the numbers from the text, so hiding the gutter with **Options ▸ Line numbers** hides them too.
64+
65+## See also
66+
67+- Every item and its key: [Menus](../reference/menus.md)
68+- Getting a server running: [How to enable completion](enable-completion.md)
69+- What the editor asks, and why: [Colouring and completion](../explanation/colouring-and-completion.md)
new file mode 100644
@@ -0,0 +1,69 @@
1+# How to ask what the code means
2+
3+This guide shows how to follow a name through a project: where it is declared, what implements it, everywhere it is used, and what is wrong with it. It assumes Turbo Go is installed and a language server is running — the status bar says `LSP: ready` when it is.
4+
5+For moving around a file — searching, jumping to a line, switching windows — see [How to move around a file](navigate-code.md) instead.
6+
7+## Put the cursor on a name
8+
9+Any character of it will do. Every question below asks about the **position of the cursor**, not about a selection, so there is nothing to highlight first.
10+
11+## Ask
12+
13+| To find | Do | Shortcut |
14+| --- | --- | --- |
15+| What it is | **Code ▸ Describe symbol** | `F1` |
16+| Where it is declared | **Code ▸ Go to definition** | `F12` |
17+| Where its *type* is declared | **Code ▸ Go to type definition** | |
18+| What implements it | **Code ▸ Find implementations…** | |
19+| Everywhere it is used | **Code ▸ Find references…** | `Shift-F12` |
20+
21+One answer takes you straight there. Several open a list showing each file, its line, and the text of that line:
22+
23+```
24+Implementations (2)
25+ main.go:7 type French struct{}
26+ main.go:11 type English struct{}
27+```
28+
29+Move with the arrow keys, `Enter` to go, `Esc` to stay where you are.
30+
31+## When nothing comes back
32+
33+Three different things look alike, and the status bar tells them apart:
34+
35+| It says | Meaning |
36+| --- | --- |
37+| `No references found` | The server answered, and there are none |
38+| Anything else, such as `Loading…` | The server has not finished indexing. Wait a moment and ask again. |
39+| `LSP: off` on the status bar | No server is running. See [How to enable completion](enable-completion.md). |
40+
41+The middle one is worth knowing: a server still indexing answers every question with nothing, and that is indistinguishable from a real answer unless the editor says so.
42+
43+## Find something by name instead
44+
45+- **Code ▸ Symbol in file…** lists what the file in front declares, indented, with each symbol's kind — an outline you can walk.
46+- **Code ▸ Symbol in project…** (`Ctrl-T`) asks for a name and searches everywhere. What counts as a match is the server's decision; gopls matches loosely, so a few letters usually do.
47+
48+## See what is wrong
49+
50+**Code ▸ Problems…** lists every problem the server has reported, for **every file it has loaded** — usually more than the one you are editing. Choosing one goes to the line.
51+
52+Lines with a problem carry a mark in the gutter, beside the line number:
53+
54+| Mark | Meaning |
55+| --- | --- |
56+| `×` | An error |
57+| `!` | A warning |
58+| `i` | Information |
59+| `·` | A hint |
60+
61+A line with more than one problem shows the worst of them.
62+
63+**The marks need the line numbers.** They sit in the column that separates the numbers from the text, so hiding the gutter with **Options ▸ Line numbers** hides them too.
64+
65+## See also
66+
67+- Every item and its key: [Menus](../reference/menus.md)
68+- Getting a server running: [How to enable completion](enable-completion.md)
69+- What the editor asks, and why: [Colouring and completion](../explanation/colouring-and-completion.md)
added docs/en/how-to/browse-a-project.md +70 -0
new file mode 100644
@@ -0,0 +1,70 @@
1+# How to browse a project and open files from a tree
2+
3+This guide shows how to open the project tree, walk it, and open a file from it. It assumes Turbo Go is already installed.
4+
5+## Open the tree
6+
7+Start the editor **from the project's own directory**, then press `F9`, or choose **Window ▸ Project tree**.
8+
9+A window opens showing the project's files, named after the directory the editor was started in:
10+
11+```
12+╔═[x]════════════ turbo-go ════════════2═[■]╗
13+║ ▶ .turbo-go ║
14+║ ▼ internal ║
15+║ ▶ app ║
16+║ ▼ ui ║
17+║ window.go ║
18+║ .gitignore ║
19+║ go.mod ║
20+║ main.go ║
21+╚══════════════════════════════════════════╝
22+```
23+
24+Directories come first, then files, each group sorted. `.git` is the only thing hidden — `.turbo-go`, `.gitignore` and the rest are files of your project, and you may well want to open them.
25+
26+Pressing `F9` again brings that window forward rather than opening a second tree.
27+
28+## Walk it
29+
30+| Key | Effect |
31+| --- | --- |
32+| `↑` `↓` | Move the highlight |
33+| `→` | Open a closed directory; on anything else, step to the next row |
34+| `←` | Close an open directory; on anything else, step out to the directory it is in |
35+| `Enter` | Open a file, or open and close a directory |
36+| `Home` `End` | First / last row |
37+| `PgUp` `PgDn` | A screenful at a time |
38+
39+A directory is read the first time you open it, so a tree on a large project costs one listing rather than a walk of everything.
40+
41+## Open a file
42+
43+Put the highlight on it and press `Enter`, or click it twice.
44+
45+The file opens in a window of its own, in front of the tree. A file that is already open is brought forward rather than opened twice.
46+
47+## See a file that was made after you opened the tree
48+
49+The tree does not watch the disk. Press **`F5`** or **`Ctrl-R`** with the tree focused, and it re-reads the project — keeping open whatever you had open, and keeping the highlight on the same entry.
50+
51+Saving a file refreshes the tree for you, so a **File ▸ Save as** into a new name shows up without asking. A file created another way — `go build` in a terminal window, or `git checkout` — needs the refresh key.
52+
53+## Work with the tree and a file side by side
54+
55+The tree is an ordinary window, so all the window commands apply:
56+
57+- **Window ▸ Tile** puts the tree and your file side by side.
58+- Drag its bottom-right corner to make it narrower once you know your way around.
59+- `[x]` closes it; `F9` brings it back.
60+
61+## Variants
62+
63+- **You started the editor from a subdirectory.** The tree is rooted there, showing only that part of the project. Start from the project's own directory instead — the same rule `.turbo-go/settings.toml` follows.
64+- **A directory shows as open but empty.** It could not be read, most often a permissions problem. The rest of the tree is unaffected; fix the permissions and press `F5`.
65+
66+## See also
67+
68+- Every key and what the tree shows, exactly: [Project tree reference](../reference/project-tree.md)
69+- Why it is a window rather than a docked panel, and why it does not watch the disk: [Project tree](../explanation/project-tree.md)
70+- Colouring it: [Theme file format](../reference/themes.md)
new file mode 100644
@@ -0,0 +1,70 @@
1+# How to browse a project and open files from a tree
2+
3+This guide shows how to open the project tree, walk it, and open a file from it. It assumes Turbo Go is already installed.
4+
5+## Open the tree
6+
7+Start the editor **from the project's own directory**, then press `F9`, or choose **Window ▸ Project tree**.
8+
9+A window opens showing the project's files, named after the directory the editor was started in:
10+
11+```
12+╔═[x]════════════ turbo-go ════════════2═[■]╗
13+║ ▶ .turbo-go ║
14+║ ▼ internal ║
15+║ ▶ app ║
16+║ ▼ ui ║
17+║ window.go ║
18+║ .gitignore ║
19+║ go.mod ║
20+║ main.go ║
21+╚══════════════════════════════════════════╝
22+```
23+
24+Directories come first, then files, each group sorted. `.git` is the only thing hidden — `.turbo-go`, `.gitignore` and the rest are files of your project, and you may well want to open them.
25+
26+Pressing `F9` again brings that window forward rather than opening a second tree.
27+
28+## Walk it
29+
30+| Key | Effect |
31+| --- | --- |
32+| `↑` `↓` | Move the highlight |
33+| `→` | Open a closed directory; on anything else, step to the next row |
34+| `←` | Close an open directory; on anything else, step out to the directory it is in |
35+| `Enter` | Open a file, or open and close a directory |
36+| `Home` `End` | First / last row |
37+| `PgUp` `PgDn` | A screenful at a time |
38+
39+A directory is read the first time you open it, so a tree on a large project costs one listing rather than a walk of everything.
40+
41+## Open a file
42+
43+Put the highlight on it and press `Enter`, or click it twice.
44+
45+The file opens in a window of its own, in front of the tree. A file that is already open is brought forward rather than opened twice.
46+
47+## See a file that was made after you opened the tree
48+
49+The tree does not watch the disk. Press **`F5`** or **`Ctrl-R`** with the tree focused, and it re-reads the project — keeping open whatever you had open, and keeping the highlight on the same entry.
50+
51+Saving a file refreshes the tree for you, so a **File ▸ Save as** into a new name shows up without asking. A file created another way — `go build` in a terminal window, or `git checkout` — needs the refresh key.
52+
53+## Work with the tree and a file side by side
54+
55+The tree is an ordinary window, so all the window commands apply:
56+
57+- **Window ▸ Tile** puts the tree and your file side by side.
58+- Drag its bottom-right corner to make it narrower once you know your way around.
59+- `[x]` closes it; `F9` brings it back.
60+
61+## Variants
62+
63+- **You started the editor from a subdirectory.** The tree is rooted there, showing only that part of the project. Start from the project's own directory instead — the same rule `.turbo-go/settings.toml` follows.
64+- **A directory shows as open but empty.** It could not be read, most often a permissions problem. The rest of the tree is unaffected; fix the permissions and press `F5`.
65+
66+## See also
67+
68+- Every key and what the tree shows, exactly: [Project tree reference](../reference/project-tree.md)
69+- Why it is a window rather than a docked panel, and why it does not watch the disk: [Project tree](../explanation/project-tree.md)
70+- Colouring it: [Theme file format](../reference/themes.md)
added docs/en/how-to/configure-a-project.md +83 -0
new file mode 100644
@@ -0,0 +1,83 @@
1+# How to give a project its own settings
2+
3+This guide shows how to pin a theme and turn on automatic saving for one project, so everyone who opens it gets the same editor. It assumes you already have Turbo Go installed.
4+
5+## Create the settings file
6+
7+Start the editor **from the project's own directory**, then choose **Options ▸ Create project settings**.
8+
9+That writes `.turbo-go/settings.toml`, filled in with the theme you are using right now, and opens it for editing — coloured, because Turbo Go colours TOML:
10+
11+```toml
12+# turbo-go project settings.
13+#
14+# These apply to everyone who opens this project in turbo-go. Delete this file
15+# and the editor falls back to its own defaults.
16+
17+[editor]
18+
19+# The colour theme to start in. `turbo-go -list-themes` lists them all.
20+# A -theme flag on the command line overrides this.
21+theme = "turbo-classic"
22+
23+# Write modified files by themselves, a short while after you stop typing.
24+# On, because a project that has gone to the trouble of having a settings file
25+# has said what it wants; set it to false and save, and it stops at once.
26+autosave = true
27+
28+# How long that while is. Any Go duration: "500ms", "2s", "1m".
29+autosave_delay = "2s"
30+```
31+
32+The file is read when the editor starts, and **again every time you save it** — so a change is in force the moment you press `F2`. The status bar confirms it: `Applied .turbo-go/settings.toml — autosave on (2s)`.
33+
34+That covers the settings this file holds, not the theme: **Options ▸ Theme** is the live way to change that, and writes your choice back here for you.
35+
36+The menu item you just used is now greyed out, and **Options ▸ Project settings…** beside it is not. That is the rule for all three of the project's files: you can create the one you have not got, and open the one you have.
37+
38+## Automatic saving
39+
40+It is already on: the file you were just given says `autosave = true`.
41+
42+Any file with a name is written two seconds after you stop typing. The status bar says `Saved main.go` when it happens. Nothing is written while you are still typing — each keystroke pushes the wait out again.
43+
44+Two things change as a consequence, both on purpose:
45+
46+- **Closing a window stops asking** whether to save. It was going to be saved anyway.
47+- **Leaving the editor stops asking** too, for the same reason.
48+
49+A file that has never been named is the exception: automatic saving never opens a dialog, so an untitled window keeps its `*` and is still asked about when you close it.
50+
51+To turn it off, set `autosave` to `false` and save; the status bar answers `autosave off`, and it stops there and then. To wait longer or less, change `autosave_delay`:
52+
53+```toml
54+autosave_delay = "500ms"
55+```
56+
57+## Pin the theme
58+
59+Set `theme` to any name from `turbo-go -list-themes`, or simply pick one with **Options ▸ Theme** — with a settings file present, choosing a theme writes it into the file for you, keeping your comments and layout as they were.
60+
61+## Try a different theme without changing the file
62+
63+Pass `-theme` on the command line. It wins over the project's choice for that run only:
64+
65+```bash
66+turbo-go -theme turbo-dark main.go
67+```
68+
69+## Edit the file later
70+
71+**Options ▸ Project settings…** opens it again. It is greyed out in a project that has none.
72+
73+## Variants
74+
75+- **You start the editor from a subdirectory.** The settings are not found: only `./.turbo-go` is looked at, with no walk up towards the project root. Start from the project's own directory, or pass `-theme` for that run.
76+- **The file has a mistake in it.** The editor says so on standard error and opens with its defaults, so you can fix the file in the editor itself.
77+- **You share the project.** `.turbo-go/settings.toml` is an ordinary file; commit it to agree on a theme across a team, or add it to `.gitignore` to keep it to yourself.
78+
79+## See also
80+
81+- Every key, with its type and default: [Project settings reference](../reference/project-settings.md)
82+- Why the file is not searched for in parent directories, and why autosave waits: [Project settings](../explanation/project-settings.md)
83+- Writing a theme to pin: [How to write your own theme](write-a-theme.md)
new file mode 100644
@@ -0,0 +1,83 @@
1+# How to give a project its own settings
2+
3+This guide shows how to pin a theme and turn on automatic saving for one project, so everyone who opens it gets the same editor. It assumes you already have Turbo Go installed.
4+
5+## Create the settings file
6+
7+Start the editor **from the project's own directory**, then choose **Options ▸ Create project settings**.
8+
9+That writes `.turbo-go/settings.toml`, filled in with the theme you are using right now, and opens it for editing — coloured, because Turbo Go colours TOML:
10+
11+```toml
12+# turbo-go project settings.
13+#
14+# These apply to everyone who opens this project in turbo-go. Delete this file
15+# and the editor falls back to its own defaults.
16+
17+[editor]
18+
19+# The colour theme to start in. `turbo-go -list-themes` lists them all.
20+# A -theme flag on the command line overrides this.
21+theme = "turbo-classic"
22+
23+# Write modified files by themselves, a short while after you stop typing.
24+# On, because a project that has gone to the trouble of having a settings file
25+# has said what it wants; set it to false and save, and it stops at once.
26+autosave = true
27+
28+# How long that while is. Any Go duration: "500ms", "2s", "1m".
29+autosave_delay = "2s"
30+```
31+
32+The file is read when the editor starts, and **again every time you save it** — so a change is in force the moment you press `F2`. The status bar confirms it: `Applied .turbo-go/settings.toml — autosave on (2s)`.
33+
34+That covers the settings this file holds, not the theme: **Options ▸ Theme** is the live way to change that, and writes your choice back here for you.
35+
36+The menu item you just used is now greyed out, and **Options ▸ Project settings…** beside it is not. That is the rule for all three of the project's files: you can create the one you have not got, and open the one you have.
37+
38+## Automatic saving
39+
40+It is already on: the file you were just given says `autosave = true`.
41+
42+Any file with a name is written two seconds after you stop typing. The status bar says `Saved main.go` when it happens. Nothing is written while you are still typing — each keystroke pushes the wait out again.
43+
44+Two things change as a consequence, both on purpose:
45+
46+- **Closing a window stops asking** whether to save. It was going to be saved anyway.
47+- **Leaving the editor stops asking** too, for the same reason.
48+
49+A file that has never been named is the exception: automatic saving never opens a dialog, so an untitled window keeps its `*` and is still asked about when you close it.
50+
51+To turn it off, set `autosave` to `false` and save; the status bar answers `autosave off`, and it stops there and then. To wait longer or less, change `autosave_delay`:
52+
53+```toml
54+autosave_delay = "500ms"
55+```
56+
57+## Pin the theme
58+
59+Set `theme` to any name from `turbo-go -list-themes`, or simply pick one with **Options ▸ Theme** — with a settings file present, choosing a theme writes it into the file for you, keeping your comments and layout as they were.
60+
61+## Try a different theme without changing the file
62+
63+Pass `-theme` on the command line. It wins over the project's choice for that run only:
64+
65+```bash
66+turbo-go -theme turbo-dark main.go
67+```
68+
69+## Edit the file later
70+
71+**Options ▸ Project settings…** opens it again. It is greyed out in a project that has none.
72+
73+## Variants
74+
75+- **You start the editor from a subdirectory.** The settings are not found: only `./.turbo-go` is looked at, with no walk up towards the project root. Start from the project's own directory, or pass `-theme` for that run.
76+- **The file has a mistake in it.** The editor says so on standard error and opens with its defaults, so you can fix the file in the editor itself.
77+- **You share the project.** `.turbo-go/settings.toml` is an ordinary file; commit it to agree on a theme across a team, or add it to `.gitignore` to keep it to yourself.
78+
79+## See also
80+
81+- Every key, with its type and default: [Project settings reference](../reference/project-settings.md)
82+- Why the file is not searched for in parent directories, and why autosave waits: [Project settings](../explanation/project-settings.md)
83+- Writing a theme to pin: [How to write your own theme](write-a-theme.md)
added docs/en/how-to/enable-completion.md +95 -0
new file mode 100644
@@ -0,0 +1,95 @@
1+# How to enable Go completion
2+
3+This guide shows how to get completion, hovers and go-to-definition working. It assumes Turbo Go is already installed and that you know what a Go module is.
4+
5+Completion comes from **gopls**, the official Go language server. Turbo Go does not bundle it: editing and colouring work without it, and only completion is lost.
6+
7+## 1. Install gopls
8+
9+```bash
10+go install golang.org/x/tools/gopls@latest
11+```
12+
13+## 2. Make sure Turbo Go can find it
14+
15+Turbo Go looks on `PATH` first, then in `$GOBIN`, then in `$GOPATH/bin`. `go install` writes to the last of those, which is very often not on `PATH` — so this usually works without any further step. Check:
16+
17+```bash
18+gopls version
19+```
20+
21+If that says "command not found" but Turbo Go still finds it, that is expected and fine.
22+
23+## 3. Open a file inside a module
24+
25+```bash
26+cd /path/to/your/module # the directory holding go.mod
27+turbo-go main.go
28+```
29+
30+Turbo Go walks up from the file looking for `go.mod` and starts gopls in the directory it finds. **Outside a module, gopls has very little to say** — this is the most common reason completion appears not to work.
31+
32+## 4. Ask for a completion
33+
34+Put the cursor after a dot and press **Ctrl-Space**:
35+
36+```go
37+fmt.
38+```
39+
40+A list drops down under the cursor. Keep typing to narrow it, **↑ ↓** to walk it, **Enter** or **Tab** to accept, **Escape** to dismiss.
41+
42+Typing a `.` asks for a completion by itself, so most of the time you do not press anything.
43+
44+## Checking what the server is doing
45+
46+The right-hand end of the status bar shows the language server's state: `LSP: starting…`, `LSP: ready`, or why there is none. `Run ▸ Language server status` shows the same thing in a box.
47+
48+## Variants
49+
50+**You do not want a language server at all:**
51+
52+```bash
53+turbo-go -no-lsp main.go
54+```
55+
56+**Completion is dead in a window that started without a name.** An Untitled window has no file to announce to gopls until it is saved — press **F2** and give it a name ending in `.go`, somewhere under the module. From that save on, completion, hover and the error marks work in that window; there is no need to quit and relaunch.
57+
58+**Completion is empty in a file that does compile.** gopls needs the file's package to build. Check `go build ./...` first — a package that does not compile often yields nothing useful.
59+
60+**The first completion after opening a large module is slow.** gopls is loading the module graph. The status bar says `LSP: starting…` until it is ready; requests made before then are refused rather than queued.
61+
62+**A request takes too long.** Every request gives up after three seconds, so a stuck server slows the editor but never freezes it. The status bar reports the failure.
63+
64+**The list is empty in a file that does not compile.** gopls answers *nothing at all* — no error, an empty list — for a package it cannot load. A duplicate declaration or an unresolved import is enough. The editor now says which problem is in the way:
65+
66+```
67+No completions — this file does not compile: main redeclared in this block
68+```
69+
70+`Run ▸ Language server status` shows the same thing with the server's path, the workspace root, and whether it has been told about this file. Fix the package first — `go build ./...` is the quickest check.
71+
72+**Ctrl-Space does nothing.** tmux, screen and IDE terminals frequently claim `Ctrl-Space` before the editor sees it. Type a `.` instead, which asks for a completion by itself, or use `Run ▸ Completion`.
73+
74+## What else the server gives you
75+
76+Completion is the loudest thing it does and the least of what it knows. The same connection answers eight more questions, all of them in the **Code** menu and all of them about the symbol under the cursor — no selection needed.
77+
78+| Key | What it does |
79+| --- | --- |
80+| **Ctrl-Space** | Completion list |
81+| **F1** | Describe the symbol under the cursor |
82+| **F12** | Jump to where it is declared |
83+| **Shift-F12** | List everywhere it is used |
84+| **Ctrl-T** | Find a symbol by name anywhere in the project |
85+
86+And, without a key: *Go to type definition*, *Find implementations…*, *Symbol in file…* and *Problems…*.
87+
88+Problems it finds arrive unasked. The first error in the file you are editing appears on the right of the status bar, prefixed with `⚠`; every line with a problem gets a mark in the gutter (`×` for an error, `!` for a warning); and **Code ▸ Problems…** lists all of them, for every file the server has loaded.
89+
90+[How to ask what the code means](ask-about-code.md) walks through all of it.
91+
92+## See also
93+
94+- Why the server is optional: [Colouring and completion](../explanation/colouring-and-completion.md)
95+- Every key: [keyboard reference](../reference/keyboard.md)
new file mode 100644
@@ -0,0 +1,95 @@
1+# How to enable Go completion
2+
3+This guide shows how to get completion, hovers and go-to-definition working. It assumes Turbo Go is already installed and that you know what a Go module is.
4+
5+Completion comes from **gopls**, the official Go language server. Turbo Go does not bundle it: editing and colouring work without it, and only completion is lost.
6+
7+## 1. Install gopls
8+
9+```bash
10+go install golang.org/x/tools/gopls@latest
11+```
12+
13+## 2. Make sure Turbo Go can find it
14+
15+Turbo Go looks on `PATH` first, then in `$GOBIN`, then in `$GOPATH/bin`. `go install` writes to the last of those, which is very often not on `PATH` — so this usually works without any further step. Check:
16+
17+```bash
18+gopls version
19+```
20+
21+If that says "command not found" but Turbo Go still finds it, that is expected and fine.
22+
23+## 3. Open a file inside a module
24+
25+```bash
26+cd /path/to/your/module # the directory holding go.mod
27+turbo-go main.go
28+```
29+
30+Turbo Go walks up from the file looking for `go.mod` and starts gopls in the directory it finds. **Outside a module, gopls has very little to say** — this is the most common reason completion appears not to work.
31+
32+## 4. Ask for a completion
33+
34+Put the cursor after a dot and press **Ctrl-Space**:
35+
36+```go
37+fmt.
38+```
39+
40+A list drops down under the cursor. Keep typing to narrow it, **↑ ↓** to walk it, **Enter** or **Tab** to accept, **Escape** to dismiss.
41+
42+Typing a `.` asks for a completion by itself, so most of the time you do not press anything.
43+
44+## Checking what the server is doing
45+
46+The right-hand end of the status bar shows the language server's state: `LSP: starting…`, `LSP: ready`, or why there is none. `Run ▸ Language server status` shows the same thing in a box.
47+
48+## Variants
49+
50+**You do not want a language server at all:**
51+
52+```bash
53+turbo-go -no-lsp main.go
54+```
55+
56+**Completion is dead in a window that started without a name.** An Untitled window has no file to announce to gopls until it is saved — press **F2** and give it a name ending in `.go`, somewhere under the module. From that save on, completion, hover and the error marks work in that window; there is no need to quit and relaunch.
57+
58+**Completion is empty in a file that does compile.** gopls needs the file's package to build. Check `go build ./...` first — a package that does not compile often yields nothing useful.
59+
60+**The first completion after opening a large module is slow.** gopls is loading the module graph. The status bar says `LSP: starting…` until it is ready; requests made before then are refused rather than queued.
61+
62+**A request takes too long.** Every request gives up after three seconds, so a stuck server slows the editor but never freezes it. The status bar reports the failure.
63+
64+**The list is empty in a file that does not compile.** gopls answers *nothing at all* — no error, an empty list — for a package it cannot load. A duplicate declaration or an unresolved import is enough. The editor now says which problem is in the way:
65+
66+```
67+No completions — this file does not compile: main redeclared in this block
68+```
69+
70+`Run ▸ Language server status` shows the same thing with the server's path, the workspace root, and whether it has been told about this file. Fix the package first — `go build ./...` is the quickest check.
71+
72+**Ctrl-Space does nothing.** tmux, screen and IDE terminals frequently claim `Ctrl-Space` before the editor sees it. Type a `.` instead, which asks for a completion by itself, or use `Run ▸ Completion`.
73+
74+## What else the server gives you
75+
76+Completion is the loudest thing it does and the least of what it knows. The same connection answers eight more questions, all of them in the **Code** menu and all of them about the symbol under the cursor — no selection needed.
77+
78+| Key | What it does |
79+| --- | --- |
80+| **Ctrl-Space** | Completion list |
81+| **F1** | Describe the symbol under the cursor |
82+| **F12** | Jump to where it is declared |
83+| **Shift-F12** | List everywhere it is used |
84+| **Ctrl-T** | Find a symbol by name anywhere in the project |
85+
86+And, without a key: *Go to type definition*, *Find implementations…*, *Symbol in file…* and *Problems…*.
87+
88+Problems it finds arrive unasked. The first error in the file you are editing appears on the right of the status bar, prefixed with `⚠`; every line with a problem gets a mark in the gutter (`×` for an error, `!` for a warning); and **Code ▸ Problems…** lists all of them, for every file the server has loaded.
89+
90+[How to ask what the code means](ask-about-code.md) walks through all of it.
91+
92+## See also
93+
94+- Why the server is optional: [Colouring and completion](../explanation/colouring-and-completion.md)
95+- Every key: [keyboard reference](../reference/keyboard.md)
added docs/en/how-to/install.md +89 -0
new file mode 100644
@@ -0,0 +1,89 @@
1+# How to install and build Turbo Go
2+
3+This guide shows how to get a working `turbo-go` binary. It assumes you have Go 1.26 or later and can use a terminal.
4+
5+## The short way, from a checkout
6+
7+```bash
8+git clone ssh://git@rickub.com/turbo-editors/turbo-go.git
9+cd turbo-go
10+make install
11+```
12+
13+That builds the editor, puts it where your shell looks for commands, and tells you what it found: the Go version, where the binary went, whether that directory is on your `PATH`, and whether `gopls` is installed. The build goes to a temporary file first, so a failed build never replaces a working installation.
14+
15+Then, from any Go project:
16+
17+```bash
18+turbo-go main.go
19+```
20+
21+### Options
22+
23+```bash
24+scripts/install.sh --prefix ~/bin # install somewhere of your choosing
25+scripts/install.sh --with-gopls # install the language server too
26+scripts/install.sh --uninstall # remove it again (make uninstall)
27+scripts/install.sh --help
28+```
29+
30+Without `--prefix`, the editor goes where `go install` would put it: `$GOBIN`, or `$GOPATH/bin` when `GOBIN` is unset — usually `~/go/bin`.
31+
32+## Just build it, without installing
33+
34+```bash
35+make build
36+./bin/turbo-go main.go
37+```
38+
39+## From the module proxy, without a checkout
40+
41+```bash
42+go install rickub.com/turbo-editors/turbo-go@latest
43+```
44+
45+If the command is then "not found", the install directory is not on your `PATH`:
46+
47+```bash
48+export PATH="$PATH:$(go env GOPATH)/bin"
49+```
50+
51+## Check it works
52+
53+```bash
54+turbo-go -version
55+turbo-go -list-themes
56+```
57+
58+The first names the commit the binary was built from, which is what to quote in a bug report; [the version number](../reference/versioning.md) explains what each form means. The second prints the themes compiled into the binary and tells you where your own would go.
59+
60+## Variants
61+
62+- **You only want to run it once**: `go run rickub.com/turbo-editors/turbo-go@latest main.go`
63+- **You want the binary somewhere specific**: `go build -o /usr/local/bin/turbo-go .`
64+- **Your terminal has no true colour**: use `turbo-go -theme turbo-classic`, which is built from the sixteen ANSI colours only. `turbo-dark` and `borland-light` use 24-bit colours.
65+
66+## When something goes wrong
67+
68+**`the installed binary does not run`.** The installer prints whatever the system said just above that line — read it first, because it names the actual problem.
69+
70+The installer replaces the binary rather than writing over the one that is there, so a reinstall gives the file a fresh identity. That matters on macOS, which caches a binary's code signature against its inode: writing new bytes into the old inode leaves the cached signature describing something else, and the kernel then refuses to run a binary that built and installed perfectly. If you have an older copy installed by something that used `cp`, removing it first clears any such state:
71+
72+```bash
73+scripts/install.sh --uninstall
74+scripts/install.sh
75+```
76+
77+**`Go x.y or later is needed`.** The version comes from `go.mod`, so it cannot drift from what the code actually needs. Upgrade Go, or build from a tag that matches the toolchain you have.
78+
79+**`build failed; nothing was installed`.** Your existing installation is untouched — the build goes to a temporary file first. The compiler's own output is printed above the message.
80+
81+## Terminal requirements
82+
83+Turbo Go needs a terminal that reports its size and supports mouse reporting — every mainstream one does. It reads `TERM` through tcell; if the display is wrong, check that `TERM` matches your terminal (`xterm-256color` is a safe default).
84+
85+## See also
86+
87+- Every flag: [command line reference](../reference/cli.md)
88+- Getting completion working: [How to enable Go completion](enable-completion.md)
89+- A guided first session: [Your first file in Turbo Go](../tutorials/getting-started.md)
new file mode 100644
@@ -0,0 +1,89 @@
1+# How to install and build Turbo Go
2+
3+This guide shows how to get a working `turbo-go` binary. It assumes you have Go 1.26 or later and can use a terminal.
4+
5+## The short way, from a checkout
6+
7+```bash
8+git clone ssh://git@rickub.com/turbo-editors/turbo-go.git
9+cd turbo-go
10+make install
11+```
12+
13+That builds the editor, puts it where your shell looks for commands, and tells you what it found: the Go version, where the binary went, whether that directory is on your `PATH`, and whether `gopls` is installed. The build goes to a temporary file first, so a failed build never replaces a working installation.
14+
15+Then, from any Go project:
16+
17+```bash
18+turbo-go main.go
19+```
20+
21+### Options
22+
23+```bash
24+scripts/install.sh --prefix ~/bin # install somewhere of your choosing
25+scripts/install.sh --with-gopls # install the language server too
26+scripts/install.sh --uninstall # remove it again (make uninstall)
27+scripts/install.sh --help
28+```
29+
30+Without `--prefix`, the editor goes where `go install` would put it: `$GOBIN`, or `$GOPATH/bin` when `GOBIN` is unset — usually `~/go/bin`.
31+
32+## Just build it, without installing
33+
34+```bash
35+make build
36+./bin/turbo-go main.go
37+```
38+
39+## From the module proxy, without a checkout
40+
41+```bash
42+go install rickub.com/turbo-editors/turbo-go@latest
43+```
44+
45+If the command is then "not found", the install directory is not on your `PATH`:
46+
47+```bash
48+export PATH="$PATH:$(go env GOPATH)/bin"
49+```
50+
51+## Check it works
52+
53+```bash
54+turbo-go -version
55+turbo-go -list-themes
56+```
57+
58+The first names the commit the binary was built from, which is what to quote in a bug report; [the version number](../reference/versioning.md) explains what each form means. The second prints the themes compiled into the binary and tells you where your own would go.
59+
60+## Variants
61+
62+- **You only want to run it once**: `go run rickub.com/turbo-editors/turbo-go@latest main.go`
63+- **You want the binary somewhere specific**: `go build -o /usr/local/bin/turbo-go .`
64+- **Your terminal has no true colour**: use `turbo-go -theme turbo-classic`, which is built from the sixteen ANSI colours only. `turbo-dark` and `borland-light` use 24-bit colours.
65+
66+## When something goes wrong
67+
68+**`the installed binary does not run`.** The installer prints whatever the system said just above that line — read it first, because it names the actual problem.
69+
70+The installer replaces the binary rather than writing over the one that is there, so a reinstall gives the file a fresh identity. That matters on macOS, which caches a binary's code signature against its inode: writing new bytes into the old inode leaves the cached signature describing something else, and the kernel then refuses to run a binary that built and installed perfectly. If you have an older copy installed by something that used `cp`, removing it first clears any such state:
71+
72+```bash
73+scripts/install.sh --uninstall
74+scripts/install.sh
75+```
76+
77+**`Go x.y or later is needed`.** The version comes from `go.mod`, so it cannot drift from what the code actually needs. Upgrade Go, or build from a tag that matches the toolchain you have.
78+
79+**`build failed; nothing was installed`.** Your existing installation is untouched — the build goes to a temporary file first. The compiler's own output is printed above the message.
80+
81+## Terminal requirements
82+
83+Turbo Go needs a terminal that reports its size and supports mouse reporting — every mainstream one does. It reads `TERM` through tcell; if the display is wrong, check that `TERM` matches your terminal (`xterm-256color` is a safe default).
84+
85+## See also
86+
87+- Every flag: [command line reference](../reference/cli.md)
88+- Getting completion working: [How to enable Go completion](enable-completion.md)
89+- A guided first session: [Your first file in Turbo Go](../tutorials/getting-started.md)
added docs/en/how-to/make-a-release.md +103 -0
new file mode 100644
@@ -0,0 +1,103 @@
1+# How to make a release
2+
3+This guide shows how to cut a release so that the editor reports its own version correctly. It assumes you can push to the repository.
4+
5+## Check what you are about to release
6+
7+```sh
8+make version
9+```
10+
11+```
12+v0.1.0-14-g88a4c38 (88a4c38)
13+```
14+
15+Fourteen commits past `v0.1.0`. A `-dirty` on the end means you have uncommitted changes — commit or stash them first, or the release will carry that suffix for ever.
16+
17+## Tag it
18+
19+```sh
20+git tag -a v0.2.0 -m "v0.2.0"
21+git push origin v0.2.0
22+```
23+
24+The tag is what the version comes from, so it has to exist before you build anything you intend to hand out. Annotated (`-a`) rather than lightweight, because `git describe` prefers annotated tags.
25+
26+## Build the release binary
27+
28+```sh
29+make build
30+./bin/turbo-go -version
31+```
32+
33+```
34+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
35+```
36+
37+No `-14-g…` suffix: you are exactly on the tag. That is what tells you the tag took.
38+
39+## Check the About box
40+
41+Start the editor and press `Alt-H`, then `A`.
42+
43+```
44+Turbo Go 0.2.0
45+
46+A Turbo C-style editor for Go,
47+written in Go.
48+
49+Commit: 88a4c38
50+Built: 2026-08-31 18:04 UTC
51+Theme: Turbo Classic
52+```
53+
54+## Or use the scripts and let the workflow publish
55+
56+That is the way a release is actually cut. Put the version and its one-line description in `release.env` — it is gitignored, so CI never sees it:
57+
58+```sh
59+TAG="v1.0.0"
60+ABOUT="Turbo Go"
61+```
62+
63+Then run one script:
64+
65+```sh
66+./01-release.tag.sh
67+```
68+
69+It runs `make check`, refuses a tag already taken locally or on `origin`, refuses a `go.mod` carrying a `replace` directive, commits anything outstanding, pushes the branch, and only then tags and pushes the tag. That order matters: a tag pushed before the branch points at a commit the remote has never seen, and a tag created before a failed push is left behind for somebody to find.
70+
71+That is the last thing you run by hand. Pushing the tag starts `.github/workflows/release.yml`; follow it on the repository's Actions tab. It runs the suite, cross-compiles the binaries with `./02-build-releases.sh` — the same script you can run on your machine — and creates the release page with them: the tag's message, the `go install` line, links to the documentation **at that tag**, one binary per platform, the `SHA256SUMS` and the README that describes the downloads.
72+
73+The job publishes with its own `GITHUB_TOKEN`, which is the only credential Rickub's release API accepts — a personal token is refused. There is nothing to configure and no secret to keep, which is why the old `02-release.publish.sh` and `04-release.upload-binaries.sh` are gone.
74+
75+`02-build-releases.sh` cross-compiles every platform and **stamps `TAG` itself**, by overriding the Makefile's version: `make ldflags VERSION=v1.0.0`. The release *is* `v1.0.0`, so that is what its binaries say — whatever `git describe` would have answered, and whether or not the tag has been created yet. It then runs the staged binary for this machine and checks it reports the version, which is the only proof that what ships carries it.
76+
77+You can see what the workflow will publish without publishing anything, or build the binaries by hand:
78+
79+```sh
80+./02-build-releases.sh v1.0.0 # writes release/v1.0.0/, pushes nothing
81+```
82+
83+With no argument it reads `TAG` from `release.env`; the workflow has no `release.env`, so it passes the tag it was started by.
84+
85+The suite includes tests that run `01-release.tag.sh` against a throwaway clone. They skip themselves when `TURBO_GO_RELEASING` is set, which the script exports before calling `make check` — removing that line makes a release recurse until something runs out. The workflow sets the same variable for its own `go test` step.
86+
87+## Variants
88+
89+- **You install rather than distribute a binary.** `make install` and `scripts/install.sh` stamp the same way, so an installed editor names the commit it came from. There is nothing extra to do.
90+- **Someone installs with `go install`.** `go install rickub.com/turbo-editors/turbo-go@v0.2.0` reports `0.2.0` from the module version, with no commit and no build date. That is the Go tool's own record; nothing needs stamping.
91+- **You tagged the wrong commit.** If the tag has not been pushed, delete it (`git tag -d v1.0.0`), tag the right one, and rebuild. Once it is on `origin`, do not move it: the module proxy has cached `go install …@v1.0.0` and the release page already carries binaries with that number, which is why `01-release.tag.sh` refuses a tag that exists. Bump `TAG` and release again.
92+- **About says `devel`.** The binary was built with a plain `go build .` rather than through `make`. Nothing is wrong with it; it simply has no tag stamped, because the Go build system does not read git tags. Use `make build`.
93+- **About says `unknown`.** Nothing named the build at all — a `go run .`, or a build from a directory with no git history. Use `make build` from the checkout.
94+- **You have no git at all**, having downloaded a source archive. `make build` still works and the binary reports `unknown`. Pass the version yourself if you need one:
95+ ```sh
96+ go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" -o bin/turbo-go .
97+ ```
98+
99+## See also
100+
101+- Every source of the number, and what each build reports: [The version number](../reference/versioning.md)
102+- Why there is no version constant in the source: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source)
103+- Installing onto your PATH: [How to install and build Turbo Go](install.md)
new file mode 100644
@@ -0,0 +1,103 @@
1+# How to make a release
2+
3+This guide shows how to cut a release so that the editor reports its own version correctly. It assumes you can push to the repository.
4+
5+## Check what you are about to release
6+
7+```sh
8+make version
9+```
10+
11+```
12+v0.1.0-14-g88a4c38 (88a4c38)
13+```
14+
15+Fourteen commits past `v0.1.0`. A `-dirty` on the end means you have uncommitted changes — commit or stash them first, or the release will carry that suffix for ever.
16+
17+## Tag it
18+
19+```sh
20+git tag -a v0.2.0 -m "v0.2.0"
21+git push origin v0.2.0
22+```
23+
24+The tag is what the version comes from, so it has to exist before you build anything you intend to hand out. Annotated (`-a`) rather than lightweight, because `git describe` prefers annotated tags.
25+
26+## Build the release binary
27+
28+```sh
29+make build
30+./bin/turbo-go -version
31+```
32+
33+```
34+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
35+```
36+
37+No `-14-g…` suffix: you are exactly on the tag. That is what tells you the tag took.
38+
39+## Check the About box
40+
41+Start the editor and press `Alt-H`, then `A`.
42+
43+```
44+Turbo Go 0.2.0
45+
46+A Turbo C-style editor for Go,
47+written in Go.
48+
49+Commit: 88a4c38
50+Built: 2026-08-31 18:04 UTC
51+Theme: Turbo Classic
52+```
53+
54+## Or use the scripts and let the workflow publish
55+
56+That is the way a release is actually cut. Put the version and its one-line description in `release.env` — it is gitignored, so CI never sees it:
57+
58+```sh
59+TAG="v1.0.0"
60+ABOUT="Turbo Go"
61+```
62+
63+Then run one script:
64+
65+```sh
66+./01-release.tag.sh
67+```
68+
69+It runs `make check`, refuses a tag already taken locally or on `origin`, refuses a `go.mod` carrying a `replace` directive, commits anything outstanding, pushes the branch, and only then tags and pushes the tag. That order matters: a tag pushed before the branch points at a commit the remote has never seen, and a tag created before a failed push is left behind for somebody to find.
70+
71+That is the last thing you run by hand. Pushing the tag starts `.github/workflows/release.yml`; follow it on the repository's Actions tab. It runs the suite, cross-compiles the binaries with `./02-build-releases.sh` — the same script you can run on your machine — and creates the release page with them: the tag's message, the `go install` line, links to the documentation **at that tag**, one binary per platform, the `SHA256SUMS` and the README that describes the downloads.
72+
73+The job publishes with its own `GITHUB_TOKEN`, which is the only credential Rickub's release API accepts — a personal token is refused. There is nothing to configure and no secret to keep, which is why the old `02-release.publish.sh` and `04-release.upload-binaries.sh` are gone.
74+
75+`02-build-releases.sh` cross-compiles every platform and **stamps `TAG` itself**, by overriding the Makefile's version: `make ldflags VERSION=v1.0.0`. The release *is* `v1.0.0`, so that is what its binaries say — whatever `git describe` would have answered, and whether or not the tag has been created yet. It then runs the staged binary for this machine and checks it reports the version, which is the only proof that what ships carries it.
76+
77+You can see what the workflow will publish without publishing anything, or build the binaries by hand:
78+
79+```sh
80+./02-build-releases.sh v1.0.0 # writes release/v1.0.0/, pushes nothing
81+```
82+
83+With no argument it reads `TAG` from `release.env`; the workflow has no `release.env`, so it passes the tag it was started by.
84+
85+The suite includes tests that run `01-release.tag.sh` against a throwaway clone. They skip themselves when `TURBO_GO_RELEASING` is set, which the script exports before calling `make check` — removing that line makes a release recurse until something runs out. The workflow sets the same variable for its own `go test` step.
86+
87+## Variants
88+
89+- **You install rather than distribute a binary.** `make install` and `scripts/install.sh` stamp the same way, so an installed editor names the commit it came from. There is nothing extra to do.
90+- **Someone installs with `go install`.** `go install rickub.com/turbo-editors/turbo-go@v0.2.0` reports `0.2.0` from the module version, with no commit and no build date. That is the Go tool's own record; nothing needs stamping.
91+- **You tagged the wrong commit.** If the tag has not been pushed, delete it (`git tag -d v1.0.0`), tag the right one, and rebuild. Once it is on `origin`, do not move it: the module proxy has cached `go install …@v1.0.0` and the release page already carries binaries with that number, which is why `01-release.tag.sh` refuses a tag that exists. Bump `TAG` and release again.
92+- **About says `devel`.** The binary was built with a plain `go build .` rather than through `make`. Nothing is wrong with it; it simply has no tag stamped, because the Go build system does not read git tags. Use `make build`.
93+- **About says `unknown`.** Nothing named the build at all — a `go run .`, or a build from a directory with no git history. Use `make build` from the checkout.
94+- **You have no git at all**, having downloaded a source archive. `make build` still works and the binary reports `unknown`. Pass the version yourself if you need one:
95+ ```sh
96+ go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" -o bin/turbo-go .
97+ ```
98+
99+## See also
100+
101+- Every source of the number, and what each build reports: [The version number](../reference/versioning.md)
102+- Why there is no version constant in the source: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source)
103+- Installing onto your PATH: [How to install and build Turbo Go](install.md)
added docs/en/how-to/navigate-code.md +74 -0
new file mode 100644
@@ -0,0 +1,74 @@
1+# How to move around a file
2+
3+This guide shows how to get to the piece of code you are looking for. It assumes you have a file open.
4+
5+## Find text
6+
7+Press **Ctrl-F**, type what you are looking for, and press **Enter**. The first match is selected.
8+
9+- **F7** — next match
10+- **Shift-F7** — previous match
11+
12+The search **wraps round**: pressing F7 repeatedly cycles through every match rather than stopping at the bottom of the file. Case is ignored unless you tick `Case sensitive` in the Find box.
13+
14+## Jump to a line number
15+
16+Press **Ctrl-G**, type the number, press **Enter**. Lines count from one, as they do in compiler messages.
17+
18+## Jump to a declaration
19+
20+Put the cursor on a name and press **F12**. Turbo Go asks the language server where it is declared and opens that file, on that line. When there is more than one declaration, it offers the list.
21+
22+That is one of eight questions the **Code** menu puts to the server — what implements this, where is it used, what is wrong with this file. See [How to ask what the code means](ask-about-code.md).
23+
24+If the file is already open, its window comes forward instead of opening a second one.
25+
26+> This needs gopls. See [How to enable Go completion](enable-completion.md).
27+
28+## Select and edit whole lines
29+
30+| | |
31+| --- | --- |
32+| **Double-click a word** | Select it. Typing then replaces it; holding the button after the second click extends the selection from the start of the word. |
33+| **Ctrl-N** | Open a blank line **above** the cursor. The cursor stays on its own text, now one line lower — room made over what you are looking at. |
34+| **Ctrl-Y** | Delete the line the cursor is on. The cursor keeps its line number, so holding the key deletes a run of lines. |
35+
36+Both are Turbo C's keys. `Ctrl-Y` is why **redo is `Ctrl-R`** and no longer `Ctrl-Y`: between the editor looking like Turbo C and a habit picked up here, the first won. `Ctrl-Shift-Z` was not available to move redo to — a terminal delivers it as plain `Ctrl-Z`.
37+
38+A double-click away from a word — on a space or a bracket — moves the cursor and selects nothing. Editors disagree about what a run of punctuation means, and nothing is at least an answer you can predict.
39+
40+## Move by word, line and file
41+
42+| | |
43+| --- | --- |
44+| **Ctrl-←** / **Ctrl-→** | Previous / next word |
45+| **Home** / **End** | Start / end of the line |
46+| **Ctrl-Home** / **Ctrl-End** | Start / end of the file |
47+| **PgUp** / **PgDn** | One screenful |
48+
49+Hold **Shift** with any of these to select as you go.
50+
51+## Move between windows
52+
53+| | |
54+| --- | --- |
55+| **F6** | The window behind the current one |
56+| **Alt-1****Alt-9** | That numbered window — the number is in its top-right corner |
57+| **Alt-0** | A list of every open window |
58+
59+If the windows are on top of each other, `Window ▸ Tile` lays them out side by side and `Window ▸ Cascade` stacks them with every title visible.
60+
61+## Variants
62+
63+**The file is not Go.** Everything above works except F12, which needs a language server. Colouring is off too: only `.go` files are coloured.
64+
65+**You want to see where you are.** The right-hand end of the status bar always shows `line:column`, counting from one.
66+
67+**You resized the terminal.** Windows follow it: one that filled the terminal still fills it, and one you had moved keeps its corner where you put it. Nothing is ever left larger than the terminal.
68+
69+**The line is longer than the window.** The view scrolls sideways to follow the cursor; the bar along the bottom of the window shows how far along you are.
70+
71+## See also
72+
73+- Every key: [keyboard reference](../reference/keyboard.md)
74+- Every menu item: [menu reference](../reference/menus.md)
new file mode 100644
@@ -0,0 +1,74 @@
1+# How to move around a file
2+
3+This guide shows how to get to the piece of code you are looking for. It assumes you have a file open.
4+
5+## Find text
6+
7+Press **Ctrl-F**, type what you are looking for, and press **Enter**. The first match is selected.
8+
9+- **F7** — next match
10+- **Shift-F7** — previous match
11+
12+The search **wraps round**: pressing F7 repeatedly cycles through every match rather than stopping at the bottom of the file. Case is ignored unless you tick `Case sensitive` in the Find box.
13+
14+## Jump to a line number
15+
16+Press **Ctrl-G**, type the number, press **Enter**. Lines count from one, as they do in compiler messages.
17+
18+## Jump to a declaration
19+
20+Put the cursor on a name and press **F12**. Turbo Go asks the language server where it is declared and opens that file, on that line. When there is more than one declaration, it offers the list.
21+
22+That is one of eight questions the **Code** menu puts to the server — what implements this, where is it used, what is wrong with this file. See [How to ask what the code means](ask-about-code.md).
23+
24+If the file is already open, its window comes forward instead of opening a second one.
25+
26+> This needs gopls. See [How to enable Go completion](enable-completion.md).
27+
28+## Select and edit whole lines
29+
30+| | |
31+| --- | --- |
32+| **Double-click a word** | Select it. Typing then replaces it; holding the button after the second click extends the selection from the start of the word. |
33+| **Ctrl-N** | Open a blank line **above** the cursor. The cursor stays on its own text, now one line lower — room made over what you are looking at. |
34+| **Ctrl-Y** | Delete the line the cursor is on. The cursor keeps its line number, so holding the key deletes a run of lines. |
35+
36+Both are Turbo C's keys. `Ctrl-Y` is why **redo is `Ctrl-R`** and no longer `Ctrl-Y`: between the editor looking like Turbo C and a habit picked up here, the first won. `Ctrl-Shift-Z` was not available to move redo to — a terminal delivers it as plain `Ctrl-Z`.
37+
38+A double-click away from a word — on a space or a bracket — moves the cursor and selects nothing. Editors disagree about what a run of punctuation means, and nothing is at least an answer you can predict.
39+
40+## Move by word, line and file
41+
42+| | |
43+| --- | --- |
44+| **Ctrl-←** / **Ctrl-→** | Previous / next word |
45+| **Home** / **End** | Start / end of the line |
46+| **Ctrl-Home** / **Ctrl-End** | Start / end of the file |
47+| **PgUp** / **PgDn** | One screenful |
48+
49+Hold **Shift** with any of these to select as you go.
50+
51+## Move between windows
52+
53+| | |
54+| --- | --- |
55+| **F6** | The window behind the current one |
56+| **Alt-1****Alt-9** | That numbered window — the number is in its top-right corner |
57+| **Alt-0** | A list of every open window |
58+
59+If the windows are on top of each other, `Window ▸ Tile` lays them out side by side and `Window ▸ Cascade` stacks them with every title visible.
60+
61+## Variants
62+
63+**The file is not Go.** Everything above works except F12, which needs a language server. Colouring is off too: only `.go` files are coloured.
64+
65+**You want to see where you are.** The right-hand end of the status bar always shows `line:column`, counting from one.
66+
67+**You resized the terminal.** Windows follow it: one that filled the terminal still fills it, and one you had moved keeps its corner where you put it. Nothing is ever left larger than the terminal.
68+
69+**The line is longer than the window.** The view scrolls sideways to follow the cursor; the bar along the bottom of the window shows how far along you are.
70+
71+## See also
72+
73+- Every key: [keyboard reference](../reference/keyboard.md)
74+- Every menu item: [menu reference](../reference/menus.md)
added docs/en/how-to/run-go-commands.md +214 -0
new file mode 100644
@@ -0,0 +1,214 @@
1+# How to run go commands from the editor
2+
3+This guide shows how to format, vet, build, test and run your project without leaving Turbo Go. It assumes the editor is installed and you have a Go project.
4+
5+## Get a starter file
6+
7+Start the editor **from the project's own directory**, then choose **Go ▸ Create tools file** (`Alt-G`, then `C`).
8+
9+That writes `.turbo-go/tools.toml` with the five commands a Go project runs before it commits, and opens it:
10+
11+```toml
12+[[tool]]
13+name = "~F~ormat"
14+command = "gofmt -l -w ."
15+output = "popup"
16+
17+[[tool]]
18+name = "~T~est"
19+command = "go test ./..."
20+output = "popup"
21+
22+[[tool]]
23+name = "~R~un"
24+command = "go run ."
25+# A terminal, not a popup: a program that reads the keyboard has to be able to
26+# be answered, and one that runs long has to be able to be interrupted.
27+output = "terminal"
28+```
29+
30+Each `[[tool]]` becomes one line of the **Go** menu, in the order they appear — unless it names a `menu` of its own, which the next-but-one section covers. The file is read every time the menu opens, so an edit takes effect immediately.
31+
32+## Run one
33+
34+`Alt-G`, then the letter between the tildes — `F` to format, `T` to test.
35+
36+A **popup** opens at once and fills in as the command runs. Its title carries the command and, once it has ended, how it went:
37+
38+```
39+┌──────────── go vet ./... — exit 1 ────────────┐
40+│ main.go:6:2: unreachable code │
41+│ │
42+│ [ Close ] │
43+└───────────────────────────────────────────────┘
44+```
45+
46+| Key | Effect |
47+| --- | --- |
48+| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output |
49+| `Escape` | Close it — and **stop the command** if it is still running |
50+| `Enter` | Close it |
51+
52+A command that succeeded silently shows `(no output)` rather than a blank box, so you can tell it from one that has not started.
53+
54+The popup follows the output as it arrives until you scroll back, and then leaves you where you are.
55+
56+## Choose where the output goes
57+
58+Set `output` on a tool:
59+
60+| `output` | What you get |
61+| --- | --- |
62+| `popup` | A dialog that fills in as it runs. The default. |
63+| `terminal` | A terminal window: colours, `Ctrl-C`, and the keyboard reaches the program |
64+| `editor` | An editing window once it has finished, to search with `Ctrl-F` |
65+
66+`Run` is `terminal` in the starter file, and it is the example of why the key exists: a popup cannot answer a program that reads from the keyboard, and cannot be interrupted with `Ctrl-C`.
67+
68+Reach for `editor` when the output is something to work through — a long `go test -v`, or a coverage report you want to search.
69+
70+## A long command holds the editor
71+
72+A popup is modal: while `go build` runs, you cannot type anywhere else. `Escape` closes it and stops the command.
73+
74+If that gets in the way for a particular command, give it `output = "terminal"` — the window is an ordinary one and you can carry on working beside it. That is what making the key configurable is for.
75+
76+## What happens to your open files
77+
78+`Format` rewrites files on disk — including the one you are looking at. When a command finishes, the editor **re-reads every open file that has no unsaved changes**, so the formatted version appears without you doing anything. The status bar says how many.
79+
80+A file with unsaved changes is **left alone**, and the status bar says so too:
81+
82+```
83+Reloaded 2 files; 1 file with unsaved changes left alone
84+```
85+
86+That is deliberate: your edit and the formatter genuinely disagree, and the editor is not the one that should decide which wins. Save first (`F2`) and run the command again, or keep editing and format later.
87+
88+## Add your own commands
89+
90+Edit `.turbo-go/tools.toml`. A command goes to `sh -c`, so one entry can be a whole sequence:
91+
92+```toml
93+[[tool]]
94+name = "~C~heck"
95+command = "gofmt -l -w . && go vet ./... && go test ./..."
96+output = "popup"
97+
98+[[tool]]
99+name = "Tid~y~"
100+command = "go mod tidy"
101+output = "popup"
102+
103+[[tool]]
104+name = "Cover~a~ge"
105+command = "go test -coverprofile=cover.out ./... && go tool cover -func=cover.out"
106+output = "editor"
107+```
108+
109+Give each a hot key with tildes, and keep them distinct — the menu answers the first match it finds.
110+
111+## Put a tool in a menu of its own
112+
113+A tool that has nothing to do with Go does not belong in the Go menu. Give it a `menu`:
114+
115+```toml
116+[[tool]]
117+name = "~E~cho"
118+command = "echo TADA"
119+output = "terminal"
120+menu = "Tools"
121+
122+[[tool]]
123+name = "~U~p"
124+command = "docker compose up -d"
125+menu = "Docker"
126+
127+[[tool]]
128+name = "~D~own"
129+command = "docker compose down"
130+menu = "Docker"
131+```
132+
133+That gives you a **Tools** menu and a **Docker** menu on the bar, between Go and Help, in the order the names first appear in the file. Docker holds both its tools. Nothing needs restarting: save the file and the bar follows.
134+
135+The name is yours to choose — there is no list to pick from. Leave `menu` out and the tool stays in Go, which is where all five starter commands are.
136+
137+### The hot key is chosen for you
138+
139+You cannot know, when writing the file, which letters the editor's own menus have taken. So it works it out: the first letter of the name that nothing else claims gets the tildes.
140+
141+`Tools` gets `Alt-T`. `Format` gets `Alt-M`, because `F` is File's, `o` is Options' and `r` is Run's.
142+
143+Write the tildes yourself — `menu = "Doc~k~er"` — and a free letter is kept. A taken one is not: the bar answers the *first* menu matching a key, so honouring your choice would make one of the two menus unreachable. It picks another letter and says nothing.
144+
145+## Variants
146+
147+- **You want `golangci-lint` instead of `go vet`.** Change the `Lint` command. `go vet` is the default because it ships with the toolchain and is never missing; anything else you have to install.
148+- **You started the editor from a subdirectory.** Commands run there, so `./...` covers only that subtree. Start from the project root.
149+- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read tools` where the commands would be, and **Create tools file** is still there.
150+- **A command is not installed.** The popup shows `command not found` and `— exit 127`, which is what a shell would have said.
151+- **You want a menu named after one that exists.** `menu = "File"` gives you a second File menu, further along the bar, with a different hot key. Nothing stops you; nothing recommends it either.
152+- **Your menu has no hot key.** Every letter in its name was already taken. `F10` and the arrow keys reach it, and so does the mouse. Rename it to something with a free letter.
153+- **You misspelt `output`'s value.** The whole file is refused and the menu says `Cannot read tools`, naming the tool and listing what it could have been. A silent fallback would have sent the output somewhere you did not ask for.
154+
155+## Ask for a value when the command runs
156+
157+Some commands need something typed each time: a module path, a crate name, a test to filter on. Put a `{{label}}` where the value goes:
158+
159+```toml
160+[[tool]]
161+name = "~I~nit module"
162+command = "go mod init {{module path}}"
163+output = "popup"
164+```
165+
166+Choosing it now opens a box titled **Init module** with one field, labelled `module path`. Type the value and press Enter; the command runs with it. Escape, and nothing runs.
167+
168+The value is quoted, so a path with a space in it stays one argument.
169+
170+### Several values at once
171+
172+One field each, in the order they appear:
173+
174+```toml
175+[[tool]]
176+name = "~C~opy"
177+command = "cp {{from}} {{to}}"
178+```
179+
180+**Tab** moves between the fields, **Enter** runs it.
181+
182+### One field standing for several arguments
183+
184+Quoting is wrong when you mean "put these flags on the end". Add `...` inside the braces and the value goes in verbatim:
185+
186+```toml
187+[[tool]]
188+name = "Test ~o~ne"
189+command = "go test {{extra flags...}} ./..."
190+```
191+
192+Type `-run TestParse -v` and all of it reaches the command as separate arguments.
193+
194+### The same value twice
195+
196+Write the label twice; you are asked once:
197+
198+```toml
199+[[tool]]
200+name = "~N~ew directory"
201+command = "mkdir {{name}} && cd {{name}}"
202+```
203+
204+### Variants
205+
206+- **The value is the same most times.** Run it once and the box remembers what you typed, for the rest of the session. It is not written to disk.
207+- **Your command has braces in it already.** `awk '{print $1}'` and `find . -exec rm {} +` are left alone: only double braces ask for anything.
208+- **The command asks for more values than fit on screen.** The editor says so rather than opening a box whose OK button is below the bottom of the terminal. Make the terminal taller, or split the command into two tools.
209+
210+## See also
211+
212+- Every key of the file and every rule: [Go tools reference](../reference/go-tools.md)
213+- Why each command gets a terminal window, and why an unmodified file reloads: [Go tools](../explanation/go-tools.md)
214+- The windows the commands run in: [Terminal windows](../reference/terminal.md)
new file mode 100644
@@ -0,0 +1,214 @@
1+# How to run go commands from the editor
2+
3+This guide shows how to format, vet, build, test and run your project without leaving Turbo Go. It assumes the editor is installed and you have a Go project.
4+
5+## Get a starter file
6+
7+Start the editor **from the project's own directory**, then choose **Go ▸ Create tools file** (`Alt-G`, then `C`).
8+
9+That writes `.turbo-go/tools.toml` with the five commands a Go project runs before it commits, and opens it:
10+
11+```toml
12+[[tool]]
13+name = "~F~ormat"
14+command = "gofmt -l -w ."
15+output = "popup"
16+
17+[[tool]]
18+name = "~T~est"
19+command = "go test ./..."
20+output = "popup"
21+
22+[[tool]]
23+name = "~R~un"
24+command = "go run ."
25+# A terminal, not a popup: a program that reads the keyboard has to be able to
26+# be answered, and one that runs long has to be able to be interrupted.
27+output = "terminal"
28+```
29+
30+Each `[[tool]]` becomes one line of the **Go** menu, in the order they appear — unless it names a `menu` of its own, which the next-but-one section covers. The file is read every time the menu opens, so an edit takes effect immediately.
31+
32+## Run one
33+
34+`Alt-G`, then the letter between the tildes — `F` to format, `T` to test.
35+
36+A **popup** opens at once and fills in as the command runs. Its title carries the command and, once it has ended, how it went:
37+
38+```
39+┌──────────── go vet ./... — exit 1 ────────────┐
40+│ main.go:6:2: unreachable code │
41+│ │
42+│ [ Close ] │
43+└───────────────────────────────────────────────┘
44+```
45+
46+| Key | Effect |
47+| --- | --- |
48+| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output |
49+| `Escape` | Close it — and **stop the command** if it is still running |
50+| `Enter` | Close it |
51+
52+A command that succeeded silently shows `(no output)` rather than a blank box, so you can tell it from one that has not started.
53+
54+The popup follows the output as it arrives until you scroll back, and then leaves you where you are.
55+
56+## Choose where the output goes
57+
58+Set `output` on a tool:
59+
60+| `output` | What you get |
61+| --- | --- |
62+| `popup` | A dialog that fills in as it runs. The default. |
63+| `terminal` | A terminal window: colours, `Ctrl-C`, and the keyboard reaches the program |
64+| `editor` | An editing window once it has finished, to search with `Ctrl-F` |
65+
66+`Run` is `terminal` in the starter file, and it is the example of why the key exists: a popup cannot answer a program that reads from the keyboard, and cannot be interrupted with `Ctrl-C`.
67+
68+Reach for `editor` when the output is something to work through — a long `go test -v`, or a coverage report you want to search.
69+
70+## A long command holds the editor
71+
72+A popup is modal: while `go build` runs, you cannot type anywhere else. `Escape` closes it and stops the command.
73+
74+If that gets in the way for a particular command, give it `output = "terminal"` — the window is an ordinary one and you can carry on working beside it. That is what making the key configurable is for.
75+
76+## What happens to your open files
77+
78+`Format` rewrites files on disk — including the one you are looking at. When a command finishes, the editor **re-reads every open file that has no unsaved changes**, so the formatted version appears without you doing anything. The status bar says how many.
79+
80+A file with unsaved changes is **left alone**, and the status bar says so too:
81+
82+```
83+Reloaded 2 files; 1 file with unsaved changes left alone
84+```
85+
86+That is deliberate: your edit and the formatter genuinely disagree, and the editor is not the one that should decide which wins. Save first (`F2`) and run the command again, or keep editing and format later.
87+
88+## Add your own commands
89+
90+Edit `.turbo-go/tools.toml`. A command goes to `sh -c`, so one entry can be a whole sequence:
91+
92+```toml
93+[[tool]]
94+name = "~C~heck"
95+command = "gofmt -l -w . && go vet ./... && go test ./..."
96+output = "popup"
97+
98+[[tool]]
99+name = "Tid~y~"
100+command = "go mod tidy"
101+output = "popup"
102+
103+[[tool]]
104+name = "Cover~a~ge"
105+command = "go test -coverprofile=cover.out ./... && go tool cover -func=cover.out"
106+output = "editor"
107+```
108+
109+Give each a hot key with tildes, and keep them distinct — the menu answers the first match it finds.
110+
111+## Put a tool in a menu of its own
112+
113+A tool that has nothing to do with Go does not belong in the Go menu. Give it a `menu`:
114+
115+```toml
116+[[tool]]
117+name = "~E~cho"
118+command = "echo TADA"
119+output = "terminal"
120+menu = "Tools"
121+
122+[[tool]]
123+name = "~U~p"
124+command = "docker compose up -d"
125+menu = "Docker"
126+
127+[[tool]]
128+name = "~D~own"
129+command = "docker compose down"
130+menu = "Docker"
131+```
132+
133+That gives you a **Tools** menu and a **Docker** menu on the bar, between Go and Help, in the order the names first appear in the file. Docker holds both its tools. Nothing needs restarting: save the file and the bar follows.
134+
135+The name is yours to choose — there is no list to pick from. Leave `menu` out and the tool stays in Go, which is where all five starter commands are.
136+
137+### The hot key is chosen for you
138+
139+You cannot know, when writing the file, which letters the editor's own menus have taken. So it works it out: the first letter of the name that nothing else claims gets the tildes.
140+
141+`Tools` gets `Alt-T`. `Format` gets `Alt-M`, because `F` is File's, `o` is Options' and `r` is Run's.
142+
143+Write the tildes yourself — `menu = "Doc~k~er"` — and a free letter is kept. A taken one is not: the bar answers the *first* menu matching a key, so honouring your choice would make one of the two menus unreachable. It picks another letter and says nothing.
144+
145+## Variants
146+
147+- **You want `golangci-lint` instead of `go vet`.** Change the `Lint` command. `go vet` is the default because it ships with the toolchain and is never missing; anything else you have to install.
148+- **You started the editor from a subdirectory.** Commands run there, so `./...` covers only that subtree. Start from the project root.
149+- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read tools` where the commands would be, and **Create tools file** is still there.
150+- **A command is not installed.** The popup shows `command not found` and `— exit 127`, which is what a shell would have said.
151+- **You want a menu named after one that exists.** `menu = "File"` gives you a second File menu, further along the bar, with a different hot key. Nothing stops you; nothing recommends it either.
152+- **Your menu has no hot key.** Every letter in its name was already taken. `F10` and the arrow keys reach it, and so does the mouse. Rename it to something with a free letter.
153+- **You misspelt `output`'s value.** The whole file is refused and the menu says `Cannot read tools`, naming the tool and listing what it could have been. A silent fallback would have sent the output somewhere you did not ask for.
154+
155+## Ask for a value when the command runs
156+
157+Some commands need something typed each time: a module path, a crate name, a test to filter on. Put a `{{label}}` where the value goes:
158+
159+```toml
160+[[tool]]
161+name = "~I~nit module"
162+command = "go mod init {{module path}}"
163+output = "popup"
164+```
165+
166+Choosing it now opens a box titled **Init module** with one field, labelled `module path`. Type the value and press Enter; the command runs with it. Escape, and nothing runs.
167+
168+The value is quoted, so a path with a space in it stays one argument.
169+
170+### Several values at once
171+
172+One field each, in the order they appear:
173+
174+```toml
175+[[tool]]
176+name = "~C~opy"
177+command = "cp {{from}} {{to}}"
178+```
179+
180+**Tab** moves between the fields, **Enter** runs it.
181+
182+### One field standing for several arguments
183+
184+Quoting is wrong when you mean "put these flags on the end". Add `...` inside the braces and the value goes in verbatim:
185+
186+```toml
187+[[tool]]
188+name = "Test ~o~ne"
189+command = "go test {{extra flags...}} ./..."
190+```
191+
192+Type `-run TestParse -v` and all of it reaches the command as separate arguments.
193+
194+### The same value twice
195+
196+Write the label twice; you are asked once:
197+
198+```toml
199+[[tool]]
200+name = "~N~ew directory"
201+command = "mkdir {{name}} && cd {{name}}"
202+```
203+
204+### Variants
205+
206+- **The value is the same most times.** Run it once and the box remembers what you typed, for the rest of the session. It is not written to disk.
207+- **Your command has braces in it already.** `awk '{print $1}'` and `find . -exec rm {} +` are left alone: only double braces ask for anything.
208+- **The command asks for more values than fit on screen.** The editor says so rather than opening a box whose OK button is below the bottom of the terminal. Make the terminal taller, or split the command into two tools.
209+
210+## See also
211+
212+- Every key of the file and every rule: [Go tools reference](../reference/go-tools.md)
213+- Why each command gets a terminal window, and why an unmodified file reloads: [Go tools](../explanation/go-tools.md)
214+- The windows the commands run in: [Terminal windows](../reference/terminal.md)
added docs/en/how-to/run-the-tests.md +95 -0
new file mode 100644
@@ -0,0 +1,95 @@
1+# How to run the tests
2+
3+This guide shows how to run and read Turbo Go's test suite. It assumes you have a checkout and Go 1.26 or later.
4+
5+## The whole suite
6+
7+```bash
8+make test
9+```
10+
11+That is the single documented command. It runs `go test ./...` across every package.
12+
13+## Variants
14+
15+**See each test by name:**
16+
17+```bash
18+make test-verbose
19+```
20+
21+**Measure coverage per package:**
22+
23+```bash
24+make cover
25+```
26+
27+**One package only:**
28+
29+```bash
30+go test ./internal/golang/
31+```
32+
33+**Without starting a language server.** One test in `internal/golang` starts a real `gopls` when it finds one. To skip it:
34+
35+```bash
36+go test -short ./...
37+```
38+
39+**With the race detector.** The LSP client is concurrent, so this is worth running before touching it:
40+
41+```bash
42+go test -race ./...
43+```
44+
45+**Everything a commit should pass:**
46+
47+```bash
48+make check
49+```
50+
51+This runs `go fmt`, `go vet` and the tests, in that order.
52+
53+## What the suite covers
54+
55+No test needs a real terminal. The widgets and the editor are drawn onto tcell's `SimulationScreen` — a real `Screen` that draws into memory — so the assertions are made on the picture a terminal would actually show. The LSP client is driven against a language server running in the same process, over an in-memory pipe.
56+
57+The one exception is `TestAgainstRealGopls`, which starts the real thing. It **skips itself** when `gopls` is not installed, so a checkout without one still has a green suite.
58+
59+## Testing against an unreleased turbo-core
60+
61+Most of Turbo Go is turbo-core, and this repository depends on it by version, from the module proxy:
62+
63+```
64+require rickub.com/turbo-editors/turbo-core v0.2.0
65+```
66+
67+A change made in a turbo-core checkout beside this one is therefore invisible here until it is published. To test it before that, make a workspace:
68+
69+```bash
70+go work init . ../turbo-core
71+make test
72+```
73+
74+Every import of the library now resolves to that checkout. Nothing in `go.mod` or `go.sum` changes, so there is no edit to undo. Check it took effect — this is the mistake worth guarding against, because everything still builds and still passes if it did not:
75+
76+```bash
77+go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app
78+```
79+
80+The answer should be your checkout, not a path under `pkg/mod`. When you are done, `rm go.work go.work.sum`; it is gitignored, so it cannot be committed by accident.
81+
82+## Code quality
83+
84+The test suite is not the whole gate. Quality is measured separately:
85+
86+```bash
87+python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .
88+```
89+
90+It writes a report under `.quality/` and exits non-zero if the gate fails.
91+
92+## See also
93+
94+- Why the tests are shaped this way: [Architecture](../explanation/architecture.md)
95+- Every make target: [command line reference](../reference/cli.md)
new file mode 100644
@@ -0,0 +1,95 @@
1+# How to run the tests
2+
3+This guide shows how to run and read Turbo Go's test suite. It assumes you have a checkout and Go 1.26 or later.
4+
5+## The whole suite
6+
7+```bash
8+make test
9+```
10+
11+That is the single documented command. It runs `go test ./...` across every package.
12+
13+## Variants
14+
15+**See each test by name:**
16+
17+```bash
18+make test-verbose
19+```
20+
21+**Measure coverage per package:**
22+
23+```bash
24+make cover
25+```
26+
27+**One package only:**
28+
29+```bash
30+go test ./internal/golang/
31+```
32+
33+**Without starting a language server.** One test in `internal/golang` starts a real `gopls` when it finds one. To skip it:
34+
35+```bash
36+go test -short ./...
37+```
38+
39+**With the race detector.** The LSP client is concurrent, so this is worth running before touching it:
40+
41+```bash
42+go test -race ./...
43+```
44+
45+**Everything a commit should pass:**
46+
47+```bash
48+make check
49+```
50+
51+This runs `go fmt`, `go vet` and the tests, in that order.
52+
53+## What the suite covers
54+
55+No test needs a real terminal. The widgets and the editor are drawn onto tcell's `SimulationScreen` — a real `Screen` that draws into memory — so the assertions are made on the picture a terminal would actually show. The LSP client is driven against a language server running in the same process, over an in-memory pipe.
56+
57+The one exception is `TestAgainstRealGopls`, which starts the real thing. It **skips itself** when `gopls` is not installed, so a checkout without one still has a green suite.
58+
59+## Testing against an unreleased turbo-core
60+
61+Most of Turbo Go is turbo-core, and this repository depends on it by version, from the module proxy:
62+
63+```
64+require rickub.com/turbo-editors/turbo-core v0.2.0
65+```
66+
67+A change made in a turbo-core checkout beside this one is therefore invisible here until it is published. To test it before that, make a workspace:
68+
69+```bash
70+go work init . ../turbo-core
71+make test
72+```
73+
74+Every import of the library now resolves to that checkout. Nothing in `go.mod` or `go.sum` changes, so there is no edit to undo. Check it took effect — this is the mistake worth guarding against, because everything still builds and still passes if it did not:
75+
76+```bash
77+go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app
78+```
79+
80+The answer should be your checkout, not a path under `pkg/mod`. When you are done, `rm go.work go.work.sum`; it is gitignored, so it cannot be committed by accident.
81+
82+## Code quality
83+
84+The test suite is not the whole gate. Quality is measured separately:
85+
86+```bash
87+python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .
88+```
89+
90+It writes a report under `.quality/` and exits non-zero if the gate fails.
91+
92+## See also
93+
94+- Why the tests are shaped this way: [Architecture](../explanation/architecture.md)
95+- Every make target: [command line reference](../reference/cli.md)
added docs/en/how-to/talk-to-an-agent.md +177 -0
new file mode 100644
@@ -0,0 +1,177 @@
1+# How to talk to a coding agent from the editor
2+
3+This guide shows how to point Turbo Go at an agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com), open a window onto it, and hold a conversation about the code you are editing. It assumes you already have Turbo Go running in a project.
4+
5+Turbo Go is an ACP **client**. It starts the agent as a child process and talks JSON-RPC to it over its standard input and output — the same arrangement Zed uses, so an agent that works there works here.
6+
7+## Tell the editor about an agent
8+
9+Agents are listed in `acp.toml`. Choose **Agent ▸ Create agents file** and the editor writes a starter one into `.turbo-go/acp.toml` and opens it.
10+
11+An agent is one `[[agent]]` block:
12+
13+```toml
14+[[agent]]
15+name = "Bob (llama.cpp)"
16+command = "docker"
17+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
18+env = { TELEMETRY_ENABLED = "false" }
19+```
20+
21+`name` is what the Agent menu shows and what the window is called. `command` and `args` are how the agent is started. That is the whole of it — the file is read again every time you open a window, so you never restart the editor to try a change.
22+
23+List as many as you like. Each becomes its own line in the menu, and each window you open from it is a separate process with a conversation of its own.
24+
25+## Put the agent's own configuration beside it
26+
27+Most agents have a configuration file of their own, and `.turbo-go/` is a reasonable place to keep it so that it travels with the project. For `docker agent`, saving this as `.turbo-go/agent.yaml` is what the `args` above point at:
28+
29+```yaml
30+providers:
31+ llamacpp:
32+ api_type: openai_chatcompletions
33+ base_url: http://localhost:8080/v1
34+
35+models:
36+ mellum2:
37+ provider: llamacpp
38+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
39+ temperature: 0.7
40+ provider_opts:
41+ context_size: 262144
42+
43+agents:
44+ root:
45+ model: mellum2
46+ description: A helpful AI assistant running on a local llama.cpp server
47+ instruction: |
48+ You name is Bob 🤓, you are a knowledgeable code assistant.
49+ Be helpful, accurate, and concise in your responses.
50+ You have access to the local filesystem and shell: use these tools
51+ toolsets:
52+ - type: filesystem
53+ - type: shell
54+```
55+
56+## Open a window on it
57+
58+Press `Alt-A`, or choose **Agent** from the menu bar, and pick the agent by name.
59+
60+A window opens, split in two: the conversation above, and a box to type in below. The agent is started when the window opens and stopped when it closes.
61+
62+```
63+┌ Bob (llama.cpp) ───────────────────────────────[■]┐
64+│ ‣ You │
65+│ What does buildMenus do? │
66+│ │
67+│ ‣ Shell ls -1 internal/ ✓ done │
68+│ golang │
69+│ │
70+│ ‣ Bob │
71+│ It assembles the menu bar. Here is the shape: │
72+│ │
73+│ ```go │
74+│ func (a *App) buildMenus() *ui.MenuBar { │
75+│ return ui.NewMenuBar(a.allMenus()...) │
76+│ } │
77+│ ``` │
78+├───────────────────────────────────────────────────┤
79+│ > _ │
80+└───────────────────────────────────────────────────┘
81+```
82+
83+Code the agent sends inside a fenced block is coloured by the same scanners the editor uses for files, so a Go answer is coloured as Go and a shell answer as shell. A fence naming a language the editor does not colour is left plain rather than guessed at.
84+
85+## Hold the conversation
86+
87+| Key | Effect |
88+| --- | --- |
89+| `Enter` | Send what you have typed |
90+| `Alt-Enter` | Start a new line instead of sending |
91+| `Tab` | Move between the conversation and the input box |
92+| `PgUp` `PgDn` | Scroll the conversation a screenful at a time |
93+| `Esc` | Stop the turn in progress |
94+| `Ctrl-W` | Close the window, and stop the agent with it |
95+
96+While the agent is answering, its reply appears as it is written rather than all at once, and the rule between the two panes turns a spinner beside the word *thinking*. `Esc` interrupts it — the agent is told to stop, and what it had already said stays in the window.
97+
98+## Use the agent's own commands
99+
100+Some agents answer to commands — `/compact`, `/web`, `/plan` — and tell the editor which ones. Type `/` as the first character of the box and the list opens over the conversation: each command, what it does, and in angle brackets what it wants after its name.
101+
102+Type on to narrow it, `↑` `↓` to move, then `Tab` to complete. A command that takes something is completed with a space after it, ready for you to type the rest; press `Enter` when the line is what you mean. If nothing appears when you type `/`, the agent announced no commands — **Agent ▸ Agent status** says so — and `/` is only a character.
103+
104+## Point the agent at a file
105+
106+Type `@` anywhere in the box and the project's files appear. Type a few letters of the file's name to narrow the list, `Tab` to take the highlighted one:
107+
108+```
109+> explain what @internal/scanner.go does
110+```
111+
112+When you press `Enter`, the agent is given the **file**, not merely its name: its text when the agent accepts embedded context, a link to it otherwise. If the file is open in the editor with unsaved changes, it is your unsaved version that goes. The line stays in the conversation as you typed it.
113+
114+Several files in one prompt is several `@`. A word that begins with `@` but is not a file — an e-mail address — is left as text.
115+
116+## Take something out of the conversation
117+
118+Press `Tab` to put the cursor in the conversation. The rule changes to say what the keys now do.
119+
120+| Key | Effect |
121+| --- | --- |
122+| `↑` `↓` `PgUp` `PgDn` | Move the cursor through what was said |
123+| `Shift-↑` `Shift-↓` | Select whole lines |
124+| Drag with the mouse | The same, by hand |
125+| `Ctrl-C` | Copy |
126+| `Esc` | Drop the selection |
127+| `Tab` | Back to the box |
128+
129+**With nothing selected, `Ctrl-C` copies the block the cursor is on** — one fenced code block, one paragraph, one tool's output — without the speaker's label above it and without the sentence after it. That is almost always what you wanted, and it saves selecting it by hand.
130+
131+What is copied goes to **two** clipboards: this editor's, so `Shift-Ins` pastes it into a file you have open, and your system's, so `Ctrl-V` pastes it anywhere else. The indentation the conversation is drawn with is taken off, so pasted code lands flush against the margin.
132+
133+The system half travels through your terminal (an escape sequence called OSC 52). Most terminals do it; a few refuse it for security, and some need it turned on. If `Ctrl-V` elsewhere gives you nothing, that is where to look — the editor's own clipboard has the text either way.
134+
135+## Answer the agent when it asks permission
136+
137+An agent with a shell or a filesystem toolset asks before it uses one. A dialog names the tool and the exact command, and offers the choices the agent itself proposed — normally *Allow this action*, *Allow and remember my choice*, and *Skip this action*.
138+
139+```
140+┌───────────── Bob (llama.cpp) wants to run ─────────────┐
141+│ │
142+│ Shell │
143+│ ls -1 │
144+│ │
145+│ [ Allow ] [ Allow always ] [ Skip ] │
146+└────────────────────────────────────────────────────────┘
147+```
148+
149+*Allow always* is remembered by the agent, not by the editor, so what it covers and how long it lasts are the agent's business. Escape is the same answer as *Skip*.
150+
151+Nothing runs before you answer. An agent waiting on a permission dialog is simply blocked, which is the point.
152+
153+## Let the agent see what you have not saved yet
154+
155+The editor offers the agent its own filesystem: when the agent reads a file you have open with unsaved changes, it is given **the text in the buffer**, not the older text on disk. That is usually what you want — you are asking about the edit you just made.
156+
157+When the agent writes a file, the change lands in the buffer and the window is marked modified, so you can read it, undo it with `Ctrl-Z`, or save it with `F2`. A file you do not have open is read from and written to disk directly.
158+
159+## Run several agents at once
160+
161+Each window is its own process and its own conversation. Opening the same agent twice gives two independent sessions, and opening two different agents lets you put a fast local model and a slower careful one side by side — **Window ▸ Tile** arranges them.
162+
163+Leaving the editor stops every agent.
164+
165+## Variants
166+
167+- **You want the agent to run somewhere other than the project root.** Add `cwd = "backend"` to its block. The path is relative to the project, and is both where the process starts and what the agent is told the working directory is.
168+- **The agent needs a credential.** Put it in `env`, or rely on it being in the environment you started the editor from — the agent inherits it.
169+- **The agent's commands do not appear when you type `/`.** Open **Agent ▸ Agent status** with the window in front. If it lists no commands, the agent announced none — or announced them in a shape this editor could not read, in which case the dialog names the update and the decoding error. To see exactly what went over the wire, start the editor with `TURBO_ACP_TRACE=/tmp/acp.log` and read the file: `->` is what the editor sent, `<-` what the agent answered.
170+- **The agent will not start.** **Agent ▸ Agent status** lists what was read from `acp.toml`, what each agent's command line came out as, and the error from anything that failed to start. Whatever the agent writes to its standard error is shown there too, which is where a misconfigured model endpoint reports itself.
171+- **You keep the same agent in every project.** Put the `[[agent]]` block in `~/.config/turbo-go/acp.toml` instead. A project's own file is read afterwards and an agent with the same `name` in it replaces yours.
172+
173+## See also
174+
175+- Every key of the file, and exactly how much of the protocol is implemented: [Agents and ACP reference](../reference/acp.md)
176+- Why an agent is a window rather than a panel, and why permissions are modal: [Agent windows](../explanation/agent-windows.md)
177+- The protocol itself: [agentclientprotocol.com](https://agentclientprotocol.com)
new file mode 100644
@@ -0,0 +1,177 @@
1+# How to talk to a coding agent from the editor
2+
3+This guide shows how to point Turbo Go at an agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com), open a window onto it, and hold a conversation about the code you are editing. It assumes you already have Turbo Go running in a project.
4+
5+Turbo Go is an ACP **client**. It starts the agent as a child process and talks JSON-RPC to it over its standard input and output — the same arrangement Zed uses, so an agent that works there works here.
6+
7+## Tell the editor about an agent
8+
9+Agents are listed in `acp.toml`. Choose **Agent ▸ Create agents file** and the editor writes a starter one into `.turbo-go/acp.toml` and opens it.
10+
11+An agent is one `[[agent]]` block:
12+
13+```toml
14+[[agent]]
15+name = "Bob (llama.cpp)"
16+command = "docker"
17+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
18+env = { TELEMETRY_ENABLED = "false" }
19+```
20+
21+`name` is what the Agent menu shows and what the window is called. `command` and `args` are how the agent is started. That is the whole of it — the file is read again every time you open a window, so you never restart the editor to try a change.
22+
23+List as many as you like. Each becomes its own line in the menu, and each window you open from it is a separate process with a conversation of its own.
24+
25+## Put the agent's own configuration beside it
26+
27+Most agents have a configuration file of their own, and `.turbo-go/` is a reasonable place to keep it so that it travels with the project. For `docker agent`, saving this as `.turbo-go/agent.yaml` is what the `args` above point at:
28+
29+```yaml
30+providers:
31+ llamacpp:
32+ api_type: openai_chatcompletions
33+ base_url: http://localhost:8080/v1
34+
35+models:
36+ mellum2:
37+ provider: llamacpp
38+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
39+ temperature: 0.7
40+ provider_opts:
41+ context_size: 262144
42+
43+agents:
44+ root:
45+ model: mellum2
46+ description: A helpful AI assistant running on a local llama.cpp server
47+ instruction: |
48+ You name is Bob 🤓, you are a knowledgeable code assistant.
49+ Be helpful, accurate, and concise in your responses.
50+ You have access to the local filesystem and shell: use these tools
51+ toolsets:
52+ - type: filesystem
53+ - type: shell
54+```
55+
56+## Open a window on it
57+
58+Press `Alt-A`, or choose **Agent** from the menu bar, and pick the agent by name.
59+
60+A window opens, split in two: the conversation above, and a box to type in below. The agent is started when the window opens and stopped when it closes.
61+
62+```
63+┌ Bob (llama.cpp) ───────────────────────────────[■]┐
64+│ ‣ You │
65+│ What does buildMenus do? │
66+│ │
67+│ ‣ Shell ls -1 internal/ ✓ done │
68+│ golang │
69+│ │
70+│ ‣ Bob │
71+│ It assembles the menu bar. Here is the shape: │
72+│ │
73+│ ```go │
74+│ func (a *App) buildMenus() *ui.MenuBar { │
75+│ return ui.NewMenuBar(a.allMenus()...) │
76+│ } │
77+│ ``` │
78+├───────────────────────────────────────────────────┤
79+│ > _ │
80+└───────────────────────────────────────────────────┘
81+```
82+
83+Code the agent sends inside a fenced block is coloured by the same scanners the editor uses for files, so a Go answer is coloured as Go and a shell answer as shell. A fence naming a language the editor does not colour is left plain rather than guessed at.
84+
85+## Hold the conversation
86+
87+| Key | Effect |
88+| --- | --- |
89+| `Enter` | Send what you have typed |
90+| `Alt-Enter` | Start a new line instead of sending |
91+| `Tab` | Move between the conversation and the input box |
92+| `PgUp` `PgDn` | Scroll the conversation a screenful at a time |
93+| `Esc` | Stop the turn in progress |
94+| `Ctrl-W` | Close the window, and stop the agent with it |
95+
96+While the agent is answering, its reply appears as it is written rather than all at once, and the rule between the two panes turns a spinner beside the word *thinking*. `Esc` interrupts it — the agent is told to stop, and what it had already said stays in the window.
97+
98+## Use the agent's own commands
99+
100+Some agents answer to commands — `/compact`, `/web`, `/plan` — and tell the editor which ones. Type `/` as the first character of the box and the list opens over the conversation: each command, what it does, and in angle brackets what it wants after its name.
101+
102+Type on to narrow it, `↑` `↓` to move, then `Tab` to complete. A command that takes something is completed with a space after it, ready for you to type the rest; press `Enter` when the line is what you mean. If nothing appears when you type `/`, the agent announced no commands — **Agent ▸ Agent status** says so — and `/` is only a character.
103+
104+## Point the agent at a file
105+
106+Type `@` anywhere in the box and the project's files appear. Type a few letters of the file's name to narrow the list, `Tab` to take the highlighted one:
107+
108+```
109+> explain what @internal/scanner.go does
110+```
111+
112+When you press `Enter`, the agent is given the **file**, not merely its name: its text when the agent accepts embedded context, a link to it otherwise. If the file is open in the editor with unsaved changes, it is your unsaved version that goes. The line stays in the conversation as you typed it.
113+
114+Several files in one prompt is several `@`. A word that begins with `@` but is not a file — an e-mail address — is left as text.
115+
116+## Take something out of the conversation
117+
118+Press `Tab` to put the cursor in the conversation. The rule changes to say what the keys now do.
119+
120+| Key | Effect |
121+| --- | --- |
122+| `↑` `↓` `PgUp` `PgDn` | Move the cursor through what was said |
123+| `Shift-↑` `Shift-↓` | Select whole lines |
124+| Drag with the mouse | The same, by hand |
125+| `Ctrl-C` | Copy |
126+| `Esc` | Drop the selection |
127+| `Tab` | Back to the box |
128+
129+**With nothing selected, `Ctrl-C` copies the block the cursor is on** — one fenced code block, one paragraph, one tool's output — without the speaker's label above it and without the sentence after it. That is almost always what you wanted, and it saves selecting it by hand.
130+
131+What is copied goes to **two** clipboards: this editor's, so `Shift-Ins` pastes it into a file you have open, and your system's, so `Ctrl-V` pastes it anywhere else. The indentation the conversation is drawn with is taken off, so pasted code lands flush against the margin.
132+
133+The system half travels through your terminal (an escape sequence called OSC 52). Most terminals do it; a few refuse it for security, and some need it turned on. If `Ctrl-V` elsewhere gives you nothing, that is where to look — the editor's own clipboard has the text either way.
134+
135+## Answer the agent when it asks permission
136+
137+An agent with a shell or a filesystem toolset asks before it uses one. A dialog names the tool and the exact command, and offers the choices the agent itself proposed — normally *Allow this action*, *Allow and remember my choice*, and *Skip this action*.
138+
139+```
140+┌───────────── Bob (llama.cpp) wants to run ─────────────┐
141+│ │
142+│ Shell │
143+│ ls -1 │
144+│ │
145+│ [ Allow ] [ Allow always ] [ Skip ] │
146+└────────────────────────────────────────────────────────┘
147+```
148+
149+*Allow always* is remembered by the agent, not by the editor, so what it covers and how long it lasts are the agent's business. Escape is the same answer as *Skip*.
150+
151+Nothing runs before you answer. An agent waiting on a permission dialog is simply blocked, which is the point.
152+
153+## Let the agent see what you have not saved yet
154+
155+The editor offers the agent its own filesystem: when the agent reads a file you have open with unsaved changes, it is given **the text in the buffer**, not the older text on disk. That is usually what you want — you are asking about the edit you just made.
156+
157+When the agent writes a file, the change lands in the buffer and the window is marked modified, so you can read it, undo it with `Ctrl-Z`, or save it with `F2`. A file you do not have open is read from and written to disk directly.
158+
159+## Run several agents at once
160+
161+Each window is its own process and its own conversation. Opening the same agent twice gives two independent sessions, and opening two different agents lets you put a fast local model and a slower careful one side by side — **Window ▸ Tile** arranges them.
162+
163+Leaving the editor stops every agent.
164+
165+## Variants
166+
167+- **You want the agent to run somewhere other than the project root.** Add `cwd = "backend"` to its block. The path is relative to the project, and is both where the process starts and what the agent is told the working directory is.
168+- **The agent needs a credential.** Put it in `env`, or rely on it being in the environment you started the editor from — the agent inherits it.
169+- **The agent's commands do not appear when you type `/`.** Open **Agent ▸ Agent status** with the window in front. If it lists no commands, the agent announced none — or announced them in a shape this editor could not read, in which case the dialog names the update and the decoding error. To see exactly what went over the wire, start the editor with `TURBO_ACP_TRACE=/tmp/acp.log` and read the file: `->` is what the editor sent, `<-` what the agent answered.
170+- **The agent will not start.** **Agent ▸ Agent status** lists what was read from `acp.toml`, what each agent's command line came out as, and the error from anything that failed to start. Whatever the agent writes to its standard error is shown there too, which is where a misconfigured model endpoint reports itself.
171+- **You keep the same agent in every project.** Put the `[[agent]]` block in `~/.config/turbo-go/acp.toml` instead. A project's own file is read afterwards and an agent with the same `name` in it replaces yours.
172+
173+## See also
174+
175+- Every key of the file, and exactly how much of the protocol is implemented: [Agents and ACP reference](../reference/acp.md)
176+- Why an agent is a window rather than a panel, and why permissions are modal: [Agent windows](../explanation/agent-windows.md)
177+- The protocol itself: [agentclientprotocol.com](https://agentclientprotocol.com)
added docs/en/how-to/use-a-terminal.md +61 -0
new file mode 100644
@@ -0,0 +1,61 @@
1+# How to run shell commands without leaving the editor
2+
3+This guide shows how to open a terminal window, build and test the code you are editing in it, and get back to the file. It assumes you already have Turbo Go running with a file open.
4+
5+## Open a terminal
6+
7+Press `F8`, or choose **Window ▸ New terminal**.
8+
9+A new window opens running your shell, in the directory of the file you were editing. That is normally the directory you want: `go build ./...` and `git diff` both act on the package you are looking at.
10+
11+The window is called after the shell, and renames itself when a program inside it sets a title — `vim`, `htop` and `ssh` all do.
12+
13+## Run something
14+
15+Type into it as you would into any terminal. The shell gets nearly every key, including the ones the editor would otherwise use: `Ctrl-C` interrupts, `Ctrl-W` deletes a word, `Ctrl-R` searches the history.
16+
17+What the editor keeps is short, and deliberate — it is the way back out:
18+
19+| Key | Effect, even with a terminal in front |
20+| --- | --- |
21+| `F8` | Open another terminal |
22+| `F6` | Move to the next window |
23+| `F10` | Open the menu bar |
24+| `F2` `F3` `F4` | Save, Open, New |
25+| `Alt-1``Alt-9` | Bring that window forward |
26+| `Alt-X` | Leave the editor |
27+
28+## Read back through what scrolled off
29+
30+`Shift-PgUp` and `Shift-PgDn` walk the history a screenful at a time; the mouse wheel moves three lines. Two thousand lines are kept.
31+
32+Typing anything brings you straight back to the live screen, so you never have to scroll back down before running the next command.
33+
34+## Work with the file and the shell side by side
35+
36+A terminal is an ordinary window, so the window commands all apply to it:
37+
38+- **Window ▸ Tile** puts the file and the terminal side by side.
39+- **Window ▸ Maximise**, or the `[■]` box at the right of its title bar, gives the terminal the whole desktop while a build runs. The box then reads `[▬]`, and pressing it puts the window back.
40+- Drag its bottom-right corner to resize it — the shell is told its new size, so `less` and `vim` reflow.
41+
42+## Close it
43+
44+`Ctrl-W` is the shell's, not the editor's, so closing a terminal is done another way:
45+
46+- **File ▸ Close**, or
47+- click the `[x]` box in its top-left corner.
48+
49+Either ends the shell running in it. Nothing is asked first: a terminal holds a running process, not unsaved work, and closing the window is how you say you have finished with it. Leaving the editor closes every terminal at once.
50+
51+## Variants
52+
53+- **You want a different shell.** The shell is taken from `$SHELL`, falling back to `/bin/sh`; on Windows from `%COMSPEC%`, falling back to `cmd.exe`. Start the editor with `SHELL=/bin/zsh turbo-go` to change it for that run.
54+- **No file is open.** The terminal starts in the directory the editor was started from.
55+- **You are on Windows.** Terminal windows run in a pseudo-console (ConPTY), which needs Windows 10 version 1809 or later, and the shell is `%COMSPEC%` — cmd.exe. This path has been built and vetted but not yet run by the authors, who work on Linux and macOS. The first time you use it, try the five things it has to get right — `F8`, type `dir`, resize the window, run a menu command that says `output = "terminal"`, and interrupt a long one with `Ctrl-C` — and report whatever did not behave.
56+
57+## See also
58+
59+- Everything the terminal implements, exactly: [Terminal windows reference](../reference/terminal.md)
60+- Why it runs a real shell rather than capturing command output: [Terminal windows](../explanation/terminal-windows.md)
61+- The colours it uses: [Theme file format](../reference/themes.md)
new file mode 100644
@@ -0,0 +1,61 @@
1+# How to run shell commands without leaving the editor
2+
3+This guide shows how to open a terminal window, build and test the code you are editing in it, and get back to the file. It assumes you already have Turbo Go running with a file open.
4+
5+## Open a terminal
6+
7+Press `F8`, or choose **Window ▸ New terminal**.
8+
9+A new window opens running your shell, in the directory of the file you were editing. That is normally the directory you want: `go build ./...` and `git diff` both act on the package you are looking at.
10+
11+The window is called after the shell, and renames itself when a program inside it sets a title — `vim`, `htop` and `ssh` all do.
12+
13+## Run something
14+
15+Type into it as you would into any terminal. The shell gets nearly every key, including the ones the editor would otherwise use: `Ctrl-C` interrupts, `Ctrl-W` deletes a word, `Ctrl-R` searches the history.
16+
17+What the editor keeps is short, and deliberate — it is the way back out:
18+
19+| Key | Effect, even with a terminal in front |
20+| --- | --- |
21+| `F8` | Open another terminal |
22+| `F6` | Move to the next window |
23+| `F10` | Open the menu bar |
24+| `F2` `F3` `F4` | Save, Open, New |
25+| `Alt-1``Alt-9` | Bring that window forward |
26+| `Alt-X` | Leave the editor |
27+
28+## Read back through what scrolled off
29+
30+`Shift-PgUp` and `Shift-PgDn` walk the history a screenful at a time; the mouse wheel moves three lines. Two thousand lines are kept.
31+
32+Typing anything brings you straight back to the live screen, so you never have to scroll back down before running the next command.
33+
34+## Work with the file and the shell side by side
35+
36+A terminal is an ordinary window, so the window commands all apply to it:
37+
38+- **Window ▸ Tile** puts the file and the terminal side by side.
39+- **Window ▸ Maximise**, or the `[■]` box at the right of its title bar, gives the terminal the whole desktop while a build runs. The box then reads `[▬]`, and pressing it puts the window back.
40+- Drag its bottom-right corner to resize it — the shell is told its new size, so `less` and `vim` reflow.
41+
42+## Close it
43+
44+`Ctrl-W` is the shell's, not the editor's, so closing a terminal is done another way:
45+
46+- **File ▸ Close**, or
47+- click the `[x]` box in its top-left corner.
48+
49+Either ends the shell running in it. Nothing is asked first: a terminal holds a running process, not unsaved work, and closing the window is how you say you have finished with it. Leaving the editor closes every terminal at once.
50+
51+## Variants
52+
53+- **You want a different shell.** The shell is taken from `$SHELL`, falling back to `/bin/sh`; on Windows from `%COMSPEC%`, falling back to `cmd.exe`. Start the editor with `SHELL=/bin/zsh turbo-go` to change it for that run.
54+- **No file is open.** The terminal starts in the directory the editor was started from.
55+- **You are on Windows.** Terminal windows run in a pseudo-console (ConPTY), which needs Windows 10 version 1809 or later, and the shell is `%COMSPEC%` — cmd.exe. This path has been built and vetted but not yet run by the authors, who work on Linux and macOS. The first time you use it, try the five things it has to get right — `F8`, type `dir`, resize the window, run a menu command that says `output = "terminal"`, and interrupt a long one with `Ctrl-C` — and report whatever did not behave.
56+
57+## See also
58+
59+- Everything the terminal implements, exactly: [Terminal windows reference](../reference/terminal.md)
60+- Why it runs a real shell rather than capturing command output: [Terminal windows](../explanation/terminal-windows.md)
61+- The colours it uses: [Theme file format](../reference/themes.md)
added docs/en/how-to/use-snippets.md +93 -0
new file mode 100644
@@ -0,0 +1,93 @@
1+# How to insert snippets from a menu
2+
3+This guide shows how to set up reusable pieces of text and put them into a file at the cursor. It assumes Turbo Go is already installed.
4+
5+## Get a starter file
6+
7+Start the editor **from the project's own directory**, then choose **Snippets ▸ Create snippets file** (`Alt-N`, then `C`).
8+
9+That writes `.turbo-go/snippets.toml`, filled in with a few worked examples, and opens it — coloured, because Turbo Go colours TOML:
10+
11+```toml
12+[[snippet]]
13+name = "if err != nil"
14+group = "Go"
15+languages = ["go"]
16+body = """
17+if err != nil {
18+ return err
19+}"""
20+
21+[[snippet]]
22+name = "TODO"
23+body = "TODO: "
24+```
25+
26+Each `[[snippet]]` becomes one line of the menu. The file is read every time the menu opens, so editing it takes effect immediately — no restart.
27+
28+## Insert one
29+
30+Open **Snippets** (`Alt-N`). Snippets sharing a `group` appear together in a submenu of that name; one with no group goes into **General**.
31+
32+| Key | Effect |
33+| --- | --- |
34+| `Alt-N`, or `F10` then `→` to Snippets | Open the menu |
35+| `↑` `↓` | Move down the groups |
36+| `→`, or `Enter` | Open the highlighted group |
37+| `↑` `↓` then `Enter` | Insert the highlighted snippet |
38+| `←` | Back out of a group |
39+| `Escape` | Put the whole menu away |
40+
41+The snippet goes in at the cursor. **Lines after the first are indented to match the line you inserted it on**, so a multi-line snippet dropped into a nested block lands where you would have typed it:
42+
43+```
44+func f() {
45+ | ← cursor here
46+}
47+```
48+
49+becomes
50+
51+```
52+func f() {
53+ if err != nil {
54+ return err
55+ }
56+}
57+```
58+
59+It is one undo step: `Ctrl-Z` takes the whole snippet back out.
60+
61+## Keep snippets across every project
62+
63+Put them in `~/.config/turbo-go/snippets.toml` — the same directory your own themes go in. Those appear in every project, and a project's own file adds to them rather than replacing them.
64+
65+Where a project and you use the same `name` in the same `group`, **the project's wins**: it is the more specific statement of the two.
66+
67+## Show a snippet only where it makes sense
68+
69+Add `languages`, using the names the editor uses — `go`, `toml`, `markdown`, `javascript`, `html`, `bash`:
70+
71+```toml
72+[[snippet]]
73+name = "strict mode"
74+group = "Shell"
75+languages = ["bash"]
76+body = "set -euo pipefail"
77+```
78+
79+That snippet then appears only when a shell script is the front window. Leave `languages` out and the snippet is offered everywhere, which is what you want for a licence header or a `TODO`.
80+
81+A group left with nothing after filtering does not appear at all.
82+
83+## Variants
84+
85+- **You started the editor from a subdirectory.** The project's file is not found: only `./.turbo-go` is looked at, the same rule `settings.toml` follows. Your own snippets still appear.
86+- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read snippets` where the groups would be, and **Create snippets file** is still there. Open the file and fix it.
87+- **You want tabs in a body.** Write `\t`, as the starter file does — TOML turns it into a tab when it reads the file.
88+
89+## See also
90+
91+- Every key of the file and every rule: [Snippets reference](../reference/snippets.md)
92+- Why the menu is rebuilt each time it opens, and why insertion re-indents: [Snippets](../explanation/snippets.md)
93+- The other file in `.turbo-go`: [Project settings](../reference/project-settings.md)
new file mode 100644
@@ -0,0 +1,93 @@
1+# How to insert snippets from a menu
2+
3+This guide shows how to set up reusable pieces of text and put them into a file at the cursor. It assumes Turbo Go is already installed.
4+
5+## Get a starter file
6+
7+Start the editor **from the project's own directory**, then choose **Snippets ▸ Create snippets file** (`Alt-N`, then `C`).
8+
9+That writes `.turbo-go/snippets.toml`, filled in with a few worked examples, and opens it — coloured, because Turbo Go colours TOML:
10+
11+```toml
12+[[snippet]]
13+name = "if err != nil"
14+group = "Go"
15+languages = ["go"]
16+body = """
17+if err != nil {
18+ return err
19+}"""
20+
21+[[snippet]]
22+name = "TODO"
23+body = "TODO: "
24+```
25+
26+Each `[[snippet]]` becomes one line of the menu. The file is read every time the menu opens, so editing it takes effect immediately — no restart.
27+
28+## Insert one
29+
30+Open **Snippets** (`Alt-N`). Snippets sharing a `group` appear together in a submenu of that name; one with no group goes into **General**.
31+
32+| Key | Effect |
33+| --- | --- |
34+| `Alt-N`, or `F10` then `→` to Snippets | Open the menu |
35+| `↑` `↓` | Move down the groups |
36+| `→`, or `Enter` | Open the highlighted group |
37+| `↑` `↓` then `Enter` | Insert the highlighted snippet |
38+| `←` | Back out of a group |
39+| `Escape` | Put the whole menu away |
40+
41+The snippet goes in at the cursor. **Lines after the first are indented to match the line you inserted it on**, so a multi-line snippet dropped into a nested block lands where you would have typed it:
42+
43+```
44+func f() {
45+ | ← cursor here
46+}
47+```
48+
49+becomes
50+
51+```
52+func f() {
53+ if err != nil {
54+ return err
55+ }
56+}
57+```
58+
59+It is one undo step: `Ctrl-Z` takes the whole snippet back out.
60+
61+## Keep snippets across every project
62+
63+Put them in `~/.config/turbo-go/snippets.toml` — the same directory your own themes go in. Those appear in every project, and a project's own file adds to them rather than replacing them.
64+
65+Where a project and you use the same `name` in the same `group`, **the project's wins**: it is the more specific statement of the two.
66+
67+## Show a snippet only where it makes sense
68+
69+Add `languages`, using the names the editor uses — `go`, `toml`, `markdown`, `javascript`, `html`, `bash`:
70+
71+```toml
72+[[snippet]]
73+name = "strict mode"
74+group = "Shell"
75+languages = ["bash"]
76+body = "set -euo pipefail"
77+```
78+
79+That snippet then appears only when a shell script is the front window. Leave `languages` out and the snippet is offered everywhere, which is what you want for a licence header or a `TODO`.
80+
81+A group left with nothing after filtering does not appear at all.
82+
83+## Variants
84+
85+- **You started the editor from a subdirectory.** The project's file is not found: only `./.turbo-go` is looked at, the same rule `settings.toml` follows. Your own snippets still appear.
86+- **The file has a mistake in it.** The menu shows a greyed-out `Cannot read snippets` where the groups would be, and **Create snippets file** is still there. Open the file and fix it.
87+- **You want tabs in a body.** Write `\t`, as the starter file does — TOML turns it into a tab when it reads the file.
88+
89+## See also
90+
91+- Every key of the file and every rule: [Snippets reference](../reference/snippets.md)
92+- Why the menu is rebuilt each time it opens, and why insertion re-indents: [Snippets](../explanation/snippets.md)
93+- The other file in `.turbo-go`: [Project settings](../reference/project-settings.md)
added docs/en/how-to/write-a-theme.md +152 -0
new file mode 100644
@@ -0,0 +1,152 @@
1+# How to write your own theme
2+
3+This guide shows how to add a colour theme of your own. It assumes you know where your configuration directory is and can edit a TOML file.
4+
5+## 1. Find where themes go
6+
7+```bash
8+turbo-go -list-themes
9+```
10+
11+The last line tells you the directory — `~/.config/turbo-go/themes` on Linux, `~/Library/Application Support/turbo-go/themes` on macOS. Create it:
12+
13+```bash
14+mkdir -p ~/.config/turbo-go/themes
15+```
16+
17+## 2. Start from an existing theme
18+
19+The quickest start is to inherit from one that already works and override only what you want:
20+
21+```toml
22+# ~/.config/turbo-go/themes/mine.toml
23+name = "Mine"
24+description = "Turbo Classic, but the comments are readable."
25+inherits = "turbo-classic"
26+
27+[colors]
28+"syntax.comment" = { fg = "#8a8a8a", italic = true }
29+"syntax.string" = { fg = "#87d7af" }
30+```
31+
32+Everything you do not set is taken from `turbo-classic`.
33+
34+**Inherit from a theme whose ground is the same as yours.** The colours you leave out were chosen against the background of the theme you inherit from, so a dark theme built on `turbo-classic` will show, here and there, a colour picked for Borland navy. If you are writing a dark theme, inherit from `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` or `monochrome-dark`; if a light one, from `borland-light`, `catppuccin-latte`, `intellij-light` or `monochrome-light`. That is also why the eleven themes that ship in the binary each state their palette in full rather than inheriting most of it — a test holds them to it, because a shipped theme is one the project is answerable for.
35+
36+## 3. Use it
37+
38+```bash
39+turbo-go -theme mine main.go
40+```
41+
42+Or from inside the editor: `Options ▸ Theme…`, which lists every theme it can find.
43+
44+## 4. Iterate
45+
46+Edit the file, then restart the editor. There is no live reload.
47+
48+If the theme fails to load, Turbo Go falls back to the default rather than refusing to start. To see *why* it failed:
49+
50+```bash
51+turbo-go -list-themes
52+```
53+
54+A broken theme is listed with the parse error beside it — an unknown colour name is an error, not a silent fallback, so a typo is pointed at rather than quietly repainting half the screen.
55+
56+## 5. Check it stays readable
57+
58+The project holds every theme it ships to five measured rules, and they are worth applying to your own. `make test` runs them.
59+
60+| Rule | Why |
61+| --- | --- |
62+| The cursor is at least 64 apart from the line it sits on, in its strongest channel | A terminal draws its cursor over the cell; one that blends in cannot be found |
63+| The cursor is never a plain reversal of that line | A terminal that draws its cursor by inverting the cell would invert it back into invisibility |
64+| The current line is at least 16 from the page | `turbo-dark` once used ten, which is no highlight at all |
65+| Text you have to read is at least 64 from its background | Furniture — the desktop, a shadow, a scrollbar trough, a disabled entry — is exempt: it exists to recede |
66+| Comments read at 4.5:1 or better against their background, by WCAG relative luminance | A comment is prose, read word by word. `turbo-classic` drew them in `#808080` on its navy: 128 channel values apart, so the rule above waved it through, and 4.05:1 to read, which is below the W3C's floor for body text. Comments were the dimmest colour in six of the eight shipped themes. |
67+
68+
69+The contrast rule is applied to the six themes this project authors. The two Catppuccin themes are exempt, and the exemption is written where it is made: their colours are somebody else's published palette, faithfully copied, and Catppuccin puts comments at 2.87:1 in Frappé and 2.83:1 in Latte. A theme called Catppuccin that is not those exact values is a different theme wearing a borrowed name, so the fix — if anyone wants one — is upstream.
70+
71+It is applied to comments and to nothing else. `syntax.punctuation` is quieter still in several themes and stays that way: punctuation is recognised by shape, not read.
72+
73+A sixth rule catches the mistake no measurement finds: **two syntax classes a reader meets side by side must not be drawn identically**. `turbo-classic` once painted `syntax.link` the same lime as `syntax.string`, so a Markdown link and an inline code span were the same thing on screen — every colour readable, every key set, and the two simply equal. Both monochromes pass this rule with no hue at all, by using bold, italic and underline instead.
74+
75+## Variants
76+
77+**Override a shipped theme rather than adding one.** Name your file after it — `turbo-classic.toml` — and yours wins. The embedded one is not replaced, so deleting your file brings it back.
78+
79+**Start from scratch.** Leave `inherits` out. Set at least `default`; every key you do not set falls back along the dots to it, so a theme with one line is still a usable theme.
80+
81+**Colour only the syntax.** One key does it:
82+
83+```toml
84+[colors]
85+syntax = { fg = "silver" }
86+```
87+
88+`syntax.keyword`, `syntax.string` and the rest all fall back to it.
89+
90+**Keep the terminal's own colours.** Use `default` as a colour value:
91+
92+```toml
93+[colors]
94+"editor.text" = { fg = "default", bg = "default" }
95+```
96+
97+**Test it in a checkout without installing it.** Point the editor at any directory:
98+
99+```bash
100+TURBO_GO_THEME_DIR=./my-themes turbo-go -theme mine main.go
101+```
102+
103+**The cursor is hard to see.** `editor.cursor` does two things: its **background** is sent to the terminal as the cursor's own colour, and it also paints the cell underneath as a fallback for terminals that ignore that. Set it to something loud:
104+
105+```toml
106+[colors]
107+"editor.cursor" = { fg = "#000000", bg = "#ff8700" }
108+```
109+
110+Two things make a cursor colour a bad one, and the test suite rejects both: a plain reversal of the line — which terminals that draw their cursor by inverting the cell turn back into invisibility — and anything less than 64 channel values away from the line it sits on.
111+
112+**The cursor's line is hard to find.** That is `editor.currentline`, and it is a different key. It has to differ from `editor.text` by at least 16 channel values to count as a highlight at all.
113+
114+**Markdown and HTML look plain.** Five keys belong to the markup languages and have no equivalent in Go, so a theme written before they existed does not set them:
115+
116+```toml
117+[colors]
118+"syntax.heading" = { fg = "white", bold = true }
119+"syntax.tag" = { fg = "aqua" }
120+"syntax.attribute" = { fg = "yellow" }
121+"syntax.emphasis" = { fg = "fuchsia", bold = true }
122+"syntax.link" = { fg = "aqua", underline = true }
123+```
124+
125+Give `syntax.link` a different colour from `syntax.string`: a link and an inline `code` span sit side by side in most prose, and sharing a colour makes them one blur. The shipped `turbo-classic` had exactly that fault until it was looked at on a real terminal.
126+
127+**The project tree looks flat.** It has four keys of its own, and none of them falls back to `list`:
128+
129+```toml
130+[colors]
131+"tree.text" = { fg = "silver", bg = "navy" }
132+"tree.directory" = { fg = "white", bg = "navy", bold = true }
133+"tree.selected" = { fg = "black", bg = "aqua" }
134+"tree.unfocused" = { fg = "black", bg = "gray" }
135+```
136+
137+Give `tree.text` the same background as `window.body`, so the tree looks like part of its window, and make `tree.selected` clearly different from it — the test suite holds every shipped theme to at least 64 channel values between the two, because a highlight the same colour as the page is no highlight.
138+
139+**Terminal windows look wrong.** They have two keys of their own, and neither falls back to `editor`:
140+
141+```toml
142+[colors]
143+"terminal.text" = { fg = "silver", bg = "black" }
144+"terminal.cursor" = { fg = "black", bg = "aqua" }
145+```
146+
147+`terminal.text` is what a shell's output gets when it names no colour of its own — set it to something close to a real terminal rather than to your editor background, or `less` and `htop` will look out of place. A program that does name its colours keeps them either way.
148+
149+## See also
150+
151+- Every key you may set, and every colour name: [theme file reference](../reference/themes.md)
152+- Why the format is TOML with two kinds of inheritance: [Design decisions](../explanation/design-decisions.md)
new file mode 100644
@@ -0,0 +1,152 @@
1+# How to write your own theme
2+
3+This guide shows how to add a colour theme of your own. It assumes you know where your configuration directory is and can edit a TOML file.
4+
5+## 1. Find where themes go
6+
7+```bash
8+turbo-go -list-themes
9+```
10+
11+The last line tells you the directory — `~/.config/turbo-go/themes` on Linux, `~/Library/Application Support/turbo-go/themes` on macOS. Create it:
12+
13+```bash
14+mkdir -p ~/.config/turbo-go/themes
15+```
16+
17+## 2. Start from an existing theme
18+
19+The quickest start is to inherit from one that already works and override only what you want:
20+
21+```toml
22+# ~/.config/turbo-go/themes/mine.toml
23+name = "Mine"
24+description = "Turbo Classic, but the comments are readable."
25+inherits = "turbo-classic"
26+
27+[colors]
28+"syntax.comment" = { fg = "#8a8a8a", italic = true }
29+"syntax.string" = { fg = "#87d7af" }
30+```
31+
32+Everything you do not set is taken from `turbo-classic`.
33+
34+**Inherit from a theme whose ground is the same as yours.** The colours you leave out were chosen against the background of the theme you inherit from, so a dark theme built on `turbo-classic` will show, here and there, a colour picked for Borland navy. If you are writing a dark theme, inherit from `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` or `monochrome-dark`; if a light one, from `borland-light`, `catppuccin-latte`, `intellij-light` or `monochrome-light`. That is also why the eleven themes that ship in the binary each state their palette in full rather than inheriting most of it — a test holds them to it, because a shipped theme is one the project is answerable for.
35+
36+## 3. Use it
37+
38+```bash
39+turbo-go -theme mine main.go
40+```
41+
42+Or from inside the editor: `Options ▸ Theme…`, which lists every theme it can find.
43+
44+## 4. Iterate
45+
46+Edit the file, then restart the editor. There is no live reload.
47+
48+If the theme fails to load, Turbo Go falls back to the default rather than refusing to start. To see *why* it failed:
49+
50+```bash
51+turbo-go -list-themes
52+```
53+
54+A broken theme is listed with the parse error beside it — an unknown colour name is an error, not a silent fallback, so a typo is pointed at rather than quietly repainting half the screen.
55+
56+## 5. Check it stays readable
57+
58+The project holds every theme it ships to five measured rules, and they are worth applying to your own. `make test` runs them.
59+
60+| Rule | Why |
61+| --- | --- |
62+| The cursor is at least 64 apart from the line it sits on, in its strongest channel | A terminal draws its cursor over the cell; one that blends in cannot be found |
63+| The cursor is never a plain reversal of that line | A terminal that draws its cursor by inverting the cell would invert it back into invisibility |
64+| The current line is at least 16 from the page | `turbo-dark` once used ten, which is no highlight at all |
65+| Text you have to read is at least 64 from its background | Furniture — the desktop, a shadow, a scrollbar trough, a disabled entry — is exempt: it exists to recede |
66+| Comments read at 4.5:1 or better against their background, by WCAG relative luminance | A comment is prose, read word by word. `turbo-classic` drew them in `#808080` on its navy: 128 channel values apart, so the rule above waved it through, and 4.05:1 to read, which is below the W3C's floor for body text. Comments were the dimmest colour in six of the eight shipped themes. |
67+
68+
69+The contrast rule is applied to the six themes this project authors. The two Catppuccin themes are exempt, and the exemption is written where it is made: their colours are somebody else's published palette, faithfully copied, and Catppuccin puts comments at 2.87:1 in Frappé and 2.83:1 in Latte. A theme called Catppuccin that is not those exact values is a different theme wearing a borrowed name, so the fix — if anyone wants one — is upstream.
70+
71+It is applied to comments and to nothing else. `syntax.punctuation` is quieter still in several themes and stays that way: punctuation is recognised by shape, not read.
72+
73+A sixth rule catches the mistake no measurement finds: **two syntax classes a reader meets side by side must not be drawn identically**. `turbo-classic` once painted `syntax.link` the same lime as `syntax.string`, so a Markdown link and an inline code span were the same thing on screen — every colour readable, every key set, and the two simply equal. Both monochromes pass this rule with no hue at all, by using bold, italic and underline instead.
74+
75+## Variants
76+
77+**Override a shipped theme rather than adding one.** Name your file after it — `turbo-classic.toml` — and yours wins. The embedded one is not replaced, so deleting your file brings it back.
78+
79+**Start from scratch.** Leave `inherits` out. Set at least `default`; every key you do not set falls back along the dots to it, so a theme with one line is still a usable theme.
80+
81+**Colour only the syntax.** One key does it:
82+
83+```toml
84+[colors]
85+syntax = { fg = "silver" }
86+```
87+
88+`syntax.keyword`, `syntax.string` and the rest all fall back to it.
89+
90+**Keep the terminal's own colours.** Use `default` as a colour value:
91+
92+```toml
93+[colors]
94+"editor.text" = { fg = "default", bg = "default" }
95+```
96+
97+**Test it in a checkout without installing it.** Point the editor at any directory:
98+
99+```bash
100+TURBO_GO_THEME_DIR=./my-themes turbo-go -theme mine main.go
101+```
102+
103+**The cursor is hard to see.** `editor.cursor` does two things: its **background** is sent to the terminal as the cursor's own colour, and it also paints the cell underneath as a fallback for terminals that ignore that. Set it to something loud:
104+
105+```toml
106+[colors]
107+"editor.cursor" = { fg = "#000000", bg = "#ff8700" }
108+```
109+
110+Two things make a cursor colour a bad one, and the test suite rejects both: a plain reversal of the line — which terminals that draw their cursor by inverting the cell turn back into invisibility — and anything less than 64 channel values away from the line it sits on.
111+
112+**The cursor's line is hard to find.** That is `editor.currentline`, and it is a different key. It has to differ from `editor.text` by at least 16 channel values to count as a highlight at all.
113+
114+**Markdown and HTML look plain.** Five keys belong to the markup languages and have no equivalent in Go, so a theme written before they existed does not set them:
115+
116+```toml
117+[colors]
118+"syntax.heading" = { fg = "white", bold = true }
119+"syntax.tag" = { fg = "aqua" }
120+"syntax.attribute" = { fg = "yellow" }
121+"syntax.emphasis" = { fg = "fuchsia", bold = true }
122+"syntax.link" = { fg = "aqua", underline = true }
123+```
124+
125+Give `syntax.link` a different colour from `syntax.string`: a link and an inline `code` span sit side by side in most prose, and sharing a colour makes them one blur. The shipped `turbo-classic` had exactly that fault until it was looked at on a real terminal.
126+
127+**The project tree looks flat.** It has four keys of its own, and none of them falls back to `list`:
128+
129+```toml
130+[colors]
131+"tree.text" = { fg = "silver", bg = "navy" }
132+"tree.directory" = { fg = "white", bg = "navy", bold = true }
133+"tree.selected" = { fg = "black", bg = "aqua" }
134+"tree.unfocused" = { fg = "black", bg = "gray" }
135+```
136+
137+Give `tree.text` the same background as `window.body`, so the tree looks like part of its window, and make `tree.selected` clearly different from it — the test suite holds every shipped theme to at least 64 channel values between the two, because a highlight the same colour as the page is no highlight.
138+
139+**Terminal windows look wrong.** They have two keys of their own, and neither falls back to `editor`:
140+
141+```toml
142+[colors]
143+"terminal.text" = { fg = "silver", bg = "black" }
144+"terminal.cursor" = { fg = "black", bg = "aqua" }
145+```
146+
147+`terminal.text` is what a shell's output gets when it names no colour of its own — set it to something close to a real terminal rather than to your editor background, or `less` and `htop` will look out of place. A program that does name its colours keeps them either way.
148+
149+## See also
150+
151+- Every key you may set, and every colour name: [theme file reference](../reference/themes.md)
152+- Why the format is TOML with two kinds of inheritance: [Design decisions](../explanation/design-decisions.md)
added docs/en/reference/acp.md +239 -0
new file mode 100644
@@ -0,0 +1,239 @@
1+# Agents and ACP
2+
3+Turbo Go is a client for the [Agent Client Protocol](https://agentclientprotocol.com). It starts each agent as a child process and exchanges JSON-RPC 2.0 messages with it over stdin and stdout, one message per line.
4+
5+## Where the file lives
6+
7+| Path | Read | Purpose |
8+| --- | --- | --- |
9+| `~/.config/turbo-go/acp.toml` | first | Agents you want in every project |
10+| `<project>/.turbo-go/acp.toml` | second | Agents belonging to this project |
11+
12+Both are optional. Where an agent's `name` appears in both, the project's replaces the user's, being the more specific statement — the same rule [snippets](snippets.md) follow. A missing file is not an error; a file that is present but unreadable is, and is reported under **Agent ▸ Agent status** rather than silently leaving the menu empty.
13+
14+`TURBO_GO_DIR` overrides the directory the user-level file is looked for in. The project file is always `.turbo-go/acp.toml` under the directory the editor was started in — there is no walk up the tree, for the same reason [project settings](project-settings.md) do not walk up.
15+
16+## File format
17+
18+One `[[agent]]` block per agent, in the order you want them in the menu.
19+
20+```toml
21+[[agent]]
22+name = "Bob (llama.cpp)"
23+command = "docker"
24+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
25+env = { TELEMETRY_ENABLED = "false" }
26+cwd = "."
27+```
28+
29+| Key | Type | Required | Meaning |
30+| --- | --- | --- | --- |
31+| `name` | string | **yes** | What the Agent menu shows and what the window is titled. Must be unique within the merged set. |
32+| `command` | string | **yes** | The executable to run. Looked up on `PATH` unless it contains a separator. |
33+| `args` | list of strings | no | Its arguments, passed as given — no shell, so no quoting, globbing or `&&`. |
34+| `env` | table of strings | no | Environment variables added to the ones the editor was started with. A name given here wins. |
35+| `cwd` | string | no | Where the process starts, and the `cwd` the agent is told about. Relative to the project root. Defaults to the project root. |
36+
37+`env` may also be written as a sub-table, which is the same thing:
38+
39+```toml
40+[[agent]]
41+name = "Bob (llama.cpp)"
42+command = "docker"
43+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
44+
45+[agent.env]
46+TELEMETRY_ENABLED = "false"
47+```
48+
49+### What is refused
50+
51+The file is refused as a whole, rather than partly loaded, when any of these hold. A half-loaded menu offering three of your five agents is worse than an error saying why.
52+
53+| Problem | Message |
54+| --- | --- |
55+| an agent with no `name` | `reading …/acp.toml: agent 1 has no name` |
56+| an agent with no `command` | `reading …/acp.toml: agent "Bob" has no command` |
57+| two agents with the same `name` | `reading …/acp.toml: two agents are called "Bob"` |
58+| a key the format does not define | `reading …/acp.toml: agent.comand is not a key this file has` |
59+
60+The last one is deliberate: a misspelt key that was quietly ignored would look exactly like one that had no effect.
61+
62+## The Agent menu
63+
64+`Alt-A` opens it. It is on the bar whether or not any agent is configured, because that is where **Create agents file** has to be reachable from.
65+
66+| Item | Enabled when | Effect |
67+| --- | --- | --- |
68+| *one item per agent, by name* | always | Start that agent and open a window on it |
69+| **Create agents file** | no `acp.toml` in the project | Write the starter file and open it |
70+| **Cancel turn** | a turn is running in the front window | `session/cancel` |
71+| **Agent status** | always | What was loaded, what each command line is, and what failed |
72+
73+## Keys inside an agent window
74+
75+An agent window is an ordinary window: `F6`, `Alt-1``Alt-9`, Tile, Maximise, `[x]` and `[■]` all work on it. Inside it:
76+
77+| Key | Effect |
78+| --- | --- |
79+| `Enter` | Send the input box as a prompt |
80+| `Alt-Enter` | Insert a newline in the input box |
81+| `Tab` | Move focus between the conversation and the input box |
82+| `Ctrl-C`, `Ctrl-Ins` | Copy the selection, or the block the cursor is on |
83+| `Esc` | Drop the selection; with none, cancel the turn in progress |
84+| `Ctrl-W` | Close the window and stop the agent |
85+
86+With the **input box** focused:
87+
88+| Key | Effect |
89+| --- | --- |
90+| `↑` `↓` `←` `→` `Home` `End` | Move the cursor in what you are typing |
91+| `Backspace` `Delete` | Edit it; backspace at the start of a line joins it to the one above |
92+| `/` as the first character | Open the list of the agent's commands — see [Commands and mentions](#commands-and-mentions) |
93+| `@` | Open the list of the project's files, narrowed by what you type after it |
94+| `↑` `↓` `PgUp` `PgDn`, list open | Move through the list |
95+| `Tab`, list open | Take the highlighted entry |
96+| `Enter`, list open | Take the highlighted entry; on a word that is already complete, send |
97+| `Esc`, list open | Close the list until the text changes |
98+
99+With the **conversation** focused:
100+
101+| Key | Effect |
102+| --- | --- |
103+| `↑` `↓` | Move the cursor one line |
104+| `PgUp` `PgDn` | Move it a screenful |
105+| `Home` `End` | The start of the conversation, and the end |
106+| `Shift-` any of those | Extend the selection instead |
107+| Drag with button 1 | Select by hand |
108+| Wheel | Scroll three lines, leaving the cursor where it is |
109+
110+Unlike a terminal window, an agent window does **not** take the editor's shortcuts: there is no shell to need `Ctrl-F`, so it keeps its usual meaning. `Ctrl-C` is the exception, and only because nothing else in an agent window wants it.
111+
112+## Commands and mentions
113+
114+Two characters open a list over the bottom of the conversation while you type. They are the same two Zed uses, so an agent's own documentation — "type `/web` to search" — holds here too.
115+
116+### `/` — the agent's commands
117+
118+An agent may announce commands with `available_commands_update`, at the start of the session or at any point during it. Typing `/` as the **first character** of the box lists them: the name, the agent's description, and, in angle brackets, what it expects after the name when it expects something. Keep typing to narrow the list; the match is on the start of the name and ignores case.
119+
120+`Tab` completes the highlighted command. A command that takes input is completed with a trailing space, so the next thing you type is its argument; one that takes none is completed to the bare name. `Enter` completes too, except on a word that already reads exactly as a command, where it sends.
121+
122+On the wire a command is **text**: `/web agent client protocol` goes out as one text block, and the agent recognises it by its first word. That is the whole protocol for commands, and it is why a `/` anywhere but the start of the box is just a character.
123+
124+With no commands announced, `/` is a character and `Tab` keeps its ordinary meaning. **Agent ▸ Agent status** lists the commands with their descriptions.
125+
126+### `@` — a file from the project
127+
128+Typing `@` anywhere in the box lists the project's files, relative to the project root with forward slashes. What you type after the `@` narrows the list: files whose own name begins with it come first, then files whose path merely contains it. `Tab` or `Enter` completes the highlighted one and adds a space.
129+
130+When the prompt is sent, each `@name` that names a file the list knew becomes a content block **in place of the name**:
131+
132+| The agent declared | The block sent |
133+| --- | --- |
134+| `promptCapabilities.embeddedContext: true` | `resource` — the file's `uri`, `mimeType` and full `text`, read the way `fs/read_text_file` reads it: from the open buffer when the file is open and modified |
135+| anything else, or the file could not be read | `resource_link` — the `uri`, `name` and `mimeType`, for the agent to fetch itself |
136+
137+The words either side go as text blocks, so `explain @docs/README.md please` is three blocks: `explain `, the file, ` please`. The conversation keeps the line as you typed it.
138+
139+A word that begins with `@` and names no file stays text — an e-mail address in a prompt is not a file — and `@main.go` does not name `main.gopher`: the name has to end the word.
140+
141+The list is the project walked from its root, `.git` left out, at most 5 000 files, and at most 200 of them shown at once. Past either limit, type one more letter. It is walked afresh each time `@` opens the list, so a file the agent just created is in it.
142+
143+## Copying
144+
145+Selection is by **whole lines**. Nothing in a conversation is edited, so half a line is never what somebody means, and whole lines keep a copied code block's indentation intact.
146+
147+With nothing selected, copying takes the **region the cursor is on**: one fenced code block, one passage of prose, one tool call's output. A speaker's label and a tool call's heading are furniture and are regions of their own, so neither is ever copied with what it sits above.
148+
149+The indentation the conversation is drawn with is removed, so pasted code is flush.
150+
151+The text goes to two places at once:
152+
153+| Clipboard | How | Pasted with |
154+| --- | --- | --- |
155+| The editor's | directly | `Shift-Ins`, into a file open here |
156+| The system's | OSC 52, through the terminal | `Ctrl-V`, anywhere else |
157+
158+Nothing checks whether the terminal accepted the second: there is no reply to check, and a terminal may refuse OSC 52 for security or need it turned on. The editor's own clipboard has the text either way, and the status bar says how many lines were copied.
159+
160+## How much of the protocol is implemented
161+
162+Protocol version **1**. Turbo Go sends its version in `initialize` and accepts whatever version the agent answers with, provided it is one it knows.
163+
164+### What the editor calls on the agent
165+
166+| Method | Implemented | Notes |
167+| --- | --- | --- |
168+| `initialize` | yes | Advertises the `fs` capability below; `terminal` is not advertised |
169+| `session/new` | yes | `cwd` from the agent's `cwd` key; `mcpServers` is always empty — MCP servers are the agent's own business |
170+| `session/prompt` | yes | Text blocks, and one `resource` or `resource_link` block per file named with `@` — see [Commands and mentions](#commands-and-mentions) |
171+| `session/cancel` | yes | `Esc`, and **Agent ▸ Cancel turn** |
172+| `session/load` | **no** | Conversations do not survive closing the window |
173+| `authenticate` | **no** | An agent that lists `authMethods` is reported as needing a login the editor cannot perform |
174+
175+### What the agent may call on the editor
176+
177+| Method | Implemented | Notes |
178+| --- | --- | --- |
179+| `session/update` | yes | See the table below |
180+| `session/request_permission` | yes | A modal dialog carrying the agent's own options |
181+| `fs/read_text_file` | yes | From the open buffer when the file is open and modified, otherwise from disk |
182+| `fs/write_text_file` | yes | Into the open buffer when the file is open, otherwise to disk |
183+| `terminal/*` | **no** | Not advertised, so a conforming agent will not ask |
184+
185+### Session updates
186+
187+| `sessionUpdate` | Shown as |
188+| --- | --- |
189+| `agent_message_chunk` | The agent's reply, appended as it arrives |
190+| `agent_thought_chunk` | The same, in the comment colour, under a *thinking* label |
191+| `user_message_chunk` | Your own message, as the agent echoes it back |
192+| `tool_call` | A line naming the tool and its title, with its status |
193+| `tool_call_update` | Folded onto the line the `toolCallId` matches, carrying its output |
194+| `plan` | The entries as a list, each with its status |
195+| `available_commands_update` | The list `/` opens in the input box; also listed, with descriptions, by **Agent ▸ Agent status** |
196+| `usage_update` | The token count on the status bar while the window is in front |
197+| anything else | Ignored, and counted; the count is in **Agent status** |
198+
199+While a turn is running, the rule between the panes turns a spinner. It is drawn from the clock rather than from a counter, so two windows thinking at once turn in step and nothing has to be reset when a turn begins. The window's *title* deliberately does not animate: it is also what the window list and the `Alt`-digit menu show, and a name changing eight times a second makes both flicker.
200+
201+An unknown update is ignored rather than refused: the protocol grows, and an editor that stopped talking to an agent because it learnt a new kind of message would be wrong more often than it was right.
202+
203+## Colouring
204+
205+The conversation is drawn with keys every theme already sets, so none of them needed touching:
206+
207+| Part | Class |
208+| --- | --- |
209+| A speaker's name | `syntax.keyword` |
210+| A thought | `syntax.comment` |
211+| A tool call and its status | `syntax.type` |
212+| A failed tool call, and the editor's own notices | `diagnostic.error` |
213+| A selected line, and the cursor's bar | `editor.selection` |
214+| Code inside a fence | the scanner for the fence's language |
215+| Everything else | the window's plain text |
216+
217+A fenced block naming a language the editor colours — `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — is coloured by that scanner. One naming anything else, or nothing at all, is left plain.
218+
219+## Tracing the conversation with an agent
220+
221+| Variable | Effect |
222+| --- | --- |
223+| `TURBO_ACP_TRACE=<file>` | Append every message to and from every agent to that file, one per line, stamped with the time and marked `->` (sent) or `<-` (received) |
224+
225+It is for the one question the screen cannot answer — *what did the agent actually send?* An update this editor cannot decode is counted under **Agent ▸ Agent status**, which also names the last one and its error; the trace shows the message itself. A file that cannot be opened means no trace and nothing else: the trace is never allowed to break the editor.
226+
227+## Limits
228+
229+- **One session per window.** Closing the window ends the session; there is no resume.
230+- **Text and files only.** The editor sends text, and the files you name with `@`; not images or audio, whatever the agent's `promptCapabilities` say.
231+- **No authentication.** An agent that requires a login must be logged in by its own CLI before the editor starts it.
232+- **`args` are not a shell command.** `command = "sh"`, `args = ["-c", "…"]` is how to get one deliberately.
233+- **One entry is capped** at a megabyte of text. An agent printing a whole build log cannot make the window unusable; what was dropped is said in the entry itself.
234+
235+## See also
236+
237+- The task: [How to talk to a coding agent from the editor](../how-to/talk-to-an-agent.md)
238+- The reasoning: [Agent windows](../explanation/agent-windows.md)
239+- The protocol: [agentclientprotocol.com](https://agentclientprotocol.com)
new file mode 100644
@@ -0,0 +1,239 @@
1+# Agents and ACP
2+
3+Turbo Go is a client for the [Agent Client Protocol](https://agentclientprotocol.com). It starts each agent as a child process and exchanges JSON-RPC 2.0 messages with it over stdin and stdout, one message per line.
4+
5+## Where the file lives
6+
7+| Path | Read | Purpose |
8+| --- | --- | --- |
9+| `~/.config/turbo-go/acp.toml` | first | Agents you want in every project |
10+| `<project>/.turbo-go/acp.toml` | second | Agents belonging to this project |
11+
12+Both are optional. Where an agent's `name` appears in both, the project's replaces the user's, being the more specific statement — the same rule [snippets](snippets.md) follow. A missing file is not an error; a file that is present but unreadable is, and is reported under **Agent ▸ Agent status** rather than silently leaving the menu empty.
13+
14+`TURBO_GO_DIR` overrides the directory the user-level file is looked for in. The project file is always `.turbo-go/acp.toml` under the directory the editor was started in — there is no walk up the tree, for the same reason [project settings](project-settings.md) do not walk up.
15+
16+## File format
17+
18+One `[[agent]]` block per agent, in the order you want them in the menu.
19+
20+```toml
21+[[agent]]
22+name = "Bob (llama.cpp)"
23+command = "docker"
24+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
25+env = { TELEMETRY_ENABLED = "false" }
26+cwd = "."
27+```
28+
29+| Key | Type | Required | Meaning |
30+| --- | --- | --- | --- |
31+| `name` | string | **yes** | What the Agent menu shows and what the window is titled. Must be unique within the merged set. |
32+| `command` | string | **yes** | The executable to run. Looked up on `PATH` unless it contains a separator. |
33+| `args` | list of strings | no | Its arguments, passed as given — no shell, so no quoting, globbing or `&&`. |
34+| `env` | table of strings | no | Environment variables added to the ones the editor was started with. A name given here wins. |
35+| `cwd` | string | no | Where the process starts, and the `cwd` the agent is told about. Relative to the project root. Defaults to the project root. |
36+
37+`env` may also be written as a sub-table, which is the same thing:
38+
39+```toml
40+[[agent]]
41+name = "Bob (llama.cpp)"
42+command = "docker"
43+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
44+
45+[agent.env]
46+TELEMETRY_ENABLED = "false"
47+```
48+
49+### What is refused
50+
51+The file is refused as a whole, rather than partly loaded, when any of these hold. A half-loaded menu offering three of your five agents is worse than an error saying why.
52+
53+| Problem | Message |
54+| --- | --- |
55+| an agent with no `name` | `reading …/acp.toml: agent 1 has no name` |
56+| an agent with no `command` | `reading …/acp.toml: agent "Bob" has no command` |
57+| two agents with the same `name` | `reading …/acp.toml: two agents are called "Bob"` |
58+| a key the format does not define | `reading …/acp.toml: agent.comand is not a key this file has` |
59+
60+The last one is deliberate: a misspelt key that was quietly ignored would look exactly like one that had no effect.
61+
62+## The Agent menu
63+
64+`Alt-A` opens it. It is on the bar whether or not any agent is configured, because that is where **Create agents file** has to be reachable from.
65+
66+| Item | Enabled when | Effect |
67+| --- | --- | --- |
68+| *one item per agent, by name* | always | Start that agent and open a window on it |
69+| **Create agents file** | no `acp.toml` in the project | Write the starter file and open it |
70+| **Cancel turn** | a turn is running in the front window | `session/cancel` |
71+| **Agent status** | always | What was loaded, what each command line is, and what failed |
72+
73+## Keys inside an agent window
74+
75+An agent window is an ordinary window: `F6`, `Alt-1``Alt-9`, Tile, Maximise, `[x]` and `[■]` all work on it. Inside it:
76+
77+| Key | Effect |
78+| --- | --- |
79+| `Enter` | Send the input box as a prompt |
80+| `Alt-Enter` | Insert a newline in the input box |
81+| `Tab` | Move focus between the conversation and the input box |
82+| `Ctrl-C`, `Ctrl-Ins` | Copy the selection, or the block the cursor is on |
83+| `Esc` | Drop the selection; with none, cancel the turn in progress |
84+| `Ctrl-W` | Close the window and stop the agent |
85+
86+With the **input box** focused:
87+
88+| Key | Effect |
89+| --- | --- |
90+| `↑` `↓` `←` `→` `Home` `End` | Move the cursor in what you are typing |
91+| `Backspace` `Delete` | Edit it; backspace at the start of a line joins it to the one above |
92+| `/` as the first character | Open the list of the agent's commands — see [Commands and mentions](#commands-and-mentions) |
93+| `@` | Open the list of the project's files, narrowed by what you type after it |
94+| `↑` `↓` `PgUp` `PgDn`, list open | Move through the list |
95+| `Tab`, list open | Take the highlighted entry |
96+| `Enter`, list open | Take the highlighted entry; on a word that is already complete, send |
97+| `Esc`, list open | Close the list until the text changes |
98+
99+With the **conversation** focused:
100+
101+| Key | Effect |
102+| --- | --- |
103+| `↑` `↓` | Move the cursor one line |
104+| `PgUp` `PgDn` | Move it a screenful |
105+| `Home` `End` | The start of the conversation, and the end |
106+| `Shift-` any of those | Extend the selection instead |
107+| Drag with button 1 | Select by hand |
108+| Wheel | Scroll three lines, leaving the cursor where it is |
109+
110+Unlike a terminal window, an agent window does **not** take the editor's shortcuts: there is no shell to need `Ctrl-F`, so it keeps its usual meaning. `Ctrl-C` is the exception, and only because nothing else in an agent window wants it.
111+
112+## Commands and mentions
113+
114+Two characters open a list over the bottom of the conversation while you type. They are the same two Zed uses, so an agent's own documentation — "type `/web` to search" — holds here too.
115+
116+### `/` — the agent's commands
117+
118+An agent may announce commands with `available_commands_update`, at the start of the session or at any point during it. Typing `/` as the **first character** of the box lists them: the name, the agent's description, and, in angle brackets, what it expects after the name when it expects something. Keep typing to narrow the list; the match is on the start of the name and ignores case.
119+
120+`Tab` completes the highlighted command. A command that takes input is completed with a trailing space, so the next thing you type is its argument; one that takes none is completed to the bare name. `Enter` completes too, except on a word that already reads exactly as a command, where it sends.
121+
122+On the wire a command is **text**: `/web agent client protocol` goes out as one text block, and the agent recognises it by its first word. That is the whole protocol for commands, and it is why a `/` anywhere but the start of the box is just a character.
123+
124+With no commands announced, `/` is a character and `Tab` keeps its ordinary meaning. **Agent ▸ Agent status** lists the commands with their descriptions.
125+
126+### `@` — a file from the project
127+
128+Typing `@` anywhere in the box lists the project's files, relative to the project root with forward slashes. What you type after the `@` narrows the list: files whose own name begins with it come first, then files whose path merely contains it. `Tab` or `Enter` completes the highlighted one and adds a space.
129+
130+When the prompt is sent, each `@name` that names a file the list knew becomes a content block **in place of the name**:
131+
132+| The agent declared | The block sent |
133+| --- | --- |
134+| `promptCapabilities.embeddedContext: true` | `resource` — the file's `uri`, `mimeType` and full `text`, read the way `fs/read_text_file` reads it: from the open buffer when the file is open and modified |
135+| anything else, or the file could not be read | `resource_link` — the `uri`, `name` and `mimeType`, for the agent to fetch itself |
136+
137+The words either side go as text blocks, so `explain @docs/README.md please` is three blocks: `explain `, the file, ` please`. The conversation keeps the line as you typed it.
138+
139+A word that begins with `@` and names no file stays text — an e-mail address in a prompt is not a file — and `@main.go` does not name `main.gopher`: the name has to end the word.
140+
141+The list is the project walked from its root, `.git` left out, at most 5 000 files, and at most 200 of them shown at once. Past either limit, type one more letter. It is walked afresh each time `@` opens the list, so a file the agent just created is in it.
142+
143+## Copying
144+
145+Selection is by **whole lines**. Nothing in a conversation is edited, so half a line is never what somebody means, and whole lines keep a copied code block's indentation intact.
146+
147+With nothing selected, copying takes the **region the cursor is on**: one fenced code block, one passage of prose, one tool call's output. A speaker's label and a tool call's heading are furniture and are regions of their own, so neither is ever copied with what it sits above.
148+
149+The indentation the conversation is drawn with is removed, so pasted code is flush.
150+
151+The text goes to two places at once:
152+
153+| Clipboard | How | Pasted with |
154+| --- | --- | --- |
155+| The editor's | directly | `Shift-Ins`, into a file open here |
156+| The system's | OSC 52, through the terminal | `Ctrl-V`, anywhere else |
157+
158+Nothing checks whether the terminal accepted the second: there is no reply to check, and a terminal may refuse OSC 52 for security or need it turned on. The editor's own clipboard has the text either way, and the status bar says how many lines were copied.
159+
160+## How much of the protocol is implemented
161+
162+Protocol version **1**. Turbo Go sends its version in `initialize` and accepts whatever version the agent answers with, provided it is one it knows.
163+
164+### What the editor calls on the agent
165+
166+| Method | Implemented | Notes |
167+| --- | --- | --- |
168+| `initialize` | yes | Advertises the `fs` capability below; `terminal` is not advertised |
169+| `session/new` | yes | `cwd` from the agent's `cwd` key; `mcpServers` is always empty — MCP servers are the agent's own business |
170+| `session/prompt` | yes | Text blocks, and one `resource` or `resource_link` block per file named with `@` — see [Commands and mentions](#commands-and-mentions) |
171+| `session/cancel` | yes | `Esc`, and **Agent ▸ Cancel turn** |
172+| `session/load` | **no** | Conversations do not survive closing the window |
173+| `authenticate` | **no** | An agent that lists `authMethods` is reported as needing a login the editor cannot perform |
174+
175+### What the agent may call on the editor
176+
177+| Method | Implemented | Notes |
178+| --- | --- | --- |
179+| `session/update` | yes | See the table below |
180+| `session/request_permission` | yes | A modal dialog carrying the agent's own options |
181+| `fs/read_text_file` | yes | From the open buffer when the file is open and modified, otherwise from disk |
182+| `fs/write_text_file` | yes | Into the open buffer when the file is open, otherwise to disk |
183+| `terminal/*` | **no** | Not advertised, so a conforming agent will not ask |
184+
185+### Session updates
186+
187+| `sessionUpdate` | Shown as |
188+| --- | --- |
189+| `agent_message_chunk` | The agent's reply, appended as it arrives |
190+| `agent_thought_chunk` | The same, in the comment colour, under a *thinking* label |
191+| `user_message_chunk` | Your own message, as the agent echoes it back |
192+| `tool_call` | A line naming the tool and its title, with its status |
193+| `tool_call_update` | Folded onto the line the `toolCallId` matches, carrying its output |
194+| `plan` | The entries as a list, each with its status |
195+| `available_commands_update` | The list `/` opens in the input box; also listed, with descriptions, by **Agent ▸ Agent status** |
196+| `usage_update` | The token count on the status bar while the window is in front |
197+| anything else | Ignored, and counted; the count is in **Agent status** |
198+
199+While a turn is running, the rule between the panes turns a spinner. It is drawn from the clock rather than from a counter, so two windows thinking at once turn in step and nothing has to be reset when a turn begins. The window's *title* deliberately does not animate: it is also what the window list and the `Alt`-digit menu show, and a name changing eight times a second makes both flicker.
200+
201+An unknown update is ignored rather than refused: the protocol grows, and an editor that stopped talking to an agent because it learnt a new kind of message would be wrong more often than it was right.
202+
203+## Colouring
204+
205+The conversation is drawn with keys every theme already sets, so none of them needed touching:
206+
207+| Part | Class |
208+| --- | --- |
209+| A speaker's name | `syntax.keyword` |
210+| A thought | `syntax.comment` |
211+| A tool call and its status | `syntax.type` |
212+| A failed tool call, and the editor's own notices | `diagnostic.error` |
213+| A selected line, and the cursor's bar | `editor.selection` |
214+| Code inside a fence | the scanner for the fence's language |
215+| Everything else | the window's plain text |
216+
217+A fenced block naming a language the editor colours — `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — is coloured by that scanner. One naming anything else, or nothing at all, is left plain.
218+
219+## Tracing the conversation with an agent
220+
221+| Variable | Effect |
222+| --- | --- |
223+| `TURBO_ACP_TRACE=<file>` | Append every message to and from every agent to that file, one per line, stamped with the time and marked `->` (sent) or `<-` (received) |
224+
225+It is for the one question the screen cannot answer — *what did the agent actually send?* An update this editor cannot decode is counted under **Agent ▸ Agent status**, which also names the last one and its error; the trace shows the message itself. A file that cannot be opened means no trace and nothing else: the trace is never allowed to break the editor.
226+
227+## Limits
228+
229+- **One session per window.** Closing the window ends the session; there is no resume.
230+- **Text and files only.** The editor sends text, and the files you name with `@`; not images or audio, whatever the agent's `promptCapabilities` say.
231+- **No authentication.** An agent that requires a login must be logged in by its own CLI before the editor starts it.
232+- **`args` are not a shell command.** `command = "sh"`, `args = ["-c", "…"]` is how to get one deliberately.
233+- **One entry is capped** at a megabyte of text. An agent printing a whole build log cannot make the window unusable; what was dropped is said in the entry itself.
234+
235+## See also
236+
237+- The task: [How to talk to a coding agent from the editor](../how-to/talk-to-an-agent.md)
238+- The reasoning: [Agent windows](../explanation/agent-windows.md)
239+- The protocol: [agentclientprotocol.com](https://agentclientprotocol.com)
added docs/en/reference/cli.md +100 -0
new file mode 100644
@@ -0,0 +1,100 @@
1+# Reference: command line
2+
3+> Neutral description of the `turbo-go` command, its flags, and the environment it reads.
4+
5+## Synopsis
6+
7+```
8+turbo-go [flags] [file...]
9+```
10+
11+Each `file` is opened in its own window. A file that does not exist yet is opened as an empty buffer bound to that path. With no file at all, one empty untitled window is opened.
12+
13+## Flags
14+
15+| Flag | Type | Default | Description |
16+| --- | --- | --- | --- |
17+| `-theme <name>` | string | the project's, else `turbo-classic` | Theme to start with, overriding the project's own. An unknown name falls back to the default without an error. |
18+| `-list-themes` | bool | `false` | Print every loadable theme with its description, then the user theme directory, and exit. |
19+| `-no-lsp` | bool | `false` | Do not start a language server. Colouring and editing are unaffected. |
20+| `-version` | bool | `false` | Print `Turbo Go <version>` on one line, with the commit and build date when the build recorded them, and exit. See [the version number](versioning.md). |
21+| `-h`, `-help` | bool | `false` | Print the flag list and exit. |
22+
23+## Environment
24+
25+| Variable | Read by | Effect |
26+| --- | --- | --- |
27+| `TURBO_GO_THEME_DIR` | theme loading | Directory to read user themes from, instead of the platform configuration directory. |
28+| `TERM` | tcell | Which terminal description to use. |
29+| `GOBIN`, `GOPATH`, `HOME` | gopls lookup | Searched, in that order, when `gopls` is not on `PATH`. |
30+
31+## Files
32+
33+| Path | Purpose |
34+| --- | --- |
35+| `$TURBO_GO_THEME_DIR/*.toml` | User themes, when the variable is set. |
36+| `./.turbo-go/settings.toml` | This project's settings, read once at start-up. See [project settings](project-settings.md). |
37+| `~/.config/turbo-go/themes/*.toml` | User themes on Linux (`os.UserConfigDir`). |
38+| `~/Library/Application Support/turbo-go/themes/*.toml` | User themes on macOS. |
39+| `<module>/go.mod` | Located by walking up from the first file; its directory becomes the language server's root. |
40+
41+## Exit status
42+
43+| Status | Meaning |
44+| --- | --- |
45+| `0` | The editor exited normally, or an informational flag was used. |
46+| `1` | The terminal could not be opened or initialised. The reason is printed to standard error. |
47+
48+## Make targets
49+
50+Run from a checkout.
51+
52+| Target | What it runs |
53+| --- | --- |
54+| `make help` | List the targets. This is the default. |
55+| `make test` | `go test ./...` |
56+| `make test-verbose` | `go test -v ./...` |
57+| `make cover` | `go test -cover ./...` |
58+| `make build` | `go build -o bin/turbo-go .` |
59+| `make install` | `scripts/install.sh` — build and install onto your PATH |
60+| `make uninstall` | `scripts/install.sh --uninstall` |
61+| `make run FILE=x.go` | `make build`, then `./bin/turbo-go x.go` |
62+| `make fmt` | `go fmt ./...` |
63+| `make vet` | `go vet ./...` |
64+| `make check` | `fmt`, then `vet`, then `test` |
65+| `make clean` | Remove `bin/` |
66+
67+## Examples
68+
69+```bash
70+turbo-go # one empty window
71+turbo-go main.go go.mod # two windows
72+turbo-go -theme turbo-dark main.go # a different theme
73+turbo-go -no-lsp main.go # no language server
74+turbo-go -list-themes # what themes exist
75+```
76+
77+## Installer
78+
79+`scripts/install.sh`, also reachable as `make install`.
80+
81+| Option | Description |
82+| --- | --- |
83+| `-p`, `--prefix DIR` | Install into `DIR` instead of `$GOBIN` or `$GOPATH/bin`. |
84+| `--with-gopls` | Install `gopls` as well, if it is not already there. |
85+| `--uninstall` | Remove an installed `turbo-go` and stop. |
86+| `-h`, `--help` | Print the options and stop. |
87+
88+| Exit status | Meaning |
89+| --- | --- |
90+| `0` | Installed, removed, or help printed. |
91+| `1` | Go missing or too old, the build failed, or the destination could not be written. Nothing is installed and an existing installation is left untouched. |
92+
93+## Errors
94+
95+| Message | Cause |
96+| --- | --- |
97+| `turbo-go: opening the terminal: …` | tcell could not open the terminal; usually `TERM` is unset or unknown. |
98+| `turbo-go: initialising the terminal: …` | The terminal was opened but could not be put into raw mode. |
99+| `Cannot open` (in a dialog) | The path is a directory, or is not readable. |
100+| `Cannot save` (in a dialog) | The directory does not exist, or is not writable. |
new file mode 100644
@@ -0,0 +1,100 @@
1+# Reference: command line
2+
3+> Neutral description of the `turbo-go` command, its flags, and the environment it reads.
4+
5+## Synopsis
6+
7+```
8+turbo-go [flags] [file...]
9+```
10+
11+Each `file` is opened in its own window. A file that does not exist yet is opened as an empty buffer bound to that path. With no file at all, one empty untitled window is opened.
12+
13+## Flags
14+
15+| Flag | Type | Default | Description |
16+| --- | --- | --- | --- |
17+| `-theme <name>` | string | the project's, else `turbo-classic` | Theme to start with, overriding the project's own. An unknown name falls back to the default without an error. |
18+| `-list-themes` | bool | `false` | Print every loadable theme with its description, then the user theme directory, and exit. |
19+| `-no-lsp` | bool | `false` | Do not start a language server. Colouring and editing are unaffected. |
20+| `-version` | bool | `false` | Print `Turbo Go <version>` on one line, with the commit and build date when the build recorded them, and exit. See [the version number](versioning.md). |
21+| `-h`, `-help` | bool | `false` | Print the flag list and exit. |
22+
23+## Environment
24+
25+| Variable | Read by | Effect |
26+| --- | --- | --- |
27+| `TURBO_GO_THEME_DIR` | theme loading | Directory to read user themes from, instead of the platform configuration directory. |
28+| `TERM` | tcell | Which terminal description to use. |
29+| `GOBIN`, `GOPATH`, `HOME` | gopls lookup | Searched, in that order, when `gopls` is not on `PATH`. |
30+
31+## Files
32+
33+| Path | Purpose |
34+| --- | --- |
35+| `$TURBO_GO_THEME_DIR/*.toml` | User themes, when the variable is set. |
36+| `./.turbo-go/settings.toml` | This project's settings, read once at start-up. See [project settings](project-settings.md). |
37+| `~/.config/turbo-go/themes/*.toml` | User themes on Linux (`os.UserConfigDir`). |
38+| `~/Library/Application Support/turbo-go/themes/*.toml` | User themes on macOS. |
39+| `<module>/go.mod` | Located by walking up from the first file; its directory becomes the language server's root. |
40+
41+## Exit status
42+
43+| Status | Meaning |
44+| --- | --- |
45+| `0` | The editor exited normally, or an informational flag was used. |
46+| `1` | The terminal could not be opened or initialised. The reason is printed to standard error. |
47+
48+## Make targets
49+
50+Run from a checkout.
51+
52+| Target | What it runs |
53+| --- | --- |
54+| `make help` | List the targets. This is the default. |
55+| `make test` | `go test ./...` |
56+| `make test-verbose` | `go test -v ./...` |
57+| `make cover` | `go test -cover ./...` |
58+| `make build` | `go build -o bin/turbo-go .` |
59+| `make install` | `scripts/install.sh` — build and install onto your PATH |
60+| `make uninstall` | `scripts/install.sh --uninstall` |
61+| `make run FILE=x.go` | `make build`, then `./bin/turbo-go x.go` |
62+| `make fmt` | `go fmt ./...` |
63+| `make vet` | `go vet ./...` |
64+| `make check` | `fmt`, then `vet`, then `test` |
65+| `make clean` | Remove `bin/` |
66+
67+## Examples
68+
69+```bash
70+turbo-go # one empty window
71+turbo-go main.go go.mod # two windows
72+turbo-go -theme turbo-dark main.go # a different theme
73+turbo-go -no-lsp main.go # no language server
74+turbo-go -list-themes # what themes exist
75+```
76+
77+## Installer
78+
79+`scripts/install.sh`, also reachable as `make install`.
80+
81+| Option | Description |
82+| --- | --- |
83+| `-p`, `--prefix DIR` | Install into `DIR` instead of `$GOBIN` or `$GOPATH/bin`. |
84+| `--with-gopls` | Install `gopls` as well, if it is not already there. |
85+| `--uninstall` | Remove an installed `turbo-go` and stop. |
86+| `-h`, `--help` | Print the options and stop. |
87+
88+| Exit status | Meaning |
89+| --- | --- |
90+| `0` | Installed, removed, or help printed. |
91+| `1` | Go missing or too old, the build failed, or the destination could not be written. Nothing is installed and an existing installation is left untouched. |
92+
93+## Errors
94+
95+| Message | Cause |
96+| --- | --- |
97+| `turbo-go: opening the terminal: …` | tcell could not open the terminal; usually `TERM` is unset or unknown. |
98+| `turbo-go: initialising the terminal: …` | The terminal was opened but could not be put into raw mode. |
99+| `Cannot open` (in a dialog) | The path is a directory, or is not readable. |
100+| `Cannot save` (in a dialog) | The directory does not exist, or is not writable. |
added docs/en/reference/go-tools.md +236 -0
new file mode 100644
@@ -0,0 +1,236 @@
1+# Reference: go tools
2+
3+> Neutral description of `.turbo-go/tools.toml`, the Go menu, and what running a command does.
4+
5+## File
6+
7+| Property | Value |
8+| --- | --- |
9+| Path | `./.turbo-go/tools.toml` |
10+| Search | The working directory only. Parent directories are **not** searched. |
11+| Read | Every time one of its menus opens, for the items |
12+| Re-read | Whenever the file's size or modification time changes, for the **set** of menus |
13+| Missing file | Not an error |
14+| Unreadable file | An error, shown in the menu |
15+| User-level file | **None.** Unlike snippets, there is no `~/.config/turbo-go/tools.toml`. |
16+
17+## File format
18+
19+One `[[tool]]` table per command.
20+
21+| Key | Type | Required | Description |
22+| --- | --- | --- | --- |
23+| `name` | string | yes | What the menu shows. May carry a hot key written with tildes, as in `"~T~est"`. |
24+| `command` | string | yes | The shell command to run |
25+| `output` | string | no | Where its output goes: `popup`, `terminal` or `editor`. Absent means `popup`. |
26+| `menu` | string | no | Which menu it appears in. Absent means `Go`. Any name; the menu is created for you. May carry a hot key written with tildes. |
27+
28+`menu` is not checked against a list, because there is no list: a name that no other tool uses simply creates a menu. A tool with no `name`, no `command`, or an `output` naming something that does not exist makes the whole file an error. An unknown `output` is **refused rather than corrected**: `"termnial"` would otherwise look as though it had worked while sending the output somewhere else.
29+
30+### Example
31+
32+```toml
33+[[tool]]
34+name = "~T~est"
35+command = "go test ./..."
36+output = "popup"
37+
38+[[tool]]
39+name = "~E~cho"
40+command = "echo TADA"
41+output = "terminal"
42+menu = "Tools"
43+```
44+
45+## The starter file
46+
47+**Go ▸ Create tools file** writes these five, in this order:
48+
49+| Name | Command | Output |
50+| --- | --- | --- |
51+| Format | `gofmt -l -w .` | `popup` |
52+| Lint | `go vet ./...` | `popup` |
53+| Build | `go build ./...` | `popup` |
54+| Test | `go test ./...` | `popup` |
55+| Run | `go run .` | `terminal` |
56+
57+None of them names a `menu`, so all five are in the Go menu. Every one names its `output`, including the four that name the default: the key is the interesting part of the format, and a file where it appears once is a file where nobody notices it exists.
58+
59+The item is greyed out once the project has a tools file, so it cannot overwrite one. The file is written through a temporary file in the same directory, renamed into place.
60+
61+## The Go menu
62+
63+Always on the bar, whether or not a tools file exists. Its hot key is `Alt-G`.
64+
65+| Item | Condition |
66+| --- | --- |
67+| One line per tool with no `menu`, in file order | The file holds at least one |
68+| `Cannot read tools`, greyed out | The file is present but unreadable |
69+| `Create tools file` | The project has no tools file |
70+| `Open tools file` | The project has one |
71+
72+## Menus a tool asks for
73+
74+A `menu` naming anything other than `Go` puts a menu of that name on the bar.
75+
76+| Property | Value |
77+| --- | --- |
78+| Position | Between Go and Help |
79+| Order | The order each name first appears in the file |
80+| Items | One line per tool naming that menu, in file order. Nothing else — `Create tools file` and `Open tools file` stay in Go. |
81+| Unreadable file | No menus at all; the Go menu carries the error |
82+| While the editor runs | Added, removed and renamed as the file changes, without restarting |
83+
84+### Hot keys
85+
86+Assigned automatically, because a name from a file cannot be checked against the fixed menus in advance.
87+
88+| Case | Result |
89+| --- | --- |
90+| No tildes in the name | The first letter no other menu has claimed is marked. `Format` becomes `For~m~at`: `F` is File's, `o` is Options', `r` is Run's. |
91+| Tildes naming a free letter | Kept as written. `Doc~k~er` answers to `Alt-K`. |
92+| Tildes naming a taken letter | Dropped, and a free letter chosen instead. `~F~oo` becomes `F~o~o`. |
93+| Every letter taken | No hot key. `F10` and the mouse still open it. |
94+
95+The letters the editor's own menus hold are `F`, `E`, `S`, `R`, `O`, `W`, `N` (Snippets), `G` and `H`.
96+
97+## Running a command
98+
99+Common to every output:
100+
101+| Property | Value |
102+| --- | --- |
103+| Shell | `/bin/sh -c "<command>"` on Linux and macOS; `cmd.exe /S /C "<command>"` — the shell `%COMSPEC%` names — on Windows |
104+| Directory | The directory the editor was started in |
105+| Standard error | Merged into standard output, in the order the command wrote them |
106+
107+Going through a shell means pipes, globs, `&&` and `;` all work, so one tool can be a sequence. On Windows the shell is cmd.exe, which knows `&&`, `|` and `>` but does not expand globs, and where `;` is not a separator.
108+
109+### `output = "popup"`
110+
111+| Property | Value |
112+| --- | --- |
113+| Opens | Immediately, before the command has finished |
114+| Modal | Yes: nothing else in the editor can be used while it is up |
115+| Fills in | As output arrives, following it until you scroll back |
116+| Title while running | `<command> — running` |
117+| Title when finished | `<command> — ok`, or `<command> — exit <n>` |
118+| Empty output, finished | Shows `(no output)` |
119+| Empty output, running | Shows nothing |
120+| Output cap | 10000 lines; past it the oldest go and a `… n earlier lines dropped …` line says so |
121+
122+| Key | Effect |
123+| --- | --- |
124+| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output |
125+| Wheel | The same |
126+| `Escape`, `Enter`, **Close** | Close it, **stopping the command** if it is still running |
127+
128+Closing stops the command because there is no other way to interrupt one whose output is not in a terminal.
129+
130+### `output = "terminal"`
131+
132+| Property | Value |
133+| --- | --- |
134+| Window | A terminal window of its own, titled with the command |
135+| Environment | The editor's own, with `TERM` set to `xterm-256color` |
136+| After it exits | The window stays, showing its output |
137+| Modal | No: the editor carries on beside it |
138+
139+Because it is a real terminal, colours, paging, `Ctrl-C` and reading from the keyboard all work. See [Terminal windows](terminal.md).
140+
141+Keys in a **finished** terminal window:
142+
143+| Key | Effect |
144+| --- | --- |
145+| `Shift-PgUp`, `Shift-PgDn` | Read back through the output |
146+| `Ctrl-W` | Close the window |
147+| Anything else | Reaches the editor, not the dead shell |
148+
149+### `output = "editor"`
150+
151+| Property | Value |
152+| --- | --- |
153+| Shows | A popup while it runs, as above |
154+| On closing the popup | An editing window holding the output, titled with the command |
155+| Filled | Once, when the command has finished — not as it goes |
156+| The window | An ordinary editing window with no file name: searchable with `Ctrl-F`, and `Save as` keeps it |
157+
158+## Reloading after a command
159+
160+When a command finishes, every open file is considered.
161+
162+| The file | What happens |
163+| --- | --- |
164+| Unmodified, and changed on disk | Re-read; its syntax is re-decided and its title refreshed |
165+| Unmodified, and unchanged on disk | Left alone, not counted |
166+| Has unsaved changes | Left alone and counted as skipped |
167+| Has never been named | Left alone |
168+| Has gone from disk | Left alone |
169+
170+The cursor stays where it was, clamped into whatever the file now holds. The undo history is discarded, because undoing back past a reload would restore text the file no longer has.
171+
172+The project tree is refreshed at the same moment.
173+
174+| Status bar | When |
175+| --- | --- |
176+| `Running <command>` | The window opens |
177+| `Reloaded 2 files` | Two files were re-read, none skipped |
178+| `Reloaded 2 files; 1 file with unsaved changes left alone` | Some were skipped |
179+| `Command finished; 1 file with unsaved changes left alone` | Nothing was re-read, something was skipped |
180+
181+## Errors
182+
183+| Message | Cause |
184+| --- | --- |
185+| `Cannot read tools` in the menu | The file is present but not valid TOML, or holds a tool with no name or no command |
186+| `Already there: .turbo-go/tools.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. |
187+| `This project has no .turbo-go/tools.toml yet.` | Opening in a project that has none, likewise |
188+| `Cannot tell which directory this is: …` | The working directory could not be read |
189+| `Terminal windows are not supported on this platform yet` | Running a command in a terminal needs a pseudo-terminal, which Linux, macOS and Windows have; see [Terminal windows](terminal.md) |
190+
191+## Asking for a value
192+
193+A `{{label}}` anywhere in a command is a value the editor asks for before it runs, in a box titled after the tool. The text between the braces is what the box asks for.
194+
195+| Written | Asked for | Substituted |
196+| --- | --- | --- |
197+| `{{module path}}` | `module path` | shell-quoted |
198+| `{{extra flags...}}` | `extra flags` | verbatim |
199+
200+A value is **shell-quoted** by default, so a path with a space in it stays one argument. A trailing `...` inside the braces asks for it verbatim instead, which is how one field can stand for several arguments.
201+
202+```toml
203+[[tool]]
204+name = "~I~nit module"
205+command = "go mod init {{module path}}"
206+output = "popup"
207+```
208+
209+| Rule | Behaviour |
210+| --- | --- |
211+| Several placeholders | One box, one field each, in the order they appear in the command |
212+| The same label twice | One field; every occurrence gets what is typed into it |
213+| A label written both ways | Asked for once; each occurrence honours its own braces |
214+| Escape, or Cancel | The command does not run |
215+| A field left empty | Substituted as empty — the command reports its own complaint |
216+| Running the tool again | The box starts from what was typed last time, for this session only |
217+| More fields than fit on screen | Refused, with a message saying how many fit |
218+
219+**Double braces, not single.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary commands, and a single-brace syntax would read the first as a request for a value called `print $1`.
220+
221+Nothing is written to disk. A value somebody typed this afternoon is not a decision the project made, so it does not go in the project's own directory.
222+
223+### Errors
224+
225+| Error | Cause |
226+| --- | --- |
227+| `tool "X": "{{module" is never closed` | An opening `{{` with no `}}` after it |
228+| `tool "X": {{}} asks for a value but does not say what it is` | A placeholder with no label, or one that is only `...` |
229+
230+Both are refused when the file is read, so a half-typed placeholder never reaches the shell with its braces still in it.
231+
232+## See also
233+
234+- [How to run go commands from the editor](../how-to/run-go-commands.md)
235+- [Go tools](../explanation/go-tools.md)
236+- [Terminal windows](terminal.md)
new file mode 100644
@@ -0,0 +1,236 @@
1+# Reference: go tools
2+
3+> Neutral description of `.turbo-go/tools.toml`, the Go menu, and what running a command does.
4+
5+## File
6+
7+| Property | Value |
8+| --- | --- |
9+| Path | `./.turbo-go/tools.toml` |
10+| Search | The working directory only. Parent directories are **not** searched. |
11+| Read | Every time one of its menus opens, for the items |
12+| Re-read | Whenever the file's size or modification time changes, for the **set** of menus |
13+| Missing file | Not an error |
14+| Unreadable file | An error, shown in the menu |
15+| User-level file | **None.** Unlike snippets, there is no `~/.config/turbo-go/tools.toml`. |
16+
17+## File format
18+
19+One `[[tool]]` table per command.
20+
21+| Key | Type | Required | Description |
22+| --- | --- | --- | --- |
23+| `name` | string | yes | What the menu shows. May carry a hot key written with tildes, as in `"~T~est"`. |
24+| `command` | string | yes | The shell command to run |
25+| `output` | string | no | Where its output goes: `popup`, `terminal` or `editor`. Absent means `popup`. |
26+| `menu` | string | no | Which menu it appears in. Absent means `Go`. Any name; the menu is created for you. May carry a hot key written with tildes. |
27+
28+`menu` is not checked against a list, because there is no list: a name that no other tool uses simply creates a menu. A tool with no `name`, no `command`, or an `output` naming something that does not exist makes the whole file an error. An unknown `output` is **refused rather than corrected**: `"termnial"` would otherwise look as though it had worked while sending the output somewhere else.
29+
30+### Example
31+
32+```toml
33+[[tool]]
34+name = "~T~est"
35+command = "go test ./..."
36+output = "popup"
37+
38+[[tool]]
39+name = "~E~cho"
40+command = "echo TADA"
41+output = "terminal"
42+menu = "Tools"
43+```
44+
45+## The starter file
46+
47+**Go ▸ Create tools file** writes these five, in this order:
48+
49+| Name | Command | Output |
50+| --- | --- | --- |
51+| Format | `gofmt -l -w .` | `popup` |
52+| Lint | `go vet ./...` | `popup` |
53+| Build | `go build ./...` | `popup` |
54+| Test | `go test ./...` | `popup` |
55+| Run | `go run .` | `terminal` |
56+
57+None of them names a `menu`, so all five are in the Go menu. Every one names its `output`, including the four that name the default: the key is the interesting part of the format, and a file where it appears once is a file where nobody notices it exists.
58+
59+The item is greyed out once the project has a tools file, so it cannot overwrite one. The file is written through a temporary file in the same directory, renamed into place.
60+
61+## The Go menu
62+
63+Always on the bar, whether or not a tools file exists. Its hot key is `Alt-G`.
64+
65+| Item | Condition |
66+| --- | --- |
67+| One line per tool with no `menu`, in file order | The file holds at least one |
68+| `Cannot read tools`, greyed out | The file is present but unreadable |
69+| `Create tools file` | The project has no tools file |
70+| `Open tools file` | The project has one |
71+
72+## Menus a tool asks for
73+
74+A `menu` naming anything other than `Go` puts a menu of that name on the bar.
75+
76+| Property | Value |
77+| --- | --- |
78+| Position | Between Go and Help |
79+| Order | The order each name first appears in the file |
80+| Items | One line per tool naming that menu, in file order. Nothing else — `Create tools file` and `Open tools file` stay in Go. |
81+| Unreadable file | No menus at all; the Go menu carries the error |
82+| While the editor runs | Added, removed and renamed as the file changes, without restarting |
83+
84+### Hot keys
85+
86+Assigned automatically, because a name from a file cannot be checked against the fixed menus in advance.
87+
88+| Case | Result |
89+| --- | --- |
90+| No tildes in the name | The first letter no other menu has claimed is marked. `Format` becomes `For~m~at`: `F` is File's, `o` is Options', `r` is Run's. |
91+| Tildes naming a free letter | Kept as written. `Doc~k~er` answers to `Alt-K`. |
92+| Tildes naming a taken letter | Dropped, and a free letter chosen instead. `~F~oo` becomes `F~o~o`. |
93+| Every letter taken | No hot key. `F10` and the mouse still open it. |
94+
95+The letters the editor's own menus hold are `F`, `E`, `S`, `R`, `O`, `W`, `N` (Snippets), `G` and `H`.
96+
97+## Running a command
98+
99+Common to every output:
100+
101+| Property | Value |
102+| --- | --- |
103+| Shell | `/bin/sh -c "<command>"` on Linux and macOS; `cmd.exe /S /C "<command>"` — the shell `%COMSPEC%` names — on Windows |
104+| Directory | The directory the editor was started in |
105+| Standard error | Merged into standard output, in the order the command wrote them |
106+
107+Going through a shell means pipes, globs, `&&` and `;` all work, so one tool can be a sequence. On Windows the shell is cmd.exe, which knows `&&`, `|` and `>` but does not expand globs, and where `;` is not a separator.
108+
109+### `output = "popup"`
110+
111+| Property | Value |
112+| --- | --- |
113+| Opens | Immediately, before the command has finished |
114+| Modal | Yes: nothing else in the editor can be used while it is up |
115+| Fills in | As output arrives, following it until you scroll back |
116+| Title while running | `<command> — running` |
117+| Title when finished | `<command> — ok`, or `<command> — exit <n>` |
118+| Empty output, finished | Shows `(no output)` |
119+| Empty output, running | Shows nothing |
120+| Output cap | 10000 lines; past it the oldest go and a `… n earlier lines dropped …` line says so |
121+
122+| Key | Effect |
123+| --- | --- |
124+| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Read through the output |
125+| Wheel | The same |
126+| `Escape`, `Enter`, **Close** | Close it, **stopping the command** if it is still running |
127+
128+Closing stops the command because there is no other way to interrupt one whose output is not in a terminal.
129+
130+### `output = "terminal"`
131+
132+| Property | Value |
133+| --- | --- |
134+| Window | A terminal window of its own, titled with the command |
135+| Environment | The editor's own, with `TERM` set to `xterm-256color` |
136+| After it exits | The window stays, showing its output |
137+| Modal | No: the editor carries on beside it |
138+
139+Because it is a real terminal, colours, paging, `Ctrl-C` and reading from the keyboard all work. See [Terminal windows](terminal.md).
140+
141+Keys in a **finished** terminal window:
142+
143+| Key | Effect |
144+| --- | --- |
145+| `Shift-PgUp`, `Shift-PgDn` | Read back through the output |
146+| `Ctrl-W` | Close the window |
147+| Anything else | Reaches the editor, not the dead shell |
148+
149+### `output = "editor"`
150+
151+| Property | Value |
152+| --- | --- |
153+| Shows | A popup while it runs, as above |
154+| On closing the popup | An editing window holding the output, titled with the command |
155+| Filled | Once, when the command has finished — not as it goes |
156+| The window | An ordinary editing window with no file name: searchable with `Ctrl-F`, and `Save as` keeps it |
157+
158+## Reloading after a command
159+
160+When a command finishes, every open file is considered.
161+
162+| The file | What happens |
163+| --- | --- |
164+| Unmodified, and changed on disk | Re-read; its syntax is re-decided and its title refreshed |
165+| Unmodified, and unchanged on disk | Left alone, not counted |
166+| Has unsaved changes | Left alone and counted as skipped |
167+| Has never been named | Left alone |
168+| Has gone from disk | Left alone |
169+
170+The cursor stays where it was, clamped into whatever the file now holds. The undo history is discarded, because undoing back past a reload would restore text the file no longer has.
171+
172+The project tree is refreshed at the same moment.
173+
174+| Status bar | When |
175+| --- | --- |
176+| `Running <command>` | The window opens |
177+| `Reloaded 2 files` | Two files were re-read, none skipped |
178+| `Reloaded 2 files; 1 file with unsaved changes left alone` | Some were skipped |
179+| `Command finished; 1 file with unsaved changes left alone` | Nothing was re-read, something was skipped |
180+
181+## Errors
182+
183+| Message | Cause |
184+| --- | --- |
185+| `Cannot read tools` in the menu | The file is present but not valid TOML, or holds a tool with no name or no command |
186+| `Already there: .turbo-go/tools.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. |
187+| `This project has no .turbo-go/tools.toml yet.` | Opening in a project that has none, likewise |
188+| `Cannot tell which directory this is: …` | The working directory could not be read |
189+| `Terminal windows are not supported on this platform yet` | Running a command in a terminal needs a pseudo-terminal, which Linux, macOS and Windows have; see [Terminal windows](terminal.md) |
190+
191+## Asking for a value
192+
193+A `{{label}}` anywhere in a command is a value the editor asks for before it runs, in a box titled after the tool. The text between the braces is what the box asks for.
194+
195+| Written | Asked for | Substituted |
196+| --- | --- | --- |
197+| `{{module path}}` | `module path` | shell-quoted |
198+| `{{extra flags...}}` | `extra flags` | verbatim |
199+
200+A value is **shell-quoted** by default, so a path with a space in it stays one argument. A trailing `...` inside the braces asks for it verbatim instead, which is how one field can stand for several arguments.
201+
202+```toml
203+[[tool]]
204+name = "~I~nit module"
205+command = "go mod init {{module path}}"
206+output = "popup"
207+```
208+
209+| Rule | Behaviour |
210+| --- | --- |
211+| Several placeholders | One box, one field each, in the order they appear in the command |
212+| The same label twice | One field; every occurrence gets what is typed into it |
213+| A label written both ways | Asked for once; each occurrence honours its own braces |
214+| Escape, or Cancel | The command does not run |
215+| A field left empty | Substituted as empty — the command reports its own complaint |
216+| Running the tool again | The box starts from what was typed last time, for this session only |
217+| More fields than fit on screen | Refused, with a message saying how many fit |
218+
219+**Double braces, not single.** `awk '{print $1}'` and `find . -exec rm {} +` are ordinary commands, and a single-brace syntax would read the first as a request for a value called `print $1`.
220+
221+Nothing is written to disk. A value somebody typed this afternoon is not a decision the project made, so it does not go in the project's own directory.
222+
223+### Errors
224+
225+| Error | Cause |
226+| --- | --- |
227+| `tool "X": "{{module" is never closed` | An opening `{{` with no `}}` after it |
228+| `tool "X": {{}} asks for a value but does not say what it is` | A placeholder with no label, or one that is only `...` |
229+
230+Both are refused when the file is read, so a half-typed placeholder never reaches the shell with its braces still in it.
231+
232+## See also
233+
234+- [How to run go commands from the editor](../how-to/run-go-commands.md)
235+- [Go tools](../explanation/go-tools.md)
236+- [Terminal windows](terminal.md)
added docs/en/reference/keyboard.md +187 -0
new file mode 100644
@@ -0,0 +1,187 @@
1+# Reference: keyboard
2+
3+> Complete list of the keys Turbo Go answers to, grouped by what has the focus.
4+
5+Where two spellings exist, both work: the Turbo C one and the modern one.
6+
7+## Global
8+
9+Handled wherever the focus is, unless a dialog or the completion popup is open.
10+
11+| Key | Action |
12+| --- | --- |
13+| `F1` | Describe the symbol under the cursor; with no file open, show the keyboard help |
14+| `F2` | Save |
15+| `F3` | Open |
16+| `F4` | New |
17+| `F6` | Next window |
18+| `F7` | Find next |
19+| `Shift-F7` | Find previous |
20+| `F8` | Open a terminal window |
21+| `F9` | Open the project tree |
22+| `F10` | Open the menu bar |
23+| `F12` | Go to definition |
24+| `Shift-F12` | Find references |
25+| `Ctrl-T` | Find a symbol anywhere in the project |
26+| `Ctrl-F` | Find |
27+| `Ctrl-G` | Go to line |
28+| `Ctrl-W` | Close the current window |
29+| `Alt-X` | Exit |
30+| `Alt-1``Alt-9` | Bring window 1…9 forward |
31+| `Alt-0` | List the open windows |
32+| `Alt-N` | Open the Snippets menu |
33+| `Alt-G` | Open the Go menu |
34+| `Alt-<letter>` | Open the menu whose title carries that letter |
35+
36+A menu the project's tools file adds gets its letter assigned rather than fixed, so it is never one of the above. The rules are in [Go tools](go-tools.md#hot-keys).
37+
38+## Editing
39+
40+Handled by the window that has the focus.
41+
42+### Movement
43+
44+| Key | Action |
45+| --- | --- |
46+| `←` `→` `↑` `↓` | One character or one line |
47+| `Ctrl-←` `Ctrl-→` | Start of the previous / next word |
48+| `Home` `End` | Start / end of the line |
49+| `Ctrl-Home` `Ctrl-End` | Start / end of the file |
50+| `PgUp` `PgDn` | One screenful |
51+| `Shift` + any of the above | The same movement, extending the selection |
52+
53+### Changing the text
54+
55+| Key | Action |
56+| --- | --- |
57+| any printable character | Insert it, replacing the selection |
58+| `Enter` | Split the line, copying the current line's indentation |
59+| `Backspace` | Delete the selection, or the character before the cursor |
60+| `Delete` | Delete the selection, or the character under the cursor |
61+| `Tab` | Insert a tab; with a selection, indent every line it touches |
62+| `Shift-Tab` | Remove one level of indentation from every line the selection touches |
63+
64+### Clipboard and history
65+
66+| Key | Also | Action |
67+| --- | --- | --- |
68+| `Ctrl-C` | `Ctrl-Ins` | Copy the selection |
69+| `Ctrl-X` | `Shift-Del` | Cut the selection |
70+| `Ctrl-V` | `Shift-Ins` | Paste |
71+| `Ctrl-A` | | Select the whole file |
72+| `Ctrl-Z` | | Undo |
73+| `Ctrl-R` | | Redo |
74+| `Ctrl-N` | | Insert a blank line above the cursor |
75+| `Ctrl-Y` | | Delete the line the cursor is on |
76+
77+A run of typed characters, or a run of backspaces, is a **single** undo step. Moving the cursor ends the run.
78+
79+### Language server
80+
81+| Key | Action |
82+| --- | --- |
83+| `Ctrl-Space` | Ask for a completion list |
84+| `.` | Ask for a completion list, as a side effect of typing it |
85+| `F1` | Describe the symbol under the cursor |
86+| `F12` | Go to the declaration |
87+
88+## Menu bar
89+
90+Once a menu is open.
91+
92+| Key | Action |
93+| --- | --- |
94+| `←` `→` | Previous / next menu |
95+| `↑` `↓` | Previous / next item, skipping separators and disabled items |
96+| `Enter` | Run the highlighted item |
97+| `<letter>` | Run the item whose label carries that letter |
98+| `Escape` | Close the menu |
99+
100+An item marked `▶` opens a submenu instead of running:
101+
102+| Key | Action |
103+| --- | --- |
104+| `→`, `Enter` | Open the highlighted submenu; `→` on an item without one moves to the next menu |
105+| `←` | Step back out to the parent menu |
106+| `Escape` | Close the whole menu, wherever you are |
107+| `↑` `↓` | Walk the submenu |
108+| `<letter>` | Run the submenu item whose label carries that letter |
109+
110+Any other key is swallowed, so stray typing never reaches the file behind.
111+
112+## Dialogs
113+
114+| Key | Action |
115+| --- | --- |
116+| `Tab` / `Shift-Tab` | Next / previous control |
117+| `↑` `↓` | Walk the focused list; when the focused control has no use for them, the next / previous control |
118+| `Enter` | Press the default button, from wherever the focus is |
119+| `Escape` | Cancel |
120+| `Alt-<letter>` | Press the button whose label carries that letter |
121+| `Ctrl-U` | Clear the focused input field |
122+
123+A dialog is modal: every key it does not use is swallowed rather than passed to the editor behind it.
124+
125+### The Open and Save As box
126+
127+| | |
128+| --- | --- |
129+| Focus on opening | The **Name** field, so a name can be typed straight away. The first `↓` therefore moves the focus to the list; the second moves the highlight. |
130+| Moving the highlight | Puts that entry's name into the **Name** field, so the field always says what **OK** will act on. Highlighting `../` clears it. |
131+| `Enter` on the list | Opens the highlighted file, or browses into the highlighted directory |
132+| **OK** | Acts on the **Name** field; when the field is empty, acts on whatever the list has highlighted |
133+| A name that is a directory | Browses into it rather than closing the dialog |
134+| Double click | The same as `Enter` on that entry |
135+
136+Dot-files are not listed. Directories come before files, each group sorted, with `../` first.
137+
138+## Completion popup
139+
140+| Key | Action |
141+| --- | --- |
142+| `↑` `↓` | Previous / next suggestion |
143+| `PgUp` `PgDn` | Eight at a time |
144+| `Enter`, `Tab` | Accept the highlighted suggestion |
145+| `Escape` | Dismiss the list |
146+| any printable character | Passed through to the editor; the list narrows to what still matches |
147+
148+## Terminal windows
149+
150+A terminal window in front gets **every key except** the function keys, `Alt-X` and `Alt-0``Alt-9`, which stay with the editor so there is always a way out of a full-screen program. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` and `Alt-<letter>` therefore reach the shell rather than the editor.
151+
152+| Key | Action |
153+| --- | --- |
154+| `Shift-PgUp` `Shift-PgDn` | Read back / forward one screenful through the history |
155+| anything else not reserved above | Sent to the shell, returning the view to the live screen |
156+
157+The exact byte each key sends is in [Terminal windows](terminal.md).
158+
159+## Project tree
160+
161+Handled when the tree window has the focus. The full rules are in [Project tree](project-tree.md).
162+
163+| Key | Action |
164+| --- | --- |
165+| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Move the highlight |
166+| `→` | Expand a closed directory, else step to the next row |
167+| `←` | Collapse an open directory, else step out to its directory |
168+| `Enter` | Open a file; expand or collapse a directory |
169+| `F5`, `Ctrl-R` | Re-read the project |
170+
171+## Mouse
172+
173+| Action | Effect |
174+| --- | --- |
175+| Click in the text | Place the cursor |
176+| Drag in the text | Select |
177+| Wheel | Scroll three lines |
178+| Click a menu title | Open or close that menu |
179+| Click a status-bar hint | Run it |
180+| Click a window | Bring it forward |
181+| Drag a title bar | Move the window |
182+| Drag the bottom-right corner | Resize the window |
183+| Click `[x]` | Close the window |
184+| Click `[■]` | Fill the desktop with the window |
185+| Click `[▬]` | Put a filled window back where it was |
186+| Wheel over a terminal | Scroll three lines through its history |
187+| Click a tree row | Highlight it; a second click opens it |
new file mode 100644
@@ -0,0 +1,187 @@
1+# Reference: keyboard
2+
3+> Complete list of the keys Turbo Go answers to, grouped by what has the focus.
4+
5+Where two spellings exist, both work: the Turbo C one and the modern one.
6+
7+## Global
8+
9+Handled wherever the focus is, unless a dialog or the completion popup is open.
10+
11+| Key | Action |
12+| --- | --- |
13+| `F1` | Describe the symbol under the cursor; with no file open, show the keyboard help |
14+| `F2` | Save |
15+| `F3` | Open |
16+| `F4` | New |
17+| `F6` | Next window |
18+| `F7` | Find next |
19+| `Shift-F7` | Find previous |
20+| `F8` | Open a terminal window |
21+| `F9` | Open the project tree |
22+| `F10` | Open the menu bar |
23+| `F12` | Go to definition |
24+| `Shift-F12` | Find references |
25+| `Ctrl-T` | Find a symbol anywhere in the project |
26+| `Ctrl-F` | Find |
27+| `Ctrl-G` | Go to line |
28+| `Ctrl-W` | Close the current window |
29+| `Alt-X` | Exit |
30+| `Alt-1``Alt-9` | Bring window 1…9 forward |
31+| `Alt-0` | List the open windows |
32+| `Alt-N` | Open the Snippets menu |
33+| `Alt-G` | Open the Go menu |
34+| `Alt-<letter>` | Open the menu whose title carries that letter |
35+
36+A menu the project's tools file adds gets its letter assigned rather than fixed, so it is never one of the above. The rules are in [Go tools](go-tools.md#hot-keys).
37+
38+## Editing
39+
40+Handled by the window that has the focus.
41+
42+### Movement
43+
44+| Key | Action |
45+| --- | --- |
46+| `←` `→` `↑` `↓` | One character or one line |
47+| `Ctrl-←` `Ctrl-→` | Start of the previous / next word |
48+| `Home` `End` | Start / end of the line |
49+| `Ctrl-Home` `Ctrl-End` | Start / end of the file |
50+| `PgUp` `PgDn` | One screenful |
51+| `Shift` + any of the above | The same movement, extending the selection |
52+
53+### Changing the text
54+
55+| Key | Action |
56+| --- | --- |
57+| any printable character | Insert it, replacing the selection |
58+| `Enter` | Split the line, copying the current line's indentation |
59+| `Backspace` | Delete the selection, or the character before the cursor |
60+| `Delete` | Delete the selection, or the character under the cursor |
61+| `Tab` | Insert a tab; with a selection, indent every line it touches |
62+| `Shift-Tab` | Remove one level of indentation from every line the selection touches |
63+
64+### Clipboard and history
65+
66+| Key | Also | Action |
67+| --- | --- | --- |
68+| `Ctrl-C` | `Ctrl-Ins` | Copy the selection |
69+| `Ctrl-X` | `Shift-Del` | Cut the selection |
70+| `Ctrl-V` | `Shift-Ins` | Paste |
71+| `Ctrl-A` | | Select the whole file |
72+| `Ctrl-Z` | | Undo |
73+| `Ctrl-R` | | Redo |
74+| `Ctrl-N` | | Insert a blank line above the cursor |
75+| `Ctrl-Y` | | Delete the line the cursor is on |
76+
77+A run of typed characters, or a run of backspaces, is a **single** undo step. Moving the cursor ends the run.
78+
79+### Language server
80+
81+| Key | Action |
82+| --- | --- |
83+| `Ctrl-Space` | Ask for a completion list |
84+| `.` | Ask for a completion list, as a side effect of typing it |
85+| `F1` | Describe the symbol under the cursor |
86+| `F12` | Go to the declaration |
87+
88+## Menu bar
89+
90+Once a menu is open.
91+
92+| Key | Action |
93+| --- | --- |
94+| `←` `→` | Previous / next menu |
95+| `↑` `↓` | Previous / next item, skipping separators and disabled items |
96+| `Enter` | Run the highlighted item |
97+| `<letter>` | Run the item whose label carries that letter |
98+| `Escape` | Close the menu |
99+
100+An item marked `▶` opens a submenu instead of running:
101+
102+| Key | Action |
103+| --- | --- |
104+| `→`, `Enter` | Open the highlighted submenu; `→` on an item without one moves to the next menu |
105+| `←` | Step back out to the parent menu |
106+| `Escape` | Close the whole menu, wherever you are |
107+| `↑` `↓` | Walk the submenu |
108+| `<letter>` | Run the submenu item whose label carries that letter |
109+
110+Any other key is swallowed, so stray typing never reaches the file behind.
111+
112+## Dialogs
113+
114+| Key | Action |
115+| --- | --- |
116+| `Tab` / `Shift-Tab` | Next / previous control |
117+| `↑` `↓` | Walk the focused list; when the focused control has no use for them, the next / previous control |
118+| `Enter` | Press the default button, from wherever the focus is |
119+| `Escape` | Cancel |
120+| `Alt-<letter>` | Press the button whose label carries that letter |
121+| `Ctrl-U` | Clear the focused input field |
122+
123+A dialog is modal: every key it does not use is swallowed rather than passed to the editor behind it.
124+
125+### The Open and Save As box
126+
127+| | |
128+| --- | --- |
129+| Focus on opening | The **Name** field, so a name can be typed straight away. The first `↓` therefore moves the focus to the list; the second moves the highlight. |
130+| Moving the highlight | Puts that entry's name into the **Name** field, so the field always says what **OK** will act on. Highlighting `../` clears it. |
131+| `Enter` on the list | Opens the highlighted file, or browses into the highlighted directory |
132+| **OK** | Acts on the **Name** field; when the field is empty, acts on whatever the list has highlighted |
133+| A name that is a directory | Browses into it rather than closing the dialog |
134+| Double click | The same as `Enter` on that entry |
135+
136+Dot-files are not listed. Directories come before files, each group sorted, with `../` first.
137+
138+## Completion popup
139+
140+| Key | Action |
141+| --- | --- |
142+| `↑` `↓` | Previous / next suggestion |
143+| `PgUp` `PgDn` | Eight at a time |
144+| `Enter`, `Tab` | Accept the highlighted suggestion |
145+| `Escape` | Dismiss the list |
146+| any printable character | Passed through to the editor; the list narrows to what still matches |
147+
148+## Terminal windows
149+
150+A terminal window in front gets **every key except** the function keys, `Alt-X` and `Alt-0``Alt-9`, which stay with the editor so there is always a way out of a full-screen program. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` and `Alt-<letter>` therefore reach the shell rather than the editor.
151+
152+| Key | Action |
153+| --- | --- |
154+| `Shift-PgUp` `Shift-PgDn` | Read back / forward one screenful through the history |
155+| anything else not reserved above | Sent to the shell, returning the view to the live screen |
156+
157+The exact byte each key sends is in [Terminal windows](terminal.md).
158+
159+## Project tree
160+
161+Handled when the tree window has the focus. The full rules are in [Project tree](project-tree.md).
162+
163+| Key | Action |
164+| --- | --- |
165+| `↑` `↓` `PgUp` `PgDn` `Home` `End` | Move the highlight |
166+| `→` | Expand a closed directory, else step to the next row |
167+| `←` | Collapse an open directory, else step out to its directory |
168+| `Enter` | Open a file; expand or collapse a directory |
169+| `F5`, `Ctrl-R` | Re-read the project |
170+
171+## Mouse
172+
173+| Action | Effect |
174+| --- | --- |
175+| Click in the text | Place the cursor |
176+| Drag in the text | Select |
177+| Wheel | Scroll three lines |
178+| Click a menu title | Open or close that menu |
179+| Click a status-bar hint | Run it |
180+| Click a window | Bring it forward |
181+| Drag a title bar | Move the window |
182+| Drag the bottom-right corner | Resize the window |
183+| Click `[x]` | Close the window |
184+| Click `[■]` | Fill the desktop with the window |
185+| Click `[▬]` | Put a filled window back where it was |
186+| Wheel over a terminal | Scroll three lines through its history |
187+| Click a tree row | Highlight it; a second click opens it |
added docs/en/reference/languages.md +238 -0
new file mode 100644
@@ -0,0 +1,238 @@
1+# Reference: languages coloured
2+
3+> Neutral description of which files Turbo Go colours, how it decides, and what each scanner recognises.
4+
5+## Recognition
6+
7+A file's **extension** decides whenever it is one of these:
8+
9+| Extension | Language |
10+| --- | --- |
11+| `.go` | Go |
12+| `.toml` | TOML |
13+| `.yaml`, `.yml` | YAML |
14+| `.md`, `.markdown` | Markdown |
15+| `.js`, `.mjs`, `.cjs` | JavaScript |
16+| `.html`, `.htm` | HTML |
17+| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML |
18+| `.sh`, `.bash`, `.zsh` | Shell |
19+| `.dockerfile`, `.containerfile` | Dockerfile |
20+
21+Extensions are matched case-insensitively, and only the last one counts: `main.go.backup` is not Go.
22+
23+A file whose extension decides nothing is looked up by **name** next. Only files that carry no useful extension need this:
24+
25+| Name | Language |
26+| --- | --- |
27+| `Dockerfile`, `Containerfile` | Dockerfile |
28+
29+A name matches on the whole of it or on the part before the first dot, ignoring case — so `Dockerfile`, `dockerfile` and `Dockerfile.dev` are all recognised, while `Dockerfile.md` is Markdown, because the extension is consulted first.
30+
31+A file that neither table claims is a **shell script** when its first line is a shebang naming a shell — `sh`, `bash`, `zsh`, `dash` or `ksh`, as a path element or as the argument to `env`. That is what colours `configure`, a git hook, or a script somebody renamed.
32+
33+| First line | Result |
34+| --- | --- |
35+| `#!/bin/sh` | Shell |
36+| `#!/usr/bin/env bash` | Shell |
37+| `#!/usr/bin/env -S bash -e` | Shell |
38+| `#!/usr/bin/env python3` | Not coloured |
39+| Anything not starting `#!` | Not coloured |
40+
41+The order is fixed — extension, then name, then first line — and the first to decide wins: a `.go` file starting with a shebang is Go.
42+
43+Everything else is shown in plain text. That is not an error — opening a PNG in the editor is not a mistake, it is just not coloured.
44+
45+## Classes
46+
47+Every scanner produces the same vocabulary of classes, and each maps to one theme key.
48+
49+| Class | Theme key | Produced by |
50+| --- | --- | --- |
51+| `identifier` | `syntax.identifier` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
52+| `keyword` | `syntax.keyword` | Go, JavaScript, shell, HTML (doctype), XML, Dockerfile |
53+| `type` | `syntax.type` | Go, TOML (table headers), YAML (tags) |
54+| `builtin` | `syntax.builtin` | Go, JavaScript, shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) |
55+| `constant` | `syntax.constant` | Go, TOML, JavaScript, shell, YAML, HTML and XML (entities) |
56+| `function` | `syntax.function` | Go, JavaScript, shell (the command) |
57+| `string` | `syntax.string` | all |
58+| `char` | `syntax.char` | Go |
59+| `number` | `syntax.number` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
60+| `comment` | `syntax.comment` | Go, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile |
61+| `operator` | `syntax.operator` | Go, TOML, JavaScript, shell, HTML, YAML (block scalar headers), XML, Dockerfile |
62+| `punctuation` | `syntax.punctuation` | Go, TOML, JavaScript, shell, Markdown, YAML, Dockerfile |
63+| `heading` | `syntax.heading` | Markdown |
64+| `tag` | `syntax.tag` | HTML, XML |
65+| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (flags) |
66+| `emphasis` | `syntax.emphasis` | Markdown |
67+| `link` | `syntax.link` | Markdown |
68+
69+## Go
70+
71+Tokenised by `go/scanner`, the lexer the Go toolchain itself uses. See [Colouring and completion](../explanation/colouring-and-completion.md).
72+
73+## TOML
74+
75+| Recognised | As |
76+| --- | --- |
77+| `# comment` | comment |
78+| `[table]`, `[[array]]` | the name as a type, the brackets as punctuation |
79+| `key =` | identifier, then operator |
80+| `"basic"`, `'literal'`, `"""multi-line"""`, `'''multi-line'''` | string |
81+| `true`, `false` | constant |
82+| numbers, dates, times, `inf`, `nan` | number |
83+
84+## YAML
85+
86+A compose file, a Kubernetes manifest and a CI workflow are all this: there is no separate dialect, because a dialect would be somebody else's schema to keep in step with.
87+
88+| Recognised | As |
89+| --- | --- |
90+| `# comment` | comment |
91+| `key:` before a space or the end of the line | the key as an identifier, the colon as punctuation |
92+| `"quoted": 1`, `'quoted': 1` | the quoted key as an identifier |
93+| `- ` opening a sequence entry | punctuation |
94+| `"…"`, `'…'` | string |
95+| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constant, whatever their case |
96+| numbers, dates and times written without quotes | number |
97+| `&anchor`, `*alias` | builtin |
98+| `!!str`, `!Custom` | type |
99+| `---`, `...` | the whole line as punctuation |
100+| `{`, `}`, `[`, `]`, `,` | punctuation |
101+| `\|`, `>`, with their chomping and indentation indicators | the header as an operator, the body as a string |
102+
103+**A colon is a separator only when a space or the end of the line follows it.** `image: nginx:1.27` is a key and one value, and `url: http://example.com/x` is a key and one URL — colouring the inner colons as separators would put every image tag and every URL in three colours.
104+
105+**A block scalar's extent is decided by indentation**, not by a delimiter. The first content line after `|` or `>` fixes the block's indentation; every line indented at least that far belongs to it, and the first line that is not ends it. **A blank line inside a block stays inside it**: a literal scalar keeps its empty lines, and ending the block at the first paragraph break would cut a shell script in a CI file in half.
106+
107+**A `#` needs a space before it to start a comment**, so `colour: ff#00aa` is one scalar.
108+
109+| Not recognised | Because |
110+| --- | --- |
111+| The schema of a compose file, a manifest or a workflow | Colouring `services:` differently from any other key means carrying somebody else's schema, and it goes stale the day they add a key |
112+| Multi-document streams as separate documents | `---` is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries |
113+| Whether a bare word is a string or a number to a parser | `1.2.3` is a version to a reader and a string to YAML; the scanner colours what it looks like |
114+
115+## Markdown
116+
117+| Recognised | As |
118+| --- | --- |
119+| `# Heading``###### Heading` | the whole line as a heading |
120+| `**bold**`, `__bold__`, `*italic*`, `_italic_` | emphasis |
121+| `` `code` `` | string |
122+| `[text](target)`, `![alt](src)` | the whole thing as a link |
123+| `- `, `* `, `+ `, `1. `, `1) ` | the marker as punctuation |
124+| `>` | punctuation |
125+| `---`, `***`, `___` | punctuation |
126+| ` ``` ` and `~~~` fences | the whole block, opening and closing lines included, as a string |
127+
128+A fenced block is **one colour whatever language it announces**: ```` ```go ```` does not colour its contents as Go. The run of markers that opens a block must be matched by the same character to close it, so a backtick fence is not closed by a tilde one. An unclosed fence colours to the end of the file.
129+
130+The run of markers opening emphasis must be matched by a run of the same length, so `**bold**` is one span rather than two italics.
131+
132+## JavaScript
133+
134+| Recognised | As |
135+| --- | --- |
136+| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword |
137+| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant |
138+| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin |
139+| a name immediately before `(` | function |
140+| `"…"`, `'…'` | string |
141+| `` `` ``, interpolations included, across lines | string |
142+| `//` to end of line, `/* … */` across lines | comment |
143+| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number |
144+| runs of `+-*/%=<>!&|^~?:` | operator |
145+| `()[]{},;.` | punctuation |
146+
147+**Regular-expression literals are not recognised.** Telling `/x/g` from a division needs to know whether the previous token could end an expression; a wrong guess colours the rest of a line as a string, which is worse than leaving a regex the colour of an operator.
148+
149+Globals are recognised by name, so a file that shadows `Math` still has it coloured as a builtin — the same rule Go's predeclared identifiers follow.
150+
151+## HTML
152+
153+| Recognised | As |
154+| --- | --- |
155+| `<tag`, `</tag`, `>`, `/>` | tag |
156+| attribute names, including `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute |
157+| `=` | operator |
158+| `"…"`, `'…'` | string |
159+| `<!-- … -->`, across lines | comment |
160+| `&amp;`, `&#169;` | constant |
161+| `<!DOCTYPE …>` and other declarations | keyword |
162+
163+Text between tags is not coloured. A bare `&` with no `;` within 32 characters is left alone, because it is legal text.
164+
165+**The contents of `<script>` and `<style>` are not coloured** as JavaScript and CSS.
166+
167+## XML
168+
169+Its own scanner rather than HTML's, for one reason that matters: CDATA. The whole point of `<![CDATA[ … ]]>` is that its contents are *not* markup, and colouring the tags inside one as tags is exactly backwards.
170+
171+| Recognised | As |
172+| --- | --- |
173+| `<?xml version="1.0"?>` and other processing instructions | the target and `?>` as keyword, the pairs between as attributes and strings |
174+| `<!DOCTYPE …>` and the other `<!` forms | keyword |
175+| `<!-- … -->`, across lines | comment |
176+| `<![CDATA[ … ]]>`, across lines | string |
177+| `<tag`, `</tag`, `>`, `/>` | tag |
178+| `<ns:tag>`, `xsi:type` | the prefix and the local name as **one** span |
179+| attribute names | attribute |
180+| `=` | operator |
181+| `"…"`, `'…'` | string |
182+| `&amp;`, `&#169;` | constant |
183+
184+**A comment and a CDATA section close on different delimiters**, and are carried separately: a `-->` inside a CDATA section does not end it.
185+
186+**A bare `&` with no semicolon within 32 characters is left alone**, because it is legal text in plenty of documents and swallowing the rest of the line would be the bigger mistake.
187+
188+Text between tags is not coloured.
189+
190+## Shell
191+
192+Applies to `sh`, `bash` and `zsh` alike: the keywords recognised are the ones they share.
193+
194+| Recognised | As |
195+| --- | --- |
196+| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword |
197+| `true`, `false` | constant |
198+| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin |
199+| `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin |
200+| the **first bare word on a line** | function |
201+| every later bare word, and `NAME` in `NAME=value` | identifier |
202+| `'…'`, with nothing escaped or expanded inside | string |
203+| `"…"`, with the expansions inside it coloured as expansions | string |
204+| `#` to end of line | comment |
205+
206+`$(a $(b) c)` is one span: nesting is counted. An option such as `-euo` is one word, not a minus and a word.
207+
208+**Heredocs are not recognised.** `<<EOF` and the text after it are coloured as ordinary shell.
209+
210+## Dockerfile
211+
212+| Recognised | As |
213+| --- | --- |
214+| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | keyword, in any case |
215+| `AS`, `NONE` | keyword |
216+| `# comment`, including the `# syntax=` and `# escape=` directives | comment |
217+| `--from=builder`, `--chown=me:me` | the flag name as an attribute |
218+| `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, as one span to the closing brace |
219+| `"…"`, `'…'` | string |
220+| a trailing `\` | operator |
221+| numbers | number |
222+| paths and image references — `/usr/local/bin`, `golang:1.26-alpine` | identifier, as **one** span |
223+
224+**Only the first word of a line can be an instruction**, and a word that is not one is an argument — which is what keeps a continuation line's first word out of the keyword colour.
225+
226+**Nothing crosses a line break.** A `\` joins two lines for Docker, but each half still reads as a command and is coloured on its own.
227+
228+| Not recognised | Because |
229+| --- | --- |
230+| The shell inside a `RUN` | It would mean running the shell scanner over part of a line and mapping its columns out, and `RUN` may hold any language |
231+| Heredocs in a `RUN` | The same reason the shell scanner does not recognise them |
232+| Which stage a `--from` names | Nothing here reads the rest of the file |
233+
234+## See also
235+
236+- [Theme file format](themes.md) — every key these classes resolve to
237+- [Colouring and completion](../explanation/colouring-and-completion.md) — why the scanners are written this way
238+- [How to write your own theme](../how-to/write-a-theme.md)
new file mode 100644
@@ -0,0 +1,238 @@
1+# Reference: languages coloured
2+
3+> Neutral description of which files Turbo Go colours, how it decides, and what each scanner recognises.
4+
5+## Recognition
6+
7+A file's **extension** decides whenever it is one of these:
8+
9+| Extension | Language |
10+| --- | --- |
11+| `.go` | Go |
12+| `.toml` | TOML |
13+| `.yaml`, `.yml` | YAML |
14+| `.md`, `.markdown` | Markdown |
15+| `.js`, `.mjs`, `.cjs` | JavaScript |
16+| `.html`, `.htm` | HTML |
17+| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML |
18+| `.sh`, `.bash`, `.zsh` | Shell |
19+| `.dockerfile`, `.containerfile` | Dockerfile |
20+
21+Extensions are matched case-insensitively, and only the last one counts: `main.go.backup` is not Go.
22+
23+A file whose extension decides nothing is looked up by **name** next. Only files that carry no useful extension need this:
24+
25+| Name | Language |
26+| --- | --- |
27+| `Dockerfile`, `Containerfile` | Dockerfile |
28+
29+A name matches on the whole of it or on the part before the first dot, ignoring case — so `Dockerfile`, `dockerfile` and `Dockerfile.dev` are all recognised, while `Dockerfile.md` is Markdown, because the extension is consulted first.
30+
31+A file that neither table claims is a **shell script** when its first line is a shebang naming a shell — `sh`, `bash`, `zsh`, `dash` or `ksh`, as a path element or as the argument to `env`. That is what colours `configure`, a git hook, or a script somebody renamed.
32+
33+| First line | Result |
34+| --- | --- |
35+| `#!/bin/sh` | Shell |
36+| `#!/usr/bin/env bash` | Shell |
37+| `#!/usr/bin/env -S bash -e` | Shell |
38+| `#!/usr/bin/env python3` | Not coloured |
39+| Anything not starting `#!` | Not coloured |
40+
41+The order is fixed — extension, then name, then first line — and the first to decide wins: a `.go` file starting with a shebang is Go.
42+
43+Everything else is shown in plain text. That is not an error — opening a PNG in the editor is not a mistake, it is just not coloured.
44+
45+## Classes
46+
47+Every scanner produces the same vocabulary of classes, and each maps to one theme key.
48+
49+| Class | Theme key | Produced by |
50+| --- | --- | --- |
51+| `identifier` | `syntax.identifier` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
52+| `keyword` | `syntax.keyword` | Go, JavaScript, shell, HTML (doctype), XML, Dockerfile |
53+| `type` | `syntax.type` | Go, TOML (table headers), YAML (tags) |
54+| `builtin` | `syntax.builtin` | Go, JavaScript, shell (builtins and expansions), YAML (anchors and aliases), Dockerfile (variables) |
55+| `constant` | `syntax.constant` | Go, TOML, JavaScript, shell, YAML, HTML and XML (entities) |
56+| `function` | `syntax.function` | Go, JavaScript, shell (the command) |
57+| `string` | `syntax.string` | all |
58+| `char` | `syntax.char` | Go |
59+| `number` | `syntax.number` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
60+| `comment` | `syntax.comment` | Go, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile |
61+| `operator` | `syntax.operator` | Go, TOML, JavaScript, shell, HTML, YAML (block scalar headers), XML, Dockerfile |
62+| `punctuation` | `syntax.punctuation` | Go, TOML, JavaScript, shell, Markdown, YAML, Dockerfile |
63+| `heading` | `syntax.heading` | Markdown |
64+| `tag` | `syntax.tag` | HTML, XML |
65+| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (flags) |
66+| `emphasis` | `syntax.emphasis` | Markdown |
67+| `link` | `syntax.link` | Markdown |
68+
69+## Go
70+
71+Tokenised by `go/scanner`, the lexer the Go toolchain itself uses. See [Colouring and completion](../explanation/colouring-and-completion.md).
72+
73+## TOML
74+
75+| Recognised | As |
76+| --- | --- |
77+| `# comment` | comment |
78+| `[table]`, `[[array]]` | the name as a type, the brackets as punctuation |
79+| `key =` | identifier, then operator |
80+| `"basic"`, `'literal'`, `"""multi-line"""`, `'''multi-line'''` | string |
81+| `true`, `false` | constant |
82+| numbers, dates, times, `inf`, `nan` | number |
83+
84+## YAML
85+
86+A compose file, a Kubernetes manifest and a CI workflow are all this: there is no separate dialect, because a dialect would be somebody else's schema to keep in step with.
87+
88+| Recognised | As |
89+| --- | --- |
90+| `# comment` | comment |
91+| `key:` before a space or the end of the line | the key as an identifier, the colon as punctuation |
92+| `"quoted": 1`, `'quoted': 1` | the quoted key as an identifier |
93+| `- ` opening a sequence entry | punctuation |
94+| `"…"`, `'…'` | string |
95+| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constant, whatever their case |
96+| numbers, dates and times written without quotes | number |
97+| `&anchor`, `*alias` | builtin |
98+| `!!str`, `!Custom` | type |
99+| `---`, `...` | the whole line as punctuation |
100+| `{`, `}`, `[`, `]`, `,` | punctuation |
101+| `\|`, `>`, with their chomping and indentation indicators | the header as an operator, the body as a string |
102+
103+**A colon is a separator only when a space or the end of the line follows it.** `image: nginx:1.27` is a key and one value, and `url: http://example.com/x` is a key and one URL — colouring the inner colons as separators would put every image tag and every URL in three colours.
104+
105+**A block scalar's extent is decided by indentation**, not by a delimiter. The first content line after `|` or `>` fixes the block's indentation; every line indented at least that far belongs to it, and the first line that is not ends it. **A blank line inside a block stays inside it**: a literal scalar keeps its empty lines, and ending the block at the first paragraph break would cut a shell script in a CI file in half.
106+
107+**A `#` needs a space before it to start a comment**, so `colour: ff#00aa` is one scalar.
108+
109+| Not recognised | Because |
110+| --- | --- |
111+| The schema of a compose file, a manifest or a workflow | Colouring `services:` differently from any other key means carrying somebody else's schema, and it goes stale the day they add a key |
112+| Multi-document streams as separate documents | `---` is coloured, but nothing is reset at it; nothing in the colouring depends on document boundaries |
113+| Whether a bare word is a string or a number to a parser | `1.2.3` is a version to a reader and a string to YAML; the scanner colours what it looks like |
114+
115+## Markdown
116+
117+| Recognised | As |
118+| --- | --- |
119+| `# Heading``###### Heading` | the whole line as a heading |
120+| `**bold**`, `__bold__`, `*italic*`, `_italic_` | emphasis |
121+| `` `code` `` | string |
122+| `[text](target)`, `![alt](src)` | the whole thing as a link |
123+| `- `, `* `, `+ `, `1. `, `1) ` | the marker as punctuation |
124+| `>` | punctuation |
125+| `---`, `***`, `___` | punctuation |
126+| ` ``` ` and `~~~` fences | the whole block, opening and closing lines included, as a string |
127+
128+A fenced block is **one colour whatever language it announces**: ```` ```go ```` does not colour its contents as Go. The run of markers that opens a block must be matched by the same character to close it, so a backtick fence is not closed by a tilde one. An unclosed fence colours to the end of the file.
129+
130+The run of markers opening emphasis must be matched by a run of the same length, so `**bold**` is one span rather than two italics.
131+
132+## JavaScript
133+
134+| Recognised | As |
135+| --- | --- |
136+| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword |
137+| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant |
138+| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin |
139+| a name immediately before `(` | function |
140+| `"…"`, `'…'` | string |
141+| `` `` ``, interpolations included, across lines | string |
142+| `//` to end of line, `/* … */` across lines | comment |
143+| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number |
144+| runs of `+-*/%=<>!&|^~?:` | operator |
145+| `()[]{},;.` | punctuation |
146+
147+**Regular-expression literals are not recognised.** Telling `/x/g` from a division needs to know whether the previous token could end an expression; a wrong guess colours the rest of a line as a string, which is worse than leaving a regex the colour of an operator.
148+
149+Globals are recognised by name, so a file that shadows `Math` still has it coloured as a builtin — the same rule Go's predeclared identifiers follow.
150+
151+## HTML
152+
153+| Recognised | As |
154+| --- | --- |
155+| `<tag`, `</tag`, `>`, `/>` | tag |
156+| attribute names, including `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute |
157+| `=` | operator |
158+| `"…"`, `'…'` | string |
159+| `<!-- … -->`, across lines | comment |
160+| `&amp;`, `&#169;` | constant |
161+| `<!DOCTYPE …>` and other declarations | keyword |
162+
163+Text between tags is not coloured. A bare `&` with no `;` within 32 characters is left alone, because it is legal text.
164+
165+**The contents of `<script>` and `<style>` are not coloured** as JavaScript and CSS.
166+
167+## XML
168+
169+Its own scanner rather than HTML's, for one reason that matters: CDATA. The whole point of `<![CDATA[ … ]]>` is that its contents are *not* markup, and colouring the tags inside one as tags is exactly backwards.
170+
171+| Recognised | As |
172+| --- | --- |
173+| `<?xml version="1.0"?>` and other processing instructions | the target and `?>` as keyword, the pairs between as attributes and strings |
174+| `<!DOCTYPE …>` and the other `<!` forms | keyword |
175+| `<!-- … -->`, across lines | comment |
176+| `<![CDATA[ … ]]>`, across lines | string |
177+| `<tag`, `</tag`, `>`, `/>` | tag |
178+| `<ns:tag>`, `xsi:type` | the prefix and the local name as **one** span |
179+| attribute names | attribute |
180+| `=` | operator |
181+| `"…"`, `'…'` | string |
182+| `&amp;`, `&#169;` | constant |
183+
184+**A comment and a CDATA section close on different delimiters**, and are carried separately: a `-->` inside a CDATA section does not end it.
185+
186+**A bare `&` with no semicolon within 32 characters is left alone**, because it is legal text in plenty of documents and swallowing the rest of the line would be the bigger mistake.
187+
188+Text between tags is not coloured.
189+
190+## Shell
191+
192+Applies to `sh`, `bash` and `zsh` alike: the keywords recognised are the ones they share.
193+
194+| Recognised | As |
195+| --- | --- |
196+| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword |
197+| `true`, `false` | constant |
198+| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin |
199+| `$NAME`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin |
200+| the **first bare word on a line** | function |
201+| every later bare word, and `NAME` in `NAME=value` | identifier |
202+| `'…'`, with nothing escaped or expanded inside | string |
203+| `"…"`, with the expansions inside it coloured as expansions | string |
204+| `#` to end of line | comment |
205+
206+`$(a $(b) c)` is one span: nesting is counted. An option such as `-euo` is one word, not a minus and a word.
207+
208+**Heredocs are not recognised.** `<<EOF` and the text after it are coloured as ordinary shell.
209+
210+## Dockerfile
211+
212+| Recognised | As |
213+| --- | --- |
214+| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | keyword, in any case |
215+| `AS`, `NONE` | keyword |
216+| `# comment`, including the `# syntax=` and `# escape=` directives | comment |
217+| `--from=builder`, `--chown=me:me` | the flag name as an attribute |
218+| `$NAME`, `${NAME}`, `${NAME:-default}` | builtin, as one span to the closing brace |
219+| `"…"`, `'…'` | string |
220+| a trailing `\` | operator |
221+| numbers | number |
222+| paths and image references — `/usr/local/bin`, `golang:1.26-alpine` | identifier, as **one** span |
223+
224+**Only the first word of a line can be an instruction**, and a word that is not one is an argument — which is what keeps a continuation line's first word out of the keyword colour.
225+
226+**Nothing crosses a line break.** A `\` joins two lines for Docker, but each half still reads as a command and is coloured on its own.
227+
228+| Not recognised | Because |
229+| --- | --- |
230+| The shell inside a `RUN` | It would mean running the shell scanner over part of a line and mapping its columns out, and `RUN` may hold any language |
231+| Heredocs in a `RUN` | The same reason the shell scanner does not recognise them |
232+| Which stage a `--from` names | Nothing here reads the rest of the file |
233+
234+## See also
235+
236+- [Theme file format](themes.md) — every key these classes resolve to
237+- [Colouring and completion](../explanation/colouring-and-completion.md) — why the scanners are written this way
238+- [How to write your own theme](../how-to/write-a-theme.md)
added docs/en/reference/menus.md +150 -0
new file mode 100644
@@ -0,0 +1,150 @@
1+# Reference: menus
2+
3+> Complete list of the menu bar's entries, with their shortcuts and the conditions under which they can be chosen.
4+
5+An item marked **needs a file** is greyed out when no window is open.
6+
7+The bar always holds File, Edit, Search, Run, Code, Options, Window, Snippets, Go and Help, in that order. A project's tools file can add menus of its own between Go and Help; they are listed under [Project menus](#project-menus) below.
8+
9+## File
10+
11+| Item | Shortcut | Needs a file | Effect |
12+| --- | --- | --- | --- |
13+| New | `F4` | | Open an empty untitled window |
14+| Open… | `F3` | | Open the file browser and open what is chosen |
15+| Save | `F2` | yes | Write the file; asks for a name if it has none |
16+| Save as… | | yes | Ask for a name and write to it, adopting that path |
17+| Close | `Ctrl-W` | yes | Close the current window, offering to save first |
18+| Exit | `Alt-X` | | Leave the editor, offering to save each modified file |
19+
20+## Edit
21+
22+| Item | Shortcut | Needs a file | Effect |
23+| --- | --- | --- | --- |
24+| Undo | `Ctrl-Z` | yes | Revert the last change |
25+| Redo | `Ctrl-R` | yes | Re-apply the last undone change. **`Ctrl-Y` used to do this**; it deletes a line now, as it did in Turbo C. |
26+| Insert line | `Ctrl-N` | yes | Open a blank line above the cursor, leaving the cursor on its own text |
27+| Delete line | `Ctrl-Y` | yes | Remove the line the cursor is on. The cursor stays on the same line number, so holding the key deletes a run. |
28+| Cut | `Shift-Del` | yes | Copy the selection to the clipboard and remove it |
29+| Copy | `Ctrl-Ins` | yes | Copy the selection to the clipboard |
30+| Paste | `Shift-Ins` | yes | Insert the clipboard, replacing the selection |
31+| Select all | `Ctrl-A` | yes | Select the whole file |
32+
33+The clipboard is shared between every window of one editor session.
34+
35+## Search
36+
37+| Item | Shortcut | Needs a file | Effect |
38+| --- | --- | --- | --- |
39+| Find… | `Ctrl-F` | yes | Ask for text and options, then jump to the first match |
40+| Find next | `F7` | yes | Jump to the next match, wrapping round the end of the file |
41+| Find previous | `Shift-F7` | yes | Jump to the previous match, wrapping round the start |
42+| Go to line… | `Ctrl-G` | yes | Ask for a line number, counting from one |
43+
44+## Run
45+
46+| Item | Shortcut | Needs a file | Effect |
47+| --- | --- | --- | --- |
48+| Completion | `Ctrl-Space` | yes | Open the completion list at the cursor |
49+| Language server status | | | Show whether a language server is running, and what it is doing |
50+
51+## Code
52+
53+Everything the editor asks the language server about the symbol **under the cursor**. Nothing here needs a selection: almost every request in the protocol takes a position rather than a range, so there is nothing extra to say by selecting first.
54+
55+Its hot key is `Alt-C`.
56+
57+| Item | Shortcut | Needs a file | Effect |
58+| --- | --- | --- | --- |
59+| Describe symbol | `F1` | yes | Show what the language server knows about the symbol under the cursor |
60+| Go to definition | `F12` | yes | Where the symbol is declared. One answer opens it; several offer the list. |
61+| Go to type definition | | yes | Where the *type* of the symbol is declared, which is a different question |
62+| Find implementations… | | yes | What implements it: the types satisfying an interface, the impl blocks of a trait |
63+| Find references… | `Shift-F12` | yes | Where it is used, counting its declaration |
64+| Symbol in file… | | yes | The file's own outline, indented, with each symbol's kind. Choosing one goes to it. |
65+| Symbol in project… | `Ctrl-T` | no | Ask for a name and search the whole project |
66+| Problems… | | no | Every problem the server has reported, for every file it has spoken about |
67+
68+A list of places shows the file, the line, and the text of that line — twelve entries reading `handler.go:42` say nothing about which one you want. The text comes from an open window when there is one, so a file edited and not saved is listed as it now reads.
69+
70+Three answers are told apart, and the difference matters: **nothing found** says so in the question's own words (`No references found`), **the server is not ready** shows what it is doing instead, and **one answer** simply takes you there without a dialog.
71+
72+*Symbol in file* has no shortcut on purpose. The obvious one is `Ctrl-Shift-O`, and a terminal cannot tell that from `Ctrl-O` — the shift is lost before the editor sees it.
73+
74+## Options
75+
76+| Item | Needs a file | Effect |
77+| --- | --- | --- |
78+| Theme… | | List every loadable theme and apply the one chosen, immediately. With a project settings file present, also writes the choice into it. |
79+| Line numbers | yes | Show or hide the gutter in the current window |
80+| Create project settings | | Write `.turbo-go/settings.toml` with the theme in use, and open it. **Greyed out once the project has one.** |
81+| Project settings… | | Open `.turbo-go/settings.toml`. **Greyed out until the project has one.** |
82+
83+## Window
84+
85+| Item | Shortcut | Needs a file | Effect |
86+| --- | --- | --- | --- |
87+| Next | `F6` | yes | Bring the window behind the current one forward |
88+| New terminal | `F8` | no | Open a window running a shell, in the directory of the file in front |
89+| Project tree | `F9` | no | Open a window showing the project's files; brings the existing one forward when there is one |
90+| Tile | | yes | Lay every window out in a grid, none overlapping |
91+| Cascade | | yes | Stack the windows offset, every title visible |
92+| Maximise | | yes | Give the current window the whole desktop, or put it back where it was if it already has it. The same toggle as the `[■]` box on the window's own frame. |
93+| List… | `Alt-0` | yes | List the open windows and bring the chosen one forward |
94+
95+## Snippets
96+
97+Built from `.turbo-go/snippets.toml` and your own snippets file each time it opens. Its hot key is `Alt-N`, because Search already answers to S.
98+
99+| Item | Needs a file | Effect |
100+| --- | --- | --- |
101+| One submenu per group | | Insert the chosen snippet at the cursor; the items need a file open |
102+| Create snippets file | no | Write `.turbo-go/snippets.toml` with worked examples, then open it. **Greyed out once the project has one.** |
103+| Open snippets file | no | Open `.turbo-go/snippets.toml`. **Greyed out until the project has one.** Always the project's file, never your own. |
104+
105+See [Snippets](snippets.md).
106+
107+## Go
108+
109+Built from `.turbo-go/tools.toml` each time it opens. Its hot key is `Alt-G`.
110+
111+| Item | Effect |
112+| --- | --- |
113+| One line per tool that names no `menu` | Run that command, showing its output where the tool asked: a popup, a terminal window, or an editing window |
114+| Create tools file | Write `.turbo-go/tools.toml` with the five Go commands, then open it. **Greyed out once the project has one.** |
115+| Open tools file | Open `.turbo-go/tools.toml`. **Greyed out until the project has one.** |
116+
117+See [Go tools](go-tools.md).
118+
119+## Project menus
120+
121+Not fixed: one menu per `menu` name in `.turbo-go/tools.toml`, in the order the names first appear there, between Go and Help. A project with no tools file, or whose tools all stay in Go, has none.
122+
123+| Item | Effect |
124+| --- | --- |
125+| One line per tool naming that menu | Run that command, showing its output where the tool asked |
126+
127+Their hot keys are assigned rather than fixed, so that a name from a file can never take a letter one of the menus above already answers to. The rules are in [Go tools](go-tools.md#hot-keys).
128+
129+## Help
130+
131+| Item | Effect |
132+| --- | --- |
133+| Keyboard | Show the keys worth knowing |
134+| About | Show the version, the commit and build date when the build recorded them, and the current theme. See [the version number](versioning.md). |
135+
136+## Status bar
137+
138+The hints along the bottom are clickable and run the same actions.
139+
140+| Hint | Action |
141+| --- | --- |
142+| `F1 Describe` | Code ▸ Describe symbol |
143+| `F2 Save` | File ▸ Save |
144+| `F3 Open` | File ▸ Open… |
145+| `F6 Window` | Window ▸ Next |
146+| `F7 Next` | Search ▸ Find next |
147+| `F10 Menu` | Open the menu bar |
148+| `Alt-X Exit` | File ▸ Exit |
149+
150+The right-hand end shows, in this order: the cursor as `line:column`, then either the first error the language server reported for this file (prefixed `⚠`) or the language server's state.
new file mode 100644
@@ -0,0 +1,150 @@
1+# Reference: menus
2+
3+> Complete list of the menu bar's entries, with their shortcuts and the conditions under which they can be chosen.
4+
5+An item marked **needs a file** is greyed out when no window is open.
6+
7+The bar always holds File, Edit, Search, Run, Code, Options, Window, Snippets, Go and Help, in that order. A project's tools file can add menus of its own between Go and Help; they are listed under [Project menus](#project-menus) below.
8+
9+## File
10+
11+| Item | Shortcut | Needs a file | Effect |
12+| --- | --- | --- | --- |
13+| New | `F4` | | Open an empty untitled window |
14+| Open… | `F3` | | Open the file browser and open what is chosen |
15+| Save | `F2` | yes | Write the file; asks for a name if it has none |
16+| Save as… | | yes | Ask for a name and write to it, adopting that path |
17+| Close | `Ctrl-W` | yes | Close the current window, offering to save first |
18+| Exit | `Alt-X` | | Leave the editor, offering to save each modified file |
19+
20+## Edit
21+
22+| Item | Shortcut | Needs a file | Effect |
23+| --- | --- | --- | --- |
24+| Undo | `Ctrl-Z` | yes | Revert the last change |
25+| Redo | `Ctrl-R` | yes | Re-apply the last undone change. **`Ctrl-Y` used to do this**; it deletes a line now, as it did in Turbo C. |
26+| Insert line | `Ctrl-N` | yes | Open a blank line above the cursor, leaving the cursor on its own text |
27+| Delete line | `Ctrl-Y` | yes | Remove the line the cursor is on. The cursor stays on the same line number, so holding the key deletes a run. |
28+| Cut | `Shift-Del` | yes | Copy the selection to the clipboard and remove it |
29+| Copy | `Ctrl-Ins` | yes | Copy the selection to the clipboard |
30+| Paste | `Shift-Ins` | yes | Insert the clipboard, replacing the selection |
31+| Select all | `Ctrl-A` | yes | Select the whole file |
32+
33+The clipboard is shared between every window of one editor session.
34+
35+## Search
36+
37+| Item | Shortcut | Needs a file | Effect |
38+| --- | --- | --- | --- |
39+| Find… | `Ctrl-F` | yes | Ask for text and options, then jump to the first match |
40+| Find next | `F7` | yes | Jump to the next match, wrapping round the end of the file |
41+| Find previous | `Shift-F7` | yes | Jump to the previous match, wrapping round the start |
42+| Go to line… | `Ctrl-G` | yes | Ask for a line number, counting from one |
43+
44+## Run
45+
46+| Item | Shortcut | Needs a file | Effect |
47+| --- | --- | --- | --- |
48+| Completion | `Ctrl-Space` | yes | Open the completion list at the cursor |
49+| Language server status | | | Show whether a language server is running, and what it is doing |
50+
51+## Code
52+
53+Everything the editor asks the language server about the symbol **under the cursor**. Nothing here needs a selection: almost every request in the protocol takes a position rather than a range, so there is nothing extra to say by selecting first.
54+
55+Its hot key is `Alt-C`.
56+
57+| Item | Shortcut | Needs a file | Effect |
58+| --- | --- | --- | --- |
59+| Describe symbol | `F1` | yes | Show what the language server knows about the symbol under the cursor |
60+| Go to definition | `F12` | yes | Where the symbol is declared. One answer opens it; several offer the list. |
61+| Go to type definition | | yes | Where the *type* of the symbol is declared, which is a different question |
62+| Find implementations… | | yes | What implements it: the types satisfying an interface, the impl blocks of a trait |
63+| Find references… | `Shift-F12` | yes | Where it is used, counting its declaration |
64+| Symbol in file… | | yes | The file's own outline, indented, with each symbol's kind. Choosing one goes to it. |
65+| Symbol in project… | `Ctrl-T` | no | Ask for a name and search the whole project |
66+| Problems… | | no | Every problem the server has reported, for every file it has spoken about |
67+
68+A list of places shows the file, the line, and the text of that line — twelve entries reading `handler.go:42` say nothing about which one you want. The text comes from an open window when there is one, so a file edited and not saved is listed as it now reads.
69+
70+Three answers are told apart, and the difference matters: **nothing found** says so in the question's own words (`No references found`), **the server is not ready** shows what it is doing instead, and **one answer** simply takes you there without a dialog.
71+
72+*Symbol in file* has no shortcut on purpose. The obvious one is `Ctrl-Shift-O`, and a terminal cannot tell that from `Ctrl-O` — the shift is lost before the editor sees it.
73+
74+## Options
75+
76+| Item | Needs a file | Effect |
77+| --- | --- | --- |
78+| Theme… | | List every loadable theme and apply the one chosen, immediately. With a project settings file present, also writes the choice into it. |
79+| Line numbers | yes | Show or hide the gutter in the current window |
80+| Create project settings | | Write `.turbo-go/settings.toml` with the theme in use, and open it. **Greyed out once the project has one.** |
81+| Project settings… | | Open `.turbo-go/settings.toml`. **Greyed out until the project has one.** |
82+
83+## Window
84+
85+| Item | Shortcut | Needs a file | Effect |
86+| --- | --- | --- | --- |
87+| Next | `F6` | yes | Bring the window behind the current one forward |
88+| New terminal | `F8` | no | Open a window running a shell, in the directory of the file in front |
89+| Project tree | `F9` | no | Open a window showing the project's files; brings the existing one forward when there is one |
90+| Tile | | yes | Lay every window out in a grid, none overlapping |
91+| Cascade | | yes | Stack the windows offset, every title visible |
92+| Maximise | | yes | Give the current window the whole desktop, or put it back where it was if it already has it. The same toggle as the `[■]` box on the window's own frame. |
93+| List… | `Alt-0` | yes | List the open windows and bring the chosen one forward |
94+
95+## Snippets
96+
97+Built from `.turbo-go/snippets.toml` and your own snippets file each time it opens. Its hot key is `Alt-N`, because Search already answers to S.
98+
99+| Item | Needs a file | Effect |
100+| --- | --- | --- |
101+| One submenu per group | | Insert the chosen snippet at the cursor; the items need a file open |
102+| Create snippets file | no | Write `.turbo-go/snippets.toml` with worked examples, then open it. **Greyed out once the project has one.** |
103+| Open snippets file | no | Open `.turbo-go/snippets.toml`. **Greyed out until the project has one.** Always the project's file, never your own. |
104+
105+See [Snippets](snippets.md).
106+
107+## Go
108+
109+Built from `.turbo-go/tools.toml` each time it opens. Its hot key is `Alt-G`.
110+
111+| Item | Effect |
112+| --- | --- |
113+| One line per tool that names no `menu` | Run that command, showing its output where the tool asked: a popup, a terminal window, or an editing window |
114+| Create tools file | Write `.turbo-go/tools.toml` with the five Go commands, then open it. **Greyed out once the project has one.** |
115+| Open tools file | Open `.turbo-go/tools.toml`. **Greyed out until the project has one.** |
116+
117+See [Go tools](go-tools.md).
118+
119+## Project menus
120+
121+Not fixed: one menu per `menu` name in `.turbo-go/tools.toml`, in the order the names first appear there, between Go and Help. A project with no tools file, or whose tools all stay in Go, has none.
122+
123+| Item | Effect |
124+| --- | --- |
125+| One line per tool naming that menu | Run that command, showing its output where the tool asked |
126+
127+Their hot keys are assigned rather than fixed, so that a name from a file can never take a letter one of the menus above already answers to. The rules are in [Go tools](go-tools.md#hot-keys).
128+
129+## Help
130+
131+| Item | Effect |
132+| --- | --- |
133+| Keyboard | Show the keys worth knowing |
134+| About | Show the version, the commit and build date when the build recorded them, and the current theme. See [the version number](versioning.md). |
135+
136+## Status bar
137+
138+The hints along the bottom are clickable and run the same actions.
139+
140+| Hint | Action |
141+| --- | --- |
142+| `F1 Describe` | Code ▸ Describe symbol |
143+| `F2 Save` | File ▸ Save |
144+| `F3 Open` | File ▸ Open… |
145+| `F6 Window` | Window ▸ Next |
146+| `F7 Next` | Search ▸ Find next |
147+| `F10 Menu` | Open the menu bar |
148+| `Alt-X Exit` | File ▸ Exit |
149+
150+The right-hand end shows, in this order: the cursor as `line:column`, then either the first error the language server reported for this file (prefixed `⚠`) or the language server's state.
added docs/en/reference/project-settings.md +111 -0
new file mode 100644
@@ -0,0 +1,111 @@
1+# Reference: project settings
2+
3+> Neutral description of `.turbo-go/settings.toml`: where it is looked for, what it may contain, and what writes to it.
4+
5+## Location
6+
7+| Property | Value |
8+| --- | --- |
9+| Directory | `.turbo-go` in the editor's working directory |
10+| File | `.turbo-go/settings.toml` |
11+| Search | The working directory only. Parent directories are **not** searched. |
12+| Read | When the editor starts, and again whenever the file is saved from inside the editor |
13+| Required | No. A project without one gets the defaults below. |
14+
15+## Keys
16+
17+Every key is optional, and every key lives in the `[editor]` table. A key that is absent keeps its default; a key present with any value overrides it, including a value equal to the default.
18+
19+| Key | Type | Default | Description |
20+| --- | --- | --- | --- |
21+| `theme` | string | the editor's own default (`turbo-classic`) | Name of the colour theme to start in, as listed by `turbo-go -list-themes` |
22+| `autosave` | boolean | `false` | Whether modified files are written without being asked. The file **Create project settings** writes sets it to `true`; the default here is what applies to a project with no settings file at all. |
23+| `autosave_delay` | string | `"2s"` | How long after the last keystroke to wait. A Go duration: `"500ms"`, `"2s"`, `"1m"`. Only consulted when `autosave` is true. |
24+
25+### Example
26+
27+```toml
28+[editor]
29+theme = "turbo-dark"
30+autosave = true
31+autosave_delay = "500ms"
32+```
33+
34+## Theme precedence
35+
36+Highest first:
37+
38+| Source | Wins over |
39+| --- | --- |
40+| `-theme` on the command line | everything |
41+| `theme` in the settings file | the built-in default |
42+| The built-in default `turbo-classic` | — |
43+
44+An unknown theme name at any level falls back to the built-in default rather than failing.
45+
46+## When a change takes effect
47+
48+The file is read at start-up, and **again every time it is saved from inside the editor** — so a change made in the editor is in force the moment you press `F2`, with no restart.
49+
50+| Key | Re-applied on save | Why |
51+| --- | --- | --- |
52+| `autosave` | yes | |
53+| `autosave_delay` | yes | |
54+| `theme` | **no** | Options ▸ Theme is the live way to change it, and already writes the choice back here. A `-theme` flag given on the command line is the more explicit statement for that session and is not overridden by a file being saved. |
55+
56+| Outcome | Status bar |
57+| --- | --- |
58+| Read and applied | `Applied .turbo-go/settings.toml — autosave on (2s)` |
59+| Read and applied, autosave off | `Applied .turbo-go/settings.toml — autosave off` |
60+| Saved, but no longer valid TOML | `Saved, but not applied: …` — the previous values stay in force |
61+
62+Saving is saving, whoever did it: automatic saving writing the settings file re-applies them exactly as `F2` does. Editing the file **outside** the editor is not noticed; nothing watches it.
63+
64+## Automatic saving
65+
66+| Behaviour | Detail |
67+| --- | --- |
68+| Trigger | The delay elapsing with no edit in any window |
69+| Scope | Every open file with a name, not only the front one |
70+| Deadline | One for the whole editor, restarted by any edit in any window |
71+| Files with no name | Never saved; never asked about |
72+| Report | `Saved <name>` on the status bar |
73+| Failure | Reported on the status bar, never in a dialog, and not retried until the next edit |
74+| Closing a window | Saves instead of asking, when the file has a name |
75+| Leaving the editor | Saves instead of asking, when the file has a name |
76+
77+## Writes
78+
79+The settings file is written by exactly two actions. Nothing else in the editor writes to it, and nothing creates it by itself.
80+
81+| Action | Effect |
82+| --- | --- |
83+| **Options ▸ Create project settings** | Creates `.turbo-go/settings.toml` with the theme in use, `autosave = true`, and explanatory comments. Greyed out once the project has one, so it cannot be chosen twice. |
84+| **Options ▸ Theme** | Rewrites the `theme` value **only when the file already exists**. Comments, blank lines, key order and any trailing comment on the theme line are kept. |
85+
86+Both write through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact.
87+
88+## Menu items
89+
90+| Item | Menu | Needs a file | Effect |
91+| --- | --- | --- | --- |
92+| Create project settings | Options | refuses the file | As above, then opens the file. Greyed out once the project has one. |
93+| Project settings… | Options | requires the file | Opens `.turbo-go/settings.toml`. Greyed out until the project has one. |
94+
95+## Errors
96+
97+| Message | Cause |
98+| --- | --- |
99+| `turbo-go: reading …/settings.toml: …` on standard error | The file is present but is not valid TOML. The editor opens with its defaults. |
100+| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` is not a Go duration |
101+| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` is zero or negative |
102+| `Already there: .turbo-go/settings.toml` on the status bar | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. |
103+| `Saved, but not applied: …` on the status bar | The settings file was written but no longer parses. The previous values stay in force. |
104+| `This project has no .turbo-go/settings.toml yet.` | **Project settings…** in a project that has none |
105+| `Theme set for this session only: …` | The theme changed but the settings file could not be written |
106+
107+## See also
108+
109+- [How to give a project its own settings](../how-to/configure-a-project.md)
110+- [Project settings](../explanation/project-settings.md)
111+- [Theme file format](themes.md) — a different file, in the same language
new file mode 100644
@@ -0,0 +1,111 @@
1+# Reference: project settings
2+
3+> Neutral description of `.turbo-go/settings.toml`: where it is looked for, what it may contain, and what writes to it.
4+
5+## Location
6+
7+| Property | Value |
8+| --- | --- |
9+| Directory | `.turbo-go` in the editor's working directory |
10+| File | `.turbo-go/settings.toml` |
11+| Search | The working directory only. Parent directories are **not** searched. |
12+| Read | When the editor starts, and again whenever the file is saved from inside the editor |
13+| Required | No. A project without one gets the defaults below. |
14+
15+## Keys
16+
17+Every key is optional, and every key lives in the `[editor]` table. A key that is absent keeps its default; a key present with any value overrides it, including a value equal to the default.
18+
19+| Key | Type | Default | Description |
20+| --- | --- | --- | --- |
21+| `theme` | string | the editor's own default (`turbo-classic`) | Name of the colour theme to start in, as listed by `turbo-go -list-themes` |
22+| `autosave` | boolean | `false` | Whether modified files are written without being asked. The file **Create project settings** writes sets it to `true`; the default here is what applies to a project with no settings file at all. |
23+| `autosave_delay` | string | `"2s"` | How long after the last keystroke to wait. A Go duration: `"500ms"`, `"2s"`, `"1m"`. Only consulted when `autosave` is true. |
24+
25+### Example
26+
27+```toml
28+[editor]
29+theme = "turbo-dark"
30+autosave = true
31+autosave_delay = "500ms"
32+```
33+
34+## Theme precedence
35+
36+Highest first:
37+
38+| Source | Wins over |
39+| --- | --- |
40+| `-theme` on the command line | everything |
41+| `theme` in the settings file | the built-in default |
42+| The built-in default `turbo-classic` | — |
43+
44+An unknown theme name at any level falls back to the built-in default rather than failing.
45+
46+## When a change takes effect
47+
48+The file is read at start-up, and **again every time it is saved from inside the editor** — so a change made in the editor is in force the moment you press `F2`, with no restart.
49+
50+| Key | Re-applied on save | Why |
51+| --- | --- | --- |
52+| `autosave` | yes | |
53+| `autosave_delay` | yes | |
54+| `theme` | **no** | Options ▸ Theme is the live way to change it, and already writes the choice back here. A `-theme` flag given on the command line is the more explicit statement for that session and is not overridden by a file being saved. |
55+
56+| Outcome | Status bar |
57+| --- | --- |
58+| Read and applied | `Applied .turbo-go/settings.toml — autosave on (2s)` |
59+| Read and applied, autosave off | `Applied .turbo-go/settings.toml — autosave off` |
60+| Saved, but no longer valid TOML | `Saved, but not applied: …` — the previous values stay in force |
61+
62+Saving is saving, whoever did it: automatic saving writing the settings file re-applies them exactly as `F2` does. Editing the file **outside** the editor is not noticed; nothing watches it.
63+
64+## Automatic saving
65+
66+| Behaviour | Detail |
67+| --- | --- |
68+| Trigger | The delay elapsing with no edit in any window |
69+| Scope | Every open file with a name, not only the front one |
70+| Deadline | One for the whole editor, restarted by any edit in any window |
71+| Files with no name | Never saved; never asked about |
72+| Report | `Saved <name>` on the status bar |
73+| Failure | Reported on the status bar, never in a dialog, and not retried until the next edit |
74+| Closing a window | Saves instead of asking, when the file has a name |
75+| Leaving the editor | Saves instead of asking, when the file has a name |
76+
77+## Writes
78+
79+The settings file is written by exactly two actions. Nothing else in the editor writes to it, and nothing creates it by itself.
80+
81+| Action | Effect |
82+| --- | --- |
83+| **Options ▸ Create project settings** | Creates `.turbo-go/settings.toml` with the theme in use, `autosave = true`, and explanatory comments. Greyed out once the project has one, so it cannot be chosen twice. |
84+| **Options ▸ Theme** | Rewrites the `theme` value **only when the file already exists**. Comments, blank lines, key order and any trailing comment on the theme line are kept. |
85+
86+Both write through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact.
87+
88+## Menu items
89+
90+| Item | Menu | Needs a file | Effect |
91+| --- | --- | --- | --- |
92+| Create project settings | Options | refuses the file | As above, then opens the file. Greyed out once the project has one. |
93+| Project settings… | Options | requires the file | Opens `.turbo-go/settings.toml`. Greyed out until the project has one. |
94+
95+## Errors
96+
97+| Message | Cause |
98+| --- | --- |
99+| `turbo-go: reading …/settings.toml: …` on standard error | The file is present but is not valid TOML. The editor opens with its defaults. |
100+| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` is not a Go duration |
101+| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` is zero or negative |
102+| `Already there: .turbo-go/settings.toml` on the status bar | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. |
103+| `Saved, but not applied: …` on the status bar | The settings file was written but no longer parses. The previous values stay in force. |
104+| `This project has no .turbo-go/settings.toml yet.` | **Project settings…** in a project that has none |
105+| `Theme set for this session only: …` | The theme changed but the settings file could not be written |
106+
107+## See also
108+
109+- [How to give a project its own settings](../how-to/configure-a-project.md)
110+- [Project settings](../explanation/project-settings.md)
111+- [Theme file format](themes.md) — a different file, in the same language
added docs/en/reference/project-tree.md +102 -0
new file mode 100644
@@ -0,0 +1,102 @@
1+# Reference: project tree
2+
3+> Neutral description of the project tree window: what it shows, what it hides, and the keys it answers to.
4+
5+## Opening
6+
7+| Route | Condition |
8+| --- | --- |
9+| `F9` | Always |
10+| **Window ▸ Project tree** | Always |
11+
12+Neither requires a file to be open. Both bring the existing tree forward when one is already open: there is at most one tree window.
13+
14+## Root
15+
16+| Property | Value |
17+| --- | --- |
18+| Rooted at | The directory the editor was started in (`os.Getwd()`) |
19+| Search | That directory only. Parent directories are **not** searched, the same rule `.turbo-go/settings.toml` follows. |
20+| Window title | The base name of that directory |
21+| Root row | Not shown; the first row is the first entry inside the project |
22+
23+## What is listed
24+
25+| Rule | Detail |
26+| --- | --- |
27+| Order | Directories first, then files; each group sorted by name |
28+| Hidden | `.git` only |
29+| Shown | Every other entry, dot-entries included — `.turbo-go`, `.gitignore`, `.qlty` |
30+| Reading | A directory is read the first time it is expanded, and not before |
31+| Unreadable directory | Shows as expanded and empty; the rest of the tree is unaffected |
32+
33+## Markers
34+
35+| Marker | Meaning |
36+| --- | --- |
37+| `▶ ` | A directory that is closed |
38+| `▼ ` | A directory that is open |
39+| (two spaces) | A file — indented by a marker's width so names line up |
40+
41+Each level of depth adds two more spaces of indentation.
42+
43+## Keys
44+
45+Handled when the tree window has the focus.
46+
47+| Key | Action |
48+| --- | --- |
49+| `↑` `↓` | Previous / next row |
50+| `PgUp` `PgDn` | A screenful at a time |
51+| `Home` `End` | First / last row |
52+| `→` | Expand a closed directory; otherwise move to the next row |
53+| `←` | Collapse an open directory; otherwise move to the directory this row is in |
54+| `Enter` | Open a file; expand or collapse a directory |
55+| `F5`, `Ctrl-R` | Re-read the project |
56+
57+The editor's own shortcuts apply as usual: `F6` moves to the next window, `Ctrl-W` closes the tree, `Alt-X` leaves.
58+
59+## Mouse
60+
61+| Action | Effect |
62+| --- | --- |
63+| Click a row | Move the highlight to it |
64+| Click the highlighted row | Act on it, as `Enter` does |
65+| Wheel up / down | Move the highlight three rows |
66+
67+## Refreshing
68+
69+| Trigger | Effect |
70+| --- | --- |
71+| `F5` or `Ctrl-R` | Re-reads every directory that has been opened |
72+| Saving a file | The same, automatically |
73+| Expanding a directory | Reads that directory, if it has not been read |
74+
75+Refreshing keeps the shape of the tree: a directory that was open stays open, one that has been deleted takes its branch with it, and directories nobody has opened stay unread. The highlight stays on the same entry, or on the nearest remaining row when that entry has gone.
76+
77+The tree does **not** watch the filesystem. A file created by a terminal window, or by `git checkout`, appears only after a refresh.
78+
79+## Colours
80+
81+| Theme key | What it colours |
82+| --- | --- |
83+| `tree.text` | A file's name, and the tree's background |
84+| `tree.directory` | A directory's name |
85+| `tree.selected` | The highlighted row, when the tree has the focus |
86+| `tree.unfocused` | The highlighted row, when it does not |
87+
88+These do not fall back to the `list.*` keys: dotted fallback runs along the dots and stops at `default`. See [Theme file format](themes.md).
89+
90+## Errors
91+
92+| Message | Cause |
93+| --- | --- |
94+| `Cannot tell which directory this is: …` | The working directory could not be read |
95+| `reading …: …` | The project directory could not be read |
96+| `… is not a directory` | The root resolved to a file |
97+
98+## See also
99+
100+- [How to browse a project and open files from a tree](../how-to/browse-a-project.md)
101+- [Project tree](../explanation/project-tree.md)
102+- [Keyboard](keyboard.md)
new file mode 100644
@@ -0,0 +1,102 @@
1+# Reference: project tree
2+
3+> Neutral description of the project tree window: what it shows, what it hides, and the keys it answers to.
4+
5+## Opening
6+
7+| Route | Condition |
8+| --- | --- |
9+| `F9` | Always |
10+| **Window ▸ Project tree** | Always |
11+
12+Neither requires a file to be open. Both bring the existing tree forward when one is already open: there is at most one tree window.
13+
14+## Root
15+
16+| Property | Value |
17+| --- | --- |
18+| Rooted at | The directory the editor was started in (`os.Getwd()`) |
19+| Search | That directory only. Parent directories are **not** searched, the same rule `.turbo-go/settings.toml` follows. |
20+| Window title | The base name of that directory |
21+| Root row | Not shown; the first row is the first entry inside the project |
22+
23+## What is listed
24+
25+| Rule | Detail |
26+| --- | --- |
27+| Order | Directories first, then files; each group sorted by name |
28+| Hidden | `.git` only |
29+| Shown | Every other entry, dot-entries included — `.turbo-go`, `.gitignore`, `.qlty` |
30+| Reading | A directory is read the first time it is expanded, and not before |
31+| Unreadable directory | Shows as expanded and empty; the rest of the tree is unaffected |
32+
33+## Markers
34+
35+| Marker | Meaning |
36+| --- | --- |
37+| `▶ ` | A directory that is closed |
38+| `▼ ` | A directory that is open |
39+| (two spaces) | A file — indented by a marker's width so names line up |
40+
41+Each level of depth adds two more spaces of indentation.
42+
43+## Keys
44+
45+Handled when the tree window has the focus.
46+
47+| Key | Action |
48+| --- | --- |
49+| `↑` `↓` | Previous / next row |
50+| `PgUp` `PgDn` | A screenful at a time |
51+| `Home` `End` | First / last row |
52+| `→` | Expand a closed directory; otherwise move to the next row |
53+| `←` | Collapse an open directory; otherwise move to the directory this row is in |
54+| `Enter` | Open a file; expand or collapse a directory |
55+| `F5`, `Ctrl-R` | Re-read the project |
56+
57+The editor's own shortcuts apply as usual: `F6` moves to the next window, `Ctrl-W` closes the tree, `Alt-X` leaves.
58+
59+## Mouse
60+
61+| Action | Effect |
62+| --- | --- |
63+| Click a row | Move the highlight to it |
64+| Click the highlighted row | Act on it, as `Enter` does |
65+| Wheel up / down | Move the highlight three rows |
66+
67+## Refreshing
68+
69+| Trigger | Effect |
70+| --- | --- |
71+| `F5` or `Ctrl-R` | Re-reads every directory that has been opened |
72+| Saving a file | The same, automatically |
73+| Expanding a directory | Reads that directory, if it has not been read |
74+
75+Refreshing keeps the shape of the tree: a directory that was open stays open, one that has been deleted takes its branch with it, and directories nobody has opened stay unread. The highlight stays on the same entry, or on the nearest remaining row when that entry has gone.
76+
77+The tree does **not** watch the filesystem. A file created by a terminal window, or by `git checkout`, appears only after a refresh.
78+
79+## Colours
80+
81+| Theme key | What it colours |
82+| --- | --- |
83+| `tree.text` | A file's name, and the tree's background |
84+| `tree.directory` | A directory's name |
85+| `tree.selected` | The highlighted row, when the tree has the focus |
86+| `tree.unfocused` | The highlighted row, when it does not |
87+
88+These do not fall back to the `list.*` keys: dotted fallback runs along the dots and stops at `default`. See [Theme file format](themes.md).
89+
90+## Errors
91+
92+| Message | Cause |
93+| --- | --- |
94+| `Cannot tell which directory this is: …` | The working directory could not be read |
95+| `reading …: …` | The project directory could not be read |
96+| `… is not a directory` | The root resolved to a file |
97+
98+## See also
99+
100+- [How to browse a project and open files from a tree](../how-to/browse-a-project.md)
101+- [Project tree](../explanation/project-tree.md)
102+- [Keyboard](keyboard.md)
added docs/en/reference/snippets.md +114 -0
new file mode 100644
@@ -0,0 +1,114 @@
1+# Reference: snippets
2+
3+> Neutral description of the snippets files, the Snippets menu, and how a snippet is inserted.
4+
5+## Files
6+
7+Both are read, and both are optional.
8+
9+| File | Holds |
10+| --- | --- |
11+| `./.turbo-go/snippets.toml` | The project's snippets |
12+| `$TURBO_GO_SNIPPET_DIR/snippets.toml`, else `<user config>/turbo-go/snippets.toml` | Your own, shared across projects |
13+
14+`<user config>` is `os.UserConfigDir()`: `~/.config` on Linux, `~/Library/Application Support` on macOS.
15+
16+| Property | Value |
17+| --- | --- |
18+| Project search | The working directory only. Parent directories are **not** searched. |
19+| Read | Every time the Snippets menu opens |
20+| Order | Your own first, then the project's |
21+| Name clash | Same `group` **and** `name` → the project's replaces yours |
22+| Missing file | Not an error |
23+| Unreadable file | An error, shown in the menu |
24+
25+## File format
26+
27+One `[[snippet]]` table per snippet.
28+
29+| Key | Type | Required | Description |
30+| --- | --- | --- | --- |
31+| `name` | string | yes | What the menu shows |
32+| `body` | string | yes | The text inserted at the cursor |
33+| `group` | string | no | The submenu it goes in; absent means `General` |
34+| `languages` | array of strings | no | Restricts the snippet to those languages; absent means every file |
35+
36+`languages` uses the editor's own language names: `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. See [Languages coloured](languages.md).
37+
38+A snippet with no `name` or no `body` makes the whole file an error — it could not be shown or could not be inserted.
39+
40+### Example
41+
42+```toml
43+[[snippet]]
44+name = "if err != nil"
45+group = "Go"
46+languages = ["go"]
47+body = """
48+if err != nil {
49+ return err
50+}"""
51+```
52+
53+TOML's `"""` strings drop the newline immediately after the opening quotes, and interpret `\t` as a tab.
54+
55+## The menu
56+
57+| Item | Condition |
58+| --- | --- |
59+| One submenu per group, in the order the groups first appear in the files | A group with at least one snippet applying to the front window |
60+| `Cannot read snippets`, greyed out | A file is present but unreadable |
61+| `Create snippets file` | The project has no snippets file |
62+| `Open snippets file` | The project has one |
63+
64+The menu's hot key is `Alt-N`, not `Alt-S`: Search already answers to S.
65+
66+Groups, and the snippets inside them, come out in the order they were read, so the menu matches the files.
67+
68+A snippet item is greyed out when there is no file open to insert into — a terminal or the project tree in front counts as no file.
69+
70+### Filtering
71+
72+| Front window | Snippets offered |
73+| --- | --- |
74+| A file of a recognised language | Those naming that language, plus those naming none |
75+| A file of no recognised language | Those naming none |
76+| A terminal, the project tree, or nothing | Those naming none |
77+
78+## Insertion
79+
80+| Behaviour | Detail |
81+| --- | --- |
82+| Position | At the cursor |
83+| First line | Inserted where the cursor is |
84+| Later lines | Prefixed with the leading whitespace of the line the cursor was on |
85+| Blank lines in the body | Left blank, not padded with whitespace |
86+| Undo | One step for the whole snippet |
87+| Cursor after | At the end of the inserted text |
88+| Report | `Snippet inserted` on the status bar |
89+
90+The indent copied is the **whitespace prefix of the current line**, tabs or spaces as they were, so a snippet follows whatever the file already uses.
91+
92+## Menu items
93+
94+| Item | Menu | Effect |
95+| --- | --- | --- |
96+| Create snippets file | Snippets | Writes `.turbo-go/snippets.toml` with worked examples, then opens it. Greyed out once the project has one. |
97+| Open snippets file | Snippets | Opens `.turbo-go/snippets.toml`. Greyed out until the project has one. Always the project's file, never your own — it is the file the item above it writes. |
98+
99+The file is written through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact.
100+
101+## Errors
102+
103+| Message | Cause |
104+| --- | --- |
105+| `Cannot read snippets` in the menu | A snippets file is present but not valid TOML, or holds a snippet with no name or no body |
106+| `Already there: .turbo-go/snippets.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. |
107+| `This project has no .turbo-go/snippets.toml yet.` | Opening in a project that has none, likewise |
108+| `Cannot tell which directory this is: …` | The working directory could not be read |
109+
110+## See also
111+
112+- [How to insert snippets from a menu](../how-to/use-snippets.md)
113+- [Snippets](../explanation/snippets.md)
114+- [Keyboard](keyboard.md)
new file mode 100644
@@ -0,0 +1,114 @@
1+# Reference: snippets
2+
3+> Neutral description of the snippets files, the Snippets menu, and how a snippet is inserted.
4+
5+## Files
6+
7+Both are read, and both are optional.
8+
9+| File | Holds |
10+| --- | --- |
11+| `./.turbo-go/snippets.toml` | The project's snippets |
12+| `$TURBO_GO_SNIPPET_DIR/snippets.toml`, else `<user config>/turbo-go/snippets.toml` | Your own, shared across projects |
13+
14+`<user config>` is `os.UserConfigDir()`: `~/.config` on Linux, `~/Library/Application Support` on macOS.
15+
16+| Property | Value |
17+| --- | --- |
18+| Project search | The working directory only. Parent directories are **not** searched. |
19+| Read | Every time the Snippets menu opens |
20+| Order | Your own first, then the project's |
21+| Name clash | Same `group` **and** `name` → the project's replaces yours |
22+| Missing file | Not an error |
23+| Unreadable file | An error, shown in the menu |
24+
25+## File format
26+
27+One `[[snippet]]` table per snippet.
28+
29+| Key | Type | Required | Description |
30+| --- | --- | --- | --- |
31+| `name` | string | yes | What the menu shows |
32+| `body` | string | yes | The text inserted at the cursor |
33+| `group` | string | no | The submenu it goes in; absent means `General` |
34+| `languages` | array of strings | no | Restricts the snippet to those languages; absent means every file |
35+
36+`languages` uses the editor's own language names: `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. See [Languages coloured](languages.md).
37+
38+A snippet with no `name` or no `body` makes the whole file an error — it could not be shown or could not be inserted.
39+
40+### Example
41+
42+```toml
43+[[snippet]]
44+name = "if err != nil"
45+group = "Go"
46+languages = ["go"]
47+body = """
48+if err != nil {
49+ return err
50+}"""
51+```
52+
53+TOML's `"""` strings drop the newline immediately after the opening quotes, and interpret `\t` as a tab.
54+
55+## The menu
56+
57+| Item | Condition |
58+| --- | --- |
59+| One submenu per group, in the order the groups first appear in the files | A group with at least one snippet applying to the front window |
60+| `Cannot read snippets`, greyed out | A file is present but unreadable |
61+| `Create snippets file` | The project has no snippets file |
62+| `Open snippets file` | The project has one |
63+
64+The menu's hot key is `Alt-N`, not `Alt-S`: Search already answers to S.
65+
66+Groups, and the snippets inside them, come out in the order they were read, so the menu matches the files.
67+
68+A snippet item is greyed out when there is no file open to insert into — a terminal or the project tree in front counts as no file.
69+
70+### Filtering
71+
72+| Front window | Snippets offered |
73+| --- | --- |
74+| A file of a recognised language | Those naming that language, plus those naming none |
75+| A file of no recognised language | Those naming none |
76+| A terminal, the project tree, or nothing | Those naming none |
77+
78+## Insertion
79+
80+| Behaviour | Detail |
81+| --- | --- |
82+| Position | At the cursor |
83+| First line | Inserted where the cursor is |
84+| Later lines | Prefixed with the leading whitespace of the line the cursor was on |
85+| Blank lines in the body | Left blank, not padded with whitespace |
86+| Undo | One step for the whole snippet |
87+| Cursor after | At the end of the inserted text |
88+| Report | `Snippet inserted` on the status bar |
89+
90+The indent copied is the **whitespace prefix of the current line**, tabs or spaces as they were, so a snippet follows whatever the file already uses.
91+
92+## Menu items
93+
94+| Item | Menu | Effect |
95+| --- | --- | --- |
96+| Create snippets file | Snippets | Writes `.turbo-go/snippets.toml` with worked examples, then opens it. Greyed out once the project has one. |
97+| Open snippets file | Snippets | Opens `.turbo-go/snippets.toml`. Greyed out until the project has one. Always the project's file, never your own — it is the file the item above it writes. |
98+
99+The file is written through a temporary file in the same directory, renamed into place, so an interrupted write leaves the previous file intact.
100+
101+## Errors
102+
103+| Message | Cause |
104+| --- | --- |
105+| `Cannot read snippets` in the menu | A snippets file is present but not valid TOML, or holds a snippet with no name or no body |
106+| `Already there: .turbo-go/snippets.toml` | Creating in a project that already has one. Unreachable from the menu, which greys the item out; still possible for a caller that is not a menu. |
107+| `This project has no .turbo-go/snippets.toml yet.` | Opening in a project that has none, likewise |
108+| `Cannot tell which directory this is: …` | The working directory could not be read |
109+
110+## See also
111+
112+- [How to insert snippets from a menu](../how-to/use-snippets.md)
113+- [Snippets](../explanation/snippets.md)
114+- [Keyboard](keyboard.md)
added docs/en/reference/terminal.md +228 -0
new file mode 100644
@@ -0,0 +1,228 @@
1+# Reference: terminal windows
2+
3+> Neutral description of the terminal windows Turbo Go opens, the keys they answer to, and the escape sequences the emulator implements.
4+
5+## Opening
6+
7+| Route | Condition |
8+| --- | --- |
9+| `F8` | Always |
10+| **Window ▸ New terminal** | Always |
11+
12+Neither requires a file to be open.
13+
14+## The shell
15+
16+| Property | Value |
17+| --- | --- |
18+| Program | `$SHELL`, or `/bin/sh` when it is unset or empty; on Windows `%COMSPEC%`, or `cmd.exe` |
19+| Working directory | The directory of the file in the front window; the editor's working directory when no file is open |
20+| `TERM` | `xterm-256color`, always — replacing any inherited value |
21+| Environment | The editor's own, with `TERM` replaced |
22+| Controlling terminal | Yes: on Linux and macOS the shell runs in its own session with the pseudo-terminal as its controlling terminal; on Windows it is attached to a pseudo-console. Either way job control and `Ctrl-C` work |
23+| Initial size | The window's, updated whenever the window is resized |
24+
25+## Platform support
26+
27+| Platform | Behaviour |
28+| --- | --- |
29+| Linux | Supported (`/dev/ptmx`) |
30+| macOS | Supported (`/dev/ptmx`) |
31+| Windows | Supported (pseudo-console, ConPTY): Windows 10 version 1809 or later. Built and vetted; **not yet run by the authors** on a Windows machine |
32+| Others | `F8` opens a message saying terminal windows are not supported yet; nothing else changes |
33+
34+## Keys
35+
36+### After the program has gone
37+
38+A window whose command has finished keeps its output, but stops behaving like a terminal: only `Shift-PgUp` and `Shift-PgDn` are still taken, and every other key reaches the editor — which is what lets `Ctrl-W` close it.
39+
40+### Sent to the shell
41+
42+Every key not listed under "kept by the editor" below, encoded as a terminal expects it.
43+
44+| Key | Bytes sent |
45+| --- | --- |
46+| printable character | its UTF-8 encoding |
47+| `Alt-<key>` | `ESC` followed by that key's own bytes |
48+| `Ctrl-A``Ctrl-Z` | `0x01``0x1a` |
49+| `Enter` | `\r` |
50+| `Tab` | `\t` |
51+| `Shift-Tab` | `ESC [ Z` |
52+| `Backspace` | `0x7f` |
53+| `Escape` | `0x1b` |
54+| `↑` `↓` `→` `←` | `ESC [ A B C D`, or `ESC O A B C D` in application cursor mode |
55+| `Home` `End` | `ESC [ H`, `ESC [ F`, or the `ESC O` forms in application cursor mode |
56+| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` |
57+| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` |
58+| `F1``F4` | `ESC O P Q R S` |
59+| `F5``F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` |
60+
61+A key with no terminal meaning sends nothing.
62+
63+### Kept by the editor
64+
65+| Key | Action |
66+| --- | --- |
67+| `F1``F12` | Their usual editor action |
68+| `Alt-X` | Exit |
69+| `Alt-0``Alt-9` | List windows / bring window 1…9 forward |
70+
71+Function keys therefore never reach a program inside a terminal window.
72+
73+### Handled by the terminal window itself
74+
75+| Key | Action |
76+| --- | --- |
77+| `Shift-PgUp` | Back one screenful through the history |
78+| `Shift-PgDn` | Forward one screenful |
79+
80+Any key sent to the shell also returns the view to the live screen.
81+
82+## Mouse
83+
84+| Action | Effect |
85+| --- | --- |
86+| Wheel up / down | Scroll three lines through the history |
87+| Click | Brings the window forward; not forwarded to the program |
88+
89+Mouse reporting is not implemented, so a program is never told about clicks.
90+
91+## History
92+
93+| Property | Value |
94+| --- | --- |
95+| Lines kept | 2000 |
96+| What is kept | Lines scrolled off the top of the primary screen only |
97+| Alternate screen | Not kept — a full-screen program leaves no history behind |
98+
99+## Emulation
100+
101+`TERM` is `xterm-256color`. What is implemented of it:
102+
103+### Control characters
104+
105+| Byte | Effect |
106+| --- | --- |
107+| `0x07` BEL | Noted; the editor does not sound it |
108+| `0x08` BS | Cursor left one column |
109+| `0x09` HT | To the next tab stop, every 8 columns |
110+| `0x0a` `0x0b` `0x0c` | Line feed |
111+| `0x0d` CR | To column 1 |
112+
113+### Escape sequences
114+
115+| Sequence | Name | Effect |
116+| --- | --- | --- |
117+| `ESC D` | IND | Line feed |
118+| `ESC E` | NEL | Carriage return and line feed |
119+| `ESC M` | RI | Reverse line feed, keeping the column |
120+| `ESC 7` | DECSC | Save cursor and style |
121+| `ESC 8` | DECRC | Restore cursor and style |
122+| `ESC c` | RIS | Full reset |
123+
124+### CSI sequences
125+
126+| Sequence | Name | Effect |
127+| --- | --- | --- |
128+| `CSI n A B C D` | CUU CUD CUF CUB | Move n cells up, down, right, left |
129+| `CSI n E F` | CNL CPL | n lines down / up, to column 1 |
130+| `CSI n G` | CHA | To column n |
131+| `CSI r ; c H`, `CSI r ; c f` | CUP HVP | To row r, column c |
132+| `CSI n d` | VPA | To row n |
133+| `CSI n J` | ED | Erase display: 0 to end, 1 to start, 2 or 3 all |
134+| `CSI n K` | EL | Erase line: 0 to end, 1 to start, 2 all |
135+| `CSI n L` | IL | Insert n blank lines at the cursor |
136+| `CSI n M` | DL | Delete n lines at the cursor |
137+| `CSI n @` | ICH | Insert n blank cells |
138+| `CSI n P` | DCH | Delete n cells |
139+| `CSI n X` | ECH | Erase n cells in place |
140+| `CSI n S` | SU | Scroll the region up n lines |
141+| `CSI n T` | SD | Scroll the region down n lines |
142+| `CSI t ; b r` | DECSTBM | Set the scroll region to rows t…b |
143+| `CSI s`, `CSI u` | SCP RCP | Save / restore the cursor |
144+| `CSI … m` | SGR | Colours and attributes, below |
145+
146+`IL` and `DL` do nothing when the cursor is outside the scroll region.
147+
148+### Private modes
149+
150+Set with `CSI ? n h`, cleared with `CSI ? n l`.
151+
152+| n | Name | Effect |
153+| --- | --- | --- |
154+| 1 | DECCKM | Application cursor keys: arrows send `ESC O x` |
155+| 7 | DECAWM | Auto-wrap at the right margin |
156+| 25 | DECTCEM | Show the cursor |
157+| 47, 1047 | | Alternate screen |
158+| 1048 | | Save / restore the cursor |
159+| 1049 | | Save the cursor, then the alternate screen |
160+
161+Any other mode is parsed and ignored.
162+
163+### SGR
164+
165+| Code | Effect |
166+| --- | --- |
167+| 0 | Reset |
168+| 1, 22 | Bold on / off |
169+| 2, 22 | Dim on / off |
170+| 3, 23 | Italic on / off |
171+| 4, 24 | Underline on / off |
172+| 5, 6, 25 | Blink on / off |
173+| 7, 27 | Reverse on / off |
174+| 9, 29 | Strike-through on / off |
175+| 30–37, 40–47 | The eight normal colours, foreground / background |
176+| 90–97, 100–107 | The eight bright colours, foreground / background |
177+| 38;5;n, 48;5;n | Palette colour n of 256 |
178+| 38;2;r;g;b, 48;2;r;g;b | 24-bit colour |
179+| 39, 49 | Back to the theme's colour |
180+
181+The sixteen named colours are tcell's, which means the palette the user's own terminal is configured with, not fixed hex values. An extended colour that runs out of parameters partway leaves the style unchanged. Any other code is ignored.
182+
183+### OSC
184+
185+| Sequence | Effect |
186+| --- | --- |
187+| `OSC 0 ; text BEL`, `OSC 2 ; text BEL` | Set the window title |
188+| `OSC … ST` | The `ESC \` terminator is accepted in place of BEL |
189+
190+The title is capped at 4096 bytes. Other OSC commands are parsed and ignored.
191+
192+### Consumed and ignored
193+
194+Parsed correctly, so they never appear as stray characters, but with no effect:
195+
196+| Sequence | Name |
197+| --- | --- |
198+| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — read to their string terminator |
199+| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <space>` | Character-set and line-size selectors — the emulator works in UTF-8 regardless |
200+| `CSI ? n h`, `CSI ? n l` for any other n | Private modes not listed above |
201+| Any other CSI final byte, SGR code, or OSC command | |
202+
203+### Not implemented
204+
205+Mouse reporting, bracketed paste, shift-in / shift-out, double-width lines, sixel and other graphics protocols, and the DEC status and device-attribute reports. A program that asks for one of these gets no reply, so a program that waits for one waits forever.
206+
207+## Colours
208+
209+| Theme key | What it colours |
210+| --- | --- |
211+| `terminal.text` | Every cell whose colour the program did not choose |
212+| `terminal.cursor` | The cell under the cursor, when the window has the focus |
213+
214+See [Theme file format](themes.md).
215+
216+## Errors
217+
218+| Message | Cause |
219+| --- | --- |
220+| Terminal windows are not supported on this platform yet | The build has no pseudo-terminal support: any platform other than Linux, macOS and Windows |
221+| `openpt: …`, `grantpt: …`, `ptsname: …` | The operating system refused to open a pseudo-terminal |
222+| `fork/exec …: no such file or directory` | `$SHELL` names a program that does not exist |
223+
224+## See also
225+
226+- [How to run shell commands without leaving the editor](../how-to/use-a-terminal.md)
227+- [Terminal windows](../explanation/terminal-windows.md)
228+- [Keyboard](keyboard.md)
new file mode 100644
@@ -0,0 +1,228 @@
1+# Reference: terminal windows
2+
3+> Neutral description of the terminal windows Turbo Go opens, the keys they answer to, and the escape sequences the emulator implements.
4+
5+## Opening
6+
7+| Route | Condition |
8+| --- | --- |
9+| `F8` | Always |
10+| **Window ▸ New terminal** | Always |
11+
12+Neither requires a file to be open.
13+
14+## The shell
15+
16+| Property | Value |
17+| --- | --- |
18+| Program | `$SHELL`, or `/bin/sh` when it is unset or empty; on Windows `%COMSPEC%`, or `cmd.exe` |
19+| Working directory | The directory of the file in the front window; the editor's working directory when no file is open |
20+| `TERM` | `xterm-256color`, always — replacing any inherited value |
21+| Environment | The editor's own, with `TERM` replaced |
22+| Controlling terminal | Yes: on Linux and macOS the shell runs in its own session with the pseudo-terminal as its controlling terminal; on Windows it is attached to a pseudo-console. Either way job control and `Ctrl-C` work |
23+| Initial size | The window's, updated whenever the window is resized |
24+
25+## Platform support
26+
27+| Platform | Behaviour |
28+| --- | --- |
29+| Linux | Supported (`/dev/ptmx`) |
30+| macOS | Supported (`/dev/ptmx`) |
31+| Windows | Supported (pseudo-console, ConPTY): Windows 10 version 1809 or later. Built and vetted; **not yet run by the authors** on a Windows machine |
32+| Others | `F8` opens a message saying terminal windows are not supported yet; nothing else changes |
33+
34+## Keys
35+
36+### After the program has gone
37+
38+A window whose command has finished keeps its output, but stops behaving like a terminal: only `Shift-PgUp` and `Shift-PgDn` are still taken, and every other key reaches the editor — which is what lets `Ctrl-W` close it.
39+
40+### Sent to the shell
41+
42+Every key not listed under "kept by the editor" below, encoded as a terminal expects it.
43+
44+| Key | Bytes sent |
45+| --- | --- |
46+| printable character | its UTF-8 encoding |
47+| `Alt-<key>` | `ESC` followed by that key's own bytes |
48+| `Ctrl-A``Ctrl-Z` | `0x01``0x1a` |
49+| `Enter` | `\r` |
50+| `Tab` | `\t` |
51+| `Shift-Tab` | `ESC [ Z` |
52+| `Backspace` | `0x7f` |
53+| `Escape` | `0x1b` |
54+| `↑` `↓` `→` `←` | `ESC [ A B C D`, or `ESC O A B C D` in application cursor mode |
55+| `Home` `End` | `ESC [ H`, `ESC [ F`, or the `ESC O` forms in application cursor mode |
56+| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` |
57+| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` |
58+| `F1``F4` | `ESC O P Q R S` |
59+| `F5``F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` |
60+
61+A key with no terminal meaning sends nothing.
62+
63+### Kept by the editor
64+
65+| Key | Action |
66+| --- | --- |
67+| `F1``F12` | Their usual editor action |
68+| `Alt-X` | Exit |
69+| `Alt-0``Alt-9` | List windows / bring window 1…9 forward |
70+
71+Function keys therefore never reach a program inside a terminal window.
72+
73+### Handled by the terminal window itself
74+
75+| Key | Action |
76+| --- | --- |
77+| `Shift-PgUp` | Back one screenful through the history |
78+| `Shift-PgDn` | Forward one screenful |
79+
80+Any key sent to the shell also returns the view to the live screen.
81+
82+## Mouse
83+
84+| Action | Effect |
85+| --- | --- |
86+| Wheel up / down | Scroll three lines through the history |
87+| Click | Brings the window forward; not forwarded to the program |
88+
89+Mouse reporting is not implemented, so a program is never told about clicks.
90+
91+## History
92+
93+| Property | Value |
94+| --- | --- |
95+| Lines kept | 2000 |
96+| What is kept | Lines scrolled off the top of the primary screen only |
97+| Alternate screen | Not kept — a full-screen program leaves no history behind |
98+
99+## Emulation
100+
101+`TERM` is `xterm-256color`. What is implemented of it:
102+
103+### Control characters
104+
105+| Byte | Effect |
106+| --- | --- |
107+| `0x07` BEL | Noted; the editor does not sound it |
108+| `0x08` BS | Cursor left one column |
109+| `0x09` HT | To the next tab stop, every 8 columns |
110+| `0x0a` `0x0b` `0x0c` | Line feed |
111+| `0x0d` CR | To column 1 |
112+
113+### Escape sequences
114+
115+| Sequence | Name | Effect |
116+| --- | --- | --- |
117+| `ESC D` | IND | Line feed |
118+| `ESC E` | NEL | Carriage return and line feed |
119+| `ESC M` | RI | Reverse line feed, keeping the column |
120+| `ESC 7` | DECSC | Save cursor and style |
121+| `ESC 8` | DECRC | Restore cursor and style |
122+| `ESC c` | RIS | Full reset |
123+
124+### CSI sequences
125+
126+| Sequence | Name | Effect |
127+| --- | --- | --- |
128+| `CSI n A B C D` | CUU CUD CUF CUB | Move n cells up, down, right, left |
129+| `CSI n E F` | CNL CPL | n lines down / up, to column 1 |
130+| `CSI n G` | CHA | To column n |
131+| `CSI r ; c H`, `CSI r ; c f` | CUP HVP | To row r, column c |
132+| `CSI n d` | VPA | To row n |
133+| `CSI n J` | ED | Erase display: 0 to end, 1 to start, 2 or 3 all |
134+| `CSI n K` | EL | Erase line: 0 to end, 1 to start, 2 all |
135+| `CSI n L` | IL | Insert n blank lines at the cursor |
136+| `CSI n M` | DL | Delete n lines at the cursor |
137+| `CSI n @` | ICH | Insert n blank cells |
138+| `CSI n P` | DCH | Delete n cells |
139+| `CSI n X` | ECH | Erase n cells in place |
140+| `CSI n S` | SU | Scroll the region up n lines |
141+| `CSI n T` | SD | Scroll the region down n lines |
142+| `CSI t ; b r` | DECSTBM | Set the scroll region to rows t…b |
143+| `CSI s`, `CSI u` | SCP RCP | Save / restore the cursor |
144+| `CSI … m` | SGR | Colours and attributes, below |
145+
146+`IL` and `DL` do nothing when the cursor is outside the scroll region.
147+
148+### Private modes
149+
150+Set with `CSI ? n h`, cleared with `CSI ? n l`.
151+
152+| n | Name | Effect |
153+| --- | --- | --- |
154+| 1 | DECCKM | Application cursor keys: arrows send `ESC O x` |
155+| 7 | DECAWM | Auto-wrap at the right margin |
156+| 25 | DECTCEM | Show the cursor |
157+| 47, 1047 | | Alternate screen |
158+| 1048 | | Save / restore the cursor |
159+| 1049 | | Save the cursor, then the alternate screen |
160+
161+Any other mode is parsed and ignored.
162+
163+### SGR
164+
165+| Code | Effect |
166+| --- | --- |
167+| 0 | Reset |
168+| 1, 22 | Bold on / off |
169+| 2, 22 | Dim on / off |
170+| 3, 23 | Italic on / off |
171+| 4, 24 | Underline on / off |
172+| 5, 6, 25 | Blink on / off |
173+| 7, 27 | Reverse on / off |
174+| 9, 29 | Strike-through on / off |
175+| 30–37, 40–47 | The eight normal colours, foreground / background |
176+| 90–97, 100–107 | The eight bright colours, foreground / background |
177+| 38;5;n, 48;5;n | Palette colour n of 256 |
178+| 38;2;r;g;b, 48;2;r;g;b | 24-bit colour |
179+| 39, 49 | Back to the theme's colour |
180+
181+The sixteen named colours are tcell's, which means the palette the user's own terminal is configured with, not fixed hex values. An extended colour that runs out of parameters partway leaves the style unchanged. Any other code is ignored.
182+
183+### OSC
184+
185+| Sequence | Effect |
186+| --- | --- |
187+| `OSC 0 ; text BEL`, `OSC 2 ; text BEL` | Set the window title |
188+| `OSC … ST` | The `ESC \` terminator is accepted in place of BEL |
189+
190+The title is capped at 4096 bytes. Other OSC commands are parsed and ignored.
191+
192+### Consumed and ignored
193+
194+Parsed correctly, so they never appear as stray characters, but with no effect:
195+
196+| Sequence | Name |
197+| --- | --- |
198+| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — read to their string terminator |
199+| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <space>` | Character-set and line-size selectors — the emulator works in UTF-8 regardless |
200+| `CSI ? n h`, `CSI ? n l` for any other n | Private modes not listed above |
201+| Any other CSI final byte, SGR code, or OSC command | |
202+
203+### Not implemented
204+
205+Mouse reporting, bracketed paste, shift-in / shift-out, double-width lines, sixel and other graphics protocols, and the DEC status and device-attribute reports. A program that asks for one of these gets no reply, so a program that waits for one waits forever.
206+
207+## Colours
208+
209+| Theme key | What it colours |
210+| --- | --- |
211+| `terminal.text` | Every cell whose colour the program did not choose |
212+| `terminal.cursor` | The cell under the cursor, when the window has the focus |
213+
214+See [Theme file format](themes.md).
215+
216+## Errors
217+
218+| Message | Cause |
219+| --- | --- |
220+| Terminal windows are not supported on this platform yet | The build has no pseudo-terminal support: any platform other than Linux, macOS and Windows |
221+| `openpt: …`, `grantpt: …`, `ptsname: …` | The operating system refused to open a pseudo-terminal |
222+| `fork/exec …: no such file or directory` | `$SHELL` names a program that does not exist |
223+
224+## See also
225+
226+- [How to run shell commands without leaving the editor](../how-to/use-a-terminal.md)
227+- [Terminal windows](../explanation/terminal-windows.md)
228+- [Keyboard](keyboard.md)
added docs/en/reference/themes.md +236 -0
new file mode 100644
@@ -0,0 +1,236 @@
1+# Reference: theme file format
2+
3+> Neutral, exhaustive description of a Turbo Go theme file.
4+
5+A theme is a TOML file. Themes are read from the user theme directory first, then from the ones embedded in the binary; a user file wins over an embedded theme of the same name.
6+
7+## Locations
8+
9+| Location | Notes |
10+| --- | --- |
11+| `$TURBO_GO_THEME_DIR` | Used when the variable is set and non-empty. |
12+| `~/.config/turbo-go/themes` | Linux (`os.UserConfigDir`). |
13+| `~/Library/Application Support/turbo-go/themes` | macOS. |
14+| embedded | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. |
15+
16+A theme's **name** for `-theme` and for `Options ▸ Theme…` is its file name without `.toml`. It may not contain `/`, `\` or `..`.
17+
18+## The themes that ship
19+
20+| Name | Ground | For |
21+| --- | --- | --- |
22+| `turbo-classic` | Borland navy | The default: the palette Turbo C had |
23+| `turbo-dark` | Neutral dark grey | Modern terminals with true colour |
24+| `borland-light` | Paper white | Bright rooms and projectors |
25+| `cappuccino` | Espresso brown | The Turbo layout with the temperature up: milk in the text, caramel where Turbo Dark puts blue |
26+| `catppuccin-frappe` | Warm slate | The Catppuccin Frappé palette, unchanged: pastel accents on a soft dark ground |
27+| `catppuccin-latte` | Warm paper | The Catppuccin Latte palette, unchanged: the same mapping with the saturation a light ground needs |
28+| `cobalt` | Deep navy | The Cobalt palette, accents kept as loud as they are known for |
29+| `darcula` | Charcoal | After JetBrains' Darcula: orange keywords, green strings, and the orange punctuation that makes it recognisable |
30+| `intellij-light` | White | After JetBrains' IntelliJ Light: blue bold keywords, green bold strings |
31+| `monochrome-dark` | Black and greys | No hue at all — code told apart by lightness, bold, italic and underline |
32+| `monochrome-light` | Paper and greys | The same, the other way up: on paper the darkest grey is the loudest |
33+
34+Every one of them **states its whole palette** rather than inheriting most of it. A theme you write yourself may inherit; see [writing one](../how-to/write-a-theme.md).
35+
36+## A name a theme used to answer to
37+
38+`monochrome` still loads. It is what this theme shipped as before `monochrome-light` joined it and the pair was renamed, and a settings file or a `-theme` flag saying `monochrome` gets `monochrome-dark`.
39+
40+| Retired name | Loads |
41+| --- | --- |
42+| `monochrome` | `monochrome-dark` |
43+
44+A retired name is **not** listed by `-theme` or by **Options ▸ Theme…**, so each theme appears once, under the name it has now. A theme of your own called `monochrome.toml` still wins over it, exactly as it would for any other name.
45+
46+## Top-level fields
47+
48+| Field | Type | Default | Description |
49+| --- | --- | --- | --- |
50+| `name` | string | the file's base name | Display name, shown in the theme picker and the About box. |
51+| `description` | string | `""` | One line, shown by `-list-themes`. |
52+| `inherits` | string | none | Name of a theme to start from. Its resolved styles are the base; this file overrides what it names. Chains are capped at 16 hops. |
53+| `colors` | table | `{}` | The styles. Keys are the style keys below. |
54+
55+## Entry fields
56+
57+Each value under `[colors]` is an inline table:
58+
59+| Field | Type | Default | Description |
60+| --- | --- | --- | --- |
61+| `fg` | string | inherited | Foreground colour. |
62+| `bg` | string | inherited | Background colour. |
63+| `bold` | bool | `false` | Switch bold on. |
64+| `underline` | bool | `false` | Switch underline on. |
65+| `italic` | bool | `false` | Switch italic on. |
66+| `reverse` | bool | `false` | Swap foreground and background. |
67+| `dim` | bool | `false` | Switch dim on. |
68+| `blink` | bool | `false` | Switch blink on. |
69+
70+Attributes are only ever switched **on**; there is no way to switch an inherited attribute off other than by not inheriting it.
71+
72+## Colour values
73+
74+| Form | Example | Notes |
75+| --- | --- | --- |
76+| ANSI name | `navy`, `aqua`, `silver`, `fuchsia` | The sixteen names, plus the full W3C list. |
77+| Hex literal | `#5fafd7` | 24-bit; tcell approximates it on terminals without true colour. |
78+| `default` | `default` | Whatever the terminal itself uses. |
79+| `-` | `-` | Same as `default`. |
80+| `""` | `""` | Same as `default`. |
81+
82+The sixteen ANSI names: `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`.
83+
84+An unrecognised colour is a **load error**, not a silent fallback.
85+
86+## Style keys
87+
88+Undefined keys fall back along the dots, and finally to `default`.
89+
90+### Base
91+
92+| Key | What it colours |
93+| --- | --- |
94+| `default` | The last resort of every lookup |
95+| `desktop` | The patterned backdrop behind the windows |
96+| `shadow` | The cells a window darkens behind itself |
97+
98+### Menu bar
99+
100+| Key | What it colours |
101+| --- | --- |
102+| `menu.bar` | The row of titles |
103+| `menu.item` | A drop-down entry |
104+| `menu.selected` | The highlighted entry |
105+| `menu.shortcut` | The hot letter of a label |
106+| `menu.disabled` | An entry that cannot be chosen |
107+
108+### Windows
109+
110+| Key | What it colours |
111+| --- | --- |
112+| `window.frame.active` | The frame of the focused window |
113+| `window.frame.inactive` | Every other frame |
114+| `window.title.active` | The focused window's title |
115+| `window.title.inactive` | Every other title |
116+| `window.body` | The interior, before its content draws |
117+
118+### Bars
119+
120+| Key | What it colours |
121+| --- | --- |
122+| `statusbar` | The bar itself |
123+| `statusbar.key` | The `Fn` part of a hint |
124+| `statusbar.hint` | The right-aligned text |
125+| `scrollbar` | A scroll bar's track |
126+| `scrollbar.thumb` | Its thumb and arrows |
127+
128+### Dialogs and controls
129+
130+| Key | What it colours |
131+| --- | --- |
132+| `dialog.frame` | A dialog's frame |
133+| `dialog.body` | Its interior |
134+| `dialog.title` | Its title |
135+| `dialog.label` | A line of static text |
136+| `button` | A button |
137+| `button.focused` | The focused button |
138+| `button.shortcut` | The hot letter of a button |
139+| `input` | An input field |
140+| `input.focused` | The focused input field |
141+| `input.selection` | Selected text in an input field |
142+| `list` | A list box |
143+| `list.selected` | Its highlighted line, when focused |
144+| `list.unfocused` | Its highlighted line, when not |
145+| `checkbox` | A check box |
146+| `checkbox.focused` | The focused check box |
147+
148+### Editor
149+
150+| Key | What it colours |
151+| --- | --- |
152+| `editor.text` | Text no other rule claims |
153+| `editor.selection` | Selected text |
154+| `editor.linenumber` | The line-number gutter |
155+| `editor.currentline` | The line the cursor is on |
156+| `editor.cursor` | The cursor. Its **background** is also sent to the terminal as its cursor colour, and its foreground paints the character underneath. |
157+
158+### Terminal
159+
160+| Key | What it colours |
161+| --- | --- |
162+| `terminal.text` | Every cell of a terminal window whose colour the program running in it did not choose |
163+| `terminal.cursor` | The cell under a terminal's cursor, when that window has the focus |
164+
165+A program that names its own colours keeps them: these two only fill in what it left unset. See [Terminal windows](terminal.md).
166+
167+### Project tree
168+
169+| Key | What it colours |
170+| --- | --- |
171+| `tree.text` | A file's name in the project tree, and the tree's background |
172+| `tree.directory` | A directory's name |
173+| `tree.selected` | The highlighted row, when the tree has the focus |
174+| `tree.unfocused` | The highlighted row, when it does not |
175+
176+These are separate from the `list.*` keys on purpose: a dialog's list is coloured against a dialog, and reusing it would highlight a tree row in the very colour a window's body already is. See [Project tree](project-tree.md).
177+
178+### Syntax
179+
180+| Key | What it colours |
181+| --- | --- |
182+| `syntax.identifier` | An ordinary name |
183+| `syntax.keyword` | `func`, `if`, `package`, … |
184+| `syntax.type` | `int`, `string`, and a name after `type` |
185+| `syntax.builtin` | `len`, `append`, `make`, … |
186+| `syntax.constant` | `true`, `false`, `nil`, `iota` |
187+| `syntax.function` | A name before `(`, or after `func` |
188+| `syntax.string` | A string literal |
189+| `syntax.char` | A rune literal |
190+| `syntax.number` | An integer, float or imaginary literal |
191+| `syntax.comment` | `//` and `/* */` |
192+| `syntax.operator` | `+`, `:=`, `<-`, … |
193+| `syntax.punctuation` | Brackets, commas, dots, semicolons |
194+| `syntax.heading` | A Markdown heading, whole line |
195+| `syntax.tag` | An HTML element name and its brackets |
196+| `syntax.attribute` | An HTML attribute's name |
197+| `syntax.emphasis` | Markdown bold and italic |
198+| `syntax.link` | A Markdown link or image |
199+
200+Which language produces which class is in [Languages coloured](languages.md).
201+
202+### Completion and diagnostics
203+
204+| Key | What it colours |
205+| --- | --- |
206+| `completion.frame` | The popup's frame |
207+| `completion.item` | A suggestion |
208+| `completion.selected` | The highlighted suggestion |
209+| `completion.detail` | The kind tag beside a suggestion |
210+| `diagnostic.error` | An error from the language server |
211+| `diagnostic.warning` | A warning |
212+| `diagnostic.info` | A note |
213+
214+## Example
215+
216+```toml
217+name = "Mine"
218+description = "Turbo Classic, with readable comments."
219+inherits = "turbo-classic"
220+
221+[colors]
222+"syntax.comment" = { fg = "#8a8a8a", italic = true }
223+"syntax.string" = { fg = "#87d7af" }
224+"editor.currentline" = { bg = "#00005f" }
225+```
226+
227+## Errors
228+
229+| Message | Cause |
230+| --- | --- |
231+| `theme: not found: "x"` | No `x.toml` in the user directory or among the embedded themes. |
232+| `theme: not found: "…" is not a plain theme name` | The name contains `/`, `\` or `..`. |
233+| `invalid TOML: …` | The file is not valid TOML. |
234+| `colors."k": fg: unknown colour "…"` | The colour name is not recognised. |
235+| `inherits: chain deeper than 16, probably a loop` | Two themes inherit from each other, directly or through others. |
236+| `inherits "x": theme: not found` | The parent named does not exist. |
new file mode 100644
@@ -0,0 +1,236 @@
1+# Reference: theme file format
2+
3+> Neutral, exhaustive description of a Turbo Go theme file.
4+
5+A theme is a TOML file. Themes are read from the user theme directory first, then from the ones embedded in the binary; a user file wins over an embedded theme of the same name.
6+
7+## Locations
8+
9+| Location | Notes |
10+| --- | --- |
11+| `$TURBO_GO_THEME_DIR` | Used when the variable is set and non-empty. |
12+| `~/.config/turbo-go/themes` | Linux (`os.UserConfigDir`). |
13+| `~/Library/Application Support/turbo-go/themes` | macOS. |
14+| embedded | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. |
15+
16+A theme's **name** for `-theme` and for `Options ▸ Theme…` is its file name without `.toml`. It may not contain `/`, `\` or `..`.
17+
18+## The themes that ship
19+
20+| Name | Ground | For |
21+| --- | --- | --- |
22+| `turbo-classic` | Borland navy | The default: the palette Turbo C had |
23+| `turbo-dark` | Neutral dark grey | Modern terminals with true colour |
24+| `borland-light` | Paper white | Bright rooms and projectors |
25+| `cappuccino` | Espresso brown | The Turbo layout with the temperature up: milk in the text, caramel where Turbo Dark puts blue |
26+| `catppuccin-frappe` | Warm slate | The Catppuccin Frappé palette, unchanged: pastel accents on a soft dark ground |
27+| `catppuccin-latte` | Warm paper | The Catppuccin Latte palette, unchanged: the same mapping with the saturation a light ground needs |
28+| `cobalt` | Deep navy | The Cobalt palette, accents kept as loud as they are known for |
29+| `darcula` | Charcoal | After JetBrains' Darcula: orange keywords, green strings, and the orange punctuation that makes it recognisable |
30+| `intellij-light` | White | After JetBrains' IntelliJ Light: blue bold keywords, green bold strings |
31+| `monochrome-dark` | Black and greys | No hue at all — code told apart by lightness, bold, italic and underline |
32+| `monochrome-light` | Paper and greys | The same, the other way up: on paper the darkest grey is the loudest |
33+
34+Every one of them **states its whole palette** rather than inheriting most of it. A theme you write yourself may inherit; see [writing one](../how-to/write-a-theme.md).
35+
36+## A name a theme used to answer to
37+
38+`monochrome` still loads. It is what this theme shipped as before `monochrome-light` joined it and the pair was renamed, and a settings file or a `-theme` flag saying `monochrome` gets `monochrome-dark`.
39+
40+| Retired name | Loads |
41+| --- | --- |
42+| `monochrome` | `monochrome-dark` |
43+
44+A retired name is **not** listed by `-theme` or by **Options ▸ Theme…**, so each theme appears once, under the name it has now. A theme of your own called `monochrome.toml` still wins over it, exactly as it would for any other name.
45+
46+## Top-level fields
47+
48+| Field | Type | Default | Description |
49+| --- | --- | --- | --- |
50+| `name` | string | the file's base name | Display name, shown in the theme picker and the About box. |
51+| `description` | string | `""` | One line, shown by `-list-themes`. |
52+| `inherits` | string | none | Name of a theme to start from. Its resolved styles are the base; this file overrides what it names. Chains are capped at 16 hops. |
53+| `colors` | table | `{}` | The styles. Keys are the style keys below. |
54+
55+## Entry fields
56+
57+Each value under `[colors]` is an inline table:
58+
59+| Field | Type | Default | Description |
60+| --- | --- | --- | --- |
61+| `fg` | string | inherited | Foreground colour. |
62+| `bg` | string | inherited | Background colour. |
63+| `bold` | bool | `false` | Switch bold on. |
64+| `underline` | bool | `false` | Switch underline on. |
65+| `italic` | bool | `false` | Switch italic on. |
66+| `reverse` | bool | `false` | Swap foreground and background. |
67+| `dim` | bool | `false` | Switch dim on. |
68+| `blink` | bool | `false` | Switch blink on. |
69+
70+Attributes are only ever switched **on**; there is no way to switch an inherited attribute off other than by not inheriting it.
71+
72+## Colour values
73+
74+| Form | Example | Notes |
75+| --- | --- | --- |
76+| ANSI name | `navy`, `aqua`, `silver`, `fuchsia` | The sixteen names, plus the full W3C list. |
77+| Hex literal | `#5fafd7` | 24-bit; tcell approximates it on terminals without true colour. |
78+| `default` | `default` | Whatever the terminal itself uses. |
79+| `-` | `-` | Same as `default`. |
80+| `""` | `""` | Same as `default`. |
81+
82+The sixteen ANSI names: `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`.
83+
84+An unrecognised colour is a **load error**, not a silent fallback.
85+
86+## Style keys
87+
88+Undefined keys fall back along the dots, and finally to `default`.
89+
90+### Base
91+
92+| Key | What it colours |
93+| --- | --- |
94+| `default` | The last resort of every lookup |
95+| `desktop` | The patterned backdrop behind the windows |
96+| `shadow` | The cells a window darkens behind itself |
97+
98+### Menu bar
99+
100+| Key | What it colours |
101+| --- | --- |
102+| `menu.bar` | The row of titles |
103+| `menu.item` | A drop-down entry |
104+| `menu.selected` | The highlighted entry |
105+| `menu.shortcut` | The hot letter of a label |
106+| `menu.disabled` | An entry that cannot be chosen |
107+
108+### Windows
109+
110+| Key | What it colours |
111+| --- | --- |
112+| `window.frame.active` | The frame of the focused window |
113+| `window.frame.inactive` | Every other frame |
114+| `window.title.active` | The focused window's title |
115+| `window.title.inactive` | Every other title |
116+| `window.body` | The interior, before its content draws |
117+
118+### Bars
119+
120+| Key | What it colours |
121+| --- | --- |
122+| `statusbar` | The bar itself |
123+| `statusbar.key` | The `Fn` part of a hint |
124+| `statusbar.hint` | The right-aligned text |
125+| `scrollbar` | A scroll bar's track |
126+| `scrollbar.thumb` | Its thumb and arrows |
127+
128+### Dialogs and controls
129+
130+| Key | What it colours |
131+| --- | --- |
132+| `dialog.frame` | A dialog's frame |
133+| `dialog.body` | Its interior |
134+| `dialog.title` | Its title |
135+| `dialog.label` | A line of static text |
136+| `button` | A button |
137+| `button.focused` | The focused button |
138+| `button.shortcut` | The hot letter of a button |
139+| `input` | An input field |
140+| `input.focused` | The focused input field |
141+| `input.selection` | Selected text in an input field |
142+| `list` | A list box |
143+| `list.selected` | Its highlighted line, when focused |
144+| `list.unfocused` | Its highlighted line, when not |
145+| `checkbox` | A check box |
146+| `checkbox.focused` | The focused check box |
147+
148+### Editor
149+
150+| Key | What it colours |
151+| --- | --- |
152+| `editor.text` | Text no other rule claims |
153+| `editor.selection` | Selected text |
154+| `editor.linenumber` | The line-number gutter |
155+| `editor.currentline` | The line the cursor is on |
156+| `editor.cursor` | The cursor. Its **background** is also sent to the terminal as its cursor colour, and its foreground paints the character underneath. |
157+
158+### Terminal
159+
160+| Key | What it colours |
161+| --- | --- |
162+| `terminal.text` | Every cell of a terminal window whose colour the program running in it did not choose |
163+| `terminal.cursor` | The cell under a terminal's cursor, when that window has the focus |
164+
165+A program that names its own colours keeps them: these two only fill in what it left unset. See [Terminal windows](terminal.md).
166+
167+### Project tree
168+
169+| Key | What it colours |
170+| --- | --- |
171+| `tree.text` | A file's name in the project tree, and the tree's background |
172+| `tree.directory` | A directory's name |
173+| `tree.selected` | The highlighted row, when the tree has the focus |
174+| `tree.unfocused` | The highlighted row, when it does not |
175+
176+These are separate from the `list.*` keys on purpose: a dialog's list is coloured against a dialog, and reusing it would highlight a tree row in the very colour a window's body already is. See [Project tree](project-tree.md).
177+
178+### Syntax
179+
180+| Key | What it colours |
181+| --- | --- |
182+| `syntax.identifier` | An ordinary name |
183+| `syntax.keyword` | `func`, `if`, `package`, … |
184+| `syntax.type` | `int`, `string`, and a name after `type` |
185+| `syntax.builtin` | `len`, `append`, `make`, … |
186+| `syntax.constant` | `true`, `false`, `nil`, `iota` |
187+| `syntax.function` | A name before `(`, or after `func` |
188+| `syntax.string` | A string literal |
189+| `syntax.char` | A rune literal |
190+| `syntax.number` | An integer, float or imaginary literal |
191+| `syntax.comment` | `//` and `/* */` |
192+| `syntax.operator` | `+`, `:=`, `<-`, … |
193+| `syntax.punctuation` | Brackets, commas, dots, semicolons |
194+| `syntax.heading` | A Markdown heading, whole line |
195+| `syntax.tag` | An HTML element name and its brackets |
196+| `syntax.attribute` | An HTML attribute's name |
197+| `syntax.emphasis` | Markdown bold and italic |
198+| `syntax.link` | A Markdown link or image |
199+
200+Which language produces which class is in [Languages coloured](languages.md).
201+
202+### Completion and diagnostics
203+
204+| Key | What it colours |
205+| --- | --- |
206+| `completion.frame` | The popup's frame |
207+| `completion.item` | A suggestion |
208+| `completion.selected` | The highlighted suggestion |
209+| `completion.detail` | The kind tag beside a suggestion |
210+| `diagnostic.error` | An error from the language server |
211+| `diagnostic.warning` | A warning |
212+| `diagnostic.info` | A note |
213+
214+## Example
215+
216+```toml
217+name = "Mine"
218+description = "Turbo Classic, with readable comments."
219+inherits = "turbo-classic"
220+
221+[colors]
222+"syntax.comment" = { fg = "#8a8a8a", italic = true }
223+"syntax.string" = { fg = "#87d7af" }
224+"editor.currentline" = { bg = "#00005f" }
225+```
226+
227+## Errors
228+
229+| Message | Cause |
230+| --- | --- |
231+| `theme: not found: "x"` | No `x.toml` in the user directory or among the embedded themes. |
232+| `theme: not found: "…" is not a plain theme name` | The name contains `/`, `\` or `..`. |
233+| `invalid TOML: …` | The file is not valid TOML. |
234+| `colors."k": fg: unknown colour "…"` | The colour name is not recognised. |
235+| `inherits: chain deeper than 16, probably a loop` | Two themes inherit from each other, directly or through others. |
236+| `inherits "x": theme: not found` | The parent named does not exist. |
added docs/en/reference/versioning.md +150 -0
new file mode 100644
@@ -0,0 +1,150 @@
1+# Reference: the version number
2+
3+> Neutral description of where the version Turbo Go reports comes from, and what each way of building it produces.
4+
5+## Where the number comes from
6+
7+Three sources, consulted in this order. The first that answers wins.
8+
9+| Order | Source | Set by |
10+| --- | --- | --- |
11+| 1 | Linker stamps | `make build`, `make install`, `scripts/install.sh` |
12+| 2 | Go build information | The Go tool, automatically |
13+| 3 | `unknown` | Nothing — the value reported when no source could name the build |
14+
15+There is **no version constant in the source**. A number written into a `.go` file has to be edited as part of releasing, and is wrong the moment someone forgets.
16+
17+## Linker stamps
18+
19+Three package-level variables in turbo-core's `version` package, set with `-ldflags -X`.
20+
21+| Variable | Filled from | Example |
22+| --- | --- | --- |
23+| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` |
24+| `commit` | `git rev-parse --short HEAD` | `88a4c38` |
25+| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-08-31T18:04:05Z` |
26+
27+```sh
28+go build -ldflags "\
29+ -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' \
30+ -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \
31+ -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-08-31T18:04:05Z'" .
32+```
33+
34+A leading `v` is dropped for display: the tag is `v0.2.0`, the About box says `0.2.0`.
35+
36+## Go build information
37+
38+Read from `runtime/debug.ReadBuildInfo()` when nothing was stamped.
39+
40+| Field read | Used for |
41+| --- | --- |
42+| `Main.Version` | The number, unless it is empty, `(devel)`, or a pseudo-version |
43+| `vcs.revision` | The commit, abbreviated to seven characters |
44+| `vcs.modified` | Whether `-dirty` is appended |
45+
46+`vcs.time` is **not** used. It records when the commit was made, not when the binary was linked, so reporting it as a build date would be wrong on every binary built later than its own commit.
47+
48+A **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — is the Go tool naming a commit that no tag names. It is reported as `devel`, not shown as written: its `0.1.1` is a patch release that does not exist.
49+
50+## What each build reports
51+
52+| Built by | Number | Commit | Built |
53+| --- | --- | --- | --- |
54+| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | yes | yes |
55+| The same, on a tagged commit | `0.2.0` | yes | yes |
56+| The same, with uncommitted changes | `0.1.0-14-g88a4c38-dirty` | yes | yes |
57+| `go install rickub.com/turbo-editors/turbo-go@v0.2.0` | `0.2.0` | no | no |
58+| `go build .` in a checkout | `devel` | yes | no |
59+| `go build .` in a checkout with uncommitted changes | `devel-dirty` | yes | no |
60+| `go run .` | `unknown` | no | no |
61+| A checkout with no git, and no stamps | `unknown` | no | no |
62+
63+Only the stamped rows can report a tag: the Go build system does not read git tags.
64+
65+## Checked at build time
66+
67+A linker stamp is a string, and a wrong one is not an error. `-X` naming a symbol that does not exist links happily and stamps nothing; the binary then falls back to Go build information and reports a version the build never meant — often `devel`, on a binary attached to a release. Nothing but running the binary catches it, so every build that produces one runs it.
68+
69+`scripts/check-version.sh` is what runs.
70+
71+| Called by | On | A failure fails |
72+| --- | --- | --- |
73+| `make build` | `bin/turbo-go`, with `$(VERSION)` and `$(COMMIT)` | the build |
74+| `scripts/install.sh` | the staged binary, **before** it is installed | the install, leaving the binary already there untouched |
75+| `02-build-releases.sh` | the one staged asset this machine can run, with the tag | the release build |
76+
77+```sh
78+scripts/check-version.sh bin/turbo-go v0.2.0 88a4c38 # a stamped build
79+scripts/check-version.sh bin/turbo-go # nothing to expect
80+```
81+
82+| Arguments | Passes when |
83+| --- | --- |
84+| binary, version, commit | the reported number **equals** the version with its leading `v` dropped, and the commit appears in the output |
85+| binary, version | the number equals it |
86+| binary | the number is anything but `unknown` |
87+
88+The version comparison is an equality, not a search. `0.2.0` is a substring of `10.2.0`, and of a commit hash that happens to contain it; a stamp that is nearly right is exactly what this exists to catch.
89+
90+| Exit | Meaning |
91+| --- | --- |
92+| `0` | The binary reports what the build meant. The line it printed is echoed. |
93+| `1` | It does not run, is not there, or reports something else. |
94+| `2` | No binary was named. |
95+
96+## Where it is shown
97+
98+### `-version`
99+
100+One line, carrying every part that is known.
101+
102+```
103+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
104+Turbo Go 0.2.0 (88a4c38)
105+Turbo Go 0.2.0
106+```
107+
108+### Help ▸ About
109+
110+One line per known fact. A fact the build did not record has **no line**, rather than an empty one.
111+
112+```
113+Turbo Go 0.2.0
114+
115+A Turbo C-style editor for Go,
116+written in Go.
117+
118+Commit: 88a4c38
119+Built: 2026-08-31 18:04 UTC
120+Theme: Turbo Classic
121+```
122+
123+`Built` is rendered in UTC as `YYYY-MM-DD HH:MM UTC`. A stamp that is not valid RFC 3339 is shown exactly as it was given, rather than dropped.
124+
125+### `make version`
126+
127+Prints what this checkout would stamp, without building.
128+
129+```
130+$ make version
131+v0.1.0-14-g88a4c38 (88a4c38)
132+```
133+
134+### `make ldflags`
135+
136+Prints the linker flags a stamped build uses, so a script can reuse them instead of repeating the `-X` paths.
137+
138+```
139+$ make ldflags
140+-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' -X '….commit=7f8b36a' -X '….built=2026-08-31T19:02:03Z'
141+```
142+
143+`02-build-releases.sh` reads it for its cross-compiles, overriding the version with the tag it is releasing — `make ldflags VERSION=v0.2.0` — so the binaries say what the release says rather than what `git describe` says. A binary cross-compiled without it reports `devel`, whatever the release it is attached to says.
144+
145+## See also
146+
147+- Cutting a release so the number is right: [How to make a release](../how-to/make-a-release.md)
148+- What `-version` is **not** for: it is written for a person. A script that needs the number should compare with `grep -F`, or ask git, rather than reading a field out of it.
149+- Why there is no version constant: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source)
150+- The `-version` flag among the others: [Command line](cli.md)
new file mode 100644
@@ -0,0 +1,150 @@
1+# Reference: the version number
2+
3+> Neutral description of where the version Turbo Go reports comes from, and what each way of building it produces.
4+
5+## Where the number comes from
6+
7+Three sources, consulted in this order. The first that answers wins.
8+
9+| Order | Source | Set by |
10+| --- | --- | --- |
11+| 1 | Linker stamps | `make build`, `make install`, `scripts/install.sh` |
12+| 2 | Go build information | The Go tool, automatically |
13+| 3 | `unknown` | Nothing — the value reported when no source could name the build |
14+
15+There is **no version constant in the source**. A number written into a `.go` file has to be edited as part of releasing, and is wrong the moment someone forgets.
16+
17+## Linker stamps
18+
19+Three package-level variables in turbo-core's `version` package, set with `-ldflags -X`.
20+
21+| Variable | Filled from | Example |
22+| --- | --- | --- |
23+| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` |
24+| `commit` | `git rev-parse --short HEAD` | `88a4c38` |
25+| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-08-31T18:04:05Z` |
26+
27+```sh
28+go build -ldflags "\
29+ -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' \
30+ -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \
31+ -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-08-31T18:04:05Z'" .
32+```
33+
34+A leading `v` is dropped for display: the tag is `v0.2.0`, the About box says `0.2.0`.
35+
36+## Go build information
37+
38+Read from `runtime/debug.ReadBuildInfo()` when nothing was stamped.
39+
40+| Field read | Used for |
41+| --- | --- |
42+| `Main.Version` | The number, unless it is empty, `(devel)`, or a pseudo-version |
43+| `vcs.revision` | The commit, abbreviated to seven characters |
44+| `vcs.modified` | Whether `-dirty` is appended |
45+
46+`vcs.time` is **not** used. It records when the commit was made, not when the binary was linked, so reporting it as a build date would be wrong on every binary built later than its own commit.
47+
48+A **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — is the Go tool naming a commit that no tag names. It is reported as `devel`, not shown as written: its `0.1.1` is a patch release that does not exist.
49+
50+## What each build reports
51+
52+| Built by | Number | Commit | Built |
53+| --- | --- | --- | --- |
54+| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | yes | yes |
55+| The same, on a tagged commit | `0.2.0` | yes | yes |
56+| The same, with uncommitted changes | `0.1.0-14-g88a4c38-dirty` | yes | yes |
57+| `go install rickub.com/turbo-editors/turbo-go@v0.2.0` | `0.2.0` | no | no |
58+| `go build .` in a checkout | `devel` | yes | no |
59+| `go build .` in a checkout with uncommitted changes | `devel-dirty` | yes | no |
60+| `go run .` | `unknown` | no | no |
61+| A checkout with no git, and no stamps | `unknown` | no | no |
62+
63+Only the stamped rows can report a tag: the Go build system does not read git tags.
64+
65+## Checked at build time
66+
67+A linker stamp is a string, and a wrong one is not an error. `-X` naming a symbol that does not exist links happily and stamps nothing; the binary then falls back to Go build information and reports a version the build never meant — often `devel`, on a binary attached to a release. Nothing but running the binary catches it, so every build that produces one runs it.
68+
69+`scripts/check-version.sh` is what runs.
70+
71+| Called by | On | A failure fails |
72+| --- | --- | --- |
73+| `make build` | `bin/turbo-go`, with `$(VERSION)` and `$(COMMIT)` | the build |
74+| `scripts/install.sh` | the staged binary, **before** it is installed | the install, leaving the binary already there untouched |
75+| `02-build-releases.sh` | the one staged asset this machine can run, with the tag | the release build |
76+
77+```sh
78+scripts/check-version.sh bin/turbo-go v0.2.0 88a4c38 # a stamped build
79+scripts/check-version.sh bin/turbo-go # nothing to expect
80+```
81+
82+| Arguments | Passes when |
83+| --- | --- |
84+| binary, version, commit | the reported number **equals** the version with its leading `v` dropped, and the commit appears in the output |
85+| binary, version | the number equals it |
86+| binary | the number is anything but `unknown` |
87+
88+The version comparison is an equality, not a search. `0.2.0` is a substring of `10.2.0`, and of a commit hash that happens to contain it; a stamp that is nearly right is exactly what this exists to catch.
89+
90+| Exit | Meaning |
91+| --- | --- |
92+| `0` | The binary reports what the build meant. The line it printed is echoed. |
93+| `1` | It does not run, is not there, or reports something else. |
94+| `2` | No binary was named. |
95+
96+## Where it is shown
97+
98+### `-version`
99+
100+One line, carrying every part that is known.
101+
102+```
103+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
104+Turbo Go 0.2.0 (88a4c38)
105+Turbo Go 0.2.0
106+```
107+
108+### Help ▸ About
109+
110+One line per known fact. A fact the build did not record has **no line**, rather than an empty one.
111+
112+```
113+Turbo Go 0.2.0
114+
115+A Turbo C-style editor for Go,
116+written in Go.
117+
118+Commit: 88a4c38
119+Built: 2026-08-31 18:04 UTC
120+Theme: Turbo Classic
121+```
122+
123+`Built` is rendered in UTC as `YYYY-MM-DD HH:MM UTC`. A stamp that is not valid RFC 3339 is shown exactly as it was given, rather than dropped.
124+
125+### `make version`
126+
127+Prints what this checkout would stamp, without building.
128+
129+```
130+$ make version
131+v0.1.0-14-g88a4c38 (88a4c38)
132+```
133+
134+### `make ldflags`
135+
136+Prints the linker flags a stamped build uses, so a script can reuse them instead of repeating the `-X` paths.
137+
138+```
139+$ make ldflags
140+-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' -X '….commit=7f8b36a' -X '….built=2026-08-31T19:02:03Z'
141+```
142+
143+`02-build-releases.sh` reads it for its cross-compiles, overriding the version with the tag it is releasing — `make ldflags VERSION=v0.2.0` — so the binaries say what the release says rather than what `git describe` says. A binary cross-compiled without it reports `devel`, whatever the release it is attached to says.
144+
145+## See also
146+
147+- Cutting a release so the number is right: [How to make a release](../how-to/make-a-release.md)
148+- What `-version` is **not** for: it is written for a person. A script that needs the number should compare with `grep -F`, or ask git, rather than reading a field out of it.
149+- Why there is no version constant: [Design decisions](../explanation/design-decisions.md#the-version-is-a-property-of-the-build-not-of-the-source)
150+- The `-version` flag among the others: [Command line](cli.md)
added docs/en/tutorials/getting-started.md +202 -0
new file mode 100644
@@ -0,0 +1,202 @@
1+# Tutorial: your first file in Turbo Go
2+
3+By the end of this tutorial, you will have built the editor, written a small Go program inside it, watched the keywords turn colour as you typed, saved the file, and run it. It takes about ten minutes.
4+
5+No prior knowledge of Turbo Go is needed. You need Go 1.26 or later and a terminal — that is all.
6+
7+## Prerequisites
8+
9+Check that Go is there:
10+
11+```bash
12+go version
13+```
14+
15+You should see something like:
16+
17+```
18+go version go1.26.5 linux/arm64
19+```
20+
21+If that command fails, install Go first: https://go.dev/dl/
22+
23+## Step 1 — Build the editor
24+
25+From the project directory, type:
26+
27+```bash
28+make build
29+```
30+
31+You should see:
32+
33+```
34+go build -o bin/turbo-go .
35+```
36+
37+and then nothing more. Silence is success: Go says nothing when a build works.
38+
39+We now have an executable at `bin/turbo-go`. Remember where it is, so we can start it from anywhere:
40+
41+```bash
42+export TURBO="$PWD/bin/turbo-go"
43+```
44+
45+## Step 2 — Create a place to work
46+
47+Turbo Go is at its best inside a Go module, so let us make one:
48+
49+```bash
50+mkdir -p /tmp/hello && cd /tmp/hello
51+go mod init hello
52+```
53+
54+You should see:
55+
56+```
57+go: creating new go.mod: module hello
58+```
59+
60+We have just created an empty Go module.
61+
62+## Step 3 — Open the editor
63+
64+Start Turbo Go on a file that does not exist yet:
65+
66+```bash
67+$TURBO main.go
68+```
69+
70+The screen fills with a blue desktop. You should see:
71+
72+- a **menu bar** across the top: `File Edit Search Run Code Options Window Snippets Go Help`
73+- a **window** framed in a double line, titled `main.go`
74+- a **status bar** along the bottom: `F1 Describe F2 Save F3 Open …`
75+
76+The cursor is blinking at line 1, column 1 — the status bar says `1:1` on the right.
77+
78+We are inside the editor.
79+
80+## Step 4 — Type a Go program
81+
82+Type these five lines, pressing Enter at the end of each:
83+
84+```go
85+package main
86+
87+import "fmt"
88+
89+func main() {
90+```
91+
92+Watch the colours as you type. `package`, `import` and `func` turn **white and bold** the moment the word ends: they are keywords. `"fmt"` turns **green**: it is a string. `main` turns **yellow and bold** as soon as you type the `(` after it, because that makes it a function.
93+
94+Now press **Tab**. The cursor jumps to column 9 — the status bar on the right changes to `5:2`, because a tab is one character wide in the file even though it fills eight columns on screen.
95+
96+Type the body of the function:
97+
98+```go
99+fmt.Println("Hello from Turbo Go!")
100+```
101+
102+> When you type the `.` after `fmt`, the status bar briefly shows a message about the language server. That is expected: completion needs `gopls`, which we have not set up. The [completion guide](../how-to/enable-completion.md) covers it later; ignore it for now.
103+
104+Press **Enter**. Look at the new line: the cursor is *already* at column 9. Turbo Go copied the indentation of the line above, which is what you want nine times out of ten.
105+
106+This time we do not want it, so press **Shift-Tab** to take that indent back off, then type the closing brace:
107+
108+```go
109+}
110+```
111+
112+The window title now reads `main.go *`. The star means there are unsaved changes.
113+
114+We have just written a complete Go program, with the editor colouring it as we went.
115+
116+## Step 5 — Save it
117+
118+Press **F2**.
119+
120+The star disappears from the title, and the status bar says:
121+
122+```
123+Saved main.go
124+```
125+
126+We have just written the file to disk.
127+
128+## Step 6 — Look at the file from outside
129+
130+Leave the editor by pressing **Alt-X**. The terminal comes back as it was.
131+
132+Check what we wrote:
133+
134+```bash
135+cat main.go
136+```
137+
138+You should see:
139+
140+```go
141+package main
142+
143+import "fmt"
144+
145+func main() {
146+ fmt.Println("Hello from Turbo Go!")
147+}
148+```
149+
150+## Step 7 — Run it
151+
152+```bash
153+go run main.go
154+```
155+
156+You should see:
157+
158+```
159+Hello from Turbo Go!
160+```
161+
162+That is a working Go program, written entirely inside the editor.
163+
164+## Step 8 — Change the theme
165+
166+Open the file again:
167+
168+```bash
169+$TURBO main.go
170+```
171+
172+Press **F10**. The `File` menu drops open. Press **→** five times: the menu walks along the bar to `Options`, whose first item, `Theme…`, is highlighted. Press **Enter**.
173+
174+A list of eleven appears, in alphabetical order, with the theme you are using already highlighted:
175+
176+```
177+borland-light
178+cappuccino
179+catppuccin-frappe
180+catppuccin-latte
181+cobalt
182+darcula
183+intellij-light
184+monochrome-dark
185+monochrome-light
186+turbo-classic
187+turbo-dark
188+```
189+
190+`turbo-classic` is the highlighted row, because that is the theme you are in. Press **↓** once to move to `turbo-dark`, then press **Enter**.
191+
192+The whole editor repaints in dark grey, and the status bar says `Theme: Turbo Dark`.
193+
194+Press **Alt-X** to leave.
195+
196+## What now?
197+
198+You have built the editor, written a Go program in it, saved it, run it, and changed how it looks.
199+
200+- To do specific things — enable completion, write a theme of your own, search a file → see the [how-to guides](../how-to/)
201+- To look up a key or a menu item → see the [reference](../reference/)
202+- To understand how the colouring and the completion actually work → see the [explanation](../explanation/)
new file mode 100644
@@ -0,0 +1,202 @@
1+# Tutorial: your first file in Turbo Go
2+
3+By the end of this tutorial, you will have built the editor, written a small Go program inside it, watched the keywords turn colour as you typed, saved the file, and run it. It takes about ten minutes.
4+
5+No prior knowledge of Turbo Go is needed. You need Go 1.26 or later and a terminal — that is all.
6+
7+## Prerequisites
8+
9+Check that Go is there:
10+
11+```bash
12+go version
13+```
14+
15+You should see something like:
16+
17+```
18+go version go1.26.5 linux/arm64
19+```
20+
21+If that command fails, install Go first: https://go.dev/dl/
22+
23+## Step 1 — Build the editor
24+
25+From the project directory, type:
26+
27+```bash
28+make build
29+```
30+
31+You should see:
32+
33+```
34+go build -o bin/turbo-go .
35+```
36+
37+and then nothing more. Silence is success: Go says nothing when a build works.
38+
39+We now have an executable at `bin/turbo-go`. Remember where it is, so we can start it from anywhere:
40+
41+```bash
42+export TURBO="$PWD/bin/turbo-go"
43+```
44+
45+## Step 2 — Create a place to work
46+
47+Turbo Go is at its best inside a Go module, so let us make one:
48+
49+```bash
50+mkdir -p /tmp/hello && cd /tmp/hello
51+go mod init hello
52+```
53+
54+You should see:
55+
56+```
57+go: creating new go.mod: module hello
58+```
59+
60+We have just created an empty Go module.
61+
62+## Step 3 — Open the editor
63+
64+Start Turbo Go on a file that does not exist yet:
65+
66+```bash
67+$TURBO main.go
68+```
69+
70+The screen fills with a blue desktop. You should see:
71+
72+- a **menu bar** across the top: `File Edit Search Run Code Options Window Snippets Go Help`
73+- a **window** framed in a double line, titled `main.go`
74+- a **status bar** along the bottom: `F1 Describe F2 Save F3 Open …`
75+
76+The cursor is blinking at line 1, column 1 — the status bar says `1:1` on the right.
77+
78+We are inside the editor.
79+
80+## Step 4 — Type a Go program
81+
82+Type these five lines, pressing Enter at the end of each:
83+
84+```go
85+package main
86+
87+import "fmt"
88+
89+func main() {
90+```
91+
92+Watch the colours as you type. `package`, `import` and `func` turn **white and bold** the moment the word ends: they are keywords. `"fmt"` turns **green**: it is a string. `main` turns **yellow and bold** as soon as you type the `(` after it, because that makes it a function.
93+
94+Now press **Tab**. The cursor jumps to column 9 — the status bar on the right changes to `5:2`, because a tab is one character wide in the file even though it fills eight columns on screen.
95+
96+Type the body of the function:
97+
98+```go
99+fmt.Println("Hello from Turbo Go!")
100+```
101+
102+> When you type the `.` after `fmt`, the status bar briefly shows a message about the language server. That is expected: completion needs `gopls`, which we have not set up. The [completion guide](../how-to/enable-completion.md) covers it later; ignore it for now.
103+
104+Press **Enter**. Look at the new line: the cursor is *already* at column 9. Turbo Go copied the indentation of the line above, which is what you want nine times out of ten.
105+
106+This time we do not want it, so press **Shift-Tab** to take that indent back off, then type the closing brace:
107+
108+```go
109+}
110+```
111+
112+The window title now reads `main.go *`. The star means there are unsaved changes.
113+
114+We have just written a complete Go program, with the editor colouring it as we went.
115+
116+## Step 5 — Save it
117+
118+Press **F2**.
119+
120+The star disappears from the title, and the status bar says:
121+
122+```
123+Saved main.go
124+```
125+
126+We have just written the file to disk.
127+
128+## Step 6 — Look at the file from outside
129+
130+Leave the editor by pressing **Alt-X**. The terminal comes back as it was.
131+
132+Check what we wrote:
133+
134+```bash
135+cat main.go
136+```
137+
138+You should see:
139+
140+```go
141+package main
142+
143+import "fmt"
144+
145+func main() {
146+ fmt.Println("Hello from Turbo Go!")
147+}
148+```
149+
150+## Step 7 — Run it
151+
152+```bash
153+go run main.go
154+```
155+
156+You should see:
157+
158+```
159+Hello from Turbo Go!
160+```
161+
162+That is a working Go program, written entirely inside the editor.
163+
164+## Step 8 — Change the theme
165+
166+Open the file again:
167+
168+```bash
169+$TURBO main.go
170+```
171+
172+Press **F10**. The `File` menu drops open. Press **→** five times: the menu walks along the bar to `Options`, whose first item, `Theme…`, is highlighted. Press **Enter**.
173+
174+A list of eleven appears, in alphabetical order, with the theme you are using already highlighted:
175+
176+```
177+borland-light
178+cappuccino
179+catppuccin-frappe
180+catppuccin-latte
181+cobalt
182+darcula
183+intellij-light
184+monochrome-dark
185+monochrome-light
186+turbo-classic
187+turbo-dark
188+```
189+
190+`turbo-classic` is the highlighted row, because that is the theme you are in. Press **↓** once to move to `turbo-dark`, then press **Enter**.
191+
192+The whole editor repaints in dark grey, and the status bar says `Theme: Turbo Dark`.
193+
194+Press **Alt-X** to leave.
195+
196+## What now?
197+
198+You have built the editor, written a Go program in it, saved it, run it, and changed how it looks.
199+
200+- To do specific things — enable completion, write a theme of your own, search a file → see the [how-to guides](../how-to/)
201+- To look up a key or a menu item → see the [reference](../reference/)
202+- To understand how the colouring and the completion actually work → see the [explanation](../explanation/)
added docs/fr/README.md +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+# Turbo Go — documentation
2+
3+Turbo Go est un éditeur pour Go dans le style de Turbo C : un IDE plein écran en terminal, avec menus, fenêtres déplaçables, coloration syntaxique pour neuf langages, thèmes, complétion via `gopls`, fenêtres shell, réglages par projet, arbre du projet, snippets, et la chaîne d'outils Go à un menu de distance.
4+
5+Cette documentation suit la méthode [Diátaxis](https://diataxis.fr). Quatre types de page, quatre besoins différents — allez directement à celui qui correspond à ce que vous cherchez.
6+
7+| Je veux… | Aller à |
8+| --- | --- |
9+| **apprendre** l'éditeur en l'utilisant | [Tutoriels](tutorials/) |
10+| **faire** quelque chose de précis | [Guides pratiques](how-to/) |
11+| **consulter** un détail exact | [Référence](reference/) |
12+| **comprendre** comment et pourquoi ça marche | [Explications](explanation/) |
13+
14+## Tutoriels — apprendre en faisant
15+
16+- [Votre premier fichier dans Turbo Go](tutorials/getting-started.md) — compiler, ouvrir l'éditeur, écrire un programme Go, le colorer, l'enregistrer et l'exécuter.
17+
18+## Guides pratiques — des recettes pour une tâche
19+
20+- [Installer et compiler Turbo Go](how-to/install.md)
21+- [Lancer les tests](how-to/run-the-tests.md)
22+- [Activer la complétion Go](how-to/enable-completion.md)
23+- [Écrire son propre thème](how-to/write-a-theme.md)
24+- [Se déplacer dans un fichier](how-to/navigate-code.md)
25+- [Interroger le code](how-to/ask-about-code.md)
26+- [Lancer des commandes shell sans quitter l'éditeur](how-to/use-a-terminal.md)
27+- [Donner ses propres réglages à un projet](how-to/configure-a-project.md)
28+- [Parcourir un projet et ouvrir des fichiers depuis un arbre](how-to/browse-a-project.md)
29+- [Insérer des snippets depuis un menu](how-to/use-snippets.md)
30+- [Lancer les commandes go depuis l'éditeur](how-to/run-go-commands.md)
31+- [Dialoguer avec un agent de code depuis l'éditeur](how-to/talk-to-an-agent.md)
32+- [Faire une release](how-to/make-a-release.md)
33+
34+## Référence — les détails exacts
35+
36+- [Ligne de commande](reference/cli.md)
37+- [Clavier](reference/keyboard.md)
38+- [Menus](reference/menus.md)
39+- [Format des fichiers de thème](reference/themes.md)
40+- [Fenêtres terminal](reference/terminal.md)
41+- [Réglages de projet](reference/project-settings.md)
42+- [Arbre du projet](reference/project-tree.md)
43+- [Langages colorés](reference/languages.md)
44+- [Snippets](reference/snippets.md)
45+- [Outils go](reference/go-tools.md)
46+- [Agents et ACP](reference/acp.md)
47+- [Le numéro de version](reference/versioning.md)
48+
49+## Explications — comprendre
50+
51+- [Architecture](explanation/architecture.md)
52+- [Décisions de conception](explanation/design-decisions.md)
53+- [Coloration et complétion](explanation/colouring-and-completion.md)
54+- [Fenêtres terminal](explanation/terminal-windows.md)
55+- [Réglages de projet](explanation/project-settings.md)
56+- [Arbre du projet](explanation/project-tree.md)
57+- [Snippets](explanation/snippets.md)
58+- [Outils go](explanation/go-tools.md)
59+- [Fenêtres agent](explanation/agent-windows.md)
new file mode 100644
@@ -0,0 +1,59 @@
1+# Turbo Go — documentation
2+
3+Turbo Go est un éditeur pour Go dans le style de Turbo C : un IDE plein écran en terminal, avec menus, fenêtres déplaçables, coloration syntaxique pour neuf langages, thèmes, complétion via `gopls`, fenêtres shell, réglages par projet, arbre du projet, snippets, et la chaîne d'outils Go à un menu de distance.
4+
5+Cette documentation suit la méthode [Diátaxis](https://diataxis.fr). Quatre types de page, quatre besoins différents — allez directement à celui qui correspond à ce que vous cherchez.
6+
7+| Je veux… | Aller à |
8+| --- | --- |
9+| **apprendre** l'éditeur en l'utilisant | [Tutoriels](tutorials/) |
10+| **faire** quelque chose de précis | [Guides pratiques](how-to/) |
11+| **consulter** un détail exact | [Référence](reference/) |
12+| **comprendre** comment et pourquoi ça marche | [Explications](explanation/) |
13+
14+## Tutoriels — apprendre en faisant
15+
16+- [Votre premier fichier dans Turbo Go](tutorials/getting-started.md) — compiler, ouvrir l'éditeur, écrire un programme Go, le colorer, l'enregistrer et l'exécuter.
17+
18+## Guides pratiques — des recettes pour une tâche
19+
20+- [Installer et compiler Turbo Go](how-to/install.md)
21+- [Lancer les tests](how-to/run-the-tests.md)
22+- [Activer la complétion Go](how-to/enable-completion.md)
23+- [Écrire son propre thème](how-to/write-a-theme.md)
24+- [Se déplacer dans un fichier](how-to/navigate-code.md)
25+- [Interroger le code](how-to/ask-about-code.md)
26+- [Lancer des commandes shell sans quitter l'éditeur](how-to/use-a-terminal.md)
27+- [Donner ses propres réglages à un projet](how-to/configure-a-project.md)
28+- [Parcourir un projet et ouvrir des fichiers depuis un arbre](how-to/browse-a-project.md)
29+- [Insérer des snippets depuis un menu](how-to/use-snippets.md)
30+- [Lancer les commandes go depuis l'éditeur](how-to/run-go-commands.md)
31+- [Dialoguer avec un agent de code depuis l'éditeur](how-to/talk-to-an-agent.md)
32+- [Faire une release](how-to/make-a-release.md)
33+
34+## Référence — les détails exacts
35+
36+- [Ligne de commande](reference/cli.md)
37+- [Clavier](reference/keyboard.md)
38+- [Menus](reference/menus.md)
39+- [Format des fichiers de thème](reference/themes.md)
40+- [Fenêtres terminal](reference/terminal.md)
41+- [Réglages de projet](reference/project-settings.md)
42+- [Arbre du projet](reference/project-tree.md)
43+- [Langages colorés](reference/languages.md)
44+- [Snippets](reference/snippets.md)
45+- [Outils go](reference/go-tools.md)
46+- [Agents et ACP](reference/acp.md)
47+- [Le numéro de version](reference/versioning.md)
48+
49+## Explications — comprendre
50+
51+- [Architecture](explanation/architecture.md)
52+- [Décisions de conception](explanation/design-decisions.md)
53+- [Coloration et complétion](explanation/colouring-and-completion.md)
54+- [Fenêtres terminal](explanation/terminal-windows.md)
55+- [Réglages de projet](explanation/project-settings.md)
56+- [Arbre du projet](explanation/project-tree.md)
57+- [Snippets](explanation/snippets.md)
58+- [Outils go](explanation/go-tools.md)
59+- [Fenêtres agent](explanation/agent-windows.md)
added docs/fr/explanation/agent-windows.md +114 -0
new file mode 100644
@@ -0,0 +1,114 @@
1+# Fenêtres agent
2+
3+Cette page explique pourquoi dialoguer avec un agent prend cette forme-là. Pour savoir comment faire, voir [Dialoguer avec un agent de code](../how-to/talk-to-an-agent.md) ; pour les touches et le format de fichier exacts, [Agents et ACP](../reference/acp.md).
4+
5+## Pourquoi un protocole plutôt qu'un fournisseur
6+
7+Un éditeur qui voulait offrir une fenêtre de conversation avait deux façons de l'obtenir. Parler directement aux fournisseurs de modèles — un client HTTP par fournisseur, un jeu de clés d'API à stocker, une boucle d'appel d'outils à écrire, et un nouvel exemplaire de chaque dès que quelqu'un veut un fournisseur dont l'éditeur n'a jamais entendu parler. Ou parler un seul protocole au programme auquel l'utilisateur fait déjà confiance pour ce travail.
8+
9+L'[Agent Client Protocol](https://agentclientprotocol.com) est la seconde. L'agent est un processus fils ; l'éditeur lui envoie des invites et dessine ce qui revient. L'éditeur ne détient aucune clé d'API, ne connaît aucun fournisseur, et n'implémente aucune boucle d'appel d'outils — et le même code parle à `docker agent` devant un llama.cpp local, à un agent dans le nuage, ou à quelque chose que vous avez écrit cet après-midi.
10+
11+Cela veut aussi dire que l'éditeur n'est pas l'endroit où atterrit un nouveau modèle. Sa prise en charge est une ligne dans le fichier de configuration de *votre* agent, un fichier que cet éditeur ne lit pas.
12+
13+## Pourquoi cela vit dans turbo-core
14+
15+Turbo Go est [une commande, un profil et un analyseur](architecture.md) ; tout le reste est la bibliothèque que partagent tous les éditeurs Turbo. Une fenêtre agent est une fenêtre, un menu, une boîte modale et un tour de boucle d'événements — quatre choses qui appartiennent toutes à `turbo-core/app`. La construire ici aurait voulu dire ajouter à la bibliothèque une couture générale « laisser un éditeur ajouter une fenêtre et un menu depuis l'extérieur », puis s'en servir exactement une fois.
16+
17+Le client du protocole, le modèle de conversation et la fenêtre sont donc `turbo-core/acp`, à côté de `terminal` et `filetree`, qui ont la même forme. Ce que Turbo Go apporte, c'est l'`acp.toml` de départ qu'il propose d'écrire — la seule part de tout ceci qui parle de projets Go. Turbo Rust et Turbo Python obtiendront des fenêtres agent en écrivant un fichier de départ à eux, et rien d'autre.
18+
19+## Pourquoi une fenêtre, et non un panneau
20+
21+Le même raisonnement que celui tranché pour l'[arbre de projet](project-tree.md). Un panneau ancré voudrait dire que le bureau acquiert la notion de bords réservés, et que `fitInto`, les modes d'agrandissement, la maximisation, la mosaïque et la cascade doivent tous les respecter — une modification des fondations de l'interface pour un seul widget. En tant que fenêtre ordinaire, un agent obtient `F6`, les `Alt`-chiffres, `[x]`, `[■]` et Tile gratuitement.
22+
23+Cela fait aussi tomber « plusieurs agents à la fois » au lieu de le concevoir : deux fenêtres sont deux processus et deux conversations, et Tile met un modèle local rapide à côté d'un modèle lent et soigneux. Un panneau aurait dû se doter d'onglets pour en faire autant.
24+
25+## Pourquoi un processus par fenêtre, démarré à l'ouverture
26+
27+Un agent est une conversation, et une conversation a un début. Démarrer le processus avec la fenêtre fait que le dossier de travail de l'agent, son environnement et sa session appartiennent tous à cette fenêtre, et que la fermer est une fin sans ambiguïté — le même marché que passent les [fenêtres terminal](terminal-windows.md), et pour la même raison : ce que la fenêtre contient est un processus en cours, pas un travail non enregistré, donc la fermer ne demande rien.
28+
29+L'autre solution — un agent unique et durable multiplexé sur plusieurs fenêtres — aurait voulu dire que l'éditeur décide à quelle fenêtre appartient un `session/update`, et quoi faire d'une fenêtre dont la session a disparu alors que le processus vit encore. Deux processus coûtent moins cher que cette comptabilité.
30+
31+## Pourquoi la boîte de permission est ouverte depuis la boucle d'événements, et non depuis le message
32+
33+`session/request_permission` arrive sur la goroutine de lecture de la connexion, et la réponse vient d'une boîte de dialogue que l'utilisateur doit regarder. La réponse ne peut donc pas être faite là où la requête est traitée, et la boîte ne peut pas non plus y être ouverte : tout ce qui dessine appartient à la goroutine principale.
34+
35+La requête est donc *enregistrée*, et la boucle d'événements la remarque à son tour suivant et ouvre la boîte. C'est la quatrième fois que ce projet arrive à la même conclusion — l'[enregistrement automatique](project-settings.md), la ré-annonce au serveur de langage et les redessins du terminal sont les autres — et la raison est toujours la même : `PostEvent` a le droit de jeter ce qui ne rentre pas, donc un événement peut provoquer un tour de boucle mais ne doit jamais être le seul porteur d'un fait.
36+
37+C'est pourquoi la couche JSON-RPC a dû apprendre à répondre à une requête *plus tard*. C'est aussi toute la raison pour laquelle `jsonrpc` a été extrait de `lsp` : les questions d'un serveur de langage peuvent toutes être répondues sur-le-champ, et celles d'un agent non.
38+
39+## Pourquoi l'agent reçoit le tampon plutôt que le fichier
40+
41+Quand l'agent lit un fichier que vous avez ouvert et pas enregistré, il reçoit le texte que vous avez sous les yeux, pas celui du disque. L'autre solution est un agent qui relit la version que vous venez de dépasser, ce qui est faux précisément au moment où vous avez le plus de chances de poser la question — vous avez changé quelque chose et vous voulez savoir ce qu'il en est.
42+
43+Le coût est que l'agent voit un texte qu'aucun autre outil ne voit, donc une réponse citant un numéro de ligne peut ne pas correspondre à ce que dit `go build`. C'est accepté : c'est déjà vrai de la complétion, qui répond depuis le tampon depuis que l'éditeur sait parler à `gopls`.
44+
45+Les écritures suivent le même chemin, dans le tampon, marqué modifié. Un agent qui modifie un fichier laisse la modification sous vos yeux, annulable avec `Ctrl-Z` et non enregistrée jusqu'à ce que vous fassiez `F2`. Un agent réécrivant discrètement un fichier sous une fenêtre que vous avez ouverte serait la pire version possible de cette fonctionnalité.
46+
47+## Pourquoi les couleurs sont les classes syntaxiques, et non de nouvelles clés de thème
48+
49+L'[arbre de projet](project-tree.md) a eu besoin de clés de thème à lui, parce qu'il aurait sinon emprunté `list.selected`, une couleur choisie sur un fond de *dialogue*, et dessiné sa ligne sélectionnée dans la couleur qui se trouve dessous. Rien de tel ici : le corps d'une fenêtre agent est `window.body`, ce sur quoi les classes syntaxiques sont déjà choisies et déjà testées pour le contraste.
50+
51+Le nom d'un interlocuteur est donc dessiné dans le style des mots-clés, une réflexion dans celui des commentaires, un appel d'outil dans celui des types, et le code selon ce qu'en dit son propre analyseur. Onze thèmes colorent correctement les fenêtres agent sans avoir été touchés, et un thème écrit par quelqu'un l'an dernier aussi.
52+
53+Ce à quoi on renonce, c'est l'expressivité : un thème ne peut pas rendre les réflexions discrètes sans rendre aussi les commentaires discrets, puisque c'est la même clé. Si cela s'avère gênant à l'usage, des clés `agent.*` pourront être ajoutées plus tard — les règles de contraste et le test de complétude en sont le prix, et il ne vaut d'être payé que si quelqu'un veut la distinction.
54+
55+## Pourquoi la transcription est un modèle que la fenêtre se contente de dessiner
56+
57+L'agent envoie des jetons : `"I"`, `" found"`, `" agent"`, `".yaml"`. Une fenêtre qui ajouterait chacun à une liste de lignes serait une fenêtre incapable de reformater, incapable de distinguer la prose d'un bloc de code délimité, et impossible à tester sans agent vivant.
58+
59+La conversation est donc une valeur — `acp.Transcript` — qui fusionne les fragments en entrées, replie chaque `tool_call_update` sur le `tool_call` dont l'identifiant correspond, et remet à la fenêtre une liste de blocs qui sont soit de la prose, soit du code dans un langage nommé. Elle ne sait rien d'un terminal, ce qui permet de la tester en appelant des fonctions et en comparant des valeurs, la règle d'organisation que suivent déjà `buffer`, `lsp` et `syntax`.
60+
61+C'est aussi ce qui rend les tests de dessin déterministes. Le projet s'est déjà fait mordre par des tests qui portaient sur un écran pendant qu'un processus vivant y écrivait, et cela a caché un vrai défaut pendant toute une session ; une fenêtre dessinée à partir d'une transcription figée ne peut courir contre rien.
62+
63+## Ce qui a été volontairement laissé de côté
64+
65+- **La reprise de session.** `session/load` existe, et s'en servir voudrait dire décider où les conversations sont stockées, combien de temps elles sont gardées, et ce qui se passe quand le projet a déménagé. C'est une fonctionnalité à part entière.
66+- **L'authentification.** Un agent qui a besoin d'une connexion est prié de se connecter avec sa propre CLI. Stocker un identifiant est une responsabilité que cet éditeur a jusqu'ici entièrement évitée, et une méthode de protocole n'est pas une bonne raison de commencer.
67+- **La capacité `terminal`.** Un agent peut déjà avoir un shell par ses propres jeux d'outils, comme le fait `docker agent`. L'annoncer voudrait dire que l'éditeur lance des commandes pour le compte de l'agent et possède la sortie — le menu des outils fait déjà cela, et mieux, pour des commandes que *vous* avez choisies.
68+- **Les images dans les invites.** L'éditeur a du texte et des fichiers à envoyer, et un terminal pour dessiner.
69+
70+## Voir aussi
71+
72+- [Architecture](architecture.md) — ce qui est ici et ce qui est dans la bibliothèque
73+- [Fenêtres terminal](terminal-windows.md) — l'autre fenêtre qui contient un processus vivant
74+- [Arbre de projet](project-tree.md) — là où l'argument fenêtre-et-non-panneau a été posé la première fois
75+
76+## Pourquoi la copie va dans deux presse-papiers
77+
78+« Copie ça pour que je m'en serve ailleurs » veut généralement dire *complètement ailleurs* — une autre fenêtre, un navigateur, un message à un collègue. Un presse-papiers qui ne fonctionnerait qu'à l'intérieur de cet éditeur répondrait à la plus petite moitié de la demande, et à celle qu'on avait le moins de chances de poser.
79+
80+Une copie part donc dans les deux : celui de l'éditeur, d'où `Shift-Ins` colle, et celui du système, atteint en le demandant au terminal par OSC 52. Rien ne vérifie le second, parce qu'il n'y a rien à vérifier — la séquence n'a pas de réponse, un terminal peut la refuser par sécurité, et certains demandent qu'on l'active. Un message promettant quelque chose qui n'a pas eu lieu serait pire qu'un message qui se tait : la barre d'état dit seulement combien de lignes ont été copiées, ce qui est vrai dans les deux cas.
81+
82+## Pourquoi copier sans rien sélectionner copie un bloc entier
83+
84+Ce qu'on veut extraire d'une conversation, c'est presque toujours un bloc de code. Obliger à le sélectionner d'abord — six frappes, ou un glissement qu'il faut viser — est un travail que l'éditeur a déjà de quoi faire à votre place : c'est lui qui a mis la conversation en page, donc il sait exactement où ce bloc commence et finit.
85+
86+Les lignes portent donc une **région** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Sans rien de sélectionné, `Ctrl-C` copie la région sur laquelle est le curseur. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et reçoivent des régions à eux, ce qui garde `‣ Bob (llama.cpp)` hors d'un bloc collé dans un fichier source.
87+
88+Cette dernière partie n'a pas été conçue, elle a été trouvée. La première version copiait le libellé avec le code ; c'est en copiant depuis le vrai binaire et en relisant la charge utile OSC 52 sur le fil qu'on s'en est aperçu.
89+
90+## Pourquoi l'indicateur d'activité est dessiné à partir de l'horloge
91+
92+Un agent qui réfléchit vingt secondes n'envoie strictement rien, et une fenêtre qui aurait l'air figée serait indiscernable d'une fenêtre réellement bloquée. L'indicateur est la réponse la moins chère possible à « est-ce que ça marche encore ? ».
93+
94+C'est une fonction du temps — `Spinner(now)` — et non un compteur que quelque chose incrémente. Rien n'a besoin d'être remis à zéro au début d'un tour, deux fenêtres qui réfléchissent en même temps tournent en phase, et un test peut porter sur une image sans attendre qu'elle arrive — la même raison pour laquelle `editor.View` et `app.App` ont tous deux une horloge injectable.
95+
96+Dessiner à partir de l'horloge veut dire qu'autre chose doit *provoquer* le redessin : une session en cours de tour réveille donc la boucle d'événements à la cadence de l'indicateur. Cela a le droit d'être un minuteur précisément parce qu'un battement perdu ne peut rien laisser en plan : il demande un tour de boucle et ne porte jamais de fait — la règle à laquelle ce projet est maintenant arrivé cinq fois.
97+
98+Le **titre** de la fenêtre, lui, n'est délibérément pas animé. C'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui changerait huit fois par seconde les ferait scintiller sans rien apporter.
99+
100+## Pourquoi les commandes sont une liste dans la zone de saisie, et non un menu
101+
102+Les commandes d'un agent arrivent sur le fil sous forme de liste — `available_commands_update` — et peuvent changer pendant la session. Un menu construit à partir d'elles devrait être reconstruit à chaque mise à jour, se trouverait loin de l'endroit où la commande se tape, et finirait quand même par mettre `/web ` dans la zone de saisie, parce que c'est la seule chose que le protocole laisse un client envoyer : une commande est une invite texte que l'agent reconnaît à son premier mot.
103+
104+La liste s'ouvre donc là où est le texte, sur le caractère qui commence une commande, et se ferme quand le mot est complet. C'est la même forme que la fenêtre de complétion au-dessus d'un fichier, pour la même raison : ce que vous choisissez est ce que vous tapez. Utiliser `/` et `@` plutôt que des touches propres à l'éditeur est délibéré — ce sont les caractères de Zed, si bien que la documentation d'un agent est vraie ici sans table de correspondance.
105+
106+`Entrée` a deux sens sur la liste, ordonnés par le degré d'achèvement du mot : elle complète un mot inachevé, et envoie un mot achevé. L'alternative — `Entrée` complète toujours, une seconde `Entrée` envoie — coûte une frappe à chaque commande et n'apporte rien, parce qu'un mot qui se lit déjà exactement comme une commande n'a plus rien à compléter.
107+
108+## Pourquoi une mention emporte le fichier, quand elle le peut
109+
110+Le protocole offre deux façons de nommer un fichier dans une invite : un `resource_link`, qui est une URI que l'agent va chercher lui-même, et une `resource` incorporée, qui est l'URI *et le texte*. La spécification appelle la seconde « la façon préférée d'inclure du contexte », et la raison est celle qui fait répondre `fs/read_text_file` depuis le tampon : l'éditeur sait sur le fichier des choses que le disque ignore. Un agent qui suit un lien vers un fichier que vous avez modifié sans l'enregistrer lit la version que vous venez de quitter, ce qui est faux précisément au moment où vous êtes le plus susceptible de demander.
111+
112+L'éditeur envoie donc le texte quand l'agent a déclaré `promptCapabilities.embeddedContext`, lu par le même chemin que `fs/read_text_file`, et un lien sinon — jamais rien. Un fichier qui ne peut pas être lu part aussi en lien, pour que l'agent sache au moins quel fichier était visé.
113+
114+La mention remplace le nom dans le texte plutôt que de voyager à côté. Envoyer `explique @main.go` comme texte *et* comme pièce jointe donnerait à l'agent le nom deux fois en le laissant les apparier ; mettre le bloc là où était le nom lui donne le fichier là où la phrase en a besoin. La conversation, elle, garde la ligne telle que tapée : c'est ce que vous avez dit, et la fenêtre est le compte rendu de la conversation, pas du fil.
new file mode 100644
@@ -0,0 +1,114 @@
1+# Fenêtres agent
2+
3+Cette page explique pourquoi dialoguer avec un agent prend cette forme-là. Pour savoir comment faire, voir [Dialoguer avec un agent de code](../how-to/talk-to-an-agent.md) ; pour les touches et le format de fichier exacts, [Agents et ACP](../reference/acp.md).
4+
5+## Pourquoi un protocole plutôt qu'un fournisseur
6+
7+Un éditeur qui voulait offrir une fenêtre de conversation avait deux façons de l'obtenir. Parler directement aux fournisseurs de modèles — un client HTTP par fournisseur, un jeu de clés d'API à stocker, une boucle d'appel d'outils à écrire, et un nouvel exemplaire de chaque dès que quelqu'un veut un fournisseur dont l'éditeur n'a jamais entendu parler. Ou parler un seul protocole au programme auquel l'utilisateur fait déjà confiance pour ce travail.
8+
9+L'[Agent Client Protocol](https://agentclientprotocol.com) est la seconde. L'agent est un processus fils ; l'éditeur lui envoie des invites et dessine ce qui revient. L'éditeur ne détient aucune clé d'API, ne connaît aucun fournisseur, et n'implémente aucune boucle d'appel d'outils — et le même code parle à `docker agent` devant un llama.cpp local, à un agent dans le nuage, ou à quelque chose que vous avez écrit cet après-midi.
10+
11+Cela veut aussi dire que l'éditeur n'est pas l'endroit où atterrit un nouveau modèle. Sa prise en charge est une ligne dans le fichier de configuration de *votre* agent, un fichier que cet éditeur ne lit pas.
12+
13+## Pourquoi cela vit dans turbo-core
14+
15+Turbo Go est [une commande, un profil et un analyseur](architecture.md) ; tout le reste est la bibliothèque que partagent tous les éditeurs Turbo. Une fenêtre agent est une fenêtre, un menu, une boîte modale et un tour de boucle d'événements — quatre choses qui appartiennent toutes à `turbo-core/app`. La construire ici aurait voulu dire ajouter à la bibliothèque une couture générale « laisser un éditeur ajouter une fenêtre et un menu depuis l'extérieur », puis s'en servir exactement une fois.
16+
17+Le client du protocole, le modèle de conversation et la fenêtre sont donc `turbo-core/acp`, à côté de `terminal` et `filetree`, qui ont la même forme. Ce que Turbo Go apporte, c'est l'`acp.toml` de départ qu'il propose d'écrire — la seule part de tout ceci qui parle de projets Go. Turbo Rust et Turbo Python obtiendront des fenêtres agent en écrivant un fichier de départ à eux, et rien d'autre.
18+
19+## Pourquoi une fenêtre, et non un panneau
20+
21+Le même raisonnement que celui tranché pour l'[arbre de projet](project-tree.md). Un panneau ancré voudrait dire que le bureau acquiert la notion de bords réservés, et que `fitInto`, les modes d'agrandissement, la maximisation, la mosaïque et la cascade doivent tous les respecter — une modification des fondations de l'interface pour un seul widget. En tant que fenêtre ordinaire, un agent obtient `F6`, les `Alt`-chiffres, `[x]`, `[■]` et Tile gratuitement.
22+
23+Cela fait aussi tomber « plusieurs agents à la fois » au lieu de le concevoir : deux fenêtres sont deux processus et deux conversations, et Tile met un modèle local rapide à côté d'un modèle lent et soigneux. Un panneau aurait dû se doter d'onglets pour en faire autant.
24+
25+## Pourquoi un processus par fenêtre, démarré à l'ouverture
26+
27+Un agent est une conversation, et une conversation a un début. Démarrer le processus avec la fenêtre fait que le dossier de travail de l'agent, son environnement et sa session appartiennent tous à cette fenêtre, et que la fermer est une fin sans ambiguïté — le même marché que passent les [fenêtres terminal](terminal-windows.md), et pour la même raison : ce que la fenêtre contient est un processus en cours, pas un travail non enregistré, donc la fermer ne demande rien.
28+
29+L'autre solution — un agent unique et durable multiplexé sur plusieurs fenêtres — aurait voulu dire que l'éditeur décide à quelle fenêtre appartient un `session/update`, et quoi faire d'une fenêtre dont la session a disparu alors que le processus vit encore. Deux processus coûtent moins cher que cette comptabilité.
30+
31+## Pourquoi la boîte de permission est ouverte depuis la boucle d'événements, et non depuis le message
32+
33+`session/request_permission` arrive sur la goroutine de lecture de la connexion, et la réponse vient d'une boîte de dialogue que l'utilisateur doit regarder. La réponse ne peut donc pas être faite là où la requête est traitée, et la boîte ne peut pas non plus y être ouverte : tout ce qui dessine appartient à la goroutine principale.
34+
35+La requête est donc *enregistrée*, et la boucle d'événements la remarque à son tour suivant et ouvre la boîte. C'est la quatrième fois que ce projet arrive à la même conclusion — l'[enregistrement automatique](project-settings.md), la ré-annonce au serveur de langage et les redessins du terminal sont les autres — et la raison est toujours la même : `PostEvent` a le droit de jeter ce qui ne rentre pas, donc un événement peut provoquer un tour de boucle mais ne doit jamais être le seul porteur d'un fait.
36+
37+C'est pourquoi la couche JSON-RPC a dû apprendre à répondre à une requête *plus tard*. C'est aussi toute la raison pour laquelle `jsonrpc` a été extrait de `lsp` : les questions d'un serveur de langage peuvent toutes être répondues sur-le-champ, et celles d'un agent non.
38+
39+## Pourquoi l'agent reçoit le tampon plutôt que le fichier
40+
41+Quand l'agent lit un fichier que vous avez ouvert et pas enregistré, il reçoit le texte que vous avez sous les yeux, pas celui du disque. L'autre solution est un agent qui relit la version que vous venez de dépasser, ce qui est faux précisément au moment où vous avez le plus de chances de poser la question — vous avez changé quelque chose et vous voulez savoir ce qu'il en est.
42+
43+Le coût est que l'agent voit un texte qu'aucun autre outil ne voit, donc une réponse citant un numéro de ligne peut ne pas correspondre à ce que dit `go build`. C'est accepté : c'est déjà vrai de la complétion, qui répond depuis le tampon depuis que l'éditeur sait parler à `gopls`.
44+
45+Les écritures suivent le même chemin, dans le tampon, marqué modifié. Un agent qui modifie un fichier laisse la modification sous vos yeux, annulable avec `Ctrl-Z` et non enregistrée jusqu'à ce que vous fassiez `F2`. Un agent réécrivant discrètement un fichier sous une fenêtre que vous avez ouverte serait la pire version possible de cette fonctionnalité.
46+
47+## Pourquoi les couleurs sont les classes syntaxiques, et non de nouvelles clés de thème
48+
49+L'[arbre de projet](project-tree.md) a eu besoin de clés de thème à lui, parce qu'il aurait sinon emprunté `list.selected`, une couleur choisie sur un fond de *dialogue*, et dessiné sa ligne sélectionnée dans la couleur qui se trouve dessous. Rien de tel ici : le corps d'une fenêtre agent est `window.body`, ce sur quoi les classes syntaxiques sont déjà choisies et déjà testées pour le contraste.
50+
51+Le nom d'un interlocuteur est donc dessiné dans le style des mots-clés, une réflexion dans celui des commentaires, un appel d'outil dans celui des types, et le code selon ce qu'en dit son propre analyseur. Onze thèmes colorent correctement les fenêtres agent sans avoir été touchés, et un thème écrit par quelqu'un l'an dernier aussi.
52+
53+Ce à quoi on renonce, c'est l'expressivité : un thème ne peut pas rendre les réflexions discrètes sans rendre aussi les commentaires discrets, puisque c'est la même clé. Si cela s'avère gênant à l'usage, des clés `agent.*` pourront être ajoutées plus tard — les règles de contraste et le test de complétude en sont le prix, et il ne vaut d'être payé que si quelqu'un veut la distinction.
54+
55+## Pourquoi la transcription est un modèle que la fenêtre se contente de dessiner
56+
57+L'agent envoie des jetons : `"I"`, `" found"`, `" agent"`, `".yaml"`. Une fenêtre qui ajouterait chacun à une liste de lignes serait une fenêtre incapable de reformater, incapable de distinguer la prose d'un bloc de code délimité, et impossible à tester sans agent vivant.
58+
59+La conversation est donc une valeur — `acp.Transcript` — qui fusionne les fragments en entrées, replie chaque `tool_call_update` sur le `tool_call` dont l'identifiant correspond, et remet à la fenêtre une liste de blocs qui sont soit de la prose, soit du code dans un langage nommé. Elle ne sait rien d'un terminal, ce qui permet de la tester en appelant des fonctions et en comparant des valeurs, la règle d'organisation que suivent déjà `buffer`, `lsp` et `syntax`.
60+
61+C'est aussi ce qui rend les tests de dessin déterministes. Le projet s'est déjà fait mordre par des tests qui portaient sur un écran pendant qu'un processus vivant y écrivait, et cela a caché un vrai défaut pendant toute une session ; une fenêtre dessinée à partir d'une transcription figée ne peut courir contre rien.
62+
63+## Ce qui a été volontairement laissé de côté
64+
65+- **La reprise de session.** `session/load` existe, et s'en servir voudrait dire décider où les conversations sont stockées, combien de temps elles sont gardées, et ce qui se passe quand le projet a déménagé. C'est une fonctionnalité à part entière.
66+- **L'authentification.** Un agent qui a besoin d'une connexion est prié de se connecter avec sa propre CLI. Stocker un identifiant est une responsabilité que cet éditeur a jusqu'ici entièrement évitée, et une méthode de protocole n'est pas une bonne raison de commencer.
67+- **La capacité `terminal`.** Un agent peut déjà avoir un shell par ses propres jeux d'outils, comme le fait `docker agent`. L'annoncer voudrait dire que l'éditeur lance des commandes pour le compte de l'agent et possède la sortie — le menu des outils fait déjà cela, et mieux, pour des commandes que *vous* avez choisies.
68+- **Les images dans les invites.** L'éditeur a du texte et des fichiers à envoyer, et un terminal pour dessiner.
69+
70+## Voir aussi
71+
72+- [Architecture](architecture.md) — ce qui est ici et ce qui est dans la bibliothèque
73+- [Fenêtres terminal](terminal-windows.md) — l'autre fenêtre qui contient un processus vivant
74+- [Arbre de projet](project-tree.md) — là où l'argument fenêtre-et-non-panneau a été posé la première fois
75+
76+## Pourquoi la copie va dans deux presse-papiers
77+
78+« Copie ça pour que je m'en serve ailleurs » veut généralement dire *complètement ailleurs* — une autre fenêtre, un navigateur, un message à un collègue. Un presse-papiers qui ne fonctionnerait qu'à l'intérieur de cet éditeur répondrait à la plus petite moitié de la demande, et à celle qu'on avait le moins de chances de poser.
79+
80+Une copie part donc dans les deux : celui de l'éditeur, d'où `Shift-Ins` colle, et celui du système, atteint en le demandant au terminal par OSC 52. Rien ne vérifie le second, parce qu'il n'y a rien à vérifier — la séquence n'a pas de réponse, un terminal peut la refuser par sécurité, et certains demandent qu'on l'active. Un message promettant quelque chose qui n'a pas eu lieu serait pire qu'un message qui se tait : la barre d'état dit seulement combien de lignes ont été copiées, ce qui est vrai dans les deux cas.
81+
82+## Pourquoi copier sans rien sélectionner copie un bloc entier
83+
84+Ce qu'on veut extraire d'une conversation, c'est presque toujours un bloc de code. Obliger à le sélectionner d'abord — six frappes, ou un glissement qu'il faut viser — est un travail que l'éditeur a déjà de quoi faire à votre place : c'est lui qui a mis la conversation en page, donc il sait exactement où ce bloc commence et finit.
85+
86+Les lignes portent donc une **région** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Sans rien de sélectionné, `Ctrl-C` copie la région sur laquelle est le curseur. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et reçoivent des régions à eux, ce qui garde `‣ Bob (llama.cpp)` hors d'un bloc collé dans un fichier source.
87+
88+Cette dernière partie n'a pas été conçue, elle a été trouvée. La première version copiait le libellé avec le code ; c'est en copiant depuis le vrai binaire et en relisant la charge utile OSC 52 sur le fil qu'on s'en est aperçu.
89+
90+## Pourquoi l'indicateur d'activité est dessiné à partir de l'horloge
91+
92+Un agent qui réfléchit vingt secondes n'envoie strictement rien, et une fenêtre qui aurait l'air figée serait indiscernable d'une fenêtre réellement bloquée. L'indicateur est la réponse la moins chère possible à « est-ce que ça marche encore ? ».
93+
94+C'est une fonction du temps — `Spinner(now)` — et non un compteur que quelque chose incrémente. Rien n'a besoin d'être remis à zéro au début d'un tour, deux fenêtres qui réfléchissent en même temps tournent en phase, et un test peut porter sur une image sans attendre qu'elle arrive — la même raison pour laquelle `editor.View` et `app.App` ont tous deux une horloge injectable.
95+
96+Dessiner à partir de l'horloge veut dire qu'autre chose doit *provoquer* le redessin : une session en cours de tour réveille donc la boucle d'événements à la cadence de l'indicateur. Cela a le droit d'être un minuteur précisément parce qu'un battement perdu ne peut rien laisser en plan : il demande un tour de boucle et ne porte jamais de fait — la règle à laquelle ce projet est maintenant arrivé cinq fois.
97+
98+Le **titre** de la fenêtre, lui, n'est délibérément pas animé. C'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui changerait huit fois par seconde les ferait scintiller sans rien apporter.
99+
100+## Pourquoi les commandes sont une liste dans la zone de saisie, et non un menu
101+
102+Les commandes d'un agent arrivent sur le fil sous forme de liste — `available_commands_update` — et peuvent changer pendant la session. Un menu construit à partir d'elles devrait être reconstruit à chaque mise à jour, se trouverait loin de l'endroit où la commande se tape, et finirait quand même par mettre `/web ` dans la zone de saisie, parce que c'est la seule chose que le protocole laisse un client envoyer : une commande est une invite texte que l'agent reconnaît à son premier mot.
103+
104+La liste s'ouvre donc là où est le texte, sur le caractère qui commence une commande, et se ferme quand le mot est complet. C'est la même forme que la fenêtre de complétion au-dessus d'un fichier, pour la même raison : ce que vous choisissez est ce que vous tapez. Utiliser `/` et `@` plutôt que des touches propres à l'éditeur est délibéré — ce sont les caractères de Zed, si bien que la documentation d'un agent est vraie ici sans table de correspondance.
105+
106+`Entrée` a deux sens sur la liste, ordonnés par le degré d'achèvement du mot : elle complète un mot inachevé, et envoie un mot achevé. L'alternative — `Entrée` complète toujours, une seconde `Entrée` envoie — coûte une frappe à chaque commande et n'apporte rien, parce qu'un mot qui se lit déjà exactement comme une commande n'a plus rien à compléter.
107+
108+## Pourquoi une mention emporte le fichier, quand elle le peut
109+
110+Le protocole offre deux façons de nommer un fichier dans une invite : un `resource_link`, qui est une URI que l'agent va chercher lui-même, et une `resource` incorporée, qui est l'URI *et le texte*. La spécification appelle la seconde « la façon préférée d'inclure du contexte », et la raison est celle qui fait répondre `fs/read_text_file` depuis le tampon : l'éditeur sait sur le fichier des choses que le disque ignore. Un agent qui suit un lien vers un fichier que vous avez modifié sans l'enregistrer lit la version que vous venez de quitter, ce qui est faux précisément au moment où vous êtes le plus susceptible de demander.
111+
112+L'éditeur envoie donc le texte quand l'agent a déclaré `promptCapabilities.embeddedContext`, lu par le même chemin que `fs/read_text_file`, et un lien sinon — jamais rien. Un fichier qui ne peut pas être lu part aussi en lien, pour que l'agent sache au moins quel fichier était visé.
113+
114+La mention remplace le nom dans le texte plutôt que de voyager à côté. Envoyer `explique @main.go` comme texte *et* comme pièce jointe donnerait à l'agent le nom deux fois en le laissant les apparier ; mettre le bloc là où était le nom lui donne le fichier là où la phrase en a besoin. La conversation, elle, garde la ligne telle que tapée : c'est ce que vous avez dit, et la fenêtre est le compte rendu de la conversation, pas du fil.
added docs/fr/explanation/architecture.md +85 -0
new file mode 100644
@@ -0,0 +1,85 @@
1+# Architecture — explication
2+
3+## De quoi s'agit-il ?
4+
5+Turbo Go est une commande, un profil et un analyseur. Tout le reste — le composant d'édition, les fenêtres, les menus, les dialogues, les thèmes, l'émulateur de terminal, l'arborescence de fichiers, le client LSP — est [turbo-core](https://rickub.com/turbo-editors/turbo-core), la bibliothèque sur laquelle repose chaque éditeur Turbo.
6+
7+Cela n'a pas toujours été vrai. Turbo Go était un unique programme d'environ onze mille cinq cents lignes en quatorze paquets jusqu'à ce que l'on veuille un second éditeur ; à ce moment-là, les quatorze ont migré dans une bibliothèque et un seul est resté ici. Cette page parle de la coupure qui en résulte.
8+
9+## Ce qu'il y a dans ce dépôt
10+
11+```
12+main.go les drapeaux, le terminal, et le câblage
13+internal/golang la totalité de ce qui fait Turbo Go
14+ golang.go le profil : nom, menu, serveur, marqueur de racine
15+ scan.go l'analyseur Go, bâti sur go/scanner
16+ templates.go trois déclarations //go:embed
17+ *.toml.tmpl les trois fichiers de départ qu'un projet reçoit, embarqués
18+```
19+
20+Environ quatre cents lignes. Il n'y a pas d'`internal/app`, pas d'`internal/ui`, pas d'`internal/buffer` — ceux-là existent une fois, dans la bibliothèque, et chaque éditeur bâti dessus s'en sert sans les modifier.
21+
22+## Ce que fait `main`
23+
24+Six choses, dans cet ordre :
25+
26+1. Analyse les drapeaux.
27+2. Appelle `golang.Register()`, qui apprend à la bibliothèque à colorer les fichiers `.go`.
28+3. Construit `golang.Profile()` — la valeur qui dit que cet éditeur est Turbo Go.
29+4. Lit `.turbo-go/settings.toml` dans le répertoire courant, s'il y en a un.
30+5. Ouvre le terminal et passe l'écran, le nom du thème et le profil à `app.New`.
31+6. Démarre gopls à la racine du module, et lance la boucle d'événements.
32+
33+C'est toute la commande. Chaque décision qu'elle prend — quel thème l'emporte, quels fichiers ouvrir, faut-il démarrer un serveur de langage — porte sur *cette exécution*, pas sur Go.
34+
35+## Le profil est la couture
36+
37+```go
38+profile.Profile{
39+ Name: "Turbo Go",
40+ Slug: "turbo-go",
41+ Language: "Go",
42+ ToolsMenu: "~G~o",
43+ RootMarkers: []string{"go.mod"},
44+ Server: profile.Server{Command: "gopls", Args: []string{"serve"}, },
45+ Templates: profile.Templates{Settings: , Snippets: , Tools: },
46+}
47+```
48+
49+Tout ce qui était un `"turbo-go"`, un `"gopls"` ou un `"go.mod"` en dur quelque part dans onze mille lignes est ici un champ. La bibliothèque les lit ; rien dans la bibliothèque ne sait ce qu'ils veulent dire.
50+
51+`Slug` porte plus qu'il n'y paraît. Le binaire est `turbo-go`, le répertoire de projet est `.turbo-go`, la configuration de l'utilisateur vit dans `~/.config/turbo-go`, et les variables d'environnement qui la remplacent sont `TURBO_GO_THEME_DIR` et `TURBO_GO_SNIPPET_DIR` — toutes dérivées de ce seul mot. Ces noms sont inchangés par le refactoring, et délibérément : quelqu'un qui a posé `TURBO_GO_THEME_DIR` l'a fait face à un binaire publié.
52+
53+## Pourquoi l'analyseur Go est ici et pas dans la bibliothèque
54+
55+turbo-core colore huit langages lui-même : TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell. Ce sont ceux que tout éditeur rencontre quel que soit son objet — la configuration d'un projet est du TOML ou du YAML, sa documentation du Markdown, ses scripts du shell, sa construction d'image un Dockerfile.
56+
57+Go n'en fait pas partie. Le langage qui *définit* un éditeur est enregistré par cet éditeur, ce qui explique qu'un fichier `.rs` s'ouvre ici en texte brut et qu'un fichier `.go` s'ouvre en texte brut dans Turbo Rust.
58+
59+L'analyseur Go est aussi celui qui ressemble le *moins* aux autres. Tous les langages de la bibliothèque sont analysés ligne à ligne avec `syntax.LineScanner` ; Go passe par `go/scanner`, l'analyseur lexical qu'utilise la chaîne d'outils Go elle-même, et convertit ses décalages en octets avec `syntax.LineIndex`. Si la bibliothèque accepte les deux formes, c'est à cause de cet analyseur.
60+
61+## Ce qui a bougé, et ce qui n'a pas bougé
62+
63+| Était | Est |
64+| --- | --- |
65+| `internal/buffer`, `internal/ui`, `internal/editor`, … | `turbo-core/buffer`, `turbo-core/ui`, `turbo-core/editor`, … |
66+| `internal/syntax` — six langages | `turbo-core/syntax` — huit, plus un registre ; Go vit ici |
67+| `internal/app` avec une constante `Name` | `turbo-core/app` recevant un `profile.Profile` |
68+| `internal/lsp` codant gopls en dur | `turbo-core/lsp` recevant un `profile.Server` |
69+| `moduleRoot` dans `main.go` | `app.ProjectRoot(p, files)`, avec `go.mod` dans le profil |
70+| `settings.DirName = ".turbo-go"` | `p.ProjectDir()` |
71+
72+**Rien du comportement de l'éditeur n'a changé.** Les menus, les touches, les thèmes, les formats de fichiers et les variables d'environnement sont ce qu'ils étaient. Ce qui a changé, c'est l'endroit où le code habite.
73+
74+## Pourquoi une bibliothèque plutôt qu'un fork
75+
76+L'alternative à l'extraction de turbo-core était de copier Turbo Go et d'en changer les parties Go. Elle a été rejetée avant d'être entreprise : deux copies de onze mille lignes divergent en un mois, et chaque correctif doit être fait deux fois par quelqu'un qui se souvient qu'il y en a deux.
77+
78+Le coût, accepté : une modification d'un menu touche désormais tous les éditeurs à la fois, et Turbo Go ne peut plus prendre une décision qui n'arrange que Go sans soit la mettre dans le profil, soit la défendre dans la bibliothèque. C'est une vraie contrainte, et c'est elle qui fait que les éditeurs restent le même éditeur.
79+
80+## Comment cela se relie au reste
81+
82+- Ce que fait chaque paquet de la bibliothèque : [la référence des paquets de turbo-core](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/fr/reference/packages.md)
83+- Comment marche la coloration ici : [Coloration et complétion](colouring-and-completion.md)
84+- Pourquoi le menu d'outils est une donnée : [Outils Go](go-tools.md)
85+- Les décisions qui ont survécu au refactoring : [Décisions de conception](design-decisions.md)
new file mode 100644
@@ -0,0 +1,85 @@
1+# Architecture — explication
2+
3+## De quoi s'agit-il ?
4+
5+Turbo Go est une commande, un profil et un analyseur. Tout le reste — le composant d'édition, les fenêtres, les menus, les dialogues, les thèmes, l'émulateur de terminal, l'arborescence de fichiers, le client LSP — est [turbo-core](https://rickub.com/turbo-editors/turbo-core), la bibliothèque sur laquelle repose chaque éditeur Turbo.
6+
7+Cela n'a pas toujours été vrai. Turbo Go était un unique programme d'environ onze mille cinq cents lignes en quatorze paquets jusqu'à ce que l'on veuille un second éditeur ; à ce moment-là, les quatorze ont migré dans une bibliothèque et un seul est resté ici. Cette page parle de la coupure qui en résulte.
8+
9+## Ce qu'il y a dans ce dépôt
10+
11+```
12+main.go les drapeaux, le terminal, et le câblage
13+internal/golang la totalité de ce qui fait Turbo Go
14+ golang.go le profil : nom, menu, serveur, marqueur de racine
15+ scan.go l'analyseur Go, bâti sur go/scanner
16+ templates.go trois déclarations //go:embed
17+ *.toml.tmpl les trois fichiers de départ qu'un projet reçoit, embarqués
18+```
19+
20+Environ quatre cents lignes. Il n'y a pas d'`internal/app`, pas d'`internal/ui`, pas d'`internal/buffer` — ceux-là existent une fois, dans la bibliothèque, et chaque éditeur bâti dessus s'en sert sans les modifier.
21+
22+## Ce que fait `main`
23+
24+Six choses, dans cet ordre :
25+
26+1. Analyse les drapeaux.
27+2. Appelle `golang.Register()`, qui apprend à la bibliothèque à colorer les fichiers `.go`.
28+3. Construit `golang.Profile()` — la valeur qui dit que cet éditeur est Turbo Go.
29+4. Lit `.turbo-go/settings.toml` dans le répertoire courant, s'il y en a un.
30+5. Ouvre le terminal et passe l'écran, le nom du thème et le profil à `app.New`.
31+6. Démarre gopls à la racine du module, et lance la boucle d'événements.
32+
33+C'est toute la commande. Chaque décision qu'elle prend — quel thème l'emporte, quels fichiers ouvrir, faut-il démarrer un serveur de langage — porte sur *cette exécution*, pas sur Go.
34+
35+## Le profil est la couture
36+
37+```go
38+profile.Profile{
39+ Name: "Turbo Go",
40+ Slug: "turbo-go",
41+ Language: "Go",
42+ ToolsMenu: "~G~o",
43+ RootMarkers: []string{"go.mod"},
44+ Server: profile.Server{Command: "gopls", Args: []string{"serve"}, },
45+ Templates: profile.Templates{Settings: , Snippets: , Tools: },
46+}
47+```
48+
49+Tout ce qui était un `"turbo-go"`, un `"gopls"` ou un `"go.mod"` en dur quelque part dans onze mille lignes est ici un champ. La bibliothèque les lit ; rien dans la bibliothèque ne sait ce qu'ils veulent dire.
50+
51+`Slug` porte plus qu'il n'y paraît. Le binaire est `turbo-go`, le répertoire de projet est `.turbo-go`, la configuration de l'utilisateur vit dans `~/.config/turbo-go`, et les variables d'environnement qui la remplacent sont `TURBO_GO_THEME_DIR` et `TURBO_GO_SNIPPET_DIR` — toutes dérivées de ce seul mot. Ces noms sont inchangés par le refactoring, et délibérément : quelqu'un qui a posé `TURBO_GO_THEME_DIR` l'a fait face à un binaire publié.
52+
53+## Pourquoi l'analyseur Go est ici et pas dans la bibliothèque
54+
55+turbo-core colore huit langages lui-même : TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell. Ce sont ceux que tout éditeur rencontre quel que soit son objet — la configuration d'un projet est du TOML ou du YAML, sa documentation du Markdown, ses scripts du shell, sa construction d'image un Dockerfile.
56+
57+Go n'en fait pas partie. Le langage qui *définit* un éditeur est enregistré par cet éditeur, ce qui explique qu'un fichier `.rs` s'ouvre ici en texte brut et qu'un fichier `.go` s'ouvre en texte brut dans Turbo Rust.
58+
59+L'analyseur Go est aussi celui qui ressemble le *moins* aux autres. Tous les langages de la bibliothèque sont analysés ligne à ligne avec `syntax.LineScanner` ; Go passe par `go/scanner`, l'analyseur lexical qu'utilise la chaîne d'outils Go elle-même, et convertit ses décalages en octets avec `syntax.LineIndex`. Si la bibliothèque accepte les deux formes, c'est à cause de cet analyseur.
60+
61+## Ce qui a bougé, et ce qui n'a pas bougé
62+
63+| Était | Est |
64+| --- | --- |
65+| `internal/buffer`, `internal/ui`, `internal/editor`, … | `turbo-core/buffer`, `turbo-core/ui`, `turbo-core/editor`, … |
66+| `internal/syntax` — six langages | `turbo-core/syntax` — huit, plus un registre ; Go vit ici |
67+| `internal/app` avec une constante `Name` | `turbo-core/app` recevant un `profile.Profile` |
68+| `internal/lsp` codant gopls en dur | `turbo-core/lsp` recevant un `profile.Server` |
69+| `moduleRoot` dans `main.go` | `app.ProjectRoot(p, files)`, avec `go.mod` dans le profil |
70+| `settings.DirName = ".turbo-go"` | `p.ProjectDir()` |
71+
72+**Rien du comportement de l'éditeur n'a changé.** Les menus, les touches, les thèmes, les formats de fichiers et les variables d'environnement sont ce qu'ils étaient. Ce qui a changé, c'est l'endroit où le code habite.
73+
74+## Pourquoi une bibliothèque plutôt qu'un fork
75+
76+L'alternative à l'extraction de turbo-core était de copier Turbo Go et d'en changer les parties Go. Elle a été rejetée avant d'être entreprise : deux copies de onze mille lignes divergent en un mois, et chaque correctif doit être fait deux fois par quelqu'un qui se souvient qu'il y en a deux.
77+
78+Le coût, accepté : une modification d'un menu touche désormais tous les éditeurs à la fois, et Turbo Go ne peut plus prendre une décision qui n'arrange que Go sans soit la mettre dans le profil, soit la défendre dans la bibliothèque. C'est une vraie contrainte, et c'est elle qui fait que les éditeurs restent le même éditeur.
79+
80+## Comment cela se relie au reste
81+
82+- Ce que fait chaque paquet de la bibliothèque : [la référence des paquets de turbo-core](https://rickub.com/turbo-editors/turbo-core/blob/main/docs/fr/reference/packages.md)
83+- Comment marche la coloration ici : [Coloration et complétion](colouring-and-completion.md)
84+- Pourquoi le menu d'outils est une donnée : [Outils Go](go-tools.md)
85+- Les décisions qui ont survécu au refactoring : [Décisions de conception](design-decisions.md)
added docs/fr/explanation/colouring-and-completion.md +122 -0
new file mode 100644
@@ -0,0 +1,122 @@
1+# Coloration et complétion — explication
2+
3+## De quoi s'agit-il ?
4+
5+Les deux fonctionnalités qui font de Turbo Go un éditeur *pour Go* plutôt qu'un éditeur de texte qui se trouve être écrit en Go : la coloration syntaxique et la complétion par un serveur de langage. Elles fonctionnent de façons très différentes, et cette différence est instructive.
6+
7+## Coloration : le tokeniseur du compilateur lui-même
8+
9+Turbo Go n'a pas de définition de syntaxe. Il appelle `go/scanner` — l'analyseur lexical qu'utilise la chaîne d'outils Go elle-même — et transforme les jetons obtenus en intervalles colorés.
10+
11+Cela signifie que les mots-clés, les littéraux et les opérateurs sont reconnus **exactement** comme le compilateur les reconnaît. Les chaînes brutes, l'insertion automatique de points-virgules, les séparateurs `0x_FF`, tout. Il n'y a aucune expression régulière à se tromper subtilement, ni aucune table à mettre à jour quand le langage évolue.
12+
13+### La tolérance est tout l'enjeu
14+
15+Le source sous un curseur est syntaxiquement invalide la plupart du temps pendant qu'on le tape. Une chaîne à moitié écrite, une accolade non fermée, un identifiant interrompu au milieu. Un coloriseur qui abandonne devant une entrée invalide est un coloriseur qui s'éteint précisément au moment où on le regarde.
16+
17+L'analyseur tourne donc dans son mode le plus indulgent et **toute erreur de syntaxe est écartée**. Il rend malgré tout des jetons exploitables : une chaîne non terminée revient comme une chaîne allant jusqu'à la fin de la ligne, un `/*` non fermé comme un commentaire allant jusqu'à la fin du fichier. Ce qui est précisément le comportement souhaité — les couleurs restent stables, et elles indiquent ce qui ne va pas.
18+
19+### Ce que l'éditeur ajoute
20+
21+Trois distinctions que l'analyseur ne fait pas, parce qu'elles relèvent de la lecture et non de l'analyse :
22+
23+- un identifiant avant `(`, ou après `func`, est une **fonction**
24+- un identifiant après `type`, `struct` ou `interface` est un **type**
25+- parenthèses, virgules, points et points-virgules sont de la **ponctuation**, séparée des opérateurs qui calculent, pour qu'un thème puisse les atténuer
26+
27+Les noms prédéclarés — `int`, `error`, `nil`, `len`, `min` — sont reconnus par leur nom, pas comme mots-clés, parce qu'ils n'en sont pas : un fichier peut les masquer, et les colorer quand même comme les prédéclarés est ce que font tous les autres éditeurs Go.
28+
29+### Pourquoi c'est assez rapide pour être fait naïvement
30+
31+Analyser tout le fichier à chaque frappe paraît coûteux, et le serait. Cela n'arrive pas : le tampon tient un compteur de révision, incrémenté à chaque modification, et le coloriseur ne réanalyse que lorsque ce nombre a bougé. L'éditeur se redessine bien plus souvent que le texte ne change — chaque déplacement du curseur, chaque défilement — et tous ces redessins sont gratuits.
32+
33+### Le prix de ce choix
34+
35+**Un langage n'est coloré que si quelqu'un a écrit un scanner pour lui.** Il n'existe aucun langage de définition dans lequel écrire une définition : chacun est du code Go.
36+
37+Huit d'entre eux — TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell — vivent dans turbo-core, parce que tout éditeur bâti dessus les rencontre quel que soit son langage. L'analyseur Go vit ici, dans `internal/golang`, et est enregistré au démarrage par `syntax.Register`. C'est pourquoi un fichier `.rs` s'ouvre en texte brut dans Turbo Go : cet éditeur enregistre Go et rien d'autre.
38+
39+C'est une vraie limitation, et elle a été acceptée délibérément : ceci est un éditeur pour Go. Un coloriseur générique aurait apporté une dépendance, un format de définition, et un écart permanent entre « ce que le coloriseur croit que Go est » et ce que Go est.
40+
41+### Les huit autres langages
42+
43+Le TOML est venu en premier, et il a mérité son scanner en étant incontournable : l'éditeur lit deux fichiers TOML — les fichiers de thème et le `.turbo-go/settings.toml` d'un projet — et tous deux sont faits pour être édités dans l'éditeur lui-même. Livrer un fichier de réglages plein de commentaires explicatifs puis l'afficher en gris uniforme aurait été curieux.
44+
45+Markdown, JavaScript, HTML et shell ont suivi pour une raison plus simple : c'est ce qui accompagne le Go dans un projet Go. Un dépôt a un `README.md`, quelques scripts, et souvent une page ou un peu de JavaScript ; un éditeur qui ne colore que les fichiers `.go` vous fait le quitter pour tout le reste. YAML, XML et les Dockerfiles les ont rejoints pour le même motif : ce qu'un projet Go garde à côté de son code est désormais tout aussi souvent un fichier compose, un workflow d'intégration continue ou une construction d'image — et un fichier compose en gris uni est justement celui dont on veut le plus voir la forme. Ils sont désormais partagés : écrits une fois ici, et hérités par tout éditeur bâti sur turbo-core.
46+
47+Chacun fait quelques centaines de lignes et ils partagent une petite mécanique commune — une ligne, une position dedans, et les spans trouvés jusque-là. Ce qu'ils ne partagent **pas**, c'est la moindre tentative de moteur général. Pas de langage de motifs, pas de format de grammaire, pas de table d'expressions régulières : chaque scanner est du Go ordinaire qu'un lecteur peut suivre, et en ajouter un sixième consiste à en écrire un sixième plutôt qu'à apprendre une notation.
48+
49+Ils s'arrêtent à des endroits que la [référence](../reference/languages.md) énonce franchement, et ces arrêts ont été choisis, non subis :
50+
51+- **Pas d'expressions régulières JavaScript.** Distinguer `/x/g` d'une division exige de savoir si le token précédent pouvait terminer une expression. Une mauvaise supposition colore le reste de la ligne comme une chaîne — un échec bien plus bruyant que de laisser une regex à la couleur d'un opérateur.
52+- **Pas de heredocs shell.** Suivre `<<EOF` jusqu'à son délimiteur signifie porter un mot arbitraire d'une ligne à l'autre, avec en plus les graphies `<<-` et à délimiteur cité, pour une construction qui est en général quelques lignes de texte brut.
53+- **Pas de JavaScript dans `<script>`.** Cela demande de suivre un élément sur plusieurs lignes et de remapper les colonnes d'un autre scanner — et le même argument réclamerait ensuite le CSS.
54+- **Pas de langage dans une clôture Markdown.** ```` ```go ```` est d'une seule couleur. Le colorer correctement suppose que chaque scanner soit atteignable depuis tous les autres, ce qui est le début du moteur général que ce paquet n'a pas.
55+
56+La règle qui les unit est qu'un scanner **ne devine rien**. Là où une construction ne peut être reconnue sans en savoir plus que ce qu'une ligne contient, elle est laissée telle quelle plutôt qu'approximée : un coloriseur qui se trompe est pire qu'un coloriseur discret.
57+
58+### Cinq classes dont Go n'a rien à dire
59+
60+Les douze classes que produit le tokeniseur Go couvrent presque entièrement les huit autres langages — une chaîne est une chaîne dans tous. Cinq choses n'avaient pas de place : un titre Markdown, ses emphases et ses liens, une balise et un attribut HTML.
61+
62+Réutiliser les classes existantes était l'option la moins chère, et c'est celle retenue pour le TOML, où un en-tête de table se lit réellement comme un type et une clé comme un identifiant. Elle ne tient pas pour les langages de balisage : un titre n'est pas un mot-clé, une balise non plus, et un thème qui voudrait des titres discrets et des mots-clés voyants ne pourrait pas le dire. D'où `syntax.heading`, `syntax.tag`, `syntax.attribute`, `syntax.emphasis` et `syntax.link`.
63+
64+Le coût est réel et pèse sur les thèmes écrits ailleurs : un thème qui n'en définit aucune retombe le long des points sur `syntax` puis `default`, donc le Markdown reste lisible mais ses titres ne se distinguent pas. Chaque thème livré définit les cinq, et un test échoue si l'un cesse de le faire.
65+
66+## Complétion : le programme de quelqu'un d'autre
67+
68+La complétion fonctionne à l'inverse. Turbo Go ne connaît rien au système de types de Go et n'essaie pas : il demande à `gopls`, via le Language Server Protocol, et dessine la réponse.
69+
70+### Optionnelle, et pas par hasard
71+
72+L'éditeur est pleinement utilisable sans serveur de langage. Pas dégradé — le tampon, la coloration, les thèmes, les fenêtres, la recherche, tout fonctionne à l'identique. Seules la complétion, la description de symbole et le saut à la définition manquent, et la barre d'état le dit avec l'unique commande qui corrige la situation.
73+
74+C'est garanti par construction plutôt que par discipline. `Language` enveloppe toute la conversation, et sans serveur chaque méthode ne fait strictement rien. Il n'y a aucun `if server != nil` ailleurs dans l'éditeur, parce qu'il n'y a rien à vérifier.
75+
76+### Le piège du protocole
77+
78+Le protocole compte les colonnes en **unités de code UTF-16**. L'éditeur les compte en runes. En ASCII, les deux donnent le même nombre — ce qui est exactement pourquoi se tromper là-dessus survit aux tests, jusqu'à ce que quelqu'un ouvre un fichier comportant un accent dans un commentaire, et que chaque complétion en aval tombe une colonne à côté.
79+
80+Chaque position qui franchit cette frontière est donc convertie, et la conversion est testée avec une clef de sol, qui nécessite une paire de substitution et compte donc pour deux.
81+
82+### L'autre piège
83+
84+`gopls` pose des questions à son client. Au démarrage, il demande `workspace/configuration` — et **attend la réponse**. Un client qui ne fait qu'émettre des requêtes et lire des réponses ne termine jamais son initialisation, et se bloque sans la moindre erreur.
85+
86+La connexion route donc les requêtes serveur → client vers un gestionnaire, et le client répond par une configuration vide, ce qui signifie « utilise tes valeurs par défaut ».
87+
88+### Toujours bornée
89+
90+Chaque requête a une échéance : trois secondes pour une complétion, trente pour la poignée de main, car un `gopls` froid a un graphe de modules à charger avant de pouvoir dire bonjour. Un serveur qui cesse de répondre ralentit l'éditeur et ne l'arrête jamais.
91+
92+Une complétion qui arrive après que vous ayez tapé trois caractères de plus n'est pas une complétion, c'est une interruption — c'est pourquoi la requête est faite de façon synchrone et abandonnée rapidement, plutôt que livrée en retard.
93+
94+## Neuf questions, une seule connexion
95+
96+La complétion est la chose la plus bruyante que fasse le serveur de langage, et la moins instructive. La même connexion en répond huit autres, qui se répartissent en trois sortes selon ce qui revient.
97+
98+**Quelque chose à lire.** `hover` — qu'est-ce que c'est ? — dessiné dans une boîte.
99+
100+**Des lieux dans le code.** `definition`, `typeDefinition`, `implementation`, `references`. Une requête chacune, une seule forme de réponse pour toutes, ce qui explique qu'elles ne soient qu'une fonction en dessous. Un lieu unique est ouvert ; plusieurs sont proposés en liste, parce qu'une réponse unique est l'exception plutôt que la règle — une interface Go a autant de définitions qu'elle a d'implémentations, et cet éditeur a longtemps pris la première en jetant les autres.
101+
102+**Des noms.** `documentSymbol` pour le plan d'un fichier, `workspace/symbol` pour une recherche dans tout le projet. Le protocole a trois formes pour un symbole et l'éditeur en veut une : l'aplatissement se fait donc là où les réponses arrivent, pas là où elles sont dessinées.
103+
104+Et une chose que personne ne demande : **`publishDiagnostics` arrive de lui-même**, dès que le serveur a un avis, pour tous les fichiers qu'il a chargés — le plus souvent davantage que celui qu'on a devant soi. C'est pourquoi Problems liste tous les fichiers et pas seulement le courant, et pourquoi la marque dans la gouttière apparaît sans qu'on ait appuyé sur quoi que ce soit.
105+
106+L'éditeur ne demande rien de tout cela avant que le serveur ne se dise prêt, et dit laquelle des deux situations s'applique quand une question reste sans réponse. « Rien trouvé » et « je n'ai pas fini de charger » sont la même réponse vide et une nouvelle très différente ; les confondre est la façon la plus déroutante dont la complétion ait jamais échoué ici, et les huit autres en auraient hérité gratuitement.
107+
108+## Deux fonctionnalités, deux formes
109+
110+Il vaut la peine de remarquer pourquoi elles ont fini si différentes.
111+
112+La coloration doit être **instantanée et toujours assez juste**, sur un texte le plus souvent invalide. Cela appelle une réponse locale, tolérante et bon marché — et le tokeniseur est déjà dans la bibliothèque standard.
113+
114+La complétion doit être **occasionnellement juste sur tout le programme**, dépendances comprises. C'est le travail d'un compilateur, c'est coûteux, et c'est déjà résolu par un programme qui ne fait que cela.
115+
116+La première méritait d'être écrite. La seconde méritait d'être demandée.
117+
118+## Liens avec le reste
119+
120+- Où vivent ces deux mécanismes dans le code : [Architecture](architecture.md)
121+- La politique de dépendances qui a façonné les deux : [Décisions de conception](design-decisions.md)
122+- Faire fonctionner la complétion : [Activer la complétion Go](../how-to/enable-completion.md)
new file mode 100644
@@ -0,0 +1,122 @@
1+# Coloration et complétion — explication
2+
3+## De quoi s'agit-il ?
4+
5+Les deux fonctionnalités qui font de Turbo Go un éditeur *pour Go* plutôt qu'un éditeur de texte qui se trouve être écrit en Go : la coloration syntaxique et la complétion par un serveur de langage. Elles fonctionnent de façons très différentes, et cette différence est instructive.
6+
7+## Coloration : le tokeniseur du compilateur lui-même
8+
9+Turbo Go n'a pas de définition de syntaxe. Il appelle `go/scanner` — l'analyseur lexical qu'utilise la chaîne d'outils Go elle-même — et transforme les jetons obtenus en intervalles colorés.
10+
11+Cela signifie que les mots-clés, les littéraux et les opérateurs sont reconnus **exactement** comme le compilateur les reconnaît. Les chaînes brutes, l'insertion automatique de points-virgules, les séparateurs `0x_FF`, tout. Il n'y a aucune expression régulière à se tromper subtilement, ni aucune table à mettre à jour quand le langage évolue.
12+
13+### La tolérance est tout l'enjeu
14+
15+Le source sous un curseur est syntaxiquement invalide la plupart du temps pendant qu'on le tape. Une chaîne à moitié écrite, une accolade non fermée, un identifiant interrompu au milieu. Un coloriseur qui abandonne devant une entrée invalide est un coloriseur qui s'éteint précisément au moment où on le regarde.
16+
17+L'analyseur tourne donc dans son mode le plus indulgent et **toute erreur de syntaxe est écartée**. Il rend malgré tout des jetons exploitables : une chaîne non terminée revient comme une chaîne allant jusqu'à la fin de la ligne, un `/*` non fermé comme un commentaire allant jusqu'à la fin du fichier. Ce qui est précisément le comportement souhaité — les couleurs restent stables, et elles indiquent ce qui ne va pas.
18+
19+### Ce que l'éditeur ajoute
20+
21+Trois distinctions que l'analyseur ne fait pas, parce qu'elles relèvent de la lecture et non de l'analyse :
22+
23+- un identifiant avant `(`, ou après `func`, est une **fonction**
24+- un identifiant après `type`, `struct` ou `interface` est un **type**
25+- parenthèses, virgules, points et points-virgules sont de la **ponctuation**, séparée des opérateurs qui calculent, pour qu'un thème puisse les atténuer
26+
27+Les noms prédéclarés — `int`, `error`, `nil`, `len`, `min` — sont reconnus par leur nom, pas comme mots-clés, parce qu'ils n'en sont pas : un fichier peut les masquer, et les colorer quand même comme les prédéclarés est ce que font tous les autres éditeurs Go.
28+
29+### Pourquoi c'est assez rapide pour être fait naïvement
30+
31+Analyser tout le fichier à chaque frappe paraît coûteux, et le serait. Cela n'arrive pas : le tampon tient un compteur de révision, incrémenté à chaque modification, et le coloriseur ne réanalyse que lorsque ce nombre a bougé. L'éditeur se redessine bien plus souvent que le texte ne change — chaque déplacement du curseur, chaque défilement — et tous ces redessins sont gratuits.
32+
33+### Le prix de ce choix
34+
35+**Un langage n'est coloré que si quelqu'un a écrit un scanner pour lui.** Il n'existe aucun langage de définition dans lequel écrire une définition : chacun est du code Go.
36+
37+Huit d'entre eux — TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell — vivent dans turbo-core, parce que tout éditeur bâti dessus les rencontre quel que soit son langage. L'analyseur Go vit ici, dans `internal/golang`, et est enregistré au démarrage par `syntax.Register`. C'est pourquoi un fichier `.rs` s'ouvre en texte brut dans Turbo Go : cet éditeur enregistre Go et rien d'autre.
38+
39+C'est une vraie limitation, et elle a été acceptée délibérément : ceci est un éditeur pour Go. Un coloriseur générique aurait apporté une dépendance, un format de définition, et un écart permanent entre « ce que le coloriseur croit que Go est » et ce que Go est.
40+
41+### Les huit autres langages
42+
43+Le TOML est venu en premier, et il a mérité son scanner en étant incontournable : l'éditeur lit deux fichiers TOML — les fichiers de thème et le `.turbo-go/settings.toml` d'un projet — et tous deux sont faits pour être édités dans l'éditeur lui-même. Livrer un fichier de réglages plein de commentaires explicatifs puis l'afficher en gris uniforme aurait été curieux.
44+
45+Markdown, JavaScript, HTML et shell ont suivi pour une raison plus simple : c'est ce qui accompagne le Go dans un projet Go. Un dépôt a un `README.md`, quelques scripts, et souvent une page ou un peu de JavaScript ; un éditeur qui ne colore que les fichiers `.go` vous fait le quitter pour tout le reste. YAML, XML et les Dockerfiles les ont rejoints pour le même motif : ce qu'un projet Go garde à côté de son code est désormais tout aussi souvent un fichier compose, un workflow d'intégration continue ou une construction d'image — et un fichier compose en gris uni est justement celui dont on veut le plus voir la forme. Ils sont désormais partagés : écrits une fois ici, et hérités par tout éditeur bâti sur turbo-core.
46+
47+Chacun fait quelques centaines de lignes et ils partagent une petite mécanique commune — une ligne, une position dedans, et les spans trouvés jusque-là. Ce qu'ils ne partagent **pas**, c'est la moindre tentative de moteur général. Pas de langage de motifs, pas de format de grammaire, pas de table d'expressions régulières : chaque scanner est du Go ordinaire qu'un lecteur peut suivre, et en ajouter un sixième consiste à en écrire un sixième plutôt qu'à apprendre une notation.
48+
49+Ils s'arrêtent à des endroits que la [référence](../reference/languages.md) énonce franchement, et ces arrêts ont été choisis, non subis :
50+
51+- **Pas d'expressions régulières JavaScript.** Distinguer `/x/g` d'une division exige de savoir si le token précédent pouvait terminer une expression. Une mauvaise supposition colore le reste de la ligne comme une chaîne — un échec bien plus bruyant que de laisser une regex à la couleur d'un opérateur.
52+- **Pas de heredocs shell.** Suivre `<<EOF` jusqu'à son délimiteur signifie porter un mot arbitraire d'une ligne à l'autre, avec en plus les graphies `<<-` et à délimiteur cité, pour une construction qui est en général quelques lignes de texte brut.
53+- **Pas de JavaScript dans `<script>`.** Cela demande de suivre un élément sur plusieurs lignes et de remapper les colonnes d'un autre scanner — et le même argument réclamerait ensuite le CSS.
54+- **Pas de langage dans une clôture Markdown.** ```` ```go ```` est d'une seule couleur. Le colorer correctement suppose que chaque scanner soit atteignable depuis tous les autres, ce qui est le début du moteur général que ce paquet n'a pas.
55+
56+La règle qui les unit est qu'un scanner **ne devine rien**. Là où une construction ne peut être reconnue sans en savoir plus que ce qu'une ligne contient, elle est laissée telle quelle plutôt qu'approximée : un coloriseur qui se trompe est pire qu'un coloriseur discret.
57+
58+### Cinq classes dont Go n'a rien à dire
59+
60+Les douze classes que produit le tokeniseur Go couvrent presque entièrement les huit autres langages — une chaîne est une chaîne dans tous. Cinq choses n'avaient pas de place : un titre Markdown, ses emphases et ses liens, une balise et un attribut HTML.
61+
62+Réutiliser les classes existantes était l'option la moins chère, et c'est celle retenue pour le TOML, où un en-tête de table se lit réellement comme un type et une clé comme un identifiant. Elle ne tient pas pour les langages de balisage : un titre n'est pas un mot-clé, une balise non plus, et un thème qui voudrait des titres discrets et des mots-clés voyants ne pourrait pas le dire. D'où `syntax.heading`, `syntax.tag`, `syntax.attribute`, `syntax.emphasis` et `syntax.link`.
63+
64+Le coût est réel et pèse sur les thèmes écrits ailleurs : un thème qui n'en définit aucune retombe le long des points sur `syntax` puis `default`, donc le Markdown reste lisible mais ses titres ne se distinguent pas. Chaque thème livré définit les cinq, et un test échoue si l'un cesse de le faire.
65+
66+## Complétion : le programme de quelqu'un d'autre
67+
68+La complétion fonctionne à l'inverse. Turbo Go ne connaît rien au système de types de Go et n'essaie pas : il demande à `gopls`, via le Language Server Protocol, et dessine la réponse.
69+
70+### Optionnelle, et pas par hasard
71+
72+L'éditeur est pleinement utilisable sans serveur de langage. Pas dégradé — le tampon, la coloration, les thèmes, les fenêtres, la recherche, tout fonctionne à l'identique. Seules la complétion, la description de symbole et le saut à la définition manquent, et la barre d'état le dit avec l'unique commande qui corrige la situation.
73+
74+C'est garanti par construction plutôt que par discipline. `Language` enveloppe toute la conversation, et sans serveur chaque méthode ne fait strictement rien. Il n'y a aucun `if server != nil` ailleurs dans l'éditeur, parce qu'il n'y a rien à vérifier.
75+
76+### Le piège du protocole
77+
78+Le protocole compte les colonnes en **unités de code UTF-16**. L'éditeur les compte en runes. En ASCII, les deux donnent le même nombre — ce qui est exactement pourquoi se tromper là-dessus survit aux tests, jusqu'à ce que quelqu'un ouvre un fichier comportant un accent dans un commentaire, et que chaque complétion en aval tombe une colonne à côté.
79+
80+Chaque position qui franchit cette frontière est donc convertie, et la conversion est testée avec une clef de sol, qui nécessite une paire de substitution et compte donc pour deux.
81+
82+### L'autre piège
83+
84+`gopls` pose des questions à son client. Au démarrage, il demande `workspace/configuration` — et **attend la réponse**. Un client qui ne fait qu'émettre des requêtes et lire des réponses ne termine jamais son initialisation, et se bloque sans la moindre erreur.
85+
86+La connexion route donc les requêtes serveur → client vers un gestionnaire, et le client répond par une configuration vide, ce qui signifie « utilise tes valeurs par défaut ».
87+
88+### Toujours bornée
89+
90+Chaque requête a une échéance : trois secondes pour une complétion, trente pour la poignée de main, car un `gopls` froid a un graphe de modules à charger avant de pouvoir dire bonjour. Un serveur qui cesse de répondre ralentit l'éditeur et ne l'arrête jamais.
91+
92+Une complétion qui arrive après que vous ayez tapé trois caractères de plus n'est pas une complétion, c'est une interruption — c'est pourquoi la requête est faite de façon synchrone et abandonnée rapidement, plutôt que livrée en retard.
93+
94+## Neuf questions, une seule connexion
95+
96+La complétion est la chose la plus bruyante que fasse le serveur de langage, et la moins instructive. La même connexion en répond huit autres, qui se répartissent en trois sortes selon ce qui revient.
97+
98+**Quelque chose à lire.** `hover` — qu'est-ce que c'est ? — dessiné dans une boîte.
99+
100+**Des lieux dans le code.** `definition`, `typeDefinition`, `implementation`, `references`. Une requête chacune, une seule forme de réponse pour toutes, ce qui explique qu'elles ne soient qu'une fonction en dessous. Un lieu unique est ouvert ; plusieurs sont proposés en liste, parce qu'une réponse unique est l'exception plutôt que la règle — une interface Go a autant de définitions qu'elle a d'implémentations, et cet éditeur a longtemps pris la première en jetant les autres.
101+
102+**Des noms.** `documentSymbol` pour le plan d'un fichier, `workspace/symbol` pour une recherche dans tout le projet. Le protocole a trois formes pour un symbole et l'éditeur en veut une : l'aplatissement se fait donc là où les réponses arrivent, pas là où elles sont dessinées.
103+
104+Et une chose que personne ne demande : **`publishDiagnostics` arrive de lui-même**, dès que le serveur a un avis, pour tous les fichiers qu'il a chargés — le plus souvent davantage que celui qu'on a devant soi. C'est pourquoi Problems liste tous les fichiers et pas seulement le courant, et pourquoi la marque dans la gouttière apparaît sans qu'on ait appuyé sur quoi que ce soit.
105+
106+L'éditeur ne demande rien de tout cela avant que le serveur ne se dise prêt, et dit laquelle des deux situations s'applique quand une question reste sans réponse. « Rien trouvé » et « je n'ai pas fini de charger » sont la même réponse vide et une nouvelle très différente ; les confondre est la façon la plus déroutante dont la complétion ait jamais échoué ici, et les huit autres en auraient hérité gratuitement.
107+
108+## Deux fonctionnalités, deux formes
109+
110+Il vaut la peine de remarquer pourquoi elles ont fini si différentes.
111+
112+La coloration doit être **instantanée et toujours assez juste**, sur un texte le plus souvent invalide. Cela appelle une réponse locale, tolérante et bon marché — et le tokeniseur est déjà dans la bibliothèque standard.
113+
114+La complétion doit être **occasionnellement juste sur tout le programme**, dépendances comprises. C'est le travail d'un compilateur, c'est coûteux, et c'est déjà résolu par un programme qui ne fait que cela.
115+
116+La première méritait d'être écrite. La seconde méritait d'être demandée.
117+
118+## Liens avec le reste
119+
120+- Où vivent ces deux mécanismes dans le code : [Architecture](architecture.md)
121+- La politique de dépendances qui a façonné les deux : [Décisions de conception](design-decisions.md)
122+- Faire fonctionner la complétion : [Activer la complétion Go](../how-to/enable-completion.md)
added docs/fr/explanation/design-decisions.md +109 -0
new file mode 100644
@@ -0,0 +1,109 @@
1+# Décisions de conception — explication
2+
3+## De quoi s'agit-il ?
4+
5+Les choix qui ont façonné Turbo Go, quelles étaient les alternatives, et pourquoi elles ont été écartées. C'est la page à lire avant de modifier quelque chose qui paraît arbitraire.
6+
7+## Deux dépendances, pas une de plus
8+
9+Turbo Go dépend de `tcell/v2` et de `BurntSushi/toml`. Tout le reste est la bibliothèque standard — y compris le tokeniseur, le client JSON-RPC, le cadrage LSP et la gestion des fichiers.
10+
11+**Ce qui a été écarté.** `go.lsp.dev/jsonrpc2` aurait économisé peut-être trois cents lignes de turbo-core's `lsp`. `rivo/tview` en aurait économisé bien davantage dans turbo-core's `ui`. Une bibliothèque de coloration syntaxique aurait apporté cinquante langages au lieu d'un.
12+
13+**Pourquoi.** Un éditeur est un programme qu'on garde des années et qu'on modifie souvent. Chaque dépendance en est un morceau qu'on ne peut pas modifier, pas tester entièrement, et qu'il faut suivre. Le protocole est assez simple pour être écrit, et l'avoir écrit a placé toute la conversation à un endroit qu'un lecteur peut suivre. Trois cents lignes qu'on comprend valent mieux que trois cents qu'on hérite.
14+
15+L'exception confirme la règle : `tcell` n'est pas une commodité, c'est la base de compatibilité des terminaux, et la réimplémenter ne serait ni un petit travail ni un travail honnête.
16+
17+## Le framework de widgets est écrit à la main
18+
19+`tview` a des widgets. `bubbletea` a une architecture. Aucun des deux n'a ce qu'avait Turbo Vision : des fenêtres déplaçables qui se recouvrent avec des ombres, une barre de menus à lettres d'accès et des dialogues modaux, le tout dessiné en caractères semi-graphiques sur seize couleurs.
20+
21+L'architecture à la Elm de `bubbletea` redessine toute la vue à chaque message. Ce modèle est excellent pour un formulaire et malcommode pour un éditeur plein écran où les fenêtres s'empilent et où le curseur doit se trouver dans une cellule précise.
22+
23+Écrire le framework a coûté environ mille cinq cents lignes. En échange, l'éditeur ressemble à Turbo C plutôt qu'à une interface moderne portant un fond bleu, et chaque décision d'affichage se trouve à un fichier de distance.
24+
25+## Les rectangles sont en coordonnées écran absolues
26+
27+Le `Bounds()` de chaque widget indique où il se trouve réellement sur le terminal, pas où il se trouve par rapport à son parent. Tester si un clic l'atteint est alors un simple test de rectangle, et aucun événement n'a jamais besoin d'être traduit en descendant.
28+
29+**Le coût** est que les conteneurs placent leurs enfants dans l'espace de l'écran. **L'alternative** — des coordonnées relatives avec une traduction à chaque saut — déplace le calcul de la mise en page vers la gestion des événements, où il est fait bien plus souvent et où il est bien plus facile de se tromper. Le découpage se compose quand même correctement, puisqu'un peintre intersecte le découpage de son parent : un enfant dont le calcul est faux ne dessine rien plutôt que de dessiner par-dessus ses voisins.
30+
31+## Toute modification passe par une seule fonction
32+
33+`buffer.ReplaceRange` est le seul endroit où le texte est modifié. Insertion, retour arrière, suppression, indentation, collage et annulation y convergent tous, et c'est le seul endroit où sont maintenus l'historique d'annulation, le drapeau « modifié », le compteur de révision et le curseur.
34+
35+L'alternative — chaque opération tenant sa propre comptabilité — est la façon dont naissent les bugs d'annulation. Il y a exactement une chose à réussir, et elle est testée directement.
36+
37+## Les fenêtres suivent le terminal, elles ne s'y mettent pas à l'échelle
38+
39+Une fenêtre a un **mode de croissance**, qui nomme les bords du bureau qu'elle suit. Une fenêtre de document suit les bords droit et bas : son coin supérieur gauche reste où il est, et son coin opposé se déplace exactement autant que celui du terminal. Une fenêtre qui remplissait le terminal le remplit donc toujours, et une fenêtre que vous aviez décalée garde son décalage.
40+
41+**L'alternative était la mise à l'échelle proportionnelle** — multiplier le rectangle de chaque fenêtre par le rapport des tailles. Elle a été écartée parce qu'elle déplace des fenêtres que l'utilisateur a placées exprès, et parce que les arrondis la rendent destructive : réduisez puis agrandissez, et plus rien n'est où il était. Turbo Vision utilisait des modes de croissance, et c'est toujours la bonne réponse.
42+
43+Quel que soit son mode, une fenêtre est ensuite bornée à la taille du bureau. Une fenêtre plus grande que le bureau qui la contient a des parties que personne ne peut atteindre.
44+
45+
46+## Les cases d'une fenêtre disent ce qu'elles vont faire, pas ce que la fenêtre est
47+
48+Le cadre porte deux cases : `[x]` à gauche ferme la fenêtre, `[■]` à droite lui donne tout le bureau.
49+
50+La case de fermeture était `[■]` — celle de Turbo Vision — et il fallait qu'elle bouge. Deux cases sur un même cadre doivent se distinguer d'un coup d'œil, et un bloc plein se lit bien plus volontiers « remplir l'écran » que « fermer ». `[x]` veut dire fermer depuis trente ans ; le bloc est allé au travail auquel il ressemble.
51+
52+La case d'agrandissement **change avec l'état de la fenêtre** : `[■]` tant qu'il reste de la place, `[▬]` une fois que la fenêtre remplit le bureau. L'autre solution était un symbole fixe, et elle rend le bouton ambigu précisément au moment où l'on en a besoin : on voit bien que la fenêtre est grande, mais pas si l'actionner va l'agrandir encore ou la remettre en place. Un contrôle qui montre son *état* laisse déduire l'action ; un contrôle qui montre son *action*, non.
53+
54+Une fenêtre qui n'a nulle part où s'agrandir n'affiche **aucune case**, plutôt qu'une case sans effet. Seul le bureau sait quelle surface une fenêtre remplirait : une fenêtre qui n'est sur aucun bureau n'a rien à proposer.
55+
56+**Window ▸ Maximise est le même bascule**, pas une action à sens unique. Un menu et un bouton en désaccord sur le sens d'« agrandir » seraient un bug qu'on signale, pas une subtilité qu'on apprécie.
57+
58+## L'annulation fusionne les séries de frappe
59+
60+Taper `func` puis Ctrl-Z retire les quatre lettres. Une série de retours arrière aussi. Déplacer le curseur clôt la série, et la frappe ne fusionne jamais avec l'effacement.
61+
62+L'annulation caractère par caractère est ce que donne une implémentation naïve, et c'est ce que faisait Turbo C lui-même. C'est aussi ce dont plus personne ne veut.
63+
64+## Les thèmes sont en TOML, avec deux formes d'héritage
65+
66+**Entre fichiers**, `inherits` prend les styles résolus du parent comme point de départ. Un thème à vous peut donc tenir en cinq lignes.
67+
68+**Entre clés**, le long des points : `syntax.keyword` retombe sur `syntax`, et `syntax` sur `default`. Cela se produit deux fois — une fois à l'analyse, pour qu'une entrée ne définissant que `fg` hérite de son `bg`, et une fois à la lecture, pour qu'un thème qui ne mentionne jamais `syntax.keyword` colore quand même les mots-clés.
69+
70+C'est cette seconde forme qui fait qu'un thème partiel est un thème utilisable, et c'est pourquoi il n'existe pas de thème laissant la moitié de l'écran non peinte.
71+
72+**Pourquoi TOML plutôt que JSON.** Les commentaires. Un thème est un fichier que l'on modifie à la main et que l'on annote.
73+
74+**Une couleur inconnue est une erreur**, pas un repli silencieux sur la couleur par défaut du terminal. Une faute de frappe qui repeint discrètement la moitié de l'écran est bien plus difficile à trouver qu'une qui le dit au chargement.
75+
76+## Le serveur de langage est optionnel par construction
77+
78+`app.Language` enveloppe toute la conversation avec gopls, et lorsqu'il n'y a pas de serveur, chaque méthode ne fait rien plutôt que d'échouer. Rien d'autre dans l'éditeur ne se demande si un serveur de langage existe.
79+
80+L'alternative — vérifier `nil` à chacun des vingt points d'appel — offre vingt occasions d'oublier. Ici, oublier est impossible : il n'y a rien à vérifier.
81+
82+C'est pourquoi `gopls` n'est ni embarqué, ni téléchargé, ni requis. Il est cherché dans le `PATH` et dans `GOPATH/bin`, et son absence est signalée sur la barre d'état avec l'unique commande qui la corrige.
83+
84+## L'enregistrement est atomique, et fidèle à l'octet près
85+
86+Un enregistrement écrit dans un fichier temporaire du même répertoire puis le renomme sur la cible, en conservant les permissions d'origine. Un enregistrement interrompu ne peut pas laisser un fichier source à moitié écrit.
87+
88+Par ailleurs, les fins de ligne avec lesquelles un fichier a été lu et son saut de ligne final — ou son absence — sont mémorisés : ouvrir puis enregistrer un fichier non modifié le reproduit octet pour octet. Un éditeur qui normalise silencieusement les fins de ligne transforme une modification d'une ligne en un diff du fichier entier.
89+
90+## Le presse-papier est celui de l'éditeur
91+
92+Un programme en terminal ne peut pas lire le presse-papier du système de façon portable. Plutôt que de faire semblant, Turbo Go partage un presse-papier entre ses propres fenêtres, ce que faisait Turbo C.
93+
94+## La version est une propriété du build, pas des sources
95+
96+La version était autrefois `const Version = "0.1.0"` dans a subdirectory. Elle était juste le jour où elle a été écrite et fausse pendant les quatorze commits suivants, parce que rien dans le fait de valider, taguer ou installer ne touche à une constante Go. Une boîte About, c'est ce que l'on regarde au moment de signaler un bug ; un numéro qui y nomme une release que le binaire n'est pas est pire que pas de numéro, parce qu'on le croit.
97+
98+Le numéro est donc pris au build. L'éditeur de liens estampille `git describe --tags --dirty` dans turbo-core's `version` package depuis le Makefile et depuis l'installeur, ce qui fait que `make install` produit un éditeur qui nomme le commit dont il vient. Quand rien ne l'a estampillé, le binaire interroge `runtime/debug.ReadBuildInfo()`, qui couvre le seul chemin impossible à estampiller : `go install rickub.com/turbo-editors/turbo-go@v0.2.0`, où aucun Makefile n'intervient et où l'outil Go connaît la version du module. Ce n'est que si les deux se taisent qu'il annonce `unknown` — délibérément pas un numéro, puisque l'échec contre lequel tout ceci est conçu est justement une version vraisemblable que personne n'a posée.
99+
100+Deux limites du système de build expliquent le reste de la conception. **Il ne lit pas les tags git**, donc un `go build .` nu ne pourra jamais annoncer `0.1.0-14-g88a4c38`, si astucieux que soit le code ; il annonce `devel` plus le commit, et la documentation le dit plutôt que de laisser croire que tous les builds se valent. Et ce qu'il annonce *effectivement* pour un tel build est une **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — affichée comme `devel` à la place, parce que son `0.1.1` est un correctif qui n'existe pas et serait lu comme tel.
101+
102+`vcs.time` est délibérément inutilisé. C'est l'horodatage du commit, et tout binaire est lié après le commit dont il provient : l'étiqueter « Built » serait faux sur chacun d'eux. Une date de build ne s'affiche que si un build en a réellement estampillé une, la même règle que suit la boîte About de bout en bout : **un fait que personne n'a enregistré n'a pas de ligne**, plutôt qu'une ligne vide qui se lit comme un échec à la remplir.
103+
104+Rejeté : une cible `make release` qui tague, construit et pousse. Publier tient en trois commandes git, et les emballer masque laquelle a échoué ; l'estampillage de la version était la partie qu'on ne pouvait pas faire à la main de façon fiable, et c'est celle qui a été automatisée.
105+
106+## Liens avec le reste
107+
108+- Ce que sont les paquets et comment ils s'articulent : [Architecture](architecture.md)
109+- Comment fonctionnent la coloration et la complétion : [Coloration et complétion](colouring-and-completion.md)
new file mode 100644
@@ -0,0 +1,109 @@
1+# Décisions de conception — explication
2+
3+## De quoi s'agit-il ?
4+
5+Les choix qui ont façonné Turbo Go, quelles étaient les alternatives, et pourquoi elles ont été écartées. C'est la page à lire avant de modifier quelque chose qui paraît arbitraire.
6+
7+## Deux dépendances, pas une de plus
8+
9+Turbo Go dépend de `tcell/v2` et de `BurntSushi/toml`. Tout le reste est la bibliothèque standard — y compris le tokeniseur, le client JSON-RPC, le cadrage LSP et la gestion des fichiers.
10+
11+**Ce qui a été écarté.** `go.lsp.dev/jsonrpc2` aurait économisé peut-être trois cents lignes de turbo-core's `lsp`. `rivo/tview` en aurait économisé bien davantage dans turbo-core's `ui`. Une bibliothèque de coloration syntaxique aurait apporté cinquante langages au lieu d'un.
12+
13+**Pourquoi.** Un éditeur est un programme qu'on garde des années et qu'on modifie souvent. Chaque dépendance en est un morceau qu'on ne peut pas modifier, pas tester entièrement, et qu'il faut suivre. Le protocole est assez simple pour être écrit, et l'avoir écrit a placé toute la conversation à un endroit qu'un lecteur peut suivre. Trois cents lignes qu'on comprend valent mieux que trois cents qu'on hérite.
14+
15+L'exception confirme la règle : `tcell` n'est pas une commodité, c'est la base de compatibilité des terminaux, et la réimplémenter ne serait ni un petit travail ni un travail honnête.
16+
17+## Le framework de widgets est écrit à la main
18+
19+`tview` a des widgets. `bubbletea` a une architecture. Aucun des deux n'a ce qu'avait Turbo Vision : des fenêtres déplaçables qui se recouvrent avec des ombres, une barre de menus à lettres d'accès et des dialogues modaux, le tout dessiné en caractères semi-graphiques sur seize couleurs.
20+
21+L'architecture à la Elm de `bubbletea` redessine toute la vue à chaque message. Ce modèle est excellent pour un formulaire et malcommode pour un éditeur plein écran où les fenêtres s'empilent et où le curseur doit se trouver dans une cellule précise.
22+
23+Écrire le framework a coûté environ mille cinq cents lignes. En échange, l'éditeur ressemble à Turbo C plutôt qu'à une interface moderne portant un fond bleu, et chaque décision d'affichage se trouve à un fichier de distance.
24+
25+## Les rectangles sont en coordonnées écran absolues
26+
27+Le `Bounds()` de chaque widget indique où il se trouve réellement sur le terminal, pas où il se trouve par rapport à son parent. Tester si un clic l'atteint est alors un simple test de rectangle, et aucun événement n'a jamais besoin d'être traduit en descendant.
28+
29+**Le coût** est que les conteneurs placent leurs enfants dans l'espace de l'écran. **L'alternative** — des coordonnées relatives avec une traduction à chaque saut — déplace le calcul de la mise en page vers la gestion des événements, où il est fait bien plus souvent et où il est bien plus facile de se tromper. Le découpage se compose quand même correctement, puisqu'un peintre intersecte le découpage de son parent : un enfant dont le calcul est faux ne dessine rien plutôt que de dessiner par-dessus ses voisins.
30+
31+## Toute modification passe par une seule fonction
32+
33+`buffer.ReplaceRange` est le seul endroit où le texte est modifié. Insertion, retour arrière, suppression, indentation, collage et annulation y convergent tous, et c'est le seul endroit où sont maintenus l'historique d'annulation, le drapeau « modifié », le compteur de révision et le curseur.
34+
35+L'alternative — chaque opération tenant sa propre comptabilité — est la façon dont naissent les bugs d'annulation. Il y a exactement une chose à réussir, et elle est testée directement.
36+
37+## Les fenêtres suivent le terminal, elles ne s'y mettent pas à l'échelle
38+
39+Une fenêtre a un **mode de croissance**, qui nomme les bords du bureau qu'elle suit. Une fenêtre de document suit les bords droit et bas : son coin supérieur gauche reste où il est, et son coin opposé se déplace exactement autant que celui du terminal. Une fenêtre qui remplissait le terminal le remplit donc toujours, et une fenêtre que vous aviez décalée garde son décalage.
40+
41+**L'alternative était la mise à l'échelle proportionnelle** — multiplier le rectangle de chaque fenêtre par le rapport des tailles. Elle a été écartée parce qu'elle déplace des fenêtres que l'utilisateur a placées exprès, et parce que les arrondis la rendent destructive : réduisez puis agrandissez, et plus rien n'est où il était. Turbo Vision utilisait des modes de croissance, et c'est toujours la bonne réponse.
42+
43+Quel que soit son mode, une fenêtre est ensuite bornée à la taille du bureau. Une fenêtre plus grande que le bureau qui la contient a des parties que personne ne peut atteindre.
44+
45+
46+## Les cases d'une fenêtre disent ce qu'elles vont faire, pas ce que la fenêtre est
47+
48+Le cadre porte deux cases : `[x]` à gauche ferme la fenêtre, `[■]` à droite lui donne tout le bureau.
49+
50+La case de fermeture était `[■]` — celle de Turbo Vision — et il fallait qu'elle bouge. Deux cases sur un même cadre doivent se distinguer d'un coup d'œil, et un bloc plein se lit bien plus volontiers « remplir l'écran » que « fermer ». `[x]` veut dire fermer depuis trente ans ; le bloc est allé au travail auquel il ressemble.
51+
52+La case d'agrandissement **change avec l'état de la fenêtre** : `[■]` tant qu'il reste de la place, `[▬]` une fois que la fenêtre remplit le bureau. L'autre solution était un symbole fixe, et elle rend le bouton ambigu précisément au moment où l'on en a besoin : on voit bien que la fenêtre est grande, mais pas si l'actionner va l'agrandir encore ou la remettre en place. Un contrôle qui montre son *état* laisse déduire l'action ; un contrôle qui montre son *action*, non.
53+
54+Une fenêtre qui n'a nulle part où s'agrandir n'affiche **aucune case**, plutôt qu'une case sans effet. Seul le bureau sait quelle surface une fenêtre remplirait : une fenêtre qui n'est sur aucun bureau n'a rien à proposer.
55+
56+**Window ▸ Maximise est le même bascule**, pas une action à sens unique. Un menu et un bouton en désaccord sur le sens d'« agrandir » seraient un bug qu'on signale, pas une subtilité qu'on apprécie.
57+
58+## L'annulation fusionne les séries de frappe
59+
60+Taper `func` puis Ctrl-Z retire les quatre lettres. Une série de retours arrière aussi. Déplacer le curseur clôt la série, et la frappe ne fusionne jamais avec l'effacement.
61+
62+L'annulation caractère par caractère est ce que donne une implémentation naïve, et c'est ce que faisait Turbo C lui-même. C'est aussi ce dont plus personne ne veut.
63+
64+## Les thèmes sont en TOML, avec deux formes d'héritage
65+
66+**Entre fichiers**, `inherits` prend les styles résolus du parent comme point de départ. Un thème à vous peut donc tenir en cinq lignes.
67+
68+**Entre clés**, le long des points : `syntax.keyword` retombe sur `syntax`, et `syntax` sur `default`. Cela se produit deux fois — une fois à l'analyse, pour qu'une entrée ne définissant que `fg` hérite de son `bg`, et une fois à la lecture, pour qu'un thème qui ne mentionne jamais `syntax.keyword` colore quand même les mots-clés.
69+
70+C'est cette seconde forme qui fait qu'un thème partiel est un thème utilisable, et c'est pourquoi il n'existe pas de thème laissant la moitié de l'écran non peinte.
71+
72+**Pourquoi TOML plutôt que JSON.** Les commentaires. Un thème est un fichier que l'on modifie à la main et que l'on annote.
73+
74+**Une couleur inconnue est une erreur**, pas un repli silencieux sur la couleur par défaut du terminal. Une faute de frappe qui repeint discrètement la moitié de l'écran est bien plus difficile à trouver qu'une qui le dit au chargement.
75+
76+## Le serveur de langage est optionnel par construction
77+
78+`app.Language` enveloppe toute la conversation avec gopls, et lorsqu'il n'y a pas de serveur, chaque méthode ne fait rien plutôt que d'échouer. Rien d'autre dans l'éditeur ne se demande si un serveur de langage existe.
79+
80+L'alternative — vérifier `nil` à chacun des vingt points d'appel — offre vingt occasions d'oublier. Ici, oublier est impossible : il n'y a rien à vérifier.
81+
82+C'est pourquoi `gopls` n'est ni embarqué, ni téléchargé, ni requis. Il est cherché dans le `PATH` et dans `GOPATH/bin`, et son absence est signalée sur la barre d'état avec l'unique commande qui la corrige.
83+
84+## L'enregistrement est atomique, et fidèle à l'octet près
85+
86+Un enregistrement écrit dans un fichier temporaire du même répertoire puis le renomme sur la cible, en conservant les permissions d'origine. Un enregistrement interrompu ne peut pas laisser un fichier source à moitié écrit.
87+
88+Par ailleurs, les fins de ligne avec lesquelles un fichier a été lu et son saut de ligne final — ou son absence — sont mémorisés : ouvrir puis enregistrer un fichier non modifié le reproduit octet pour octet. Un éditeur qui normalise silencieusement les fins de ligne transforme une modification d'une ligne en un diff du fichier entier.
89+
90+## Le presse-papier est celui de l'éditeur
91+
92+Un programme en terminal ne peut pas lire le presse-papier du système de façon portable. Plutôt que de faire semblant, Turbo Go partage un presse-papier entre ses propres fenêtres, ce que faisait Turbo C.
93+
94+## La version est une propriété du build, pas des sources
95+
96+La version était autrefois `const Version = "0.1.0"` dans a subdirectory. Elle était juste le jour où elle a été écrite et fausse pendant les quatorze commits suivants, parce que rien dans le fait de valider, taguer ou installer ne touche à une constante Go. Une boîte About, c'est ce que l'on regarde au moment de signaler un bug ; un numéro qui y nomme une release que le binaire n'est pas est pire que pas de numéro, parce qu'on le croit.
97+
98+Le numéro est donc pris au build. L'éditeur de liens estampille `git describe --tags --dirty` dans turbo-core's `version` package depuis le Makefile et depuis l'installeur, ce qui fait que `make install` produit un éditeur qui nomme le commit dont il vient. Quand rien ne l'a estampillé, le binaire interroge `runtime/debug.ReadBuildInfo()`, qui couvre le seul chemin impossible à estampiller : `go install rickub.com/turbo-editors/turbo-go@v0.2.0`, où aucun Makefile n'intervient et où l'outil Go connaît la version du module. Ce n'est que si les deux se taisent qu'il annonce `unknown` — délibérément pas un numéro, puisque l'échec contre lequel tout ceci est conçu est justement une version vraisemblable que personne n'a posée.
99+
100+Deux limites du système de build expliquent le reste de la conception. **Il ne lit pas les tags git**, donc un `go build .` nu ne pourra jamais annoncer `0.1.0-14-g88a4c38`, si astucieux que soit le code ; il annonce `devel` plus le commit, et la documentation le dit plutôt que de laisser croire que tous les builds se valent. Et ce qu'il annonce *effectivement* pour un tel build est une **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — affichée comme `devel` à la place, parce que son `0.1.1` est un correctif qui n'existe pas et serait lu comme tel.
101+
102+`vcs.time` est délibérément inutilisé. C'est l'horodatage du commit, et tout binaire est lié après le commit dont il provient : l'étiqueter « Built » serait faux sur chacun d'eux. Une date de build ne s'affiche que si un build en a réellement estampillé une, la même règle que suit la boîte About de bout en bout : **un fait que personne n'a enregistré n'a pas de ligne**, plutôt qu'une ligne vide qui se lit comme un échec à la remplir.
103+
104+Rejeté : une cible `make release` qui tague, construit et pousse. Publier tient en trois commandes git, et les emballer masque laquelle a échoué ; l'estampillage de la version était la partie qu'on ne pouvait pas faire à la main de façon fiable, et c'est celle qui a été automatisée.
105+
106+## Liens avec le reste
107+
108+- Ce que sont les paquets et comment ils s'articulent : [Architecture](architecture.md)
109+- Comment fonctionnent la coloration et la complétion : [Coloration et complétion](colouring-and-completion.md)
added docs/fr/explanation/go-tools.md +117 -0
new file mode 100644
@@ -0,0 +1,117 @@
1+# Outils go — explication
2+
3+## De quoi s'agit-il ?
4+
5+Un menu **Go** dont les commandes viennent d'un fichier TOML, chacune lancée dans une fenêtre terminal, et les fichiers ouverts relus ensuite. Cette page explique pourquoi chacun de ces trois points est ce qu'il est.
6+
7+## Pourquoi la sortie a trois destinations, et une popup par défaut
8+
9+La première version mettait chaque commande dans une fenêtre terminal, et c'était le mauvais défaut pour quatre des cinq.
10+
11+Un terminal est la bonne réponse quand le programme est *interactif ou long* : `go run .` sur quelque chose qui lit l'entrée standard doit pouvoir recevoir une réponse, et une compilation qui s'avère durer une minute doit pouvoir être interrompue par `Ctrl-C`. Ni l'un ni l'autre n'est vrai de `go vet ./...`, qui affiche quatre lignes et s'arrête. Lui donner une fenêtre entière — qu'il faut ensuite fermer, sur un bureau où les fenêtres se recouvrent et sont numérotées — est plus de cérémonie que le résultat n'en mérite.
12+
13+Une popup est la bonne réponse pour une commande qu'on lance, qu'on lit et qu'on referme. Elle est modale, ce qui est un coût réel et nommé dans le [guide](../how-to/run-go-commands.md) : un `go build` qu'on n'attendait pas lent immobilise l'éditeur jusqu'à sa fin ou jusqu'à `Échap`. Ce coût a été accepté exprès, parce que l'alternative — un dialogue surgissant trois secondes plus tard — avale ce qu'on était en train de taper à cet instant.
14+
15+La popup s'ouvre donc **immédiatement et se remplit**. On voit la progression, rien ne surprend, et `Échap` la ferme et arrête la commande — le seul moyen d'interrompre quelque chose dont la sortie n'est pas dans un terminal.
16+
17+Une fenêtre d'édition est la bonne réponse pour une sortie qu'on va éplucher : un long `go test -v`, un rapport de couverture. C'est un buffer ordinaire, donc `Ctrl-F` y cherche et `Save as` le conserve. Elle est remplie à la fin de la commande plutôt qu'au fil de l'eau, parce qu'un buffer qui grandit sous le curseur pendant qu'on y cherche est l'inverse de ce que ce mode vise.
18+
19+Aucune des trois ne convient à tout, et c'est pourquoi `output` est dans le fichier et non dans le code. `Run` en est l'exemple travaillé : c'est la seule commande du fichier de départ qui dit `terminal`, et le commentaire à côté dit pourquoi.
20+
21+## Pourquoi la fenêtre terminal reste
22+
23+L'éditeur en avait déjà une — un vrai pseudo-terminal avec émulateur VT, construit pour les fenêtres `F8` — donc `output = "terminal"` coûte un champ dans ses options et offre gratuitement les couleurs, la pagination, `Ctrl-C`, l'entrée clavier et l'historique, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.
24+
25+La fenêtre reste après la fin de la commande, et c'est le but : la sortie est ce qu'on a demandé, et une fenêtre qui disparaîtrait avec elle serait inutile.
26+
27+Cela a demandé un correctif à part. Une vue terminal consommait toute touche qu'on lui donnait et l'écrivait au shell ; une fois le shell parti, l'écriture échouait en silence et la touche était consommée quand même — `Ctrl-W` ne pouvait donc jamais fermer une fenêtre terminée, et la souris était la seule issue. Une vue terminée ne prend plus que les touches de défilement et laisse passer le reste vers l'éditeur.
28+
29+## Pourquoi le code de sortie est toujours dans le titre
30+
31+`go build ./...` qui réussit n'affiche rien du tout. Une popup au corps vide et au titre neutre est indistinguable d'une popup dont la commande n'a pas démarré, et le lecteur en est réduit à deviner la seule chose qu'il voulait savoir.
32+
33+Le titre porte donc le verdict — `— ok` ou `— exit 1` — et un corps vide affiche `(no output)` une fois la commande terminée. Pendant qu'elle tourne, le corps reste vide : « (no output) » est un verdict, et une commande en cours n'y est pas parvenue.
34+
35+## Pourquoi les commandes sont dans un fichier
36+
37+Cinq commandes codées en dur auraient répondu à la demande. Elles auraient aussi été fausses en une semaine.
38+
39+`go vet` est le linter par défaut parce qu'il fait partie de la chaîne d'outils et n'est jamais absent — mais bien des projets veulent `golangci-lint`. `go run .` suppose le paquet main à la racine. Un projet avec un `Makefile` veut `make check`. Un projet qui génère du code veut `go generate ./...` avant tout. Rien de cela n'est connaissable d'ici, et tout cela fait une ligne dans un fichier.
40+
41+Les cinq sont donc des **défauts, pas du code** : c'est le contenu du fichier de départ qu'écrit **Go ▸ Create tools file**, et en changer une consiste à éditer un fichier plutôt qu'à recompiler un éditeur. Le fichier est relu à chaque ouverture du menu, pour la même raison que le menu Snippets : une modification doit prendre effet aussitôt, et le fichier est souvent ouvert dans la fenêtre derrière le menu.
42+
43+Les commandes passent par `sh -c``cmd.exe /S /C` sous Windows — plutôt que d'être découpées en argv ici. Le fichier est celui de l'utilisateur, donc les tubes, les globs et `&&` sont des fonctionnalités plutôt que des dangers, et une entrée peut être `gofmt -l -w . && go vet ./... && go test ./...`. Découper un argv supposerait d'inventer des règles de citation pour une chaîne écrite à la main.
44+
45+## Pourquoi il n'y a pas de fichier d'outils utilisateur
46+
47+Les snippets sont lus depuis deux fichiers — le vôtre et celui du projet — parce que vos snippets sont vos habitudes et doivent vous suivre.
48+
49+Les outils ne sont pas ainsi. Ils appartiennent à la chaîne de compilation d'un projet : un fichier d'outils global proposerait `go build ./...` dans un dépôt Rust et `cargo test` dans un dépôt Go. Le fichier est par projet, et c'est toute la règle.
50+
51+## Pourquoi un outil peut nommer son propre menu
52+
53+Un menu nommé **Go** contenant `docker compose up` ment sur ce qu'il est. Le premier fichier d'outils que l'on écrit déborde de Go, parce que les commandes d'un projet ne parlent pas toutes du langage dans lequel il est écrit : conteneurs, bases de données, déploiements, une cible de `Makefile` ajoutée en 2019.
54+
55+Deux formes ont été envisagées. Un **second menu fixe** nommé Tools — tout ce qui est Go dans Go, le reste dans Tools — c'est une clé de plus dans le format et aucun problème de nommage, mais cela ne fait que déplacer le mensonge : un menu Tools contenant `docker compose up`, `psql` et un script de déploiement est tout aussi indifférencié, et dès qu'il y a dix entrées personne n'en retrouve une. Et un **second fichier**, `menus.toml`, garde le fichier d'outils simple au prix de deux fichiers qui doivent s'accorder sur les outils qui existent.
56+
57+Le menu est donc un **nom libre porté par l'outil**, dans l'unique fichier : `menu = "Docker"`. Un nom que rien d'autre n'emploie crée le menu ; omettre la clé signifie Go. Il n'y a pas de liste de noms autorisés, parce qu'une liste serait la liste des projets de quelqu'un d'autre.
58+
59+Go reste fixe sur la barre plutôt que de devenir un nom parmi d'autres venu du fichier. **Go ▸ Create tools file** doit être atteignable dans un projet qui n'a aucun fichier d'outils — c'est précisément le projet qui en a besoin — et un menu qui n'existe qu'une fois le fichier créé ne peut pas proposer de créer le fichier.
60+
61+## Pourquoi la touche d'accès n'appartient pas au fichier
62+
63+L'auteur d'un fichier d'outils ne peut pas savoir quelles lettres sont libres. Il voit `File`, `Edit`, `Search`, `Run`, `Options`, `Window`, `Snippets`, `Go` et `Help` sur la barre, mais seulement en comptant les soulignements, et un projet partagé à plusieurs dépendrait alors du fait que personne n'ajoute un menu qui entre en collision.
64+
65+Les collisions sont ici **silencieuses**, et c'est ce qui vaut qu'on se prémunisse contre elles. La barre répond au premier menu dont la touche correspond ; un second menu revendiquant la même lettre n'est pas une erreur et se dessine normalement — il ne s'ouvre simplement jamais. Le piège s'est déjà refermé une fois dans cet éditeur : `Snippets` et `Search` voulaient tous deux le `S`, `Snippets` était celui qu'on ne pouvait pas ouvrir, et tous les tests passaient. Le correctif d'alors fut de déplacer Snippets sur `N` à la main. Laisser un fichier nommer des menus en ferait un danger permanent plutôt qu'une erreur isolée, donc l'attribution revient à l'éditeur : la première lettre du nom que rien d'autre ne revendique.
66+
67+Les tildes écrits dans le nom sont honorés **quand la lettre est libre**, et écartés sans bruit sinon. Refuser le fichier était l'alternative, et elle est pire : la collision dépend des menus qui existent, donc un fichier d'outils qui marchait cesserait de marcher le jour où une version de l'éditeur ajoute un menu. Entre un menu sur une lettre que vous n'avez pas demandée et un menu que vous ne pouvez pas ouvrir, la première est la moindre perte.
68+
69+Quand toutes les lettres d'un nom sont prises, le menu n'a pas de touche d'accès du tout. `F10`, les flèches et la souris l'atteignent encore, et l'alternative — aller chercher une lettre qui n'est pas dans le nom — mettrait un soulignement sous rien.
70+
71+## Pourquoi la barre est reconstruite depuis un stat
72+
73+`Menu.OnOpen` remplit les entrées d'un menu juste avant qu'il ne se déroule, et c'est ainsi que les menus Go et Snippets suivent leurs fichiers sans redémarrage. Cela ne suffit pas ici : l'*ensemble* des menus appartient à la barre et non à un menu, et ajouter `menu = "Docker"` au fichier doit poser Docker sur la barre.
74+
75+Lire et analyser le fichier à chaque tour de la boucle d'événements y parviendrait, et ferait aussi ce travail pour rien à chaque frappe dans un fichier que personne n'a modifié. La barre porte donc la taille et la date de modification du fichier d'outils dont elle est issue, et un `stat` par tour décide s'il faut reconstruire. Modifier le fichier dans la fenêtre devant soi, l'enregistrer et voir la barre changer, c'est le cas visé.
76+
77+## Pourquoi les fichiers ouverts sont relus, et seulement certains
78+
79+`Format` est le premier item du menu et il réécrit les fichiers sur le disque — y compris celui qu'on regarde. Sans rien de plus, l'éditeur resterait assis sur une copie périmée, et le `F2` suivant réécrirait votre version non formatée par-dessus le travail de gofmt. Ce n'est pas une aspérité : c'est la fonctionnalité qui se défait toute seule, en silence.
80+
81+À la fin d'une commande, l'éditeur relit donc chaque fichier ouvert. La partie intéressante est ceux qu'il refuse de toucher.
82+
83+**Un fichier ayant des modifications non enregistrées est laissé tel quel**, et la barre d'état dit combien ont été ignorés. Le recharger jetterait un travail que l'utilisateur n'a pas enregistré, ce qu'aucune commodité ne justifie. Et le conflit est réel : le formateur et la modification non enregistrée ne sont pas d'accord sur ce que le fichier doit dire, et l'éditeur n'est pas en position de trancher. Le nommer et s'arrêter est l'issue honnête — l'utilisateur peut enregistrer et relancer, ou continuer à éditer et formater plus tard.
84+
85+Deux décisions plus petites à l'intérieur :
86+
87+- **Le curseur reste où il était**, borné à ce que le fichier contient désormais. Un formateur déplace les lignes ; remettre le curseur en haut ferait perdre sa place au lecteur pour rien.
88+- **L'historique d'annulation est jeté.** Annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus, ce qui est pire que de ne pas pouvoir annuler.
89+
90+## Pourquoi le rechargement a lieu sur la boucle d'événements
91+
92+La fin de la commande est remarquée par la goroutine qui lit le terminal, laquelle ne peut toucher ni un buffer ni le bureau. Elle positionne donc un drapeau, et le rechargement se fait en tête du tour suivant de la boucle.
93+
94+C'est la quatrième chose construite ainsi dans cet éditeur — l'annonce au serveur de langage, les redessins de terminal, l'échéance d'autosave, et maintenant ceci. La règle qu'elles partagent mérite d'être énoncée une fois de plus : **le réveil peut être perdu, l'état ne doit pas l'être.** `PostEvent` jette ce qui ne tient pas dans sa file, donc tout ce qui dépend de l'arrivée d'un message est un bug qui attend un moment de charge. Un drapeau que la boucle vérifie elle-même ne peut pas disparaître.
95+
96+## Pourquoi une commande peut demander une valeur, et pourquoi en doubles accolades
97+
98+`go mod init` a besoin d'un chemin de module. `cargo new` a besoin d'un nom de caisse. `go test -run` a besoin d'un motif. Aucun de ces éléments ne peut vivre dans le fichier d'outils, parce que la réponse change à chaque fois — et un outil qui ne peut pas demander est un outil qu'il faut éditer avant chaque usage, ce qui n'est pas un outil.
99+
100+Un `{{libellé}}` dans une commande est donc une valeur que l'éditeur demande d'abord, dans une boîte portant le nom de l'outil.
101+
102+**Une seule accolade était l'écriture évidente, et elle est fausse.** `awk '{print $1}'` et `find . -exec rm {} +` sont des choses ordinaires à mettre dans un fichier d'outils, et lire la première comme un libellé transforme une commande qui marche en une boîte demandant « print $1 ». Les doubles accolades n'entrent en collision presque avec rien, et la seule construction avec laquelle elles le font — un bloc imbriqué en awk — est assez rare pour être signalée plutôt que contournée par la conception.
103+
104+**La valeur est protégée par défaut**, parce que l'inverse échoue en silence. Un chemin contenant une espace, substitué tel quel, devient deux arguments et la commande parle d'un fichier qui n'existe pas. La protection fait marcher ce cas et rend l'autre — « ajoute ces trois options à la fin » — impossible ; c'est pourquoi un `...` dans les accolades demande la valeur telle quelle. Deux comportements, tous deux documentés, plutôt qu'un seul faux une fois sur deux.
105+
106+**Rien n'est retenu sur le disque.** La boîte repart de ce qui avait été tapé, pour la session. L'écrire dans le répertoire propre au projet a été envisagé puis rejeté : ce répertoire contient ce que le projet a décidé, et un filtre tapé en poursuivant un test n'en fait pas partie. Ce serait aussi la première chose qui y changerait sans que personne ne modifie un fichier.
107+
108+**Un fichier illisible est refusé à la lecture**, pas au moment où l'outil est choisi. Un `{{` non fermé atteignant le shell donne une commande qui échoue avec des accolades dedans, ce qui ne nomme ni l'outil ni le fichier ; refuser au chargement nomme les deux. C'est la règle que suit déjà une valeur d'`output` inconnue.
109+
110+**La boîte est refusée quand elle ne tient pas.** Un outil demandant plus de valeurs que le terminal n'a de lignes donnerait une boîte dont le bouton OK est sous le bas de l'écran — à laquelle on ne peut répondre que par Échap, qui annule. Dire « celui-ci demande douze valeurs et neuf tiennent » n'est pire que rien que si l'on préfère l'apprendre en essayant.
111+
112+## Liens avec le reste
113+
114+- Toutes les clés du fichier et toutes les règles : [Référence des outils go](../reference/go-tools.md)
115+- L'utiliser : [Lancer les commandes go depuis l'éditeur](../how-to/run-go-commands.md)
116+- Les fenêtres qu'emploie `output = "terminal"`, et pourquoi ce sont de vrais terminaux : [Fenêtres terminal](terminal-windows.md)
117+- L'autre menu construit depuis un fichier : [Snippets](snippets.md)
new file mode 100644
@@ -0,0 +1,117 @@
1+# Outils go — explication
2+
3+## De quoi s'agit-il ?
4+
5+Un menu **Go** dont les commandes viennent d'un fichier TOML, chacune lancée dans une fenêtre terminal, et les fichiers ouverts relus ensuite. Cette page explique pourquoi chacun de ces trois points est ce qu'il est.
6+
7+## Pourquoi la sortie a trois destinations, et une popup par défaut
8+
9+La première version mettait chaque commande dans une fenêtre terminal, et c'était le mauvais défaut pour quatre des cinq.
10+
11+Un terminal est la bonne réponse quand le programme est *interactif ou long* : `go run .` sur quelque chose qui lit l'entrée standard doit pouvoir recevoir une réponse, et une compilation qui s'avère durer une minute doit pouvoir être interrompue par `Ctrl-C`. Ni l'un ni l'autre n'est vrai de `go vet ./...`, qui affiche quatre lignes et s'arrête. Lui donner une fenêtre entière — qu'il faut ensuite fermer, sur un bureau où les fenêtres se recouvrent et sont numérotées — est plus de cérémonie que le résultat n'en mérite.
12+
13+Une popup est la bonne réponse pour une commande qu'on lance, qu'on lit et qu'on referme. Elle est modale, ce qui est un coût réel et nommé dans le [guide](../how-to/run-go-commands.md) : un `go build` qu'on n'attendait pas lent immobilise l'éditeur jusqu'à sa fin ou jusqu'à `Échap`. Ce coût a été accepté exprès, parce que l'alternative — un dialogue surgissant trois secondes plus tard — avale ce qu'on était en train de taper à cet instant.
14+
15+La popup s'ouvre donc **immédiatement et se remplit**. On voit la progression, rien ne surprend, et `Échap` la ferme et arrête la commande — le seul moyen d'interrompre quelque chose dont la sortie n'est pas dans un terminal.
16+
17+Une fenêtre d'édition est la bonne réponse pour une sortie qu'on va éplucher : un long `go test -v`, un rapport de couverture. C'est un buffer ordinaire, donc `Ctrl-F` y cherche et `Save as` le conserve. Elle est remplie à la fin de la commande plutôt qu'au fil de l'eau, parce qu'un buffer qui grandit sous le curseur pendant qu'on y cherche est l'inverse de ce que ce mode vise.
18+
19+Aucune des trois ne convient à tout, et c'est pourquoi `output` est dans le fichier et non dans le code. `Run` en est l'exemple travaillé : c'est la seule commande du fichier de départ qui dit `terminal`, et le commentaire à côté dit pourquoi.
20+
21+## Pourquoi la fenêtre terminal reste
22+
23+L'éditeur en avait déjà une — un vrai pseudo-terminal avec émulateur VT, construit pour les fenêtres `F8` — donc `output = "terminal"` coûte un champ dans ses options et offre gratuitement les couleurs, la pagination, `Ctrl-C`, l'entrée clavier et l'historique, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.
24+
25+La fenêtre reste après la fin de la commande, et c'est le but : la sortie est ce qu'on a demandé, et une fenêtre qui disparaîtrait avec elle serait inutile.
26+
27+Cela a demandé un correctif à part. Une vue terminal consommait toute touche qu'on lui donnait et l'écrivait au shell ; une fois le shell parti, l'écriture échouait en silence et la touche était consommée quand même — `Ctrl-W` ne pouvait donc jamais fermer une fenêtre terminée, et la souris était la seule issue. Une vue terminée ne prend plus que les touches de défilement et laisse passer le reste vers l'éditeur.
28+
29+## Pourquoi le code de sortie est toujours dans le titre
30+
31+`go build ./...` qui réussit n'affiche rien du tout. Une popup au corps vide et au titre neutre est indistinguable d'une popup dont la commande n'a pas démarré, et le lecteur en est réduit à deviner la seule chose qu'il voulait savoir.
32+
33+Le titre porte donc le verdict — `— ok` ou `— exit 1` — et un corps vide affiche `(no output)` une fois la commande terminée. Pendant qu'elle tourne, le corps reste vide : « (no output) » est un verdict, et une commande en cours n'y est pas parvenue.
34+
35+## Pourquoi les commandes sont dans un fichier
36+
37+Cinq commandes codées en dur auraient répondu à la demande. Elles auraient aussi été fausses en une semaine.
38+
39+`go vet` est le linter par défaut parce qu'il fait partie de la chaîne d'outils et n'est jamais absent — mais bien des projets veulent `golangci-lint`. `go run .` suppose le paquet main à la racine. Un projet avec un `Makefile` veut `make check`. Un projet qui génère du code veut `go generate ./...` avant tout. Rien de cela n'est connaissable d'ici, et tout cela fait une ligne dans un fichier.
40+
41+Les cinq sont donc des **défauts, pas du code** : c'est le contenu du fichier de départ qu'écrit **Go ▸ Create tools file**, et en changer une consiste à éditer un fichier plutôt qu'à recompiler un éditeur. Le fichier est relu à chaque ouverture du menu, pour la même raison que le menu Snippets : une modification doit prendre effet aussitôt, et le fichier est souvent ouvert dans la fenêtre derrière le menu.
42+
43+Les commandes passent par `sh -c``cmd.exe /S /C` sous Windows — plutôt que d'être découpées en argv ici. Le fichier est celui de l'utilisateur, donc les tubes, les globs et `&&` sont des fonctionnalités plutôt que des dangers, et une entrée peut être `gofmt -l -w . && go vet ./... && go test ./...`. Découper un argv supposerait d'inventer des règles de citation pour une chaîne écrite à la main.
44+
45+## Pourquoi il n'y a pas de fichier d'outils utilisateur
46+
47+Les snippets sont lus depuis deux fichiers — le vôtre et celui du projet — parce que vos snippets sont vos habitudes et doivent vous suivre.
48+
49+Les outils ne sont pas ainsi. Ils appartiennent à la chaîne de compilation d'un projet : un fichier d'outils global proposerait `go build ./...` dans un dépôt Rust et `cargo test` dans un dépôt Go. Le fichier est par projet, et c'est toute la règle.
50+
51+## Pourquoi un outil peut nommer son propre menu
52+
53+Un menu nommé **Go** contenant `docker compose up` ment sur ce qu'il est. Le premier fichier d'outils que l'on écrit déborde de Go, parce que les commandes d'un projet ne parlent pas toutes du langage dans lequel il est écrit : conteneurs, bases de données, déploiements, une cible de `Makefile` ajoutée en 2019.
54+
55+Deux formes ont été envisagées. Un **second menu fixe** nommé Tools — tout ce qui est Go dans Go, le reste dans Tools — c'est une clé de plus dans le format et aucun problème de nommage, mais cela ne fait que déplacer le mensonge : un menu Tools contenant `docker compose up`, `psql` et un script de déploiement est tout aussi indifférencié, et dès qu'il y a dix entrées personne n'en retrouve une. Et un **second fichier**, `menus.toml`, garde le fichier d'outils simple au prix de deux fichiers qui doivent s'accorder sur les outils qui existent.
56+
57+Le menu est donc un **nom libre porté par l'outil**, dans l'unique fichier : `menu = "Docker"`. Un nom que rien d'autre n'emploie crée le menu ; omettre la clé signifie Go. Il n'y a pas de liste de noms autorisés, parce qu'une liste serait la liste des projets de quelqu'un d'autre.
58+
59+Go reste fixe sur la barre plutôt que de devenir un nom parmi d'autres venu du fichier. **Go ▸ Create tools file** doit être atteignable dans un projet qui n'a aucun fichier d'outils — c'est précisément le projet qui en a besoin — et un menu qui n'existe qu'une fois le fichier créé ne peut pas proposer de créer le fichier.
60+
61+## Pourquoi la touche d'accès n'appartient pas au fichier
62+
63+L'auteur d'un fichier d'outils ne peut pas savoir quelles lettres sont libres. Il voit `File`, `Edit`, `Search`, `Run`, `Options`, `Window`, `Snippets`, `Go` et `Help` sur la barre, mais seulement en comptant les soulignements, et un projet partagé à plusieurs dépendrait alors du fait que personne n'ajoute un menu qui entre en collision.
64+
65+Les collisions sont ici **silencieuses**, et c'est ce qui vaut qu'on se prémunisse contre elles. La barre répond au premier menu dont la touche correspond ; un second menu revendiquant la même lettre n'est pas une erreur et se dessine normalement — il ne s'ouvre simplement jamais. Le piège s'est déjà refermé une fois dans cet éditeur : `Snippets` et `Search` voulaient tous deux le `S`, `Snippets` était celui qu'on ne pouvait pas ouvrir, et tous les tests passaient. Le correctif d'alors fut de déplacer Snippets sur `N` à la main. Laisser un fichier nommer des menus en ferait un danger permanent plutôt qu'une erreur isolée, donc l'attribution revient à l'éditeur : la première lettre du nom que rien d'autre ne revendique.
66+
67+Les tildes écrits dans le nom sont honorés **quand la lettre est libre**, et écartés sans bruit sinon. Refuser le fichier était l'alternative, et elle est pire : la collision dépend des menus qui existent, donc un fichier d'outils qui marchait cesserait de marcher le jour où une version de l'éditeur ajoute un menu. Entre un menu sur une lettre que vous n'avez pas demandée et un menu que vous ne pouvez pas ouvrir, la première est la moindre perte.
68+
69+Quand toutes les lettres d'un nom sont prises, le menu n'a pas de touche d'accès du tout. `F10`, les flèches et la souris l'atteignent encore, et l'alternative — aller chercher une lettre qui n'est pas dans le nom — mettrait un soulignement sous rien.
70+
71+## Pourquoi la barre est reconstruite depuis un stat
72+
73+`Menu.OnOpen` remplit les entrées d'un menu juste avant qu'il ne se déroule, et c'est ainsi que les menus Go et Snippets suivent leurs fichiers sans redémarrage. Cela ne suffit pas ici : l'*ensemble* des menus appartient à la barre et non à un menu, et ajouter `menu = "Docker"` au fichier doit poser Docker sur la barre.
74+
75+Lire et analyser le fichier à chaque tour de la boucle d'événements y parviendrait, et ferait aussi ce travail pour rien à chaque frappe dans un fichier que personne n'a modifié. La barre porte donc la taille et la date de modification du fichier d'outils dont elle est issue, et un `stat` par tour décide s'il faut reconstruire. Modifier le fichier dans la fenêtre devant soi, l'enregistrer et voir la barre changer, c'est le cas visé.
76+
77+## Pourquoi les fichiers ouverts sont relus, et seulement certains
78+
79+`Format` est le premier item du menu et il réécrit les fichiers sur le disque — y compris celui qu'on regarde. Sans rien de plus, l'éditeur resterait assis sur une copie périmée, et le `F2` suivant réécrirait votre version non formatée par-dessus le travail de gofmt. Ce n'est pas une aspérité : c'est la fonctionnalité qui se défait toute seule, en silence.
80+
81+À la fin d'une commande, l'éditeur relit donc chaque fichier ouvert. La partie intéressante est ceux qu'il refuse de toucher.
82+
83+**Un fichier ayant des modifications non enregistrées est laissé tel quel**, et la barre d'état dit combien ont été ignorés. Le recharger jetterait un travail que l'utilisateur n'a pas enregistré, ce qu'aucune commodité ne justifie. Et le conflit est réel : le formateur et la modification non enregistrée ne sont pas d'accord sur ce que le fichier doit dire, et l'éditeur n'est pas en position de trancher. Le nommer et s'arrêter est l'issue honnête — l'utilisateur peut enregistrer et relancer, ou continuer à éditer et formater plus tard.
84+
85+Deux décisions plus petites à l'intérieur :
86+
87+- **Le curseur reste où il était**, borné à ce que le fichier contient désormais. Un formateur déplace les lignes ; remettre le curseur en haut ferait perdre sa place au lecteur pour rien.
88+- **L'historique d'annulation est jeté.** Annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus, ce qui est pire que de ne pas pouvoir annuler.
89+
90+## Pourquoi le rechargement a lieu sur la boucle d'événements
91+
92+La fin de la commande est remarquée par la goroutine qui lit le terminal, laquelle ne peut toucher ni un buffer ni le bureau. Elle positionne donc un drapeau, et le rechargement se fait en tête du tour suivant de la boucle.
93+
94+C'est la quatrième chose construite ainsi dans cet éditeur — l'annonce au serveur de langage, les redessins de terminal, l'échéance d'autosave, et maintenant ceci. La règle qu'elles partagent mérite d'être énoncée une fois de plus : **le réveil peut être perdu, l'état ne doit pas l'être.** `PostEvent` jette ce qui ne tient pas dans sa file, donc tout ce qui dépend de l'arrivée d'un message est un bug qui attend un moment de charge. Un drapeau que la boucle vérifie elle-même ne peut pas disparaître.
95+
96+## Pourquoi une commande peut demander une valeur, et pourquoi en doubles accolades
97+
98+`go mod init` a besoin d'un chemin de module. `cargo new` a besoin d'un nom de caisse. `go test -run` a besoin d'un motif. Aucun de ces éléments ne peut vivre dans le fichier d'outils, parce que la réponse change à chaque fois — et un outil qui ne peut pas demander est un outil qu'il faut éditer avant chaque usage, ce qui n'est pas un outil.
99+
100+Un `{{libellé}}` dans une commande est donc une valeur que l'éditeur demande d'abord, dans une boîte portant le nom de l'outil.
101+
102+**Une seule accolade était l'écriture évidente, et elle est fausse.** `awk '{print $1}'` et `find . -exec rm {} +` sont des choses ordinaires à mettre dans un fichier d'outils, et lire la première comme un libellé transforme une commande qui marche en une boîte demandant « print $1 ». Les doubles accolades n'entrent en collision presque avec rien, et la seule construction avec laquelle elles le font — un bloc imbriqué en awk — est assez rare pour être signalée plutôt que contournée par la conception.
103+
104+**La valeur est protégée par défaut**, parce que l'inverse échoue en silence. Un chemin contenant une espace, substitué tel quel, devient deux arguments et la commande parle d'un fichier qui n'existe pas. La protection fait marcher ce cas et rend l'autre — « ajoute ces trois options à la fin » — impossible ; c'est pourquoi un `...` dans les accolades demande la valeur telle quelle. Deux comportements, tous deux documentés, plutôt qu'un seul faux une fois sur deux.
105+
106+**Rien n'est retenu sur le disque.** La boîte repart de ce qui avait été tapé, pour la session. L'écrire dans le répertoire propre au projet a été envisagé puis rejeté : ce répertoire contient ce que le projet a décidé, et un filtre tapé en poursuivant un test n'en fait pas partie. Ce serait aussi la première chose qui y changerait sans que personne ne modifie un fichier.
107+
108+**Un fichier illisible est refusé à la lecture**, pas au moment où l'outil est choisi. Un `{{` non fermé atteignant le shell donne une commande qui échoue avec des accolades dedans, ce qui ne nomme ni l'outil ni le fichier ; refuser au chargement nomme les deux. C'est la règle que suit déjà une valeur d'`output` inconnue.
109+
110+**La boîte est refusée quand elle ne tient pas.** Un outil demandant plus de valeurs que le terminal n'a de lignes donnerait une boîte dont le bouton OK est sous le bas de l'écran — à laquelle on ne peut répondre que par Échap, qui annule. Dire « celui-ci demande douze valeurs et neuf tiennent » n'est pire que rien que si l'on préfère l'apprendre en essayant.
111+
112+## Liens avec le reste
113+
114+- Toutes les clés du fichier et toutes les règles : [Référence des outils go](../reference/go-tools.md)
115+- L'utiliser : [Lancer les commandes go depuis l'éditeur](../how-to/run-go-commands.md)
116+- Les fenêtres qu'emploie `output = "terminal"`, et pourquoi ce sont de vrais terminaux : [Fenêtres terminal](terminal-windows.md)
117+- L'autre menu construit depuis un fichier : [Snippets](snippets.md)
added docs/fr/explanation/project-settings.md +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+# Réglages de projet — explication
2+
3+## De quoi s'agit-il ?
4+
5+Un projet peut garder un `.turbo-go/settings.toml` à côté de son code, disant quel thème utiliser et s'il faut enregistrer les fichiers automatiquement. Cette page traite des décisions contenues dans cette phrase : pourquoi le fichier n'est cherché qu'à un seul endroit, pourquoi le créer est une entrée de menu plutôt que quelque chose qui arrive tout seul, et pourquoi la sauvegarde automatique fonctionne comme elle le fait.
6+
7+## Pourquoi le dossier n'est pas cherché vers le haut
8+
9+`go.mod` est trouvé en remontant depuis le fichier ouvert jusqu'à en croiser un, et le serveur de langage fait exactement cela. Le fichier de réglages, délibérément, non.
10+
11+La remontée est juste pour `go.mod` parce qu'un module a une frontière réelle : le fichier est au-dessus de vous ou il n'y est pas, et être dans un module est un fait à propos du code. « Le projet » n'est pas un fait à propos du code. C'est l'endroit où vous avez décidé de travailler, et la même arborescence est plusieurs projets selon ce que vous y faites — le `services/api` d'un monorepo est un projet quand vous travaillez sur l'API, et une partie d'un plus grand le reste du temps.
12+
13+Une remontée ferait aussi agir le réglage à distance. Vous ouvrez un fichier, et les couleurs de l'éditeur changent à cause d'un fichier trois dossiers plus haut dont vous ignoriez l'existence. Toute explication de ce comportement commence par « eh bien, il cherche vers le haut », alors que la règle qu'on préfère pouvoir énoncer est celle qui est maintenant vraie : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.**
14+
15+Le coût est réel et mérite d'être nommé. Lancez l'éditeur depuis un sous-répertoire et le thème du projet ne s'applique pas. La réponse est de lancer depuis la racine du projet, là où vous feriez `go build` et `git` de toute façon.
16+
17+## Pourquoi créer le fichier est une entrée de menu
18+
19+L'autre solution était tentante : à la première fois qu'on choisit un thème, écrire `.turbo-go/settings.toml` pour que le choix persiste. Tout éditeur qui stocke un état d'espace de travail fait quelque chose d'approchant.
20+
21+Elle a été écartée parce qu'elle dépose un dossier dans le dépôt de quelqu'un comme effet de bord d'un essai de couleur. L'utilisateur est à un `git status` d'une modification qu'il n'a pas faite, dans un projet qui n'est peut-être pas le sien, éventuellement en pleine revue. Un thème choisi pour être regardé dix secondes ne doit rien laisser derrière lui.
22+
23+Le fichier n'est donc créé que par **Options ▸ Create project settings**, et son existence signifie quelque chose : ce projet a des réglages, exprès. C'est aussi ce qui rend la règle d'écriture simple à énoncer — **le thème est écrit dans le fichier quand le fichier existe, et pas autrement** — sans nulle part une case « voulez-vous vous en souvenir ? ».
24+
25+## Pourquoi le thème est réécrit sur place plutôt que réencodé
26+
27+Une fois le fichier créé, choisir un thème le réécrit. Sérialiser la structure `Settings` vers du TOML ferait quatre lignes et supprimerait tous les commentaires du fichier.
28+
29+Cela compte plus ici qu'ailleurs, parce que ce fichier est *fait* pour être édité à la main. C'est la raison d'être de la coloration TOML dans l'éditeur ; le fichier créé est surtout des commentaires expliquant les clés ; une équipe y ajoutera ses propres commentaires disant pourquoi elle a choisi ce qu'elle a choisi. Tout perdre à la première tentative d'un autre thème serait une suppression silencieuse et surprenante de ce que quelqu'un a écrit.
30+
31+La réécriture trouve donc la ligne `theme` dans la table `[editor]` et change la valeur entre le `=` et un éventuel commentaire de fin de ligne. Tout le reste du fichier revient octet pour octet. Cela fait une quarantaine de lignes au lieu de quatre, et c'est la différence entre un fichier où l'on peut mettre des choses et un fichier qui les mange.
32+
33+## Pourquoi la sauvegarde automatique attend une pause
34+
35+Trois déclencheurs ont été envisagés.
36+
37+**À intervalle fixe** est le plus simple et il est faux : il écrit au milieu d'une modification. La moitié d'un identifiant renommé atteint le disque, un observateur de fichiers reconstruit, et une suite de tests échoue sur du code qui n'a jamais été l'intention de personne.
38+
39+**À la sortie de la fenêtre** n'écrit jamais pendant qu'on travaille, ce qui semble prudent et signifie que ce qui est sur le disque peut avoir une heure de retard sur ce qui est à l'écran — précisément quand cela compte, puisque la raison de vouloir l'autosave est en général un outil qui surveille le fichier.
40+
41+**Après une pause dans la frappe** est ce sur quoi les autres éditeurs et celui-ci se sont arrêtés. Deux secondes, c'est assez long pour qu'une pause de réflexion ne soit pas une écriture, assez court pour qu'une reconstruction suive de près une modification. Une série de frappes fait une écriture, pas une par touche.
42+
43+Il y a une seule échéance pour tout l'éditeur plutôt qu'une par fenêtre, parce que « vous avez cessé de taper » est un seul événement. Une échéance par fenêtre enregistrerait le fichier que vous avez quitté à un moment différent de celui que vous avez sous les yeux, ce que personne ne pourrait observer et qui fait davantage d'état à maintenir juste.
44+
45+## Pourquoi l'échéance est vérifiée, le minuteur ne faisant que pousser
46+
47+C'est le même piège que l'annonce au serveur de langage et que les redessins de terminal ont rencontré, et il vaut d'être énoncé une fois de plus parce qu'il reviendra.
48+
49+L'éditeur est bloqué dans `PollEvent`. Pour remarquer une échéance alors que rien ne se passe, il faut le réveiller, et le seul moyen de le réveiller depuis un minuteur est `PostEvent` — qui **jette** les événements quand sa file est pleine.
50+
51+Le minuteur n'est donc pas ce qui décide. L'échéance est un état, vérifié en tête de chaque tour de boucle, exactement comme `announceOpenDocuments` vérifie si le serveur de langage est prêt. Le seul rôle du minuteur est de garantir qu'un tour ait lieu. Une poussée perdue coûte une sauvegarde en retard jusqu'à la frappe ou au clic suivant ; un design où le minuteur enregistrerait lui-même la perdrait tout court.
52+
53+## Pourquoi une sauvegarde automatique en échec n'ouvre pas de dialogue
54+
55+Une sauvegarde que personne n'a demandée ne doit pas interrompre par une fenêtre modale, et une modale qui revient toutes les deux secondes parce qu'un fichier est en lecture seule est pire que le problème qu'elle signale. Cela va donc dans la barre d'état, et l'échéance est effacée *avant* la tentative d'écriture : un fichier qui ne peut pas être écrit est essayé une fois par modification, et non indéfiniment.
56+
57+## Pourquoi la coloration TOML réutilise les classes de Go
58+
59+Ajouter `syntax.tomlkey` et compagnie aurait signifié que tous les thèmes — y compris ceux écrits par les utilisateurs — auraient silencieusement échoué à colorer le TOML jusqu'à leur mise à jour.
60+
61+Les classes déjà présentes conviennent : un en-tête de table nomme une structure, il se lit donc comme un type ; une clé nomme une chose, elle se lit donc comme un identifiant ; `true` et `false` sont des constantes parce que c'est ce qu'elles sont. Le résultat est que tout thème qui a jamais fonctionné colore le TOML correctement, sans modification et sans nouvelle clé. Le scanner est écrit à la main pour la même raison que l'émulateur de terminal — le TOML est un langage petit et entièrement spécifié, et c'est un fichier contre une troisième dépendance.
62+
63+## Liens avec le reste
64+
65+- Les clés exactes et leurs valeurs par défaut : [Référence des réglages de projet](../reference/project-settings.md)
66+- En mettre en place : [Donner ses propres réglages à un projet](../how-to/configure-a-project.md)
67+- L'autre fichier TOML que lit l'éditeur : [Format des fichiers de thème](../reference/themes.md)
68+- Où `settings` se situe parmi les paquets : [Architecture](architecture.md)
new file mode 100644
@@ -0,0 +1,68 @@
1+# Réglages de projet — explication
2+
3+## De quoi s'agit-il ?
4+
5+Un projet peut garder un `.turbo-go/settings.toml` à côté de son code, disant quel thème utiliser et s'il faut enregistrer les fichiers automatiquement. Cette page traite des décisions contenues dans cette phrase : pourquoi le fichier n'est cherché qu'à un seul endroit, pourquoi le créer est une entrée de menu plutôt que quelque chose qui arrive tout seul, et pourquoi la sauvegarde automatique fonctionne comme elle le fait.
6+
7+## Pourquoi le dossier n'est pas cherché vers le haut
8+
9+`go.mod` est trouvé en remontant depuis le fichier ouvert jusqu'à en croiser un, et le serveur de langage fait exactement cela. Le fichier de réglages, délibérément, non.
10+
11+La remontée est juste pour `go.mod` parce qu'un module a une frontière réelle : le fichier est au-dessus de vous ou il n'y est pas, et être dans un module est un fait à propos du code. « Le projet » n'est pas un fait à propos du code. C'est l'endroit où vous avez décidé de travailler, et la même arborescence est plusieurs projets selon ce que vous y faites — le `services/api` d'un monorepo est un projet quand vous travaillez sur l'API, et une partie d'un plus grand le reste du temps.
12+
13+Une remontée ferait aussi agir le réglage à distance. Vous ouvrez un fichier, et les couleurs de l'éditeur changent à cause d'un fichier trois dossiers plus haut dont vous ignoriez l'existence. Toute explication de ce comportement commence par « eh bien, il cherche vers le haut », alors que la règle qu'on préfère pouvoir énoncer est celle qui est maintenant vraie : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.**
14+
15+Le coût est réel et mérite d'être nommé. Lancez l'éditeur depuis un sous-répertoire et le thème du projet ne s'applique pas. La réponse est de lancer depuis la racine du projet, là où vous feriez `go build` et `git` de toute façon.
16+
17+## Pourquoi créer le fichier est une entrée de menu
18+
19+L'autre solution était tentante : à la première fois qu'on choisit un thème, écrire `.turbo-go/settings.toml` pour que le choix persiste. Tout éditeur qui stocke un état d'espace de travail fait quelque chose d'approchant.
20+
21+Elle a été écartée parce qu'elle dépose un dossier dans le dépôt de quelqu'un comme effet de bord d'un essai de couleur. L'utilisateur est à un `git status` d'une modification qu'il n'a pas faite, dans un projet qui n'est peut-être pas le sien, éventuellement en pleine revue. Un thème choisi pour être regardé dix secondes ne doit rien laisser derrière lui.
22+
23+Le fichier n'est donc créé que par **Options ▸ Create project settings**, et son existence signifie quelque chose : ce projet a des réglages, exprès. C'est aussi ce qui rend la règle d'écriture simple à énoncer — **le thème est écrit dans le fichier quand le fichier existe, et pas autrement** — sans nulle part une case « voulez-vous vous en souvenir ? ».
24+
25+## Pourquoi le thème est réécrit sur place plutôt que réencodé
26+
27+Une fois le fichier créé, choisir un thème le réécrit. Sérialiser la structure `Settings` vers du TOML ferait quatre lignes et supprimerait tous les commentaires du fichier.
28+
29+Cela compte plus ici qu'ailleurs, parce que ce fichier est *fait* pour être édité à la main. C'est la raison d'être de la coloration TOML dans l'éditeur ; le fichier créé est surtout des commentaires expliquant les clés ; une équipe y ajoutera ses propres commentaires disant pourquoi elle a choisi ce qu'elle a choisi. Tout perdre à la première tentative d'un autre thème serait une suppression silencieuse et surprenante de ce que quelqu'un a écrit.
30+
31+La réécriture trouve donc la ligne `theme` dans la table `[editor]` et change la valeur entre le `=` et un éventuel commentaire de fin de ligne. Tout le reste du fichier revient octet pour octet. Cela fait une quarantaine de lignes au lieu de quatre, et c'est la différence entre un fichier où l'on peut mettre des choses et un fichier qui les mange.
32+
33+## Pourquoi la sauvegarde automatique attend une pause
34+
35+Trois déclencheurs ont été envisagés.
36+
37+**À intervalle fixe** est le plus simple et il est faux : il écrit au milieu d'une modification. La moitié d'un identifiant renommé atteint le disque, un observateur de fichiers reconstruit, et une suite de tests échoue sur du code qui n'a jamais été l'intention de personne.
38+
39+**À la sortie de la fenêtre** n'écrit jamais pendant qu'on travaille, ce qui semble prudent et signifie que ce qui est sur le disque peut avoir une heure de retard sur ce qui est à l'écran — précisément quand cela compte, puisque la raison de vouloir l'autosave est en général un outil qui surveille le fichier.
40+
41+**Après une pause dans la frappe** est ce sur quoi les autres éditeurs et celui-ci se sont arrêtés. Deux secondes, c'est assez long pour qu'une pause de réflexion ne soit pas une écriture, assez court pour qu'une reconstruction suive de près une modification. Une série de frappes fait une écriture, pas une par touche.
42+
43+Il y a une seule échéance pour tout l'éditeur plutôt qu'une par fenêtre, parce que « vous avez cessé de taper » est un seul événement. Une échéance par fenêtre enregistrerait le fichier que vous avez quitté à un moment différent de celui que vous avez sous les yeux, ce que personne ne pourrait observer et qui fait davantage d'état à maintenir juste.
44+
45+## Pourquoi l'échéance est vérifiée, le minuteur ne faisant que pousser
46+
47+C'est le même piège que l'annonce au serveur de langage et que les redessins de terminal ont rencontré, et il vaut d'être énoncé une fois de plus parce qu'il reviendra.
48+
49+L'éditeur est bloqué dans `PollEvent`. Pour remarquer une échéance alors que rien ne se passe, il faut le réveiller, et le seul moyen de le réveiller depuis un minuteur est `PostEvent` — qui **jette** les événements quand sa file est pleine.
50+
51+Le minuteur n'est donc pas ce qui décide. L'échéance est un état, vérifié en tête de chaque tour de boucle, exactement comme `announceOpenDocuments` vérifie si le serveur de langage est prêt. Le seul rôle du minuteur est de garantir qu'un tour ait lieu. Une poussée perdue coûte une sauvegarde en retard jusqu'à la frappe ou au clic suivant ; un design où le minuteur enregistrerait lui-même la perdrait tout court.
52+
53+## Pourquoi une sauvegarde automatique en échec n'ouvre pas de dialogue
54+
55+Une sauvegarde que personne n'a demandée ne doit pas interrompre par une fenêtre modale, et une modale qui revient toutes les deux secondes parce qu'un fichier est en lecture seule est pire que le problème qu'elle signale. Cela va donc dans la barre d'état, et l'échéance est effacée *avant* la tentative d'écriture : un fichier qui ne peut pas être écrit est essayé une fois par modification, et non indéfiniment.
56+
57+## Pourquoi la coloration TOML réutilise les classes de Go
58+
59+Ajouter `syntax.tomlkey` et compagnie aurait signifié que tous les thèmes — y compris ceux écrits par les utilisateurs — auraient silencieusement échoué à colorer le TOML jusqu'à leur mise à jour.
60+
61+Les classes déjà présentes conviennent : un en-tête de table nomme une structure, il se lit donc comme un type ; une clé nomme une chose, elle se lit donc comme un identifiant ; `true` et `false` sont des constantes parce que c'est ce qu'elles sont. Le résultat est que tout thème qui a jamais fonctionné colore le TOML correctement, sans modification et sans nouvelle clé. Le scanner est écrit à la main pour la même raison que l'émulateur de terminal — le TOML est un langage petit et entièrement spécifié, et c'est un fichier contre une troisième dépendance.
62+
63+## Liens avec le reste
64+
65+- Les clés exactes et leurs valeurs par défaut : [Référence des réglages de projet](../reference/project-settings.md)
66+- En mettre en place : [Donner ses propres réglages à un projet](../how-to/configure-a-project.md)
67+- L'autre fichier TOML que lit l'éditeur : [Format des fichiers de thème](../reference/themes.md)
68+- Où `settings` se situe parmi les paquets : [Architecture](architecture.md)
added docs/fr/explanation/project-tree.md +58 -0
new file mode 100644
@@ -0,0 +1,58 @@
1+# Arbre du projet — explication
2+
3+## De quoi s'agit-il ?
4+
5+`F9` ouvre une fenêtre listant les fichiers du projet, et `Entrée` sur l'un d'eux l'ouvre. Cette page traite des trois décisions contenues là-dedans : où l'arbre s'enracine, pourquoi c'est une fenêtre plutôt qu'un panneau sur le côté, et pourquoi il ne remarque pas tout seul l'apparition de fichiers.
6+
7+## Pourquoi la racine est le répertoire de travail
8+
9+L'éditeur contient déjà deux réponses différentes à « qu'est-ce que le projet ».
10+
11+Le serveur de langage remonte depuis le fichier ouvert jusqu'à trouver un `go.mod`, parce qu'un module a une frontière réelle — en être à l'intérieur est un fait à propos du code, et gopls a besoin de ce dossier précis pour travailler. Le fichier de réglages, lui, ne remonte pas du tout : `.turbo-go/settings.toml` est cherché dans le répertoire de travail et nulle part ailleurs.
12+
13+L'arbre suit le fichier de réglages, et il vaut la peine de dire pourquoi l'*autre* règle était tentante. Remonter jusqu'au `go.mod` ferait qu'ouvrir un fichier de n'importe où dans un projet montrerait tout le projet, ce que fait habituellement un explorateur. Mais cela signifie aussi que la racine de l'arbre dépend d'un fichier trois dossiers plus loin auquel vous n'avez peut-être pas pensé, et cela cesse d'être prévisible dès qu'un dépôt contient plus d'un module — un monorepo vous montrerait le module auquel appartient le fichier que vous avez ouvert par hasard.
14+
15+La règle retenue est celle qui tient en une phrase et qui est vraie partout dans l'éditeur : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.** Elle coûte quelque chose, et ce coût est nommé dans le [guide](../how-to/browse-a-project.md) : lancez depuis un sous-dossier et vous obtenez l'arbre de ce sous-dossier. La réponse est de lancer depuis la racine du projet, là où vous feriez `go build` et `git` de toute façon.
16+
17+## Pourquoi `.git` est masqué et rien d'autre
18+
19+Le dialogue Open masque toute entrée commençant par un point. Recopier cela ici était la chose évidente, et aurait été faux.
20+
21+`.turbo-go/settings.toml` est un fichier que cet éditeur demande aux gens d'éditer — c'est la raison d'être de la coloration TOML. `.gitignore` et `.qlty/qlty.toml` sont eux aussi des fichiers du projet. Un arbre qui les cacherait rendrait la configuration de l'éditeur inaccessible depuis l'explorateur de fichiers de l'éditeur, ce qui est un endroit curieux où aboutir.
22+
23+`.git` diffère par nature et non par orthographe : rien à l'intérieur n'est fait pour être ouvert à la main, et il contient assez d'objets pour enterrer tout le reste dans la liste. Un seul nom, masqué pour une raison énonçable. Respecter aussi `.gitignore` a été envisagé et écarté pour l'instant : cela masquerait `bin/` et `release/`, ce qui serait réellement plus agréable, et cela coûte un moteur de motifs gitignore — négations, `**`, ancrage — qui est une fonctionnalité à part entière plutôt qu'un détail d'arbre.
24+
25+## Pourquoi une fenêtre, pas un panneau
26+
27+Tous les autres éditeurs mettent leur arbre de fichiers dans une bande fixe à gauche. C'était l'alternative, et elle a été écartée à cause de ce qu'elle aurait coûté au reste de l'éditeur.
28+
29+Un panneau ancré signifie que le bureau n'est plus un simple rectangle où vivent les fenêtres. `Desktop` aurait besoin d'une notion de bords réservés ; `Window.fitInto` et les modes de croissance devraient les respecter ; agrandir voudrait dire « tout le bureau sauf le panneau » ; la mise en mosaïque et en cascade devraient en tenir compte. C'est une modification des fondations de toute l'interface, pour un seul widget.
30+
31+En fenêtre ordinaire, l'arbre obtient tout gratuitement et se comporte comme le reste : `F6` l'atteint, `Alt-2` le remonte, `[x]` le ferme, `[■]` lui donne tout le bureau, **Window ▸ Tile** le met à côté de votre fichier. Rien dans `ui` n'a eu à changer. Si un panneau ancré est voulu plus tard, c'est une fonctionnalité de `ui` à concevoir pour elle-même, plutôt qu'à faire passer en douce avec un explorateur de fichiers.
32+
33+## Pourquoi il n'y en a qu'un
34+
35+Deux arbres sur le même projet seraient deux vues d'une même chose sans rien pour les distinguer, et le projet ne peut pas changer pendant que l'éditeur tourne — la racine est fixée au démarrage. `F9` sur un arbre ouvert le remonte donc au lieu d'en créer un autre, exactement comme ouvrir un fichier déjà ouvert remonte sa fenêtre.
36+
37+## Pourquoi il ne surveille pas le disque
38+
39+Un arbre qui remarquerait `go build` produisant `bin/` serait meilleur. Le faire correctement signifie surveiller le système de fichiers, et en Go cela signifie `fsnotify` — une troisième dépendance, contre un projet qui s'en tient à deux depuis le début et qui traite l'ajout d'une dépendance comme une décision à défendre.
40+
41+Ce n'est pas non plus une petite dépendance en comportement : surveillances récursives, descripteurs épuisés sur les grosses arborescences, et une sémantique différente sur chaque plateforme — pour une fonctionnalité dont le mode d'échec est une ligne périmée dans une liste.
42+
43+L'arbre relit donc à la demande, et l'éditeur choisit les moments dont il peut être sûr. Enregistrer un fichier en est un : c'est l'éditeur qui l'a fait, il le sait donc. `F5` et `Ctrl-R` sont l'autre, parce qu'une compilation dans une fenêtre terminal est quelque chose que seul l'utilisateur sait terminée. Le rafraîchissement conserve la forme de l'arbre et ne relit que les dossiers réellement ouverts : il coûte ce qui est à l'écran, pas un parcours du projet.
44+
45+## Pourquoi l'arbre a ses propres clés de thème
46+
47+L'économie évidente était de le dessiner avec les clés `list.*` — un arbre est une liste, après tout, et cela n'aurait ajouté aucune clé que les thèmes utilisateur puissent manquer.
48+
49+Cela ne marche pas, et la raison mérite d'être consignée. `list.selected` est colorée pour ressortir sur un **dialogue**. Dans `turbo-classic` c'est blanc sur navy, et `window.body` est silver sur **navy** — un arbre dans une fenêtre aurait surligné sa ligne sélectionnée exactement dans la couleur de fond sur laquelle elle repose. La sélection aurait été invisible dans le thème que l'éditeur livre par défaut.
50+
51+D'où `tree.text`, `tree.directory`, `tree.selected` et `tree.unfocused`, et un test qui tient chaque thème livré à un contraste minimal entre la première et la troisième, de la même façon que les couleurs du curseur sont vérifiées. Un thème utilisateur qui n'en définit aucune retombe le long des points sur `default` : un arbre lisible, sans la distinction fichier/dossier, plutôt que rien du tout.
52+
53+## Liens avec le reste
54+
55+- Toutes les touches et toutes les règles, exactement : [Référence de l'arbre du projet](../reference/project-tree.md)
56+- L'utiliser : [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md)
57+- L'autre endroit où « le projet » est défini de la même façon : [Réglages de projet](project-settings.md)
58+- Où `filetree` se situe parmi les paquets : [Architecture](architecture.md)
new file mode 100644
@@ -0,0 +1,58 @@
1+# Arbre du projet — explication
2+
3+## De quoi s'agit-il ?
4+
5+`F9` ouvre une fenêtre listant les fichiers du projet, et `Entrée` sur l'un d'eux l'ouvre. Cette page traite des trois décisions contenues là-dedans : où l'arbre s'enracine, pourquoi c'est une fenêtre plutôt qu'un panneau sur le côté, et pourquoi il ne remarque pas tout seul l'apparition de fichiers.
6+
7+## Pourquoi la racine est le répertoire de travail
8+
9+L'éditeur contient déjà deux réponses différentes à « qu'est-ce que le projet ».
10+
11+Le serveur de langage remonte depuis le fichier ouvert jusqu'à trouver un `go.mod`, parce qu'un module a une frontière réelle — en être à l'intérieur est un fait à propos du code, et gopls a besoin de ce dossier précis pour travailler. Le fichier de réglages, lui, ne remonte pas du tout : `.turbo-go/settings.toml` est cherché dans le répertoire de travail et nulle part ailleurs.
12+
13+L'arbre suit le fichier de réglages, et il vaut la peine de dire pourquoi l'*autre* règle était tentante. Remonter jusqu'au `go.mod` ferait qu'ouvrir un fichier de n'importe où dans un projet montrerait tout le projet, ce que fait habituellement un explorateur. Mais cela signifie aussi que la racine de l'arbre dépend d'un fichier trois dossiers plus loin auquel vous n'avez peut-être pas pensé, et cela cesse d'être prévisible dès qu'un dépôt contient plus d'un module — un monorepo vous montrerait le module auquel appartient le fichier que vous avez ouvert par hasard.
14+
15+La règle retenue est celle qui tient en une phrase et qui est vraie partout dans l'éditeur : **le projet est le dossier depuis lequel vous avez lancé l'éditeur.** Elle coûte quelque chose, et ce coût est nommé dans le [guide](../how-to/browse-a-project.md) : lancez depuis un sous-dossier et vous obtenez l'arbre de ce sous-dossier. La réponse est de lancer depuis la racine du projet, là où vous feriez `go build` et `git` de toute façon.
16+
17+## Pourquoi `.git` est masqué et rien d'autre
18+
19+Le dialogue Open masque toute entrée commençant par un point. Recopier cela ici était la chose évidente, et aurait été faux.
20+
21+`.turbo-go/settings.toml` est un fichier que cet éditeur demande aux gens d'éditer — c'est la raison d'être de la coloration TOML. `.gitignore` et `.qlty/qlty.toml` sont eux aussi des fichiers du projet. Un arbre qui les cacherait rendrait la configuration de l'éditeur inaccessible depuis l'explorateur de fichiers de l'éditeur, ce qui est un endroit curieux où aboutir.
22+
23+`.git` diffère par nature et non par orthographe : rien à l'intérieur n'est fait pour être ouvert à la main, et il contient assez d'objets pour enterrer tout le reste dans la liste. Un seul nom, masqué pour une raison énonçable. Respecter aussi `.gitignore` a été envisagé et écarté pour l'instant : cela masquerait `bin/` et `release/`, ce qui serait réellement plus agréable, et cela coûte un moteur de motifs gitignore — négations, `**`, ancrage — qui est une fonctionnalité à part entière plutôt qu'un détail d'arbre.
24+
25+## Pourquoi une fenêtre, pas un panneau
26+
27+Tous les autres éditeurs mettent leur arbre de fichiers dans une bande fixe à gauche. C'était l'alternative, et elle a été écartée à cause de ce qu'elle aurait coûté au reste de l'éditeur.
28+
29+Un panneau ancré signifie que le bureau n'est plus un simple rectangle où vivent les fenêtres. `Desktop` aurait besoin d'une notion de bords réservés ; `Window.fitInto` et les modes de croissance devraient les respecter ; agrandir voudrait dire « tout le bureau sauf le panneau » ; la mise en mosaïque et en cascade devraient en tenir compte. C'est une modification des fondations de toute l'interface, pour un seul widget.
30+
31+En fenêtre ordinaire, l'arbre obtient tout gratuitement et se comporte comme le reste : `F6` l'atteint, `Alt-2` le remonte, `[x]` le ferme, `[■]` lui donne tout le bureau, **Window ▸ Tile** le met à côté de votre fichier. Rien dans `ui` n'a eu à changer. Si un panneau ancré est voulu plus tard, c'est une fonctionnalité de `ui` à concevoir pour elle-même, plutôt qu'à faire passer en douce avec un explorateur de fichiers.
32+
33+## Pourquoi il n'y en a qu'un
34+
35+Deux arbres sur le même projet seraient deux vues d'une même chose sans rien pour les distinguer, et le projet ne peut pas changer pendant que l'éditeur tourne — la racine est fixée au démarrage. `F9` sur un arbre ouvert le remonte donc au lieu d'en créer un autre, exactement comme ouvrir un fichier déjà ouvert remonte sa fenêtre.
36+
37+## Pourquoi il ne surveille pas le disque
38+
39+Un arbre qui remarquerait `go build` produisant `bin/` serait meilleur. Le faire correctement signifie surveiller le système de fichiers, et en Go cela signifie `fsnotify` — une troisième dépendance, contre un projet qui s'en tient à deux depuis le début et qui traite l'ajout d'une dépendance comme une décision à défendre.
40+
41+Ce n'est pas non plus une petite dépendance en comportement : surveillances récursives, descripteurs épuisés sur les grosses arborescences, et une sémantique différente sur chaque plateforme — pour une fonctionnalité dont le mode d'échec est une ligne périmée dans une liste.
42+
43+L'arbre relit donc à la demande, et l'éditeur choisit les moments dont il peut être sûr. Enregistrer un fichier en est un : c'est l'éditeur qui l'a fait, il le sait donc. `F5` et `Ctrl-R` sont l'autre, parce qu'une compilation dans une fenêtre terminal est quelque chose que seul l'utilisateur sait terminée. Le rafraîchissement conserve la forme de l'arbre et ne relit que les dossiers réellement ouverts : il coûte ce qui est à l'écran, pas un parcours du projet.
44+
45+## Pourquoi l'arbre a ses propres clés de thème
46+
47+L'économie évidente était de le dessiner avec les clés `list.*` — un arbre est une liste, après tout, et cela n'aurait ajouté aucune clé que les thèmes utilisateur puissent manquer.
48+
49+Cela ne marche pas, et la raison mérite d'être consignée. `list.selected` est colorée pour ressortir sur un **dialogue**. Dans `turbo-classic` c'est blanc sur navy, et `window.body` est silver sur **navy** — un arbre dans une fenêtre aurait surligné sa ligne sélectionnée exactement dans la couleur de fond sur laquelle elle repose. La sélection aurait été invisible dans le thème que l'éditeur livre par défaut.
50+
51+D'où `tree.text`, `tree.directory`, `tree.selected` et `tree.unfocused`, et un test qui tient chaque thème livré à un contraste minimal entre la première et la troisième, de la même façon que les couleurs du curseur sont vérifiées. Un thème utilisateur qui n'en définit aucune retombe le long des points sur `default` : un arbre lisible, sans la distinction fichier/dossier, plutôt que rien du tout.
52+
53+## Liens avec le reste
54+
55+- Toutes les touches et toutes les règles, exactement : [Référence de l'arbre du projet](../reference/project-tree.md)
56+- L'utiliser : [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md)
57+- L'autre endroit où « le projet » est défini de la même façon : [Réglages de projet](project-settings.md)
58+- Où `filetree` se situe parmi les paquets : [Architecture](architecture.md)
added docs/fr/explanation/snippets.md +62 -0
new file mode 100644
@@ -0,0 +1,62 @@
1+# Snippets — explication
2+
3+## De quoi s'agit-il ?
4+
5+Un menu **Snippets** dont le contenu vient d'un fichier TOML, et un snippet choisi déposé dans le fichier que vous éditez. Cette page traite des trois décisions qui lui donnent sa forme : pourquoi le menu est reconstruit à chaque ouverture, pourquoi l'éditeur a gagné de vrais sous-menus pour lui, et pourquoi l'insertion réindente.
6+
7+## Pourquoi le menu est construit au moment où il s'ouvre
8+
9+Tous les autres menus de l'éditeur sont décidés une fois, dans `New()`. Celui-ci ne peut pas l'être, et pour deux raisons indépendantes.
10+
11+La première est le fichier. Les snippets vivent dans du TOML, et tout l'intérêt est que vous l'éditiez — souvent dans cet éditeur, dans la fenêtre que l'entrée **Create snippets file** vient d'ouvrir pour vous. Un menu construit au démarrage montrerait l'état du fichier au lancement, et il faudrait redémarrer pour voir un snippet qu'on vient d'écrire. C'est le genre de friction qui fait qu'une fonctionnalité n'est pas utilisée du tout.
12+
13+La seconde est la fenêtre au premier plan. Le menu est filtré par ce que vous éditez, donc il change quand vous appuyez sur `F6`. Il n'existe aucun instant du démarrage où la réponse existe.
14+
15+`ui.Menu` a donc gagné un champ `OnOpen` : une fonction que la barre appelle juste avant de dérouler un menu, laissant son propriétaire regarnir `Items` d'abord. C'est le même mécanisme de communication ascendante que partout ailleurs dans ce code — un champ fonction, pas une interface — et il s'exécute exactement au moment où le contenu va être vu, pas plus souvent.
16+
17+## Pourquoi l'éditeur a gagné des sous-menus
18+
19+`ui.MenuItem` ne savait pas imbriquer, et l'ajouter a été la plus grosse pièce de ce travail : un second panneau à placer et à dessiner, des flèches qui signifient « plus profond » et « ressortir », le pointeur qui ouvre une branche au survol et la referme en la quittant, et une fermeture qui range les deux panneaux d'un coup.
20+
21+L'alternative était un seul panneau plat avec les groupes en intitulés grisés entre des filets. Cela fonctionne, ne demande rien de neuf, et s'effondre sur le cas même pour lequel la fonctionnalité existe : un projet de trente snippets donne un menu plus haut que le terminal. Un regroupement qui étiquette sans replier ne résout pas le problème qu'il semble résoudre.
22+
23+C'est délibérément **un seul niveau**. Le format est des groupes contenant des snippets — exactement un niveau — et une profondeur générale supposerait de remplacer les deux indices de la barre par un chemin, dans le widget dont dépendent déjà tous les dialogues et tous les tests de menu. C'est du travail spéculatif sur la partie la plus porteuse de l'interface.
24+
25+Deux détails du sous-menu méritent d'être nommés, parce qu'ils ont été choisis et non subis :
26+
27+- **Droite et gauche sont asymétriques avec Échap.** Droite ouvre une branche, ou passe au menu suivant quand l'entrée n'en a pas : elle signifie donc toujours « plus profond », où que l'on soit. Gauche *ressort* d'un sous-menu vers son parent, tandis qu'Échap referme tout le menu — parce qu'annuler doit vouloir dire annuler, de n'importe où.
28+- **Le panneau bascule à gauche, et sa largeur est aussi bornée.** Un sous-menu qui dépasserait le bord droit est dessiné de l'autre côté de son parent. Basculer ne suffit pas : un panneau plus large que le terminal ne peut pas être rendu visible en le déplaçant, donc la largeur est bornée aussi et les intitulés longs sont coupés par le peintre. Un cadre sans bord droit paraît cassé d'une façon dont un intitulé tronqué ne l'est pas.
29+
30+## Pourquoi l'insertion réindente
31+
32+Un snippet est du texte, et l'implémentation évidente est de l'insérer. C'est juste pour une seule ligne et faux pour tout le reste, c'est-à-dire pour l'essentiel de ce que les gens gardent en snippets.
33+
34+Déposé tel quel, un corps multi-ligne repart en colonne zéro. Inséré dans une fonction, dans une boucle, dans un `switch` — là où l'on insère justement un `if err != nil` — le résultat est un texte dont aucun formateur, aucun compilateur et aucun lecteur ne se satisfait, et la première chose qu'on fait est de le réindenter à la main. Une fonctionnalité dont la sortie doit être corrigée chaque fois ne fait gagner de temps à personne.
35+
36+Les lignes après la première reçoivent donc l'indentation de la ligne où était le curseur. Cela recopie ce que le fichier emploie déjà — tabulations ou espaces, en telle quantité — plutôt que d'imposer un choix, ce qui compte dans un projet à l'histoire mêlée.
37+
38+Deux décisions plus petites à l'intérieur :
39+
40+- **Une ligne vide du corps reste vide.** La compléter jusqu'à l'indentation y mettrait des espaces en fin de ligne, que tout formateur supprime ensuite — du bruit dans le diff de l'enregistrement suivant.
41+- **C'est une seule annulation.** Un snippet est une seule action pour qui l'a choisi, donc `Ctrl-Z` doit tout reprendre. Cela découle de faire toute l'insertion en un seul `ReplaceRange`, la règle que le buffer impose déjà à toute autre modification.
42+
43+Les emplacements et les tabulations successives — `${1:nom}` et le passage de l'un à l'autre — ont été envisagés et laissés de côté. C'est une seconde fonctionnalité, avec son propre état à maintenir à travers les modifications, alors que ce qui était demandé est du texte réutilisable.
44+
45+## Pourquoi deux fichiers, et pourquoi le projet gagne
46+
47+Vos snippets vous appartiennent et doivent vous suivre d'un projet à l'autre ; ceux d'un projet lui appartiennent et doivent arriver avec un clone. Ni l'un ni l'autre n'est la réponse complète, donc les deux sont lus.
48+
49+Quand un nom entre en conflit dans le même groupe, celui du projet remplace le vôtre. C'est le plus spécifique des deux énoncés, et c'est celui dont une équipe a convenu — la même raison qui fait qu'un drapeau `-theme` l'emporte sur le réglage d'un projet, tandis que le réglage d'un projet l'emporte sur le défaut intégré.
50+
51+## Pourquoi un fichier illisible est bruyant
52+
53+Une faute de frappe dans le TOML pourrait faire disparaître tous les snippets en silence et laisser un menu ne contenant que **Create snippets file** — ce qui ressemble exactement à un projet sans snippets, et vous envoie créer un fichier que vous avez déjà.
54+
55+Le menu affiche donc un `Cannot read snippets` grisé là où les groupes seraient. Il ne peut pas être choisi, il est là où vous regardiez, et l'entrée de création reste en dessous : il y a une issue dans les deux cas.
56+
57+## Liens avec le reste
58+
59+- Toutes les clés et toutes les règles : [Référence des snippets](../reference/snippets.md)
60+- Les mettre en place : [Insérer des snippets depuis un menu](../how-to/use-snippets.md)
61+- L'autre fichier du même dossier : [Réglages de projet](project-settings.md)
62+- Les noms de langages qu'emploie `languages` : [Langages colorés](../reference/languages.md)
new file mode 100644
@@ -0,0 +1,62 @@
1+# Snippets — explication
2+
3+## De quoi s'agit-il ?
4+
5+Un menu **Snippets** dont le contenu vient d'un fichier TOML, et un snippet choisi déposé dans le fichier que vous éditez. Cette page traite des trois décisions qui lui donnent sa forme : pourquoi le menu est reconstruit à chaque ouverture, pourquoi l'éditeur a gagné de vrais sous-menus pour lui, et pourquoi l'insertion réindente.
6+
7+## Pourquoi le menu est construit au moment où il s'ouvre
8+
9+Tous les autres menus de l'éditeur sont décidés une fois, dans `New()`. Celui-ci ne peut pas l'être, et pour deux raisons indépendantes.
10+
11+La première est le fichier. Les snippets vivent dans du TOML, et tout l'intérêt est que vous l'éditiez — souvent dans cet éditeur, dans la fenêtre que l'entrée **Create snippets file** vient d'ouvrir pour vous. Un menu construit au démarrage montrerait l'état du fichier au lancement, et il faudrait redémarrer pour voir un snippet qu'on vient d'écrire. C'est le genre de friction qui fait qu'une fonctionnalité n'est pas utilisée du tout.
12+
13+La seconde est la fenêtre au premier plan. Le menu est filtré par ce que vous éditez, donc il change quand vous appuyez sur `F6`. Il n'existe aucun instant du démarrage où la réponse existe.
14+
15+`ui.Menu` a donc gagné un champ `OnOpen` : une fonction que la barre appelle juste avant de dérouler un menu, laissant son propriétaire regarnir `Items` d'abord. C'est le même mécanisme de communication ascendante que partout ailleurs dans ce code — un champ fonction, pas une interface — et il s'exécute exactement au moment où le contenu va être vu, pas plus souvent.
16+
17+## Pourquoi l'éditeur a gagné des sous-menus
18+
19+`ui.MenuItem` ne savait pas imbriquer, et l'ajouter a été la plus grosse pièce de ce travail : un second panneau à placer et à dessiner, des flèches qui signifient « plus profond » et « ressortir », le pointeur qui ouvre une branche au survol et la referme en la quittant, et une fermeture qui range les deux panneaux d'un coup.
20+
21+L'alternative était un seul panneau plat avec les groupes en intitulés grisés entre des filets. Cela fonctionne, ne demande rien de neuf, et s'effondre sur le cas même pour lequel la fonctionnalité existe : un projet de trente snippets donne un menu plus haut que le terminal. Un regroupement qui étiquette sans replier ne résout pas le problème qu'il semble résoudre.
22+
23+C'est délibérément **un seul niveau**. Le format est des groupes contenant des snippets — exactement un niveau — et une profondeur générale supposerait de remplacer les deux indices de la barre par un chemin, dans le widget dont dépendent déjà tous les dialogues et tous les tests de menu. C'est du travail spéculatif sur la partie la plus porteuse de l'interface.
24+
25+Deux détails du sous-menu méritent d'être nommés, parce qu'ils ont été choisis et non subis :
26+
27+- **Droite et gauche sont asymétriques avec Échap.** Droite ouvre une branche, ou passe au menu suivant quand l'entrée n'en a pas : elle signifie donc toujours « plus profond », où que l'on soit. Gauche *ressort* d'un sous-menu vers son parent, tandis qu'Échap referme tout le menu — parce qu'annuler doit vouloir dire annuler, de n'importe où.
28+- **Le panneau bascule à gauche, et sa largeur est aussi bornée.** Un sous-menu qui dépasserait le bord droit est dessiné de l'autre côté de son parent. Basculer ne suffit pas : un panneau plus large que le terminal ne peut pas être rendu visible en le déplaçant, donc la largeur est bornée aussi et les intitulés longs sont coupés par le peintre. Un cadre sans bord droit paraît cassé d'une façon dont un intitulé tronqué ne l'est pas.
29+
30+## Pourquoi l'insertion réindente
31+
32+Un snippet est du texte, et l'implémentation évidente est de l'insérer. C'est juste pour une seule ligne et faux pour tout le reste, c'est-à-dire pour l'essentiel de ce que les gens gardent en snippets.
33+
34+Déposé tel quel, un corps multi-ligne repart en colonne zéro. Inséré dans une fonction, dans une boucle, dans un `switch` — là où l'on insère justement un `if err != nil` — le résultat est un texte dont aucun formateur, aucun compilateur et aucun lecteur ne se satisfait, et la première chose qu'on fait est de le réindenter à la main. Une fonctionnalité dont la sortie doit être corrigée chaque fois ne fait gagner de temps à personne.
35+
36+Les lignes après la première reçoivent donc l'indentation de la ligne où était le curseur. Cela recopie ce que le fichier emploie déjà — tabulations ou espaces, en telle quantité — plutôt que d'imposer un choix, ce qui compte dans un projet à l'histoire mêlée.
37+
38+Deux décisions plus petites à l'intérieur :
39+
40+- **Une ligne vide du corps reste vide.** La compléter jusqu'à l'indentation y mettrait des espaces en fin de ligne, que tout formateur supprime ensuite — du bruit dans le diff de l'enregistrement suivant.
41+- **C'est une seule annulation.** Un snippet est une seule action pour qui l'a choisi, donc `Ctrl-Z` doit tout reprendre. Cela découle de faire toute l'insertion en un seul `ReplaceRange`, la règle que le buffer impose déjà à toute autre modification.
42+
43+Les emplacements et les tabulations successives — `${1:nom}` et le passage de l'un à l'autre — ont été envisagés et laissés de côté. C'est une seconde fonctionnalité, avec son propre état à maintenir à travers les modifications, alors que ce qui était demandé est du texte réutilisable.
44+
45+## Pourquoi deux fichiers, et pourquoi le projet gagne
46+
47+Vos snippets vous appartiennent et doivent vous suivre d'un projet à l'autre ; ceux d'un projet lui appartiennent et doivent arriver avec un clone. Ni l'un ni l'autre n'est la réponse complète, donc les deux sont lus.
48+
49+Quand un nom entre en conflit dans le même groupe, celui du projet remplace le vôtre. C'est le plus spécifique des deux énoncés, et c'est celui dont une équipe a convenu — la même raison qui fait qu'un drapeau `-theme` l'emporte sur le réglage d'un projet, tandis que le réglage d'un projet l'emporte sur le défaut intégré.
50+
51+## Pourquoi un fichier illisible est bruyant
52+
53+Une faute de frappe dans le TOML pourrait faire disparaître tous les snippets en silence et laisser un menu ne contenant que **Create snippets file** — ce qui ressemble exactement à un projet sans snippets, et vous envoie créer un fichier que vous avez déjà.
54+
55+Le menu affiche donc un `Cannot read snippets` grisé là où les groupes seraient. Il ne peut pas être choisi, il est là où vous regardiez, et l'entrée de création reste en dessous : il y a une issue dans les deux cas.
56+
57+## Liens avec le reste
58+
59+- Toutes les clés et toutes les règles : [Référence des snippets](../reference/snippets.md)
60+- Les mettre en place : [Insérer des snippets depuis un menu](../how-to/use-snippets.md)
61+- L'autre fichier du même dossier : [Réglages de projet](project-settings.md)
62+- Les noms de langages qu'emploie `languages` : [Langages colorés](../reference/languages.md)
added docs/fr/explanation/terminal-windows.md +72 -0
new file mode 100644
@@ -0,0 +1,72 @@
1+# Fenêtres terminal — explication
2+
3+## De quoi s'agit-il ?
4+
5+`F8` ouvre une fenêtre contenant un shell. Cette phrase masque l'essentiel du travail : pour mettre un shell dans une fenêtre, un éditeur doit devenir un émulateur de terminal. Cette page raconte ce que cela a impliqué, et quelles solutions moins coûteuses ont été écartées en chemin.
6+
7+## Pourquoi un vrai pseudo-terminal
8+
9+La version bon marché évidente consiste à lancer une commande avec `exec.Command`, à capturer sa sortie et à l'afficher dans un panneau en lecture seule. Beaucoup d'éditeurs livrent exactement cela, et cela échoue précisément sur ce pour quoi on veut un terminal.
10+
11+Un programme se comporte différemment quand sa sortie est un tube plutôt qu'un terminal. `go test` abandonne ses couleurs. `git log` ne pagine pas. `ls` affiche un nom par ligne. Rien d'interactif ne fonctionne : ni `vim`, ni `ssh`, ni `git rebase -i`, ni la réponse à une invite, ni `Ctrl-C` — sans terminal de contrôle, il n'y a aucun signal à envoyer.
12+
13+Le shell reçoit donc un vrai pseudo-terminal : `/dev/ptmx` sur les deux plateformes supportées, le fils dans une session à lui avec l'esclave comme terminal de contrôle, et `TIOCSWINSZ` à chaque redimensionnement de la fenêtre. Cela offre gratuitement le contrôle de tâches, `isatty`, `SIGWINCH` et la couleur, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.
14+
15+Le prix à payer est que l'éditeur doit ensuite relire ce qu'un terminal est censé comprendre — c'est-à-dire l'émulateur.
16+
17+## Pourquoi écrire l'émulateur plutôt que d'en emprunter un
18+
19+Go dispose de bibliothèques d'émulation de terminal. En prendre une aurait signifié une troisième dépendance, dans un projet qui en a exactement deux et qui affiche une réticence assumée à en ajouter une troisième.
20+
21+Ce que l'on met en balance n'est pas « émulateur » contre « pas d'émulateur », mais *quelle quantité* d'émulateur. Ce dont ont besoin un shell, `go test`, `git`, `less`, `htop` et `vim` forme une liste bien délimitée : déplacement du curseur, la famille effacement / insertion-suppression, une région de défilement, SGR dans ses trois profondeurs de couleur, l'écran alternatif, le retour à la ligne automatique, la visibilité du curseur et les touches curseur application. Cela représente environ six cents lignes, c'est écrit noir sur blanc dans ECMA-48, et cela se teste en écrivant des octets en entrée et en lisant une grille en sortie — sans shell, sans temporisation, sans écran.
22+
23+À comparer avec ce qu'apporte une bibliothèque généraliste : jeux de caractères, protocoles de rapport souris, sixel, collage entre crochets, rapports d'état DEC. Tout cela est réel, rien n'est nécessaire ici, et tout cela constitue de la surface à maintenir.
24+
25+L'émulateur est donc écrit à la main et volontairement partiel, et la [référence](../reference/terminal.md) dit exactement où il s'arrête. Un programme qui demande quelque chose d'absent obtient le silence plutôt que de la corruption, ce qui est le bon mode d'échec : `htop` s'affiche, la sortie `sixel` n'apparaît simplement pas.
26+
27+## À qui revient la touche
28+
29+C'est la décision qui pèse le plus sur la sensation d'usage de l'éditeur, et la première version s'était trompée.
30+
31+Les raccourcis globaux de l'éditeur sont examinés avant que la fenêtre du premier plan ne voie quoi que ce soit. C'est juste pour un éditeur, et faux dès l'instant où cette fenêtre est un shell, parce que les deux revendiquent les mêmes touches. `Ctrl-W` ferme une fenêtre dans Turbo C et supprime un mot dans tous les shells. `Ctrl-F` est Rechercher ici et avancer-d'un-caractère dans readline. `Ctrl-C` est copier, et aussi le seul moyen d'arrêter une commande emballée.
32+
33+La règle retenue inverse l'ordre habituel, mais uniquement pour les touches réellement disputées :
34+
35+**Un terminal ayant le focus reçoit tout, sauf les touches de fonction, `Alt-X` et `Alt-0`…`Alt-9`.**
36+
37+Ces exceptions ne sont pas un compromis entre les deux revendications — ce sont la *sortie*. Un programme plein écran comme `vim` recouvre la fenêtre et s'empare de la souris ; sans touche réservée, il n'y aurait aucun moyen d'atteindre la barre de menus, de changer de fenêtre ou de quitter l'éditeur sans d'abord quitter le programme. Les touches de fonction sont la réservation naturelle parce que c'est vers elles qu'un utilisateur de terminal se tourne le moins, et `Alt-X` parce que quitter un éditeur ne devrait jamais faire de doute.
38+
39+Ce que cela coûte est réel et mérite d'être nommé : `Alt-B` et `Alt-F` atteignent le shell, donc le déplacement par mot de readline fonctionne, mais un programme dans une fenêtre terminal ne verra jamais `F1``F12`. Le menu par touches de fonction de `htop` est inaccessible. C'est l'arbitrage, et il a été rendu en faveur du fait de toujours pouvoir sortir.
40+
41+## Pourquoi fermer un terminal ne demande rien
42+
43+Fermer un fichier modifié demande s'il faut l'enregistrer. Fermer un terminal ne demande rien du tout, et cette asymétrie est délibérée.
44+
45+Une fenêtre au travail non enregistré contient quelque chose qui serait *perdu*. Un terminal contient un processus en cours, et fermer la fenêtre est la façon ordinaire de dire qu'on en a fini — comme on ferme l'onglet d'un émulateur de terminal. Demander « êtes-vous sûr ? » à chaque fois désapprendrait la réponse à quiconque, ce qui est le problème général des confirmations qui se déclenchent sur le cas courant.
46+
47+Quitter l'éditeur ferme tous les terminaux pour la même raison, en sens inverse : une fenêtre est la seule prise sur ces shells, donc les laisser survivre à l'éditeur abandonnerait des processus que plus rien ne peut atteindre.
48+
49+## Pourquoi les redessins sont cadencés
50+
51+Le shell écrit depuis une goroutine à lui ; l'éditeur dessine depuis la principale. Réveiller la boucle d'événements à chaque bloc de sortie semblait évident et se trompait deux fois.
52+
53+Une compilation écrit bien plus vite qu'un écran ne peut être utilement repeint : la plupart de ces redessins sont donc du gaspillage. Pire, le mécanisme de réveil de la boucle depuis une autre goroutine est le `PostEvent` de tcell, qui **jette** les événements quand sa file est pleine — de sorte que la rafale qui a le plus besoin d'un redessin est justement celle dont le réveil final est perdu, et la fenêtre se fige en pleine compilation sur un texte périmé. Ce bug exact avait déjà été rencontré une fois ailleurs dans cet éditeur, du côté du serveur de langage.
54+
55+La vue positionne donc un drapeau, et une horloge demande un redessin soixante fois par seconde tant que le drapeau est levé. Un réveil perdu ne peut rien bloquer, puisque le tic suivant est à seize millisecondes.
56+
57+## Windows : une pseudo-console, et pourquoi c'est un fichier à part
58+
59+Les pseudo-terminaux sont la seule partie non portable de tout ceci. Linux et macOS passent tous deux par `/dev/ptmx` et ne diffèrent que par l'`ioctl` qui accorde l'esclave. Windows n'a rien de tel : il a des **pseudo-consoles** — ConPTY, depuis Windows 10 version 1809 — un objet détenu par `conhost.exe` et relié à deux tubes de l'éditeur. Ce que le shell affiche arrive sur l'un des tubes sous la forme des mêmes séquences VT qu'un shell Unix écrit dans un pty, ce qui est la raison pour laquelle l'émulateur de ce côté n'a eu besoin d'aucun code Windows ; ce que l'éditeur écrit dans l'autre tube parvient au shell comme des frappes de touches.
60+
61+Trois choses en ont fait un fichier à part plutôt qu'une variante du fichier Unix. Le processus doit être créé à la main, parce que l'attacher à une pseudo-console exige un enregistrement de démarrage étendu que l'`os/exec` de Go ne sait pas porter. Le shell est `%COMSPEC%` — cmd.exe — plutôt que `$SHELL`, et cmd.exe lit sa ligne de commande selon ses propres règles : la ligne qui lance une commande du menu est donc composée pour lui mot pour mot, la commande entre une seule paire de guillemets, au lieu d'être échappée comme tout autre programme l'attend. Et `conhost.exe` garde le tube de sortie ouvert jusqu'à la fermeture de la console, quoi que fasse le shell ; une goroutine attend donc la fin du shell puis ferme la console — c'est ce qui transforme une commande terminée en la fin d'entrée sur laquelle la fenêtre compte pour le dire. Le contrôle de tâches est celui de cmd.exe et non du noyau : `Ctrl-C` interrompt le programme en cours comme il le ferait dans une fenêtre de console.
62+
63+Les fichiers par plateforme restent séparés pour que chaque plateforme ait une implémentation honnête derrière une petite interface, et qu'une plateforme qui n'a ni l'un ni l'autre — les BSD, aujourd'hui — reçoive `ErrUnsupported`, que `F8` le dise clairement, et que rien d'autre dans l'éditeur ne soit affecté.
64+
65+**Le chemin Windows a été compilé et vérifié, pas exécuté.** turbo-core est développé sous Linux et son auteur travaille sous macOS. Les parties pures — le bloc d'environnement, la ligne de commande que veut cmd.exe — sont testées unitairement sur toute plateforme, et les appels à l'API compilent et passent `go vet` sous `GOOS=windows` ; personne n'a encore appuyé sur `F8` sur une machine Windows. [Le guide](../how-to/use-a-terminal.md) dit quoi essayer en premier.
66+
67+## Liens avec le reste
68+
69+- La liste exacte de ce qui est implémenté : [référence des fenêtres terminal](../reference/terminal.md)
70+- En utiliser une : [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md)
71+-`terminal` se situe parmi les paquets, et pourquoi le graphe est orienté : [Architecture](architecture.md)
72+- Le décompte de dépendances que cette page ne cesse d'invoquer : [Décisions de conception](design-decisions.md)
new file mode 100644
@@ -0,0 +1,72 @@
1+# Fenêtres terminal — explication
2+
3+## De quoi s'agit-il ?
4+
5+`F8` ouvre une fenêtre contenant un shell. Cette phrase masque l'essentiel du travail : pour mettre un shell dans une fenêtre, un éditeur doit devenir un émulateur de terminal. Cette page raconte ce que cela a impliqué, et quelles solutions moins coûteuses ont été écartées en chemin.
6+
7+## Pourquoi un vrai pseudo-terminal
8+
9+La version bon marché évidente consiste à lancer une commande avec `exec.Command`, à capturer sa sortie et à l'afficher dans un panneau en lecture seule. Beaucoup d'éditeurs livrent exactement cela, et cela échoue précisément sur ce pour quoi on veut un terminal.
10+
11+Un programme se comporte différemment quand sa sortie est un tube plutôt qu'un terminal. `go test` abandonne ses couleurs. `git log` ne pagine pas. `ls` affiche un nom par ligne. Rien d'interactif ne fonctionne : ni `vim`, ni `ssh`, ni `git rebase -i`, ni la réponse à une invite, ni `Ctrl-C` — sans terminal de contrôle, il n'y a aucun signal à envoyer.
12+
13+Le shell reçoit donc un vrai pseudo-terminal : `/dev/ptmx` sur les deux plateformes supportées, le fils dans une session à lui avec l'esclave comme terminal de contrôle, et `TIOCSWINSZ` à chaque redimensionnement de la fenêtre. Cela offre gratuitement le contrôle de tâches, `isatty`, `SIGWINCH` et la couleur, parce que ce sont les mêmes mécanismes que ceux de tous les autres terminaux.
14+
15+Le prix à payer est que l'éditeur doit ensuite relire ce qu'un terminal est censé comprendre — c'est-à-dire l'émulateur.
16+
17+## Pourquoi écrire l'émulateur plutôt que d'en emprunter un
18+
19+Go dispose de bibliothèques d'émulation de terminal. En prendre une aurait signifié une troisième dépendance, dans un projet qui en a exactement deux et qui affiche une réticence assumée à en ajouter une troisième.
20+
21+Ce que l'on met en balance n'est pas « émulateur » contre « pas d'émulateur », mais *quelle quantité* d'émulateur. Ce dont ont besoin un shell, `go test`, `git`, `less`, `htop` et `vim` forme une liste bien délimitée : déplacement du curseur, la famille effacement / insertion-suppression, une région de défilement, SGR dans ses trois profondeurs de couleur, l'écran alternatif, le retour à la ligne automatique, la visibilité du curseur et les touches curseur application. Cela représente environ six cents lignes, c'est écrit noir sur blanc dans ECMA-48, et cela se teste en écrivant des octets en entrée et en lisant une grille en sortie — sans shell, sans temporisation, sans écran.
22+
23+À comparer avec ce qu'apporte une bibliothèque généraliste : jeux de caractères, protocoles de rapport souris, sixel, collage entre crochets, rapports d'état DEC. Tout cela est réel, rien n'est nécessaire ici, et tout cela constitue de la surface à maintenir.
24+
25+L'émulateur est donc écrit à la main et volontairement partiel, et la [référence](../reference/terminal.md) dit exactement où il s'arrête. Un programme qui demande quelque chose d'absent obtient le silence plutôt que de la corruption, ce qui est le bon mode d'échec : `htop` s'affiche, la sortie `sixel` n'apparaît simplement pas.
26+
27+## À qui revient la touche
28+
29+C'est la décision qui pèse le plus sur la sensation d'usage de l'éditeur, et la première version s'était trompée.
30+
31+Les raccourcis globaux de l'éditeur sont examinés avant que la fenêtre du premier plan ne voie quoi que ce soit. C'est juste pour un éditeur, et faux dès l'instant où cette fenêtre est un shell, parce que les deux revendiquent les mêmes touches. `Ctrl-W` ferme une fenêtre dans Turbo C et supprime un mot dans tous les shells. `Ctrl-F` est Rechercher ici et avancer-d'un-caractère dans readline. `Ctrl-C` est copier, et aussi le seul moyen d'arrêter une commande emballée.
32+
33+La règle retenue inverse l'ordre habituel, mais uniquement pour les touches réellement disputées :
34+
35+**Un terminal ayant le focus reçoit tout, sauf les touches de fonction, `Alt-X` et `Alt-0`…`Alt-9`.**
36+
37+Ces exceptions ne sont pas un compromis entre les deux revendications — ce sont la *sortie*. Un programme plein écran comme `vim` recouvre la fenêtre et s'empare de la souris ; sans touche réservée, il n'y aurait aucun moyen d'atteindre la barre de menus, de changer de fenêtre ou de quitter l'éditeur sans d'abord quitter le programme. Les touches de fonction sont la réservation naturelle parce que c'est vers elles qu'un utilisateur de terminal se tourne le moins, et `Alt-X` parce que quitter un éditeur ne devrait jamais faire de doute.
38+
39+Ce que cela coûte est réel et mérite d'être nommé : `Alt-B` et `Alt-F` atteignent le shell, donc le déplacement par mot de readline fonctionne, mais un programme dans une fenêtre terminal ne verra jamais `F1``F12`. Le menu par touches de fonction de `htop` est inaccessible. C'est l'arbitrage, et il a été rendu en faveur du fait de toujours pouvoir sortir.
40+
41+## Pourquoi fermer un terminal ne demande rien
42+
43+Fermer un fichier modifié demande s'il faut l'enregistrer. Fermer un terminal ne demande rien du tout, et cette asymétrie est délibérée.
44+
45+Une fenêtre au travail non enregistré contient quelque chose qui serait *perdu*. Un terminal contient un processus en cours, et fermer la fenêtre est la façon ordinaire de dire qu'on en a fini — comme on ferme l'onglet d'un émulateur de terminal. Demander « êtes-vous sûr ? » à chaque fois désapprendrait la réponse à quiconque, ce qui est le problème général des confirmations qui se déclenchent sur le cas courant.
46+
47+Quitter l'éditeur ferme tous les terminaux pour la même raison, en sens inverse : une fenêtre est la seule prise sur ces shells, donc les laisser survivre à l'éditeur abandonnerait des processus que plus rien ne peut atteindre.
48+
49+## Pourquoi les redessins sont cadencés
50+
51+Le shell écrit depuis une goroutine à lui ; l'éditeur dessine depuis la principale. Réveiller la boucle d'événements à chaque bloc de sortie semblait évident et se trompait deux fois.
52+
53+Une compilation écrit bien plus vite qu'un écran ne peut être utilement repeint : la plupart de ces redessins sont donc du gaspillage. Pire, le mécanisme de réveil de la boucle depuis une autre goroutine est le `PostEvent` de tcell, qui **jette** les événements quand sa file est pleine — de sorte que la rafale qui a le plus besoin d'un redessin est justement celle dont le réveil final est perdu, et la fenêtre se fige en pleine compilation sur un texte périmé. Ce bug exact avait déjà été rencontré une fois ailleurs dans cet éditeur, du côté du serveur de langage.
54+
55+La vue positionne donc un drapeau, et une horloge demande un redessin soixante fois par seconde tant que le drapeau est levé. Un réveil perdu ne peut rien bloquer, puisque le tic suivant est à seize millisecondes.
56+
57+## Windows : une pseudo-console, et pourquoi c'est un fichier à part
58+
59+Les pseudo-terminaux sont la seule partie non portable de tout ceci. Linux et macOS passent tous deux par `/dev/ptmx` et ne diffèrent que par l'`ioctl` qui accorde l'esclave. Windows n'a rien de tel : il a des **pseudo-consoles** — ConPTY, depuis Windows 10 version 1809 — un objet détenu par `conhost.exe` et relié à deux tubes de l'éditeur. Ce que le shell affiche arrive sur l'un des tubes sous la forme des mêmes séquences VT qu'un shell Unix écrit dans un pty, ce qui est la raison pour laquelle l'émulateur de ce côté n'a eu besoin d'aucun code Windows ; ce que l'éditeur écrit dans l'autre tube parvient au shell comme des frappes de touches.
60+
61+Trois choses en ont fait un fichier à part plutôt qu'une variante du fichier Unix. Le processus doit être créé à la main, parce que l'attacher à une pseudo-console exige un enregistrement de démarrage étendu que l'`os/exec` de Go ne sait pas porter. Le shell est `%COMSPEC%` — cmd.exe — plutôt que `$SHELL`, et cmd.exe lit sa ligne de commande selon ses propres règles : la ligne qui lance une commande du menu est donc composée pour lui mot pour mot, la commande entre une seule paire de guillemets, au lieu d'être échappée comme tout autre programme l'attend. Et `conhost.exe` garde le tube de sortie ouvert jusqu'à la fermeture de la console, quoi que fasse le shell ; une goroutine attend donc la fin du shell puis ferme la console — c'est ce qui transforme une commande terminée en la fin d'entrée sur laquelle la fenêtre compte pour le dire. Le contrôle de tâches est celui de cmd.exe et non du noyau : `Ctrl-C` interrompt le programme en cours comme il le ferait dans une fenêtre de console.
62+
63+Les fichiers par plateforme restent séparés pour que chaque plateforme ait une implémentation honnête derrière une petite interface, et qu'une plateforme qui n'a ni l'un ni l'autre — les BSD, aujourd'hui — reçoive `ErrUnsupported`, que `F8` le dise clairement, et que rien d'autre dans l'éditeur ne soit affecté.
64+
65+**Le chemin Windows a été compilé et vérifié, pas exécuté.** turbo-core est développé sous Linux et son auteur travaille sous macOS. Les parties pures — le bloc d'environnement, la ligne de commande que veut cmd.exe — sont testées unitairement sur toute plateforme, et les appels à l'API compilent et passent `go vet` sous `GOOS=windows` ; personne n'a encore appuyé sur `F8` sur une machine Windows. [Le guide](../how-to/use-a-terminal.md) dit quoi essayer en premier.
66+
67+## Liens avec le reste
68+
69+- La liste exacte de ce qui est implémenté : [référence des fenêtres terminal](../reference/terminal.md)
70+- En utiliser une : [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md)
71+-`terminal` se situe parmi les paquets, et pourquoi le graphe est orienté : [Architecture](architecture.md)
72+- Le décompte de dépendances que cette page ne cesse d'invoquer : [Décisions de conception](design-decisions.md)
added docs/fr/how-to/ask-about-code.md +69 -0
new file mode 100644
@@ -0,0 +1,69 @@
1+# Comment interroger le code
2+
3+Ce guide montre comment suivre un nom dans un projet : où il est déclaré, ce qui l'implémente, partout où il est utilisé, et ce qui ne va pas avec lui. Il suppose Turbo Go installé et un serveur de langage en marche — la barre d'état affiche `LSP: ready` quand c'est le cas.
4+
5+Pour se déplacer dans un fichier — chercher, aller à une ligne, changer de fenêtre — voir plutôt [Comment se déplacer dans un fichier](navigate-code.md).
6+
7+## Placez le curseur sur un nom
8+
9+N'importe lequel de ses caractères suffit. Chaque question ci-dessous porte sur la **position du curseur**, pas sur une sélection : il n'y a rien à surligner d'abord.
10+
11+## Demandez
12+
13+| Pour trouver | Faites | Raccourci |
14+| --- | --- | --- |
15+| Ce que c'est | **Code ▸ Describe symbol** | `F1` |
16+| Où c'est déclaré | **Code ▸ Go to definition** | `F12` |
17+| Où son *type* est déclaré | **Code ▸ Go to type definition** | |
18+| Ce qui l'implémente | **Code ▸ Find implementations…** | |
19+| Partout où c'est utilisé | **Code ▸ Find references…** | `Shift-F12` |
20+
21+Une seule réponse vous y emmène directement. Plusieurs ouvrent une liste montrant le fichier, sa ligne, et le texte de cette ligne :
22+
23+```
24+Implementations (2)
25+ main.go:7 type French struct{}
26+ main.go:11 type English struct{}
27+```
28+
29+Déplacez-vous aux flèches, `Entrée` pour y aller, `Échap` pour rester.
30+
31+## Quand rien ne revient
32+
33+Trois choses se ressemblent, et la barre d'état les distingue :
34+
35+| Elle affiche | Signification |
36+| --- | --- |
37+| `No references found` | Le serveur a répondu, et il n'y en a pas |
38+| Autre chose, par exemple `Loading…` | Le serveur n'a pas fini d'indexer. Attendez un instant et redemandez. |
39+| `LSP: off` dans la barre d'état | Aucun serveur ne tourne. Voir [Comment activer la complétion](enable-completion.md). |
40+
41+La deuxième mérite d'être connue : un serveur encore en train d'indexer répond rien à toutes les questions, et c'est indiscernable d'une vraie réponse si l'éditeur ne le dit pas.
42+
43+## Chercher par le nom
44+
45+- **Code ▸ Symbol in file…** liste ce que déclare le fichier devant vous, indenté, avec la sorte de chaque symbole — un plan que l'on parcourt.
46+- **Code ▸ Symbol in project…** (`Ctrl-T`) demande un nom et cherche partout. Ce qui compte comme correspondance appartient au serveur ; gopls est tolérant, quelques lettres suffisent en général.
47+
48+## Voir ce qui ne va pas
49+
50+**Code ▸ Problems…** liste tous les problèmes signalés par le serveur, pour **tous les fichiers qu'il a chargés** — le plus souvent davantage que celui que vous éditez. En choisir un vous mène à la ligne.
51+
52+Les lignes à problème portent une marque dans la gouttière, à côté du numéro de ligne :
53+
54+| Marque | Signification |
55+| --- | --- |
56+| `×` | Une erreur |
57+| `!` | Un avertissement |
58+| `i` | Une information |
59+| `·` | Une suggestion |
60+
61+Une ligne qui a plusieurs problèmes montre le pire d'entre eux.
62+
63+**Les marques ont besoin des numéros de ligne.** Elles occupent la colonne qui sépare les numéros du texte : masquer la gouttière avec **Options ▸ Line numbers** les masque aussi.
64+
65+## Voir aussi
66+
67+- Chaque entrée et sa touche : [Menus](../reference/menus.md)
68+- Faire tourner un serveur : [Comment activer la complétion](enable-completion.md)
69+- Ce que l'éditeur demande, et pourquoi : [Coloration et complétion](../explanation/colouring-and-completion.md)
new file mode 100644
@@ -0,0 +1,69 @@
1+# Comment interroger le code
2+
3+Ce guide montre comment suivre un nom dans un projet : où il est déclaré, ce qui l'implémente, partout où il est utilisé, et ce qui ne va pas avec lui. Il suppose Turbo Go installé et un serveur de langage en marche — la barre d'état affiche `LSP: ready` quand c'est le cas.
4+
5+Pour se déplacer dans un fichier — chercher, aller à une ligne, changer de fenêtre — voir plutôt [Comment se déplacer dans un fichier](navigate-code.md).
6+
7+## Placez le curseur sur un nom
8+
9+N'importe lequel de ses caractères suffit. Chaque question ci-dessous porte sur la **position du curseur**, pas sur une sélection : il n'y a rien à surligner d'abord.
10+
11+## Demandez
12+
13+| Pour trouver | Faites | Raccourci |
14+| --- | --- | --- |
15+| Ce que c'est | **Code ▸ Describe symbol** | `F1` |
16+| Où c'est déclaré | **Code ▸ Go to definition** | `F12` |
17+| Où son *type* est déclaré | **Code ▸ Go to type definition** | |
18+| Ce qui l'implémente | **Code ▸ Find implementations…** | |
19+| Partout où c'est utilisé | **Code ▸ Find references…** | `Shift-F12` |
20+
21+Une seule réponse vous y emmène directement. Plusieurs ouvrent une liste montrant le fichier, sa ligne, et le texte de cette ligne :
22+
23+```
24+Implementations (2)
25+ main.go:7 type French struct{}
26+ main.go:11 type English struct{}
27+```
28+
29+Déplacez-vous aux flèches, `Entrée` pour y aller, `Échap` pour rester.
30+
31+## Quand rien ne revient
32+
33+Trois choses se ressemblent, et la barre d'état les distingue :
34+
35+| Elle affiche | Signification |
36+| --- | --- |
37+| `No references found` | Le serveur a répondu, et il n'y en a pas |
38+| Autre chose, par exemple `Loading…` | Le serveur n'a pas fini d'indexer. Attendez un instant et redemandez. |
39+| `LSP: off` dans la barre d'état | Aucun serveur ne tourne. Voir [Comment activer la complétion](enable-completion.md). |
40+
41+La deuxième mérite d'être connue : un serveur encore en train d'indexer répond rien à toutes les questions, et c'est indiscernable d'une vraie réponse si l'éditeur ne le dit pas.
42+
43+## Chercher par le nom
44+
45+- **Code ▸ Symbol in file…** liste ce que déclare le fichier devant vous, indenté, avec la sorte de chaque symbole — un plan que l'on parcourt.
46+- **Code ▸ Symbol in project…** (`Ctrl-T`) demande un nom et cherche partout. Ce qui compte comme correspondance appartient au serveur ; gopls est tolérant, quelques lettres suffisent en général.
47+
48+## Voir ce qui ne va pas
49+
50+**Code ▸ Problems…** liste tous les problèmes signalés par le serveur, pour **tous les fichiers qu'il a chargés** — le plus souvent davantage que celui que vous éditez. En choisir un vous mène à la ligne.
51+
52+Les lignes à problème portent une marque dans la gouttière, à côté du numéro de ligne :
53+
54+| Marque | Signification |
55+| --- | --- |
56+| `×` | Une erreur |
57+| `!` | Un avertissement |
58+| `i` | Une information |
59+| `·` | Une suggestion |
60+
61+Une ligne qui a plusieurs problèmes montre le pire d'entre eux.
62+
63+**Les marques ont besoin des numéros de ligne.** Elles occupent la colonne qui sépare les numéros du texte : masquer la gouttière avec **Options ▸ Line numbers** les masque aussi.
64+
65+## Voir aussi
66+
67+- Chaque entrée et sa touche : [Menus](../reference/menus.md)
68+- Faire tourner un serveur : [Comment activer la complétion](enable-completion.md)
69+- Ce que l'éditeur demande, et pourquoi : [Coloration et complétion](../explanation/colouring-and-completion.md)
added docs/fr/how-to/browse-a-project.md +70 -0
new file mode 100644
@@ -0,0 +1,70 @@
1+# Parcourir un projet et ouvrir des fichiers depuis un arbre
2+
3+Ce guide montre comment ouvrir l'arbre du projet, le parcourir et y ouvrir un fichier. Il suppose Turbo Go déjà installé.
4+
5+## Ouvrir l'arbre
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis appuyez sur `F9`, ou choisissez **Window ▸ Project tree**.
8+
9+Une fenêtre s'ouvre avec les fichiers du projet, nommée d'après le dossier depuis lequel l'éditeur a été lancé :
10+
11+```
12+╔═[x]════════════ turbo-go ════════════2═[■]╗
13+║ ▶ .turbo-go ║
14+║ ▼ internal ║
15+║ ▶ app ║
16+║ ▼ ui ║
17+║ window.go ║
18+║ .gitignore ║
19+║ go.mod ║
20+║ main.go ║
21+╚══════════════════════════════════════════╝
22+```
23+
24+Les dossiers viennent d'abord, puis les fichiers, chaque groupe trié. `.git` est la seule chose masquée — `.turbo-go`, `.gitignore` et les autres sont des fichiers de votre projet, que vous voudrez sans doute ouvrir.
25+
26+Appuyer de nouveau sur `F9` ramène cette fenêtre au premier plan plutôt que d'ouvrir un second arbre.
27+
28+## Le parcourir
29+
30+| Touche | Effet |
31+| --- | --- |
32+| `↑` `↓` | Déplacer la surbrillance |
33+| `→` | Ouvrir un dossier fermé ; sur autre chose, passer à la ligne suivante |
34+| `←` | Fermer un dossier ouvert ; sur autre chose, remonter au dossier qui le contient |
35+| `Entrée` | Ouvrir un fichier, ou ouvrir et fermer un dossier |
36+| `Début` `Fin` | Première / dernière ligne |
37+| `Page↑` `Page↓` | Un écran à la fois |
38+
39+Un dossier est lu la première fois que vous l'ouvrez : un arbre sur un gros projet coûte donc une lecture de dossier, pas un parcours complet.
40+
41+## Ouvrir un fichier
42+
43+Placez la surbrillance dessus et appuyez sur `Entrée`, ou cliquez-le deux fois.
44+
45+Le fichier s'ouvre dans une fenêtre à lui, devant l'arbre. Un fichier déjà ouvert est ramené au premier plan plutôt qu'ouvert deux fois.
46+
47+## Voir un fichier créé après l'ouverture de l'arbre
48+
49+L'arbre ne surveille pas le disque. Appuyez sur **`F5`** ou **`Ctrl-R`** avec l'arbre au premier plan : il relit le projet, en gardant ouvert ce que vous aviez ouvert et la surbrillance sur la même entrée.
50+
51+Enregistrer un fichier rafraîchit l'arbre pour vous : un **File ▸ Save as** sous un nouveau nom y apparaît sans rien demander. Un fichier créé autrement — un `go build` dans une fenêtre terminal, ou un `git checkout` — demande la touche de rafraîchissement.
52+
53+## Travailler avec l'arbre et un fichier côte à côte
54+
55+L'arbre est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent :
56+
57+- **Window ▸ Tile** place l'arbre et votre fichier côte à côte.
58+- Tirez son coin inférieur droit pour le rétrécir une fois que vous vous y retrouvez.
59+- `[x]` le ferme ; `F9` le ramène.
60+
61+## Variantes
62+
63+- **Vous avez lancé l'éditeur depuis un sous-dossier.** L'arbre y est enraciné et ne montre que cette partie du projet. Lancez plutôt depuis le dossier du projet — la même règle que `.turbo-go/settings.toml`.
64+- **Un dossier apparaît ouvert mais vide.** Il n'a pas pu être lu, le plus souvent un problème de permissions. Le reste de l'arbre n'est pas affecté ; corrigez les permissions et appuyez sur `F5`.
65+
66+## Voir aussi
67+
68+- Toutes les touches et tout ce que l'arbre montre, exactement : [Référence de l'arbre du projet](../reference/project-tree.md)
69+- Pourquoi c'est une fenêtre et non un panneau ancré, et pourquoi il ne surveille pas le disque : [Arbre du projet](../explanation/project-tree.md)
70+- Le colorer : [Format des fichiers de thème](../reference/themes.md)
new file mode 100644
@@ -0,0 +1,70 @@
1+# Parcourir un projet et ouvrir des fichiers depuis un arbre
2+
3+Ce guide montre comment ouvrir l'arbre du projet, le parcourir et y ouvrir un fichier. Il suppose Turbo Go déjà installé.
4+
5+## Ouvrir l'arbre
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis appuyez sur `F9`, ou choisissez **Window ▸ Project tree**.
8+
9+Une fenêtre s'ouvre avec les fichiers du projet, nommée d'après le dossier depuis lequel l'éditeur a été lancé :
10+
11+```
12+╔═[x]════════════ turbo-go ════════════2═[■]╗
13+║ ▶ .turbo-go ║
14+║ ▼ internal ║
15+║ ▶ app ║
16+║ ▼ ui ║
17+║ window.go ║
18+║ .gitignore ║
19+║ go.mod ║
20+║ main.go ║
21+╚══════════════════════════════════════════╝
22+```
23+
24+Les dossiers viennent d'abord, puis les fichiers, chaque groupe trié. `.git` est la seule chose masquée — `.turbo-go`, `.gitignore` et les autres sont des fichiers de votre projet, que vous voudrez sans doute ouvrir.
25+
26+Appuyer de nouveau sur `F9` ramène cette fenêtre au premier plan plutôt que d'ouvrir un second arbre.
27+
28+## Le parcourir
29+
30+| Touche | Effet |
31+| --- | --- |
32+| `↑` `↓` | Déplacer la surbrillance |
33+| `→` | Ouvrir un dossier fermé ; sur autre chose, passer à la ligne suivante |
34+| `←` | Fermer un dossier ouvert ; sur autre chose, remonter au dossier qui le contient |
35+| `Entrée` | Ouvrir un fichier, ou ouvrir et fermer un dossier |
36+| `Début` `Fin` | Première / dernière ligne |
37+| `Page↑` `Page↓` | Un écran à la fois |
38+
39+Un dossier est lu la première fois que vous l'ouvrez : un arbre sur un gros projet coûte donc une lecture de dossier, pas un parcours complet.
40+
41+## Ouvrir un fichier
42+
43+Placez la surbrillance dessus et appuyez sur `Entrée`, ou cliquez-le deux fois.
44+
45+Le fichier s'ouvre dans une fenêtre à lui, devant l'arbre. Un fichier déjà ouvert est ramené au premier plan plutôt qu'ouvert deux fois.
46+
47+## Voir un fichier créé après l'ouverture de l'arbre
48+
49+L'arbre ne surveille pas le disque. Appuyez sur **`F5`** ou **`Ctrl-R`** avec l'arbre au premier plan : il relit le projet, en gardant ouvert ce que vous aviez ouvert et la surbrillance sur la même entrée.
50+
51+Enregistrer un fichier rafraîchit l'arbre pour vous : un **File ▸ Save as** sous un nouveau nom y apparaît sans rien demander. Un fichier créé autrement — un `go build` dans une fenêtre terminal, ou un `git checkout` — demande la touche de rafraîchissement.
52+
53+## Travailler avec l'arbre et un fichier côte à côte
54+
55+L'arbre est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent :
56+
57+- **Window ▸ Tile** place l'arbre et votre fichier côte à côte.
58+- Tirez son coin inférieur droit pour le rétrécir une fois que vous vous y retrouvez.
59+- `[x]` le ferme ; `F9` le ramène.
60+
61+## Variantes
62+
63+- **Vous avez lancé l'éditeur depuis un sous-dossier.** L'arbre y est enraciné et ne montre que cette partie du projet. Lancez plutôt depuis le dossier du projet — la même règle que `.turbo-go/settings.toml`.
64+- **Un dossier apparaît ouvert mais vide.** Il n'a pas pu être lu, le plus souvent un problème de permissions. Le reste de l'arbre n'est pas affecté ; corrigez les permissions et appuyez sur `F5`.
65+
66+## Voir aussi
67+
68+- Toutes les touches et tout ce que l'arbre montre, exactement : [Référence de l'arbre du projet](../reference/project-tree.md)
69+- Pourquoi c'est une fenêtre et non un panneau ancré, et pourquoi il ne surveille pas le disque : [Arbre du projet](../explanation/project-tree.md)
70+- Le colorer : [Format des fichiers de thème](../reference/themes.md)
added docs/fr/how-to/configure-a-project.md +83 -0
new file mode 100644
@@ -0,0 +1,83 @@
1+# Donner ses propres réglages à un projet
2+
3+Ce guide montre comment fixer un thème et activer la sauvegarde automatique pour un projet, afin que tous ceux qui l'ouvrent aient le même éditeur. Il suppose Turbo Go déjà installé.
4+
5+## Créer le fichier de réglages
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Options ▸ Create project settings**.
8+
9+Cela écrit `.turbo-go/settings.toml`, rempli avec le thème que vous utilisez à cet instant, et l'ouvre pour édition — coloré, puisque Turbo Go colore le TOML :
10+
11+```toml
12+# turbo-go project settings.
13+#
14+# These apply to everyone who opens this project in turbo-go. Delete this file
15+# and the editor falls back to its own defaults.
16+
17+[editor]
18+
19+# The colour theme to start in. `turbo-go -list-themes` lists them all.
20+# A -theme flag on the command line overrides this.
21+theme = "turbo-classic"
22+
23+# Write modified files by themselves, a short while after you stop typing.
24+# On, because a project that has gone to the trouble of having a settings file
25+# has said what it wants; set it to false and save, and it stops at once.
26+autosave = true
27+
28+# How long that while is. Any Go duration: "500ms", "2s", "1m".
29+autosave_delay = "2s"
30+```
31+
32+Le fichier est lu au démarrage de l'éditeur, et **de nouveau à chaque fois que vous l'enregistrez** — une modification est donc en vigueur dès que vous appuyez sur `F2`. La barre d'état le confirme : `Applied .turbo-go/settings.toml — autosave on (2s)`.
33+
34+Cela vaut pour les réglages que ce fichier contient, pas pour le thème : **Options ▸ Theme** est la façon vivante de le changer, et y réécrit votre choix pour vous.
35+
36+L'entrée de menu que vous venez d'utiliser est maintenant grisée, et **Options ▸ Project settings…** à côté d'elle ne l'est plus. C'est la règle pour les trois fichiers du projet : vous pouvez créer celui que vous n'avez pas, et ouvrir celui que vous avez.
37+
38+## La sauvegarde automatique
39+
40+Elle est déjà active : le fichier qu'on vient de vous donner dit `autosave = true`.
41+
42+Tout fichier qui a un nom est écrit deux secondes après que vous ayez cessé de taper. La barre d'état affiche `Saved main.go` au moment où cela arrive. Rien n'est écrit tant que vous tapez : chaque frappe repousse l'attente.
43+
44+Deux choses changent en conséquence, toutes deux voulues :
45+
46+- **Fermer une fenêtre ne demande plus** s'il faut enregistrer. Le fichier allait l'être de toute façon.
47+- **Quitter l'éditeur ne demande plus** non plus, pour la même raison.
48+
49+Un fichier qui n'a jamais reçu de nom fait exception : la sauvegarde automatique n'ouvre jamais de dialogue, donc une fenêtre sans titre garde son `*` et la question est toujours posée à la fermeture.
50+
51+Pour la désactiver, mettez `autosave` à `false` et enregistrez ; la barre d'état répond `autosave off`, et cela s'arrête à l'instant. Pour attendre plus ou moins longtemps, changez `autosave_delay` :
52+
53+```toml
54+autosave_delay = "500ms"
55+```
56+
57+## Fixer le thème
58+
59+Donnez à `theme` un nom parmi ceux de `turbo-go -list-themes`, ou choisissez-en un simplement avec **Options ▸ Theme** : dès lors qu'un fichier de réglages existe, choisir un thème l'y écrit pour vous, en conservant vos commentaires et votre mise en page.
60+
61+## Essayer un autre thème sans toucher au fichier
62+
63+Passez `-theme` sur la ligne de commande. Il l'emporte sur le choix du projet, le temps de cette exécution seulement :
64+
65+```bash
66+turbo-go -theme turbo-dark main.go
67+```
68+
69+## Modifier le fichier plus tard
70+
71+**Options ▸ Project settings…** le rouvre. L'entrée est grisée dans un projet qui n'en a pas.
72+
73+## Variantes
74+
75+- **Vous lancez l'éditeur depuis un sous-dossier.** Les réglages ne sont pas trouvés : seul `./.turbo-go` est consulté, sans remontée vers la racine du projet. Lancez depuis le dossier du projet, ou passez `-theme` pour cette fois.
76+- **Le fichier contient une erreur.** L'éditeur le signale sur la sortie d'erreur et s'ouvre avec ses valeurs par défaut — vous pouvez donc corriger le fichier dans l'éditeur lui-même.
77+- **Vous partagez le projet.** `.turbo-go/settings.toml` est un fichier ordinaire : versionnez-le pour convenir d'un thème en équipe, ou ajoutez-le à `.gitignore` pour le garder pour vous.
78+
79+## Voir aussi
80+
81+- Chaque clé, avec son type et sa valeur par défaut : [Référence des réglages de projet](../reference/project-settings.md)
82+- Pourquoi le fichier n'est pas cherché dans les dossiers parents, et pourquoi l'autosave attend : [Réglages de projet](../explanation/project-settings.md)
83+- Écrire un thème à fixer : [Écrire son propre thème](write-a-theme.md)
new file mode 100644
@@ -0,0 +1,83 @@
1+# Donner ses propres réglages à un projet
2+
3+Ce guide montre comment fixer un thème et activer la sauvegarde automatique pour un projet, afin que tous ceux qui l'ouvrent aient le même éditeur. Il suppose Turbo Go déjà installé.
4+
5+## Créer le fichier de réglages
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Options ▸ Create project settings**.
8+
9+Cela écrit `.turbo-go/settings.toml`, rempli avec le thème que vous utilisez à cet instant, et l'ouvre pour édition — coloré, puisque Turbo Go colore le TOML :
10+
11+```toml
12+# turbo-go project settings.
13+#
14+# These apply to everyone who opens this project in turbo-go. Delete this file
15+# and the editor falls back to its own defaults.
16+
17+[editor]
18+
19+# The colour theme to start in. `turbo-go -list-themes` lists them all.
20+# A -theme flag on the command line overrides this.
21+theme = "turbo-classic"
22+
23+# Write modified files by themselves, a short while after you stop typing.
24+# On, because a project that has gone to the trouble of having a settings file
25+# has said what it wants; set it to false and save, and it stops at once.
26+autosave = true
27+
28+# How long that while is. Any Go duration: "500ms", "2s", "1m".
29+autosave_delay = "2s"
30+```
31+
32+Le fichier est lu au démarrage de l'éditeur, et **de nouveau à chaque fois que vous l'enregistrez** — une modification est donc en vigueur dès que vous appuyez sur `F2`. La barre d'état le confirme : `Applied .turbo-go/settings.toml — autosave on (2s)`.
33+
34+Cela vaut pour les réglages que ce fichier contient, pas pour le thème : **Options ▸ Theme** est la façon vivante de le changer, et y réécrit votre choix pour vous.
35+
36+L'entrée de menu que vous venez d'utiliser est maintenant grisée, et **Options ▸ Project settings…** à côté d'elle ne l'est plus. C'est la règle pour les trois fichiers du projet : vous pouvez créer celui que vous n'avez pas, et ouvrir celui que vous avez.
37+
38+## La sauvegarde automatique
39+
40+Elle est déjà active : le fichier qu'on vient de vous donner dit `autosave = true`.
41+
42+Tout fichier qui a un nom est écrit deux secondes après que vous ayez cessé de taper. La barre d'état affiche `Saved main.go` au moment où cela arrive. Rien n'est écrit tant que vous tapez : chaque frappe repousse l'attente.
43+
44+Deux choses changent en conséquence, toutes deux voulues :
45+
46+- **Fermer une fenêtre ne demande plus** s'il faut enregistrer. Le fichier allait l'être de toute façon.
47+- **Quitter l'éditeur ne demande plus** non plus, pour la même raison.
48+
49+Un fichier qui n'a jamais reçu de nom fait exception : la sauvegarde automatique n'ouvre jamais de dialogue, donc une fenêtre sans titre garde son `*` et la question est toujours posée à la fermeture.
50+
51+Pour la désactiver, mettez `autosave` à `false` et enregistrez ; la barre d'état répond `autosave off`, et cela s'arrête à l'instant. Pour attendre plus ou moins longtemps, changez `autosave_delay` :
52+
53+```toml
54+autosave_delay = "500ms"
55+```
56+
57+## Fixer le thème
58+
59+Donnez à `theme` un nom parmi ceux de `turbo-go -list-themes`, ou choisissez-en un simplement avec **Options ▸ Theme** : dès lors qu'un fichier de réglages existe, choisir un thème l'y écrit pour vous, en conservant vos commentaires et votre mise en page.
60+
61+## Essayer un autre thème sans toucher au fichier
62+
63+Passez `-theme` sur la ligne de commande. Il l'emporte sur le choix du projet, le temps de cette exécution seulement :
64+
65+```bash
66+turbo-go -theme turbo-dark main.go
67+```
68+
69+## Modifier le fichier plus tard
70+
71+**Options ▸ Project settings…** le rouvre. L'entrée est grisée dans un projet qui n'en a pas.
72+
73+## Variantes
74+
75+- **Vous lancez l'éditeur depuis un sous-dossier.** Les réglages ne sont pas trouvés : seul `./.turbo-go` est consulté, sans remontée vers la racine du projet. Lancez depuis le dossier du projet, ou passez `-theme` pour cette fois.
76+- **Le fichier contient une erreur.** L'éditeur le signale sur la sortie d'erreur et s'ouvre avec ses valeurs par défaut — vous pouvez donc corriger le fichier dans l'éditeur lui-même.
77+- **Vous partagez le projet.** `.turbo-go/settings.toml` est un fichier ordinaire : versionnez-le pour convenir d'un thème en équipe, ou ajoutez-le à `.gitignore` pour le garder pour vous.
78+
79+## Voir aussi
80+
81+- Chaque clé, avec son type et sa valeur par défaut : [Référence des réglages de projet](../reference/project-settings.md)
82+- Pourquoi le fichier n'est pas cherché dans les dossiers parents, et pourquoi l'autosave attend : [Réglages de projet](../explanation/project-settings.md)
83+- Écrire un thème à fixer : [Écrire son propre thème](write-a-theme.md)
added docs/fr/how-to/enable-completion.md +95 -0
new file mode 100644
@@ -0,0 +1,95 @@
1+# Activer la complétion Go
2+
3+Ce guide montre comment faire fonctionner la complétion, les descriptions de symboles et le saut à la définition. Il suppose que Turbo Go est déjà installé et que vous savez ce qu'est un module Go.
4+
5+La complétion vient de **gopls**, le serveur de langage officiel de Go. Turbo Go ne l'embarque pas : l'édition et la coloration fonctionnent sans lui, et seule la complétion est perdue.
6+
7+## 1. Installer gopls
8+
9+```bash
10+go install golang.org/x/tools/gopls@latest
11+```
12+
13+## 2. S'assurer que Turbo Go le trouve
14+
15+Turbo Go cherche d'abord dans le `PATH`, puis dans `$GOBIN`, puis dans `$GOPATH/bin`. `go install` écrit dans le dernier, qui n'est très souvent pas dans le `PATH` — c'est pourquoi cela marche en général sans rien faire de plus. Pour vérifier :
16+
17+```bash
18+gopls version
19+```
20+
21+Si cette commande répond « introuvable » alors que Turbo Go le trouve quand même, c'est normal et sans conséquence.
22+
23+## 3. Ouvrir un fichier à l'intérieur d'un module
24+
25+```bash
26+cd /chemin/vers/votre/module # le répertoire qui contient go.mod
27+turbo-go main.go
28+```
29+
30+Turbo Go remonte l'arborescence depuis le fichier à la recherche d'un `go.mod` et démarre gopls dans le répertoire trouvé. **Hors d'un module, gopls n'a presque rien à dire** — c'est la raison la plus fréquente pour laquelle la complétion semble ne pas fonctionner.
31+
32+## 4. Demander une complétion
33+
34+Placez le curseur après un point et appuyez sur **Ctrl-Espace** :
35+
36+```go
37+fmt.
38+```
39+
40+Une liste se déroule sous le curseur. Continuez à taper pour la restreindre, **↑ ↓** pour la parcourir, **Entrée** ou **Tab** pour accepter, **Échap** pour l'abandonner.
41+
42+Taper un `.` demande une complétion tout seul : la plupart du temps, vous n'appuyez sur rien.
43+
44+## Savoir ce que fait le serveur
45+
46+L'extrémité droite de la barre d'état indique l'état du serveur de langage : `LSP: starting…`, `LSP: ready`, ou la raison pour laquelle il n'y en a pas. `Run ▸ Language server status` affiche la même chose dans une boîte.
47+
48+## Variantes
49+
50+**Vous ne voulez pas de serveur de langage du tout :**
51+
52+```bash
53+turbo-go -no-lsp main.go
54+```
55+
56+**La complétion est morte dans une fenêtre née sans nom.** Une fenêtre « Untitled » n'a aucun fichier à annoncer à gopls tant qu'elle n'est pas enregistrée — appuyez sur **F2** et donnez-lui un nom en `.go`, quelque part sous le module. Dès cette sauvegarde, la complétion, le survol et les marques d'erreur fonctionnent dans cette fenêtre ; inutile de quitter et relancer l'éditeur.
57+
58+**La complétion est vide dans un fichier qui compile.** gopls a besoin que le paquet du fichier se construise. Vérifiez d'abord `go build ./...` — un paquet qui ne compile pas ne donne souvent rien d'utile.
59+
60+**La première complétion sur un gros module est lente.** gopls charge le graphe du module. La barre d'état affiche `LSP: starting…` jusqu'à ce qu'il soit prêt ; les requêtes faites avant sont refusées plutôt que mises en file.
61+
62+**Une requête met trop de temps.** Chaque requête abandonne au bout de trois secondes : un serveur bloqué ralentit l'éditeur mais ne le fige jamais. La barre d'état signale l'échec.
63+
64+**La liste est vide dans un fichier qui ne compile pas.** gopls ne répond *rien du tout* — pas d'erreur, une liste vide — pour un paquet qu'il ne peut pas charger. Une déclaration en double ou un import non résolu suffit. L'éditeur indique désormais quel problème fait obstacle :
65+
66+```
67+No completions — this file does not compile: main redeclared in this block
68+```
69+
70+`Run ▸ Language server status` affiche la même chose, avec le chemin du serveur, la racine du projet et l'information de savoir si ce fichier lui a été annoncé. Corrigez d'abord le paquet — `go build ./...` est la vérification la plus rapide.
71+
72+**Ctrl-Espace ne fait rien.** tmux, screen et les terminaux intégrés d'IDE interceptent très souvent `Ctrl-Espace` avant l'éditeur. Tapez plutôt un `.`, qui demande une complétion tout seul, ou passez par `Run ▸ Completion`.
73+
74+## Ce que le serveur apporte d'autre
75+
76+La complétion est ce qu'il fait de plus bruyant et le moindre de ce qu'il sait. La même connexion répond à huit autres questions, toutes dans le menu **Code** et toutes à propos du symbole sous le curseur — aucune sélection n'est nécessaire.
77+
78+| Touche | Effet |
79+| --- | --- |
80+| **Ctrl-Espace** | Liste de complétion |
81+| **F1** | Décrire le symbole sous le curseur |
82+| **F12** | Sauter là où il est déclaré |
83+| **Shift-F12** | Lister partout où il est utilisé |
84+| **Ctrl-T** | Trouver un symbole par son nom dans tout le projet |
85+
86+Et, sans touche : *Go to type definition*, *Find implementations…*, *Symbol in file…* et *Problems…*.
87+
88+Les problèmes qu'il trouve arrivent sans qu'on demande. La première erreur du fichier que vous éditez apparaît à droite de la barre d'état, précédée de `⚠` ; chaque ligne à problème reçoit une marque dans la gouttière (`×` pour une erreur, `!` pour un avertissement) ; et **Code ▸ Problems…** les liste tous, pour tous les fichiers que le serveur a chargés.
89+
90+[Comment interroger le code](ask-about-code.md) parcourt l'ensemble.
91+
92+## Voir aussi
93+
94+- Pourquoi le serveur est optionnel : [Coloration et complétion](../explanation/colouring-and-completion.md)
95+- Toutes les touches : [référence du clavier](../reference/keyboard.md)
new file mode 100644
@@ -0,0 +1,95 @@
1+# Activer la complétion Go
2+
3+Ce guide montre comment faire fonctionner la complétion, les descriptions de symboles et le saut à la définition. Il suppose que Turbo Go est déjà installé et que vous savez ce qu'est un module Go.
4+
5+La complétion vient de **gopls**, le serveur de langage officiel de Go. Turbo Go ne l'embarque pas : l'édition et la coloration fonctionnent sans lui, et seule la complétion est perdue.
6+
7+## 1. Installer gopls
8+
9+```bash
10+go install golang.org/x/tools/gopls@latest
11+```
12+
13+## 2. S'assurer que Turbo Go le trouve
14+
15+Turbo Go cherche d'abord dans le `PATH`, puis dans `$GOBIN`, puis dans `$GOPATH/bin`. `go install` écrit dans le dernier, qui n'est très souvent pas dans le `PATH` — c'est pourquoi cela marche en général sans rien faire de plus. Pour vérifier :
16+
17+```bash
18+gopls version
19+```
20+
21+Si cette commande répond « introuvable » alors que Turbo Go le trouve quand même, c'est normal et sans conséquence.
22+
23+## 3. Ouvrir un fichier à l'intérieur d'un module
24+
25+```bash
26+cd /chemin/vers/votre/module # le répertoire qui contient go.mod
27+turbo-go main.go
28+```
29+
30+Turbo Go remonte l'arborescence depuis le fichier à la recherche d'un `go.mod` et démarre gopls dans le répertoire trouvé. **Hors d'un module, gopls n'a presque rien à dire** — c'est la raison la plus fréquente pour laquelle la complétion semble ne pas fonctionner.
31+
32+## 4. Demander une complétion
33+
34+Placez le curseur après un point et appuyez sur **Ctrl-Espace** :
35+
36+```go
37+fmt.
38+```
39+
40+Une liste se déroule sous le curseur. Continuez à taper pour la restreindre, **↑ ↓** pour la parcourir, **Entrée** ou **Tab** pour accepter, **Échap** pour l'abandonner.
41+
42+Taper un `.` demande une complétion tout seul : la plupart du temps, vous n'appuyez sur rien.
43+
44+## Savoir ce que fait le serveur
45+
46+L'extrémité droite de la barre d'état indique l'état du serveur de langage : `LSP: starting…`, `LSP: ready`, ou la raison pour laquelle il n'y en a pas. `Run ▸ Language server status` affiche la même chose dans une boîte.
47+
48+## Variantes
49+
50+**Vous ne voulez pas de serveur de langage du tout :**
51+
52+```bash
53+turbo-go -no-lsp main.go
54+```
55+
56+**La complétion est morte dans une fenêtre née sans nom.** Une fenêtre « Untitled » n'a aucun fichier à annoncer à gopls tant qu'elle n'est pas enregistrée — appuyez sur **F2** et donnez-lui un nom en `.go`, quelque part sous le module. Dès cette sauvegarde, la complétion, le survol et les marques d'erreur fonctionnent dans cette fenêtre ; inutile de quitter et relancer l'éditeur.
57+
58+**La complétion est vide dans un fichier qui compile.** gopls a besoin que le paquet du fichier se construise. Vérifiez d'abord `go build ./...` — un paquet qui ne compile pas ne donne souvent rien d'utile.
59+
60+**La première complétion sur un gros module est lente.** gopls charge le graphe du module. La barre d'état affiche `LSP: starting…` jusqu'à ce qu'il soit prêt ; les requêtes faites avant sont refusées plutôt que mises en file.
61+
62+**Une requête met trop de temps.** Chaque requête abandonne au bout de trois secondes : un serveur bloqué ralentit l'éditeur mais ne le fige jamais. La barre d'état signale l'échec.
63+
64+**La liste est vide dans un fichier qui ne compile pas.** gopls ne répond *rien du tout* — pas d'erreur, une liste vide — pour un paquet qu'il ne peut pas charger. Une déclaration en double ou un import non résolu suffit. L'éditeur indique désormais quel problème fait obstacle :
65+
66+```
67+No completions — this file does not compile: main redeclared in this block
68+```
69+
70+`Run ▸ Language server status` affiche la même chose, avec le chemin du serveur, la racine du projet et l'information de savoir si ce fichier lui a été annoncé. Corrigez d'abord le paquet — `go build ./...` est la vérification la plus rapide.
71+
72+**Ctrl-Espace ne fait rien.** tmux, screen et les terminaux intégrés d'IDE interceptent très souvent `Ctrl-Espace` avant l'éditeur. Tapez plutôt un `.`, qui demande une complétion tout seul, ou passez par `Run ▸ Completion`.
73+
74+## Ce que le serveur apporte d'autre
75+
76+La complétion est ce qu'il fait de plus bruyant et le moindre de ce qu'il sait. La même connexion répond à huit autres questions, toutes dans le menu **Code** et toutes à propos du symbole sous le curseur — aucune sélection n'est nécessaire.
77+
78+| Touche | Effet |
79+| --- | --- |
80+| **Ctrl-Espace** | Liste de complétion |
81+| **F1** | Décrire le symbole sous le curseur |
82+| **F12** | Sauter là où il est déclaré |
83+| **Shift-F12** | Lister partout où il est utilisé |
84+| **Ctrl-T** | Trouver un symbole par son nom dans tout le projet |
85+
86+Et, sans touche : *Go to type definition*, *Find implementations…*, *Symbol in file…* et *Problems…*.
87+
88+Les problèmes qu'il trouve arrivent sans qu'on demande. La première erreur du fichier que vous éditez apparaît à droite de la barre d'état, précédée de `⚠` ; chaque ligne à problème reçoit une marque dans la gouttière (`×` pour une erreur, `!` pour un avertissement) ; et **Code ▸ Problems…** les liste tous, pour tous les fichiers que le serveur a chargés.
89+
90+[Comment interroger le code](ask-about-code.md) parcourt l'ensemble.
91+
92+## Voir aussi
93+
94+- Pourquoi le serveur est optionnel : [Coloration et complétion](../explanation/colouring-and-completion.md)
95+- Toutes les touches : [référence du clavier](../reference/keyboard.md)
added docs/fr/how-to/install.md +89 -0
new file mode 100644
@@ -0,0 +1,89 @@
1+# Installer et compiler Turbo Go
2+
3+Ce guide montre comment obtenir un binaire `turbo-go` fonctionnel. Il suppose que vous avez Go 1.26 ou plus récent et que vous savez utiliser un terminal.
4+
5+## La voie rapide, depuis un clone
6+
7+```bash
8+git clone ssh://git@rickub.com/turbo-editors/turbo-go.git
9+cd turbo-go
10+make install
11+```
12+
13+Cela compile l'éditeur, le place là où votre shell cherche ses commandes, et vous dit ce qu'il a trouvé : la version de Go, où le binaire a été posé, si ce répertoire est dans votre `PATH`, et si `gopls` est installé. La compilation passe d'abord par un fichier temporaire : un échec ne remplace jamais une installation qui marchait.
14+
15+Ensuite, depuis n'importe quel projet Go :
16+
17+```bash
18+turbo-go main.go
19+```
20+
21+### Options
22+
23+```bash
24+scripts/install.sh --prefix ~/bin # installer ailleurs
25+scripts/install.sh --with-gopls # installer aussi le serveur de langage
26+scripts/install.sh --uninstall # le retirer (make uninstall)
27+scripts/install.sh --help
28+```
29+
30+Sans `--prefix`, l'éditeur va là où `go install` le mettrait : `$GOBIN`, ou `$GOPATH/bin` si `GOBIN` n'est pas défini — le plus souvent `~/go/bin`.
31+
32+## Seulement compiler, sans installer
33+
34+```bash
35+make build
36+./bin/turbo-go main.go
37+```
38+
39+## Depuis le proxy de modules, sans clone
40+
41+```bash
42+go install rickub.com/turbo-editors/turbo-go@latest
43+```
44+
45+Si la commande est ensuite « introuvable », c'est que le répertoire d'installation n'est pas dans votre `PATH` :
46+
47+```bash
48+export PATH="$PATH:$(go env GOPATH)/bin"
49+```
50+
51+## Vérifier que ça marche
52+
53+```bash
54+turbo-go -version
55+turbo-go -list-themes
56+```
57+
58+La première nomme le commit dont le binaire a été construit, ce qu'il faut citer dans un rapport de bug ; [le numéro de version](../reference/versioning.md) explique ce que signifie chaque forme. La seconde affiche les thèmes compilés dans le binaire et vous indique où placer les vôtres.
59+
60+## Variantes
61+
62+- **Vous voulez juste l'essayer une fois** : `go run rickub.com/turbo-editors/turbo-go@latest main.go`
63+- **Vous voulez le binaire à un endroit précis** : `go build -o /usr/local/bin/turbo-go .`
64+- **Votre terminal ne gère pas les couleurs 24 bits** : utilisez `turbo-go -theme turbo-classic`, construit uniquement sur les seize couleurs ANSI. `turbo-dark` et `borland-light` utilisent des couleurs 24 bits.
65+
66+## Quand quelque chose ne va pas
67+
68+**`the installed binary does not run`.** L'installeur affiche juste au-dessus ce que le système a répondu — lisez-le d'abord, c'est lui qui nomme le vrai problème.
69+
70+L'installeur **remplace** le binaire au lieu d'écrire par-dessus celui qui est là : une réinstallation donne donc au fichier une identité neuve. Cela compte sous macOS, qui met en cache la signature de code d'un binaire **par inode** : écrire de nouveaux octets dans l'ancien inode laisse une signature en cache qui décrit autre chose, et le noyau refuse alors d'exécuter un binaire qui s'est pourtant compilé et installé sans erreur. Si une ancienne copie a été installée par un outil qui employait `cp`, la supprimer d'abord efface cet état :
71+
72+```bash
73+scripts/install.sh --uninstall
74+scripts/install.sh
75+```
76+
77+**`Go x.y or later is needed`.** La version vient de `go.mod` : elle ne peut donc pas diverger de ce dont le code a réellement besoin. Mettez Go à jour, ou compilez depuis une étiquette correspondant à la chaîne d'outils dont vous disposez.
78+
79+**`build failed; nothing was installed`.** Votre installation existante est intacte — la compilation passe d'abord par un fichier temporaire. La sortie du compilateur est affichée au-dessus du message.
80+
81+## Exigences côté terminal
82+
83+Turbo Go a besoin d'un terminal qui rapporte sa taille et gère la souris — tous les terminaux courants le font. Il lit `TERM` via tcell ; si l'affichage est incorrect, vérifiez que `TERM` correspond bien à votre terminal (`xterm-256color` est une valeur sûre).
84+
85+## Voir aussi
86+
87+- Toutes les options : [référence de la ligne de commande](../reference/cli.md)
88+- Faire marcher la complétion : [Activer la complétion Go](enable-completion.md)
89+- Une première session guidée : [Votre premier fichier dans Turbo Go](../tutorials/getting-started.md)
new file mode 100644
@@ -0,0 +1,89 @@
1+# Installer et compiler Turbo Go
2+
3+Ce guide montre comment obtenir un binaire `turbo-go` fonctionnel. Il suppose que vous avez Go 1.26 ou plus récent et que vous savez utiliser un terminal.
4+
5+## La voie rapide, depuis un clone
6+
7+```bash
8+git clone ssh://git@rickub.com/turbo-editors/turbo-go.git
9+cd turbo-go
10+make install
11+```
12+
13+Cela compile l'éditeur, le place là où votre shell cherche ses commandes, et vous dit ce qu'il a trouvé : la version de Go, où le binaire a été posé, si ce répertoire est dans votre `PATH`, et si `gopls` est installé. La compilation passe d'abord par un fichier temporaire : un échec ne remplace jamais une installation qui marchait.
14+
15+Ensuite, depuis n'importe quel projet Go :
16+
17+```bash
18+turbo-go main.go
19+```
20+
21+### Options
22+
23+```bash
24+scripts/install.sh --prefix ~/bin # installer ailleurs
25+scripts/install.sh --with-gopls # installer aussi le serveur de langage
26+scripts/install.sh --uninstall # le retirer (make uninstall)
27+scripts/install.sh --help
28+```
29+
30+Sans `--prefix`, l'éditeur va là où `go install` le mettrait : `$GOBIN`, ou `$GOPATH/bin` si `GOBIN` n'est pas défini — le plus souvent `~/go/bin`.
31+
32+## Seulement compiler, sans installer
33+
34+```bash
35+make build
36+./bin/turbo-go main.go
37+```
38+
39+## Depuis le proxy de modules, sans clone
40+
41+```bash
42+go install rickub.com/turbo-editors/turbo-go@latest
43+```
44+
45+Si la commande est ensuite « introuvable », c'est que le répertoire d'installation n'est pas dans votre `PATH` :
46+
47+```bash
48+export PATH="$PATH:$(go env GOPATH)/bin"
49+```
50+
51+## Vérifier que ça marche
52+
53+```bash
54+turbo-go -version
55+turbo-go -list-themes
56+```
57+
58+La première nomme le commit dont le binaire a été construit, ce qu'il faut citer dans un rapport de bug ; [le numéro de version](../reference/versioning.md) explique ce que signifie chaque forme. La seconde affiche les thèmes compilés dans le binaire et vous indique où placer les vôtres.
59+
60+## Variantes
61+
62+- **Vous voulez juste l'essayer une fois** : `go run rickub.com/turbo-editors/turbo-go@latest main.go`
63+- **Vous voulez le binaire à un endroit précis** : `go build -o /usr/local/bin/turbo-go .`
64+- **Votre terminal ne gère pas les couleurs 24 bits** : utilisez `turbo-go -theme turbo-classic`, construit uniquement sur les seize couleurs ANSI. `turbo-dark` et `borland-light` utilisent des couleurs 24 bits.
65+
66+## Quand quelque chose ne va pas
67+
68+**`the installed binary does not run`.** L'installeur affiche juste au-dessus ce que le système a répondu — lisez-le d'abord, c'est lui qui nomme le vrai problème.
69+
70+L'installeur **remplace** le binaire au lieu d'écrire par-dessus celui qui est là : une réinstallation donne donc au fichier une identité neuve. Cela compte sous macOS, qui met en cache la signature de code d'un binaire **par inode** : écrire de nouveaux octets dans l'ancien inode laisse une signature en cache qui décrit autre chose, et le noyau refuse alors d'exécuter un binaire qui s'est pourtant compilé et installé sans erreur. Si une ancienne copie a été installée par un outil qui employait `cp`, la supprimer d'abord efface cet état :
71+
72+```bash
73+scripts/install.sh --uninstall
74+scripts/install.sh
75+```
76+
77+**`Go x.y or later is needed`.** La version vient de `go.mod` : elle ne peut donc pas diverger de ce dont le code a réellement besoin. Mettez Go à jour, ou compilez depuis une étiquette correspondant à la chaîne d'outils dont vous disposez.
78+
79+**`build failed; nothing was installed`.** Votre installation existante est intacte — la compilation passe d'abord par un fichier temporaire. La sortie du compilateur est affichée au-dessus du message.
80+
81+## Exigences côté terminal
82+
83+Turbo Go a besoin d'un terminal qui rapporte sa taille et gère la souris — tous les terminaux courants le font. Il lit `TERM` via tcell ; si l'affichage est incorrect, vérifiez que `TERM` correspond bien à votre terminal (`xterm-256color` est une valeur sûre).
84+
85+## Voir aussi
86+
87+- Toutes les options : [référence de la ligne de commande](../reference/cli.md)
88+- Faire marcher la complétion : [Activer la complétion Go](enable-completion.md)
89+- Une première session guidée : [Votre premier fichier dans Turbo Go](../tutorials/getting-started.md)
added docs/fr/how-to/make-a-release.md +103 -0
new file mode 100644
@@ -0,0 +1,103 @@
1+# Comment faire une release
2+
3+Ce guide montre comment publier une version pour que l'éditeur annonce correctement la sienne. Il suppose que vous pouvez pousser sur le dépôt.
4+
5+## Vérifier ce que vous vous apprêtez à publier
6+
7+```sh
8+make version
9+```
10+
11+```
12+v0.1.0-14-g88a4c38 (88a4c38)
13+```
14+
15+Quatorze commits après `v0.1.0`. Un `-dirty` à la fin signifie que vous avez des modifications non validées — validez-les ou mettez-les de côté d'abord, sinon la release portera ce suffixe pour toujours.
16+
17+## Poser le tag
18+
19+```sh
20+git tag -a v0.2.0 -m "v0.2.0"
21+git push origin v0.2.0
22+```
23+
24+Le tag est l'origine du numéro : il doit exister avant de construire quoi que ce soit destiné à être distribué. Annoté (`-a`) plutôt que léger, parce que `git describe` préfère les tags annotés.
25+
26+## Construire le binaire de release
27+
28+```sh
29+make build
30+./bin/turbo-go -version
31+```
32+
33+```
34+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
35+```
36+
37+Pas de suffixe `-14-g…` : vous êtes exactement sur le tag. C'est ce qui vous dit que le tag a bien été pris.
38+
39+## Vérifier la boîte About
40+
41+Lancez l'éditeur et faites `Alt-H`, puis `A`.
42+
43+```
44+Turbo Go 0.2.0
45+
46+A Turbo C-style editor for Go,
47+written in Go.
48+
49+Commit: 88a4c38
50+Built: 2026-08-31 18:04 UTC
51+Theme: Turbo Classic
52+```
53+
54+## Ou utiliser les scripts et laisser le workflow publier
55+
56+C'est ainsi qu'une release est réellement faite. Mettez la version et sa description d'une ligne dans `release.env` — il est ignoré par git, donc la CI ne le voit jamais :
57+
58+```sh
59+TAG="v1.0.0"
60+ABOUT="Turbo Go"
61+```
62+
63+Puis lancez un seul script :
64+
65+```sh
66+./01-release.tag.sh
67+```
68+
69+Il lance `make check`, refuse un tag déjà pris en local ou sur `origin`, refuse un `go.mod` portant une directive `replace`, valide ce qui reste à valider, pousse la branche, et seulement ensuite pose le tag et le pousse. Cet ordre compte : un tag poussé avant la branche pointe sur un commit que le distant n'a jamais vu, et un tag créé avant un push refusé reste là pour que quelqu'un le trouve.
70+
71+C'est la dernière chose que vous lancez à la main. Le push du tag déclenche `.github/workflows/release.yml` ; suivez-le dans l'onglet Actions du dépôt. Il lance la suite de tests, compile les binaires avec `./02-build-releases.sh` — le même script que vous pouvez lancer sur votre machine — et crée la page de release avec : le message du tag, la ligne `go install`, les liens vers la documentation **à ce tag**, un binaire par plateforme, le `SHA256SUMS` et le README qui décrit les téléchargements.
72+
73+Le job publie avec son propre `GITHUB_TOKEN`, la seule authentification que l'API de release de Rickub accepte — un jeton personnel est refusé. Il n'y a rien à configurer et aucun secret à garder, ce qui explique la disparition des anciens `02-release.publish.sh` et `04-release.upload-binaries.sh`.
74+
75+`02-build-releases.sh` compile chaque plateforme et **estampille `TAG` lui-même**, en surchargeant la version du Makefile : `make ldflags VERSION=v1.0.0`. La release *est* `v1.0.0`, donc c'est ce que disent ses binaires — quoi qu'aurait répondu `git describe`, et que le tag existe déjà ou non. Il lance ensuite le binaire préparé pour cette machine et vérifie qu'il annonce bien la version : c'est la seule preuve que ce qui est distribué la porte.
76+
77+Vous pouvez voir ce que le workflow publiera sans rien publier, ou construire les binaires à la main :
78+
79+```sh
80+./02-build-releases.sh v1.0.0 # écrit release/v1.0.0/, ne pousse rien
81+```
82+
83+Sans argument il lit `TAG` dans `release.env` ; le workflow n'a pas de `release.env`, il passe donc le tag qui l'a déclenché.
84+
85+La suite de tests comprend des tests qui exécutent `01-release.tag.sh` contre un clone jetable. Ils se sautent eux-mêmes quand `TURBO_GO_RELEASING` est défini, ce que le script exporte avant d'appeler `make check` — retirer cette ligne fait récurser une release jusqu'à épuisement. Le workflow pose la même variable pour sa propre étape `go test`.
86+
87+## Variantes
88+
89+- **Vous installez au lieu de distribuer un binaire.** `make install` et `scripts/install.sh` estampillent de la même façon, donc un éditeur installé nomme le commit dont il vient. Il n'y a rien de plus à faire.
90+- **Quelqu'un installe avec `go install`.** `go install rickub.com/turbo-editors/turbo-go@v0.2.0` annonce `0.2.0` d'après la version du module, sans commit ni date de build. C'est le propre enregistrement de l'outil Go ; rien à estampiller.
91+- **Vous avez tagué le mauvais commit.** Si le tag n'a pas été poussé, supprimez-le (`git tag -d v1.0.0`), taguez le bon, et reconstruisez. Une fois sur `origin`, ne le déplacez pas : le proxy de modules a mis en cache `go install …@v1.0.0` et la page de release porte déjà des binaires avec ce numéro — c'est pour cela que `01-release.tag.sh` refuse un tag qui existe. Incrémentez `TAG` et refaites une release.
92+- **About affiche `devel`.** Le binaire a été construit par un `go build .` nu plutôt que par `make`. Il n'a rien d'anormal ; simplement aucun tag ne lui a été estampillé, parce que le système de build de Go ne lit pas les tags git. Utilisez `make build`.
93+- **About affiche `unknown`.** Rien n'a nommé le build — un `go run .`, ou une construction depuis un dossier sans historique git. Utilisez `make build` depuis le dépôt cloné.
94+- **Vous n'avez pas git du tout**, ayant téléchargé une archive des sources. `make build` fonctionne quand même et le binaire annonce `unknown`. Passez la version vous-même si vous en avez besoin :
95+ ```sh
96+ go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" -o bin/turbo-go .
97+ ```
98+
99+## Voir aussi
100+
101+- Toutes les sources du numéro, et ce qu'annonce chaque build : [Le numéro de version](../reference/versioning.md)
102+- Pourquoi il n'y a pas de constante de version dans les sources : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources)
103+- Installer dans votre PATH : [Comment installer et construire Turbo Go](install.md)
new file mode 100644
@@ -0,0 +1,103 @@
1+# Comment faire une release
2+
3+Ce guide montre comment publier une version pour que l'éditeur annonce correctement la sienne. Il suppose que vous pouvez pousser sur le dépôt.
4+
5+## Vérifier ce que vous vous apprêtez à publier
6+
7+```sh
8+make version
9+```
10+
11+```
12+v0.1.0-14-g88a4c38 (88a4c38)
13+```
14+
15+Quatorze commits après `v0.1.0`. Un `-dirty` à la fin signifie que vous avez des modifications non validées — validez-les ou mettez-les de côté d'abord, sinon la release portera ce suffixe pour toujours.
16+
17+## Poser le tag
18+
19+```sh
20+git tag -a v0.2.0 -m "v0.2.0"
21+git push origin v0.2.0
22+```
23+
24+Le tag est l'origine du numéro : il doit exister avant de construire quoi que ce soit destiné à être distribué. Annoté (`-a`) plutôt que léger, parce que `git describe` préfère les tags annotés.
25+
26+## Construire le binaire de release
27+
28+```sh
29+make build
30+./bin/turbo-go -version
31+```
32+
33+```
34+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
35+```
36+
37+Pas de suffixe `-14-g…` : vous êtes exactement sur le tag. C'est ce qui vous dit que le tag a bien été pris.
38+
39+## Vérifier la boîte About
40+
41+Lancez l'éditeur et faites `Alt-H`, puis `A`.
42+
43+```
44+Turbo Go 0.2.0
45+
46+A Turbo C-style editor for Go,
47+written in Go.
48+
49+Commit: 88a4c38
50+Built: 2026-08-31 18:04 UTC
51+Theme: Turbo Classic
52+```
53+
54+## Ou utiliser les scripts et laisser le workflow publier
55+
56+C'est ainsi qu'une release est réellement faite. Mettez la version et sa description d'une ligne dans `release.env` — il est ignoré par git, donc la CI ne le voit jamais :
57+
58+```sh
59+TAG="v1.0.0"
60+ABOUT="Turbo Go"
61+```
62+
63+Puis lancez un seul script :
64+
65+```sh
66+./01-release.tag.sh
67+```
68+
69+Il lance `make check`, refuse un tag déjà pris en local ou sur `origin`, refuse un `go.mod` portant une directive `replace`, valide ce qui reste à valider, pousse la branche, et seulement ensuite pose le tag et le pousse. Cet ordre compte : un tag poussé avant la branche pointe sur un commit que le distant n'a jamais vu, et un tag créé avant un push refusé reste là pour que quelqu'un le trouve.
70+
71+C'est la dernière chose que vous lancez à la main. Le push du tag déclenche `.github/workflows/release.yml` ; suivez-le dans l'onglet Actions du dépôt. Il lance la suite de tests, compile les binaires avec `./02-build-releases.sh` — le même script que vous pouvez lancer sur votre machine — et crée la page de release avec : le message du tag, la ligne `go install`, les liens vers la documentation **à ce tag**, un binaire par plateforme, le `SHA256SUMS` et le README qui décrit les téléchargements.
72+
73+Le job publie avec son propre `GITHUB_TOKEN`, la seule authentification que l'API de release de Rickub accepte — un jeton personnel est refusé. Il n'y a rien à configurer et aucun secret à garder, ce qui explique la disparition des anciens `02-release.publish.sh` et `04-release.upload-binaries.sh`.
74+
75+`02-build-releases.sh` compile chaque plateforme et **estampille `TAG` lui-même**, en surchargeant la version du Makefile : `make ldflags VERSION=v1.0.0`. La release *est* `v1.0.0`, donc c'est ce que disent ses binaires — quoi qu'aurait répondu `git describe`, et que le tag existe déjà ou non. Il lance ensuite le binaire préparé pour cette machine et vérifie qu'il annonce bien la version : c'est la seule preuve que ce qui est distribué la porte.
76+
77+Vous pouvez voir ce que le workflow publiera sans rien publier, ou construire les binaires à la main :
78+
79+```sh
80+./02-build-releases.sh v1.0.0 # écrit release/v1.0.0/, ne pousse rien
81+```
82+
83+Sans argument il lit `TAG` dans `release.env` ; le workflow n'a pas de `release.env`, il passe donc le tag qui l'a déclenché.
84+
85+La suite de tests comprend des tests qui exécutent `01-release.tag.sh` contre un clone jetable. Ils se sautent eux-mêmes quand `TURBO_GO_RELEASING` est défini, ce que le script exporte avant d'appeler `make check` — retirer cette ligne fait récurser une release jusqu'à épuisement. Le workflow pose la même variable pour sa propre étape `go test`.
86+
87+## Variantes
88+
89+- **Vous installez au lieu de distribuer un binaire.** `make install` et `scripts/install.sh` estampillent de la même façon, donc un éditeur installé nomme le commit dont il vient. Il n'y a rien de plus à faire.
90+- **Quelqu'un installe avec `go install`.** `go install rickub.com/turbo-editors/turbo-go@v0.2.0` annonce `0.2.0` d'après la version du module, sans commit ni date de build. C'est le propre enregistrement de l'outil Go ; rien à estampiller.
91+- **Vous avez tagué le mauvais commit.** Si le tag n'a pas été poussé, supprimez-le (`git tag -d v1.0.0`), taguez le bon, et reconstruisez. Une fois sur `origin`, ne le déplacez pas : le proxy de modules a mis en cache `go install …@v1.0.0` et la page de release porte déjà des binaires avec ce numéro — c'est pour cela que `01-release.tag.sh` refuse un tag qui existe. Incrémentez `TAG` et refaites une release.
92+- **About affiche `devel`.** Le binaire a été construit par un `go build .` nu plutôt que par `make`. Il n'a rien d'anormal ; simplement aucun tag ne lui a été estampillé, parce que le système de build de Go ne lit pas les tags git. Utilisez `make build`.
93+- **About affiche `unknown`.** Rien n'a nommé le build — un `go run .`, ou une construction depuis un dossier sans historique git. Utilisez `make build` depuis le dépôt cloné.
94+- **Vous n'avez pas git du tout**, ayant téléchargé une archive des sources. `make build` fonctionne quand même et le binaire annonce `unknown`. Passez la version vous-même si vous en avez besoin :
95+ ```sh
96+ go build -ldflags "-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0'" -o bin/turbo-go .
97+ ```
98+
99+## Voir aussi
100+
101+- Toutes les sources du numéro, et ce qu'annonce chaque build : [Le numéro de version](../reference/versioning.md)
102+- Pourquoi il n'y a pas de constante de version dans les sources : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources)
103+- Installer dans votre PATH : [Comment installer et construire Turbo Go](install.md)
added docs/fr/how-to/navigate-code.md +74 -0
new file mode 100644
@@ -0,0 +1,74 @@
1+# Se déplacer dans un fichier
2+
3+Ce guide montre comment atteindre le bout de code que vous cherchez. Il suppose qu'un fichier est ouvert.
4+
5+## Chercher du texte
6+
7+Appuyez sur **Ctrl-F**, tapez ce que vous cherchez, appuyez sur **Entrée**. La première occurrence est sélectionnée.
8+
9+- **F7** — occurrence suivante
10+- **Maj-F7** — occurrence précédente
11+
12+La recherche **boucle** : appuyer plusieurs fois sur F7 fait le tour de toutes les occurrences au lieu de s'arrêter en bas du fichier. La casse est ignorée sauf si vous cochez `Case sensitive` dans la boîte de recherche.
13+
14+## Aller à un numéro de ligne
15+
16+Appuyez sur **Ctrl-G**, tapez le numéro, appuyez sur **Entrée**. Les lignes sont numérotées à partir de un, comme dans les messages du compilateur.
17+
18+## Aller à une déclaration
19+
20+Placez le curseur sur un nom et appuyez sur **F12**. Turbo Go demande au serveur de langage où il est déclaré et ouvre ce fichier, à cette ligne. S'il y a plusieurs déclarations, il propose la liste.
21+
22+C'est l'une des huit questions que le menu **Code** pose au serveur — qu'est-ce qui implémente ceci, où est-ce utilisé, qu'est-ce qui ne va pas dans ce fichier. Voir [Comment interroger le code](ask-about-code.md).
23+
24+Si le fichier est déjà ouvert, sa fenêtre passe au premier plan au lieu d'en ouvrir une seconde.
25+
26+> Cela nécessite gopls. Voir [Activer la complétion Go](enable-completion.md).
27+
28+## Sélectionner et éditer des lignes entières
29+
30+| | |
31+| --- | --- |
32+| **Double-clic sur un mot** | Le sélectionner. Taper le remplace alors ; maintenir le bouton après le second clic étend la sélection depuis le début du mot. |
33+| **Ctrl-N** | Ouvrir une ligne vide **au-dessus** du curseur. Le curseur reste sur son propre texte, désormais une ligne plus bas — de la place faite au-dessus de ce que l'on regarde. |
34+| **Ctrl-Y** | Supprimer la ligne où est le curseur. Le curseur garde son numéro de ligne : maintenir la touche supprime une série de lignes. |
35+
36+Ce sont les touches de Turbo C. `Ctrl-Y` est la raison pour laquelle **rétablir est `Ctrl-R`** et non plus `Ctrl-Y` : entre un éditeur qui ressemble à Turbo C et une habitude prise ici, le premier l'a emporté. `Ctrl-Shift-Z` n'était pas disponible pour y déplacer rétablir — un terminal le livre comme un simple `Ctrl-Z`.
37+
38+Un double-clic hors d'un mot — sur une espace, une parenthèse — déplace le curseur et ne sélectionne rien. Les éditeurs ne s'accordent pas sur ce que veut dire « une suite de ponctuation », et ne rien sélectionner est au moins une réponse prévisible.
39+
40+## Se déplacer par mot, par ligne, par fichier
41+
42+| | |
43+| --- | --- |
44+| **Ctrl-←** / **Ctrl-→** | Mot précédent / suivant |
45+| **Origine** / **Fin** | Début / fin de la ligne |
46+| **Ctrl-Origine** / **Ctrl-Fin** | Début / fin du fichier |
47+| **Page↑** / **Page↓** | Un écran |
48+
49+Maintenez **Maj** avec l'une de ces touches pour sélectionner au passage.
50+
51+## Passer d'une fenêtre à l'autre
52+
53+| | |
54+| --- | --- |
55+| **F6** | La fenêtre située derrière |
56+| **Alt-1****Alt-9** | La fenêtre numérotée — le numéro est dans son coin supérieur droit |
57+| **Alt-0** | La liste de toutes les fenêtres ouvertes |
58+
59+Si les fenêtres se recouvrent, `Window ▸ Tile` les dispose côte à côte et `Window ▸ Cascade` les empile en laissant tous les titres visibles.
60+
61+## Variantes
62+
63+**Le fichier n'est pas du Go.** Tout ce qui précède fonctionne sauf F12, qui a besoin d'un serveur de langage. La coloration est également désactivée : seuls les fichiers `.go` sont colorés.
64+
65+**Vous voulez savoir où vous êtes.** L'extrémité droite de la barre d'état affiche en permanence `ligne:colonne`, à partir de un.
66+
67+**Vous avez redimensionné le terminal.** Les fenêtres le suivent : celle qui remplissait le terminal le remplit toujours, et celle que vous aviez déplacée garde son coin là où vous l'aviez mis. Aucune ne reste plus grande que le terminal.
68+
69+**La ligne est plus large que la fenêtre.** La vue défile latéralement pour suivre le curseur ; la barre du bas de la fenêtre indique où vous en êtes.
70+
71+## Voir aussi
72+
73+- Toutes les touches : [référence du clavier](../reference/keyboard.md)
74+- Toutes les entrées de menu : [référence des menus](../reference/menus.md)
new file mode 100644
@@ -0,0 +1,74 @@
1+# Se déplacer dans un fichier
2+
3+Ce guide montre comment atteindre le bout de code que vous cherchez. Il suppose qu'un fichier est ouvert.
4+
5+## Chercher du texte
6+
7+Appuyez sur **Ctrl-F**, tapez ce que vous cherchez, appuyez sur **Entrée**. La première occurrence est sélectionnée.
8+
9+- **F7** — occurrence suivante
10+- **Maj-F7** — occurrence précédente
11+
12+La recherche **boucle** : appuyer plusieurs fois sur F7 fait le tour de toutes les occurrences au lieu de s'arrêter en bas du fichier. La casse est ignorée sauf si vous cochez `Case sensitive` dans la boîte de recherche.
13+
14+## Aller à un numéro de ligne
15+
16+Appuyez sur **Ctrl-G**, tapez le numéro, appuyez sur **Entrée**. Les lignes sont numérotées à partir de un, comme dans les messages du compilateur.
17+
18+## Aller à une déclaration
19+
20+Placez le curseur sur un nom et appuyez sur **F12**. Turbo Go demande au serveur de langage où il est déclaré et ouvre ce fichier, à cette ligne. S'il y a plusieurs déclarations, il propose la liste.
21+
22+C'est l'une des huit questions que le menu **Code** pose au serveur — qu'est-ce qui implémente ceci, où est-ce utilisé, qu'est-ce qui ne va pas dans ce fichier. Voir [Comment interroger le code](ask-about-code.md).
23+
24+Si le fichier est déjà ouvert, sa fenêtre passe au premier plan au lieu d'en ouvrir une seconde.
25+
26+> Cela nécessite gopls. Voir [Activer la complétion Go](enable-completion.md).
27+
28+## Sélectionner et éditer des lignes entières
29+
30+| | |
31+| --- | --- |
32+| **Double-clic sur un mot** | Le sélectionner. Taper le remplace alors ; maintenir le bouton après le second clic étend la sélection depuis le début du mot. |
33+| **Ctrl-N** | Ouvrir une ligne vide **au-dessus** du curseur. Le curseur reste sur son propre texte, désormais une ligne plus bas — de la place faite au-dessus de ce que l'on regarde. |
34+| **Ctrl-Y** | Supprimer la ligne où est le curseur. Le curseur garde son numéro de ligne : maintenir la touche supprime une série de lignes. |
35+
36+Ce sont les touches de Turbo C. `Ctrl-Y` est la raison pour laquelle **rétablir est `Ctrl-R`** et non plus `Ctrl-Y` : entre un éditeur qui ressemble à Turbo C et une habitude prise ici, le premier l'a emporté. `Ctrl-Shift-Z` n'était pas disponible pour y déplacer rétablir — un terminal le livre comme un simple `Ctrl-Z`.
37+
38+Un double-clic hors d'un mot — sur une espace, une parenthèse — déplace le curseur et ne sélectionne rien. Les éditeurs ne s'accordent pas sur ce que veut dire « une suite de ponctuation », et ne rien sélectionner est au moins une réponse prévisible.
39+
40+## Se déplacer par mot, par ligne, par fichier
41+
42+| | |
43+| --- | --- |
44+| **Ctrl-←** / **Ctrl-→** | Mot précédent / suivant |
45+| **Origine** / **Fin** | Début / fin de la ligne |
46+| **Ctrl-Origine** / **Ctrl-Fin** | Début / fin du fichier |
47+| **Page↑** / **Page↓** | Un écran |
48+
49+Maintenez **Maj** avec l'une de ces touches pour sélectionner au passage.
50+
51+## Passer d'une fenêtre à l'autre
52+
53+| | |
54+| --- | --- |
55+| **F6** | La fenêtre située derrière |
56+| **Alt-1****Alt-9** | La fenêtre numérotée — le numéro est dans son coin supérieur droit |
57+| **Alt-0** | La liste de toutes les fenêtres ouvertes |
58+
59+Si les fenêtres se recouvrent, `Window ▸ Tile` les dispose côte à côte et `Window ▸ Cascade` les empile en laissant tous les titres visibles.
60+
61+## Variantes
62+
63+**Le fichier n'est pas du Go.** Tout ce qui précède fonctionne sauf F12, qui a besoin d'un serveur de langage. La coloration est également désactivée : seuls les fichiers `.go` sont colorés.
64+
65+**Vous voulez savoir où vous êtes.** L'extrémité droite de la barre d'état affiche en permanence `ligne:colonne`, à partir de un.
66+
67+**Vous avez redimensionné le terminal.** Les fenêtres le suivent : celle qui remplissait le terminal le remplit toujours, et celle que vous aviez déplacée garde son coin là où vous l'aviez mis. Aucune ne reste plus grande que le terminal.
68+
69+**La ligne est plus large que la fenêtre.** La vue défile latéralement pour suivre le curseur ; la barre du bas de la fenêtre indique où vous en êtes.
70+
71+## Voir aussi
72+
73+- Toutes les touches : [référence du clavier](../reference/keyboard.md)
74+- Toutes les entrées de menu : [référence des menus](../reference/menus.md)
added docs/fr/how-to/run-go-commands.md +214 -0
new file mode 100644
@@ -0,0 +1,214 @@
1+# Lancer les commandes go depuis l'éditeur
2+
3+Ce guide montre comment formater, vérifier, compiler, tester et exécuter votre projet sans quitter Turbo Go. Il suppose l'éditeur installé et un projet Go sous la main.
4+
5+## Obtenir un fichier de départ
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Go ▸ Create tools file** (`Alt-G`, puis `C`).
8+
9+Cela écrit `.turbo-go/tools.toml` avec les cinq commandes qu'un projet Go passe avant de commiter, et l'ouvre :
10+
11+```toml
12+[[tool]]
13+name = "~F~ormat"
14+command = "gofmt -l -w ."
15+output = "popup"
16+
17+[[tool]]
18+name = "~T~est"
19+command = "go test ./..."
20+output = "popup"
21+
22+[[tool]]
23+name = "~R~un"
24+command = "go run ."
25+# Un terminal, pas une popup : un programme qui lit le clavier doit pouvoir
26+# recevoir une réponse, et un programme long doit pouvoir être interrompu.
27+output = "terminal"
28+```
29+
30+Chaque `[[tool]]` devient une ligne du menu **Go**, dans l'ordre du fichier — sauf s'il nomme un `menu` à lui, ce que couvre la section suivante mais une. Le fichier est relu à **chaque ouverture du menu** : une modification prend effet immédiatement.
31+
32+## En lancer une
33+
34+`Alt-G`, puis la lettre entre les tildes — `F` pour formater, `T` pour tester.
35+
36+Une **popup** s'ouvre aussitôt et se remplit à mesure. Son titre porte la commande et, une fois terminée, son issue :
37+
38+```
39+┌──────────── go vet ./... — exit 1 ────────────┐
40+│ main.go:6:2: unreachable code │
41+│ │
42+│ [ Close ] │
43+└───────────────────────────────────────────────┘
44+```
45+
46+| Touche | Effet |
47+| --- | --- |
48+| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Parcourir la sortie |
49+| `Échap` | Fermer — et **arrêter la commande** si elle tourne encore |
50+| `Entrée` | Fermer |
51+
52+Une commande qui a réussi sans rien dire affiche `(no output)` plutôt qu'une boîte vide : on la distingue ainsi d'une commande qui n'a pas démarré.
53+
54+La popup suit la sortie tant que vous n'avez pas remonté ; ensuite elle vous laisse où vous êtes.
55+
56+## Choisir où va la sortie
57+
58+Posez `output` sur un outil :
59+
60+| `output` | Ce que vous obtenez |
61+| --- | --- |
62+| `popup` | Un dialogue qui se remplit pendant l'exécution. Le défaut. |
63+| `terminal` | Une fenêtre terminal : couleurs, `Ctrl-C`, et le clavier atteint le programme |
64+| `editor` | Une fenêtre d'édition une fois terminé, à fouiller avec `Ctrl-F` |
65+
66+`Run` est en `terminal` dans le fichier de départ, et c'est l'exemple même de pourquoi la clé existe : une popup ne peut pas répondre à un programme qui lit le clavier, ni être interrompue par `Ctrl-C`.
67+
68+Prenez `editor` quand la sortie est quelque chose à éplucher — un long `go test -v`, ou un rapport de couverture à parcourir.
69+
70+## Une commande longue immobilise l'éditeur
71+
72+Une popup est modale : pendant que `go build` tourne, vous ne pouvez taper nulle part ailleurs. `Échap` la ferme et arrête la commande.
73+
74+Si cela gêne pour une commande précise, donnez-lui `output = "terminal"` — la fenêtre est ordinaire et vous continuez à travailler à côté. C'est précisément l'intérêt d'avoir rendu la clé configurable.
75+
76+## Ce qui arrive à vos fichiers ouverts
77+
78+`Format` réécrit les fichiers sur le disque — y compris celui que vous regardez. À la fin d'une commande, l'éditeur **relit tout fichier ouvert n'ayant aucune modification non enregistrée** : la version formatée apparaît sans que vous ayez rien à faire. La barre d'état dit combien.
79+
80+Un fichier ayant des modifications non enregistrées est **laissé tel quel**, et la barre d'état le dit aussi :
81+
82+```
83+Reloaded 2 files; 1 file with unsaved changes left alone
84+```
85+
86+C'est délibéré : votre modification et le formateur sont réellement en désaccord, et l'éditeur n'est pas celui qui doit trancher. Enregistrez d'abord (`F2`) puis relancez la commande, ou continuez à éditer et formatez plus tard.
87+
88+## Ajouter vos propres commandes
89+
90+Éditez `.turbo-go/tools.toml`. Une commande passe par `sh -c`, donc une seule entrée peut être toute une séquence :
91+
92+```toml
93+[[tool]]
94+name = "~C~heck"
95+command = "gofmt -l -w . && go vet ./... && go test ./..."
96+output = "popup"
97+
98+[[tool]]
99+name = "Tid~y~"
100+command = "go mod tidy"
101+output = "popup"
102+
103+[[tool]]
104+name = "Cover~a~ge"
105+command = "go test -coverprofile=cover.out ./... && go tool cover -func=cover.out"
106+output = "editor"
107+```
108+
109+Donnez à chacune une touche d'accès avec des tildes, et gardez-les distinctes — le menu répond à la première correspondance trouvée.
110+
111+## Mettre un outil dans un menu à lui
112+
113+Un outil qui n'a rien à voir avec Go n'a rien à faire dans le menu Go. Donnez-lui un `menu` :
114+
115+```toml
116+[[tool]]
117+name = "~E~cho"
118+command = "echo TADA"
119+output = "terminal"
120+menu = "Tools"
121+
122+[[tool]]
123+name = "~U~p"
124+command = "docker compose up -d"
125+menu = "Docker"
126+
127+[[tool]]
128+name = "~D~own"
129+command = "docker compose down"
130+menu = "Docker"
131+```
132+
133+Vous obtenez un menu **Tools** et un menu **Docker** sur la barre, entre Go et Help, dans l'ordre où les noms apparaissent pour la première fois dans le fichier. Docker contient ses deux outils. Rien à redémarrer : enregistrez le fichier et la barre suit.
134+
135+Le nom vous appartient — il n'y a pas de liste où piocher. Omettez `menu` et l'outil reste dans Go, où sont les cinq commandes de départ.
136+
137+### La touche d'accès est choisie pour vous
138+
139+Vous ne pouvez pas savoir, en écrivant le fichier, quelles lettres les menus de l'éditeur occupent déjà. Il s'en charge : la première lettre du nom que rien d'autre ne revendique reçoit les tildes.
140+
141+`Tools` obtient `Alt-T`. `Format` obtient `Alt-M`, parce que `F` est à File, `o` à Options et `r` à Run.
142+
143+Écrivez les tildes vous-même — `menu = "Doc~k~er"` — et une lettre libre est conservée. Une lettre prise ne l'est pas : la barre répond au *premier* menu correspondant à une touche, donc honorer votre choix rendrait l'un des deux menus inatteignable. Elle en choisit une autre, sans rien dire.
144+
145+## Variantes
146+
147+- **Vous préférez `golangci-lint` à `go vet`.** Changez la commande `Lint`. `go vet` est le défaut parce qu'il fait partie de la chaîne d'outils et n'est jamais absent ; tout le reste s'installe.
148+- **Vous avez lancé l'éditeur depuis un sous-dossier.** Les commandes s'y exécutent, donc `./...` ne couvre que ce sous-arbre. Lancez depuis la racine du projet.
149+- **Le fichier contient une erreur.** Le menu affiche un `Cannot read tools` grisé à la place des commandes, et **Create tools file** reste là.
150+- **Une commande n'est pas installée.** La popup affiche `command not found` et `— exit 127`, ce qu'un shell aurait dit.
151+- **Vous voulez un menu portant le nom d'un menu existant.** `menu = "File"` vous donne un second menu File, plus loin sur la barre, avec une autre touche d'accès. Rien ne l'empêche ; rien ne le recommande non plus.
152+- **Votre menu n'a pas de touche d'accès.** Toutes les lettres de son nom étaient déjà prises. `F10` et les flèches y accèdent, la souris aussi. Renommez-le avec une lettre libre.
153+- **Vous avez mal orthographié la valeur d'`output`.** Tout le fichier est refusé et le menu affiche `Cannot read tools`, en nommant l'outil et en listant les valeurs possibles. Un repli silencieux aurait envoyé la sortie ailleurs que là où vous l'aviez demandée.
154+
155+## Demander une valeur au lancement
156+
157+Certaines commandes ont besoin de quelque chose de saisi à chaque fois : un chemin de module, un nom de caisse, un test à filtrer. Mettez un `{{libellé}}` à l'endroit où la valeur va :
158+
159+```toml
160+[[tool]]
161+name = "~I~nit module"
162+command = "go mod init {{module path}}"
163+output = "popup"
164+```
165+
166+Choisir cette entrée ouvre une boîte intitulée **Init module** avec un champ, libellé `module path`. Tapez la valeur et appuyez sur Entrée ; la commande se lance avec. Échap, et rien ne se lance.
167+
168+La valeur est protégée, si bien qu'un chemin contenant une espace reste un seul argument.
169+
170+### Plusieurs valeurs à la fois
171+
172+Un champ chacune, dans l'ordre où elles apparaissent :
173+
174+```toml
175+[[tool]]
176+name = "~C~opy"
177+command = "cp {{from}} {{to}}"
178+```
179+
180+**Tab** passe d'un champ à l'autre, **Entrée** lance.
181+
182+### Un champ valant plusieurs arguments
183+
184+La protection est le mauvais choix quand on veut dire « ajoute ces options à la fin ». Ajoutez `...` à l'intérieur des accolades et la valeur passe telle quelle :
185+
186+```toml
187+[[tool]]
188+name = "Test ~o~ne"
189+command = "go test {{extra flags...}} ./..."
190+```
191+
192+Tapez `-run TestParse -v` et le tout atteint la commande sous forme d'arguments séparés.
193+
194+### La même valeur deux fois
195+
196+Écrivez le libellé deux fois ; on ne vous le demande qu'une :
197+
198+```toml
199+[[tool]]
200+name = "~N~ew directory"
201+command = "mkdir {{name}} && cd {{name}}"
202+```
203+
204+### Variantes
205+
206+- **La valeur est souvent la même.** Lancez-le une fois et la boîte retient ce que vous avez tapé, pour le reste de la session. Rien n'est écrit sur le disque.
207+- **Votre commande contient déjà des accolades.** `awk '{print $1}'` et `find . -exec rm {} +` sont laissés tranquilles : seules les doubles accolades demandent quelque chose.
208+- **La commande demande plus de valeurs que l'écran n'en contient.** L'éditeur le dit plutôt que d'ouvrir une boîte dont le bouton OK est sous le bas du terminal. Agrandissez le terminal, ou coupez la commande en deux outils.
209+
210+## Voir aussi
211+
212+- Toutes les clés du fichier et toutes les règles : [Référence des outils go](../reference/go-tools.md)
213+- Pourquoi chaque commande a sa fenêtre terminal, et pourquoi un fichier non modifié se recharge : [Outils go](../explanation/go-tools.md)
214+- Les fenêtres dans lesquelles les commandes tournent : [Fenêtres terminal](../reference/terminal.md)
new file mode 100644
@@ -0,0 +1,214 @@
1+# Lancer les commandes go depuis l'éditeur
2+
3+Ce guide montre comment formater, vérifier, compiler, tester et exécuter votre projet sans quitter Turbo Go. Il suppose l'éditeur installé et un projet Go sous la main.
4+
5+## Obtenir un fichier de départ
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Go ▸ Create tools file** (`Alt-G`, puis `C`).
8+
9+Cela écrit `.turbo-go/tools.toml` avec les cinq commandes qu'un projet Go passe avant de commiter, et l'ouvre :
10+
11+```toml
12+[[tool]]
13+name = "~F~ormat"
14+command = "gofmt -l -w ."
15+output = "popup"
16+
17+[[tool]]
18+name = "~T~est"
19+command = "go test ./..."
20+output = "popup"
21+
22+[[tool]]
23+name = "~R~un"
24+command = "go run ."
25+# Un terminal, pas une popup : un programme qui lit le clavier doit pouvoir
26+# recevoir une réponse, et un programme long doit pouvoir être interrompu.
27+output = "terminal"
28+```
29+
30+Chaque `[[tool]]` devient une ligne du menu **Go**, dans l'ordre du fichier — sauf s'il nomme un `menu` à lui, ce que couvre la section suivante mais une. Le fichier est relu à **chaque ouverture du menu** : une modification prend effet immédiatement.
31+
32+## En lancer une
33+
34+`Alt-G`, puis la lettre entre les tildes — `F` pour formater, `T` pour tester.
35+
36+Une **popup** s'ouvre aussitôt et se remplit à mesure. Son titre porte la commande et, une fois terminée, son issue :
37+
38+```
39+┌──────────── go vet ./... — exit 1 ────────────┐
40+│ main.go:6:2: unreachable code │
41+│ │
42+│ [ Close ] │
43+└───────────────────────────────────────────────┘
44+```
45+
46+| Touche | Effet |
47+| --- | --- |
48+| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Parcourir la sortie |
49+| `Échap` | Fermer — et **arrêter la commande** si elle tourne encore |
50+| `Entrée` | Fermer |
51+
52+Une commande qui a réussi sans rien dire affiche `(no output)` plutôt qu'une boîte vide : on la distingue ainsi d'une commande qui n'a pas démarré.
53+
54+La popup suit la sortie tant que vous n'avez pas remonté ; ensuite elle vous laisse où vous êtes.
55+
56+## Choisir où va la sortie
57+
58+Posez `output` sur un outil :
59+
60+| `output` | Ce que vous obtenez |
61+| --- | --- |
62+| `popup` | Un dialogue qui se remplit pendant l'exécution. Le défaut. |
63+| `terminal` | Une fenêtre terminal : couleurs, `Ctrl-C`, et le clavier atteint le programme |
64+| `editor` | Une fenêtre d'édition une fois terminé, à fouiller avec `Ctrl-F` |
65+
66+`Run` est en `terminal` dans le fichier de départ, et c'est l'exemple même de pourquoi la clé existe : une popup ne peut pas répondre à un programme qui lit le clavier, ni être interrompue par `Ctrl-C`.
67+
68+Prenez `editor` quand la sortie est quelque chose à éplucher — un long `go test -v`, ou un rapport de couverture à parcourir.
69+
70+## Une commande longue immobilise l'éditeur
71+
72+Une popup est modale : pendant que `go build` tourne, vous ne pouvez taper nulle part ailleurs. `Échap` la ferme et arrête la commande.
73+
74+Si cela gêne pour une commande précise, donnez-lui `output = "terminal"` — la fenêtre est ordinaire et vous continuez à travailler à côté. C'est précisément l'intérêt d'avoir rendu la clé configurable.
75+
76+## Ce qui arrive à vos fichiers ouverts
77+
78+`Format` réécrit les fichiers sur le disque — y compris celui que vous regardez. À la fin d'une commande, l'éditeur **relit tout fichier ouvert n'ayant aucune modification non enregistrée** : la version formatée apparaît sans que vous ayez rien à faire. La barre d'état dit combien.
79+
80+Un fichier ayant des modifications non enregistrées est **laissé tel quel**, et la barre d'état le dit aussi :
81+
82+```
83+Reloaded 2 files; 1 file with unsaved changes left alone
84+```
85+
86+C'est délibéré : votre modification et le formateur sont réellement en désaccord, et l'éditeur n'est pas celui qui doit trancher. Enregistrez d'abord (`F2`) puis relancez la commande, ou continuez à éditer et formatez plus tard.
87+
88+## Ajouter vos propres commandes
89+
90+Éditez `.turbo-go/tools.toml`. Une commande passe par `sh -c`, donc une seule entrée peut être toute une séquence :
91+
92+```toml
93+[[tool]]
94+name = "~C~heck"
95+command = "gofmt -l -w . && go vet ./... && go test ./..."
96+output = "popup"
97+
98+[[tool]]
99+name = "Tid~y~"
100+command = "go mod tidy"
101+output = "popup"
102+
103+[[tool]]
104+name = "Cover~a~ge"
105+command = "go test -coverprofile=cover.out ./... && go tool cover -func=cover.out"
106+output = "editor"
107+```
108+
109+Donnez à chacune une touche d'accès avec des tildes, et gardez-les distinctes — le menu répond à la première correspondance trouvée.
110+
111+## Mettre un outil dans un menu à lui
112+
113+Un outil qui n'a rien à voir avec Go n'a rien à faire dans le menu Go. Donnez-lui un `menu` :
114+
115+```toml
116+[[tool]]
117+name = "~E~cho"
118+command = "echo TADA"
119+output = "terminal"
120+menu = "Tools"
121+
122+[[tool]]
123+name = "~U~p"
124+command = "docker compose up -d"
125+menu = "Docker"
126+
127+[[tool]]
128+name = "~D~own"
129+command = "docker compose down"
130+menu = "Docker"
131+```
132+
133+Vous obtenez un menu **Tools** et un menu **Docker** sur la barre, entre Go et Help, dans l'ordre où les noms apparaissent pour la première fois dans le fichier. Docker contient ses deux outils. Rien à redémarrer : enregistrez le fichier et la barre suit.
134+
135+Le nom vous appartient — il n'y a pas de liste où piocher. Omettez `menu` et l'outil reste dans Go, où sont les cinq commandes de départ.
136+
137+### La touche d'accès est choisie pour vous
138+
139+Vous ne pouvez pas savoir, en écrivant le fichier, quelles lettres les menus de l'éditeur occupent déjà. Il s'en charge : la première lettre du nom que rien d'autre ne revendique reçoit les tildes.
140+
141+`Tools` obtient `Alt-T`. `Format` obtient `Alt-M`, parce que `F` est à File, `o` à Options et `r` à Run.
142+
143+Écrivez les tildes vous-même — `menu = "Doc~k~er"` — et une lettre libre est conservée. Une lettre prise ne l'est pas : la barre répond au *premier* menu correspondant à une touche, donc honorer votre choix rendrait l'un des deux menus inatteignable. Elle en choisit une autre, sans rien dire.
144+
145+## Variantes
146+
147+- **Vous préférez `golangci-lint` à `go vet`.** Changez la commande `Lint`. `go vet` est le défaut parce qu'il fait partie de la chaîne d'outils et n'est jamais absent ; tout le reste s'installe.
148+- **Vous avez lancé l'éditeur depuis un sous-dossier.** Les commandes s'y exécutent, donc `./...` ne couvre que ce sous-arbre. Lancez depuis la racine du projet.
149+- **Le fichier contient une erreur.** Le menu affiche un `Cannot read tools` grisé à la place des commandes, et **Create tools file** reste là.
150+- **Une commande n'est pas installée.** La popup affiche `command not found` et `— exit 127`, ce qu'un shell aurait dit.
151+- **Vous voulez un menu portant le nom d'un menu existant.** `menu = "File"` vous donne un second menu File, plus loin sur la barre, avec une autre touche d'accès. Rien ne l'empêche ; rien ne le recommande non plus.
152+- **Votre menu n'a pas de touche d'accès.** Toutes les lettres de son nom étaient déjà prises. `F10` et les flèches y accèdent, la souris aussi. Renommez-le avec une lettre libre.
153+- **Vous avez mal orthographié la valeur d'`output`.** Tout le fichier est refusé et le menu affiche `Cannot read tools`, en nommant l'outil et en listant les valeurs possibles. Un repli silencieux aurait envoyé la sortie ailleurs que là où vous l'aviez demandée.
154+
155+## Demander une valeur au lancement
156+
157+Certaines commandes ont besoin de quelque chose de saisi à chaque fois : un chemin de module, un nom de caisse, un test à filtrer. Mettez un `{{libellé}}` à l'endroit où la valeur va :
158+
159+```toml
160+[[tool]]
161+name = "~I~nit module"
162+command = "go mod init {{module path}}"
163+output = "popup"
164+```
165+
166+Choisir cette entrée ouvre une boîte intitulée **Init module** avec un champ, libellé `module path`. Tapez la valeur et appuyez sur Entrée ; la commande se lance avec. Échap, et rien ne se lance.
167+
168+La valeur est protégée, si bien qu'un chemin contenant une espace reste un seul argument.
169+
170+### Plusieurs valeurs à la fois
171+
172+Un champ chacune, dans l'ordre où elles apparaissent :
173+
174+```toml
175+[[tool]]
176+name = "~C~opy"
177+command = "cp {{from}} {{to}}"
178+```
179+
180+**Tab** passe d'un champ à l'autre, **Entrée** lance.
181+
182+### Un champ valant plusieurs arguments
183+
184+La protection est le mauvais choix quand on veut dire « ajoute ces options à la fin ». Ajoutez `...` à l'intérieur des accolades et la valeur passe telle quelle :
185+
186+```toml
187+[[tool]]
188+name = "Test ~o~ne"
189+command = "go test {{extra flags...}} ./..."
190+```
191+
192+Tapez `-run TestParse -v` et le tout atteint la commande sous forme d'arguments séparés.
193+
194+### La même valeur deux fois
195+
196+Écrivez le libellé deux fois ; on ne vous le demande qu'une :
197+
198+```toml
199+[[tool]]
200+name = "~N~ew directory"
201+command = "mkdir {{name}} && cd {{name}}"
202+```
203+
204+### Variantes
205+
206+- **La valeur est souvent la même.** Lancez-le une fois et la boîte retient ce que vous avez tapé, pour le reste de la session. Rien n'est écrit sur le disque.
207+- **Votre commande contient déjà des accolades.** `awk '{print $1}'` et `find . -exec rm {} +` sont laissés tranquilles : seules les doubles accolades demandent quelque chose.
208+- **La commande demande plus de valeurs que l'écran n'en contient.** L'éditeur le dit plutôt que d'ouvrir une boîte dont le bouton OK est sous le bas du terminal. Agrandissez le terminal, ou coupez la commande en deux outils.
209+
210+## Voir aussi
211+
212+- Toutes les clés du fichier et toutes les règles : [Référence des outils go](../reference/go-tools.md)
213+- Pourquoi chaque commande a sa fenêtre terminal, et pourquoi un fichier non modifié se recharge : [Outils go](../explanation/go-tools.md)
214+- Les fenêtres dans lesquelles les commandes tournent : [Fenêtres terminal](../reference/terminal.md)
added docs/fr/how-to/run-the-tests.md +95 -0
new file mode 100644
@@ -0,0 +1,95 @@
1+# Lancer les tests
2+
3+Ce guide montre comment exécuter et lire la suite de tests de Turbo Go. Il suppose que vous avez un clone du dépôt et Go 1.26 ou plus récent.
4+
5+## Toute la suite
6+
7+```bash
8+make test
9+```
10+
11+C'est la commande unique documentée. Elle exécute `go test ./...` sur tous les paquets.
12+
13+## Variantes
14+
15+**Voir chaque test par son nom :**
16+
17+```bash
18+make test-verbose
19+```
20+
21+**Mesurer la couverture par paquet :**
22+
23+```bash
24+make cover
25+```
26+
27+**Un seul paquet :**
28+
29+```bash
30+go test ./internal/golang/
31+```
32+
33+**Sans lancer de serveur de langage.** Un test de `internal/golang` démarre un vrai `gopls` s'il en trouve un. Pour le sauter :
34+
35+```bash
36+go test -short ./...
37+```
38+
39+**Avec le détecteur de compétition.** Le client LSP est concurrent : cela vaut la peine avant d'y toucher.
40+
41+```bash
42+go test -race ./...
43+```
44+
45+**Tout ce qu'un commit devrait passer :**
46+
47+```bash
48+make check
49+```
50+
51+Cela enchaîne `go fmt`, `go vet` et les tests, dans cet ordre.
52+
53+## Ce que la suite couvre
54+
55+Aucun test n'a besoin d'un vrai terminal. Les widgets et l'éditeur sont dessinés sur le `SimulationScreen` de tcell — un vrai `Screen` qui dessine en mémoire — si bien que les assertions portent sur l'image qu'un terminal afficherait réellement. Le client LSP est testé contre un serveur de langage tournant dans le même processus, à travers un tube en mémoire.
56+
57+La seule exception est `TestAgainstRealGopls`, qui démarre le vrai serveur. Il **se saute lui-même** quand `gopls` n'est pas installé : un clone sans serveur de langage a donc quand même une suite verte.
58+
59+## Tester sur un turbo-core non publié
60+
61+L'essentiel de Turbo Go est turbo-core, et ce dépôt en dépend par version, depuis le proxy de modules :
62+
63+```
64+require rickub.com/turbo-editors/turbo-core v0.2.0
65+```
66+
67+Une modification faite dans une copie de turbo-core placée à côté de celle-ci est donc invisible ici tant qu'elle n'est pas publiée. Pour la tester avant, créez un espace de travail :
68+
69+```bash
70+go work init . ../turbo-core
71+make test
72+```
73+
74+Chaque import de la bibliothèque pointe désormais sur cette copie. Ni `go.mod` ni `go.sum` ne changent : il n'y a donc rien à défaire. Vérifiez que c'est bien pris en compte — c'est l'erreur contre laquelle il faut se prémunir, car sinon tout compile et tout passe quand même :
75+
76+```bash
77+go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app
78+```
79+
80+La réponse doit être votre copie de travail, pas un chemin sous `pkg/mod`. Une fois terminé, `rm go.work go.work.sum` ; le fichier est ignoré par git, il ne peut donc pas être commité par accident.
81+
82+## Qualité du code
83+
84+La suite de tests n'est pas toute la porte de qualité. Celle-ci se mesure à part :
85+
86+```bash
87+python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .
88+```
89+
90+Elle écrit un rapport sous `.quality/` et sort en erreur si la porte échoue.
91+
92+## Voir aussi
93+
94+- Pourquoi les tests ont cette forme : [Architecture](../explanation/architecture.md)
95+- Toutes les cibles make : [référence de la ligne de commande](../reference/cli.md)
new file mode 100644
@@ -0,0 +1,95 @@
1+# Lancer les tests
2+
3+Ce guide montre comment exécuter et lire la suite de tests de Turbo Go. Il suppose que vous avez un clone du dépôt et Go 1.26 ou plus récent.
4+
5+## Toute la suite
6+
7+```bash
8+make test
9+```
10+
11+C'est la commande unique documentée. Elle exécute `go test ./...` sur tous les paquets.
12+
13+## Variantes
14+
15+**Voir chaque test par son nom :**
16+
17+```bash
18+make test-verbose
19+```
20+
21+**Mesurer la couverture par paquet :**
22+
23+```bash
24+make cover
25+```
26+
27+**Un seul paquet :**
28+
29+```bash
30+go test ./internal/golang/
31+```
32+
33+**Sans lancer de serveur de langage.** Un test de `internal/golang` démarre un vrai `gopls` s'il en trouve un. Pour le sauter :
34+
35+```bash
36+go test -short ./...
37+```
38+
39+**Avec le détecteur de compétition.** Le client LSP est concurrent : cela vaut la peine avant d'y toucher.
40+
41+```bash
42+go test -race ./...
43+```
44+
45+**Tout ce qu'un commit devrait passer :**
46+
47+```bash
48+make check
49+```
50+
51+Cela enchaîne `go fmt`, `go vet` et les tests, dans cet ordre.
52+
53+## Ce que la suite couvre
54+
55+Aucun test n'a besoin d'un vrai terminal. Les widgets et l'éditeur sont dessinés sur le `SimulationScreen` de tcell — un vrai `Screen` qui dessine en mémoire — si bien que les assertions portent sur l'image qu'un terminal afficherait réellement. Le client LSP est testé contre un serveur de langage tournant dans le même processus, à travers un tube en mémoire.
56+
57+La seule exception est `TestAgainstRealGopls`, qui démarre le vrai serveur. Il **se saute lui-même** quand `gopls` n'est pas installé : un clone sans serveur de langage a donc quand même une suite verte.
58+
59+## Tester sur un turbo-core non publié
60+
61+L'essentiel de Turbo Go est turbo-core, et ce dépôt en dépend par version, depuis le proxy de modules :
62+
63+```
64+require rickub.com/turbo-editors/turbo-core v0.2.0
65+```
66+
67+Une modification faite dans une copie de turbo-core placée à côté de celle-ci est donc invisible ici tant qu'elle n'est pas publiée. Pour la tester avant, créez un espace de travail :
68+
69+```bash
70+go work init . ../turbo-core
71+make test
72+```
73+
74+Chaque import de la bibliothèque pointe désormais sur cette copie. Ni `go.mod` ni `go.sum` ne changent : il n'y a donc rien à défaire. Vérifiez que c'est bien pris en compte — c'est l'erreur contre laquelle il faut se prémunir, car sinon tout compile et tout passe quand même :
75+
76+```bash
77+go list -f '{{.Dir}}' rickub.com/turbo-editors/turbo-core/app
78+```
79+
80+La réponse doit être votre copie de travail, pas un chemin sous `pkg/mod`. Une fois terminé, `rm go.work go.work.sum` ; le fichier est ignoré par git, il ne peut donc pas être commité par accident.
81+
82+## Qualité du code
83+
84+La suite de tests n'est pas toute la porte de qualité. Celle-ci se mesure à part :
85+
86+```bash
87+python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .
88+```
89+
90+Elle écrit un rapport sous `.quality/` et sort en erreur si la porte échoue.
91+
92+## Voir aussi
93+
94+- Pourquoi les tests ont cette forme : [Architecture](../explanation/architecture.md)
95+- Toutes les cibles make : [référence de la ligne de commande](../reference/cli.md)
added docs/fr/how-to/talk-to-an-agent.md +177 -0
new file mode 100644
@@ -0,0 +1,177 @@
1+# Dialoguer avec un agent de code depuis l'éditeur
2+
3+Ce guide montre comment pointer Turbo Go vers un agent qui parle l'[Agent Client Protocol](https://agentclientprotocol.com), ouvrir une fenêtre dessus, et tenir une conversation sur le code en cours d'édition. Il suppose que Turbo Go est déjà lancé dans un projet.
4+
5+Turbo Go est un **client** ACP. Il lance l'agent comme processus fils et lui parle en JSON-RPC sur son entrée et sa sortie standard — le même montage que Zed, donc un agent qui fonctionne là-bas fonctionne ici.
6+
7+## Déclarer un agent à l'éditeur
8+
9+Les agents sont listés dans `acp.toml`. Choisissez **Agent ▸ Create agents file** et l'éditeur écrit un fichier de départ dans `.turbo-go/acp.toml`, puis l'ouvre.
10+
11+Un agent, c'est un bloc `[[agent]]` :
12+
13+```toml
14+[[agent]]
15+name = "Bob (llama.cpp)"
16+command = "docker"
17+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
18+env = { TELEMETRY_ENABLED = "false" }
19+```
20+
21+`name` est ce qu'affiche le menu Agent et le nom de la fenêtre. `command` et `args` disent comment démarrer l'agent. C'est tout — le fichier est relu à chaque ouverture de fenêtre, donc on ne redémarre jamais l'éditeur pour essayer une modification.
22+
23+Listez-en autant que vous voulez. Chacun devient une ligne du menu, et chaque fenêtre ouverte depuis cette ligne est un processus distinct avec sa propre conversation.
24+
25+## Placer la configuration de l'agent à côté
26+
27+La plupart des agents ont leur propre fichier de configuration, et `.turbo-go/` est un endroit raisonnable pour le garder afin qu'il voyage avec le projet. Pour `docker agent`, l'enregistrer sous `.turbo-go/agent.yaml` correspond aux `args` ci-dessus :
28+
29+```yaml
30+providers:
31+ llamacpp:
32+ api_type: openai_chatcompletions
33+ base_url: http://localhost:8080/v1
34+
35+models:
36+ mellum2:
37+ provider: llamacpp
38+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
39+ temperature: 0.7
40+ provider_opts:
41+ context_size: 262144
42+
43+agents:
44+ root:
45+ model: mellum2
46+ description: A helpful AI assistant running on a local llama.cpp server
47+ instruction: |
48+ You name is Bob 🤓, you are a knowledgeable code assistant.
49+ Be helpful, accurate, and concise in your responses.
50+ You have access to the local filesystem and shell: use these tools
51+ toolsets:
52+ - type: filesystem
53+ - type: shell
54+```
55+
56+## Ouvrir une fenêtre dessus
57+
58+Appuyez sur `Alt-A`, ou choisissez **Agent** dans la barre de menus, puis l'agent par son nom.
59+
60+Une fenêtre s'ouvre, coupée en deux : la conversation en haut, une zone de saisie en bas. L'agent est démarré à l'ouverture de la fenêtre et arrêté à sa fermeture.
61+
62+```
63+┌ Bob (llama.cpp) ───────────────────────────────[■]┐
64+│ ‣ Vous │
65+│ Que fait buildMenus ? │
66+│ │
67+│ ‣ Shell ls -1 internal/ ✓ fini │
68+│ golang │
69+│ │
70+│ ‣ Bob │
71+│ Elle assemble la barre de menus. Sa forme : │
72+│ │
73+│ ```go │
74+│ func (a *App) buildMenus() *ui.MenuBar { │
75+│ return ui.NewMenuBar(a.allMenus()...) │
76+│ } │
77+│ ``` │
78+├───────────────────────────────────────────────────┤
79+│ > _ │
80+└───────────────────────────────────────────────────┘
81+```
82+
83+Le code que l'agent envoie dans un bloc délimité est coloré par les mêmes analyseurs que l'éditeur utilise pour les fichiers : une réponse en Go est colorée comme du Go, une réponse en shell comme du shell. Un bloc annonçant un langage que l'éditeur ne colore pas est laissé brut plutôt que deviné.
84+
85+## Tenir la conversation
86+
87+| Touche | Effet |
88+| --- | --- |
89+| `Entrée` | Envoyer ce qui est saisi |
90+| `Alt-Entrée` | Passer à la ligne au lieu d'envoyer |
91+| `Tab` | Passer de la conversation à la zone de saisie, et retour |
92+| `PgUp` `PgDn` | Faire défiler la conversation d'un écran |
93+| `Échap` | Arrêter le tour en cours |
94+| `Ctrl-W` | Fermer la fenêtre, et arrêter l'agent avec elle |
95+
96+Pendant que l'agent répond, sa réponse s'affiche au fil de l'écriture plutôt que d'un bloc, et la règle entre les deux zones fait tourner un indicateur à côté du mot *thinking*. `Échap` l'interrompt — l'agent reçoit l'ordre de s'arrêter, et ce qu'il avait déjà dit reste dans la fenêtre.
97+
98+## Utiliser les commandes propres à l'agent
99+
100+Certains agents répondent à des commandes — `/compact`, `/web`, `/plan` — et disent à l'éditeur lesquelles. Tapez `/` comme premier caractère de la zone de saisie et la liste s'ouvre par-dessus la conversation : chaque commande, ce qu'elle fait, et entre chevrons ce qu'elle attend après son nom.
101+
102+Continuez à taper pour la réduire, `↑` `↓` pour vous déplacer, puis `Tab` pour compléter. Une commande qui attend quelque chose est complétée avec une espace après elle, prête à recevoir la suite ; appuyez sur `Entrée` quand la ligne dit ce que vous voulez. Si rien n'apparaît quand vous tapez `/`, l'agent n'a annoncé aucune commande — **Agent ▸ Agent status** le dit — et `/` n'est qu'un caractère.
103+
104+## Désigner un fichier à l'agent
105+
106+Tapez `@` n'importe où dans la zone de saisie et les fichiers du projet apparaissent. Tapez quelques lettres du nom du fichier pour réduire la liste, `Tab` pour prendre celui en surbrillance :
107+
108+```
109+> explique ce que fait @internal/scanner.go
110+```
111+
112+Quand vous appuyez sur `Entrée`, l'agent reçoit le **fichier**, et pas seulement son nom : son texte quand l'agent accepte le contexte incorporé, un lien vers lui sinon. Si le fichier est ouvert dans l'éditeur avec des modifications non enregistrées, c'est votre version non enregistrée qui part. La ligne reste dans la conversation telle que vous l'avez tapée.
113+
114+Plusieurs fichiers dans une invite, c'est plusieurs `@`. Un mot qui commence par `@` mais n'est pas un fichier — une adresse électronique — est laissé en texte.
115+
116+## Récupérer un morceau de la conversation
117+
118+Appuyez sur `Tab` pour placer le curseur dans la conversation. La règle change et annonce ce que font désormais les touches.
119+
120+| Touche | Effet |
121+| --- | --- |
122+| `↑` `↓` `PgUp` `PgDn` | Déplacer le curseur dans ce qui a été dit |
123+| `Shift-↑` `Shift-↓` | Sélectionner des lignes entières |
124+| Glisser à la souris | Pareil, à la main |
125+| `Ctrl-C` | Copier |
126+| `Échap` | Abandonner la sélection |
127+| `Tab` | Revenir à la zone de saisie |
128+
129+**Sans rien de sélectionné, `Ctrl-C` copie le bloc sur lequel est le curseur** — un bloc de code délimité, un paragraphe, la sortie d'un outil — sans le libellé de l'interlocuteur au-dessus ni la phrase qui suit. C'est presque toujours ce que vous vouliez, et cela évite de le sélectionner à la main.
130+
131+Ce qui est copié va dans **deux** presse-papiers : celui de l'éditeur, pour que `Shift-Ins` le colle dans un fichier ouvert ici, et celui du système, pour que `Ctrl-V` le colle n'importe où ailleurs. L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge.
132+
133+La moitié « système » passe par votre terminal (une séquence d'échappement nommée OSC 52). La plupart des terminaux la gèrent ; quelques-uns la refusent par sécurité, et certains demandent de l'activer. Si `Ctrl-V` ailleurs ne donne rien, c'est là qu'il faut regarder — le presse-papiers de l'éditeur contient le texte dans tous les cas.
134+
135+## Répondre quand l'agent demande la permission
136+
137+Un agent doté d'un outil shell ou d'un outil de fichiers demande avant de s'en servir. Une boîte de dialogue nomme l'outil et la commande exacte, et propose les choix que l'agent lui-même a proposés — d'ordinaire *Autoriser*, *Autoriser et retenir mon choix*, et *Passer*.
138+
139+```
140+┌────────── Bob (llama.cpp) veut lancer ──────────┐
141+│ │
142+│ Shell │
143+│ ls -1 │
144+│ │
145+│ [ Autoriser ] [ Toujours ] [ Passer ] │
146+└─────────────────────────────────────────────────┘
147+```
148+
149+*Toujours* est retenu par l'agent, pas par l'éditeur : ce que cela couvre et combien de temps cela dure sont l'affaire de l'agent. Échap équivaut à *Passer*.
150+
151+Rien ne s'exécute avant votre réponse. Un agent en attente d'une boîte de permission est simplement bloqué, et c'est bien le but.
152+
153+## Laisser l'agent voir ce qui n'est pas encore enregistré
154+
155+L'éditeur offre à l'agent son propre système de fichiers : quand l'agent lit un fichier que vous avez ouvert avec des modifications non enregistrées, il reçoit **le texte du tampon**, pas le texte plus ancien du disque. C'est généralement ce qu'on veut — vous posez une question sur la modification que vous venez de faire.
156+
157+Quand l'agent écrit un fichier, la modification arrive dans le tampon et la fenêtre est marquée modifiée : vous pouvez la lire, l'annuler avec `Ctrl-Z`, ou l'enregistrer avec `F2`. Un fichier que vous n'avez pas ouvert est lu et écrit directement sur le disque.
158+
159+## Faire tourner plusieurs agents à la fois
160+
161+Chaque fenêtre est son propre processus et sa propre conversation. Ouvrir deux fois le même agent donne deux sessions indépendantes, et ouvrir deux agents différents permet de mettre côte à côte un modèle local rapide et un modèle lent et soigneux — **Window ▸ Tile** les dispose.
162+
163+Quitter l'éditeur arrête tous les agents.
164+
165+## Variantes
166+
167+- **Vous voulez que l'agent tourne ailleurs qu'à la racine du projet.** Ajoutez `cwd = "backend"` à son bloc. Le chemin est relatif au projet, et c'est à la fois l'endroit où le processus démarre et le dossier de travail annoncé à l'agent.
168+- **L'agent a besoin d'un identifiant.** Mettez-le dans `env`, ou comptez sur sa présence dans l'environnement depuis lequel vous lancez l'éditeur — l'agent en hérite.
169+- **Les commandes de l'agent n'apparaissent pas quand vous tapez `/`.** Ouvrez **Agent ▸ Agent status** avec la fenêtre devant. S'il ne liste aucune commande, l'agent n'en a annoncé aucune — ou les a annoncées dans une forme que cet éditeur n'a pas su lire, auquel cas la boîte nomme la mise à jour et l'erreur de décodage. Pour voir exactement ce qui est passé sur le fil, lancez l'éditeur avec `TURBO_ACP_TRACE=/tmp/acp.log` et lisez le fichier : `->` est ce que l'éditeur a envoyé, `<-` ce que l'agent a répondu.
170+- **L'agent ne démarre pas.** **Agent ▸ Agent status** liste ce qui a été lu dans `acp.toml`, la ligne de commande obtenue pour chaque agent, et l'erreur de tout ce qui n'a pas démarré. Ce que l'agent écrit sur sa sortie d'erreur y figure aussi, et c'est là qu'un point d'accès de modèle mal configuré se signale.
171+- **Vous gardez le même agent dans tous les projets.** Mettez le bloc `[[agent]]` dans `~/.config/turbo-go/acp.toml`. Le fichier du projet est lu ensuite, et un agent du même `name` y remplace le vôtre.
172+
173+## Voir aussi
174+
175+- Chaque clé du fichier, et la part exacte du protocole implémentée : [référence Agents et ACP](../reference/acp.md)
176+- Pourquoi un agent est une fenêtre et non un panneau, et pourquoi les permissions sont modales : [Fenêtres agent](../explanation/agent-windows.md)
177+- Le protocole lui-même : [agentclientprotocol.com](https://agentclientprotocol.com)
new file mode 100644
@@ -0,0 +1,177 @@
1+# Dialoguer avec un agent de code depuis l'éditeur
2+
3+Ce guide montre comment pointer Turbo Go vers un agent qui parle l'[Agent Client Protocol](https://agentclientprotocol.com), ouvrir une fenêtre dessus, et tenir une conversation sur le code en cours d'édition. Il suppose que Turbo Go est déjà lancé dans un projet.
4+
5+Turbo Go est un **client** ACP. Il lance l'agent comme processus fils et lui parle en JSON-RPC sur son entrée et sa sortie standard — le même montage que Zed, donc un agent qui fonctionne là-bas fonctionne ici.
6+
7+## Déclarer un agent à l'éditeur
8+
9+Les agents sont listés dans `acp.toml`. Choisissez **Agent ▸ Create agents file** et l'éditeur écrit un fichier de départ dans `.turbo-go/acp.toml`, puis l'ouvre.
10+
11+Un agent, c'est un bloc `[[agent]]` :
12+
13+```toml
14+[[agent]]
15+name = "Bob (llama.cpp)"
16+command = "docker"
17+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
18+env = { TELEMETRY_ENABLED = "false" }
19+```
20+
21+`name` est ce qu'affiche le menu Agent et le nom de la fenêtre. `command` et `args` disent comment démarrer l'agent. C'est tout — le fichier est relu à chaque ouverture de fenêtre, donc on ne redémarre jamais l'éditeur pour essayer une modification.
22+
23+Listez-en autant que vous voulez. Chacun devient une ligne du menu, et chaque fenêtre ouverte depuis cette ligne est un processus distinct avec sa propre conversation.
24+
25+## Placer la configuration de l'agent à côté
26+
27+La plupart des agents ont leur propre fichier de configuration, et `.turbo-go/` est un endroit raisonnable pour le garder afin qu'il voyage avec le projet. Pour `docker agent`, l'enregistrer sous `.turbo-go/agent.yaml` correspond aux `args` ci-dessus :
28+
29+```yaml
30+providers:
31+ llamacpp:
32+ api_type: openai_chatcompletions
33+ base_url: http://localhost:8080/v1
34+
35+models:
36+ mellum2:
37+ provider: llamacpp
38+ model: JetBrains/Mellum2-12B-A2.5B-Instruct-GGUF-Q4_K_M:Q4_K_M
39+ temperature: 0.7
40+ provider_opts:
41+ context_size: 262144
42+
43+agents:
44+ root:
45+ model: mellum2
46+ description: A helpful AI assistant running on a local llama.cpp server
47+ instruction: |
48+ You name is Bob 🤓, you are a knowledgeable code assistant.
49+ Be helpful, accurate, and concise in your responses.
50+ You have access to the local filesystem and shell: use these tools
51+ toolsets:
52+ - type: filesystem
53+ - type: shell
54+```
55+
56+## Ouvrir une fenêtre dessus
57+
58+Appuyez sur `Alt-A`, ou choisissez **Agent** dans la barre de menus, puis l'agent par son nom.
59+
60+Une fenêtre s'ouvre, coupée en deux : la conversation en haut, une zone de saisie en bas. L'agent est démarré à l'ouverture de la fenêtre et arrêté à sa fermeture.
61+
62+```
63+┌ Bob (llama.cpp) ───────────────────────────────[■]┐
64+│ ‣ Vous │
65+│ Que fait buildMenus ? │
66+│ │
67+│ ‣ Shell ls -1 internal/ ✓ fini │
68+│ golang │
69+│ │
70+│ ‣ Bob │
71+│ Elle assemble la barre de menus. Sa forme : │
72+│ │
73+│ ```go │
74+│ func (a *App) buildMenus() *ui.MenuBar { │
75+│ return ui.NewMenuBar(a.allMenus()...) │
76+│ } │
77+│ ``` │
78+├───────────────────────────────────────────────────┤
79+│ > _ │
80+└───────────────────────────────────────────────────┘
81+```
82+
83+Le code que l'agent envoie dans un bloc délimité est coloré par les mêmes analyseurs que l'éditeur utilise pour les fichiers : une réponse en Go est colorée comme du Go, une réponse en shell comme du shell. Un bloc annonçant un langage que l'éditeur ne colore pas est laissé brut plutôt que deviné.
84+
85+## Tenir la conversation
86+
87+| Touche | Effet |
88+| --- | --- |
89+| `Entrée` | Envoyer ce qui est saisi |
90+| `Alt-Entrée` | Passer à la ligne au lieu d'envoyer |
91+| `Tab` | Passer de la conversation à la zone de saisie, et retour |
92+| `PgUp` `PgDn` | Faire défiler la conversation d'un écran |
93+| `Échap` | Arrêter le tour en cours |
94+| `Ctrl-W` | Fermer la fenêtre, et arrêter l'agent avec elle |
95+
96+Pendant que l'agent répond, sa réponse s'affiche au fil de l'écriture plutôt que d'un bloc, et la règle entre les deux zones fait tourner un indicateur à côté du mot *thinking*. `Échap` l'interrompt — l'agent reçoit l'ordre de s'arrêter, et ce qu'il avait déjà dit reste dans la fenêtre.
97+
98+## Utiliser les commandes propres à l'agent
99+
100+Certains agents répondent à des commandes — `/compact`, `/web`, `/plan` — et disent à l'éditeur lesquelles. Tapez `/` comme premier caractère de la zone de saisie et la liste s'ouvre par-dessus la conversation : chaque commande, ce qu'elle fait, et entre chevrons ce qu'elle attend après son nom.
101+
102+Continuez à taper pour la réduire, `↑` `↓` pour vous déplacer, puis `Tab` pour compléter. Une commande qui attend quelque chose est complétée avec une espace après elle, prête à recevoir la suite ; appuyez sur `Entrée` quand la ligne dit ce que vous voulez. Si rien n'apparaît quand vous tapez `/`, l'agent n'a annoncé aucune commande — **Agent ▸ Agent status** le dit — et `/` n'est qu'un caractère.
103+
104+## Désigner un fichier à l'agent
105+
106+Tapez `@` n'importe où dans la zone de saisie et les fichiers du projet apparaissent. Tapez quelques lettres du nom du fichier pour réduire la liste, `Tab` pour prendre celui en surbrillance :
107+
108+```
109+> explique ce que fait @internal/scanner.go
110+```
111+
112+Quand vous appuyez sur `Entrée`, l'agent reçoit le **fichier**, et pas seulement son nom : son texte quand l'agent accepte le contexte incorporé, un lien vers lui sinon. Si le fichier est ouvert dans l'éditeur avec des modifications non enregistrées, c'est votre version non enregistrée qui part. La ligne reste dans la conversation telle que vous l'avez tapée.
113+
114+Plusieurs fichiers dans une invite, c'est plusieurs `@`. Un mot qui commence par `@` mais n'est pas un fichier — une adresse électronique — est laissé en texte.
115+
116+## Récupérer un morceau de la conversation
117+
118+Appuyez sur `Tab` pour placer le curseur dans la conversation. La règle change et annonce ce que font désormais les touches.
119+
120+| Touche | Effet |
121+| --- | --- |
122+| `↑` `↓` `PgUp` `PgDn` | Déplacer le curseur dans ce qui a été dit |
123+| `Shift-↑` `Shift-↓` | Sélectionner des lignes entières |
124+| Glisser à la souris | Pareil, à la main |
125+| `Ctrl-C` | Copier |
126+| `Échap` | Abandonner la sélection |
127+| `Tab` | Revenir à la zone de saisie |
128+
129+**Sans rien de sélectionné, `Ctrl-C` copie le bloc sur lequel est le curseur** — un bloc de code délimité, un paragraphe, la sortie d'un outil — sans le libellé de l'interlocuteur au-dessus ni la phrase qui suit. C'est presque toujours ce que vous vouliez, et cela évite de le sélectionner à la main.
130+
131+Ce qui est copié va dans **deux** presse-papiers : celui de l'éditeur, pour que `Shift-Ins` le colle dans un fichier ouvert ici, et celui du système, pour que `Ctrl-V` le colle n'importe où ailleurs. L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge.
132+
133+La moitié « système » passe par votre terminal (une séquence d'échappement nommée OSC 52). La plupart des terminaux la gèrent ; quelques-uns la refusent par sécurité, et certains demandent de l'activer. Si `Ctrl-V` ailleurs ne donne rien, c'est là qu'il faut regarder — le presse-papiers de l'éditeur contient le texte dans tous les cas.
134+
135+## Répondre quand l'agent demande la permission
136+
137+Un agent doté d'un outil shell ou d'un outil de fichiers demande avant de s'en servir. Une boîte de dialogue nomme l'outil et la commande exacte, et propose les choix que l'agent lui-même a proposés — d'ordinaire *Autoriser*, *Autoriser et retenir mon choix*, et *Passer*.
138+
139+```
140+┌────────── Bob (llama.cpp) veut lancer ──────────┐
141+│ │
142+│ Shell │
143+│ ls -1 │
144+│ │
145+│ [ Autoriser ] [ Toujours ] [ Passer ] │
146+└─────────────────────────────────────────────────┘
147+```
148+
149+*Toujours* est retenu par l'agent, pas par l'éditeur : ce que cela couvre et combien de temps cela dure sont l'affaire de l'agent. Échap équivaut à *Passer*.
150+
151+Rien ne s'exécute avant votre réponse. Un agent en attente d'une boîte de permission est simplement bloqué, et c'est bien le but.
152+
153+## Laisser l'agent voir ce qui n'est pas encore enregistré
154+
155+L'éditeur offre à l'agent son propre système de fichiers : quand l'agent lit un fichier que vous avez ouvert avec des modifications non enregistrées, il reçoit **le texte du tampon**, pas le texte plus ancien du disque. C'est généralement ce qu'on veut — vous posez une question sur la modification que vous venez de faire.
156+
157+Quand l'agent écrit un fichier, la modification arrive dans le tampon et la fenêtre est marquée modifiée : vous pouvez la lire, l'annuler avec `Ctrl-Z`, ou l'enregistrer avec `F2`. Un fichier que vous n'avez pas ouvert est lu et écrit directement sur le disque.
158+
159+## Faire tourner plusieurs agents à la fois
160+
161+Chaque fenêtre est son propre processus et sa propre conversation. Ouvrir deux fois le même agent donne deux sessions indépendantes, et ouvrir deux agents différents permet de mettre côte à côte un modèle local rapide et un modèle lent et soigneux — **Window ▸ Tile** les dispose.
162+
163+Quitter l'éditeur arrête tous les agents.
164+
165+## Variantes
166+
167+- **Vous voulez que l'agent tourne ailleurs qu'à la racine du projet.** Ajoutez `cwd = "backend"` à son bloc. Le chemin est relatif au projet, et c'est à la fois l'endroit où le processus démarre et le dossier de travail annoncé à l'agent.
168+- **L'agent a besoin d'un identifiant.** Mettez-le dans `env`, ou comptez sur sa présence dans l'environnement depuis lequel vous lancez l'éditeur — l'agent en hérite.
169+- **Les commandes de l'agent n'apparaissent pas quand vous tapez `/`.** Ouvrez **Agent ▸ Agent status** avec la fenêtre devant. S'il ne liste aucune commande, l'agent n'en a annoncé aucune — ou les a annoncées dans une forme que cet éditeur n'a pas su lire, auquel cas la boîte nomme la mise à jour et l'erreur de décodage. Pour voir exactement ce qui est passé sur le fil, lancez l'éditeur avec `TURBO_ACP_TRACE=/tmp/acp.log` et lisez le fichier : `->` est ce que l'éditeur a envoyé, `<-` ce que l'agent a répondu.
170+- **L'agent ne démarre pas.** **Agent ▸ Agent status** liste ce qui a été lu dans `acp.toml`, la ligne de commande obtenue pour chaque agent, et l'erreur de tout ce qui n'a pas démarré. Ce que l'agent écrit sur sa sortie d'erreur y figure aussi, et c'est là qu'un point d'accès de modèle mal configuré se signale.
171+- **Vous gardez le même agent dans tous les projets.** Mettez le bloc `[[agent]]` dans `~/.config/turbo-go/acp.toml`. Le fichier du projet est lu ensuite, et un agent du même `name` y remplace le vôtre.
172+
173+## Voir aussi
174+
175+- Chaque clé du fichier, et la part exacte du protocole implémentée : [référence Agents et ACP](../reference/acp.md)
176+- Pourquoi un agent est une fenêtre et non un panneau, et pourquoi les permissions sont modales : [Fenêtres agent](../explanation/agent-windows.md)
177+- Le protocole lui-même : [agentclientprotocol.com](https://agentclientprotocol.com)
added docs/fr/how-to/use-a-terminal.md +61 -0
new file mode 100644
@@ -0,0 +1,61 @@
1+# Lancer des commandes shell sans quitter l'éditeur
2+
3+Ce guide montre comment ouvrir une fenêtre terminal, y compiler et tester le code en cours d'édition, puis revenir au fichier. Il suppose que Turbo Go est déjà lancé avec un fichier ouvert.
4+
5+## Ouvrir un terminal
6+
7+Appuyez sur `F8`, ou choisissez **Window ▸ New terminal**.
8+
9+Une nouvelle fenêtre s'ouvre avec votre shell, dans le dossier du fichier que vous étiez en train d'éditer. C'est normalement le dossier voulu : `go build ./...` et `git diff` portent tous deux sur le paquet que vous avez sous les yeux.
10+
11+La fenêtre porte le nom du shell, et se renomme dès qu'un programme lancé dedans définit un titre — `vim`, `htop` et `ssh` le font tous.
12+
13+## Lancer quelque chose
14+
15+Tapez dedans comme dans n'importe quel terminal. Le shell reçoit presque toutes les touches, y compris celles que l'éditeur utiliserait autrement : `Ctrl-C` interrompt, `Ctrl-W` supprime un mot, `Ctrl-R` cherche dans l'historique.
16+
17+Ce que l'éditeur conserve est court, et voulu — c'est le chemin de sortie :
18+
19+| Touche | Effet, même avec un terminal au premier plan |
20+| --- | --- |
21+| `F8` | Ouvrir un autre terminal |
22+| `F6` | Passer à la fenêtre suivante |
23+| `F10` | Ouvrir la barre de menus |
24+| `F2` `F3` `F4` | Enregistrer, Ouvrir, Nouveau |
25+| `Alt-1``Alt-9` | Passer cette fenêtre au premier plan |
26+| `Alt-X` | Quitter l'éditeur |
27+
28+## Relire ce qui a défilé
29+
30+`Shift-PgUp` et `Shift-PgDn` parcourent l'historique un écran à la fois ; la molette déplace de trois lignes. Deux mille lignes sont conservées.
31+
32+Taper quoi que ce soit ramène directement à l'écran vivant : jamais besoin de redescendre avant de lancer la commande suivante.
33+
34+## Travailler avec le fichier et le shell côte à côte
35+
36+Un terminal est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent :
37+
38+- **Window ▸ Tile** place le fichier et le terminal côte à côte.
39+- **Window ▸ Maximise**, ou la case `[■]` à droite de sa barre de titre, donne tout le bureau au terminal pendant une compilation. La case affiche alors `[▬]`, et l'actionner remet la fenêtre en place.
40+- Tirez son coin inférieur droit pour le redimensionner — le shell est prévenu de sa nouvelle taille, donc `less` et `vim` se réajustent.
41+
42+## Le fermer
43+
44+`Ctrl-W` appartient au shell, pas à l'éditeur : fermer un terminal se fait donc autrement.
45+
46+- **File ▸ Close**, ou
47+- cliquez sur la case `[x]` dans son coin supérieur gauche.
48+
49+L'un comme l'autre terminent le shell qui y tourne. Rien n'est demandé au préalable : un terminal contient un processus en cours, pas un travail non enregistré, et fermer la fenêtre est la façon de dire que vous en avez fini. Quitter l'éditeur ferme tous les terminaux d'un coup.
50+
51+## Variantes
52+
53+- **Vous voulez un autre shell.** Le shell est pris dans `$SHELL`, avec `/bin/sh` par défaut ; sous Windows dans `%COMSPEC%`, avec `cmd.exe` par défaut. Lancez l'éditeur avec `SHELL=/bin/zsh turbo-go` pour le changer le temps d'une session.
54+- **Aucun fichier n'est ouvert.** Le terminal démarre dans le dossier depuis lequel l'éditeur a été lancé.
55+- **Vous êtes sous Windows.** Les fenêtres terminal tournent dans une pseudo-console (ConPTY), ce qui demande Windows 10 version 1809 ou plus récent, et le shell est `%COMSPEC%` — cmd.exe. Ce chemin a été compilé et vérifié mais pas encore exécuté par les auteurs, qui travaillent sous Linux et macOS. La première fois, essayez les cinq choses qu'il doit réussir — `F8`, tapez `dir`, redimensionnez la fenêtre, lancez une commande du menu qui dit `output = "terminal"`, et interrompez une commande longue avec `Ctrl-C` — et signalez ce qui ne s'est pas comporté comme attendu.
56+
57+## Voir aussi
58+
59+- Tout ce que le terminal implémente, exactement : [référence des fenêtres terminal](../reference/terminal.md)
60+- Pourquoi il lance un vrai shell plutôt que de capturer la sortie d'une commande : [Fenêtres terminal](../explanation/terminal-windows.md)
61+- Les couleurs qu'il utilise : [Format des fichiers de thème](../reference/themes.md)
new file mode 100644
@@ -0,0 +1,61 @@
1+# Lancer des commandes shell sans quitter l'éditeur
2+
3+Ce guide montre comment ouvrir une fenêtre terminal, y compiler et tester le code en cours d'édition, puis revenir au fichier. Il suppose que Turbo Go est déjà lancé avec un fichier ouvert.
4+
5+## Ouvrir un terminal
6+
7+Appuyez sur `F8`, ou choisissez **Window ▸ New terminal**.
8+
9+Une nouvelle fenêtre s'ouvre avec votre shell, dans le dossier du fichier que vous étiez en train d'éditer. C'est normalement le dossier voulu : `go build ./...` et `git diff` portent tous deux sur le paquet que vous avez sous les yeux.
10+
11+La fenêtre porte le nom du shell, et se renomme dès qu'un programme lancé dedans définit un titre — `vim`, `htop` et `ssh` le font tous.
12+
13+## Lancer quelque chose
14+
15+Tapez dedans comme dans n'importe quel terminal. Le shell reçoit presque toutes les touches, y compris celles que l'éditeur utiliserait autrement : `Ctrl-C` interrompt, `Ctrl-W` supprime un mot, `Ctrl-R` cherche dans l'historique.
16+
17+Ce que l'éditeur conserve est court, et voulu — c'est le chemin de sortie :
18+
19+| Touche | Effet, même avec un terminal au premier plan |
20+| --- | --- |
21+| `F8` | Ouvrir un autre terminal |
22+| `F6` | Passer à la fenêtre suivante |
23+| `F10` | Ouvrir la barre de menus |
24+| `F2` `F3` `F4` | Enregistrer, Ouvrir, Nouveau |
25+| `Alt-1``Alt-9` | Passer cette fenêtre au premier plan |
26+| `Alt-X` | Quitter l'éditeur |
27+
28+## Relire ce qui a défilé
29+
30+`Shift-PgUp` et `Shift-PgDn` parcourent l'historique un écran à la fois ; la molette déplace de trois lignes. Deux mille lignes sont conservées.
31+
32+Taper quoi que ce soit ramène directement à l'écran vivant : jamais besoin de redescendre avant de lancer la commande suivante.
33+
34+## Travailler avec le fichier et le shell côte à côte
35+
36+Un terminal est une fenêtre ordinaire, donc toutes les commandes de fenêtre s'y appliquent :
37+
38+- **Window ▸ Tile** place le fichier et le terminal côte à côte.
39+- **Window ▸ Maximise**, ou la case `[■]` à droite de sa barre de titre, donne tout le bureau au terminal pendant une compilation. La case affiche alors `[▬]`, et l'actionner remet la fenêtre en place.
40+- Tirez son coin inférieur droit pour le redimensionner — le shell est prévenu de sa nouvelle taille, donc `less` et `vim` se réajustent.
41+
42+## Le fermer
43+
44+`Ctrl-W` appartient au shell, pas à l'éditeur : fermer un terminal se fait donc autrement.
45+
46+- **File ▸ Close**, ou
47+- cliquez sur la case `[x]` dans son coin supérieur gauche.
48+
49+L'un comme l'autre terminent le shell qui y tourne. Rien n'est demandé au préalable : un terminal contient un processus en cours, pas un travail non enregistré, et fermer la fenêtre est la façon de dire que vous en avez fini. Quitter l'éditeur ferme tous les terminaux d'un coup.
50+
51+## Variantes
52+
53+- **Vous voulez un autre shell.** Le shell est pris dans `$SHELL`, avec `/bin/sh` par défaut ; sous Windows dans `%COMSPEC%`, avec `cmd.exe` par défaut. Lancez l'éditeur avec `SHELL=/bin/zsh turbo-go` pour le changer le temps d'une session.
54+- **Aucun fichier n'est ouvert.** Le terminal démarre dans le dossier depuis lequel l'éditeur a été lancé.
55+- **Vous êtes sous Windows.** Les fenêtres terminal tournent dans une pseudo-console (ConPTY), ce qui demande Windows 10 version 1809 ou plus récent, et le shell est `%COMSPEC%` — cmd.exe. Ce chemin a été compilé et vérifié mais pas encore exécuté par les auteurs, qui travaillent sous Linux et macOS. La première fois, essayez les cinq choses qu'il doit réussir — `F8`, tapez `dir`, redimensionnez la fenêtre, lancez une commande du menu qui dit `output = "terminal"`, et interrompez une commande longue avec `Ctrl-C` — et signalez ce qui ne s'est pas comporté comme attendu.
56+
57+## Voir aussi
58+
59+- Tout ce que le terminal implémente, exactement : [référence des fenêtres terminal](../reference/terminal.md)
60+- Pourquoi il lance un vrai shell plutôt que de capturer la sortie d'une commande : [Fenêtres terminal](../explanation/terminal-windows.md)
61+- Les couleurs qu'il utilise : [Format des fichiers de thème](../reference/themes.md)
added docs/fr/how-to/use-snippets.md +93 -0
new file mode 100644
@@ -0,0 +1,93 @@
1+# Insérer des snippets depuis un menu
2+
3+Ce guide montre comment mettre en place des morceaux de texte réutilisables et les insérer dans un fichier à l'endroit du curseur. Il suppose Turbo Go déjà installé.
4+
5+## Obtenir un fichier de départ
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Snippets ▸ Create snippets file** (`Alt-N`, puis `C`).
8+
9+Cela écrit `.turbo-go/snippets.toml`, rempli de quelques exemples travaillés, et l'ouvre — coloré, puisque Turbo Go colore le TOML :
10+
11+```toml
12+[[snippet]]
13+name = "if err != nil"
14+group = "Go"
15+languages = ["go"]
16+body = """
17+if err != nil {
18+ return err
19+}"""
20+
21+[[snippet]]
22+name = "TODO"
23+body = "TODO: "
24+```
25+
26+Chaque `[[snippet]]` devient une ligne du menu. Le fichier est relu **chaque fois que le menu s'ouvre** : une modification prend effet immédiatement, sans redémarrage.
27+
28+## En insérer un
29+
30+Ouvrez **Snippets** (`Alt-N`). Les snippets partageant un `group` apparaissent ensemble dans un sous-menu de ce nom ; celui qui n'a pas de groupe va dans **General**.
31+
32+| Touche | Effet |
33+| --- | --- |
34+| `Alt-N`, ou `F10` puis `→` jusqu'à Snippets | Ouvrir le menu |
35+| `↑` `↓` | Parcourir les groupes |
36+| `→`, ou `Entrée` | Ouvrir le groupe surligné |
37+| `↑` `↓` puis `Entrée` | Insérer le snippet surligné |
38+| `←` | Ressortir d'un groupe |
39+| `Échap` | Refermer tout le menu |
40+
41+Le snippet arrive au curseur. **Les lignes suivant la première sont indentées sur la ligne où vous l'avez inséré**, de sorte qu'un snippet multi-ligne déposé dans un bloc imbriqué atterrit là où vous l'auriez tapé :
42+
43+```
44+func f() {
45+ | ← curseur ici
46+}
47+```
48+
49+devient
50+
51+```
52+func f() {
53+ if err != nil {
54+ return err
55+ }
56+}
57+```
58+
59+C'est une seule opération d'annulation : `Ctrl-Z` retire tout le snippet.
60+
61+## Garder ses snippets d'un projet à l'autre
62+
63+Mettez-les dans `~/.config/turbo-go/snippets.toml` — le même dossier que vos thèmes. Ceux-là apparaissent dans tous les projets, et le fichier d'un projet s'y **ajoute** plutôt que de les remplacer.
64+
65+Quand un projet et vous employez le même `name` dans le même `group`, **celui du projet gagne** : c'est le plus spécifique des deux énoncés.
66+
67+## N'afficher un snippet que là où il a un sens
68+
69+Ajoutez `languages`, avec les noms que l'éditeur emploie — `go`, `toml`, `markdown`, `javascript`, `html`, `bash` :
70+
71+```toml
72+[[snippet]]
73+name = "strict mode"
74+group = "Shell"
75+languages = ["bash"]
76+body = "set -euo pipefail"
77+```
78+
79+Ce snippet n'apparaît alors que si un script shell est au premier plan. Omettez `languages` et le snippet est proposé partout, ce que vous voulez pour un en-tête de licence ou un `TODO`.
80+
81+Un groupe que le filtrage laisse vide n'apparaît pas du tout.
82+
83+## Variantes
84+
85+- **Vous avez lancé l'éditeur depuis un sous-dossier.** Le fichier du projet n'est pas trouvé : seul `./.turbo-go` est consulté, la même règle que `settings.toml`. Vos propres snippets apparaissent quand même.
86+- **Le fichier contient une erreur.** Le menu affiche un `Cannot read snippets` grisé à la place des groupes, et **Create snippets file** est toujours là. Ouvrez le fichier et corrigez-le.
87+- **Vous voulez des tabulations dans un corps.** Écrivez `\t`, comme le fichier de départ — le TOML le transforme en tabulation à la lecture.
88+
89+## Voir aussi
90+
91+- Toutes les clés du fichier et toutes les règles : [Référence des snippets](../reference/snippets.md)
92+- Pourquoi le menu est reconstruit à chaque ouverture, et pourquoi l'insertion réindente : [Snippets](../explanation/snippets.md)
93+- L'autre fichier de `.turbo-go` : [Réglages de projet](../reference/project-settings.md)
new file mode 100644
@@ -0,0 +1,93 @@
1+# Insérer des snippets depuis un menu
2+
3+Ce guide montre comment mettre en place des morceaux de texte réutilisables et les insérer dans un fichier à l'endroit du curseur. Il suppose Turbo Go déjà installé.
4+
5+## Obtenir un fichier de départ
6+
7+Lancez l'éditeur **depuis le dossier du projet**, puis choisissez **Snippets ▸ Create snippets file** (`Alt-N`, puis `C`).
8+
9+Cela écrit `.turbo-go/snippets.toml`, rempli de quelques exemples travaillés, et l'ouvre — coloré, puisque Turbo Go colore le TOML :
10+
11+```toml
12+[[snippet]]
13+name = "if err != nil"
14+group = "Go"
15+languages = ["go"]
16+body = """
17+if err != nil {
18+ return err
19+}"""
20+
21+[[snippet]]
22+name = "TODO"
23+body = "TODO: "
24+```
25+
26+Chaque `[[snippet]]` devient une ligne du menu. Le fichier est relu **chaque fois que le menu s'ouvre** : une modification prend effet immédiatement, sans redémarrage.
27+
28+## En insérer un
29+
30+Ouvrez **Snippets** (`Alt-N`). Les snippets partageant un `group` apparaissent ensemble dans un sous-menu de ce nom ; celui qui n'a pas de groupe va dans **General**.
31+
32+| Touche | Effet |
33+| --- | --- |
34+| `Alt-N`, ou `F10` puis `→` jusqu'à Snippets | Ouvrir le menu |
35+| `↑` `↓` | Parcourir les groupes |
36+| `→`, ou `Entrée` | Ouvrir le groupe surligné |
37+| `↑` `↓` puis `Entrée` | Insérer le snippet surligné |
38+| `←` | Ressortir d'un groupe |
39+| `Échap` | Refermer tout le menu |
40+
41+Le snippet arrive au curseur. **Les lignes suivant la première sont indentées sur la ligne où vous l'avez inséré**, de sorte qu'un snippet multi-ligne déposé dans un bloc imbriqué atterrit là où vous l'auriez tapé :
42+
43+```
44+func f() {
45+ | ← curseur ici
46+}
47+```
48+
49+devient
50+
51+```
52+func f() {
53+ if err != nil {
54+ return err
55+ }
56+}
57+```
58+
59+C'est une seule opération d'annulation : `Ctrl-Z` retire tout le snippet.
60+
61+## Garder ses snippets d'un projet à l'autre
62+
63+Mettez-les dans `~/.config/turbo-go/snippets.toml` — le même dossier que vos thèmes. Ceux-là apparaissent dans tous les projets, et le fichier d'un projet s'y **ajoute** plutôt que de les remplacer.
64+
65+Quand un projet et vous employez le même `name` dans le même `group`, **celui du projet gagne** : c'est le plus spécifique des deux énoncés.
66+
67+## N'afficher un snippet que là où il a un sens
68+
69+Ajoutez `languages`, avec les noms que l'éditeur emploie — `go`, `toml`, `markdown`, `javascript`, `html`, `bash` :
70+
71+```toml
72+[[snippet]]
73+name = "strict mode"
74+group = "Shell"
75+languages = ["bash"]
76+body = "set -euo pipefail"
77+```
78+
79+Ce snippet n'apparaît alors que si un script shell est au premier plan. Omettez `languages` et le snippet est proposé partout, ce que vous voulez pour un en-tête de licence ou un `TODO`.
80+
81+Un groupe que le filtrage laisse vide n'apparaît pas du tout.
82+
83+## Variantes
84+
85+- **Vous avez lancé l'éditeur depuis un sous-dossier.** Le fichier du projet n'est pas trouvé : seul `./.turbo-go` est consulté, la même règle que `settings.toml`. Vos propres snippets apparaissent quand même.
86+- **Le fichier contient une erreur.** Le menu affiche un `Cannot read snippets` grisé à la place des groupes, et **Create snippets file** est toujours là. Ouvrez le fichier et corrigez-le.
87+- **Vous voulez des tabulations dans un corps.** Écrivez `\t`, comme le fichier de départ — le TOML le transforme en tabulation à la lecture.
88+
89+## Voir aussi
90+
91+- Toutes les clés du fichier et toutes les règles : [Référence des snippets](../reference/snippets.md)
92+- Pourquoi le menu est reconstruit à chaque ouverture, et pourquoi l'insertion réindente : [Snippets](../explanation/snippets.md)
93+- L'autre fichier de `.turbo-go` : [Réglages de projet](../reference/project-settings.md)
added docs/fr/how-to/write-a-theme.md +152 -0
new file mode 100644
@@ -0,0 +1,152 @@
1+# Écrire son propre thème
2+
3+Ce guide montre comment ajouter un thème de couleurs à vous. Il suppose que vous savez où se trouve votre répertoire de configuration et que vous savez éditer un fichier TOML.
4+
5+## 1. Trouver où vont les thèmes
6+
7+```bash
8+turbo-go -list-themes
9+```
10+
11+La dernière ligne indique le répertoire — `~/.config/turbo-go/themes` sous Linux, `~/Library/Application Support/turbo-go/themes` sous macOS. Créez-le :
12+
13+```bash
14+mkdir -p ~/.config/turbo-go/themes
15+```
16+
17+## 2. Partir d'un thème existant
18+
19+Le plus rapide est d'hériter d'un thème qui fonctionne déjà et de ne redéfinir que ce que vous voulez :
20+
21+```toml
22+# ~/.config/turbo-go/themes/mine.toml
23+name = "Le mien"
24+description = "Turbo Classic, mais avec des commentaires lisibles."
25+inherits = "turbo-classic"
26+
27+[colors]
28+"syntax.comment" = { fg = "#8a8a8a", italic = true }
29+"syntax.string" = { fg = "#87d7af" }
30+```
31+
32+Tout ce que vous ne définissez pas est repris de `turbo-classic`.
33+
34+**Héritez d'un thème dont le fond ressemble au vôtre.** Les couleurs que vous omettez ont été choisies contre le fond du thème dont vous héritez : un thème sombre bâti sur `turbo-classic` affichera, ici et là, une couleur pensée pour le marine Borland. Pour un thème sombre, héritez de `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` ou `monochrome-dark` ; pour un thème clair, de `borland-light`, `catppuccin-latte`, `intellij-light` ou `monochrome-light`. C'est aussi pourquoi les onze thèmes livrés dans le binaire énoncent chacun leur palette en entier au lieu d'en hériter l'essentiel — un test les y oblige, parce qu'un thème livré engage le projet.
35+
36+## 3. L'utiliser
37+
38+```bash
39+turbo-go -theme mine main.go
40+```
41+
42+Ou depuis l'éditeur : `Options ▸ Theme…`, qui liste tous les thèmes trouvés.
43+
44+## 4. Itérer
45+
46+Modifiez le fichier, puis relancez l'éditeur. Il n'y a pas de rechargement à chaud.
47+
48+Si le thème ne se charge pas, Turbo Go retombe sur le thème par défaut plutôt que de refuser de démarrer. Pour savoir *pourquoi* :
49+
50+```bash
51+turbo-go -list-themes
52+```
53+
54+Un thème cassé apparaît dans la liste avec l'erreur d'analyse à côté — un nom de couleur inconnu est une erreur, pas un repli silencieux : une faute de frappe est signalée au lieu de repeindre discrètement la moitié de l'écran.
55+
56+## 5. Vérifier qu'il reste lisible
57+
58+Le projet soumet chaque thème qu'il livre à cinq règles mesurées, et elles valent pour le vôtre. `make test` les exécute.
59+
60+| Règle | Pourquoi |
61+| --- | --- |
62+| Le curseur est à au moins 64 de la ligne qu'il occupe, dans son canal le plus fort | Le terminal dessine son curseur par-dessus la cellule ; un curseur qui se fond est introuvable |
63+| Le curseur n'est jamais une simple inversion de cette ligne | Un terminal qui dessine son curseur en inversant la cellule le rendrait invisible |
64+| La ligne courante est à au moins 16 de la page | `turbo-dark` a un jour utilisé dix, ce qui n'est pas un surlignage |
65+| Le texte destiné à la lecture est à au moins 64 de son fond | Le mobilier — bureau, ombre, gouttière d'ascenseur, entrée grisée — en est exempt : il est fait pour s'effacer |
66+| Les commentaires se lisent à 4,5:1 ou mieux sur leur fond, en luminance relative WCAG | Un commentaire est de la prose, lue mot à mot. `turbo-classic` les dessinait en `#808080` sur son bleu marine : 128 valeurs de canal d'écart, donc la règle ci-dessus laissait passer, et 4,05:1 à la lecture, sous le plancher du W3C pour du texte courant. Les commentaires étaient la couleur la plus terne dans six des onze thèmes livrés. |
67+
68+
69+La règle de contraste s'applique aux six thèmes que ce projet écrit lui-même. Les deux thèmes Catppuccin en sont exemptés, et l'exemption est écrite là où elle est faite : leurs couleurs sont la palette publiée de quelqu'un d'autre, copiée fidèlement, et Catppuccin place les commentaires à 2,87:1 en Frappé et 2,83:1 en Latte. Un thème nommé Catppuccin qui n'aurait pas exactement ces valeurs serait un autre thème portant un nom d'emprunt : le correctif, si quelqu'un en veut un, est en amont.
70+
71+Elle s'applique aux commentaires et à rien d'autre. `syntax.punctuation` est plus discret encore dans plusieurs thèmes et le reste : la ponctuation se reconnaît à sa forme, elle ne se lit pas.
72+
73+Une sixième règle attrape l'erreur qu'aucune mesure ne voit : **deux classes syntaxiques que le lecteur rencontre côte à côte ne doivent pas être dessinées à l'identique**. `turbo-classic` a un jour peint `syntax.link` du même vert que `syntax.string` : un lien Markdown et un extrait de code inline devenaient la même chose à l'écran — chaque couleur lisible, chaque clé définie, et les deux simplement égales. Les deux monochromes satisfont cette règle sans aucune teinte, en jouant du gras, de l'italique et du souligné.
74+
75+## Variantes
76+
77+**Remplacer un thème livré plutôt que d'en ajouter un.** Donnez à votre fichier le même nom — `turbo-classic.toml` — et c'est le vôtre qui gagne. Le thème embarqué n'est pas remplacé : supprimer votre fichier le fait revenir.
78+
79+**Partir de zéro.** N'écrivez pas `inherits`. Définissez au moins `default` ; toute clé non définie retombe le long des points jusqu'à lui, si bien qu'un thème d'une seule ligne reste un thème utilisable.
80+
81+**Ne colorer que la syntaxe.** Une seule clé suffit :
82+
83+```toml
84+[colors]
85+syntax = { fg = "silver" }
86+```
87+
88+`syntax.keyword`, `syntax.string` et les autres y retombent toutes.
89+
90+**Garder les couleurs du terminal.** Utilisez `default` comme valeur :
91+
92+```toml
93+[colors]
94+"editor.text" = { fg = "default", bg = "default" }
95+```
96+
97+**L'essayer depuis un clone sans l'installer.** Pointez l'éditeur sur n'importe quel répertoire :
98+
99+```bash
100+TURBO_GO_THEME_DIR=./mes-themes turbo-go -theme mine main.go
101+```
102+
103+**Le curseur est peu visible.** `editor.cursor` fait deux choses : son **fond** est envoyé au terminal comme couleur de son propre curseur, et il peint aussi la cellule en dessous, en secours pour les terminaux qui ignorent la première. Mettez-la sur quelque chose de criard :
104+
105+```toml
106+[colors]
107+"editor.cursor" = { fg = "#000000", bg = "#ff8700" }
108+```
109+
110+Deux choses font une mauvaise couleur de curseur, et la suite de tests les refuse toutes les deux : une simple inversion de la ligne — que les terminaux dessinant leur curseur par inversion ramènent à l'invisibilité — et tout écart de moins de 64 valeurs de canal avec la ligne sur laquelle il se trouve.
111+
112+**La ligne du curseur est difficile à repérer.** C'est `editor.currentline`, une autre clé. Elle doit s'écarter de `editor.text` d'au moins 16 valeurs de canal pour être un surlignage.
113+
114+**Le Markdown et le HTML rendent fade.** Cinq clés appartiennent aux langages de balisage et n'ont pas d'équivalent en Go : un thème écrit avant leur existence ne les définit pas.
115+
116+```toml
117+[colors]
118+"syntax.heading" = { fg = "white", bold = true }
119+"syntax.tag" = { fg = "aqua" }
120+"syntax.attribute" = { fg = "yellow" }
121+"syntax.emphasis" = { fg = "fuchsia", bold = true }
122+"syntax.link" = { fg = "aqua", underline = true }
123+```
124+
125+Donnez à `syntax.link` une couleur différente de `syntax.string` : un lien et un `code` en ligne se côtoient dans presque toute prose, et partager une couleur en fait une bouillie. Le `turbo-classic` livré avait exactement ce défaut jusqu'à ce qu'on le regarde sur un vrai terminal.
126+
127+**L'arbre du projet est plat.** Il a quatre clés à lui, dont aucune ne se rabat sur `list` :
128+
129+```toml
130+[colors]
131+"tree.text" = { fg = "silver", bg = "navy" }
132+"tree.directory" = { fg = "white", bg = "navy", bold = true }
133+"tree.selected" = { fg = "black", bg = "aqua" }
134+"tree.unfocused" = { fg = "black", bg = "gray" }
135+```
136+
137+Donnez à `tree.text` le même fond que `window.body`, pour que l'arbre fasse partie de sa fenêtre, et rendez `tree.selected` nettement différent — la suite de tests tient chaque thème livré à au moins 64 valeurs de canal entre les deux, parce qu'un surlignage de la couleur de la page n'est pas un surlignage.
138+
139+**Les fenêtres terminal rendent mal.** Elles ont deux clés à elles, et aucune ne se rabat sur `editor` :
140+
141+```toml
142+[colors]
143+"terminal.text" = { fg = "silver", bg = "black" }
144+"terminal.cursor" = { fg = "black", bg = "aqua" }
145+```
146+
147+`terminal.text` est ce que reçoit la sortie d'un shell lorsqu'elle ne nomme aucune couleur — donnez-lui quelque chose de proche d'un vrai terminal plutôt que du fond de votre éditeur, sans quoi `less` et `htop` détonneront. Un programme qui nomme ses couleurs les conserve dans les deux cas.
148+
149+## Voir aussi
150+
151+- Toutes les clés définissables et tous les noms de couleurs : [référence du format de thème](../reference/themes.md)
152+- Pourquoi du TOML avec deux formes d'héritage : [Décisions de conception](../explanation/design-decisions.md)
new file mode 100644
@@ -0,0 +1,152 @@
1+# Écrire son propre thème
2+
3+Ce guide montre comment ajouter un thème de couleurs à vous. Il suppose que vous savez où se trouve votre répertoire de configuration et que vous savez éditer un fichier TOML.
4+
5+## 1. Trouver où vont les thèmes
6+
7+```bash
8+turbo-go -list-themes
9+```
10+
11+La dernière ligne indique le répertoire — `~/.config/turbo-go/themes` sous Linux, `~/Library/Application Support/turbo-go/themes` sous macOS. Créez-le :
12+
13+```bash
14+mkdir -p ~/.config/turbo-go/themes
15+```
16+
17+## 2. Partir d'un thème existant
18+
19+Le plus rapide est d'hériter d'un thème qui fonctionne déjà et de ne redéfinir que ce que vous voulez :
20+
21+```toml
22+# ~/.config/turbo-go/themes/mine.toml
23+name = "Le mien"
24+description = "Turbo Classic, mais avec des commentaires lisibles."
25+inherits = "turbo-classic"
26+
27+[colors]
28+"syntax.comment" = { fg = "#8a8a8a", italic = true }
29+"syntax.string" = { fg = "#87d7af" }
30+```
31+
32+Tout ce que vous ne définissez pas est repris de `turbo-classic`.
33+
34+**Héritez d'un thème dont le fond ressemble au vôtre.** Les couleurs que vous omettez ont été choisies contre le fond du thème dont vous héritez : un thème sombre bâti sur `turbo-classic` affichera, ici et là, une couleur pensée pour le marine Borland. Pour un thème sombre, héritez de `turbo-dark`, `cappuccino`, `catppuccin-frappe`, `cobalt`, `darcula` ou `monochrome-dark` ; pour un thème clair, de `borland-light`, `catppuccin-latte`, `intellij-light` ou `monochrome-light`. C'est aussi pourquoi les onze thèmes livrés dans le binaire énoncent chacun leur palette en entier au lieu d'en hériter l'essentiel — un test les y oblige, parce qu'un thème livré engage le projet.
35+
36+## 3. L'utiliser
37+
38+```bash
39+turbo-go -theme mine main.go
40+```
41+
42+Ou depuis l'éditeur : `Options ▸ Theme…`, qui liste tous les thèmes trouvés.
43+
44+## 4. Itérer
45+
46+Modifiez le fichier, puis relancez l'éditeur. Il n'y a pas de rechargement à chaud.
47+
48+Si le thème ne se charge pas, Turbo Go retombe sur le thème par défaut plutôt que de refuser de démarrer. Pour savoir *pourquoi* :
49+
50+```bash
51+turbo-go -list-themes
52+```
53+
54+Un thème cassé apparaît dans la liste avec l'erreur d'analyse à côté — un nom de couleur inconnu est une erreur, pas un repli silencieux : une faute de frappe est signalée au lieu de repeindre discrètement la moitié de l'écran.
55+
56+## 5. Vérifier qu'il reste lisible
57+
58+Le projet soumet chaque thème qu'il livre à cinq règles mesurées, et elles valent pour le vôtre. `make test` les exécute.
59+
60+| Règle | Pourquoi |
61+| --- | --- |
62+| Le curseur est à au moins 64 de la ligne qu'il occupe, dans son canal le plus fort | Le terminal dessine son curseur par-dessus la cellule ; un curseur qui se fond est introuvable |
63+| Le curseur n'est jamais une simple inversion de cette ligne | Un terminal qui dessine son curseur en inversant la cellule le rendrait invisible |
64+| La ligne courante est à au moins 16 de la page | `turbo-dark` a un jour utilisé dix, ce qui n'est pas un surlignage |
65+| Le texte destiné à la lecture est à au moins 64 de son fond | Le mobilier — bureau, ombre, gouttière d'ascenseur, entrée grisée — en est exempt : il est fait pour s'effacer |
66+| Les commentaires se lisent à 4,5:1 ou mieux sur leur fond, en luminance relative WCAG | Un commentaire est de la prose, lue mot à mot. `turbo-classic` les dessinait en `#808080` sur son bleu marine : 128 valeurs de canal d'écart, donc la règle ci-dessus laissait passer, et 4,05:1 à la lecture, sous le plancher du W3C pour du texte courant. Les commentaires étaient la couleur la plus terne dans six des onze thèmes livrés. |
67+
68+
69+La règle de contraste s'applique aux six thèmes que ce projet écrit lui-même. Les deux thèmes Catppuccin en sont exemptés, et l'exemption est écrite là où elle est faite : leurs couleurs sont la palette publiée de quelqu'un d'autre, copiée fidèlement, et Catppuccin place les commentaires à 2,87:1 en Frappé et 2,83:1 en Latte. Un thème nommé Catppuccin qui n'aurait pas exactement ces valeurs serait un autre thème portant un nom d'emprunt : le correctif, si quelqu'un en veut un, est en amont.
70+
71+Elle s'applique aux commentaires et à rien d'autre. `syntax.punctuation` est plus discret encore dans plusieurs thèmes et le reste : la ponctuation se reconnaît à sa forme, elle ne se lit pas.
72+
73+Une sixième règle attrape l'erreur qu'aucune mesure ne voit : **deux classes syntaxiques que le lecteur rencontre côte à côte ne doivent pas être dessinées à l'identique**. `turbo-classic` a un jour peint `syntax.link` du même vert que `syntax.string` : un lien Markdown et un extrait de code inline devenaient la même chose à l'écran — chaque couleur lisible, chaque clé définie, et les deux simplement égales. Les deux monochromes satisfont cette règle sans aucune teinte, en jouant du gras, de l'italique et du souligné.
74+
75+## Variantes
76+
77+**Remplacer un thème livré plutôt que d'en ajouter un.** Donnez à votre fichier le même nom — `turbo-classic.toml` — et c'est le vôtre qui gagne. Le thème embarqué n'est pas remplacé : supprimer votre fichier le fait revenir.
78+
79+**Partir de zéro.** N'écrivez pas `inherits`. Définissez au moins `default` ; toute clé non définie retombe le long des points jusqu'à lui, si bien qu'un thème d'une seule ligne reste un thème utilisable.
80+
81+**Ne colorer que la syntaxe.** Une seule clé suffit :
82+
83+```toml
84+[colors]
85+syntax = { fg = "silver" }
86+```
87+
88+`syntax.keyword`, `syntax.string` et les autres y retombent toutes.
89+
90+**Garder les couleurs du terminal.** Utilisez `default` comme valeur :
91+
92+```toml
93+[colors]
94+"editor.text" = { fg = "default", bg = "default" }
95+```
96+
97+**L'essayer depuis un clone sans l'installer.** Pointez l'éditeur sur n'importe quel répertoire :
98+
99+```bash
100+TURBO_GO_THEME_DIR=./mes-themes turbo-go -theme mine main.go
101+```
102+
103+**Le curseur est peu visible.** `editor.cursor` fait deux choses : son **fond** est envoyé au terminal comme couleur de son propre curseur, et il peint aussi la cellule en dessous, en secours pour les terminaux qui ignorent la première. Mettez-la sur quelque chose de criard :
104+
105+```toml
106+[colors]
107+"editor.cursor" = { fg = "#000000", bg = "#ff8700" }
108+```
109+
110+Deux choses font une mauvaise couleur de curseur, et la suite de tests les refuse toutes les deux : une simple inversion de la ligne — que les terminaux dessinant leur curseur par inversion ramènent à l'invisibilité — et tout écart de moins de 64 valeurs de canal avec la ligne sur laquelle il se trouve.
111+
112+**La ligne du curseur est difficile à repérer.** C'est `editor.currentline`, une autre clé. Elle doit s'écarter de `editor.text` d'au moins 16 valeurs de canal pour être un surlignage.
113+
114+**Le Markdown et le HTML rendent fade.** Cinq clés appartiennent aux langages de balisage et n'ont pas d'équivalent en Go : un thème écrit avant leur existence ne les définit pas.
115+
116+```toml
117+[colors]
118+"syntax.heading" = { fg = "white", bold = true }
119+"syntax.tag" = { fg = "aqua" }
120+"syntax.attribute" = { fg = "yellow" }
121+"syntax.emphasis" = { fg = "fuchsia", bold = true }
122+"syntax.link" = { fg = "aqua", underline = true }
123+```
124+
125+Donnez à `syntax.link` une couleur différente de `syntax.string` : un lien et un `code` en ligne se côtoient dans presque toute prose, et partager une couleur en fait une bouillie. Le `turbo-classic` livré avait exactement ce défaut jusqu'à ce qu'on le regarde sur un vrai terminal.
126+
127+**L'arbre du projet est plat.** Il a quatre clés à lui, dont aucune ne se rabat sur `list` :
128+
129+```toml
130+[colors]
131+"tree.text" = { fg = "silver", bg = "navy" }
132+"tree.directory" = { fg = "white", bg = "navy", bold = true }
133+"tree.selected" = { fg = "black", bg = "aqua" }
134+"tree.unfocused" = { fg = "black", bg = "gray" }
135+```
136+
137+Donnez à `tree.text` le même fond que `window.body`, pour que l'arbre fasse partie de sa fenêtre, et rendez `tree.selected` nettement différent — la suite de tests tient chaque thème livré à au moins 64 valeurs de canal entre les deux, parce qu'un surlignage de la couleur de la page n'est pas un surlignage.
138+
139+**Les fenêtres terminal rendent mal.** Elles ont deux clés à elles, et aucune ne se rabat sur `editor` :
140+
141+```toml
142+[colors]
143+"terminal.text" = { fg = "silver", bg = "black" }
144+"terminal.cursor" = { fg = "black", bg = "aqua" }
145+```
146+
147+`terminal.text` est ce que reçoit la sortie d'un shell lorsqu'elle ne nomme aucune couleur — donnez-lui quelque chose de proche d'un vrai terminal plutôt que du fond de votre éditeur, sans quoi `less` et `htop` détonneront. Un programme qui nomme ses couleurs les conserve dans les deux cas.
148+
149+## Voir aussi
150+
151+- Toutes les clés définissables et tous les noms de couleurs : [référence du format de thème](../reference/themes.md)
152+- Pourquoi du TOML avec deux formes d'héritage : [Décisions de conception](../explanation/design-decisions.md)
added docs/fr/reference/acp.md +239 -0
new file mode 100644
@@ -0,0 +1,239 @@
1+# Agents et ACP
2+
3+Turbo Go est un client de l'[Agent Client Protocol](https://agentclientprotocol.com). Il lance chaque agent comme processus fils et échange avec lui des messages JSON-RPC 2.0 sur son entrée et sa sortie standard, à raison d'un message par ligne.
4+
5+## Où se trouve le fichier
6+
7+| Chemin | Lu | Rôle |
8+| --- | --- | --- |
9+| `~/.config/turbo-go/acp.toml` | en premier | Les agents que vous voulez dans tous les projets |
10+| `<projet>/.turbo-go/acp.toml` | en second | Les agents propres à ce projet |
11+
12+Les deux sont facultatifs. Quand un `name` d'agent apparaît dans les deux, celui du projet remplace celui de l'utilisateur, étant l'énoncé le plus spécifique — la règle que suivent déjà les [snippets](snippets.md). Un fichier absent n'est pas une erreur ; un fichier présent mais illisible en est une, signalée sous **Agent ▸ Agent status** plutôt que de laisser silencieusement le menu vide.
13+
14+`TURBO_GO_DIR` remplace le dossier où le fichier utilisateur est cherché. Le fichier du projet est toujours `.turbo-go/acp.toml` sous le dossier depuis lequel l'éditeur a été lancé — il n'y a pas de remontée dans l'arborescence, pour la même raison que les [réglages de projet](project-settings.md) ne remontent pas.
15+
16+## Format du fichier
17+
18+Un bloc `[[agent]]` par agent, dans l'ordre souhaité dans le menu.
19+
20+```toml
21+[[agent]]
22+name = "Bob (llama.cpp)"
23+command = "docker"
24+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
25+env = { TELEMETRY_ENABLED = "false" }
26+cwd = "."
27+```
28+
29+| Clé | Type | Requise | Signification |
30+| --- | --- | --- | --- |
31+| `name` | chaîne | **oui** | Ce qu'affiche le menu Agent et le titre de la fenêtre. Doit être unique dans l'ensemble fusionné. |
32+| `command` | chaîne | **oui** | L'exécutable à lancer. Cherché dans `PATH` sauf s'il contient un séparateur. |
33+| `args` | liste de chaînes | non | Ses arguments, passés tels quels — pas de shell, donc ni guillemets, ni jokers, ni `&&`. |
34+| `env` | table de chaînes | non | Variables d'environnement ajoutées à celles de l'éditeur. Un nom donné ici l'emporte. |
35+| `cwd` | chaîne | non | Où le processus démarre, et le `cwd` annoncé à l'agent. Relatif à la racine du projet. Par défaut, la racine du projet. |
36+
37+`env` peut aussi s'écrire en sous-table, ce qui est la même chose :
38+
39+```toml
40+[[agent]]
41+name = "Bob (llama.cpp)"
42+command = "docker"
43+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
44+
45+[agent.env]
46+TELEMETRY_ENABLED = "false"
47+```
48+
49+### Ce qui est refusé
50+
51+Le fichier est refusé dans son ensemble, plutôt que chargé à moitié, dès que l'un de ces cas se présente. Un menu à moitié chargé proposant trois de vos cinq agents est pire qu'une erreur qui dit pourquoi.
52+
53+| Problème | Message |
54+| --- | --- |
55+| un agent sans `name` | `reading …/acp.toml: agent 1 has no name` |
56+| un agent sans `command` | `reading …/acp.toml: agent "Bob" has no command` |
57+| deux agents portant le même `name` | `reading …/acp.toml: two agents are called "Bob"` |
58+| une clé que le format ne définit pas | `reading …/acp.toml: agent.comand is not a key this file has` |
59+
60+Le dernier cas est voulu : une clé mal orthographiée silencieusement ignorée ressemblerait exactement à une clé sans effet.
61+
62+## Le menu Agent
63+
64+`Alt-A` l'ouvre. Il est sur la barre qu'un agent soit configuré ou non, parce que c'est de là que **Create agents file** doit être atteignable.
65+
66+| Entrée | Active quand | Effet |
67+| --- | --- | --- |
68+| *une entrée par agent, par son nom* | toujours | Démarrer cet agent et ouvrir une fenêtre dessus |
69+| **Create agents file** | pas d'`acp.toml` dans le projet | Écrire le fichier de départ et l'ouvrir |
70+| **Cancel turn** | un tour est en cours dans la fenêtre de devant | `session/cancel` |
71+| **Agent status** | toujours | Ce qui a été chargé, la ligne de commande de chacun, et ce qui a échoué |
72+
73+## Touches dans une fenêtre agent
74+
75+Une fenêtre agent est une fenêtre ordinaire : `F6`, `Alt-1``Alt-9`, Tile, Maximise, `[x]` et `[■]` y fonctionnent tous. À l'intérieur :
76+
77+| Touche | Effet |
78+| --- | --- |
79+| `Entrée` | Envoyer la zone de saisie comme invite |
80+| `Alt-Entrée` | Insérer un saut de ligne dans la zone de saisie |
81+| `Tab` | Déplacer le focus entre la conversation et la zone de saisie |
82+| `Ctrl-C`, `Ctrl-Ins` | Copier la sélection, ou le bloc sur lequel est le curseur |
83+| `Échap` | Abandonner la sélection ; s'il n'y en a pas, annuler le tour en cours |
84+| `Ctrl-W` | Fermer la fenêtre et arrêter l'agent |
85+
86+Avec la **zone de saisie** au premier plan :
87+
88+| Touche | Effet |
89+| --- | --- |
90+| `↑` `↓` `←` `→` `Début` `Fin` | Déplacer le curseur dans ce que vous tapez |
91+| `Retour arrière` `Suppr` | L'éditer ; le retour arrière en début de ligne la joint à celle du dessus |
92+| `/` en premier caractère | Ouvrir la liste des commandes de l'agent — voir [Commandes et mentions](#commandes-et-mentions) |
93+| `@` | Ouvrir la liste des fichiers du projet, réduite par ce que vous tapez ensuite |
94+| `↑` `↓` `PgUp` `PgDn`, liste ouverte | Se déplacer dans la liste |
95+| `Tab`, liste ouverte | Prendre l'entrée en surbrillance |
96+| `Entrée`, liste ouverte | Prendre l'entrée en surbrillance ; sur un mot déjà complet, envoyer |
97+| `Échap`, liste ouverte | Fermer la liste jusqu'à ce que le texte change |
98+
99+Avec la **conversation** au premier plan :
100+
101+| Touche | Effet |
102+| --- | --- |
103+| `↑` `↓` | Déplacer le curseur d'une ligne |
104+| `PgUp` `PgDn` | Le déplacer d'un écran |
105+| `Début` `Fin` | Le début de la conversation, et la fin |
106+| `Shift-` l'une d'elles | Étendre la sélection à la place |
107+| Glisser avec le bouton 1 | Sélectionner à la main |
108+| Molette | Défiler de trois lignes sans bouger le curseur |
109+
110+Contrairement à une fenêtre terminal, une fenêtre agent ne **prend pas** les raccourcis de l'éditeur : il n'y a pas de shell qui ait besoin de `Ctrl-F`, donc cette touche garde son sens habituel. `Ctrl-C` fait exception, et seulement parce que rien d'autre n'en veut dans une fenêtre agent.
111+
112+## Commandes et mentions
113+
114+Deux caractères ouvrent une liste par-dessus le bas de la conversation pendant que vous tapez. Ce sont les deux mêmes que Zed, si bien que la documentation d'un agent — « tapez `/web` pour chercher » — reste vraie ici.
115+
116+### `/` — les commandes de l'agent
117+
118+Un agent peut annoncer des commandes par `available_commands_update`, au début de la session ou à tout moment pendant celle-ci. Taper `/` comme **premier caractère** de la zone de saisie les liste : le nom, la description donnée par l'agent et, entre chevrons, ce qu'il attend après le nom quand il attend quelque chose. Continuez à taper pour réduire la liste ; la correspondance porte sur le début du nom et ignore la casse.
119+
120+`Tab` complète la commande en surbrillance. Une commande qui prend une entrée est complétée avec une espace à la fin, pour que la suite de votre frappe soit son argument ; une qui n'en prend pas est complétée au nom seul. `Entrée` complète aussi, sauf sur un mot qui se lit déjà exactement comme une commande, où elle envoie.
121+
122+Sur le fil, une commande est du **texte** : `/web agent client protocol` part comme un seul bloc texte, et l'agent la reconnaît à son premier mot. C'est tout le protocole des commandes, et c'est pourquoi un `/` ailleurs qu'au début de la zone n'est qu'un caractère.
123+
124+Sans commande annoncée, `/` est un caractère et `Tab` garde son sens habituel. **Agent ▸ Agent status** liste les commandes avec leur description.
125+
126+### `@` — un fichier du projet
127+
128+Taper `@` n'importe où dans la zone liste les fichiers du projet, relatifs à sa racine, avec des barres obliques. Ce que vous tapez après le `@` réduit la liste : les fichiers dont le nom propre commence par cela viennent d'abord, puis ceux dont le chemin le contient seulement. `Tab` ou `Entrée` complète celui en surbrillance et ajoute une espace.
129+
130+À l'envoi de l'invite, chaque `@nom` qui désigne un fichier connu de la liste devient un bloc de contenu **à la place du nom** :
131+
132+| L'agent a déclaré | Le bloc envoyé |
133+| --- | --- |
134+| `promptCapabilities.embeddedContext: true` | `resource` — l'`uri` du fichier, son `mimeType` et son `text` entier, lu comme `fs/read_text_file` le lit : depuis le tampon ouvert quand le fichier est ouvert et modifié |
135+| autre chose, ou le fichier n'a pas pu être lu | `resource_link` — l'`uri`, le `name` et le `mimeType`, pour que l'agent aille le chercher lui-même |
136+
137+Les mots de part et d'autre partent en blocs texte, si bien que `explique @docs/README.md s'il te plaît` fait trois blocs : `explique `, le fichier, ` s'il te plaît`. La conversation garde la ligne telle que vous l'avez tapée.
138+
139+Un mot qui commence par `@` et ne désigne aucun fichier reste du texte — une adresse électronique dans une invite n'est pas un fichier — et `@main.go` ne désigne pas `main.gopher` : le nom doit terminer le mot.
140+
141+La liste est le projet parcouru depuis sa racine, `.git` exclu, au plus 5 000 fichiers, et au plus 200 d'entre eux affichés à la fois. Au-delà de l'une ou l'autre limite, tapez une lettre de plus. Le parcours est refait à chaque ouverture de la liste par `@`, si bien qu'un fichier que l'agent vient de créer y figure.
142+
143+## La copie
144+
145+La sélection porte sur des **lignes entières**. Rien ne s'édite dans une conversation, donc une demi-ligne n'est jamais ce qu'on veut dire, et des lignes entières préservent l'indentation d'un bloc de code copié.
146+
147+Sans rien de sélectionné, la copie prend la **région sur laquelle est le curseur** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et forment des régions à part : ni l'un ni l'autre n'est jamais copié avec ce qu'il surmonte.
148+
149+L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge.
150+
151+Le texte part à deux endroits à la fois :
152+
153+| Presse-papiers | Comment | Collé avec |
154+| --- | --- | --- |
155+| Celui de l'éditeur | directement | `Shift-Ins`, dans un fichier ouvert ici |
156+| Celui du système | OSC 52, à travers le terminal | `Ctrl-V`, n'importe où ailleurs |
157+
158+Rien ne vérifie que le terminal a accepté le second : il n'y a pas de réponse à vérifier, et un terminal peut refuser OSC 52 par sécurité ou demander qu'on l'active. Le presse-papiers de l'éditeur contient le texte dans tous les cas, et la barre d'état dit combien de lignes ont été copiées.
159+
160+## Quelle part du protocole est implémentée
161+
162+Version de protocole **1**. Turbo Go annonce sa version dans `initialize` et accepte la version que l'agent répond, pourvu qu'il la connaisse.
163+
164+### Ce que l'éditeur appelle sur l'agent
165+
166+| Méthode | Implémentée | Remarques |
167+| --- | --- | --- |
168+| `initialize` | oui | Annonce la capacité `fs` ci-dessous ; `terminal` n'est pas annoncée |
169+| `session/new` | oui | `cwd` vient de la clé `cwd` de l'agent ; `mcpServers` est toujours vide — les serveurs MCP sont l'affaire de l'agent |
170+| `session/prompt` | oui | Des blocs texte, et un bloc `resource` ou `resource_link` par fichier désigné par `@` — voir [Commandes et mentions](#commandes-et-mentions) |
171+| `session/cancel` | oui | `Échap`, et **Agent ▸ Cancel turn** |
172+| `session/load` | **non** | Les conversations ne survivent pas à la fermeture de la fenêtre |
173+| `authenticate` | **non** | Un agent qui liste des `authMethods` est signalé comme exigeant une connexion que l'éditeur ne sait pas faire |
174+
175+### Ce que l'agent peut appeler sur l'éditeur
176+
177+| Méthode | Implémentée | Remarques |
178+| --- | --- | --- |
179+| `session/update` | oui | Voir la table ci-dessous |
180+| `session/request_permission` | oui | Une boîte modale portant les options de l'agent lui-même |
181+| `fs/read_text_file` | oui | Depuis le tampon quand le fichier est ouvert et modifié, sinon depuis le disque |
182+| `fs/write_text_file` | oui | Dans le tampon quand le fichier est ouvert, sinon sur le disque |
183+| `terminal/*` | **non** | Non annoncée, donc un agent conforme ne la demandera pas |
184+
185+### Mises à jour de session
186+
187+| `sessionUpdate` | Affiché comme |
188+| --- | --- |
189+| `agent_message_chunk` | La réponse de l'agent, ajoutée au fil de son arrivée |
190+| `agent_thought_chunk` | La même chose, dans la couleur des commentaires, sous une étiquette *réflexion* |
191+| `user_message_chunk` | Votre propre message, tel que l'agent le renvoie |
192+| `tool_call` | Une ligne nommant l'outil et son titre, avec son état |
193+| `tool_call_update` | Repliée sur la ligne dont le `toolCallId` correspond, avec sa sortie |
194+| `plan` | Les entrées en liste, chacune avec son état |
195+| `available_commands_update` | La liste que `/` ouvre dans la zone de saisie ; aussi listée, avec les descriptions, par **Agent ▸ Agent status** |
196+| `usage_update` | Le compte de jetons dans la barre d'état quand la fenêtre est devant |
197+| tout le reste | Ignoré, et compté ; le compte figure dans **Agent status** |
198+
199+Pendant qu'un tour est en cours, la règle entre les deux zones fait tourner un indicateur. Il est dessiné à partir de l'horloge et non d'un compteur, donc deux fenêtres qui réfléchissent en même temps tournent en phase et rien n'a besoin d'être remis à zéro au début d'un tour. Le *titre* de la fenêtre, lui, n'est délibérément pas animé : c'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui change huit fois par seconde les fait scintiller tous les deux.
200+
201+Une mise à jour inconnue est ignorée plutôt que refusée : le protocole grandit, et un éditeur qui cesserait de parler à un agent parce que celui-ci a appris un nouveau type de message aurait tort plus souvent que raison.
202+
203+## Coloration
204+
205+La conversation est dessinée avec des clés que tous les thèmes définissent déjà, donc aucun n'a eu besoin d'être touché :
206+
207+| Élément | Classe |
208+| --- | --- |
209+| Le nom d'un interlocuteur | `syntax.keyword` |
210+| Une réflexion | `syntax.comment` |
211+| Un appel d'outil et son état | `syntax.type` |
212+| Un appel d'outil en échec, et les avis de l'éditeur | `diagnostic.error` |
213+| Une ligne sélectionnée, et la barre du curseur | `editor.selection` |
214+| Le code dans un bloc délimité | l'analyseur du langage annoncé |
215+| Tout le reste | le texte ordinaire de la fenêtre |
216+
217+Un bloc délimité annonçant un langage que l'éditeur colore — `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — est coloré par cet analyseur. Un bloc annonçant autre chose, ou rien, est laissé brut.
218+
219+## Tracer la conversation avec un agent
220+
221+| Variable | Effet |
222+| --- | --- |
223+| `TURBO_ACP_TRACE=<fichier>` | Ajouter à ce fichier chaque message vers et depuis chaque agent, un par ligne, horodaté et marqué `->` (envoyé) ou `<-` (reçu) |
224+
225+C'est pour la seule question à laquelle l'écran ne peut pas répondre — *qu'a réellement envoyé l'agent ?* Une mise à jour que cet éditeur ne sait pas décoder est comptée dans **Agent ▸ Agent status**, qui nomme aussi la dernière et son erreur ; la trace montre le message lui-même. Un fichier qui ne peut pas être ouvert veut dire pas de trace, et rien d'autre : la trace n'a jamais le droit de casser l'éditeur.
226+
227+## Limites
228+
229+- **Une session par fenêtre.** Fermer la fenêtre termine la session ; il n'y a pas de reprise.
230+- **Texte et fichiers seulement.** L'éditeur envoie du texte, et les fichiers que vous désignez par `@` ; ni images ni audio, quoi que disent les `promptCapabilities` de l'agent.
231+- **Pas d'authentification.** Un agent exigeant une connexion doit être connecté par sa propre CLI avant que l'éditeur ne le lance.
232+- **`args` n'est pas une commande shell.** `command = "sh"`, `args = ["-c", "…"]` est la façon délibérée d'en obtenir une.
233+- **Une entrée est plafonnée** à un mégaoctet de texte. Un agent qui déverse tout un journal de compilation ne peut pas rendre la fenêtre inutilisable ; ce qui a été perdu est signalé dans l'entrée elle-même.
234+
235+## Voir aussi
236+
237+- La tâche : [Dialoguer avec un agent de code depuis l'éditeur](../how-to/talk-to-an-agent.md)
238+- Le raisonnement : [Fenêtres agent](../explanation/agent-windows.md)
239+- Le protocole : [agentclientprotocol.com](https://agentclientprotocol.com)
new file mode 100644
@@ -0,0 +1,239 @@
1+# Agents et ACP
2+
3+Turbo Go est un client de l'[Agent Client Protocol](https://agentclientprotocol.com). Il lance chaque agent comme processus fils et échange avec lui des messages JSON-RPC 2.0 sur son entrée et sa sortie standard, à raison d'un message par ligne.
4+
5+## Où se trouve le fichier
6+
7+| Chemin | Lu | Rôle |
8+| --- | --- | --- |
9+| `~/.config/turbo-go/acp.toml` | en premier | Les agents que vous voulez dans tous les projets |
10+| `<projet>/.turbo-go/acp.toml` | en second | Les agents propres à ce projet |
11+
12+Les deux sont facultatifs. Quand un `name` d'agent apparaît dans les deux, celui du projet remplace celui de l'utilisateur, étant l'énoncé le plus spécifique — la règle que suivent déjà les [snippets](snippets.md). Un fichier absent n'est pas une erreur ; un fichier présent mais illisible en est une, signalée sous **Agent ▸ Agent status** plutôt que de laisser silencieusement le menu vide.
13+
14+`TURBO_GO_DIR` remplace le dossier où le fichier utilisateur est cherché. Le fichier du projet est toujours `.turbo-go/acp.toml` sous le dossier depuis lequel l'éditeur a été lancé — il n'y a pas de remontée dans l'arborescence, pour la même raison que les [réglages de projet](project-settings.md) ne remontent pas.
15+
16+## Format du fichier
17+
18+Un bloc `[[agent]]` par agent, dans l'ordre souhaité dans le menu.
19+
20+```toml
21+[[agent]]
22+name = "Bob (llama.cpp)"
23+command = "docker"
24+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
25+env = { TELEMETRY_ENABLED = "false" }
26+cwd = "."
27+```
28+
29+| Clé | Type | Requise | Signification |
30+| --- | --- | --- | --- |
31+| `name` | chaîne | **oui** | Ce qu'affiche le menu Agent et le titre de la fenêtre. Doit être unique dans l'ensemble fusionné. |
32+| `command` | chaîne | **oui** | L'exécutable à lancer. Cherché dans `PATH` sauf s'il contient un séparateur. |
33+| `args` | liste de chaînes | non | Ses arguments, passés tels quels — pas de shell, donc ni guillemets, ni jokers, ni `&&`. |
34+| `env` | table de chaînes | non | Variables d'environnement ajoutées à celles de l'éditeur. Un nom donné ici l'emporte. |
35+| `cwd` | chaîne | non | Où le processus démarre, et le `cwd` annoncé à l'agent. Relatif à la racine du projet. Par défaut, la racine du projet. |
36+
37+`env` peut aussi s'écrire en sous-table, ce qui est la même chose :
38+
39+```toml
40+[[agent]]
41+name = "Bob (llama.cpp)"
42+command = "docker"
43+args = ["agent", "serve", "acp", ".turbo-go/agent.yaml"]
44+
45+[agent.env]
46+TELEMETRY_ENABLED = "false"
47+```
48+
49+### Ce qui est refusé
50+
51+Le fichier est refusé dans son ensemble, plutôt que chargé à moitié, dès que l'un de ces cas se présente. Un menu à moitié chargé proposant trois de vos cinq agents est pire qu'une erreur qui dit pourquoi.
52+
53+| Problème | Message |
54+| --- | --- |
55+| un agent sans `name` | `reading …/acp.toml: agent 1 has no name` |
56+| un agent sans `command` | `reading …/acp.toml: agent "Bob" has no command` |
57+| deux agents portant le même `name` | `reading …/acp.toml: two agents are called "Bob"` |
58+| une clé que le format ne définit pas | `reading …/acp.toml: agent.comand is not a key this file has` |
59+
60+Le dernier cas est voulu : une clé mal orthographiée silencieusement ignorée ressemblerait exactement à une clé sans effet.
61+
62+## Le menu Agent
63+
64+`Alt-A` l'ouvre. Il est sur la barre qu'un agent soit configuré ou non, parce que c'est de là que **Create agents file** doit être atteignable.
65+
66+| Entrée | Active quand | Effet |
67+| --- | --- | --- |
68+| *une entrée par agent, par son nom* | toujours | Démarrer cet agent et ouvrir une fenêtre dessus |
69+| **Create agents file** | pas d'`acp.toml` dans le projet | Écrire le fichier de départ et l'ouvrir |
70+| **Cancel turn** | un tour est en cours dans la fenêtre de devant | `session/cancel` |
71+| **Agent status** | toujours | Ce qui a été chargé, la ligne de commande de chacun, et ce qui a échoué |
72+
73+## Touches dans une fenêtre agent
74+
75+Une fenêtre agent est une fenêtre ordinaire : `F6`, `Alt-1``Alt-9`, Tile, Maximise, `[x]` et `[■]` y fonctionnent tous. À l'intérieur :
76+
77+| Touche | Effet |
78+| --- | --- |
79+| `Entrée` | Envoyer la zone de saisie comme invite |
80+| `Alt-Entrée` | Insérer un saut de ligne dans la zone de saisie |
81+| `Tab` | Déplacer le focus entre la conversation et la zone de saisie |
82+| `Ctrl-C`, `Ctrl-Ins` | Copier la sélection, ou le bloc sur lequel est le curseur |
83+| `Échap` | Abandonner la sélection ; s'il n'y en a pas, annuler le tour en cours |
84+| `Ctrl-W` | Fermer la fenêtre et arrêter l'agent |
85+
86+Avec la **zone de saisie** au premier plan :
87+
88+| Touche | Effet |
89+| --- | --- |
90+| `↑` `↓` `←` `→` `Début` `Fin` | Déplacer le curseur dans ce que vous tapez |
91+| `Retour arrière` `Suppr` | L'éditer ; le retour arrière en début de ligne la joint à celle du dessus |
92+| `/` en premier caractère | Ouvrir la liste des commandes de l'agent — voir [Commandes et mentions](#commandes-et-mentions) |
93+| `@` | Ouvrir la liste des fichiers du projet, réduite par ce que vous tapez ensuite |
94+| `↑` `↓` `PgUp` `PgDn`, liste ouverte | Se déplacer dans la liste |
95+| `Tab`, liste ouverte | Prendre l'entrée en surbrillance |
96+| `Entrée`, liste ouverte | Prendre l'entrée en surbrillance ; sur un mot déjà complet, envoyer |
97+| `Échap`, liste ouverte | Fermer la liste jusqu'à ce que le texte change |
98+
99+Avec la **conversation** au premier plan :
100+
101+| Touche | Effet |
102+| --- | --- |
103+| `↑` `↓` | Déplacer le curseur d'une ligne |
104+| `PgUp` `PgDn` | Le déplacer d'un écran |
105+| `Début` `Fin` | Le début de la conversation, et la fin |
106+| `Shift-` l'une d'elles | Étendre la sélection à la place |
107+| Glisser avec le bouton 1 | Sélectionner à la main |
108+| Molette | Défiler de trois lignes sans bouger le curseur |
109+
110+Contrairement à une fenêtre terminal, une fenêtre agent ne **prend pas** les raccourcis de l'éditeur : il n'y a pas de shell qui ait besoin de `Ctrl-F`, donc cette touche garde son sens habituel. `Ctrl-C` fait exception, et seulement parce que rien d'autre n'en veut dans une fenêtre agent.
111+
112+## Commandes et mentions
113+
114+Deux caractères ouvrent une liste par-dessus le bas de la conversation pendant que vous tapez. Ce sont les deux mêmes que Zed, si bien que la documentation d'un agent — « tapez `/web` pour chercher » — reste vraie ici.
115+
116+### `/` — les commandes de l'agent
117+
118+Un agent peut annoncer des commandes par `available_commands_update`, au début de la session ou à tout moment pendant celle-ci. Taper `/` comme **premier caractère** de la zone de saisie les liste : le nom, la description donnée par l'agent et, entre chevrons, ce qu'il attend après le nom quand il attend quelque chose. Continuez à taper pour réduire la liste ; la correspondance porte sur le début du nom et ignore la casse.
119+
120+`Tab` complète la commande en surbrillance. Une commande qui prend une entrée est complétée avec une espace à la fin, pour que la suite de votre frappe soit son argument ; une qui n'en prend pas est complétée au nom seul. `Entrée` complète aussi, sauf sur un mot qui se lit déjà exactement comme une commande, où elle envoie.
121+
122+Sur le fil, une commande est du **texte** : `/web agent client protocol` part comme un seul bloc texte, et l'agent la reconnaît à son premier mot. C'est tout le protocole des commandes, et c'est pourquoi un `/` ailleurs qu'au début de la zone n'est qu'un caractère.
123+
124+Sans commande annoncée, `/` est un caractère et `Tab` garde son sens habituel. **Agent ▸ Agent status** liste les commandes avec leur description.
125+
126+### `@` — un fichier du projet
127+
128+Taper `@` n'importe où dans la zone liste les fichiers du projet, relatifs à sa racine, avec des barres obliques. Ce que vous tapez après le `@` réduit la liste : les fichiers dont le nom propre commence par cela viennent d'abord, puis ceux dont le chemin le contient seulement. `Tab` ou `Entrée` complète celui en surbrillance et ajoute une espace.
129+
130+À l'envoi de l'invite, chaque `@nom` qui désigne un fichier connu de la liste devient un bloc de contenu **à la place du nom** :
131+
132+| L'agent a déclaré | Le bloc envoyé |
133+| --- | --- |
134+| `promptCapabilities.embeddedContext: true` | `resource` — l'`uri` du fichier, son `mimeType` et son `text` entier, lu comme `fs/read_text_file` le lit : depuis le tampon ouvert quand le fichier est ouvert et modifié |
135+| autre chose, ou le fichier n'a pas pu être lu | `resource_link` — l'`uri`, le `name` et le `mimeType`, pour que l'agent aille le chercher lui-même |
136+
137+Les mots de part et d'autre partent en blocs texte, si bien que `explique @docs/README.md s'il te plaît` fait trois blocs : `explique `, le fichier, ` s'il te plaît`. La conversation garde la ligne telle que vous l'avez tapée.
138+
139+Un mot qui commence par `@` et ne désigne aucun fichier reste du texte — une adresse électronique dans une invite n'est pas un fichier — et `@main.go` ne désigne pas `main.gopher` : le nom doit terminer le mot.
140+
141+La liste est le projet parcouru depuis sa racine, `.git` exclu, au plus 5 000 fichiers, et au plus 200 d'entre eux affichés à la fois. Au-delà de l'une ou l'autre limite, tapez une lettre de plus. Le parcours est refait à chaque ouverture de la liste par `@`, si bien qu'un fichier que l'agent vient de créer y figure.
142+
143+## La copie
144+
145+La sélection porte sur des **lignes entières**. Rien ne s'édite dans une conversation, donc une demi-ligne n'est jamais ce qu'on veut dire, et des lignes entières préservent l'indentation d'un bloc de code copié.
146+
147+Sans rien de sélectionné, la copie prend la **région sur laquelle est le curseur** : un bloc de code délimité, un passage de prose, la sortie d'un appel d'outil. Le libellé d'un interlocuteur et l'en-tête d'un appel d'outil sont du mobilier et forment des régions à part : ni l'un ni l'autre n'est jamais copié avec ce qu'il surmonte.
148+
149+L'indentation d'affichage de la conversation est retirée, donc le code collé arrive collé à la marge.
150+
151+Le texte part à deux endroits à la fois :
152+
153+| Presse-papiers | Comment | Collé avec |
154+| --- | --- | --- |
155+| Celui de l'éditeur | directement | `Shift-Ins`, dans un fichier ouvert ici |
156+| Celui du système | OSC 52, à travers le terminal | `Ctrl-V`, n'importe où ailleurs |
157+
158+Rien ne vérifie que le terminal a accepté le second : il n'y a pas de réponse à vérifier, et un terminal peut refuser OSC 52 par sécurité ou demander qu'on l'active. Le presse-papiers de l'éditeur contient le texte dans tous les cas, et la barre d'état dit combien de lignes ont été copiées.
159+
160+## Quelle part du protocole est implémentée
161+
162+Version de protocole **1**. Turbo Go annonce sa version dans `initialize` et accepte la version que l'agent répond, pourvu qu'il la connaisse.
163+
164+### Ce que l'éditeur appelle sur l'agent
165+
166+| Méthode | Implémentée | Remarques |
167+| --- | --- | --- |
168+| `initialize` | oui | Annonce la capacité `fs` ci-dessous ; `terminal` n'est pas annoncée |
169+| `session/new` | oui | `cwd` vient de la clé `cwd` de l'agent ; `mcpServers` est toujours vide — les serveurs MCP sont l'affaire de l'agent |
170+| `session/prompt` | oui | Des blocs texte, et un bloc `resource` ou `resource_link` par fichier désigné par `@` — voir [Commandes et mentions](#commandes-et-mentions) |
171+| `session/cancel` | oui | `Échap`, et **Agent ▸ Cancel turn** |
172+| `session/load` | **non** | Les conversations ne survivent pas à la fermeture de la fenêtre |
173+| `authenticate` | **non** | Un agent qui liste des `authMethods` est signalé comme exigeant une connexion que l'éditeur ne sait pas faire |
174+
175+### Ce que l'agent peut appeler sur l'éditeur
176+
177+| Méthode | Implémentée | Remarques |
178+| --- | --- | --- |
179+| `session/update` | oui | Voir la table ci-dessous |
180+| `session/request_permission` | oui | Une boîte modale portant les options de l'agent lui-même |
181+| `fs/read_text_file` | oui | Depuis le tampon quand le fichier est ouvert et modifié, sinon depuis le disque |
182+| `fs/write_text_file` | oui | Dans le tampon quand le fichier est ouvert, sinon sur le disque |
183+| `terminal/*` | **non** | Non annoncée, donc un agent conforme ne la demandera pas |
184+
185+### Mises à jour de session
186+
187+| `sessionUpdate` | Affiché comme |
188+| --- | --- |
189+| `agent_message_chunk` | La réponse de l'agent, ajoutée au fil de son arrivée |
190+| `agent_thought_chunk` | La même chose, dans la couleur des commentaires, sous une étiquette *réflexion* |
191+| `user_message_chunk` | Votre propre message, tel que l'agent le renvoie |
192+| `tool_call` | Une ligne nommant l'outil et son titre, avec son état |
193+| `tool_call_update` | Repliée sur la ligne dont le `toolCallId` correspond, avec sa sortie |
194+| `plan` | Les entrées en liste, chacune avec son état |
195+| `available_commands_update` | La liste que `/` ouvre dans la zone de saisie ; aussi listée, avec les descriptions, par **Agent ▸ Agent status** |
196+| `usage_update` | Le compte de jetons dans la barre d'état quand la fenêtre est devant |
197+| tout le reste | Ignoré, et compté ; le compte figure dans **Agent status** |
198+
199+Pendant qu'un tour est en cours, la règle entre les deux zones fait tourner un indicateur. Il est dessiné à partir de l'horloge et non d'un compteur, donc deux fenêtres qui réfléchissent en même temps tournent en phase et rien n'a besoin d'être remis à zéro au début d'un tour. Le *titre* de la fenêtre, lui, n'est délibérément pas animé : c'est aussi ce qu'affichent la liste des fenêtres et le menu `Alt`-chiffre, et un nom qui change huit fois par seconde les fait scintiller tous les deux.
200+
201+Une mise à jour inconnue est ignorée plutôt que refusée : le protocole grandit, et un éditeur qui cesserait de parler à un agent parce que celui-ci a appris un nouveau type de message aurait tort plus souvent que raison.
202+
203+## Coloration
204+
205+La conversation est dessinée avec des clés que tous les thèmes définissent déjà, donc aucun n'a eu besoin d'être touché :
206+
207+| Élément | Classe |
208+| --- | --- |
209+| Le nom d'un interlocuteur | `syntax.keyword` |
210+| Une réflexion | `syntax.comment` |
211+| Un appel d'outil et son état | `syntax.type` |
212+| Un appel d'outil en échec, et les avis de l'éditeur | `diagnostic.error` |
213+| Une ligne sélectionnée, et la barre du curseur | `editor.selection` |
214+| Le code dans un bloc délimité | l'analyseur du langage annoncé |
215+| Tout le reste | le texte ordinaire de la fenêtre |
216+
217+Un bloc délimité annonçant un langage que l'éditeur colore — `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash` — est coloré par cet analyseur. Un bloc annonçant autre chose, ou rien, est laissé brut.
218+
219+## Tracer la conversation avec un agent
220+
221+| Variable | Effet |
222+| --- | --- |
223+| `TURBO_ACP_TRACE=<fichier>` | Ajouter à ce fichier chaque message vers et depuis chaque agent, un par ligne, horodaté et marqué `->` (envoyé) ou `<-` (reçu) |
224+
225+C'est pour la seule question à laquelle l'écran ne peut pas répondre — *qu'a réellement envoyé l'agent ?* Une mise à jour que cet éditeur ne sait pas décoder est comptée dans **Agent ▸ Agent status**, qui nomme aussi la dernière et son erreur ; la trace montre le message lui-même. Un fichier qui ne peut pas être ouvert veut dire pas de trace, et rien d'autre : la trace n'a jamais le droit de casser l'éditeur.
226+
227+## Limites
228+
229+- **Une session par fenêtre.** Fermer la fenêtre termine la session ; il n'y a pas de reprise.
230+- **Texte et fichiers seulement.** L'éditeur envoie du texte, et les fichiers que vous désignez par `@` ; ni images ni audio, quoi que disent les `promptCapabilities` de l'agent.
231+- **Pas d'authentification.** Un agent exigeant une connexion doit être connecté par sa propre CLI avant que l'éditeur ne le lance.
232+- **`args` n'est pas une commande shell.** `command = "sh"`, `args = ["-c", "…"]` est la façon délibérée d'en obtenir une.
233+- **Une entrée est plafonnée** à un mégaoctet de texte. Un agent qui déverse tout un journal de compilation ne peut pas rendre la fenêtre inutilisable ; ce qui a été perdu est signalé dans l'entrée elle-même.
234+
235+## Voir aussi
236+
237+- La tâche : [Dialoguer avec un agent de code depuis l'éditeur](../how-to/talk-to-an-agent.md)
238+- Le raisonnement : [Fenêtres agent](../explanation/agent-windows.md)
239+- Le protocole : [agentclientprotocol.com](https://agentclientprotocol.com)
added docs/fr/reference/cli.md +100 -0
new file mode 100644
@@ -0,0 +1,100 @@
1+# Référence : ligne de commande
2+
3+> Description neutre de la commande `turbo-go`, de ses options et de l'environnement qu'elle lit.
4+
5+## Synopsis
6+
7+```
8+turbo-go [options] [fichier...]
9+```
10+
11+Chaque `fichier` est ouvert dans sa propre fenêtre. Un fichier qui n'existe pas encore est ouvert comme un tampon vide associé à ce chemin. Sans aucun fichier, une seule fenêtre vide sans titre est ouverte.
12+
13+## Options
14+
15+| Option | Type | Défaut | Description |
16+| --- | --- | --- | --- |
17+| `-theme <nom>` | chaîne | celui du projet, sinon `turbo-classic` | Thème de démarrage, l'emportant sur celui du projet. Un nom inconnu retombe sur le thème par défaut sans erreur. |
18+| `-list-themes` | booléen | `false` | Affiche chaque thème chargeable avec sa description, puis le répertoire de thèmes utilisateur, et quitte. |
19+| `-no-lsp` | booléen | `false` | Ne démarre pas de serveur de langage. La coloration et l'édition sont inchangées. |
20+| `-version` | booléen | `false` | Affiche `Turbo Go <version>` sur une ligne, avec le commit et la date de build quand le build les a enregistrés, puis quitte. Voir [le numéro de version](versioning.md). |
21+| `-h`, `-help` | booléen | `false` | Affiche la liste des options et quitte. |
22+
23+## Environnement
24+
25+| Variable | Lue par | Effet |
26+| --- | --- | --- |
27+| `TURBO_GO_THEME_DIR` | chargement des thèmes | Répertoire des thèmes utilisateur, à la place du répertoire de configuration de la plateforme. |
28+| `TERM` | tcell | Description de terminal à utiliser. |
29+| `GOBIN`, `GOPATH`, `HOME` | recherche de gopls | Consultées, dans cet ordre, quand `gopls` n'est pas dans le `PATH`. |
30+
31+## Fichiers
32+
33+| Chemin | Rôle |
34+| --- | --- |
35+| `$TURBO_GO_THEME_DIR/*.toml` | Thèmes utilisateur, quand la variable est définie. |
36+| `./.turbo-go/settings.toml` | Les réglages de ce projet, lus une fois au démarrage. Voir [réglages de projet](project-settings.md). |
37+| `~/.config/turbo-go/themes/*.toml` | Thèmes utilisateur sous Linux (`os.UserConfigDir`). |
38+| `~/Library/Application Support/turbo-go/themes/*.toml` | Thèmes utilisateur sous macOS. |
39+| `<module>/go.mod` | Trouvé en remontant depuis le premier fichier ; son répertoire devient la racine du serveur de langage. |
40+
41+## Code de sortie
42+
43+| Code | Signification |
44+| --- | --- |
45+| `0` | L'éditeur s'est terminé normalement, ou une option d'information a été utilisée. |
46+| `1` | Le terminal n'a pas pu être ouvert ou initialisé. La raison est écrite sur la sortie d'erreur. |
47+
48+## Cibles make
49+
50+À exécuter depuis un clone.
51+
52+| Cible | Ce qu'elle lance |
53+| --- | --- |
54+| `make help` | Liste les cibles. C'est la cible par défaut. |
55+| `make test` | `go test ./...` |
56+| `make test-verbose` | `go test -v ./...` |
57+| `make cover` | `go test -cover ./...` |
58+| `make build` | `go build -o bin/turbo-go .` |
59+| `make install` | `scripts/install.sh` — compile et installe dans le PATH |
60+| `make uninstall` | `scripts/install.sh --uninstall` |
61+| `make run FILE=x.go` | `make build`, puis `./bin/turbo-go x.go` |
62+| `make fmt` | `go fmt ./...` |
63+| `make vet` | `go vet ./...` |
64+| `make check` | `fmt`, puis `vet`, puis `test` |
65+| `make clean` | Supprime `bin/` |
66+
67+## Exemples
68+
69+```bash
70+turbo-go # une fenêtre vide
71+turbo-go main.go go.mod # deux fenêtres
72+turbo-go -theme turbo-dark main.go # un autre thème
73+turbo-go -no-lsp main.go # sans serveur de langage
74+turbo-go -list-themes # quels thèmes existent
75+```
76+
77+## Installateur
78+
79+`scripts/install.sh`, également accessible via `make install`.
80+
81+| Option | Description |
82+| --- | --- |
83+| `-p`, `--prefix RÉP` | Installer dans `RÉP` au lieu de `$GOBIN` ou `$GOPATH/bin`. |
84+| `--with-gopls` | Installer aussi `gopls`, s'il n'est pas déjà présent. |
85+| `--uninstall` | Retirer un `turbo-go` installé, puis s'arrêter. |
86+| `-h`, `--help` | Afficher les options, puis s'arrêter. |
87+
88+| Code de sortie | Signification |
89+| --- | --- |
90+| `0` | Installé, retiré, ou aide affichée. |
91+| `1` | Go absent ou trop ancien, échec de compilation, ou destination non accessible en écriture. Rien n'est installé et une installation existante reste intacte. |
92+
93+## Erreurs
94+
95+| Message | Cause |
96+| --- | --- |
97+| `turbo-go: opening the terminal: …` | tcell n'a pas pu ouvrir le terminal ; en général `TERM` est absent ou inconnu. |
98+| `turbo-go: initialising the terminal: …` | Le terminal a été ouvert mais n'a pas pu être mis en mode brut. |
99+| `Cannot open` (dans une boîte) | Le chemin est un répertoire, ou n'est pas lisible. |
100+| `Cannot save` (dans une boîte) | Le répertoire n'existe pas, ou n'est pas accessible en écriture. |
new file mode 100644
@@ -0,0 +1,100 @@
1+# Référence : ligne de commande
2+
3+> Description neutre de la commande `turbo-go`, de ses options et de l'environnement qu'elle lit.
4+
5+## Synopsis
6+
7+```
8+turbo-go [options] [fichier...]
9+```
10+
11+Chaque `fichier` est ouvert dans sa propre fenêtre. Un fichier qui n'existe pas encore est ouvert comme un tampon vide associé à ce chemin. Sans aucun fichier, une seule fenêtre vide sans titre est ouverte.
12+
13+## Options
14+
15+| Option | Type | Défaut | Description |
16+| --- | --- | --- | --- |
17+| `-theme <nom>` | chaîne | celui du projet, sinon `turbo-classic` | Thème de démarrage, l'emportant sur celui du projet. Un nom inconnu retombe sur le thème par défaut sans erreur. |
18+| `-list-themes` | booléen | `false` | Affiche chaque thème chargeable avec sa description, puis le répertoire de thèmes utilisateur, et quitte. |
19+| `-no-lsp` | booléen | `false` | Ne démarre pas de serveur de langage. La coloration et l'édition sont inchangées. |
20+| `-version` | booléen | `false` | Affiche `Turbo Go <version>` sur une ligne, avec le commit et la date de build quand le build les a enregistrés, puis quitte. Voir [le numéro de version](versioning.md). |
21+| `-h`, `-help` | booléen | `false` | Affiche la liste des options et quitte. |
22+
23+## Environnement
24+
25+| Variable | Lue par | Effet |
26+| --- | --- | --- |
27+| `TURBO_GO_THEME_DIR` | chargement des thèmes | Répertoire des thèmes utilisateur, à la place du répertoire de configuration de la plateforme. |
28+| `TERM` | tcell | Description de terminal à utiliser. |
29+| `GOBIN`, `GOPATH`, `HOME` | recherche de gopls | Consultées, dans cet ordre, quand `gopls` n'est pas dans le `PATH`. |
30+
31+## Fichiers
32+
33+| Chemin | Rôle |
34+| --- | --- |
35+| `$TURBO_GO_THEME_DIR/*.toml` | Thèmes utilisateur, quand la variable est définie. |
36+| `./.turbo-go/settings.toml` | Les réglages de ce projet, lus une fois au démarrage. Voir [réglages de projet](project-settings.md). |
37+| `~/.config/turbo-go/themes/*.toml` | Thèmes utilisateur sous Linux (`os.UserConfigDir`). |
38+| `~/Library/Application Support/turbo-go/themes/*.toml` | Thèmes utilisateur sous macOS. |
39+| `<module>/go.mod` | Trouvé en remontant depuis le premier fichier ; son répertoire devient la racine du serveur de langage. |
40+
41+## Code de sortie
42+
43+| Code | Signification |
44+| --- | --- |
45+| `0` | L'éditeur s'est terminé normalement, ou une option d'information a été utilisée. |
46+| `1` | Le terminal n'a pas pu être ouvert ou initialisé. La raison est écrite sur la sortie d'erreur. |
47+
48+## Cibles make
49+
50+À exécuter depuis un clone.
51+
52+| Cible | Ce qu'elle lance |
53+| --- | --- |
54+| `make help` | Liste les cibles. C'est la cible par défaut. |
55+| `make test` | `go test ./...` |
56+| `make test-verbose` | `go test -v ./...` |
57+| `make cover` | `go test -cover ./...` |
58+| `make build` | `go build -o bin/turbo-go .` |
59+| `make install` | `scripts/install.sh` — compile et installe dans le PATH |
60+| `make uninstall` | `scripts/install.sh --uninstall` |
61+| `make run FILE=x.go` | `make build`, puis `./bin/turbo-go x.go` |
62+| `make fmt` | `go fmt ./...` |
63+| `make vet` | `go vet ./...` |
64+| `make check` | `fmt`, puis `vet`, puis `test` |
65+| `make clean` | Supprime `bin/` |
66+
67+## Exemples
68+
69+```bash
70+turbo-go # une fenêtre vide
71+turbo-go main.go go.mod # deux fenêtres
72+turbo-go -theme turbo-dark main.go # un autre thème
73+turbo-go -no-lsp main.go # sans serveur de langage
74+turbo-go -list-themes # quels thèmes existent
75+```
76+
77+## Installateur
78+
79+`scripts/install.sh`, également accessible via `make install`.
80+
81+| Option | Description |
82+| --- | --- |
83+| `-p`, `--prefix RÉP` | Installer dans `RÉP` au lieu de `$GOBIN` ou `$GOPATH/bin`. |
84+| `--with-gopls` | Installer aussi `gopls`, s'il n'est pas déjà présent. |
85+| `--uninstall` | Retirer un `turbo-go` installé, puis s'arrêter. |
86+| `-h`, `--help` | Afficher les options, puis s'arrêter. |
87+
88+| Code de sortie | Signification |
89+| --- | --- |
90+| `0` | Installé, retiré, ou aide affichée. |
91+| `1` | Go absent ou trop ancien, échec de compilation, ou destination non accessible en écriture. Rien n'est installé et une installation existante reste intacte. |
92+
93+## Erreurs
94+
95+| Message | Cause |
96+| --- | --- |
97+| `turbo-go: opening the terminal: …` | tcell n'a pas pu ouvrir le terminal ; en général `TERM` est absent ou inconnu. |
98+| `turbo-go: initialising the terminal: …` | Le terminal a été ouvert mais n'a pas pu être mis en mode brut. |
99+| `Cannot open` (dans une boîte) | Le chemin est un répertoire, ou n'est pas lisible. |
100+| `Cannot save` (dans une boîte) | Le répertoire n'existe pas, ou n'est pas accessible en écriture. |
added docs/fr/reference/go-tools.md +236 -0
new file mode 100644
@@ -0,0 +1,236 @@
1+# Référence : outils go
2+
3+> Description neutre de `.turbo-go/tools.toml`, du menu Go, et de ce que lancer une commande fait.
4+
5+## Fichier
6+
7+| Propriété | Valeur |
8+| --- | --- |
9+| Chemin | `./.turbo-go/tools.toml` |
10+| Recherche | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. |
11+| Lecture | À chaque ouverture d'un de ses menus, pour les entrées |
12+| Relecture | Dès que la taille ou la date de modification du fichier change, pour l'**ensemble** des menus |
13+| Fichier absent | Pas une erreur |
14+| Fichier illisible | Une erreur, signalée dans le menu |
15+| Fichier utilisateur | **Aucun.** Contrairement aux snippets, il n'y a pas de `~/.config/turbo-go/tools.toml`. |
16+
17+## Format du fichier
18+
19+Une table `[[tool]]` par commande.
20+
21+| Clé | Type | Obligatoire | Description |
22+| --- | --- | --- | --- |
23+| `name` | chaîne | oui | Ce que le menu affiche. Peut porter une touche d'accès écrite avec des tildes, comme `"~T~est"`. |
24+| `command` | chaîne | oui | La commande shell à exécuter |
25+| `output` | chaîne | non | Où va sa sortie : `popup`, `terminal` ou `editor`. Absent signifie `popup`. |
26+| `menu` | chaîne | non | Dans quel menu il apparaît. Absent signifie `Go`. N'importe quel nom ; le menu est créé pour vous. Peut porter une touche d'accès écrite avec des tildes. |
27+
28+`menu` n'est pas vérifié contre une liste, parce qu'il n'y en a pas : un nom qu'aucun autre outil n'emploie crée simplement un menu. Un outil sans `name`, sans `command`, ou dont l'`output` nomme quelque chose qui n'existe pas rend tout le fichier erroné. Un `output` inconnu est **refusé et non corrigé** : `"termnial"` aurait sinon l'air d'avoir fonctionné tout en envoyant la sortie ailleurs.
29+
30+### Exemple
31+
32+```toml
33+[[tool]]
34+name = "~T~est"
35+command = "go test ./..."
36+output = "popup"
37+
38+[[tool]]
39+name = "~E~cho"
40+command = "echo TADA"
41+output = "terminal"
42+menu = "Tools"
43+```
44+
45+## Le fichier de départ
46+
47+**Go ▸ Create tools file** écrit ces cinq, dans cet ordre :
48+
49+| Nom | Commande | Sortie |
50+| --- | --- | --- |
51+| Format | `gofmt -l -w .` | `popup` |
52+| Lint | `go vet ./...` | `popup` |
53+| Build | `go build ./...` | `popup` |
54+| Test | `go test ./...` | `popup` |
55+| Run | `go run .` | `terminal` |
56+
57+Aucun ne nomme de `menu`, donc les cinq sont dans le menu Go. Chacun nomme son `output`, y compris les quatre qui nomment le défaut : la clé est la partie intéressante du format, et un fichier où elle n'apparaît qu'une fois est un fichier où personne ne remarque qu'elle existe.
58+
59+L'entrée est grisée dès que le projet a un fichier d'outils : elle ne peut donc pas en écraser un. Le fichier est écrit via un fichier temporaire du même dossier, renommé en place.
60+
61+## Le menu Go
62+
63+Toujours sur la barre, qu'un fichier d'outils existe ou non. Sa touche d'accès est `Alt-G`.
64+
65+| Entrée | Condition |
66+| --- | --- |
67+| Une ligne par outil sans `menu`, dans l'ordre du fichier | Le fichier en contient au moins un |
68+| `Cannot read tools`, grisé | Le fichier est présent mais illisible |
69+| `Create tools file` | Le projet n'a pas de fichier d'outils |
70+| `Open tools file` | Le projet en a un |
71+
72+## Les menus qu'un outil réclame
73+
74+Un `menu` nommant autre chose que `Go` place sur la barre un menu de ce nom.
75+
76+| Propriété | Valeur |
77+| --- | --- |
78+| Position | Entre Go et Help |
79+| Ordre | L'ordre où chaque nom apparaît pour la première fois dans le fichier |
80+| Entrées | Une ligne par outil nommant ce menu, dans l'ordre du fichier. Rien d'autre — `Create tools file` et `Open tools file` restent dans Go. |
81+| Fichier illisible | Aucun menu ; c'est le menu Go qui porte l'erreur |
82+| Pendant que l'éditeur tourne | Ajoutés, retirés et renommés au fil des modifications du fichier, sans redémarrage |
83+
84+### Touches d'accès
85+
86+Attribuées automatiquement, parce qu'un nom venu d'un fichier ne peut pas être confronté à l'avance aux menus fixes.
87+
88+| Cas | Résultat |
89+| --- | --- |
90+| Aucun tilde dans le nom | La première lettre qu'aucun autre menu ne revendique est marquée. `Format` devient `For~m~at` : `F` est à File, `o` à Options, `r` à Run. |
91+| Des tildes nommant une lettre libre | Conservés tels quels. `Doc~k~er` répond à `Alt-K`. |
92+| Des tildes nommant une lettre prise | Abandonnés, et une lettre libre choisie à la place. `~F~oo` devient `F~o~o`. |
93+| Toutes les lettres prises | Pas de touche d'accès. `F10` et la souris l'ouvrent quand même. |
94+
95+Les lettres que les menus de l'éditeur occupent sont `F`, `E`, `S`, `R`, `O`, `W`, `N` (Snippets), `G` et `H`.
96+
97+## Lancer une commande
98+
99+Commun à toutes les sorties :
100+
101+| Propriété | Valeur |
102+| --- | --- |
103+| Shell | `/bin/sh -c "<commande>"` sous Linux et macOS ; `cmd.exe /S /C "<commande>"` — le shell que nomme `%COMSPEC%` — sous Windows |
104+| Répertoire | Celui depuis lequel l'éditeur a été lancé |
105+| Erreur standard | Mêlée à la sortie standard, dans l'ordre où la commande les a écrites |
106+
107+Passer par un shell signifie que les tubes, les globs, `&&` et `;` fonctionnent : un outil peut être une séquence. Sous Windows le shell est cmd.exe, qui connaît `&&`, `|` et `>` mais ne développe pas les globs, et où `;` n'est pas un séparateur.
108+
109+### `output = "popup"`
110+
111+| Propriété | Valeur |
112+| --- | --- |
113+| Ouverture | Immédiate, avant la fin de la commande |
114+| Modale | Oui : rien d'autre dans l'éditeur n'est utilisable tant qu'elle est là |
115+| Remplissage | À mesure que la sortie arrive, en la suivant tant qu'on n'a pas remonté |
116+| Titre pendant | `<commande> — running` |
117+| Titre à la fin | `<commande> — ok`, ou `<commande> — exit <n>` |
118+| Sortie vide, terminée | Affiche `(no output)` |
119+| Sortie vide, en cours | N'affiche rien |
120+| Plafond de sortie | 10000 lignes ; au-delà les plus anciennes partent et une ligne `… n earlier lines dropped …` le dit |
121+
122+| Touche | Effet |
123+| --- | --- |
124+| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Parcourir la sortie |
125+| Molette | Idem |
126+| `Échap`, `Entrée`, **Close** | Fermer, en **arrêtant la commande** si elle tourne encore |
127+
128+Fermer arrête la commande parce qu'il n'y a pas d'autre moyen d'interrompre celle dont la sortie n'est pas dans un terminal.
129+
130+### `output = "terminal"`
131+
132+| Propriété | Valeur |
133+| --- | --- |
134+| Fenêtre | Une fenêtre terminal à elle, titrée avec la commande |
135+| Environnement | Celui de l'éditeur, avec `TERM` à `xterm-256color` |
136+| Après la sortie | La fenêtre reste, montrant sa sortie |
137+| Modale | Non : l'éditeur continue à côté |
138+
139+Comme c'est un vrai terminal, les couleurs, la pagination, `Ctrl-C` et la lecture au clavier fonctionnent. Voir [Fenêtres terminal](terminal.md).
140+
141+Touches dans une fenêtre **terminée** :
142+
143+| Touche | Effet |
144+| --- | --- |
145+| `Maj-Page↑`, `Maj-Page↓` | Relire la sortie |
146+| `Ctrl-W` | Fermer la fenêtre |
147+| Tout le reste | Atteint l'éditeur, pas le shell mort |
148+
149+### `output = "editor"`
150+
151+| Propriété | Valeur |
152+| --- | --- |
153+| Affiche | Une popup pendant l'exécution, comme ci-dessus |
154+| À la fermeture de la popup | Une fenêtre d'édition contenant la sortie, titrée avec la commande |
155+| Remplie | Une fois, à la fin de la commande — pas au fil de l'eau |
156+| La fenêtre | Une fenêtre d'édition ordinaire sans nom de fichier : cherchable avec `Ctrl-F`, et `Save as` la conserve |
157+
158+## Rechargement après une commande
159+
160+À la fin d'une commande, chaque fichier ouvert est examiné.
161+
162+| Le fichier | Ce qui se passe |
163+| --- | --- |
164+| Non modifié, et changé sur le disque | Relu ; son langage est redécidé et son titre rafraîchi |
165+| Non modifié, et inchangé sur le disque | Laissé tel quel, non compté |
166+| A des modifications non enregistrées | Laissé tel quel et compté comme ignoré |
167+| N'a jamais reçu de nom | Laissé tel quel |
168+| A disparu du disque | Laissé tel quel |
169+
170+Le curseur reste où il était, borné à ce que le fichier contient désormais. L'historique d'annulation est jeté, parce qu'annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus.
171+
172+L'arbre du projet est rafraîchi au même moment.
173+
174+| Barre d'état | Quand |
175+| --- | --- |
176+| `Running <commande>` | La fenêtre s'ouvre |
177+| `Reloaded 2 files` | Deux fichiers relus, aucun ignoré |
178+| `Reloaded 2 files; 1 file with unsaved changes left alone` | Certains ont été ignorés |
179+| `Command finished; 1 file with unsaved changes left alone` | Rien relu, quelque chose ignoré |
180+
181+## Erreurs
182+
183+| Message | Cause |
184+| --- | --- |
185+| `Cannot read tools` dans le menu | Le fichier est présent mais n'est pas du TOML valide, ou contient un outil sans nom ou sans commande |
186+| `Already there: .turbo-go/tools.toml` | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. |
187+| `This project has no .turbo-go/tools.toml yet.` | Ouvrir dans un projet qui n'en a pas, de même |
188+| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu |
189+| `Terminal windows are not supported on this platform yet` | Lancer une commande dans un terminal exige un pseudo-terminal, que Linux, macOS et Windows possèdent ; voir [Fenêtres terminal](terminal.md) |
190+
191+## Demander une valeur
192+
193+Un `{{libellé}}` n'importe où dans une commande est une valeur que l'éditeur demande avant de lancer, dans une boîte portant le nom de l'outil. Le texte entre les accolades est ce que la boîte demande.
194+
195+| Écrit | Demandé | Substitué |
196+| --- | --- | --- |
197+| `{{chemin du module}}` | `chemin du module` | protégé pour le shell |
198+| `{{options...}}` | `options` | tel quel |
199+
200+Une valeur est **protégée pour le shell** par défaut, si bien qu'un chemin contenant une espace reste un seul argument. Un `...` final à l'intérieur des accolades la demande telle quelle, ce qui permet à un seul champ de valoir plusieurs arguments.
201+
202+```toml
203+[[tool]]
204+name = "~I~nit module"
205+command = "go mod init {{chemin du module}}"
206+output = "popup"
207+```
208+
209+| Règle | Comportement |
210+| --- | --- |
211+| Plusieurs libellés | Une boîte, un champ chacun, dans l'ordre où ils apparaissent |
212+| Le même libellé deux fois | Un seul champ ; chaque occurrence reçoit ce qui y est tapé |
213+| Un libellé écrit des deux façons | Demandé une fois ; chaque occurrence honore ses propres accolades |
214+| Échap, ou Annuler | La commande n'est pas lancée |
215+| Un champ laissé vide | Substitué par du vide — la commande dira elle-même ce qui lui manque |
216+| Relancer l'outil | La boîte repart de ce qui avait été tapé, pour cette session seulement |
217+| Plus de champs que l'écran n'en contient | Refusé, avec un message disant combien tiennent |
218+
219+**Deux accolades, pas une.** `awk '{print $1}'` et `find . -exec rm {} +` sont des commandes ordinaires, et une syntaxe à une accolade lirait la première comme une demande de valeur nommée `print $1`.
220+
221+Rien n'est écrit sur le disque. Une valeur que quelqu'un a tapée cet après-midi n'est pas une décision du projet, elle n'a donc rien à faire dans le répertoire du projet.
222+
223+### Erreurs
224+
225+| Erreur | Cause |
226+| --- | --- |
227+| `tool "X": "{{module" is never closed` | Une ouverture `{{` sans `}}` après elle |
228+| `tool "X": {{}} asks for a value but does not say what it is` | Un libellé vide, ou réduit à `...` |
229+
230+Les deux sont refusées à la lecture du fichier : un libellé à moitié tapé n'atteint donc jamais le shell avec ses accolades.
231+
232+## Voir aussi
233+
234+- [Lancer les commandes go depuis l'éditeur](../how-to/run-go-commands.md)
235+- [Outils go](../explanation/go-tools.md)
236+- [Fenêtres terminal](terminal.md)
new file mode 100644
@@ -0,0 +1,236 @@
1+# Référence : outils go
2+
3+> Description neutre de `.turbo-go/tools.toml`, du menu Go, et de ce que lancer une commande fait.
4+
5+## Fichier
6+
7+| Propriété | Valeur |
8+| --- | --- |
9+| Chemin | `./.turbo-go/tools.toml` |
10+| Recherche | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. |
11+| Lecture | À chaque ouverture d'un de ses menus, pour les entrées |
12+| Relecture | Dès que la taille ou la date de modification du fichier change, pour l'**ensemble** des menus |
13+| Fichier absent | Pas une erreur |
14+| Fichier illisible | Une erreur, signalée dans le menu |
15+| Fichier utilisateur | **Aucun.** Contrairement aux snippets, il n'y a pas de `~/.config/turbo-go/tools.toml`. |
16+
17+## Format du fichier
18+
19+Une table `[[tool]]` par commande.
20+
21+| Clé | Type | Obligatoire | Description |
22+| --- | --- | --- | --- |
23+| `name` | chaîne | oui | Ce que le menu affiche. Peut porter une touche d'accès écrite avec des tildes, comme `"~T~est"`. |
24+| `command` | chaîne | oui | La commande shell à exécuter |
25+| `output` | chaîne | non | Où va sa sortie : `popup`, `terminal` ou `editor`. Absent signifie `popup`. |
26+| `menu` | chaîne | non | Dans quel menu il apparaît. Absent signifie `Go`. N'importe quel nom ; le menu est créé pour vous. Peut porter une touche d'accès écrite avec des tildes. |
27+
28+`menu` n'est pas vérifié contre une liste, parce qu'il n'y en a pas : un nom qu'aucun autre outil n'emploie crée simplement un menu. Un outil sans `name`, sans `command`, ou dont l'`output` nomme quelque chose qui n'existe pas rend tout le fichier erroné. Un `output` inconnu est **refusé et non corrigé** : `"termnial"` aurait sinon l'air d'avoir fonctionné tout en envoyant la sortie ailleurs.
29+
30+### Exemple
31+
32+```toml
33+[[tool]]
34+name = "~T~est"
35+command = "go test ./..."
36+output = "popup"
37+
38+[[tool]]
39+name = "~E~cho"
40+command = "echo TADA"
41+output = "terminal"
42+menu = "Tools"
43+```
44+
45+## Le fichier de départ
46+
47+**Go ▸ Create tools file** écrit ces cinq, dans cet ordre :
48+
49+| Nom | Commande | Sortie |
50+| --- | --- | --- |
51+| Format | `gofmt -l -w .` | `popup` |
52+| Lint | `go vet ./...` | `popup` |
53+| Build | `go build ./...` | `popup` |
54+| Test | `go test ./...` | `popup` |
55+| Run | `go run .` | `terminal` |
56+
57+Aucun ne nomme de `menu`, donc les cinq sont dans le menu Go. Chacun nomme son `output`, y compris les quatre qui nomment le défaut : la clé est la partie intéressante du format, et un fichier où elle n'apparaît qu'une fois est un fichier où personne ne remarque qu'elle existe.
58+
59+L'entrée est grisée dès que le projet a un fichier d'outils : elle ne peut donc pas en écraser un. Le fichier est écrit via un fichier temporaire du même dossier, renommé en place.
60+
61+## Le menu Go
62+
63+Toujours sur la barre, qu'un fichier d'outils existe ou non. Sa touche d'accès est `Alt-G`.
64+
65+| Entrée | Condition |
66+| --- | --- |
67+| Une ligne par outil sans `menu`, dans l'ordre du fichier | Le fichier en contient au moins un |
68+| `Cannot read tools`, grisé | Le fichier est présent mais illisible |
69+| `Create tools file` | Le projet n'a pas de fichier d'outils |
70+| `Open tools file` | Le projet en a un |
71+
72+## Les menus qu'un outil réclame
73+
74+Un `menu` nommant autre chose que `Go` place sur la barre un menu de ce nom.
75+
76+| Propriété | Valeur |
77+| --- | --- |
78+| Position | Entre Go et Help |
79+| Ordre | L'ordre où chaque nom apparaît pour la première fois dans le fichier |
80+| Entrées | Une ligne par outil nommant ce menu, dans l'ordre du fichier. Rien d'autre — `Create tools file` et `Open tools file` restent dans Go. |
81+| Fichier illisible | Aucun menu ; c'est le menu Go qui porte l'erreur |
82+| Pendant que l'éditeur tourne | Ajoutés, retirés et renommés au fil des modifications du fichier, sans redémarrage |
83+
84+### Touches d'accès
85+
86+Attribuées automatiquement, parce qu'un nom venu d'un fichier ne peut pas être confronté à l'avance aux menus fixes.
87+
88+| Cas | Résultat |
89+| --- | --- |
90+| Aucun tilde dans le nom | La première lettre qu'aucun autre menu ne revendique est marquée. `Format` devient `For~m~at` : `F` est à File, `o` à Options, `r` à Run. |
91+| Des tildes nommant une lettre libre | Conservés tels quels. `Doc~k~er` répond à `Alt-K`. |
92+| Des tildes nommant une lettre prise | Abandonnés, et une lettre libre choisie à la place. `~F~oo` devient `F~o~o`. |
93+| Toutes les lettres prises | Pas de touche d'accès. `F10` et la souris l'ouvrent quand même. |
94+
95+Les lettres que les menus de l'éditeur occupent sont `F`, `E`, `S`, `R`, `O`, `W`, `N` (Snippets), `G` et `H`.
96+
97+## Lancer une commande
98+
99+Commun à toutes les sorties :
100+
101+| Propriété | Valeur |
102+| --- | --- |
103+| Shell | `/bin/sh -c "<commande>"` sous Linux et macOS ; `cmd.exe /S /C "<commande>"` — le shell que nomme `%COMSPEC%` — sous Windows |
104+| Répertoire | Celui depuis lequel l'éditeur a été lancé |
105+| Erreur standard | Mêlée à la sortie standard, dans l'ordre où la commande les a écrites |
106+
107+Passer par un shell signifie que les tubes, les globs, `&&` et `;` fonctionnent : un outil peut être une séquence. Sous Windows le shell est cmd.exe, qui connaît `&&`, `|` et `>` mais ne développe pas les globs, et où `;` n'est pas un séparateur.
108+
109+### `output = "popup"`
110+
111+| Propriété | Valeur |
112+| --- | --- |
113+| Ouverture | Immédiate, avant la fin de la commande |
114+| Modale | Oui : rien d'autre dans l'éditeur n'est utilisable tant qu'elle est là |
115+| Remplissage | À mesure que la sortie arrive, en la suivant tant qu'on n'a pas remonté |
116+| Titre pendant | `<commande> — running` |
117+| Titre à la fin | `<commande> — ok`, ou `<commande> — exit <n>` |
118+| Sortie vide, terminée | Affiche `(no output)` |
119+| Sortie vide, en cours | N'affiche rien |
120+| Plafond de sortie | 10000 lignes ; au-delà les plus anciennes partent et une ligne `… n earlier lines dropped …` le dit |
121+
122+| Touche | Effet |
123+| --- | --- |
124+| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Parcourir la sortie |
125+| Molette | Idem |
126+| `Échap`, `Entrée`, **Close** | Fermer, en **arrêtant la commande** si elle tourne encore |
127+
128+Fermer arrête la commande parce qu'il n'y a pas d'autre moyen d'interrompre celle dont la sortie n'est pas dans un terminal.
129+
130+### `output = "terminal"`
131+
132+| Propriété | Valeur |
133+| --- | --- |
134+| Fenêtre | Une fenêtre terminal à elle, titrée avec la commande |
135+| Environnement | Celui de l'éditeur, avec `TERM` à `xterm-256color` |
136+| Après la sortie | La fenêtre reste, montrant sa sortie |
137+| Modale | Non : l'éditeur continue à côté |
138+
139+Comme c'est un vrai terminal, les couleurs, la pagination, `Ctrl-C` et la lecture au clavier fonctionnent. Voir [Fenêtres terminal](terminal.md).
140+
141+Touches dans une fenêtre **terminée** :
142+
143+| Touche | Effet |
144+| --- | --- |
145+| `Maj-Page↑`, `Maj-Page↓` | Relire la sortie |
146+| `Ctrl-W` | Fermer la fenêtre |
147+| Tout le reste | Atteint l'éditeur, pas le shell mort |
148+
149+### `output = "editor"`
150+
151+| Propriété | Valeur |
152+| --- | --- |
153+| Affiche | Une popup pendant l'exécution, comme ci-dessus |
154+| À la fermeture de la popup | Une fenêtre d'édition contenant la sortie, titrée avec la commande |
155+| Remplie | Une fois, à la fin de la commande — pas au fil de l'eau |
156+| La fenêtre | Une fenêtre d'édition ordinaire sans nom de fichier : cherchable avec `Ctrl-F`, et `Save as` la conserve |
157+
158+## Rechargement après une commande
159+
160+À la fin d'une commande, chaque fichier ouvert est examiné.
161+
162+| Le fichier | Ce qui se passe |
163+| --- | --- |
164+| Non modifié, et changé sur le disque | Relu ; son langage est redécidé et son titre rafraîchi |
165+| Non modifié, et inchangé sur le disque | Laissé tel quel, non compté |
166+| A des modifications non enregistrées | Laissé tel quel et compté comme ignoré |
167+| N'a jamais reçu de nom | Laissé tel quel |
168+| A disparu du disque | Laissé tel quel |
169+
170+Le curseur reste où il était, borné à ce que le fichier contient désormais. L'historique d'annulation est jeté, parce qu'annuler au-delà d'un rechargement restaurerait un texte que le fichier n'a plus.
171+
172+L'arbre du projet est rafraîchi au même moment.
173+
174+| Barre d'état | Quand |
175+| --- | --- |
176+| `Running <commande>` | La fenêtre s'ouvre |
177+| `Reloaded 2 files` | Deux fichiers relus, aucun ignoré |
178+| `Reloaded 2 files; 1 file with unsaved changes left alone` | Certains ont été ignorés |
179+| `Command finished; 1 file with unsaved changes left alone` | Rien relu, quelque chose ignoré |
180+
181+## Erreurs
182+
183+| Message | Cause |
184+| --- | --- |
185+| `Cannot read tools` dans le menu | Le fichier est présent mais n'est pas du TOML valide, ou contient un outil sans nom ou sans commande |
186+| `Already there: .turbo-go/tools.toml` | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. |
187+| `This project has no .turbo-go/tools.toml yet.` | Ouvrir dans un projet qui n'en a pas, de même |
188+| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu |
189+| `Terminal windows are not supported on this platform yet` | Lancer une commande dans un terminal exige un pseudo-terminal, que Linux, macOS et Windows possèdent ; voir [Fenêtres terminal](terminal.md) |
190+
191+## Demander une valeur
192+
193+Un `{{libellé}}` n'importe où dans une commande est une valeur que l'éditeur demande avant de lancer, dans une boîte portant le nom de l'outil. Le texte entre les accolades est ce que la boîte demande.
194+
195+| Écrit | Demandé | Substitué |
196+| --- | --- | --- |
197+| `{{chemin du module}}` | `chemin du module` | protégé pour le shell |
198+| `{{options...}}` | `options` | tel quel |
199+
200+Une valeur est **protégée pour le shell** par défaut, si bien qu'un chemin contenant une espace reste un seul argument. Un `...` final à l'intérieur des accolades la demande telle quelle, ce qui permet à un seul champ de valoir plusieurs arguments.
201+
202+```toml
203+[[tool]]
204+name = "~I~nit module"
205+command = "go mod init {{chemin du module}}"
206+output = "popup"
207+```
208+
209+| Règle | Comportement |
210+| --- | --- |
211+| Plusieurs libellés | Une boîte, un champ chacun, dans l'ordre où ils apparaissent |
212+| Le même libellé deux fois | Un seul champ ; chaque occurrence reçoit ce qui y est tapé |
213+| Un libellé écrit des deux façons | Demandé une fois ; chaque occurrence honore ses propres accolades |
214+| Échap, ou Annuler | La commande n'est pas lancée |
215+| Un champ laissé vide | Substitué par du vide — la commande dira elle-même ce qui lui manque |
216+| Relancer l'outil | La boîte repart de ce qui avait été tapé, pour cette session seulement |
217+| Plus de champs que l'écran n'en contient | Refusé, avec un message disant combien tiennent |
218+
219+**Deux accolades, pas une.** `awk '{print $1}'` et `find . -exec rm {} +` sont des commandes ordinaires, et une syntaxe à une accolade lirait la première comme une demande de valeur nommée `print $1`.
220+
221+Rien n'est écrit sur le disque. Une valeur que quelqu'un a tapée cet après-midi n'est pas une décision du projet, elle n'a donc rien à faire dans le répertoire du projet.
222+
223+### Erreurs
224+
225+| Erreur | Cause |
226+| --- | --- |
227+| `tool "X": "{{module" is never closed` | Une ouverture `{{` sans `}}` après elle |
228+| `tool "X": {{}} asks for a value but does not say what it is` | Un libellé vide, ou réduit à `...` |
229+
230+Les deux sont refusées à la lecture du fichier : un libellé à moitié tapé n'atteint donc jamais le shell avec ses accolades.
231+
232+## Voir aussi
233+
234+- [Lancer les commandes go depuis l'éditeur](../how-to/run-go-commands.md)
235+- [Outils go](../explanation/go-tools.md)
236+- [Fenêtres terminal](terminal.md)
added docs/fr/reference/keyboard.md +177 -0
new file mode 100644
@@ -0,0 +1,177 @@
1+# Référence : clavier
2+
3+> Liste complète des touches auxquelles Turbo Go répond, regroupées par élément qui a le focus.
4+
5+Lorsque deux écritures existent, les deux fonctionnent : celle de Turbo C et la moderne.
6+
7+## Global
8+
9+Traitées où que soit le focus, sauf si un dialogue ou la liste de complétion est ouvert.
10+
11+| Touche | Action |
12+| --- | --- |
13+| `F1` | Décrire le symbole sous le curseur ; sans fichier ouvert, afficher l'aide clavier |
14+| `F2` | Enregistrer |
15+| `F3` | Ouvrir |
16+| `F4` | Nouveau |
17+| `F6` | Fenêtre suivante |
18+| `F7` | Occurrence suivante |
19+| `Maj-F7` | Occurrence précédente |
20+| `F8` | Ouvrir une fenêtre terminal |
21+| `F9` | Ouvrir l'arbre du projet |
22+| `F10` | Ouvrir la barre de menus |
23+| `F12` | Aller à la définition |
24+| `Shift-F12` | Trouver les références |
25+| `Ctrl-T` | Trouver un symbole dans tout le projet |
26+| `Ctrl-F` | Chercher |
27+| `Ctrl-G` | Aller à la ligne |
28+| `Ctrl-W` | Fermer la fenêtre courante |
29+| `Alt-X` | Quitter |
30+| `Alt-1``Alt-9` | Passer la fenêtre 1…9 au premier plan |
31+| `Alt-0` | Lister les fenêtres ouvertes |
32+| `Alt-N` | Ouvrir le menu Snippets |
33+| `Alt-G` | Ouvrir le menu Go |
34+| `Alt-<lettre>` | Ouvrir le menu dont le titre porte cette lettre |
35+
36+Un menu ajouté par le fichier d'outils du projet reçoit sa lettre par attribution et non par choix, donc ce n'est jamais une de celles ci-dessus. Les règles sont dans [Outils go](go-tools.md#touches-daccès).
37+
38+## Édition
39+
40+Traitées par la fenêtre qui a le focus.
41+
42+### Déplacement
43+
44+| Touche | Action |
45+| --- | --- |
46+| `←` `→` `↑` `↓` | Un caractère ou une ligne |
47+| `Ctrl-←` `Ctrl-→` | Début du mot précédent / suivant |
48+| `Origine` `Fin` | Début / fin de la ligne |
49+| `Ctrl-Origine` `Ctrl-Fin` | Début / fin du fichier |
50+| `Page↑` `Page↓` | Un écran |
51+| `Maj` + l'une des précédentes | Le même déplacement, en étendant la sélection |
52+
53+### Modification du texte
54+
55+| Touche | Action |
56+| --- | --- |
57+| tout caractère imprimable | L'insérer, en remplaçant la sélection |
58+| `Entrée` | Couper la ligne, en recopiant l'indentation de la ligne courante |
59+| `Retour arrière` | Supprimer la sélection, ou le caractère avant le curseur |
60+| `Suppr` | Supprimer la sélection, ou le caractère sous le curseur |
61+| `Tab` | Insérer une tabulation ; avec une sélection, indenter chaque ligne concernée |
62+| `Maj-Tab` | Retirer un niveau d'indentation de chaque ligne concernée |
63+
64+### Presse-papier et historique
65+
66+| Touche | Aussi | Action |
67+| --- | --- | --- |
68+| `Ctrl-C` | `Ctrl-Inser` | Copier la sélection |
69+| `Ctrl-X` | `Maj-Suppr` | Couper la sélection |
70+| `Ctrl-V` | `Maj-Inser` | Coller |
71+| `Ctrl-A` | | Tout sélectionner |
72+| `Ctrl-Z` | | Annuler |
73+| `Ctrl-R` | | Rétablir |
74+| `Ctrl-N` | | Insérer une ligne vide au-dessus du curseur |
75+| `Ctrl-Y` | | Supprimer la ligne où est le curseur |
76+
77+Une série de caractères tapés, ou une série de retours arrière, forme **une seule** étape d'annulation. Déplacer le curseur clôt la série.
78+
79+### Serveur de langage
80+
81+| Touche | Action |
82+| --- | --- |
83+| `Ctrl-Espace` | Demander une liste de complétion |
84+| `.` | Demander une liste de complétion, en effet de bord de la frappe |
85+| `F1` | Décrire le symbole sous le curseur |
86+| `F12` | Aller à la déclaration |
87+
88+## Barre de menus
89+
90+Une fois un menu ouvert.
91+
92+| Touche | Action |
93+| --- | --- |
94+| `←` `→` | Menu précédent / suivant |
95+| `↑` `↓` | Entrée précédente / suivante, en sautant les séparateurs et les entrées grisées |
96+| `Entrée` | Exécuter l'entrée surlignée |
97+| `<lettre>` | Exécuter l'entrée dont l'intitulé porte cette lettre |
98+| `Échap` | Fermer le menu |
99+
100+Toute autre touche est absorbée : une frappe égarée n'atteint jamais le fichier derrière.
101+
102+## Dialogues
103+
104+| Touche | Action |
105+| --- | --- |
106+| `Tab` / `Maj-Tab` | Contrôle suivant / précédent |
107+| `↑` `↓` | Parcourir la liste qui a le focus ; si le contrôle n'en a pas l'usage, contrôle suivant / précédent |
108+| `Entrée` | Actionner le bouton par défaut, d'où que soit le focus |
109+| `Échap` | Annuler |
110+| `Alt-<lettre>` | Actionner le bouton dont l'intitulé porte cette lettre |
111+| `Ctrl-U` | Vider le champ de saisie qui a le focus |
112+
113+Un dialogue est modal : toute touche dont il n'a pas l'usage est absorbée plutôt que transmise à l'éditeur derrière.
114+
115+### La boîte Open et Save As
116+
117+| | |
118+| --- | --- |
119+| Focus à l'ouverture | Le champ **Name**, pour pouvoir taper un nom directement. La première `↓` déplace donc le focus vers la liste ; la seconde déplace la surbrillance. |
120+| Déplacer la surbrillance | Place le nom de cette entrée dans le champ **Name** : le champ dit toujours ce sur quoi **OK** va agir. Surligner `../` le vide. |
121+| `Entrée` sur la liste | Ouvre le fichier surligné, ou entre dans le dossier surligné |
122+| **OK** | Agit sur le champ **Name** ; si le champ est vide, agit sur ce que la liste a surligné |
123+| Un nom qui est un dossier | Y entre au lieu de fermer le dialogue |
124+| Double clic | Équivaut à `Entrée` sur cette entrée |
125+
126+Les fichiers cachés ne sont pas listés. Les dossiers précèdent les fichiers, chaque groupe trié, avec `../` en premier.
127+
128+## Liste de complétion
129+
130+| Touche | Action |
131+| --- | --- |
132+| `↑` `↓` | Suggestion précédente / suivante |
133+| `Page↑` `Page↓` | Huit à la fois |
134+| `Entrée`, `Tab` | Accepter la suggestion surlignée |
135+| `Échap` | Abandonner la liste |
136+| tout caractère imprimable | Transmis à l'éditeur ; la liste se réduit à ce qui correspond encore |
137+
138+## Fenêtres terminal
139+
140+Une fenêtre terminal au premier plan reçoit **toutes les touches sauf** les touches de fonction, `Alt-X` et `Alt-0``Alt-9`, qui restent à l'éditeur pour qu'il y ait toujours une sortie hors d'un programme plein écran. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` et `Alt-<lettre>` atteignent donc le shell plutôt que l'éditeur.
141+
142+| Touche | Action |
143+| --- | --- |
144+| `Maj-Page↑` `Maj-Page↓` | Reculer / avancer d'un écran dans l'historique |
145+| toute autre touche non réservée ci-dessus | Envoyée au shell, ramenant la vue à l'écran vivant |
146+
147+Les octets exacts envoyés par chaque touche sont dans [Fenêtres terminal](terminal.md).
148+
149+## Arbre du projet
150+
151+Traitées quand la fenêtre de l'arbre a le focus. Les règles complètes sont dans [Arbre du projet](project-tree.md).
152+
153+| Touche | Action |
154+| --- | --- |
155+| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Déplacer la surbrillance |
156+| `→` | Déplier un dossier fermé, sinon aller à la ligne suivante |
157+| `←` | Replier un dossier ouvert, sinon remonter à son dossier |
158+| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier |
159+| `F5`, `Ctrl-R` | Relire le projet |
160+
161+## Souris
162+
163+| Action | Effet |
164+| --- | --- |
165+| Clic dans le texte | Placer le curseur |
166+| Glisser dans le texte | Sélectionner |
167+| Molette | Défiler de trois lignes |
168+| Clic sur un titre de menu | Ouvrir ou fermer ce menu |
169+| Clic sur un indice de la barre d'état | L'exécuter |
170+| Clic sur une fenêtre | La passer au premier plan |
171+| Glisser une barre de titre | Déplacer la fenêtre |
172+| Glisser le coin inférieur droit | Redimensionner la fenêtre |
173+| Clic sur `[x]` | Fermer la fenêtre |
174+| Clic sur `[■]` | Donner tout le bureau à la fenêtre |
175+| Clic sur `[▬]` | Remettre une fenêtre agrandie à sa taille précédente |
176+| Molette sur un terminal | Défiler de trois lignes dans son historique |
177+| Clic sur une ligne d'arbre | La surligner ; un second clic l'ouvre |
new file mode 100644
@@ -0,0 +1,177 @@
1+# Référence : clavier
2+
3+> Liste complète des touches auxquelles Turbo Go répond, regroupées par élément qui a le focus.
4+
5+Lorsque deux écritures existent, les deux fonctionnent : celle de Turbo C et la moderne.
6+
7+## Global
8+
9+Traitées où que soit le focus, sauf si un dialogue ou la liste de complétion est ouvert.
10+
11+| Touche | Action |
12+| --- | --- |
13+| `F1` | Décrire le symbole sous le curseur ; sans fichier ouvert, afficher l'aide clavier |
14+| `F2` | Enregistrer |
15+| `F3` | Ouvrir |
16+| `F4` | Nouveau |
17+| `F6` | Fenêtre suivante |
18+| `F7` | Occurrence suivante |
19+| `Maj-F7` | Occurrence précédente |
20+| `F8` | Ouvrir une fenêtre terminal |
21+| `F9` | Ouvrir l'arbre du projet |
22+| `F10` | Ouvrir la barre de menus |
23+| `F12` | Aller à la définition |
24+| `Shift-F12` | Trouver les références |
25+| `Ctrl-T` | Trouver un symbole dans tout le projet |
26+| `Ctrl-F` | Chercher |
27+| `Ctrl-G` | Aller à la ligne |
28+| `Ctrl-W` | Fermer la fenêtre courante |
29+| `Alt-X` | Quitter |
30+| `Alt-1``Alt-9` | Passer la fenêtre 1…9 au premier plan |
31+| `Alt-0` | Lister les fenêtres ouvertes |
32+| `Alt-N` | Ouvrir le menu Snippets |
33+| `Alt-G` | Ouvrir le menu Go |
34+| `Alt-<lettre>` | Ouvrir le menu dont le titre porte cette lettre |
35+
36+Un menu ajouté par le fichier d'outils du projet reçoit sa lettre par attribution et non par choix, donc ce n'est jamais une de celles ci-dessus. Les règles sont dans [Outils go](go-tools.md#touches-daccès).
37+
38+## Édition
39+
40+Traitées par la fenêtre qui a le focus.
41+
42+### Déplacement
43+
44+| Touche | Action |
45+| --- | --- |
46+| `←` `→` `↑` `↓` | Un caractère ou une ligne |
47+| `Ctrl-←` `Ctrl-→` | Début du mot précédent / suivant |
48+| `Origine` `Fin` | Début / fin de la ligne |
49+| `Ctrl-Origine` `Ctrl-Fin` | Début / fin du fichier |
50+| `Page↑` `Page↓` | Un écran |
51+| `Maj` + l'une des précédentes | Le même déplacement, en étendant la sélection |
52+
53+### Modification du texte
54+
55+| Touche | Action |
56+| --- | --- |
57+| tout caractère imprimable | L'insérer, en remplaçant la sélection |
58+| `Entrée` | Couper la ligne, en recopiant l'indentation de la ligne courante |
59+| `Retour arrière` | Supprimer la sélection, ou le caractère avant le curseur |
60+| `Suppr` | Supprimer la sélection, ou le caractère sous le curseur |
61+| `Tab` | Insérer une tabulation ; avec une sélection, indenter chaque ligne concernée |
62+| `Maj-Tab` | Retirer un niveau d'indentation de chaque ligne concernée |
63+
64+### Presse-papier et historique
65+
66+| Touche | Aussi | Action |
67+| --- | --- | --- |
68+| `Ctrl-C` | `Ctrl-Inser` | Copier la sélection |
69+| `Ctrl-X` | `Maj-Suppr` | Couper la sélection |
70+| `Ctrl-V` | `Maj-Inser` | Coller |
71+| `Ctrl-A` | | Tout sélectionner |
72+| `Ctrl-Z` | | Annuler |
73+| `Ctrl-R` | | Rétablir |
74+| `Ctrl-N` | | Insérer une ligne vide au-dessus du curseur |
75+| `Ctrl-Y` | | Supprimer la ligne où est le curseur |
76+
77+Une série de caractères tapés, ou une série de retours arrière, forme **une seule** étape d'annulation. Déplacer le curseur clôt la série.
78+
79+### Serveur de langage
80+
81+| Touche | Action |
82+| --- | --- |
83+| `Ctrl-Espace` | Demander une liste de complétion |
84+| `.` | Demander une liste de complétion, en effet de bord de la frappe |
85+| `F1` | Décrire le symbole sous le curseur |
86+| `F12` | Aller à la déclaration |
87+
88+## Barre de menus
89+
90+Une fois un menu ouvert.
91+
92+| Touche | Action |
93+| --- | --- |
94+| `←` `→` | Menu précédent / suivant |
95+| `↑` `↓` | Entrée précédente / suivante, en sautant les séparateurs et les entrées grisées |
96+| `Entrée` | Exécuter l'entrée surlignée |
97+| `<lettre>` | Exécuter l'entrée dont l'intitulé porte cette lettre |
98+| `Échap` | Fermer le menu |
99+
100+Toute autre touche est absorbée : une frappe égarée n'atteint jamais le fichier derrière.
101+
102+## Dialogues
103+
104+| Touche | Action |
105+| --- | --- |
106+| `Tab` / `Maj-Tab` | Contrôle suivant / précédent |
107+| `↑` `↓` | Parcourir la liste qui a le focus ; si le contrôle n'en a pas l'usage, contrôle suivant / précédent |
108+| `Entrée` | Actionner le bouton par défaut, d'où que soit le focus |
109+| `Échap` | Annuler |
110+| `Alt-<lettre>` | Actionner le bouton dont l'intitulé porte cette lettre |
111+| `Ctrl-U` | Vider le champ de saisie qui a le focus |
112+
113+Un dialogue est modal : toute touche dont il n'a pas l'usage est absorbée plutôt que transmise à l'éditeur derrière.
114+
115+### La boîte Open et Save As
116+
117+| | |
118+| --- | --- |
119+| Focus à l'ouverture | Le champ **Name**, pour pouvoir taper un nom directement. La première `↓` déplace donc le focus vers la liste ; la seconde déplace la surbrillance. |
120+| Déplacer la surbrillance | Place le nom de cette entrée dans le champ **Name** : le champ dit toujours ce sur quoi **OK** va agir. Surligner `../` le vide. |
121+| `Entrée` sur la liste | Ouvre le fichier surligné, ou entre dans le dossier surligné |
122+| **OK** | Agit sur le champ **Name** ; si le champ est vide, agit sur ce que la liste a surligné |
123+| Un nom qui est un dossier | Y entre au lieu de fermer le dialogue |
124+| Double clic | Équivaut à `Entrée` sur cette entrée |
125+
126+Les fichiers cachés ne sont pas listés. Les dossiers précèdent les fichiers, chaque groupe trié, avec `../` en premier.
127+
128+## Liste de complétion
129+
130+| Touche | Action |
131+| --- | --- |
132+| `↑` `↓` | Suggestion précédente / suivante |
133+| `Page↑` `Page↓` | Huit à la fois |
134+| `Entrée`, `Tab` | Accepter la suggestion surlignée |
135+| `Échap` | Abandonner la liste |
136+| tout caractère imprimable | Transmis à l'éditeur ; la liste se réduit à ce qui correspond encore |
137+
138+## Fenêtres terminal
139+
140+Une fenêtre terminal au premier plan reçoit **toutes les touches sauf** les touches de fonction, `Alt-X` et `Alt-0``Alt-9`, qui restent à l'éditeur pour qu'il y ait toujours une sortie hors d'un programme plein écran. `Ctrl-C`, `Ctrl-W`, `Ctrl-F` et `Alt-<lettre>` atteignent donc le shell plutôt que l'éditeur.
141+
142+| Touche | Action |
143+| --- | --- |
144+| `Maj-Page↑` `Maj-Page↓` | Reculer / avancer d'un écran dans l'historique |
145+| toute autre touche non réservée ci-dessus | Envoyée au shell, ramenant la vue à l'écran vivant |
146+
147+Les octets exacts envoyés par chaque touche sont dans [Fenêtres terminal](terminal.md).
148+
149+## Arbre du projet
150+
151+Traitées quand la fenêtre de l'arbre a le focus. Les règles complètes sont dans [Arbre du projet](project-tree.md).
152+
153+| Touche | Action |
154+| --- | --- |
155+| `↑` `↓` `Page↑` `Page↓` `Début` `Fin` | Déplacer la surbrillance |
156+| `→` | Déplier un dossier fermé, sinon aller à la ligne suivante |
157+| `←` | Replier un dossier ouvert, sinon remonter à son dossier |
158+| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier |
159+| `F5`, `Ctrl-R` | Relire le projet |
160+
161+## Souris
162+
163+| Action | Effet |
164+| --- | --- |
165+| Clic dans le texte | Placer le curseur |
166+| Glisser dans le texte | Sélectionner |
167+| Molette | Défiler de trois lignes |
168+| Clic sur un titre de menu | Ouvrir ou fermer ce menu |
169+| Clic sur un indice de la barre d'état | L'exécuter |
170+| Clic sur une fenêtre | La passer au premier plan |
171+| Glisser une barre de titre | Déplacer la fenêtre |
172+| Glisser le coin inférieur droit | Redimensionner la fenêtre |
173+| Clic sur `[x]` | Fermer la fenêtre |
174+| Clic sur `[■]` | Donner tout le bureau à la fenêtre |
175+| Clic sur `[▬]` | Remettre une fenêtre agrandie à sa taille précédente |
176+| Molette sur un terminal | Défiler de trois lignes dans son historique |
177+| Clic sur une ligne d'arbre | La surligner ; un second clic l'ouvre |
added docs/fr/reference/languages.md +238 -0
new file mode 100644
@@ -0,0 +1,238 @@
1+# Référence : langages colorés
2+
3+> Description neutre des fichiers que Turbo Go colore, de la façon dont il décide, et de ce que reconnaît chaque scanner.
4+
5+## Reconnaissance
6+
7+L'**extension** d'un fichier décide dès qu'elle fait partie de celles-ci :
8+
9+| Extension | Langage |
10+| --- | --- |
11+| `.go` | Go |
12+| `.toml` | TOML |
13+| `.yaml`, `.yml` | YAML |
14+| `.md`, `.markdown` | Markdown |
15+| `.js`, `.mjs`, `.cjs` | JavaScript |
16+| `.html`, `.htm` | HTML |
17+| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML |
18+| `.sh`, `.bash`, `.zsh` | Shell |
19+| `.dockerfile`, `.containerfile` | Dockerfile |
20+
21+Les extensions sont comparées sans tenir compte de la casse, et seule la dernière compte : `main.go.backup` n'est pas du Go.
22+
23+Un fichier dont l'extension ne décide de rien est ensuite cherché par son **nom**. Seuls les fichiers sans extension exploitable en ont besoin :
24+
25+| Nom | Langage |
26+| --- | --- |
27+| `Dockerfile`, `Containerfile` | Dockerfile |
28+
29+Un nom correspond sur sa totalité ou sur la partie précédant le premier point, sans tenir compte de la casse — ainsi `Dockerfile`, `dockerfile` et `Dockerfile.dev` sont tous reconnus, tandis que `Dockerfile.md` est du Markdown, puisque l'extension est consultée en premier.
30+
31+Un fichier qu'aucun des deux tableaux ne revendique est un **script shell** si sa première ligne est un shebang nommant un shell — `sh`, `bash`, `zsh`, `dash` ou `ksh`, comme élément de chemin ou comme argument d'`env`. C'est ce qui colore `configure`, un hook git, ou un script que quelqu'un a renommé.
32+
33+| Première ligne | Résultat |
34+| --- | --- |
35+| `#!/bin/sh` | Shell |
36+| `#!/usr/bin/env bash` | Shell |
37+| `#!/usr/bin/env -S bash -e` | Shell |
38+| `#!/usr/bin/env python3` | Non coloré |
39+| Tout ce qui ne commence pas par `#!` | Non coloré |
40+
41+L'ordre est fixe — extension, puis nom, puis première ligne — et le premier qui décide l'emporte : un fichier `.go` commençant par un shebang reste du Go.
42+
43+Tout le reste est affiché en texte brut. Ce n'est pas une erreur — ouvrir un PNG dans l'éditeur n'est pas une faute, c'est simplement non coloré.
44+
45+## Classes
46+
47+Tous les scanners produisent le même vocabulaire de classes, et chacune correspond à une clé de thème.
48+
49+| Classe | Clé de thème | Produite par |
50+| --- | --- | --- |
51+| `identifier` | `syntax.identifier` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
52+| `keyword` | `syntax.keyword` | Go, JavaScript, shell, HTML (doctype), XML, Dockerfile |
53+| `type` | `syntax.type` | Go, TOML (en-têtes de table), YAML (étiquettes) |
54+| `builtin` | `syntax.builtin` | Go, JavaScript, shell (builtins et expansions), YAML (ancres et alias), Dockerfile (variables) |
55+| `constant` | `syntax.constant` | Go, TOML, JavaScript, shell, YAML, HTML et XML (entités) |
56+| `function` | `syntax.function` | Go, JavaScript, shell (la commande) |
57+| `string` | `syntax.string` | tous |
58+| `char` | `syntax.char` | Go |
59+| `number` | `syntax.number` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
60+| `comment` | `syntax.comment` | Go, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile |
61+| `operator` | `syntax.operator` | Go, TOML, JavaScript, shell, HTML, YAML (en-têtes de scalaire de bloc), XML, Dockerfile |
62+| `punctuation` | `syntax.punctuation` | Go, TOML, JavaScript, shell, Markdown, YAML, Dockerfile |
63+| `heading` | `syntax.heading` | Markdown |
64+| `tag` | `syntax.tag` | HTML, XML |
65+| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (options) |
66+| `emphasis` | `syntax.emphasis` | Markdown |
67+| `link` | `syntax.link` | Markdown |
68+
69+## Go
70+
71+Tokenisé par `go/scanner`, le lexer qu'utilise la chaîne d'outils Go elle-même. Voir [Coloration et complétion](../explanation/colouring-and-completion.md).
72+
73+## TOML
74+
75+| Reconnu | Comme |
76+| --- | --- |
77+| `# commentaire` | comment |
78+| `[table]`, `[[array]]` | le nom en type, les crochets en punctuation |
79+| `clé =` | identifier, puis operator |
80+| `"basique"`, `'littérale'`, `"""multi-ligne"""`, `'''multi-ligne'''` | string |
81+| `true`, `false` | constant |
82+| nombres, dates, heures, `inf`, `nan` | number |
83+
84+## YAML
85+
86+Un fichier compose, un manifeste Kubernetes et un workflow d'intégration continue sont tous cela : il n'y a pas de dialecte séparé, parce qu'un dialecte serait le schéma de quelqu'un d'autre à maintenir en phase.
87+
88+| Reconnu | Comme |
89+| --- | --- |
90+| `# commentaire` | commentaire |
91+| `clé:` suivie d'une espace ou de la fin de ligne | la clé en identifiant, le deux-points en ponctuation |
92+| `"entre guillemets": 1`, `'apostrophes': 1` | la clé citée en identifiant |
93+| `- ` ouvrant une entrée de séquence | ponctuation |
94+| `"…"`, `'…'` | chaîne |
95+| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constante, quelle que soit la casse |
96+| nombres, dates et heures écrits sans guillemets | nombre |
97+| `&ancre`, `*alias` | builtin |
98+| `!!str`, `!Custom` | type |
99+| `---`, `...` | toute la ligne en ponctuation |
100+| `{`, `}`, `[`, `]`, `,` | ponctuation |
101+| `\|`, `>`, avec leurs indicateurs de coupe et d'indentation | l'en-tête en opérateur, le corps en chaîne |
102+
103+**Un deux-points n'est un séparateur que si une espace ou la fin de ligne le suit.** `image: nginx:1.27` est une clé et une seule valeur, et `url: http://example.com/x` une clé et une seule URL — colorer les deux-points intérieurs en séparateurs mettrait chaque étiquette d'image et chaque URL en trois couleurs.
104+
105+**L'étendue d'un scalaire de bloc est décidée par l'indentation**, pas par un délimiteur. La première ligne de contenu après `|` ou `>` fixe l'indentation du bloc ; toute ligne indentée au moins autant lui appartient, et la première qui ne l'est pas y met fin. **Une ligne vide à l'intérieur d'un bloc y reste** : un scalaire littéral conserve ses lignes vides, et terminer le bloc au premier saut de paragraphe couperait en deux un script shell dans un fichier d'intégration continue.
106+
107+**Un `#` a besoin d'une espace devant lui pour ouvrir un commentaire**, si bien que `colour: ff#00aa` est un seul scalaire.
108+
109+| Non reconnu | Parce que |
110+| --- | --- |
111+| Le schéma d'un fichier compose, d'un manifeste ou d'un workflow | Colorer `services:` autrement qu'une clé quelconque revient à transporter le schéma de quelqu'un d'autre, qui se périme le jour où il ajoute une clé |
112+| Les flux multi-documents comme documents distincts | `---` est coloré, mais rien n'est réinitialisé à cet endroit ; rien dans la coloration ne dépend des frontières de documents |
113+| Si un mot nu est une chaîne ou un nombre pour un analyseur | `1.2.3` est une version pour un lecteur et une chaîne pour YAML ; l'analyseur colore ce à quoi cela ressemble |
114+
115+## Markdown
116+
117+| Reconnu | Comme |
118+| --- | --- |
119+| `# Titre``###### Titre` | toute la ligne en heading |
120+| `**gras**`, `__gras__`, `*italique*`, `_italique_` | emphasis |
121+| `` `code` `` | string |
122+| `[texte](cible)`, `![alt](src)` | l'ensemble en link |
123+| `- `, `* `, `+ `, `1. `, `1) ` | le marqueur en punctuation |
124+| `>` | punctuation |
125+| `---`, `***`, `___` | punctuation |
126+| clôtures ` ``` ` et `~~~` | tout le bloc, lignes d'ouverture et de fermeture comprises, en string |
127+
128+Un bloc clôturé est **d'une seule couleur quel que soit le langage annoncé** : ```` ```go ```` ne colore pas son contenu en Go. La suite de marqueurs qui ouvre un bloc doit être fermée par le même caractère : une clôture en accents graves ne se ferme pas par des tildes. Une clôture non fermée colore jusqu'à la fin du fichier.
129+
130+La suite de marqueurs qui ouvre une emphase doit être fermée par une suite de même longueur, de sorte que `**gras**` fasse un seul span et non deux italiques.
131+
132+## JavaScript
133+
134+| Reconnu | Comme |
135+| --- | --- |
136+| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword |
137+| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant |
138+| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin |
139+| un nom immédiatement suivi de `(` | function |
140+| `"…"`, `'…'` | string |
141+| `` `` ``, interpolations comprises, sur plusieurs lignes | string |
142+| `//` jusqu'à la fin de la ligne, `/* … */` sur plusieurs lignes | comment |
143+| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number |
144+| suites de `+-*/%=<>!&|^~?:` | operator |
145+| `()[]{},;.` | punctuation |
146+
147+**Les littéraux d'expression régulière ne sont pas reconnus.** Distinguer `/x/g` d'une division exige de savoir si le token précédent pouvait terminer une expression ; une mauvaise supposition colore le reste de la ligne comme une chaîne, ce qui est pire que de laisser une regex à la couleur d'un opérateur.
148+
149+Les globales sont reconnues par leur nom : un fichier qui masque `Math` la voit quand même colorée comme un builtin — la même règle que les identifiants prédéclarés de Go.
150+
151+## HTML
152+
153+| Reconnu | Comme |
154+| --- | --- |
155+| `<balise`, `</balise`, `>`, `/>` | tag |
156+| noms d'attributs, dont `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute |
157+| `=` | operator |
158+| `"…"`, `'…'` | string |
159+| `<!-- … -->`, sur plusieurs lignes | comment |
160+| `&amp;`, `&#169;` | constant |
161+| `<!DOCTYPE …>` et les autres déclarations | keyword |
162+
163+Le texte entre balises n'est pas coloré. Une esperluette isolée sans `;` dans les 32 caractères qui suivent est laissée telle quelle, parce que c'est du texte légal.
164+
165+**Le contenu de `<script>` et de `<style>` n'est pas coloré** en JavaScript ni en CSS.
166+
167+## XML
168+
169+Son propre analyseur plutôt que celui du HTML, pour une raison qui compte : CDATA. Tout l'intérêt de `<![CDATA[ … ]]>` est que son contenu n'est *pas* du balisage, et colorer les balises qu'il contient comme des balises est exactement l'inverse.
170+
171+| Reconnu | Comme |
172+| --- | --- |
173+| `<?xml version="1.0"?>` et les autres instructions de traitement | la cible et `?>` en mot-clé, les paires entre les deux en attributs et chaînes |
174+| `<!DOCTYPE …>` et les autres formes `<!` | mot-clé |
175+| `<!-- … -->`, sur plusieurs lignes | commentaire |
176+| `<![CDATA[ … ]]>`, sur plusieurs lignes | chaîne |
177+| `<balise`, `</balise`, `>`, `/>` | balise |
178+| `<ns:balise>`, `xsi:type` | le préfixe et le nom local en **un seul** segment |
179+| les noms d'attributs | attribut |
180+| `=` | opérateur |
181+| `"…"`, `'…'` | chaîne |
182+| `&amp;`, `&#169;` | constante |
183+
184+**Un commentaire et une section CDATA se ferment sur des délimiteurs différents**, et sont transportés séparément : un `-->` à l'intérieur d'une section CDATA n'y met pas fin.
185+
186+**Une esperluette isolée sans point-virgule dans les 32 caractères suivants est laissée telle quelle**, parce que c'est du texte légal dans bien des documents et qu'avaler le reste de la ligne serait la plus grosse erreur.
187+
188+Le texte entre balises n'est pas coloré.
189+
190+## Shell
191+
192+S'applique indifféremment à `sh`, `bash` et `zsh` : les mots-clés reconnus sont ceux qu'ils partagent.
193+
194+| Reconnu | Comme |
195+| --- | --- |
196+| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword |
197+| `true`, `false` | constant |
198+| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin |
199+| `$NOM`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin |
200+| le **premier mot nu d'une ligne** | function |
201+| tout mot nu suivant, et `NOM` dans `NOM=valeur` | identifier |
202+| `'…'`, sans échappement ni expansion à l'intérieur | string |
203+| `"…"`, avec les expansions colorées comme telles | string |
204+| `#` jusqu'à la fin de la ligne | comment |
205+
206+`$(a $(b) c)` fait un seul span : l'imbrication est comptée. Une option comme `-euo` est un seul mot, et non un moins suivi d'un mot.
207+
208+**Les heredocs ne sont pas reconnus.** `<<EOF` et le texte qui suit sont colorés comme du shell ordinaire.
209+
210+## Dockerfile
211+
212+| Reconnu | Comme |
213+| --- | --- |
214+| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | mot-clé, quelle que soit la casse |
215+| `AS`, `NONE` | mot-clé |
216+| `# commentaire`, y compris les directives `# syntax=` et `# escape=` | commentaire |
217+| `--from=builder`, `--chown=me:me` | le nom de l'option en attribut |
218+| `$NOM`, `${NOM}`, `${NOM:-defaut}` | builtin, en un seul segment jusqu'à l'accolade fermante |
219+| `"…"`, `'…'` | chaîne |
220+| un `\` final | opérateur |
221+| les nombres | nombre |
222+| chemins et références d'images — `/usr/local/bin`, `golang:1.26-alpine` | identifiant, en **un seul** segment |
223+
224+**Seul le premier mot d'une ligne peut être une instruction**, et un mot qui n'en est pas une est un argument — ce qui garde le premier mot d'une ligne de continuation hors de la couleur des mots-clés.
225+
226+**Rien ne traverse un saut de ligne.** Un `\` joint deux lignes pour Docker, mais chaque moitié se lit encore comme une commande et est colorée pour elle-même.
227+
228+| Non reconnu | Parce que |
229+| --- | --- |
230+| Le shell à l'intérieur d'un `RUN` | Il faudrait passer l'analyseur shell sur une partie de ligne et en remonter les colonnes, et un `RUN` peut contenir n'importe quel langage |
231+| Les heredocs dans un `RUN` | La même raison que pour l'analyseur shell |
232+| Quelle étape nomme un `--from` | Rien ici ne lit le reste du fichier |
233+
234+## Voir aussi
235+
236+- [Format des fichiers de thème](themes.md) — toutes les clés vers lesquelles ces classes se résolvent
237+- [Coloration et complétion](../explanation/colouring-and-completion.md) — pourquoi les scanners sont écrits ainsi
238+- [Écrire son propre thème](../how-to/write-a-theme.md)
new file mode 100644
@@ -0,0 +1,238 @@
1+# Référence : langages colorés
2+
3+> Description neutre des fichiers que Turbo Go colore, de la façon dont il décide, et de ce que reconnaît chaque scanner.
4+
5+## Reconnaissance
6+
7+L'**extension** d'un fichier décide dès qu'elle fait partie de celles-ci :
8+
9+| Extension | Langage |
10+| --- | --- |
11+| `.go` | Go |
12+| `.toml` | TOML |
13+| `.yaml`, `.yml` | YAML |
14+| `.md`, `.markdown` | Markdown |
15+| `.js`, `.mjs`, `.cjs` | JavaScript |
16+| `.html`, `.htm` | HTML |
17+| `.xml`, `.xsd`, `.xsl`, `.xslt`, `.svg`, `.plist`, `.csproj`, `.pom` | XML |
18+| `.sh`, `.bash`, `.zsh` | Shell |
19+| `.dockerfile`, `.containerfile` | Dockerfile |
20+
21+Les extensions sont comparées sans tenir compte de la casse, et seule la dernière compte : `main.go.backup` n'est pas du Go.
22+
23+Un fichier dont l'extension ne décide de rien est ensuite cherché par son **nom**. Seuls les fichiers sans extension exploitable en ont besoin :
24+
25+| Nom | Langage |
26+| --- | --- |
27+| `Dockerfile`, `Containerfile` | Dockerfile |
28+
29+Un nom correspond sur sa totalité ou sur la partie précédant le premier point, sans tenir compte de la casse — ainsi `Dockerfile`, `dockerfile` et `Dockerfile.dev` sont tous reconnus, tandis que `Dockerfile.md` est du Markdown, puisque l'extension est consultée en premier.
30+
31+Un fichier qu'aucun des deux tableaux ne revendique est un **script shell** si sa première ligne est un shebang nommant un shell — `sh`, `bash`, `zsh`, `dash` ou `ksh`, comme élément de chemin ou comme argument d'`env`. C'est ce qui colore `configure`, un hook git, ou un script que quelqu'un a renommé.
32+
33+| Première ligne | Résultat |
34+| --- | --- |
35+| `#!/bin/sh` | Shell |
36+| `#!/usr/bin/env bash` | Shell |
37+| `#!/usr/bin/env -S bash -e` | Shell |
38+| `#!/usr/bin/env python3` | Non coloré |
39+| Tout ce qui ne commence pas par `#!` | Non coloré |
40+
41+L'ordre est fixe — extension, puis nom, puis première ligne — et le premier qui décide l'emporte : un fichier `.go` commençant par un shebang reste du Go.
42+
43+Tout le reste est affiché en texte brut. Ce n'est pas une erreur — ouvrir un PNG dans l'éditeur n'est pas une faute, c'est simplement non coloré.
44+
45+## Classes
46+
47+Tous les scanners produisent le même vocabulaire de classes, et chacune correspond à une clé de thème.
48+
49+| Classe | Clé de thème | Produite par |
50+| --- | --- | --- |
51+| `identifier` | `syntax.identifier` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
52+| `keyword` | `syntax.keyword` | Go, JavaScript, shell, HTML (doctype), XML, Dockerfile |
53+| `type` | `syntax.type` | Go, TOML (en-têtes de table), YAML (étiquettes) |
54+| `builtin` | `syntax.builtin` | Go, JavaScript, shell (builtins et expansions), YAML (ancres et alias), Dockerfile (variables) |
55+| `constant` | `syntax.constant` | Go, TOML, JavaScript, shell, YAML, HTML et XML (entités) |
56+| `function` | `syntax.function` | Go, JavaScript, shell (la commande) |
57+| `string` | `syntax.string` | tous |
58+| `char` | `syntax.char` | Go |
59+| `number` | `syntax.number` | Go, TOML, JavaScript, shell, YAML, Dockerfile |
60+| `comment` | `syntax.comment` | Go, TOML, JavaScript, shell, HTML, YAML, XML, Dockerfile |
61+| `operator` | `syntax.operator` | Go, TOML, JavaScript, shell, HTML, YAML (en-têtes de scalaire de bloc), XML, Dockerfile |
62+| `punctuation` | `syntax.punctuation` | Go, TOML, JavaScript, shell, Markdown, YAML, Dockerfile |
63+| `heading` | `syntax.heading` | Markdown |
64+| `tag` | `syntax.tag` | HTML, XML |
65+| `attribute` | `syntax.attribute` | HTML, XML, Dockerfile (options) |
66+| `emphasis` | `syntax.emphasis` | Markdown |
67+| `link` | `syntax.link` | Markdown |
68+
69+## Go
70+
71+Tokenisé par `go/scanner`, le lexer qu'utilise la chaîne d'outils Go elle-même. Voir [Coloration et complétion](../explanation/colouring-and-completion.md).
72+
73+## TOML
74+
75+| Reconnu | Comme |
76+| --- | --- |
77+| `# commentaire` | comment |
78+| `[table]`, `[[array]]` | le nom en type, les crochets en punctuation |
79+| `clé =` | identifier, puis operator |
80+| `"basique"`, `'littérale'`, `"""multi-ligne"""`, `'''multi-ligne'''` | string |
81+| `true`, `false` | constant |
82+| nombres, dates, heures, `inf`, `nan` | number |
83+
84+## YAML
85+
86+Un fichier compose, un manifeste Kubernetes et un workflow d'intégration continue sont tous cela : il n'y a pas de dialecte séparé, parce qu'un dialecte serait le schéma de quelqu'un d'autre à maintenir en phase.
87+
88+| Reconnu | Comme |
89+| --- | --- |
90+| `# commentaire` | commentaire |
91+| `clé:` suivie d'une espace ou de la fin de ligne | la clé en identifiant, le deux-points en ponctuation |
92+| `"entre guillemets": 1`, `'apostrophes': 1` | la clé citée en identifiant |
93+| `- ` ouvrant une entrée de séquence | ponctuation |
94+| `"…"`, `'…'` | chaîne |
95+| `true`, `false`, `yes`, `no`, `on`, `off`, `null` | constante, quelle que soit la casse |
96+| nombres, dates et heures écrits sans guillemets | nombre |
97+| `&ancre`, `*alias` | builtin |
98+| `!!str`, `!Custom` | type |
99+| `---`, `...` | toute la ligne en ponctuation |
100+| `{`, `}`, `[`, `]`, `,` | ponctuation |
101+| `\|`, `>`, avec leurs indicateurs de coupe et d'indentation | l'en-tête en opérateur, le corps en chaîne |
102+
103+**Un deux-points n'est un séparateur que si une espace ou la fin de ligne le suit.** `image: nginx:1.27` est une clé et une seule valeur, et `url: http://example.com/x` une clé et une seule URL — colorer les deux-points intérieurs en séparateurs mettrait chaque étiquette d'image et chaque URL en trois couleurs.
104+
105+**L'étendue d'un scalaire de bloc est décidée par l'indentation**, pas par un délimiteur. La première ligne de contenu après `|` ou `>` fixe l'indentation du bloc ; toute ligne indentée au moins autant lui appartient, et la première qui ne l'est pas y met fin. **Une ligne vide à l'intérieur d'un bloc y reste** : un scalaire littéral conserve ses lignes vides, et terminer le bloc au premier saut de paragraphe couperait en deux un script shell dans un fichier d'intégration continue.
106+
107+**Un `#` a besoin d'une espace devant lui pour ouvrir un commentaire**, si bien que `colour: ff#00aa` est un seul scalaire.
108+
109+| Non reconnu | Parce que |
110+| --- | --- |
111+| Le schéma d'un fichier compose, d'un manifeste ou d'un workflow | Colorer `services:` autrement qu'une clé quelconque revient à transporter le schéma de quelqu'un d'autre, qui se périme le jour où il ajoute une clé |
112+| Les flux multi-documents comme documents distincts | `---` est coloré, mais rien n'est réinitialisé à cet endroit ; rien dans la coloration ne dépend des frontières de documents |
113+| Si un mot nu est une chaîne ou un nombre pour un analyseur | `1.2.3` est une version pour un lecteur et une chaîne pour YAML ; l'analyseur colore ce à quoi cela ressemble |
114+
115+## Markdown
116+
117+| Reconnu | Comme |
118+| --- | --- |
119+| `# Titre``###### Titre` | toute la ligne en heading |
120+| `**gras**`, `__gras__`, `*italique*`, `_italique_` | emphasis |
121+| `` `code` `` | string |
122+| `[texte](cible)`, `![alt](src)` | l'ensemble en link |
123+| `- `, `* `, `+ `, `1. `, `1) ` | le marqueur en punctuation |
124+| `>` | punctuation |
125+| `---`, `***`, `___` | punctuation |
126+| clôtures ` ``` ` et `~~~` | tout le bloc, lignes d'ouverture et de fermeture comprises, en string |
127+
128+Un bloc clôturé est **d'une seule couleur quel que soit le langage annoncé** : ```` ```go ```` ne colore pas son contenu en Go. La suite de marqueurs qui ouvre un bloc doit être fermée par le même caractère : une clôture en accents graves ne se ferme pas par des tildes. Une clôture non fermée colore jusqu'à la fin du fichier.
129+
130+La suite de marqueurs qui ouvre une emphase doit être fermée par une suite de même longueur, de sorte que `**gras**` fasse un seul span et non deux italiques.
131+
132+## JavaScript
133+
134+| Reconnu | Comme |
135+| --- | --- |
136+| `const`, `let`, `function`, `class`, `async`, `await`, `import`, `export`, … | keyword |
137+| `true`, `false`, `null`, `undefined`, `NaN`, `Infinity`, `this` | constant |
138+| `console`, `document`, `window`, `Array`, `Object`, `Promise`, `Math`, `JSON`, … | builtin |
139+| un nom immédiatement suivi de `(` | function |
140+| `"…"`, `'…'` | string |
141+| `` `` ``, interpolations comprises, sur plusieurs lignes | string |
142+| `//` jusqu'à la fin de la ligne, `/* … */` sur plusieurs lignes | comment |
143+| `42`, `3.14`, `0x1f`, `0b1010`, `0o777`, `1_000_000`, `1e6`, `10n` | number |
144+| suites de `+-*/%=<>!&|^~?:` | operator |
145+| `()[]{},;.` | punctuation |
146+
147+**Les littéraux d'expression régulière ne sont pas reconnus.** Distinguer `/x/g` d'une division exige de savoir si le token précédent pouvait terminer une expression ; une mauvaise supposition colore le reste de la ligne comme une chaîne, ce qui est pire que de laisser une regex à la couleur d'un opérateur.
148+
149+Les globales sont reconnues par leur nom : un fichier qui masque `Math` la voit quand même colorée comme un builtin — la même règle que les identifiants prédéclarés de Go.
150+
151+## HTML
152+
153+| Reconnu | Comme |
154+| --- | --- |
155+| `<balise`, `</balise`, `>`, `/>` | tag |
156+| noms d'attributs, dont `data-*`, `xlink:href`, `@click`, `v-bind.prop` | attribute |
157+| `=` | operator |
158+| `"…"`, `'…'` | string |
159+| `<!-- … -->`, sur plusieurs lignes | comment |
160+| `&amp;`, `&#169;` | constant |
161+| `<!DOCTYPE …>` et les autres déclarations | keyword |
162+
163+Le texte entre balises n'est pas coloré. Une esperluette isolée sans `;` dans les 32 caractères qui suivent est laissée telle quelle, parce que c'est du texte légal.
164+
165+**Le contenu de `<script>` et de `<style>` n'est pas coloré** en JavaScript ni en CSS.
166+
167+## XML
168+
169+Son propre analyseur plutôt que celui du HTML, pour une raison qui compte : CDATA. Tout l'intérêt de `<![CDATA[ … ]]>` est que son contenu n'est *pas* du balisage, et colorer les balises qu'il contient comme des balises est exactement l'inverse.
170+
171+| Reconnu | Comme |
172+| --- | --- |
173+| `<?xml version="1.0"?>` et les autres instructions de traitement | la cible et `?>` en mot-clé, les paires entre les deux en attributs et chaînes |
174+| `<!DOCTYPE …>` et les autres formes `<!` | mot-clé |
175+| `<!-- … -->`, sur plusieurs lignes | commentaire |
176+| `<![CDATA[ … ]]>`, sur plusieurs lignes | chaîne |
177+| `<balise`, `</balise`, `>`, `/>` | balise |
178+| `<ns:balise>`, `xsi:type` | le préfixe et le nom local en **un seul** segment |
179+| les noms d'attributs | attribut |
180+| `=` | opérateur |
181+| `"…"`, `'…'` | chaîne |
182+| `&amp;`, `&#169;` | constante |
183+
184+**Un commentaire et une section CDATA se ferment sur des délimiteurs différents**, et sont transportés séparément : un `-->` à l'intérieur d'une section CDATA n'y met pas fin.
185+
186+**Une esperluette isolée sans point-virgule dans les 32 caractères suivants est laissée telle quelle**, parce que c'est du texte légal dans bien des documents et qu'avaler le reste de la ligne serait la plus grosse erreur.
187+
188+Le texte entre balises n'est pas coloré.
189+
190+## Shell
191+
192+S'applique indifféremment à `sh`, `bash` et `zsh` : les mots-clés reconnus sont ceux qu'ils partagent.
193+
194+| Reconnu | Comme |
195+| --- | --- |
196+| `if`, `then`, `fi`, `for`, `while`, `case`, `esac`, `function`, `return`, … | keyword |
197+| `true`, `false` | constant |
198+| `echo`, `printf`, `export`, `local`, `read`, `cd`, `set`, `source`, … | builtin |
199+| `$NOM`, `${…}`, `$(…)`, `$1`, `$?`, `$@` | builtin |
200+| le **premier mot nu d'une ligne** | function |
201+| tout mot nu suivant, et `NOM` dans `NOM=valeur` | identifier |
202+| `'…'`, sans échappement ni expansion à l'intérieur | string |
203+| `"…"`, avec les expansions colorées comme telles | string |
204+| `#` jusqu'à la fin de la ligne | comment |
205+
206+`$(a $(b) c)` fait un seul span : l'imbrication est comptée. Une option comme `-euo` est un seul mot, et non un moins suivi d'un mot.
207+
208+**Les heredocs ne sont pas reconnus.** `<<EOF` et le texte qui suit sont colorés comme du shell ordinaire.
209+
210+## Dockerfile
211+
212+| Reconnu | Comme |
213+| --- | --- |
214+| `FROM`, `RUN`, `COPY`, `ADD`, `ARG`, `ENV`, `CMD`, `ENTRYPOINT`, `EXPOSE`, `LABEL`, `USER`, `VOLUME`, `WORKDIR`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `MAINTAINER` | mot-clé, quelle que soit la casse |
215+| `AS`, `NONE` | mot-clé |
216+| `# commentaire`, y compris les directives `# syntax=` et `# escape=` | commentaire |
217+| `--from=builder`, `--chown=me:me` | le nom de l'option en attribut |
218+| `$NOM`, `${NOM}`, `${NOM:-defaut}` | builtin, en un seul segment jusqu'à l'accolade fermante |
219+| `"…"`, `'…'` | chaîne |
220+| un `\` final | opérateur |
221+| les nombres | nombre |
222+| chemins et références d'images — `/usr/local/bin`, `golang:1.26-alpine` | identifiant, en **un seul** segment |
223+
224+**Seul le premier mot d'une ligne peut être une instruction**, et un mot qui n'en est pas une est un argument — ce qui garde le premier mot d'une ligne de continuation hors de la couleur des mots-clés.
225+
226+**Rien ne traverse un saut de ligne.** Un `\` joint deux lignes pour Docker, mais chaque moitié se lit encore comme une commande et est colorée pour elle-même.
227+
228+| Non reconnu | Parce que |
229+| --- | --- |
230+| Le shell à l'intérieur d'un `RUN` | Il faudrait passer l'analyseur shell sur une partie de ligne et en remonter les colonnes, et un `RUN` peut contenir n'importe quel langage |
231+| Les heredocs dans un `RUN` | La même raison que pour l'analyseur shell |
232+| Quelle étape nomme un `--from` | Rien ici ne lit le reste du fichier |
233+
234+## Voir aussi
235+
236+- [Format des fichiers de thème](themes.md) — toutes les clés vers lesquelles ces classes se résolvent
237+- [Coloration et complétion](../explanation/colouring-and-completion.md) — pourquoi les scanners sont écrits ainsi
238+- [Écrire son propre thème](../how-to/write-a-theme.md)
added docs/fr/reference/menus.md +150 -0
new file mode 100644
@@ -0,0 +1,150 @@
1+# Référence : menus
2+
3+> Liste complète des entrées de la barre de menus, avec leurs raccourcis et les conditions dans lesquelles elles peuvent être choisies.
4+
5+Une entrée marquée **fichier requis** est grisée quand aucune fenêtre n'est ouverte.
6+
7+La barre porte toujours File, Edit, Search, Run, Code, Options, Window, Snippets, Go et Help, dans cet ordre. Le fichier d'outils d'un projet peut y ajouter ses propres menus, entre Go et Help ; ils sont décrits sous [Menus du projet](#menus-du-projet) plus bas.
8+
9+## File
10+
11+| Entrée | Raccourci | Fichier requis | Effet |
12+| --- | --- | --- | --- |
13+| New | `F4` | | Ouvrir une fenêtre vide sans titre |
14+| Open… | `F3` | | Ouvrir l'explorateur de fichiers et ouvrir ce qui est choisi |
15+| Save | `F2` | oui | Écrire le fichier ; demande un nom s'il n'en a pas |
16+| Save as… | | oui | Demander un nom et écrire dedans, en adoptant ce chemin |
17+| Close | `Ctrl-W` | oui | Fermer la fenêtre courante, en proposant d'enregistrer d'abord |
18+| Exit | `Alt-X` | | Quitter l'éditeur, en proposant d'enregistrer chaque fichier modifié |
19+
20+## Edit
21+
22+| Entrée | Raccourci | Fichier requis | Effet |
23+| --- | --- | --- | --- |
24+| Undo | `Ctrl-Z` | oui | Annuler la dernière modification |
25+| Redo | `Ctrl-R` | oui | Rétablir la dernière modification annulée. **`Ctrl-Y` le faisait avant** ; il supprime désormais une ligne, comme dans Turbo C. |
26+| Insert line | `Ctrl-N` | oui | Ouvrir une ligne vide au-dessus du curseur, en laissant le curseur sur son propre texte |
27+| Delete line | `Ctrl-Y` | oui | Supprimer la ligne où est le curseur. Le curseur reste au même numéro de ligne : maintenir la touche en supprime une série. |
28+| Cut | `Maj-Suppr` | oui | Copier la sélection dans le presse-papier et la supprimer |
29+| Copy | `Ctrl-Inser` | oui | Copier la sélection dans le presse-papier |
30+| Paste | `Maj-Inser` | oui | Insérer le presse-papier, en remplaçant la sélection |
31+| Select all | `Ctrl-A` | oui | Sélectionner tout le fichier |
32+
33+Le presse-papier est partagé entre toutes les fenêtres d'une même session.
34+
35+## Search
36+
37+| Entrée | Raccourci | Fichier requis | Effet |
38+| --- | --- | --- | --- |
39+| Find… | `Ctrl-F` | oui | Demander un texte et des options, puis aller à la première occurrence |
40+| Find next | `F7` | oui | Aller à l'occurrence suivante, en bouclant en fin de fichier |
41+| Find previous | `Maj-F7` | oui | Aller à l'occurrence précédente, en bouclant en début de fichier |
42+| Go to line… | `Ctrl-G` | oui | Demander un numéro de ligne, à partir de un |
43+
44+## Run
45+
46+| Entrée | Raccourci | Fichier requis | Effet |
47+| --- | --- | --- | --- |
48+| Completion | `Ctrl-Espace` | oui | Ouvrir la liste de complétion au curseur |
49+| Language server status | | | Indiquer si un serveur de langage tourne, et ce qu'il fait |
50+
51+## Code
52+
53+Tout ce que l'éditeur demande au serveur de langage à propos du symbole **sous le curseur**. Rien ici n'exige de sélection : presque toutes les requêtes du protocole prennent une position et non une plage, il n'y a donc rien de plus à dire en sélectionnant d'abord.
54+
55+Sa touche d'accès est `Alt-C`.
56+
57+| Entrée | Raccourci | Fichier requis | Effet |
58+| --- | --- | --- | --- |
59+| Describe symbol | `F1` | oui | Montre ce que le serveur de langage sait du symbole sous le curseur |
60+| Go to definition | `F12` | oui | Où le symbole est déclaré. Une seule réponse l'ouvre ; plusieurs proposent la liste. |
61+| Go to type definition | | oui | Où le *type* du symbole est déclaré, ce qui est une autre question |
62+| Find implementations… | | oui | Ce qui l'implémente : les types satisfaisant une interface, les blocs `impl` d'un trait |
63+| Find references… | `Shift-F12` | oui | Où il est utilisé, sa déclaration comprise |
64+| Symbol in file… | | oui | Le plan du fichier, indenté, avec la sorte de chaque symbole. En choisir un y va. |
65+| Symbol in project… | `Ctrl-T` | non | Demande un nom et cherche dans tout le projet |
66+| Problems… | | non | Tous les problèmes signalés par le serveur, pour tous les fichiers dont il a parlé |
67+
68+Une liste de lieux montre le fichier, la ligne, et le texte de cette ligne — douze entrées affichant `handler.go:42` ne disent rien de celle qu'on veut. Le texte vient d'une fenêtre ouverte quand il y en a une : un fichier modifié et non enregistré est donc listé tel qu'il se lit maintenant.
69+
70+Trois réponses sont distinguées, et la différence compte : **rien trouvé** le dit avec les mots de la question (`No references found`), **le serveur n'est pas prêt** montre ce qu'il est en train de faire, et **une seule réponse** vous y emmène sans dialogue.
71+
72+*Symbol in file* n'a pas de raccourci, à dessein. Le raccourci évident serait `Ctrl-Shift-O`, et un terminal ne sait pas le distinguer de `Ctrl-O` — la touche Maj est perdue avant que l'éditeur ne la voie.
73+
74+## Options
75+
76+| Entrée | Fichier requis | Effet |
77+| --- | --- | --- |
78+| Theme… | | Lister tous les thèmes chargeables et appliquer immédiatement celui choisi. Si un fichier de réglages de projet existe, y écrit aussi le choix. |
79+| Line numbers | oui | Afficher ou masquer la gouttière dans la fenêtre courante |
80+| Create project settings | | Écrire `.turbo-go/settings.toml` avec le thème en cours, et l'ouvrir. **Grisée dès que le projet en a un.** |
81+| Project settings… | | Ouvrir `.turbo-go/settings.toml`. **Grisée tant que le projet n'en a pas.** |
82+
83+## Window
84+
85+| Entrée | Raccourci | Fichier requis | Effet |
86+| --- | --- | --- | --- |
87+| Next | `F6` | oui | Passer au premier plan la fenêtre située derrière |
88+| New terminal | `F8` | non | Ouvrir une fenêtre exécutant un shell, dans le dossier du fichier au premier plan |
89+| Project tree | `F9` | non | Ouvrir une fenêtre montrant les fichiers du projet ; ramène l'existante au premier plan s'il y en a une |
90+| Tile | | oui | Disposer toutes les fenêtres en grille, sans recouvrement |
91+| Cascade | | oui | Empiler les fenêtres décalées, tous les titres visibles |
92+| Maximise | | oui | Donner tout le bureau à la fenêtre courante, ou la remettre où elle était si elle l'a déjà. Le même bascule que la case `[■]` sur le cadre de la fenêtre. |
93+| List… | `Alt-0` | oui | Lister les fenêtres ouvertes et passer la choisie au premier plan |
94+
95+## Snippets
96+
97+Construit depuis `.turbo-go/snippets.toml` et votre propre fichier à chaque ouverture. Sa touche d'accès est `Alt-N`, parce que Search répond déjà au S.
98+
99+| Entrée | Fichier requis | Effet |
100+| --- | --- | --- |
101+| Un sous-menu par groupe | | Insérer le snippet choisi au curseur ; les entrées exigent un fichier ouvert |
102+| Create snippets file | non | Écrire `.turbo-go/snippets.toml` avec des exemples travaillés, puis l'ouvrir. **Grisée dès que le projet en a un.** |
103+| Open snippets file | non | Ouvrir `.turbo-go/snippets.toml`. **Grisée tant que le projet n'en a pas.** Toujours le fichier du projet, jamais le vôtre. |
104+
105+Voir [Snippets](snippets.md).
106+
107+## Go
108+
109+Construit depuis `.turbo-go/tools.toml` à chaque ouverture. Sa touche d'accès est `Alt-G`.
110+
111+| Entrée | Effet |
112+| --- | --- |
113+| Une ligne par outil ne nommant aucun `menu` | Lancer cette commande, en montrant sa sortie là où l'outil le demande : une popup, une fenêtre terminal, ou une fenêtre d'édition |
114+| Create tools file | Écrire `.turbo-go/tools.toml` avec les cinq commandes Go, puis l'ouvrir. **Grisée dès que le projet en a un.** |
115+| Open tools file | Ouvrir `.turbo-go/tools.toml`. **Grisée tant que le projet n'en a pas.** |
116+
117+Voir [Outils go](go-tools.md).
118+
119+## Menus du projet
120+
121+Non fixes : un menu par nom de `menu` dans `.turbo-go/tools.toml`, dans l'ordre où les noms y apparaissent pour la première fois, entre Go et Help. Un projet sans fichier d'outils, ou dont tous les outils restent dans Go, n'en a aucun.
122+
123+| Entrée | Effet |
124+| --- | --- |
125+| Une ligne par outil nommant ce menu | Lancer cette commande, en montrant sa sortie là où l'outil le demande |
126+
127+Leurs touches d'accès sont attribuées et non fixées, pour qu'un nom venu d'un fichier ne puisse jamais prendre une lettre à laquelle un des menus ci-dessus répond déjà. Les règles sont dans [Outils go](go-tools.md#touches-daccès).
128+
129+## Help
130+
131+| Entrée | Effet |
132+| --- | --- |
133+| Keyboard | Afficher les touches à connaître |
134+| About | Afficher la version, le commit et la date de build quand le build les a enregistrés, et le thème courant. Voir [le numéro de version](versioning.md). |
135+
136+## Barre d'état
137+
138+Les indices du bas sont cliquables et exécutent les mêmes actions.
139+
140+| Indice | Action |
141+| --- | --- |
142+| `F1 Describe` | Code ▸ Describe symbol |
143+| `F2 Save` | File ▸ Save |
144+| `F3 Open` | File ▸ Open… |
145+| `F6 Window` | Window ▸ Next |
146+| `F7 Next` | Search ▸ Find next |
147+| `F10 Menu` | Ouvrir la barre de menus |
148+| `Alt-X Exit` | File ▸ Exit |
149+
150+L'extrémité droite affiche, dans cet ordre : le curseur sous la forme `ligne:colonne`, puis soit la première erreur signalée par le serveur de langage pour ce fichier (précédée de `⚠`), soit l'état du serveur de langage.
new file mode 100644
@@ -0,0 +1,150 @@
1+# Référence : menus
2+
3+> Liste complète des entrées de la barre de menus, avec leurs raccourcis et les conditions dans lesquelles elles peuvent être choisies.
4+
5+Une entrée marquée **fichier requis** est grisée quand aucune fenêtre n'est ouverte.
6+
7+La barre porte toujours File, Edit, Search, Run, Code, Options, Window, Snippets, Go et Help, dans cet ordre. Le fichier d'outils d'un projet peut y ajouter ses propres menus, entre Go et Help ; ils sont décrits sous [Menus du projet](#menus-du-projet) plus bas.
8+
9+## File
10+
11+| Entrée | Raccourci | Fichier requis | Effet |
12+| --- | --- | --- | --- |
13+| New | `F4` | | Ouvrir une fenêtre vide sans titre |
14+| Open… | `F3` | | Ouvrir l'explorateur de fichiers et ouvrir ce qui est choisi |
15+| Save | `F2` | oui | Écrire le fichier ; demande un nom s'il n'en a pas |
16+| Save as… | | oui | Demander un nom et écrire dedans, en adoptant ce chemin |
17+| Close | `Ctrl-W` | oui | Fermer la fenêtre courante, en proposant d'enregistrer d'abord |
18+| Exit | `Alt-X` | | Quitter l'éditeur, en proposant d'enregistrer chaque fichier modifié |
19+
20+## Edit
21+
22+| Entrée | Raccourci | Fichier requis | Effet |
23+| --- | --- | --- | --- |
24+| Undo | `Ctrl-Z` | oui | Annuler la dernière modification |
25+| Redo | `Ctrl-R` | oui | Rétablir la dernière modification annulée. **`Ctrl-Y` le faisait avant** ; il supprime désormais une ligne, comme dans Turbo C. |
26+| Insert line | `Ctrl-N` | oui | Ouvrir une ligne vide au-dessus du curseur, en laissant le curseur sur son propre texte |
27+| Delete line | `Ctrl-Y` | oui | Supprimer la ligne où est le curseur. Le curseur reste au même numéro de ligne : maintenir la touche en supprime une série. |
28+| Cut | `Maj-Suppr` | oui | Copier la sélection dans le presse-papier et la supprimer |
29+| Copy | `Ctrl-Inser` | oui | Copier la sélection dans le presse-papier |
30+| Paste | `Maj-Inser` | oui | Insérer le presse-papier, en remplaçant la sélection |
31+| Select all | `Ctrl-A` | oui | Sélectionner tout le fichier |
32+
33+Le presse-papier est partagé entre toutes les fenêtres d'une même session.
34+
35+## Search
36+
37+| Entrée | Raccourci | Fichier requis | Effet |
38+| --- | --- | --- | --- |
39+| Find… | `Ctrl-F` | oui | Demander un texte et des options, puis aller à la première occurrence |
40+| Find next | `F7` | oui | Aller à l'occurrence suivante, en bouclant en fin de fichier |
41+| Find previous | `Maj-F7` | oui | Aller à l'occurrence précédente, en bouclant en début de fichier |
42+| Go to line… | `Ctrl-G` | oui | Demander un numéro de ligne, à partir de un |
43+
44+## Run
45+
46+| Entrée | Raccourci | Fichier requis | Effet |
47+| --- | --- | --- | --- |
48+| Completion | `Ctrl-Espace` | oui | Ouvrir la liste de complétion au curseur |
49+| Language server status | | | Indiquer si un serveur de langage tourne, et ce qu'il fait |
50+
51+## Code
52+
53+Tout ce que l'éditeur demande au serveur de langage à propos du symbole **sous le curseur**. Rien ici n'exige de sélection : presque toutes les requêtes du protocole prennent une position et non une plage, il n'y a donc rien de plus à dire en sélectionnant d'abord.
54+
55+Sa touche d'accès est `Alt-C`.
56+
57+| Entrée | Raccourci | Fichier requis | Effet |
58+| --- | --- | --- | --- |
59+| Describe symbol | `F1` | oui | Montre ce que le serveur de langage sait du symbole sous le curseur |
60+| Go to definition | `F12` | oui | Où le symbole est déclaré. Une seule réponse l'ouvre ; plusieurs proposent la liste. |
61+| Go to type definition | | oui | Où le *type* du symbole est déclaré, ce qui est une autre question |
62+| Find implementations… | | oui | Ce qui l'implémente : les types satisfaisant une interface, les blocs `impl` d'un trait |
63+| Find references… | `Shift-F12` | oui | Où il est utilisé, sa déclaration comprise |
64+| Symbol in file… | | oui | Le plan du fichier, indenté, avec la sorte de chaque symbole. En choisir un y va. |
65+| Symbol in project… | `Ctrl-T` | non | Demande un nom et cherche dans tout le projet |
66+| Problems… | | non | Tous les problèmes signalés par le serveur, pour tous les fichiers dont il a parlé |
67+
68+Une liste de lieux montre le fichier, la ligne, et le texte de cette ligne — douze entrées affichant `handler.go:42` ne disent rien de celle qu'on veut. Le texte vient d'une fenêtre ouverte quand il y en a une : un fichier modifié et non enregistré est donc listé tel qu'il se lit maintenant.
69+
70+Trois réponses sont distinguées, et la différence compte : **rien trouvé** le dit avec les mots de la question (`No references found`), **le serveur n'est pas prêt** montre ce qu'il est en train de faire, et **une seule réponse** vous y emmène sans dialogue.
71+
72+*Symbol in file* n'a pas de raccourci, à dessein. Le raccourci évident serait `Ctrl-Shift-O`, et un terminal ne sait pas le distinguer de `Ctrl-O` — la touche Maj est perdue avant que l'éditeur ne la voie.
73+
74+## Options
75+
76+| Entrée | Fichier requis | Effet |
77+| --- | --- | --- |
78+| Theme… | | Lister tous les thèmes chargeables et appliquer immédiatement celui choisi. Si un fichier de réglages de projet existe, y écrit aussi le choix. |
79+| Line numbers | oui | Afficher ou masquer la gouttière dans la fenêtre courante |
80+| Create project settings | | Écrire `.turbo-go/settings.toml` avec le thème en cours, et l'ouvrir. **Grisée dès que le projet en a un.** |
81+| Project settings… | | Ouvrir `.turbo-go/settings.toml`. **Grisée tant que le projet n'en a pas.** |
82+
83+## Window
84+
85+| Entrée | Raccourci | Fichier requis | Effet |
86+| --- | --- | --- | --- |
87+| Next | `F6` | oui | Passer au premier plan la fenêtre située derrière |
88+| New terminal | `F8` | non | Ouvrir une fenêtre exécutant un shell, dans le dossier du fichier au premier plan |
89+| Project tree | `F9` | non | Ouvrir une fenêtre montrant les fichiers du projet ; ramène l'existante au premier plan s'il y en a une |
90+| Tile | | oui | Disposer toutes les fenêtres en grille, sans recouvrement |
91+| Cascade | | oui | Empiler les fenêtres décalées, tous les titres visibles |
92+| Maximise | | oui | Donner tout le bureau à la fenêtre courante, ou la remettre où elle était si elle l'a déjà. Le même bascule que la case `[■]` sur le cadre de la fenêtre. |
93+| List… | `Alt-0` | oui | Lister les fenêtres ouvertes et passer la choisie au premier plan |
94+
95+## Snippets
96+
97+Construit depuis `.turbo-go/snippets.toml` et votre propre fichier à chaque ouverture. Sa touche d'accès est `Alt-N`, parce que Search répond déjà au S.
98+
99+| Entrée | Fichier requis | Effet |
100+| --- | --- | --- |
101+| Un sous-menu par groupe | | Insérer le snippet choisi au curseur ; les entrées exigent un fichier ouvert |
102+| Create snippets file | non | Écrire `.turbo-go/snippets.toml` avec des exemples travaillés, puis l'ouvrir. **Grisée dès que le projet en a un.** |
103+| Open snippets file | non | Ouvrir `.turbo-go/snippets.toml`. **Grisée tant que le projet n'en a pas.** Toujours le fichier du projet, jamais le vôtre. |
104+
105+Voir [Snippets](snippets.md).
106+
107+## Go
108+
109+Construit depuis `.turbo-go/tools.toml` à chaque ouverture. Sa touche d'accès est `Alt-G`.
110+
111+| Entrée | Effet |
112+| --- | --- |
113+| Une ligne par outil ne nommant aucun `menu` | Lancer cette commande, en montrant sa sortie là où l'outil le demande : une popup, une fenêtre terminal, ou une fenêtre d'édition |
114+| Create tools file | Écrire `.turbo-go/tools.toml` avec les cinq commandes Go, puis l'ouvrir. **Grisée dès que le projet en a un.** |
115+| Open tools file | Ouvrir `.turbo-go/tools.toml`. **Grisée tant que le projet n'en a pas.** |
116+
117+Voir [Outils go](go-tools.md).
118+
119+## Menus du projet
120+
121+Non fixes : un menu par nom de `menu` dans `.turbo-go/tools.toml`, dans l'ordre où les noms y apparaissent pour la première fois, entre Go et Help. Un projet sans fichier d'outils, ou dont tous les outils restent dans Go, n'en a aucun.
122+
123+| Entrée | Effet |
124+| --- | --- |
125+| Une ligne par outil nommant ce menu | Lancer cette commande, en montrant sa sortie là où l'outil le demande |
126+
127+Leurs touches d'accès sont attribuées et non fixées, pour qu'un nom venu d'un fichier ne puisse jamais prendre une lettre à laquelle un des menus ci-dessus répond déjà. Les règles sont dans [Outils go](go-tools.md#touches-daccès).
128+
129+## Help
130+
131+| Entrée | Effet |
132+| --- | --- |
133+| Keyboard | Afficher les touches à connaître |
134+| About | Afficher la version, le commit et la date de build quand le build les a enregistrés, et le thème courant. Voir [le numéro de version](versioning.md). |
135+
136+## Barre d'état
137+
138+Les indices du bas sont cliquables et exécutent les mêmes actions.
139+
140+| Indice | Action |
141+| --- | --- |
142+| `F1 Describe` | Code ▸ Describe symbol |
143+| `F2 Save` | File ▸ Save |
144+| `F3 Open` | File ▸ Open… |
145+| `F6 Window` | Window ▸ Next |
146+| `F7 Next` | Search ▸ Find next |
147+| `F10 Menu` | Ouvrir la barre de menus |
148+| `Alt-X Exit` | File ▸ Exit |
149+
150+L'extrémité droite affiche, dans cet ordre : le curseur sous la forme `ligne:colonne`, puis soit la première erreur signalée par le serveur de langage pour ce fichier (précédée de `⚠`), soit l'état du serveur de langage.
added docs/fr/reference/project-settings.md +111 -0
new file mode 100644
@@ -0,0 +1,111 @@
1+# Référence : réglages de projet
2+
3+> Description neutre de `.turbo-go/settings.toml` : où il est cherché, ce qu'il peut contenir, et ce qui y écrit.
4+
5+## Emplacement
6+
7+| Propriété | Valeur |
8+| --- | --- |
9+| Dossier | `.turbo-go` dans le répertoire de travail de l'éditeur |
10+| Fichier | `.turbo-go/settings.toml` |
11+| Recherche | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. |
12+| Lecture | Au démarrage de l'éditeur, puis à chaque enregistrement du fichier depuis l'éditeur |
13+| Obligatoire | Non. Un projet qui n'en a pas obtient les valeurs par défaut ci-dessous. |
14+
15+## Clés
16+
17+Chaque clé est facultative, et chaque clé se trouve dans la table `[editor]`. Une clé absente garde sa valeur par défaut ; une clé présente l'emporte, y compris avec une valeur égale à la valeur par défaut.
18+
19+| Clé | Type | Défaut | Description |
20+| --- | --- | --- | --- |
21+| `theme` | chaîne | le défaut de l'éditeur (`turbo-classic`) | Nom du thème de couleurs de démarrage, tel que listé par `turbo-go -list-themes` |
22+| `autosave` | booléen | `false` | Si les fichiers modifiés sont écrits sans qu'on le demande. Le fichier qu'écrit **Create project settings** le met à `true` ; la valeur par défaut ici est celle qui s'applique à un projet sans fichier de réglages du tout. |
23+| `autosave_delay` | chaîne | `"2s"` | Combien de temps attendre après la dernière frappe. Une durée Go : `"500ms"`, `"2s"`, `"1m"`. Consultée seulement quand `autosave` vaut true. |
24+
25+### Exemple
26+
27+```toml
28+[editor]
29+theme = "turbo-dark"
30+autosave = true
31+autosave_delay = "500ms"
32+```
33+
34+## Priorité du thème
35+
36+De la plus forte à la plus faible :
37+
38+| Source | L'emporte sur |
39+| --- | --- |
40+| `-theme` sur la ligne de commande | tout |
41+| `theme` dans le fichier de réglages | le défaut intégré |
42+| Le défaut intégré `turbo-classic` | — |
43+
44+Un nom de thème inconnu, à n'importe quel niveau, retombe sur le défaut intégré plutôt que d'échouer.
45+
46+## Quand un changement prend effet
47+
48+Le fichier est lu au démarrage, et **de nouveau à chaque enregistrement depuis l'éditeur** — un changement fait dans l'éditeur est donc en vigueur dès que vous appuyez sur `F2`, sans redémarrage.
49+
50+| Clé | Ré-appliquée à l'enregistrement | Pourquoi |
51+| --- | --- | --- |
52+| `autosave` | oui | |
53+| `autosave_delay` | oui | |
54+| `theme` | **non** | Options ▸ Theme est la façon vivante de le changer, et y réécrit déjà le choix. Une option `-theme` donnée en ligne de commande est l'affirmation la plus explicite pour cette session, et l'enregistrement d'un fichier ne la contredit pas. |
55+
56+| Résultat | Barre d'état |
57+| --- | --- |
58+| Lu et appliqué | `Applied .turbo-go/settings.toml — autosave on (2s)` |
59+| Lu et appliqué, autosave désactivée | `Applied .turbo-go/settings.toml — autosave off` |
60+| Enregistré, mais plus du TOML valide | `Saved, but not applied: …` — les valeurs précédentes restent en vigueur |
61+
62+Enregistrer est enregistrer, quel qu'en soit l'auteur : la sauvegarde automatique qui écrit le fichier de réglages les ré-applique exactement comme `F2`. Une modification faite **hors** de l'éditeur n'est pas remarquée ; rien ne surveille le fichier.
63+
64+## Sauvegarde automatique
65+
66+| Comportement | Détail |
67+| --- | --- |
68+| Déclencheur | Le délai écoulé sans modification dans aucune fenêtre |
69+| Portée | Tout fichier ouvert qui a un nom, pas seulement celui au premier plan |
70+| Échéance | Une seule pour tout l'éditeur, relancée par toute modification dans n'importe quelle fenêtre |
71+| Fichiers sans nom | Jamais enregistrés ; jamais l'objet d'une question |
72+| Signalement | `Saved <nom>` dans la barre d'état |
73+| Échec | Signalé dans la barre d'état, jamais dans un dialogue, et non réessayé avant la modification suivante |
74+| Fermeture d'une fenêtre | Enregistre au lieu de demander, si le fichier a un nom |
75+| Sortie de l'éditeur | Enregistre au lieu de demander, si le fichier a un nom |
76+
77+## Écritures
78+
79+Le fichier de réglages est écrit par exactement deux actions. Rien d'autre dans l'éditeur n'y écrit, et rien ne le crée tout seul.
80+
81+| Action | Effet |
82+| --- | --- |
83+| **Options ▸ Create project settings** | Crée `.turbo-go/settings.toml` avec le thème en cours, `autosave = true` et des commentaires explicatifs. Grisée dès que le projet en a un, elle ne peut donc pas être choisie deux fois. |
84+| **Options ▸ Theme** | Réécrit la valeur de `theme` **uniquement si le fichier existe déjà**. Commentaires, lignes vides, ordre des clés et commentaire de fin de la ligne du thème sont conservés. |
85+
86+Les deux écrivent via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact.
87+
88+## Entrées de menu
89+
90+| Entrée | Menu | Fichier requis | Effet |
91+| --- | --- | --- | --- |
92+| Create project settings | Options | refuse le fichier | Comme ci-dessus, puis ouvre le fichier. Grisée dès que le projet en a un. |
93+| Project settings… | Options | exige le fichier | Ouvre `.turbo-go/settings.toml`. Grisée tant que le projet n'en a pas. |
94+
95+## Erreurs
96+
97+| Message | Cause |
98+| --- | --- |
99+| `turbo-go: reading …/settings.toml: …` sur la sortie d'erreur | Le fichier est là mais n'est pas du TOML valide. L'éditeur s'ouvre avec ses valeurs par défaut. |
100+| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` n'est pas une durée Go |
101+| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` est nul ou négatif |
102+| `Already there: .turbo-go/settings.toml` dans la barre d'état | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. |
103+| `Saved, but not applied: …` dans la barre d'état | Le fichier de réglages a été écrit mais ne s'analyse plus. Les valeurs précédentes restent en vigueur. |
104+| `This project has no .turbo-go/settings.toml yet.` | **Project settings…** dans un projet qui n'en a pas |
105+| `Theme set for this session only: …` | Le thème a changé mais le fichier de réglages n'a pas pu être écrit |
106+
107+## Voir aussi
108+
109+- [Donner ses propres réglages à un projet](../how-to/configure-a-project.md)
110+- [Réglages de projet](../explanation/project-settings.md)
111+- [Format des fichiers de thème](themes.md) — un autre fichier, dans le même langage
new file mode 100644
@@ -0,0 +1,111 @@
1+# Référence : réglages de projet
2+
3+> Description neutre de `.turbo-go/settings.toml` : où il est cherché, ce qu'il peut contenir, et ce qui y écrit.
4+
5+## Emplacement
6+
7+| Propriété | Valeur |
8+| --- | --- |
9+| Dossier | `.turbo-go` dans le répertoire de travail de l'éditeur |
10+| Fichier | `.turbo-go/settings.toml` |
11+| Recherche | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. |
12+| Lecture | Au démarrage de l'éditeur, puis à chaque enregistrement du fichier depuis l'éditeur |
13+| Obligatoire | Non. Un projet qui n'en a pas obtient les valeurs par défaut ci-dessous. |
14+
15+## Clés
16+
17+Chaque clé est facultative, et chaque clé se trouve dans la table `[editor]`. Une clé absente garde sa valeur par défaut ; une clé présente l'emporte, y compris avec une valeur égale à la valeur par défaut.
18+
19+| Clé | Type | Défaut | Description |
20+| --- | --- | --- | --- |
21+| `theme` | chaîne | le défaut de l'éditeur (`turbo-classic`) | Nom du thème de couleurs de démarrage, tel que listé par `turbo-go -list-themes` |
22+| `autosave` | booléen | `false` | Si les fichiers modifiés sont écrits sans qu'on le demande. Le fichier qu'écrit **Create project settings** le met à `true` ; la valeur par défaut ici est celle qui s'applique à un projet sans fichier de réglages du tout. |
23+| `autosave_delay` | chaîne | `"2s"` | Combien de temps attendre après la dernière frappe. Une durée Go : `"500ms"`, `"2s"`, `"1m"`. Consultée seulement quand `autosave` vaut true. |
24+
25+### Exemple
26+
27+```toml
28+[editor]
29+theme = "turbo-dark"
30+autosave = true
31+autosave_delay = "500ms"
32+```
33+
34+## Priorité du thème
35+
36+De la plus forte à la plus faible :
37+
38+| Source | L'emporte sur |
39+| --- | --- |
40+| `-theme` sur la ligne de commande | tout |
41+| `theme` dans le fichier de réglages | le défaut intégré |
42+| Le défaut intégré `turbo-classic` | — |
43+
44+Un nom de thème inconnu, à n'importe quel niveau, retombe sur le défaut intégré plutôt que d'échouer.
45+
46+## Quand un changement prend effet
47+
48+Le fichier est lu au démarrage, et **de nouveau à chaque enregistrement depuis l'éditeur** — un changement fait dans l'éditeur est donc en vigueur dès que vous appuyez sur `F2`, sans redémarrage.
49+
50+| Clé | Ré-appliquée à l'enregistrement | Pourquoi |
51+| --- | --- | --- |
52+| `autosave` | oui | |
53+| `autosave_delay` | oui | |
54+| `theme` | **non** | Options ▸ Theme est la façon vivante de le changer, et y réécrit déjà le choix. Une option `-theme` donnée en ligne de commande est l'affirmation la plus explicite pour cette session, et l'enregistrement d'un fichier ne la contredit pas. |
55+
56+| Résultat | Barre d'état |
57+| --- | --- |
58+| Lu et appliqué | `Applied .turbo-go/settings.toml — autosave on (2s)` |
59+| Lu et appliqué, autosave désactivée | `Applied .turbo-go/settings.toml — autosave off` |
60+| Enregistré, mais plus du TOML valide | `Saved, but not applied: …` — les valeurs précédentes restent en vigueur |
61+
62+Enregistrer est enregistrer, quel qu'en soit l'auteur : la sauvegarde automatique qui écrit le fichier de réglages les ré-applique exactement comme `F2`. Une modification faite **hors** de l'éditeur n'est pas remarquée ; rien ne surveille le fichier.
63+
64+## Sauvegarde automatique
65+
66+| Comportement | Détail |
67+| --- | --- |
68+| Déclencheur | Le délai écoulé sans modification dans aucune fenêtre |
69+| Portée | Tout fichier ouvert qui a un nom, pas seulement celui au premier plan |
70+| Échéance | Une seule pour tout l'éditeur, relancée par toute modification dans n'importe quelle fenêtre |
71+| Fichiers sans nom | Jamais enregistrés ; jamais l'objet d'une question |
72+| Signalement | `Saved <nom>` dans la barre d'état |
73+| Échec | Signalé dans la barre d'état, jamais dans un dialogue, et non réessayé avant la modification suivante |
74+| Fermeture d'une fenêtre | Enregistre au lieu de demander, si le fichier a un nom |
75+| Sortie de l'éditeur | Enregistre au lieu de demander, si le fichier a un nom |
76+
77+## Écritures
78+
79+Le fichier de réglages est écrit par exactement deux actions. Rien d'autre dans l'éditeur n'y écrit, et rien ne le crée tout seul.
80+
81+| Action | Effet |
82+| --- | --- |
83+| **Options ▸ Create project settings** | Crée `.turbo-go/settings.toml` avec le thème en cours, `autosave = true` et des commentaires explicatifs. Grisée dès que le projet en a un, elle ne peut donc pas être choisie deux fois. |
84+| **Options ▸ Theme** | Réécrit la valeur de `theme` **uniquement si le fichier existe déjà**. Commentaires, lignes vides, ordre des clés et commentaire de fin de la ligne du thème sont conservés. |
85+
86+Les deux écrivent via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact.
87+
88+## Entrées de menu
89+
90+| Entrée | Menu | Fichier requis | Effet |
91+| --- | --- | --- | --- |
92+| Create project settings | Options | refuse le fichier | Comme ci-dessus, puis ouvre le fichier. Grisée dès que le projet en a un. |
93+| Project settings… | Options | exige le fichier | Ouvre `.turbo-go/settings.toml`. Grisée tant que le projet n'en a pas. |
94+
95+## Erreurs
96+
97+| Message | Cause |
98+| --- | --- |
99+| `turbo-go: reading …/settings.toml: …` sur la sortie d'erreur | Le fichier est là mais n'est pas du TOML valide. L'éditeur s'ouvre avec ses valeurs par défaut. |
100+| `reading …: autosave_delay "x" is not a duration such as "2s"` | `autosave_delay` n'est pas une durée Go |
101+| `reading …: autosave_delay must be positive, not "0s"` | `autosave_delay` est nul ou négatif |
102+| `Already there: .turbo-go/settings.toml` dans la barre d'état | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. |
103+| `Saved, but not applied: …` dans la barre d'état | Le fichier de réglages a été écrit mais ne s'analyse plus. Les valeurs précédentes restent en vigueur. |
104+| `This project has no .turbo-go/settings.toml yet.` | **Project settings…** dans un projet qui n'en a pas |
105+| `Theme set for this session only: …` | Le thème a changé mais le fichier de réglages n'a pas pu être écrit |
106+
107+## Voir aussi
108+
109+- [Donner ses propres réglages à un projet](../how-to/configure-a-project.md)
110+- [Réglages de projet](../explanation/project-settings.md)
111+- [Format des fichiers de thème](themes.md) — un autre fichier, dans le même langage
added docs/fr/reference/project-tree.md +102 -0
new file mode 100644
@@ -0,0 +1,102 @@
1+# Référence : arbre du projet
2+
3+> Description neutre de la fenêtre d'arbre du projet : ce qu'elle montre, ce qu'elle cache, et les touches auxquelles elle répond.
4+
5+## Ouverture
6+
7+| Chemin | Condition |
8+| --- | --- |
9+| `F9` | Toujours |
10+| **Window ▸ Project tree** | Toujours |
11+
12+Aucun des deux n'exige qu'un fichier soit ouvert. Les deux ramènent l'arbre existant au premier plan quand il y en a déjà un : il y a au plus une fenêtre d'arbre.
13+
14+## Racine
15+
16+| Propriété | Valeur |
17+| --- | --- |
18+| Enracinée à | Le dossier depuis lequel l'éditeur a été lancé (`os.Getwd()`) |
19+| Recherche | Ce dossier seulement. Les dossiers parents ne sont **pas** parcourus, la même règle que `.turbo-go/settings.toml`. |
20+| Titre de la fenêtre | Le nom de base de ce dossier |
21+| Ligne de la racine | Non affichée ; la première ligne est la première entrée du projet |
22+
23+## Ce qui est listé
24+
25+| Règle | Détail |
26+| --- | --- |
27+| Ordre | Les dossiers d'abord, puis les fichiers ; chaque groupe trié par nom |
28+| Masqué | `.git` seulement |
29+| Affiché | Toute autre entrée, y compris celles commençant par un point — `.turbo-go`, `.gitignore`, `.qlty` |
30+| Lecture | Un dossier est lu la première fois qu'il est déplié, pas avant |
31+| Dossier illisible | Apparaît déplié et vide ; le reste de l'arbre n'est pas affecté |
32+
33+## Marqueurs
34+
35+| Marqueur | Signification |
36+| --- | --- |
37+| `▶ ` | Un dossier fermé |
38+| `▼ ` | Un dossier ouvert |
39+| (deux espaces) | Un fichier — indenté de la largeur d'un marqueur, pour que les noms s'alignent |
40+
41+Chaque niveau de profondeur ajoute deux espaces d'indentation.
42+
43+## Touches
44+
45+Traitées quand la fenêtre de l'arbre a le focus.
46+
47+| Touche | Action |
48+| --- | --- |
49+| `↑` `↓` | Ligne précédente / suivante |
50+| `Page↑` `Page↓` | Un écran à la fois |
51+| `Début` `Fin` | Première / dernière ligne |
52+| `→` | Déplier un dossier fermé ; sinon aller à la ligne suivante |
53+| `←` | Replier un dossier ouvert ; sinon remonter au dossier qui contient cette ligne |
54+| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier |
55+| `F5`, `Ctrl-R` | Relire le projet |
56+
57+Les raccourcis de l'éditeur s'appliquent comme d'habitude : `F6` passe à la fenêtre suivante, `Ctrl-W` ferme l'arbre, `Alt-X` quitte.
58+
59+## Souris
60+
61+| Action | Effet |
62+| --- | --- |
63+| Clic sur une ligne | Y déplacer la surbrillance |
64+| Clic sur la ligne surlignée | Agir dessus, comme `Entrée` |
65+| Molette haut / bas | Déplacer la surbrillance de trois lignes |
66+
67+## Rafraîchissement
68+
69+| Déclencheur | Effet |
70+| --- | --- |
71+| `F5` ou `Ctrl-R` | Relit tous les dossiers qui ont été ouverts |
72+| Enregistrer un fichier | La même chose, automatiquement |
73+| Déplier un dossier | Lit ce dossier, s'il n'a pas encore été lu |
74+
75+Le rafraîchissement conserve la forme de l'arbre : un dossier ouvert le reste, un dossier supprimé emporte sa branche, et les dossiers que personne n'a ouverts restent non lus. La surbrillance reste sur la même entrée, ou sur la ligne restante la plus proche si cette entrée a disparu.
76+
77+L'arbre ne surveille **pas** le système de fichiers. Un fichier créé par une fenêtre terminal, ou par un `git checkout`, n'apparaît qu'après un rafraîchissement.
78+
79+## Couleurs
80+
81+| Clé de thème | Ce qu'elle colore |
82+| --- | --- |
83+| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre |
84+| `tree.directory` | Le nom d'un dossier |
85+| `tree.selected` | La ligne surlignée, quand l'arbre a le focus |
86+| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas |
87+
88+Elles ne se rabattent pas sur les clés `list.*` : le rabattement suit les points et s'arrête à `default`. Voir [Format des fichiers de thème](themes.md).
89+
90+## Erreurs
91+
92+| Message | Cause |
93+| --- | --- |
94+| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu |
95+| `reading …: …` | Le dossier du projet n'a pas pu être lu |
96+| `… is not a directory` | La racine désigne un fichier |
97+
98+## Voir aussi
99+
100+- [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md)
101+- [Arbre du projet](../explanation/project-tree.md)
102+- [Clavier](keyboard.md)
new file mode 100644
@@ -0,0 +1,102 @@
1+# Référence : arbre du projet
2+
3+> Description neutre de la fenêtre d'arbre du projet : ce qu'elle montre, ce qu'elle cache, et les touches auxquelles elle répond.
4+
5+## Ouverture
6+
7+| Chemin | Condition |
8+| --- | --- |
9+| `F9` | Toujours |
10+| **Window ▸ Project tree** | Toujours |
11+
12+Aucun des deux n'exige qu'un fichier soit ouvert. Les deux ramènent l'arbre existant au premier plan quand il y en a déjà un : il y a au plus une fenêtre d'arbre.
13+
14+## Racine
15+
16+| Propriété | Valeur |
17+| --- | --- |
18+| Enracinée à | Le dossier depuis lequel l'éditeur a été lancé (`os.Getwd()`) |
19+| Recherche | Ce dossier seulement. Les dossiers parents ne sont **pas** parcourus, la même règle que `.turbo-go/settings.toml`. |
20+| Titre de la fenêtre | Le nom de base de ce dossier |
21+| Ligne de la racine | Non affichée ; la première ligne est la première entrée du projet |
22+
23+## Ce qui est listé
24+
25+| Règle | Détail |
26+| --- | --- |
27+| Ordre | Les dossiers d'abord, puis les fichiers ; chaque groupe trié par nom |
28+| Masqué | `.git` seulement |
29+| Affiché | Toute autre entrée, y compris celles commençant par un point — `.turbo-go`, `.gitignore`, `.qlty` |
30+| Lecture | Un dossier est lu la première fois qu'il est déplié, pas avant |
31+| Dossier illisible | Apparaît déplié et vide ; le reste de l'arbre n'est pas affecté |
32+
33+## Marqueurs
34+
35+| Marqueur | Signification |
36+| --- | --- |
37+| `▶ ` | Un dossier fermé |
38+| `▼ ` | Un dossier ouvert |
39+| (deux espaces) | Un fichier — indenté de la largeur d'un marqueur, pour que les noms s'alignent |
40+
41+Chaque niveau de profondeur ajoute deux espaces d'indentation.
42+
43+## Touches
44+
45+Traitées quand la fenêtre de l'arbre a le focus.
46+
47+| Touche | Action |
48+| --- | --- |
49+| `↑` `↓` | Ligne précédente / suivante |
50+| `Page↑` `Page↓` | Un écran à la fois |
51+| `Début` `Fin` | Première / dernière ligne |
52+| `→` | Déplier un dossier fermé ; sinon aller à la ligne suivante |
53+| `←` | Replier un dossier ouvert ; sinon remonter au dossier qui contient cette ligne |
54+| `Entrée` | Ouvrir un fichier ; déplier ou replier un dossier |
55+| `F5`, `Ctrl-R` | Relire le projet |
56+
57+Les raccourcis de l'éditeur s'appliquent comme d'habitude : `F6` passe à la fenêtre suivante, `Ctrl-W` ferme l'arbre, `Alt-X` quitte.
58+
59+## Souris
60+
61+| Action | Effet |
62+| --- | --- |
63+| Clic sur une ligne | Y déplacer la surbrillance |
64+| Clic sur la ligne surlignée | Agir dessus, comme `Entrée` |
65+| Molette haut / bas | Déplacer la surbrillance de trois lignes |
66+
67+## Rafraîchissement
68+
69+| Déclencheur | Effet |
70+| --- | --- |
71+| `F5` ou `Ctrl-R` | Relit tous les dossiers qui ont été ouverts |
72+| Enregistrer un fichier | La même chose, automatiquement |
73+| Déplier un dossier | Lit ce dossier, s'il n'a pas encore été lu |
74+
75+Le rafraîchissement conserve la forme de l'arbre : un dossier ouvert le reste, un dossier supprimé emporte sa branche, et les dossiers que personne n'a ouverts restent non lus. La surbrillance reste sur la même entrée, ou sur la ligne restante la plus proche si cette entrée a disparu.
76+
77+L'arbre ne surveille **pas** le système de fichiers. Un fichier créé par une fenêtre terminal, ou par un `git checkout`, n'apparaît qu'après un rafraîchissement.
78+
79+## Couleurs
80+
81+| Clé de thème | Ce qu'elle colore |
82+| --- | --- |
83+| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre |
84+| `tree.directory` | Le nom d'un dossier |
85+| `tree.selected` | La ligne surlignée, quand l'arbre a le focus |
86+| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas |
87+
88+Elles ne se rabattent pas sur les clés `list.*` : le rabattement suit les points et s'arrête à `default`. Voir [Format des fichiers de thème](themes.md).
89+
90+## Erreurs
91+
92+| Message | Cause |
93+| --- | --- |
94+| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu |
95+| `reading …: …` | Le dossier du projet n'a pas pu être lu |
96+| `… is not a directory` | La racine désigne un fichier |
97+
98+## Voir aussi
99+
100+- [Parcourir un projet et ouvrir des fichiers depuis un arbre](../how-to/browse-a-project.md)
101+- [Arbre du projet](../explanation/project-tree.md)
102+- [Clavier](keyboard.md)
added docs/fr/reference/snippets.md +114 -0
new file mode 100644
@@ -0,0 +1,114 @@
1+# Référence : snippets
2+
3+> Description neutre des fichiers de snippets, du menu Snippets, et de la façon dont un snippet est inséré.
4+
5+## Fichiers
6+
7+Les deux sont lus, et les deux sont facultatifs.
8+
9+| Fichier | Contient |
10+| --- | --- |
11+| `./.turbo-go/snippets.toml` | Les snippets du projet |
12+| `$TURBO_GO_SNIPPET_DIR/snippets.toml`, sinon `<config utilisateur>/turbo-go/snippets.toml` | Les vôtres, partagés entre projets |
13+
14+`<config utilisateur>` est `os.UserConfigDir()` : `~/.config` sous Linux, `~/Library/Application Support` sous macOS.
15+
16+| Propriété | Valeur |
17+| --- | --- |
18+| Recherche du projet | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. |
19+| Lecture | À chaque ouverture du menu Snippets |
20+| Ordre | Les vôtres d'abord, puis ceux du projet |
21+| Conflit de nom | Même `group` **et** même `name` → celui du projet remplace le vôtre |
22+| Fichier absent | Pas une erreur |
23+| Fichier illisible | Une erreur, signalée dans le menu |
24+
25+## Format du fichier
26+
27+Une table `[[snippet]]` par snippet.
28+
29+| Clé | Type | Obligatoire | Description |
30+| --- | --- | --- | --- |
31+| `name` | chaîne | oui | Ce que le menu affiche |
32+| `body` | chaîne | oui | Le texte inséré au curseur |
33+| `group` | chaîne | non | Le sous-menu où il va ; absent signifie `General` |
34+| `languages` | tableau de chaînes | non | Restreint le snippet à ces langages ; absent signifie tous les fichiers |
35+
36+`languages` emploie les noms de langages de l'éditeur : `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. Voir [Langages colorés](languages.md).
37+
38+Un snippet sans `name` ou sans `body` rend tout le fichier erroné — il n'aurait pu être affiché, ou n'aurait rien à insérer.
39+
40+### Exemple
41+
42+```toml
43+[[snippet]]
44+name = "if err != nil"
45+group = "Go"
46+languages = ["go"]
47+body = """
48+if err != nil {
49+ return err
50+}"""
51+```
52+
53+Les chaînes `"""` du TOML suppriment le saut de ligne qui suit immédiatement les guillemets d'ouverture, et interprètent `\t` comme une tabulation.
54+
55+## Le menu
56+
57+| Entrée | Condition |
58+| --- | --- |
59+| Un sous-menu par groupe, dans l'ordre d'apparition des groupes dans les fichiers | Un groupe ayant au moins un snippet applicable à la fenêtre au premier plan |
60+| `Cannot read snippets`, grisé | Un fichier est présent mais illisible |
61+| `Create snippets file` | Le projet n'a pas de fichier d'extraits |
62+| `Open snippets file` | Le projet en a un |
63+
64+La touche d'accès du menu est `Alt-N`, pas `Alt-S` : Search répond déjà au S.
65+
66+Les groupes, et les snippets à l'intérieur, sortent dans l'ordre de lecture : le menu correspond aux fichiers.
67+
68+Une entrée de snippet est grisée quand aucun fichier n'est ouvert pour l'y insérer — un terminal ou l'arbre du projet au premier plan compte comme aucun fichier.
69+
70+### Filtrage
71+
72+| Fenêtre au premier plan | Snippets proposés |
73+| --- | --- |
74+| Un fichier d'un langage reconnu | Ceux qui nomment ce langage, plus ceux qui n'en nomment aucun |
75+| Un fichier d'aucun langage reconnu | Ceux qui n'en nomment aucun |
76+| Un terminal, l'arbre du projet, ou rien | Ceux qui n'en nomment aucun |
77+
78+## Insertion
79+
80+| Comportement | Détail |
81+| --- | --- |
82+| Position | Au curseur |
83+| Première ligne | Insérée là où est le curseur |
84+| Lignes suivantes | Préfixées par l'indentation de la ligne où était le curseur |
85+| Lignes vides du corps | Laissées vides, non complétées d'espaces |
86+| Annulation | Une seule opération pour tout le snippet |
87+| Curseur ensuite | À la fin du texte inséré |
88+| Signalement | `Snippet inserted` dans la barre d'état |
89+
90+L'indentation copiée est le **préfixe d'espaces de la ligne courante**, tabulations ou espaces telles quelles : un snippet suit donc ce que le fichier emploie déjà.
91+
92+## Entrées de menu
93+
94+| Entrée | Menu | Effet |
95+| --- | --- | --- |
96+| Create snippets file | Snippets | Écrit `.turbo-go/snippets.toml` avec des exemples travaillés, puis l'ouvre. Grisée dès que le projet en a un. |
97+| Open snippets file | Snippets | Ouvre `.turbo-go/snippets.toml`. Grisée tant que le projet n'en a pas. Toujours le fichier du projet, jamais le vôtre — c'est celui qu'écrit l'entrée au-dessus. |
98+
99+Le fichier est écrit via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact.
100+
101+## Erreurs
102+
103+| Message | Cause |
104+| --- | --- |
105+| `Cannot read snippets` dans le menu | Un fichier de snippets est présent mais n'est pas du TOML valide, ou contient un snippet sans nom ou sans corps |
106+| `Already there: .turbo-go/snippets.toml` | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. |
107+| `This project has no .turbo-go/snippets.toml yet.` | Ouvrir dans un projet qui n'en a pas, de même |
108+| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu |
109+
110+## Voir aussi
111+
112+- [Insérer des snippets depuis un menu](../how-to/use-snippets.md)
113+- [Snippets](../explanation/snippets.md)
114+- [Clavier](keyboard.md)
new file mode 100644
@@ -0,0 +1,114 @@
1+# Référence : snippets
2+
3+> Description neutre des fichiers de snippets, du menu Snippets, et de la façon dont un snippet est inséré.
4+
5+## Fichiers
6+
7+Les deux sont lus, et les deux sont facultatifs.
8+
9+| Fichier | Contient |
10+| --- | --- |
11+| `./.turbo-go/snippets.toml` | Les snippets du projet |
12+| `$TURBO_GO_SNIPPET_DIR/snippets.toml`, sinon `<config utilisateur>/turbo-go/snippets.toml` | Les vôtres, partagés entre projets |
13+
14+`<config utilisateur>` est `os.UserConfigDir()` : `~/.config` sous Linux, `~/Library/Application Support` sous macOS.
15+
16+| Propriété | Valeur |
17+| --- | --- |
18+| Recherche du projet | Le répertoire de travail seulement. Les dossiers parents ne sont **pas** parcourus. |
19+| Lecture | À chaque ouverture du menu Snippets |
20+| Ordre | Les vôtres d'abord, puis ceux du projet |
21+| Conflit de nom | Même `group` **et** même `name` → celui du projet remplace le vôtre |
22+| Fichier absent | Pas une erreur |
23+| Fichier illisible | Une erreur, signalée dans le menu |
24+
25+## Format du fichier
26+
27+Une table `[[snippet]]` par snippet.
28+
29+| Clé | Type | Obligatoire | Description |
30+| --- | --- | --- | --- |
31+| `name` | chaîne | oui | Ce que le menu affiche |
32+| `body` | chaîne | oui | Le texte inséré au curseur |
33+| `group` | chaîne | non | Le sous-menu où il va ; absent signifie `General` |
34+| `languages` | tableau de chaînes | non | Restreint le snippet à ces langages ; absent signifie tous les fichiers |
35+
36+`languages` emploie les noms de langages de l'éditeur : `go`, `toml`, `yaml`, `markdown`, `javascript`, `html`, `xml`, `dockerfile`, `bash`. Voir [Langages colorés](languages.md).
37+
38+Un snippet sans `name` ou sans `body` rend tout le fichier erroné — il n'aurait pu être affiché, ou n'aurait rien à insérer.
39+
40+### Exemple
41+
42+```toml
43+[[snippet]]
44+name = "if err != nil"
45+group = "Go"
46+languages = ["go"]
47+body = """
48+if err != nil {
49+ return err
50+}"""
51+```
52+
53+Les chaînes `"""` du TOML suppriment le saut de ligne qui suit immédiatement les guillemets d'ouverture, et interprètent `\t` comme une tabulation.
54+
55+## Le menu
56+
57+| Entrée | Condition |
58+| --- | --- |
59+| Un sous-menu par groupe, dans l'ordre d'apparition des groupes dans les fichiers | Un groupe ayant au moins un snippet applicable à la fenêtre au premier plan |
60+| `Cannot read snippets`, grisé | Un fichier est présent mais illisible |
61+| `Create snippets file` | Le projet n'a pas de fichier d'extraits |
62+| `Open snippets file` | Le projet en a un |
63+
64+La touche d'accès du menu est `Alt-N`, pas `Alt-S` : Search répond déjà au S.
65+
66+Les groupes, et les snippets à l'intérieur, sortent dans l'ordre de lecture : le menu correspond aux fichiers.
67+
68+Une entrée de snippet est grisée quand aucun fichier n'est ouvert pour l'y insérer — un terminal ou l'arbre du projet au premier plan compte comme aucun fichier.
69+
70+### Filtrage
71+
72+| Fenêtre au premier plan | Snippets proposés |
73+| --- | --- |
74+| Un fichier d'un langage reconnu | Ceux qui nomment ce langage, plus ceux qui n'en nomment aucun |
75+| Un fichier d'aucun langage reconnu | Ceux qui n'en nomment aucun |
76+| Un terminal, l'arbre du projet, ou rien | Ceux qui n'en nomment aucun |
77+
78+## Insertion
79+
80+| Comportement | Détail |
81+| --- | --- |
82+| Position | Au curseur |
83+| Première ligne | Insérée là où est le curseur |
84+| Lignes suivantes | Préfixées par l'indentation de la ligne où était le curseur |
85+| Lignes vides du corps | Laissées vides, non complétées d'espaces |
86+| Annulation | Une seule opération pour tout le snippet |
87+| Curseur ensuite | À la fin du texte inséré |
88+| Signalement | `Snippet inserted` dans la barre d'état |
89+
90+L'indentation copiée est le **préfixe d'espaces de la ligne courante**, tabulations ou espaces telles quelles : un snippet suit donc ce que le fichier emploie déjà.
91+
92+## Entrées de menu
93+
94+| Entrée | Menu | Effet |
95+| --- | --- | --- |
96+| Create snippets file | Snippets | Écrit `.turbo-go/snippets.toml` avec des exemples travaillés, puis l'ouvre. Grisée dès que le projet en a un. |
97+| Open snippets file | Snippets | Ouvre `.turbo-go/snippets.toml`. Grisée tant que le projet n'en a pas. Toujours le fichier du projet, jamais le vôtre — c'est celui qu'écrit l'entrée au-dessus. |
98+
99+Le fichier est écrit via un fichier temporaire du même dossier, renommé en place : une écriture interrompue laisse le fichier précédent intact.
100+
101+## Erreurs
102+
103+| Message | Cause |
104+| --- | --- |
105+| `Cannot read snippets` dans le menu | Un fichier de snippets est présent mais n'est pas du TOML valide, ou contient un snippet sans nom ou sans corps |
106+| `Already there: .turbo-go/snippets.toml` | Créer dans un projet qui en a déjà un. Inatteignable depuis le menu, qui grise l'entrée ; reste possible pour un appelant qui n'est pas un menu. |
107+| `This project has no .turbo-go/snippets.toml yet.` | Ouvrir dans un projet qui n'en a pas, de même |
108+| `Cannot tell which directory this is: …` | Le répertoire de travail n'a pas pu être lu |
109+
110+## Voir aussi
111+
112+- [Insérer des snippets depuis un menu](../how-to/use-snippets.md)
113+- [Snippets](../explanation/snippets.md)
114+- [Clavier](keyboard.md)
added docs/fr/reference/terminal.md +228 -0
new file mode 100644
@@ -0,0 +1,228 @@
1+# Référence : fenêtres terminal
2+
3+> Description neutre des fenêtres terminal ouvertes par Turbo Go, des touches auxquelles elles répondent et des séquences d'échappement que l'émulateur implémente.
4+
5+## Ouverture
6+
7+| Chemin | Condition |
8+| --- | --- |
9+| `F8` | Toujours |
10+| **Window ▸ New terminal** | Toujours |
11+
12+Aucun des deux n'exige qu'un fichier soit ouvert.
13+
14+## Le shell
15+
16+| Propriété | Valeur |
17+| --- | --- |
18+| Programme | `$SHELL`, ou `/bin/sh` si la variable est absente ou vide ; sous Windows `%COMSPEC%`, ou `cmd.exe` |
19+| Répertoire de travail | Le dossier du fichier de la fenêtre au premier plan ; le répertoire de travail de l'éditeur si aucun fichier n'est ouvert |
20+| `TERM` | `xterm-256color`, toujours — remplaçant toute valeur héritée |
21+| Environnement | Celui de l'éditeur, avec `TERM` remplacé |
22+| Terminal de contrôle | Oui : sous Linux et macOS le shell tourne dans sa propre session avec le pseudo-terminal comme terminal de contrôle ; sous Windows il est attaché à une pseudo-console. Dans les deux cas le contrôle de tâches et `Ctrl-C` fonctionnent |
23+| Taille initiale | Celle de la fenêtre, mise à jour à chaque redimensionnement |
24+
25+## Plateformes supportées
26+
27+| Plateforme | Comportement |
28+| --- | --- |
29+| Linux | Supportée (`/dev/ptmx`) |
30+| macOS | Supportée (`/dev/ptmx`) |
31+| Windows | Supportée (pseudo-console, ConPTY) : Windows 10 version 1809 ou plus récent. Compilé et vérifié ; **pas encore exécuté par les auteurs** sur une machine Windows |
32+| Autres | `F8` ouvre un message indiquant que les fenêtres terminal ne sont pas encore supportées ; rien d'autre ne change |
33+
34+## Touches
35+
36+### Après la fin du programme
37+
38+Une fenêtre dont la commande est terminée garde sa sortie mais cesse de se comporter comme un terminal : seules `Maj-Page↑` et `Maj-Page↓` sont encore prises, et toute autre touche atteint l'éditeur — c'est ce qui permet à `Ctrl-W` de la fermer.
39+
40+### Envoyées au shell
41+
42+Toute touche non listée ci-dessous sous « conservées par l'éditeur », encodée comme un terminal l'attend.
43+
44+| Touche | Octets envoyés |
45+| --- | --- |
46+| caractère imprimable | son encodage UTF-8 |
47+| `Alt-<touche>` | `ESC` suivi des octets de cette touche |
48+| `Ctrl-A``Ctrl-Z` | `0x01``0x1a` |
49+| `Enter` | `\r` |
50+| `Tab` | `\t` |
51+| `Shift-Tab` | `ESC [ Z` |
52+| `Backspace` | `0x7f` |
53+| `Escape` | `0x1b` |
54+| `↑` `↓` `→` `←` | `ESC [ A B C D`, ou `ESC O A B C D` en mode curseur application |
55+| `Home` `End` | `ESC [ H`, `ESC [ F`, ou les formes `ESC O` en mode curseur application |
56+| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` |
57+| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` |
58+| `F1``F4` | `ESC O P Q R S` |
59+| `F5``F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` |
60+
61+Une touche sans signification pour un terminal n'envoie rien.
62+
63+### Conservées par l'éditeur
64+
65+| Touche | Action |
66+| --- | --- |
67+| `F1``F12` | Leur action habituelle dans l'éditeur |
68+| `Alt-X` | Quitter |
69+| `Alt-0``Alt-9` | Lister les fenêtres / passer la fenêtre 1…9 au premier plan |
70+
71+Les touches de fonction n'atteignent donc jamais un programme lancé dans une fenêtre terminal.
72+
73+### Traitées par la fenêtre terminal elle-même
74+
75+| Touche | Action |
76+| --- | --- |
77+| `Shift-PgUp` | Reculer d'un écran dans l'historique |
78+| `Shift-PgDn` | Avancer d'un écran |
79+
80+Toute touche envoyée au shell ramène également la vue à l'écran vivant.
81+
82+## Souris
83+
84+| Action | Effet |
85+| --- | --- |
86+| Molette haut / bas | Défiler de trois lignes dans l'historique |
87+| Clic | Passe la fenêtre au premier plan ; n'est pas transmis au programme |
88+
89+Le rapport souris n'est pas implémenté : un programme n'est jamais informé des clics.
90+
91+## Historique
92+
93+| Propriété | Valeur |
94+| --- | --- |
95+| Lignes conservées | 2000 |
96+| Ce qui est conservé | Uniquement les lignes sorties par le haut de l'écran principal |
97+| Écran alternatif | Non conservé — un programme plein écran ne laisse aucun historique |
98+
99+## Émulation
100+
101+`TERM` vaut `xterm-256color`. Voici ce qui en est implémenté.
102+
103+### Caractères de contrôle
104+
105+| Octet | Effet |
106+| --- | --- |
107+| `0x07` BEL | Noté ; l'éditeur ne l'émet pas |
108+| `0x08` BS | Curseur d'une colonne à gauche |
109+| `0x09` HT | Jusqu'à la tabulation suivante, toutes les 8 colonnes |
110+| `0x0a` `0x0b` `0x0c` | Saut de ligne |
111+| `0x0d` CR | Colonne 1 |
112+
113+### Séquences d'échappement
114+
115+| Séquence | Nom | Effet |
116+| --- | --- | --- |
117+| `ESC D` | IND | Saut de ligne |
118+| `ESC E` | NEL | Retour chariot et saut de ligne |
119+| `ESC M` | RI | Saut de ligne inverse, en conservant la colonne |
120+| `ESC 7` | DECSC | Sauvegarder le curseur et le style |
121+| `ESC 8` | DECRC | Les restaurer |
122+| `ESC c` | RIS | Réinitialisation complète |
123+
124+### Séquences CSI
125+
126+| Séquence | Nom | Effet |
127+| --- | --- | --- |
128+| `CSI n A B C D` | CUU CUD CUF CUB | Déplacer de n cellules haut, bas, droite, gauche |
129+| `CSI n E F` | CNL CPL | n lignes plus bas / plus haut, colonne 1 |
130+| `CSI n G` | CHA | Aller à la colonne n |
131+| `CSI l ; c H`, `CSI l ; c f` | CUP HVP | Aller à la ligne l, colonne c |
132+| `CSI n d` | VPA | Aller à la ligne n |
133+| `CSI n J` | ED | Effacer l'écran : 0 jusqu'à la fin, 1 jusqu'au début, 2 ou 3 tout |
134+| `CSI n K` | EL | Effacer la ligne : 0 jusqu'à la fin, 1 jusqu'au début, 2 tout |
135+| `CSI n L` | IL | Insérer n lignes vides au curseur |
136+| `CSI n M` | DL | Supprimer n lignes au curseur |
137+| `CSI n @` | ICH | Insérer n cellules vides |
138+| `CSI n P` | DCH | Supprimer n cellules |
139+| `CSI n X` | ECH | Effacer n cellules sur place |
140+| `CSI n S` | SU | Faire défiler la région de n lignes vers le haut |
141+| `CSI n T` | SD | Faire défiler la région de n lignes vers le bas |
142+| `CSI h ; b r` | DECSTBM | Définir la région de défilement aux lignes h…b |
143+| `CSI s`, `CSI u` | SCP RCP | Sauvegarder / restaurer le curseur |
144+| `CSI … m` | SGR | Couleurs et attributs, ci-dessous |
145+
146+`IL` et `DL` ne font rien lorsque le curseur est hors de la région de défilement.
147+
148+### Modes privés
149+
150+Activés par `CSI ? n h`, désactivés par `CSI ? n l`.
151+
152+| n | Nom | Effet |
153+| --- | --- | --- |
154+| 1 | DECCKM | Touches curseur application : les flèches envoient `ESC O x` |
155+| 7 | DECAWM | Retour à la ligne automatique à la marge droite |
156+| 25 | DECTCEM | Afficher le curseur |
157+| 47, 1047 | | Écran alternatif |
158+| 1048 | | Sauvegarder / restaurer le curseur |
159+| 1049 | | Sauvegarder le curseur, puis l'écran alternatif |
160+
161+Tout autre mode est analysé et ignoré.
162+
163+### SGR
164+
165+| Code | Effet |
166+| --- | --- |
167+| 0 | Réinitialisation |
168+| 1, 22 | Gras activé / désactivé |
169+| 2, 22 | Atténué activé / désactivé |
170+| 3, 23 | Italique activé / désactivé |
171+| 4, 24 | Souligné activé / désactivé |
172+| 5, 6, 25 | Clignotement activé / désactivé |
173+| 7, 27 | Vidéo inverse activée / désactivée |
174+| 9, 29 | Barré activé / désactivé |
175+| 30–37, 40–47 | Les huit couleurs normales, premier plan / fond |
176+| 90–97, 100–107 | Les huit couleurs vives, premier plan / fond |
177+| 38;5;n, 48;5;n | Couleur n de la palette de 256 |
178+| 38;2;r;g;b, 48;2;r;g;b | Couleur 24 bits |
179+| 39, 49 | Retour à la couleur du thème |
180+
181+Les seize couleurs nommées sont celles de tcell, c'est-à-dire la palette configurée dans le terminal de l'utilisateur, et non des valeurs hexadécimales figées. Une couleur étendue à court de paramètres laisse le style inchangé. Tout autre code est ignoré.
182+
183+### OSC
184+
185+| Séquence | Effet |
186+| --- | --- |
187+| `OSC 0 ; texte BEL`, `OSC 2 ; texte BEL` | Définir le titre de la fenêtre |
188+| `OSC … ST` | Le terminateur `ESC \` est accepté à la place de BEL |
189+
190+Le titre est plafonné à 4096 octets. Les autres commandes OSC sont analysées et ignorées.
191+
192+### Consommées et ignorées
193+
194+Analysées correctement, donc jamais affichées comme des caractères parasites, mais sans effet :
195+
196+| Séquence | Nom |
197+| --- | --- |
198+| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — lues jusqu'à leur terminateur de chaîne |
199+| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <espace>` | Sélecteurs de jeu de caractères et de taille de ligne — l'émulateur travaille en UTF-8 de toute façon |
200+| `CSI ? n h`, `CSI ? n l` pour tout autre n | Modes privés non listés ci-dessus |
201+| Tout autre octet final CSI, code SGR ou commande OSC | |
202+
203+### Non implémenté
204+
205+Le rapport souris, le collage entre crochets, les bascules shift-in / shift-out, les lignes double largeur, sixel et les autres protocoles graphiques, ainsi que les rapports d'état et d'attributs DEC. Un programme qui en demande un n'obtient aucune réponse : celui qui en attend une attendra indéfiniment.
206+
207+## Couleurs
208+
209+| Clé de thème | Ce qu'elle colore |
210+| --- | --- |
211+| `terminal.text` | Toute cellule dont le programme n'a pas choisi la couleur |
212+| `terminal.cursor` | La cellule sous le curseur, quand la fenêtre a le focus |
213+
214+Voir [Format des fichiers de thème](themes.md).
215+
216+## Erreurs
217+
218+| Message | Cause |
219+| --- | --- |
220+| Terminal windows are not supported on this platform yet | La compilation n'a pas de support des pseudo-terminaux : toute plateforme autre que Linux, macOS et Windows |
221+| `openpt: …`, `grantpt: …`, `ptsname: …` | Le système d'exploitation a refusé d'ouvrir un pseudo-terminal |
222+| `fork/exec …: no such file or directory` | `$SHELL` désigne un programme inexistant |
223+
224+## Voir aussi
225+
226+- [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md)
227+- [Fenêtres terminal](../explanation/terminal-windows.md)
228+- [Clavier](keyboard.md)
new file mode 100644
@@ -0,0 +1,228 @@
1+# Référence : fenêtres terminal
2+
3+> Description neutre des fenêtres terminal ouvertes par Turbo Go, des touches auxquelles elles répondent et des séquences d'échappement que l'émulateur implémente.
4+
5+## Ouverture
6+
7+| Chemin | Condition |
8+| --- | --- |
9+| `F8` | Toujours |
10+| **Window ▸ New terminal** | Toujours |
11+
12+Aucun des deux n'exige qu'un fichier soit ouvert.
13+
14+## Le shell
15+
16+| Propriété | Valeur |
17+| --- | --- |
18+| Programme | `$SHELL`, ou `/bin/sh` si la variable est absente ou vide ; sous Windows `%COMSPEC%`, ou `cmd.exe` |
19+| Répertoire de travail | Le dossier du fichier de la fenêtre au premier plan ; le répertoire de travail de l'éditeur si aucun fichier n'est ouvert |
20+| `TERM` | `xterm-256color`, toujours — remplaçant toute valeur héritée |
21+| Environnement | Celui de l'éditeur, avec `TERM` remplacé |
22+| Terminal de contrôle | Oui : sous Linux et macOS le shell tourne dans sa propre session avec le pseudo-terminal comme terminal de contrôle ; sous Windows il est attaché à une pseudo-console. Dans les deux cas le contrôle de tâches et `Ctrl-C` fonctionnent |
23+| Taille initiale | Celle de la fenêtre, mise à jour à chaque redimensionnement |
24+
25+## Plateformes supportées
26+
27+| Plateforme | Comportement |
28+| --- | --- |
29+| Linux | Supportée (`/dev/ptmx`) |
30+| macOS | Supportée (`/dev/ptmx`) |
31+| Windows | Supportée (pseudo-console, ConPTY) : Windows 10 version 1809 ou plus récent. Compilé et vérifié ; **pas encore exécuté par les auteurs** sur une machine Windows |
32+| Autres | `F8` ouvre un message indiquant que les fenêtres terminal ne sont pas encore supportées ; rien d'autre ne change |
33+
34+## Touches
35+
36+### Après la fin du programme
37+
38+Une fenêtre dont la commande est terminée garde sa sortie mais cesse de se comporter comme un terminal : seules `Maj-Page↑` et `Maj-Page↓` sont encore prises, et toute autre touche atteint l'éditeur — c'est ce qui permet à `Ctrl-W` de la fermer.
39+
40+### Envoyées au shell
41+
42+Toute touche non listée ci-dessous sous « conservées par l'éditeur », encodée comme un terminal l'attend.
43+
44+| Touche | Octets envoyés |
45+| --- | --- |
46+| caractère imprimable | son encodage UTF-8 |
47+| `Alt-<touche>` | `ESC` suivi des octets de cette touche |
48+| `Ctrl-A``Ctrl-Z` | `0x01``0x1a` |
49+| `Enter` | `\r` |
50+| `Tab` | `\t` |
51+| `Shift-Tab` | `ESC [ Z` |
52+| `Backspace` | `0x7f` |
53+| `Escape` | `0x1b` |
54+| `↑` `↓` `→` `←` | `ESC [ A B C D`, ou `ESC O A B C D` en mode curseur application |
55+| `Home` `End` | `ESC [ H`, `ESC [ F`, ou les formes `ESC O` en mode curseur application |
56+| `Insert` `Delete` | `ESC [ 2~`, `ESC [ 3~` |
57+| `PgUp` `PgDn` | `ESC [ 5~`, `ESC [ 6~` |
58+| `F1``F4` | `ESC O P Q R S` |
59+| `F5``F12` | `ESC [ 15~ 17~ 18~ 19~ 20~ 21~ 23~ 24~` |
60+
61+Une touche sans signification pour un terminal n'envoie rien.
62+
63+### Conservées par l'éditeur
64+
65+| Touche | Action |
66+| --- | --- |
67+| `F1``F12` | Leur action habituelle dans l'éditeur |
68+| `Alt-X` | Quitter |
69+| `Alt-0``Alt-9` | Lister les fenêtres / passer la fenêtre 1…9 au premier plan |
70+
71+Les touches de fonction n'atteignent donc jamais un programme lancé dans une fenêtre terminal.
72+
73+### Traitées par la fenêtre terminal elle-même
74+
75+| Touche | Action |
76+| --- | --- |
77+| `Shift-PgUp` | Reculer d'un écran dans l'historique |
78+| `Shift-PgDn` | Avancer d'un écran |
79+
80+Toute touche envoyée au shell ramène également la vue à l'écran vivant.
81+
82+## Souris
83+
84+| Action | Effet |
85+| --- | --- |
86+| Molette haut / bas | Défiler de trois lignes dans l'historique |
87+| Clic | Passe la fenêtre au premier plan ; n'est pas transmis au programme |
88+
89+Le rapport souris n'est pas implémenté : un programme n'est jamais informé des clics.
90+
91+## Historique
92+
93+| Propriété | Valeur |
94+| --- | --- |
95+| Lignes conservées | 2000 |
96+| Ce qui est conservé | Uniquement les lignes sorties par le haut de l'écran principal |
97+| Écran alternatif | Non conservé — un programme plein écran ne laisse aucun historique |
98+
99+## Émulation
100+
101+`TERM` vaut `xterm-256color`. Voici ce qui en est implémenté.
102+
103+### Caractères de contrôle
104+
105+| Octet | Effet |
106+| --- | --- |
107+| `0x07` BEL | Noté ; l'éditeur ne l'émet pas |
108+| `0x08` BS | Curseur d'une colonne à gauche |
109+| `0x09` HT | Jusqu'à la tabulation suivante, toutes les 8 colonnes |
110+| `0x0a` `0x0b` `0x0c` | Saut de ligne |
111+| `0x0d` CR | Colonne 1 |
112+
113+### Séquences d'échappement
114+
115+| Séquence | Nom | Effet |
116+| --- | --- | --- |
117+| `ESC D` | IND | Saut de ligne |
118+| `ESC E` | NEL | Retour chariot et saut de ligne |
119+| `ESC M` | RI | Saut de ligne inverse, en conservant la colonne |
120+| `ESC 7` | DECSC | Sauvegarder le curseur et le style |
121+| `ESC 8` | DECRC | Les restaurer |
122+| `ESC c` | RIS | Réinitialisation complète |
123+
124+### Séquences CSI
125+
126+| Séquence | Nom | Effet |
127+| --- | --- | --- |
128+| `CSI n A B C D` | CUU CUD CUF CUB | Déplacer de n cellules haut, bas, droite, gauche |
129+| `CSI n E F` | CNL CPL | n lignes plus bas / plus haut, colonne 1 |
130+| `CSI n G` | CHA | Aller à la colonne n |
131+| `CSI l ; c H`, `CSI l ; c f` | CUP HVP | Aller à la ligne l, colonne c |
132+| `CSI n d` | VPA | Aller à la ligne n |
133+| `CSI n J` | ED | Effacer l'écran : 0 jusqu'à la fin, 1 jusqu'au début, 2 ou 3 tout |
134+| `CSI n K` | EL | Effacer la ligne : 0 jusqu'à la fin, 1 jusqu'au début, 2 tout |
135+| `CSI n L` | IL | Insérer n lignes vides au curseur |
136+| `CSI n M` | DL | Supprimer n lignes au curseur |
137+| `CSI n @` | ICH | Insérer n cellules vides |
138+| `CSI n P` | DCH | Supprimer n cellules |
139+| `CSI n X` | ECH | Effacer n cellules sur place |
140+| `CSI n S` | SU | Faire défiler la région de n lignes vers le haut |
141+| `CSI n T` | SD | Faire défiler la région de n lignes vers le bas |
142+| `CSI h ; b r` | DECSTBM | Définir la région de défilement aux lignes h…b |
143+| `CSI s`, `CSI u` | SCP RCP | Sauvegarder / restaurer le curseur |
144+| `CSI … m` | SGR | Couleurs et attributs, ci-dessous |
145+
146+`IL` et `DL` ne font rien lorsque le curseur est hors de la région de défilement.
147+
148+### Modes privés
149+
150+Activés par `CSI ? n h`, désactivés par `CSI ? n l`.
151+
152+| n | Nom | Effet |
153+| --- | --- | --- |
154+| 1 | DECCKM | Touches curseur application : les flèches envoient `ESC O x` |
155+| 7 | DECAWM | Retour à la ligne automatique à la marge droite |
156+| 25 | DECTCEM | Afficher le curseur |
157+| 47, 1047 | | Écran alternatif |
158+| 1048 | | Sauvegarder / restaurer le curseur |
159+| 1049 | | Sauvegarder le curseur, puis l'écran alternatif |
160+
161+Tout autre mode est analysé et ignoré.
162+
163+### SGR
164+
165+| Code | Effet |
166+| --- | --- |
167+| 0 | Réinitialisation |
168+| 1, 22 | Gras activé / désactivé |
169+| 2, 22 | Atténué activé / désactivé |
170+| 3, 23 | Italique activé / désactivé |
171+| 4, 24 | Souligné activé / désactivé |
172+| 5, 6, 25 | Clignotement activé / désactivé |
173+| 7, 27 | Vidéo inverse activée / désactivée |
174+| 9, 29 | Barré activé / désactivé |
175+| 30–37, 40–47 | Les huit couleurs normales, premier plan / fond |
176+| 90–97, 100–107 | Les huit couleurs vives, premier plan / fond |
177+| 38;5;n, 48;5;n | Couleur n de la palette de 256 |
178+| 38;2;r;g;b, 48;2;r;g;b | Couleur 24 bits |
179+| 39, 49 | Retour à la couleur du thème |
180+
181+Les seize couleurs nommées sont celles de tcell, c'est-à-dire la palette configurée dans le terminal de l'utilisateur, et non des valeurs hexadécimales figées. Une couleur étendue à court de paramètres laisse le style inchangé. Tout autre code est ignoré.
182+
183+### OSC
184+
185+| Séquence | Effet |
186+| --- | --- |
187+| `OSC 0 ; texte BEL`, `OSC 2 ; texte BEL` | Définir le titre de la fenêtre |
188+| `OSC … ST` | Le terminateur `ESC \` est accepté à la place de BEL |
189+
190+Le titre est plafonné à 4096 octets. Les autres commandes OSC sont analysées et ignorées.
191+
192+### Consommées et ignorées
193+
194+Analysées correctement, donc jamais affichées comme des caractères parasites, mais sans effet :
195+
196+| Séquence | Nom |
197+| --- | --- |
198+| `ESC P …`, `ESC X …`, `ESC ^ …`, `ESC _ …` | DCS, SOS, PM, APC — lues jusqu'à leur terminateur de chaîne |
199+| `ESC (`, `ESC )`, `ESC *`, `ESC +`, `ESC %`, `ESC #`, `ESC <espace>` | Sélecteurs de jeu de caractères et de taille de ligne — l'émulateur travaille en UTF-8 de toute façon |
200+| `CSI ? n h`, `CSI ? n l` pour tout autre n | Modes privés non listés ci-dessus |
201+| Tout autre octet final CSI, code SGR ou commande OSC | |
202+
203+### Non implémenté
204+
205+Le rapport souris, le collage entre crochets, les bascules shift-in / shift-out, les lignes double largeur, sixel et les autres protocoles graphiques, ainsi que les rapports d'état et d'attributs DEC. Un programme qui en demande un n'obtient aucune réponse : celui qui en attend une attendra indéfiniment.
206+
207+## Couleurs
208+
209+| Clé de thème | Ce qu'elle colore |
210+| --- | --- |
211+| `terminal.text` | Toute cellule dont le programme n'a pas choisi la couleur |
212+| `terminal.cursor` | La cellule sous le curseur, quand la fenêtre a le focus |
213+
214+Voir [Format des fichiers de thème](themes.md).
215+
216+## Erreurs
217+
218+| Message | Cause |
219+| --- | --- |
220+| Terminal windows are not supported on this platform yet | La compilation n'a pas de support des pseudo-terminaux : toute plateforme autre que Linux, macOS et Windows |
221+| `openpt: …`, `grantpt: …`, `ptsname: …` | Le système d'exploitation a refusé d'ouvrir un pseudo-terminal |
222+| `fork/exec …: no such file or directory` | `$SHELL` désigne un programme inexistant |
223+
224+## Voir aussi
225+
226+- [Lancer des commandes shell sans quitter l'éditeur](../how-to/use-a-terminal.md)
227+- [Fenêtres terminal](../explanation/terminal-windows.md)
228+- [Clavier](keyboard.md)
added docs/fr/reference/themes.md +236 -0
new file mode 100644
@@ -0,0 +1,236 @@
1+# Référence : format des fichiers de thème
2+
3+> Description neutre et exhaustive d'un fichier de thème Turbo Go.
4+
5+Un thème est un fichier TOML. Les thèmes sont lus d'abord dans le répertoire utilisateur, puis parmi ceux embarqués dans le binaire ; un fichier utilisateur l'emporte sur un thème embarqué du même nom.
6+
7+## Emplacements
8+
9+| Emplacement | Remarques |
10+| --- | --- |
11+| `$TURBO_GO_THEME_DIR` | Utilisé quand la variable est définie et non vide. |
12+| `~/.config/turbo-go/themes` | Linux (`os.UserConfigDir`). |
13+| `~/Library/Application Support/turbo-go/themes` | macOS. |
14+| embarqués | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. |
15+
16+Le **nom** d'un thème pour `-theme` et pour `Options ▸ Theme…` est son nom de fichier sans `.toml`. Il ne peut contenir ni `/`, ni `\`, ni `..`.
17+
18+## Les thèmes livrés
19+
20+| Nom | Fond | Pour |
21+| --- | --- | --- |
22+| `turbo-classic` | Marine Borland | Le défaut : la palette de Turbo C |
23+| `turbo-dark` | Gris sombre neutre | Les terminaux modernes en couleurs vraies |
24+| `borland-light` | Blanc papier | Les pièces claires et la vidéoprojection |
25+| `cappuccino` | Brun expresso | La mise en page Turbo, en chaud : du lait dans le texte, du caramel là où Turbo Dark met du bleu |
26+| `catppuccin-frappe` | Ardoise chaude | La palette Catppuccin Frappé, inchangée : des accents pastel sur un fond doucement sombre |
27+| `catppuccin-latte` | Papier chaud | La palette Catppuccin Latte, inchangée : le même mappage avec la saturation qu'exige un fond clair |
28+| `cobalt` | Marine profond | La palette Cobalt, accents laissés aussi francs qu'on les connaît |
29+| `darcula` | Anthracite | D'après le Darcula de JetBrains : mots-clés orange, chaînes vertes, et la ponctuation orange qui le rend reconnaissable |
30+| `intellij-light` | Blanc | D'après l'IntelliJ Light de JetBrains : mots-clés bleus gras, chaînes vertes grasses |
31+| `monochrome-dark` | Noir et gris | Aucune teinte — le code se distingue par la luminosité, le gras, l'italique et le souligné |
32+| `monochrome-light` | Papier et gris | Le même, dans l'autre sens : sur papier, c'est le gris le plus foncé qui parle le plus fort |
33+
34+Chacun **énonce sa palette entière** au lieu d'en hériter l'essentiel. Un thème que vous écrivez, lui, peut hériter ; voir [en écrire un](../how-to/write-a-theme.md).
35+
36+## Un nom auquel un thème répondait autrefois
37+
38+`monochrome` se charge toujours. C'est le nom sous lequel ce thème était livré avant que `monochrome-light` ne le rejoigne et que la paire ne soit renommée : un fichier de réglages ou un `-theme` disant `monochrome` obtient `monochrome-dark`.
39+
40+| Nom retiré | Charge |
41+| --- | --- |
42+| `monochrome` | `monochrome-dark` |
43+
44+Un nom retiré n'est **pas** listé par `-theme` ni par **Options ▸ Theme…** : chaque thème n'apparaît donc qu'une fois, sous le nom qu'il porte aujourd'hui. Un thème à vous nommé `monochrome.toml` l'emporte quand même sur lui, exactement comme pour n'importe quel autre nom.
45+
46+## Champs de premier niveau
47+
48+| Champ | Type | Défaut | Description |
49+| --- | --- | --- | --- |
50+| `name` | chaîne | le nom du fichier | Nom affiché, dans le sélecteur de thème et la boîte À propos. |
51+| `description` | chaîne | `""` | Une ligne, affichée par `-list-themes`. |
52+| `inherits` | chaîne | aucun | Nom d'un thème dont partir. Ses styles résolus servent de base ; ce fichier redéfinit ce qu'il nomme. Les chaînes sont plafonnées à 16 sauts. |
53+| `colors` | table | `{}` | Les styles. Les clés sont celles listées plus bas. |
54+
55+## Champs d'une entrée
56+
57+Chaque valeur sous `[colors]` est une table en ligne :
58+
59+| Champ | Type | Défaut | Description |
60+| --- | --- | --- | --- |
61+| `fg` | chaîne | hérité | Couleur de premier plan. |
62+| `bg` | chaîne | hérité | Couleur de fond. |
63+| `bold` | booléen | `false` | Activer le gras. |
64+| `underline` | booléen | `false` | Activer le souligné. |
65+| `italic` | booléen | `false` | Activer l'italique. |
66+| `reverse` | booléen | `false` | Échanger premier plan et fond. |
67+| `dim` | booléen | `false` | Activer l'atténuation. |
68+| `blink` | booléen | `false` | Activer le clignotement. |
69+
70+Les attributs ne sont jamais qu'**activés** ; il n'existe pas de moyen de désactiver un attribut hérité autrement qu'en n'en héritant pas.
71+
72+## Valeurs de couleur
73+
74+| Forme | Exemple | Remarques |
75+| --- | --- | --- |
76+| Nom ANSI | `navy`, `aqua`, `silver`, `fuchsia` | Les seize noms, plus la liste W3C complète. |
77+| Littéral hexadécimal | `#5fafd7` | 24 bits ; tcell l'approxime sur les terminaux sans couleurs vraies. |
78+| `default` | `default` | Ce que le terminal utilise lui-même. |
79+| `-` | `-` | Identique à `default`. |
80+| `""` | `""` | Identique à `default`. |
81+
82+Les seize noms ANSI : `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`.
83+
84+Une couleur non reconnue est une **erreur de chargement**, pas un repli silencieux.
85+
86+## Clés de style
87+
88+Les clés non définies retombent le long des points, et finalement sur `default`.
89+
90+### Base
91+
92+| Clé | Ce qu'elle colore |
93+| --- | --- |
94+| `default` | Le dernier recours de toute recherche |
95+| `desktop` | Le fond texturé derrière les fenêtres |
96+| `shadow` | Les cellules qu'une fenêtre assombrit derrière elle |
97+
98+### Barre de menus
99+
100+| Clé | Ce qu'elle colore |
101+| --- | --- |
102+| `menu.bar` | La rangée de titres |
103+| `menu.item` | Une entrée déroulante |
104+| `menu.selected` | L'entrée surlignée |
105+| `menu.shortcut` | La lettre d'accès d'un intitulé |
106+| `menu.disabled` | Une entrée non choisissable |
107+
108+### Fenêtres
109+
110+| Clé | Ce qu'elle colore |
111+| --- | --- |
112+| `window.frame.active` | Le cadre de la fenêtre active |
113+| `window.frame.inactive` | Tous les autres cadres |
114+| `window.title.active` | Le titre de la fenêtre active |
115+| `window.title.inactive` | Tous les autres titres |
116+| `window.body` | L'intérieur, avant que son contenu ne se dessine |
117+
118+### Barres
119+
120+| Clé | Ce qu'elle colore |
121+| --- | --- |
122+| `statusbar` | La barre elle-même |
123+| `statusbar.key` | La partie `Fn` d'un indice |
124+| `statusbar.hint` | Le texte aligné à droite |
125+| `scrollbar` | La glissière d'une barre de défilement |
126+| `scrollbar.thumb` | Son curseur et ses flèches |
127+
128+### Dialogues et contrôles
129+
130+| Clé | Ce qu'elle colore |
131+| --- | --- |
132+| `dialog.frame` | Le cadre d'un dialogue |
133+| `dialog.body` | Son intérieur |
134+| `dialog.title` | Son titre |
135+| `dialog.label` | Une ligne de texte statique |
136+| `button` | Un bouton |
137+| `button.focused` | Le bouton qui a le focus |
138+| `button.shortcut` | La lettre d'accès d'un bouton |
139+| `input` | Un champ de saisie |
140+| `input.focused` | Le champ qui a le focus |
141+| `input.selection` | Le texte sélectionné dans un champ |
142+| `list` | Une liste |
143+| `list.selected` | Sa ligne surlignée, quand elle a le focus |
144+| `list.unfocused` | Sa ligne surlignée, sinon |
145+| `checkbox` | Une case à cocher |
146+| `checkbox.focused` | La case qui a le focus |
147+
148+### Éditeur
149+
150+| Clé | Ce qu'elle colore |
151+| --- | --- |
152+| `editor.text` | Le texte qu'aucune autre règle ne revendique |
153+| `editor.selection` | Le texte sélectionné |
154+| `editor.linenumber` | La gouttière des numéros de ligne |
155+| `editor.currentline` | La ligne où se trouve le curseur |
156+| `editor.cursor` | Le curseur. Son **fond** est aussi envoyé au terminal comme couleur de curseur, et son premier plan peint le caractère en dessous. |
157+
158+### Terminal
159+
160+| Clé | Ce qu'elle colore |
161+| --- | --- |
162+| `terminal.text` | Toute cellule d'une fenêtre terminal dont le programme qui y tourne n'a pas choisi la couleur |
163+| `terminal.cursor` | La cellule sous le curseur d'un terminal, quand cette fenêtre a le focus |
164+
165+Un programme qui nomme ses propres couleurs les conserve : ces deux clés ne remplissent que ce qu'il a laissé indéfini. Voir [Fenêtres terminal](terminal.md).
166+
167+### Arbre du projet
168+
169+| Clé | Ce qu'elle colore |
170+| --- | --- |
171+| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre |
172+| `tree.directory` | Le nom d'un dossier |
173+| `tree.selected` | La ligne surlignée, quand l'arbre a le focus |
174+| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas |
175+
176+Elles sont distinctes des clés `list.*` à dessein : la liste d'un dialogue est colorée pour ressortir sur un dialogue, et la réutiliser surlignerait une ligne d'arbre dans la couleur même qu'a déjà le corps d'une fenêtre. Voir [Arbre du projet](project-tree.md).
177+
178+### Syntaxe
179+
180+| Clé | Ce qu'elle colore |
181+| --- | --- |
182+| `syntax.identifier` | Un nom ordinaire |
183+| `syntax.keyword` | `func`, `if`, `package`, … |
184+| `syntax.type` | `int`, `string`, et un nom après `type` |
185+| `syntax.builtin` | `len`, `append`, `make`, … |
186+| `syntax.constant` | `true`, `false`, `nil`, `iota` |
187+| `syntax.function` | Un nom avant `(`, ou après `func` |
188+| `syntax.string` | Un littéral chaîne |
189+| `syntax.char` | Un littéral rune |
190+| `syntax.number` | Un littéral entier, flottant ou imaginaire |
191+| `syntax.comment` | `//` et `/* */` |
192+| `syntax.operator` | `+`, `:=`, `<-`, … |
193+| `syntax.punctuation` | Parenthèses, virgules, points, points-virgules |
194+| `syntax.heading` | Un titre Markdown, toute la ligne |
195+| `syntax.tag` | Un nom d'élément HTML et ses chevrons |
196+| `syntax.attribute` | Le nom d'un attribut HTML |
197+| `syntax.emphasis` | Le gras et l'italique Markdown |
198+| `syntax.link` | Un lien ou une image Markdown |
199+
200+Quel langage produit quelle classe est indiqué dans [Langages colorés](languages.md).
201+
202+### Complétion et diagnostics
203+
204+| Clé | Ce qu'elle colore |
205+| --- | --- |
206+| `completion.frame` | Le cadre de la liste |
207+| `completion.item` | Une suggestion |
208+| `completion.selected` | La suggestion surlignée |
209+| `completion.detail` | L'étiquette de nature à côté d'une suggestion |
210+| `diagnostic.error` | Une erreur du serveur de langage |
211+| `diagnostic.warning` | Un avertissement |
212+| `diagnostic.info` | Une note |
213+
214+## Exemple
215+
216+```toml
217+name = "Le mien"
218+description = "Turbo Classic, avec des commentaires lisibles."
219+inherits = "turbo-classic"
220+
221+[colors]
222+"syntax.comment" = { fg = "#8a8a8a", italic = true }
223+"syntax.string" = { fg = "#87d7af" }
224+"editor.currentline" = { bg = "#00005f" }
225+```
226+
227+## Erreurs
228+
229+| Message | Cause |
230+| --- | --- |
231+| `theme: not found: "x"` | Aucun `x.toml` dans le répertoire utilisateur ni parmi les thèmes embarqués. |
232+| `theme: not found: "…" is not a plain theme name` | Le nom contient `/`, `\` ou `..`. |
233+| `invalid TOML: …` | Le fichier n'est pas du TOML valide. |
234+| `colors."k": fg: unknown colour "…"` | Le nom de couleur n'est pas reconnu. |
235+| `inherits: chain deeper than 16, probably a loop` | Deux thèmes héritent l'un de l'autre, directement ou par l'intermédiaire d'autres. |
236+| `inherits "x": theme: not found` | Le parent nommé n'existe pas. |
new file mode 100644
@@ -0,0 +1,236 @@
1+# Référence : format des fichiers de thème
2+
3+> Description neutre et exhaustive d'un fichier de thème Turbo Go.
4+
5+Un thème est un fichier TOML. Les thèmes sont lus d'abord dans le répertoire utilisateur, puis parmi ceux embarqués dans le binaire ; un fichier utilisateur l'emporte sur un thème embarqué du même nom.
6+
7+## Emplacements
8+
9+| Emplacement | Remarques |
10+| --- | --- |
11+| `$TURBO_GO_THEME_DIR` | Utilisé quand la variable est définie et non vide. |
12+| `~/.config/turbo-go/themes` | Linux (`os.UserConfigDir`). |
13+| `~/Library/Application Support/turbo-go/themes` | macOS. |
14+| embarqués | `turbo-classic`, `turbo-dark`, `borland-light`, `cappuccino`, `catppuccin-frappe`, `catppuccin-latte`, `cobalt`, `darcula`, `intellij-light`, `monochrome-dark`, `monochrome-light`. |
15+
16+Le **nom** d'un thème pour `-theme` et pour `Options ▸ Theme…` est son nom de fichier sans `.toml`. Il ne peut contenir ni `/`, ni `\`, ni `..`.
17+
18+## Les thèmes livrés
19+
20+| Nom | Fond | Pour |
21+| --- | --- | --- |
22+| `turbo-classic` | Marine Borland | Le défaut : la palette de Turbo C |
23+| `turbo-dark` | Gris sombre neutre | Les terminaux modernes en couleurs vraies |
24+| `borland-light` | Blanc papier | Les pièces claires et la vidéoprojection |
25+| `cappuccino` | Brun expresso | La mise en page Turbo, en chaud : du lait dans le texte, du caramel là où Turbo Dark met du bleu |
26+| `catppuccin-frappe` | Ardoise chaude | La palette Catppuccin Frappé, inchangée : des accents pastel sur un fond doucement sombre |
27+| `catppuccin-latte` | Papier chaud | La palette Catppuccin Latte, inchangée : le même mappage avec la saturation qu'exige un fond clair |
28+| `cobalt` | Marine profond | La palette Cobalt, accents laissés aussi francs qu'on les connaît |
29+| `darcula` | Anthracite | D'après le Darcula de JetBrains : mots-clés orange, chaînes vertes, et la ponctuation orange qui le rend reconnaissable |
30+| `intellij-light` | Blanc | D'après l'IntelliJ Light de JetBrains : mots-clés bleus gras, chaînes vertes grasses |
31+| `monochrome-dark` | Noir et gris | Aucune teinte — le code se distingue par la luminosité, le gras, l'italique et le souligné |
32+| `monochrome-light` | Papier et gris | Le même, dans l'autre sens : sur papier, c'est le gris le plus foncé qui parle le plus fort |
33+
34+Chacun **énonce sa palette entière** au lieu d'en hériter l'essentiel. Un thème que vous écrivez, lui, peut hériter ; voir [en écrire un](../how-to/write-a-theme.md).
35+
36+## Un nom auquel un thème répondait autrefois
37+
38+`monochrome` se charge toujours. C'est le nom sous lequel ce thème était livré avant que `monochrome-light` ne le rejoigne et que la paire ne soit renommée : un fichier de réglages ou un `-theme` disant `monochrome` obtient `monochrome-dark`.
39+
40+| Nom retiré | Charge |
41+| --- | --- |
42+| `monochrome` | `monochrome-dark` |
43+
44+Un nom retiré n'est **pas** listé par `-theme` ni par **Options ▸ Theme…** : chaque thème n'apparaît donc qu'une fois, sous le nom qu'il porte aujourd'hui. Un thème à vous nommé `monochrome.toml` l'emporte quand même sur lui, exactement comme pour n'importe quel autre nom.
45+
46+## Champs de premier niveau
47+
48+| Champ | Type | Défaut | Description |
49+| --- | --- | --- | --- |
50+| `name` | chaîne | le nom du fichier | Nom affiché, dans le sélecteur de thème et la boîte À propos. |
51+| `description` | chaîne | `""` | Une ligne, affichée par `-list-themes`. |
52+| `inherits` | chaîne | aucun | Nom d'un thème dont partir. Ses styles résolus servent de base ; ce fichier redéfinit ce qu'il nomme. Les chaînes sont plafonnées à 16 sauts. |
53+| `colors` | table | `{}` | Les styles. Les clés sont celles listées plus bas. |
54+
55+## Champs d'une entrée
56+
57+Chaque valeur sous `[colors]` est une table en ligne :
58+
59+| Champ | Type | Défaut | Description |
60+| --- | --- | --- | --- |
61+| `fg` | chaîne | hérité | Couleur de premier plan. |
62+| `bg` | chaîne | hérité | Couleur de fond. |
63+| `bold` | booléen | `false` | Activer le gras. |
64+| `underline` | booléen | `false` | Activer le souligné. |
65+| `italic` | booléen | `false` | Activer l'italique. |
66+| `reverse` | booléen | `false` | Échanger premier plan et fond. |
67+| `dim` | booléen | `false` | Activer l'atténuation. |
68+| `blink` | booléen | `false` | Activer le clignotement. |
69+
70+Les attributs ne sont jamais qu'**activés** ; il n'existe pas de moyen de désactiver un attribut hérité autrement qu'en n'en héritant pas.
71+
72+## Valeurs de couleur
73+
74+| Forme | Exemple | Remarques |
75+| --- | --- | --- |
76+| Nom ANSI | `navy`, `aqua`, `silver`, `fuchsia` | Les seize noms, plus la liste W3C complète. |
77+| Littéral hexadécimal | `#5fafd7` | 24 bits ; tcell l'approxime sur les terminaux sans couleurs vraies. |
78+| `default` | `default` | Ce que le terminal utilise lui-même. |
79+| `-` | `-` | Identique à `default`. |
80+| `""` | `""` | Identique à `default`. |
81+
82+Les seize noms ANSI : `black` `maroon` `green` `olive` `navy` `purple` `teal` `silver` `gray` `red` `lime` `yellow` `blue` `fuchsia` `aqua` `white`.
83+
84+Une couleur non reconnue est une **erreur de chargement**, pas un repli silencieux.
85+
86+## Clés de style
87+
88+Les clés non définies retombent le long des points, et finalement sur `default`.
89+
90+### Base
91+
92+| Clé | Ce qu'elle colore |
93+| --- | --- |
94+| `default` | Le dernier recours de toute recherche |
95+| `desktop` | Le fond texturé derrière les fenêtres |
96+| `shadow` | Les cellules qu'une fenêtre assombrit derrière elle |
97+
98+### Barre de menus
99+
100+| Clé | Ce qu'elle colore |
101+| --- | --- |
102+| `menu.bar` | La rangée de titres |
103+| `menu.item` | Une entrée déroulante |
104+| `menu.selected` | L'entrée surlignée |
105+| `menu.shortcut` | La lettre d'accès d'un intitulé |
106+| `menu.disabled` | Une entrée non choisissable |
107+
108+### Fenêtres
109+
110+| Clé | Ce qu'elle colore |
111+| --- | --- |
112+| `window.frame.active` | Le cadre de la fenêtre active |
113+| `window.frame.inactive` | Tous les autres cadres |
114+| `window.title.active` | Le titre de la fenêtre active |
115+| `window.title.inactive` | Tous les autres titres |
116+| `window.body` | L'intérieur, avant que son contenu ne se dessine |
117+
118+### Barres
119+
120+| Clé | Ce qu'elle colore |
121+| --- | --- |
122+| `statusbar` | La barre elle-même |
123+| `statusbar.key` | La partie `Fn` d'un indice |
124+| `statusbar.hint` | Le texte aligné à droite |
125+| `scrollbar` | La glissière d'une barre de défilement |
126+| `scrollbar.thumb` | Son curseur et ses flèches |
127+
128+### Dialogues et contrôles
129+
130+| Clé | Ce qu'elle colore |
131+| --- | --- |
132+| `dialog.frame` | Le cadre d'un dialogue |
133+| `dialog.body` | Son intérieur |
134+| `dialog.title` | Son titre |
135+| `dialog.label` | Une ligne de texte statique |
136+| `button` | Un bouton |
137+| `button.focused` | Le bouton qui a le focus |
138+| `button.shortcut` | La lettre d'accès d'un bouton |
139+| `input` | Un champ de saisie |
140+| `input.focused` | Le champ qui a le focus |
141+| `input.selection` | Le texte sélectionné dans un champ |
142+| `list` | Une liste |
143+| `list.selected` | Sa ligne surlignée, quand elle a le focus |
144+| `list.unfocused` | Sa ligne surlignée, sinon |
145+| `checkbox` | Une case à cocher |
146+| `checkbox.focused` | La case qui a le focus |
147+
148+### Éditeur
149+
150+| Clé | Ce qu'elle colore |
151+| --- | --- |
152+| `editor.text` | Le texte qu'aucune autre règle ne revendique |
153+| `editor.selection` | Le texte sélectionné |
154+| `editor.linenumber` | La gouttière des numéros de ligne |
155+| `editor.currentline` | La ligne où se trouve le curseur |
156+| `editor.cursor` | Le curseur. Son **fond** est aussi envoyé au terminal comme couleur de curseur, et son premier plan peint le caractère en dessous. |
157+
158+### Terminal
159+
160+| Clé | Ce qu'elle colore |
161+| --- | --- |
162+| `terminal.text` | Toute cellule d'une fenêtre terminal dont le programme qui y tourne n'a pas choisi la couleur |
163+| `terminal.cursor` | La cellule sous le curseur d'un terminal, quand cette fenêtre a le focus |
164+
165+Un programme qui nomme ses propres couleurs les conserve : ces deux clés ne remplissent que ce qu'il a laissé indéfini. Voir [Fenêtres terminal](terminal.md).
166+
167+### Arbre du projet
168+
169+| Clé | Ce qu'elle colore |
170+| --- | --- |
171+| `tree.text` | Le nom d'un fichier dans l'arbre, et le fond de l'arbre |
172+| `tree.directory` | Le nom d'un dossier |
173+| `tree.selected` | La ligne surlignée, quand l'arbre a le focus |
174+| `tree.unfocused` | La ligne surlignée, quand il ne l'a pas |
175+
176+Elles sont distinctes des clés `list.*` à dessein : la liste d'un dialogue est colorée pour ressortir sur un dialogue, et la réutiliser surlignerait une ligne d'arbre dans la couleur même qu'a déjà le corps d'une fenêtre. Voir [Arbre du projet](project-tree.md).
177+
178+### Syntaxe
179+
180+| Clé | Ce qu'elle colore |
181+| --- | --- |
182+| `syntax.identifier` | Un nom ordinaire |
183+| `syntax.keyword` | `func`, `if`, `package`, … |
184+| `syntax.type` | `int`, `string`, et un nom après `type` |
185+| `syntax.builtin` | `len`, `append`, `make`, … |
186+| `syntax.constant` | `true`, `false`, `nil`, `iota` |
187+| `syntax.function` | Un nom avant `(`, ou après `func` |
188+| `syntax.string` | Un littéral chaîne |
189+| `syntax.char` | Un littéral rune |
190+| `syntax.number` | Un littéral entier, flottant ou imaginaire |
191+| `syntax.comment` | `//` et `/* */` |
192+| `syntax.operator` | `+`, `:=`, `<-`, … |
193+| `syntax.punctuation` | Parenthèses, virgules, points, points-virgules |
194+| `syntax.heading` | Un titre Markdown, toute la ligne |
195+| `syntax.tag` | Un nom d'élément HTML et ses chevrons |
196+| `syntax.attribute` | Le nom d'un attribut HTML |
197+| `syntax.emphasis` | Le gras et l'italique Markdown |
198+| `syntax.link` | Un lien ou une image Markdown |
199+
200+Quel langage produit quelle classe est indiqué dans [Langages colorés](languages.md).
201+
202+### Complétion et diagnostics
203+
204+| Clé | Ce qu'elle colore |
205+| --- | --- |
206+| `completion.frame` | Le cadre de la liste |
207+| `completion.item` | Une suggestion |
208+| `completion.selected` | La suggestion surlignée |
209+| `completion.detail` | L'étiquette de nature à côté d'une suggestion |
210+| `diagnostic.error` | Une erreur du serveur de langage |
211+| `diagnostic.warning` | Un avertissement |
212+| `diagnostic.info` | Une note |
213+
214+## Exemple
215+
216+```toml
217+name = "Le mien"
218+description = "Turbo Classic, avec des commentaires lisibles."
219+inherits = "turbo-classic"
220+
221+[colors]
222+"syntax.comment" = { fg = "#8a8a8a", italic = true }
223+"syntax.string" = { fg = "#87d7af" }
224+"editor.currentline" = { bg = "#00005f" }
225+```
226+
227+## Erreurs
228+
229+| Message | Cause |
230+| --- | --- |
231+| `theme: not found: "x"` | Aucun `x.toml` dans le répertoire utilisateur ni parmi les thèmes embarqués. |
232+| `theme: not found: "…" is not a plain theme name` | Le nom contient `/`, `\` ou `..`. |
233+| `invalid TOML: …` | Le fichier n'est pas du TOML valide. |
234+| `colors."k": fg: unknown colour "…"` | Le nom de couleur n'est pas reconnu. |
235+| `inherits: chain deeper than 16, probably a loop` | Deux thèmes héritent l'un de l'autre, directement ou par l'intermédiaire d'autres. |
236+| `inherits "x": theme: not found` | Le parent nommé n'existe pas. |
added docs/fr/reference/versioning.md +150 -0
new file mode 100644
@@ -0,0 +1,150 @@
1+# Référence : le numéro de version
2+
3+> Description neutre de l'origine de la version que Turbo Go annonce, et de ce que produit chaque façon de le construire.
4+
5+## D'où vient le numéro
6+
7+Trois sources, consultées dans cet ordre. La première qui répond l'emporte.
8+
9+| Ordre | Source | Renseignée par |
10+| --- | --- | --- |
11+| 1 | Estampilles de l'éditeur de liens | `make build`, `make install`, `scripts/install.sh` |
12+| 2 | Informations de build de Go | L'outil Go, automatiquement |
13+| 3 | `unknown` | Rien — la valeur annoncée quand aucune source n'a pu nommer le build |
14+
15+Il n'y a **aucune constante de version dans les sources**. Un numéro écrit dans un fichier `.go` doit être modifié dans le cadre d'une release, et devient faux dès que quelqu'un l'oublie.
16+
17+## Estampilles de l'éditeur de liens
18+
19+Trois variables de paquet dans le paquet `version` de turbo-core, renseignées par `-ldflags -X`.
20+
21+| Variable | Remplie depuis | Exemple |
22+| --- | --- | --- |
23+| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` |
24+| `commit` | `git rev-parse --short HEAD` | `88a4c38` |
25+| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-08-31T18:04:05Z` |
26+
27+```sh
28+go build -ldflags "\
29+ -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' \
30+ -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \
31+ -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-08-31T18:04:05Z'" .
32+```
33+
34+Un `v` initial est retiré à l'affichage : le tag est `v0.2.0`, la boîte About affiche `0.2.0`.
35+
36+## Informations de build de Go
37+
38+Lues via `runtime/debug.ReadBuildInfo()` quand rien n'a été estampillé.
39+
40+| Champ lu | Sert à |
41+| --- | --- |
42+| `Main.Version` | Le numéro, sauf s'il est vide, `(devel)`, ou une pseudo-version |
43+| `vcs.revision` | Le commit, abrégé à sept caractères |
44+| `vcs.modified` | L'ajout ou non du suffixe `-dirty` |
45+
46+`vcs.time` n'est **pas** utilisé. Il enregistre la date du commit, pas celle de l'édition de liens ; l'annoncer comme date de build serait faux sur tout binaire construit après son propre commit.
47+
48+Une **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — est la façon dont l'outil Go nomme un commit qu'aucun tag ne nomme. Elle est rapportée comme `devel`, et non affichée telle quelle : son `0.1.1` est un correctif qui n'existe pas.
49+
50+## Ce qu'annonce chaque build
51+
52+| Construit par | Numéro | Commit | Date |
53+| --- | --- | --- | --- |
54+| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | oui | oui |
55+| Les mêmes, sur un commit tagué | `0.2.0` | oui | oui |
56+| Les mêmes, avec des modifications non validées | `0.1.0-14-g88a4c38-dirty` | oui | oui |
57+| `go install rickub.com/turbo-editors/turbo-go@v0.2.0` | `0.2.0` | non | non |
58+| `go build .` dans un dépôt cloné | `devel` | oui | non |
59+| `go build .` dans un dépôt cloné avec des modifications | `devel-dirty` | oui | non |
60+| `go run .` | `unknown` | non | non |
61+| Un dossier sans git, et sans estampille | `unknown` | non | non |
62+
63+Seules les lignes estampillées peuvent annoncer un tag : le système de build de Go ne lit pas les tags git.
64+
65+## Vérifié au moment du build
66+
67+Une estampille d'édition de liens est une chaîne de caractères, et une mauvaise n'est pas une erreur. Un `-X` qui nomme un symbole inexistant s'édite sans se plaindre et n'estampille rien ; le binaire retombe alors sur les informations de build de Go et annonce une version que le build n'a jamais voulue — souvent `devel`, sur un binaire attaché à une release. Rien d'autre que l'exécution du binaire ne le détecte : chaque build qui en produit un l'exécute donc.
68+
69+C'est `scripts/check-version.sh` qui s'en charge.
70+
71+| Appelé par | Sur | Un échec fait échouer |
72+| --- | --- | --- |
73+| `make build` | `bin/turbo-go`, avec `$(VERSION)` et `$(COMMIT)` | le build |
74+| `scripts/install.sh` | le binaire en attente, **avant** son installation | l'installation, en laissant intact celui qui est déjà là |
75+| `02-build-releases.sh` | le seul artefact en attente que cette machine sait exécuter, avec le tag | la construction de la release |
76+
77+```sh
78+scripts/check-version.sh bin/turbo-go v0.2.0 88a4c38 # un build estampillé
79+scripts/check-version.sh bin/turbo-go # rien à attendre
80+```
81+
82+| Arguments | Réussit si |
83+| --- | --- |
84+| binaire, version, commit | le numéro annoncé est **égal** à la version privée de son `v` initial, et le commit apparaît dans la sortie |
85+| binaire, version | le numéro lui est égal |
86+| binaire | le numéro est autre chose qu'`unknown` |
87+
88+La comparaison de version est une égalité, pas une recherche. `0.2.0` est une sous-chaîne de `10.2.0`, et d'une empreinte de commit qui le contiendrait par hasard ; une estampille presque juste est précisément ce que cette vérification existe pour attraper.
89+
90+| Code de sortie | Signification |
91+| --- | --- |
92+| `0` | Le binaire annonce ce que le build voulait. La ligne qu'il a affichée est réémise. |
93+| `1` | Il ne s'exécute pas, n'est pas là, ou annonce autre chose. |
94+| `2` | Aucun binaire n'a été nommé. |
95+
96+## Où il s'affiche
97+
98+### `-version`
99+
100+Une ligne, portant chaque élément connu.
101+
102+```
103+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
104+Turbo Go 0.2.0 (88a4c38)
105+Turbo Go 0.2.0
106+```
107+
108+### Help ▸ About
109+
110+Une ligne par fait connu. Un fait que le build n'a pas enregistré n'a **pas de ligne**, plutôt qu'une ligne vide.
111+
112+```
113+Turbo Go 0.2.0
114+
115+A Turbo C-style editor for Go,
116+written in Go.
117+
118+Commit: 88a4c38
119+Built: 2026-08-31 18:04 UTC
120+Theme: Turbo Classic
121+```
122+
123+`Built` est rendu en UTC sous la forme `AAAA-MM-JJ HH:MM UTC`. Une estampille qui n'est pas un RFC 3339 valide est affichée exactement telle qu'elle a été donnée, plutôt que supprimée.
124+
125+### `make version`
126+
127+Affiche ce que ce dépôt estampillerait, sans construire.
128+
129+```
130+$ make version
131+v0.1.0-14-g88a4c38 (88a4c38)
132+```
133+
134+### `make ldflags`
135+
136+Affiche les options d'édition de liens qu'utilise un build estampillé, pour qu'un script puisse les réutiliser au lieu de répéter les chemins `-X`.
137+
138+```
139+$ make ldflags
140+-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' -X '….commit=7f8b36a' -X '….built=2026-08-31T19:02:03Z'
141+```
142+
143+`02-build-releases.sh` les lit pour ses compilations croisées, en surchargeant la version par le tag qu'il publie — `make ldflags VERSION=v0.2.0` — pour que les binaires disent ce que dit la release plutôt que ce que dit `git describe`. Un binaire compilé sans elles annonce `devel`, quoi que dise la release à laquelle il est attaché.
144+
145+## Voir aussi
146+
147+- Faire une release pour que le numéro soit juste : [Comment faire une release](../how-to/make-a-release.md)
148+- Ce à quoi `-version` ne sert **pas** : il est écrit pour un humain. Un script qui a besoin du numéro doit comparer avec `grep -F`, ou interroger git, plutôt que d'en extraire un champ.
149+- Pourquoi il n'y a pas de constante de version : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources)
150+- L'option `-version` parmi les autres : [Ligne de commande](cli.md)
new file mode 100644
@@ -0,0 +1,150 @@
1+# Référence : le numéro de version
2+
3+> Description neutre de l'origine de la version que Turbo Go annonce, et de ce que produit chaque façon de le construire.
4+
5+## D'où vient le numéro
6+
7+Trois sources, consultées dans cet ordre. La première qui répond l'emporte.
8+
9+| Ordre | Source | Renseignée par |
10+| --- | --- | --- |
11+| 1 | Estampilles de l'éditeur de liens | `make build`, `make install`, `scripts/install.sh` |
12+| 2 | Informations de build de Go | L'outil Go, automatiquement |
13+| 3 | `unknown` | Rien — la valeur annoncée quand aucune source n'a pu nommer le build |
14+
15+Il n'y a **aucune constante de version dans les sources**. Un numéro écrit dans un fichier `.go` doit être modifié dans le cadre d'une release, et devient faux dès que quelqu'un l'oublie.
16+
17+## Estampilles de l'éditeur de liens
18+
19+Trois variables de paquet dans le paquet `version` de turbo-core, renseignées par `-ldflags -X`.
20+
21+| Variable | Remplie depuis | Exemple |
22+| --- | --- | --- |
23+| `stamp` | `git describe --tags --dirty` | `v0.1.0-14-g88a4c38` |
24+| `commit` | `git rev-parse --short HEAD` | `88a4c38` |
25+| `built` | `date -u +%Y-%m-%dT%H:%M:%SZ` | `2026-08-31T18:04:05Z` |
26+
27+```sh
28+go build -ldflags "\
29+ -X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' \
30+ -X 'rickub.com/turbo-editors/turbo-core/version.commit=88a4c38' \
31+ -X 'rickub.com/turbo-editors/turbo-core/version.built=2026-08-31T18:04:05Z'" .
32+```
33+
34+Un `v` initial est retiré à l'affichage : le tag est `v0.2.0`, la boîte About affiche `0.2.0`.
35+
36+## Informations de build de Go
37+
38+Lues via `runtime/debug.ReadBuildInfo()` quand rien n'a été estampillé.
39+
40+| Champ lu | Sert à |
41+| --- | --- |
42+| `Main.Version` | Le numéro, sauf s'il est vide, `(devel)`, ou une pseudo-version |
43+| `vcs.revision` | Le commit, abrégé à sept caractères |
44+| `vcs.modified` | L'ajout ou non du suffixe `-dirty` |
45+
46+`vcs.time` n'est **pas** utilisé. Il enregistre la date du commit, pas celle de l'édition de liens ; l'annoncer comme date de build serait faux sur tout binaire construit après son propre commit.
47+
48+Une **pseudo-version**`v0.1.1-0.20260831165958-88a4c3859bf3` — est la façon dont l'outil Go nomme un commit qu'aucun tag ne nomme. Elle est rapportée comme `devel`, et non affichée telle quelle : son `0.1.1` est un correctif qui n'existe pas.
49+
50+## Ce qu'annonce chaque build
51+
52+| Construit par | Numéro | Commit | Date |
53+| --- | --- | --- | --- |
54+| `make build`, `make install`, `scripts/install.sh` | `0.1.0-14-g88a4c38` | oui | oui |
55+| Les mêmes, sur un commit tagué | `0.2.0` | oui | oui |
56+| Les mêmes, avec des modifications non validées | `0.1.0-14-g88a4c38-dirty` | oui | oui |
57+| `go install rickub.com/turbo-editors/turbo-go@v0.2.0` | `0.2.0` | non | non |
58+| `go build .` dans un dépôt cloné | `devel` | oui | non |
59+| `go build .` dans un dépôt cloné avec des modifications | `devel-dirty` | oui | non |
60+| `go run .` | `unknown` | non | non |
61+| Un dossier sans git, et sans estampille | `unknown` | non | non |
62+
63+Seules les lignes estampillées peuvent annoncer un tag : le système de build de Go ne lit pas les tags git.
64+
65+## Vérifié au moment du build
66+
67+Une estampille d'édition de liens est une chaîne de caractères, et une mauvaise n'est pas une erreur. Un `-X` qui nomme un symbole inexistant s'édite sans se plaindre et n'estampille rien ; le binaire retombe alors sur les informations de build de Go et annonce une version que le build n'a jamais voulue — souvent `devel`, sur un binaire attaché à une release. Rien d'autre que l'exécution du binaire ne le détecte : chaque build qui en produit un l'exécute donc.
68+
69+C'est `scripts/check-version.sh` qui s'en charge.
70+
71+| Appelé par | Sur | Un échec fait échouer |
72+| --- | --- | --- |
73+| `make build` | `bin/turbo-go`, avec `$(VERSION)` et `$(COMMIT)` | le build |
74+| `scripts/install.sh` | le binaire en attente, **avant** son installation | l'installation, en laissant intact celui qui est déjà là |
75+| `02-build-releases.sh` | le seul artefact en attente que cette machine sait exécuter, avec le tag | la construction de la release |
76+
77+```sh
78+scripts/check-version.sh bin/turbo-go v0.2.0 88a4c38 # un build estampillé
79+scripts/check-version.sh bin/turbo-go # rien à attendre
80+```
81+
82+| Arguments | Réussit si |
83+| --- | --- |
84+| binaire, version, commit | le numéro annoncé est **égal** à la version privée de son `v` initial, et le commit apparaît dans la sortie |
85+| binaire, version | le numéro lui est égal |
86+| binaire | le numéro est autre chose qu'`unknown` |
87+
88+La comparaison de version est une égalité, pas une recherche. `0.2.0` est une sous-chaîne de `10.2.0`, et d'une empreinte de commit qui le contiendrait par hasard ; une estampille presque juste est précisément ce que cette vérification existe pour attraper.
89+
90+| Code de sortie | Signification |
91+| --- | --- |
92+| `0` | Le binaire annonce ce que le build voulait. La ligne qu'il a affichée est réémise. |
93+| `1` | Il ne s'exécute pas, n'est pas là, ou annonce autre chose. |
94+| `2` | Aucun binaire n'a été nommé. |
95+
96+## Où il s'affiche
97+
98+### `-version`
99+
100+Une ligne, portant chaque élément connu.
101+
102+```
103+Turbo Go 0.2.0 (88a4c38, built 2026-08-31T18:04:05Z)
104+Turbo Go 0.2.0 (88a4c38)
105+Turbo Go 0.2.0
106+```
107+
108+### Help ▸ About
109+
110+Une ligne par fait connu. Un fait que le build n'a pas enregistré n'a **pas de ligne**, plutôt qu'une ligne vide.
111+
112+```
113+Turbo Go 0.2.0
114+
115+A Turbo C-style editor for Go,
116+written in Go.
117+
118+Commit: 88a4c38
119+Built: 2026-08-31 18:04 UTC
120+Theme: Turbo Classic
121+```
122+
123+`Built` est rendu en UTC sous la forme `AAAA-MM-JJ HH:MM UTC`. Une estampille qui n'est pas un RFC 3339 valide est affichée exactement telle qu'elle a été donnée, plutôt que supprimée.
124+
125+### `make version`
126+
127+Affiche ce que ce dépôt estampillerait, sans construire.
128+
129+```
130+$ make version
131+v0.1.0-14-g88a4c38 (88a4c38)
132+```
133+
134+### `make ldflags`
135+
136+Affiche les options d'édition de liens qu'utilise un build estampillé, pour qu'un script puisse les réutiliser au lieu de répéter les chemins `-X`.
137+
138+```
139+$ make ldflags
140+-X 'rickub.com/turbo-editors/turbo-core/version.stamp=v0.2.0' -X '….commit=7f8b36a' -X '….built=2026-08-31T19:02:03Z'
141+```
142+
143+`02-build-releases.sh` les lit pour ses compilations croisées, en surchargeant la version par le tag qu'il publie — `make ldflags VERSION=v0.2.0` — pour que les binaires disent ce que dit la release plutôt que ce que dit `git describe`. Un binaire compilé sans elles annonce `devel`, quoi que dise la release à laquelle il est attaché.
144+
145+## Voir aussi
146+
147+- Faire une release pour que le numéro soit juste : [Comment faire une release](../how-to/make-a-release.md)
148+- Ce à quoi `-version` ne sert **pas** : il est écrit pour un humain. Un script qui a besoin du numéro doit comparer avec `grep -F`, ou interroger git, plutôt que d'en extraire un champ.
149+- Pourquoi il n'y a pas de constante de version : [Décisions de conception](../explanation/design-decisions.md#la-version-est-une-propriété-du-build-pas-des-sources)
150+- L'option `-version` parmi les autres : [Ligne de commande](cli.md)
added docs/fr/tutorials/getting-started.md +202 -0
new file mode 100644
@@ -0,0 +1,202 @@
1+# Tutoriel : votre premier fichier dans Turbo Go
2+
3+À la fin de ce tutoriel, vous aurez compilé l'éditeur, écrit un petit programme Go à l'intérieur, vu les mots-clés se colorer au fil de la frappe, enregistré le fichier et exécuté le programme. Comptez une dizaine de minutes.
4+
5+Aucune connaissance préalable de Turbo Go n'est nécessaire. Il vous faut Go 1.26 ou plus récent et un terminal — rien d'autre.
6+
7+## Prérequis
8+
9+Vérifiez que Go est présent :
10+
11+```bash
12+go version
13+```
14+
15+Vous devriez voir quelque chose comme :
16+
17+```
18+go version go1.26.5 linux/arm64
19+```
20+
21+Si cette commande échoue, installez d'abord Go : https://go.dev/dl/
22+
23+## Étape 1 — Compiler l'éditeur
24+
25+Depuis le répertoire du projet, tapez :
26+
27+```bash
28+make build
29+```
30+
31+Vous devriez voir :
32+
33+```
34+go build -o bin/turbo-go .
35+```
36+
37+puis plus rien. Le silence est le signe du succès : Go ne dit rien quand une compilation réussit.
38+
39+Nous avons maintenant un exécutable dans `bin/turbo-go`. Retenons où il est, pour pouvoir le lancer de n'importe où :
40+
41+```bash
42+export TURBO="$PWD/bin/turbo-go"
43+```
44+
45+## Étape 2 — Créer un endroit où travailler
46+
47+Turbo Go donne le meilleur de lui-même à l'intérieur d'un module Go ; créons-en un :
48+
49+```bash
50+mkdir -p /tmp/hello && cd /tmp/hello
51+go mod init hello
52+```
53+
54+Vous devriez voir :
55+
56+```
57+go: creating new go.mod: module hello
58+```
59+
60+Nous venons de créer un module Go vide.
61+
62+## Étape 3 — Ouvrir l'éditeur
63+
64+Lancez Turbo Go sur un fichier qui n'existe pas encore :
65+
66+```bash
67+$TURBO main.go
68+```
69+
70+L'écran se remplit d'un bureau bleu. Vous devriez voir :
71+
72+- une **barre de menus** en haut : `File Edit Search Run Code Options Window Snippets Go Help`
73+- une **fenêtre** encadrée d'un double trait, intitulée `main.go`
74+- une **barre d'état** en bas : `F1 Describe F2 Save F3 Open …`
75+
76+Le curseur clignote ligne 1, colonne 1 — la barre d'état affiche `1:1` à droite.
77+
78+Nous sommes dans l'éditeur.
79+
80+## Étape 4 — Écrire un programme Go
81+
82+Tapez ces cinq lignes, en appuyant sur Entrée à la fin de chacune :
83+
84+```go
85+package main
86+
87+import "fmt"
88+
89+func main() {
90+```
91+
92+Observez les couleurs pendant que vous tapez. `package`, `import` et `func` deviennent **blanc gras** dès que le mot est terminé : ce sont des mots-clés. `"fmt"` devient **vert** : c'est une chaîne. `main` devient **jaune gras** dès que vous tapez la `(` qui le suit, car cela en fait une fonction.
93+
94+Appuyez maintenant sur **Tab**. Le curseur saute en colonne 9 — la barre d'état à droite passe à `5:2`, parce qu'une tabulation occupe un seul caractère dans le fichier même si elle remplit huit colonnes à l'écran.
95+
96+Tapez le corps de la fonction :
97+
98+```go
99+fmt.Println("Hello from Turbo Go!")
100+```
101+
102+> Quand vous tapez le `.` après `fmt`, la barre d'état affiche brièvement un message à propos du serveur de langage. C'est normal : la complétion a besoin de `gopls`, que nous n'avons pas installé. Le [guide de la complétion](../how-to/enable-completion.md) s'en occupe plus tard ; ignorez-le pour l'instant.
103+
104+Appuyez sur **Entrée**. Regardez la nouvelle ligne : le curseur est *déjà* en colonne 9. Turbo Go a recopié l'indentation de la ligne précédente, ce qui est ce que l'on veut neuf fois sur dix.
105+
106+Cette fois nous n'en voulons pas : appuyez sur **Maj-Tab** pour retirer cette indentation, puis tapez l'accolade fermante :
107+
108+```go
109+}
110+```
111+
112+Le titre de la fenêtre affiche désormais `main.go *`. L'étoile signale des modifications non enregistrées.
113+
114+Nous venons d'écrire un programme Go complet, coloré au fil de la frappe.
115+
116+## Étape 5 — L'enregistrer
117+
118+Appuyez sur **F2**.
119+
120+L'étoile disparaît du titre et la barre d'état affiche :
121+
122+```
123+Saved main.go
124+```
125+
126+Nous venons d'écrire le fichier sur le disque.
127+
128+## Étape 6 — Regarder le fichier depuis l'extérieur
129+
130+Quittez l'éditeur avec **Alt-X**. Le terminal revient tel qu'il était.
131+
132+Vérifiez ce que nous avons écrit :
133+
134+```bash
135+cat main.go
136+```
137+
138+Vous devriez voir :
139+
140+```go
141+package main
142+
143+import "fmt"
144+
145+func main() {
146+ fmt.Println("Hello from Turbo Go!")
147+}
148+```
149+
150+## Étape 7 — L'exécuter
151+
152+```bash
153+go run main.go
154+```
155+
156+Vous devriez voir :
157+
158+```
159+Hello from Turbo Go!
160+```
161+
162+C'est un programme Go qui fonctionne, écrit entièrement dans l'éditeur.
163+
164+## Étape 8 — Changer de thème
165+
166+Rouvrez le fichier :
167+
168+```bash
169+$TURBO main.go
170+```
171+
172+Appuyez sur **F10**. Le menu `File` se déroule. Appuyez cinq fois sur **→** : le menu se déplace le long de la barre jusqu'à `Options`, dont la première entrée, `Theme…`, est surlignée. Appuyez sur **Entrée**.
173+
174+Une liste de onze apparaît, par ordre alphabétique, le thème que vous utilisez étant déjà mis en évidence :
175+
176+```
177+borland-light
178+cappuccino
179+catppuccin-frappe
180+catppuccin-latte
181+cobalt
182+darcula
183+intellij-light
184+monochrome-dark
185+monochrome-light
186+turbo-classic
187+turbo-dark
188+```
189+
190+`turbo-classic` est la ligne en évidence, puisque c'est le thème dans lequel vous êtes. Appuyez une fois sur **↓** pour aller sur `turbo-dark`, puis sur **Entrée**.
191+
192+Tout l'éditeur se repeint en gris sombre, et la barre d'état affiche `Theme: Turbo Dark`.
193+
194+Appuyez sur **Alt-X** pour quitter.
195+
196+## Et maintenant ?
197+
198+Vous avez compilé l'éditeur, écrit un programme Go dedans, l'avez enregistré, exécuté, et vous avez changé son apparence.
199+
200+- Pour faire des choses précises — activer la complétion, écrire votre propre thème, chercher dans un fichier → voir les [guides pratiques](../how-to/)
201+- Pour retrouver une touche ou une entrée de menu → voir la [référence](../reference/)
202+- Pour comprendre comment fonctionnent réellement la coloration et la complétion → voir les [explications](../explanation/)
new file mode 100644
@@ -0,0 +1,202 @@
1+# Tutoriel : votre premier fichier dans Turbo Go
2+
3+À la fin de ce tutoriel, vous aurez compilé l'éditeur, écrit un petit programme Go à l'intérieur, vu les mots-clés se colorer au fil de la frappe, enregistré le fichier et exécuté le programme. Comptez une dizaine de minutes.
4+
5+Aucune connaissance préalable de Turbo Go n'est nécessaire. Il vous faut Go 1.26 ou plus récent et un terminal — rien d'autre.
6+
7+## Prérequis
8+
9+Vérifiez que Go est présent :
10+
11+```bash
12+go version
13+```
14+
15+Vous devriez voir quelque chose comme :
16+
17+```
18+go version go1.26.5 linux/arm64
19+```
20+
21+Si cette commande échoue, installez d'abord Go : https://go.dev/dl/
22+
23+## Étape 1 — Compiler l'éditeur
24+
25+Depuis le répertoire du projet, tapez :
26+
27+```bash
28+make build
29+```
30+
31+Vous devriez voir :
32+
33+```
34+go build -o bin/turbo-go .
35+```
36+
37+puis plus rien. Le silence est le signe du succès : Go ne dit rien quand une compilation réussit.
38+
39+Nous avons maintenant un exécutable dans `bin/turbo-go`. Retenons où il est, pour pouvoir le lancer de n'importe où :
40+
41+```bash
42+export TURBO="$PWD/bin/turbo-go"
43+```
44+
45+## Étape 2 — Créer un endroit où travailler
46+
47+Turbo Go donne le meilleur de lui-même à l'intérieur d'un module Go ; créons-en un :
48+
49+```bash
50+mkdir -p /tmp/hello && cd /tmp/hello
51+go mod init hello
52+```
53+
54+Vous devriez voir :
55+
56+```
57+go: creating new go.mod: module hello
58+```
59+
60+Nous venons de créer un module Go vide.
61+
62+## Étape 3 — Ouvrir l'éditeur
63+
64+Lancez Turbo Go sur un fichier qui n'existe pas encore :
65+
66+```bash
67+$TURBO main.go
68+```
69+
70+L'écran se remplit d'un bureau bleu. Vous devriez voir :
71+
72+- une **barre de menus** en haut : `File Edit Search Run Code Options Window Snippets Go Help`
73+- une **fenêtre** encadrée d'un double trait, intitulée `main.go`
74+- une **barre d'état** en bas : `F1 Describe F2 Save F3 Open …`
75+
76+Le curseur clignote ligne 1, colonne 1 — la barre d'état affiche `1:1` à droite.
77+
78+Nous sommes dans l'éditeur.
79+
80+## Étape 4 — Écrire un programme Go
81+
82+Tapez ces cinq lignes, en appuyant sur Entrée à la fin de chacune :
83+
84+```go
85+package main
86+
87+import "fmt"
88+
89+func main() {
90+```
91+
92+Observez les couleurs pendant que vous tapez. `package`, `import` et `func` deviennent **blanc gras** dès que le mot est terminé : ce sont des mots-clés. `"fmt"` devient **vert** : c'est une chaîne. `main` devient **jaune gras** dès que vous tapez la `(` qui le suit, car cela en fait une fonction.
93+
94+Appuyez maintenant sur **Tab**. Le curseur saute en colonne 9 — la barre d'état à droite passe à `5:2`, parce qu'une tabulation occupe un seul caractère dans le fichier même si elle remplit huit colonnes à l'écran.
95+
96+Tapez le corps de la fonction :
97+
98+```go
99+fmt.Println("Hello from Turbo Go!")
100+```
101+
102+> Quand vous tapez le `.` après `fmt`, la barre d'état affiche brièvement un message à propos du serveur de langage. C'est normal : la complétion a besoin de `gopls`, que nous n'avons pas installé. Le [guide de la complétion](../how-to/enable-completion.md) s'en occupe plus tard ; ignorez-le pour l'instant.
103+
104+Appuyez sur **Entrée**. Regardez la nouvelle ligne : le curseur est *déjà* en colonne 9. Turbo Go a recopié l'indentation de la ligne précédente, ce qui est ce que l'on veut neuf fois sur dix.
105+
106+Cette fois nous n'en voulons pas : appuyez sur **Maj-Tab** pour retirer cette indentation, puis tapez l'accolade fermante :
107+
108+```go
109+}
110+```
111+
112+Le titre de la fenêtre affiche désormais `main.go *`. L'étoile signale des modifications non enregistrées.
113+
114+Nous venons d'écrire un programme Go complet, coloré au fil de la frappe.
115+
116+## Étape 5 — L'enregistrer
117+
118+Appuyez sur **F2**.
119+
120+L'étoile disparaît du titre et la barre d'état affiche :
121+
122+```
123+Saved main.go
124+```
125+
126+Nous venons d'écrire le fichier sur le disque.
127+
128+## Étape 6 — Regarder le fichier depuis l'extérieur
129+
130+Quittez l'éditeur avec **Alt-X**. Le terminal revient tel qu'il était.
131+
132+Vérifiez ce que nous avons écrit :
133+
134+```bash
135+cat main.go
136+```
137+
138+Vous devriez voir :
139+
140+```go
141+package main
142+
143+import "fmt"
144+
145+func main() {
146+ fmt.Println("Hello from Turbo Go!")
147+}
148+```
149+
150+## Étape 7 — L'exécuter
151+
152+```bash
153+go run main.go
154+```
155+
156+Vous devriez voir :
157+
158+```
159+Hello from Turbo Go!
160+```
161+
162+C'est un programme Go qui fonctionne, écrit entièrement dans l'éditeur.
163+
164+## Étape 8 — Changer de thème
165+
166+Rouvrez le fichier :
167+
168+```bash
169+$TURBO main.go
170+```
171+
172+Appuyez sur **F10**. Le menu `File` se déroule. Appuyez cinq fois sur **→** : le menu se déplace le long de la barre jusqu'à `Options`, dont la première entrée, `Theme…`, est surlignée. Appuyez sur **Entrée**.
173+
174+Une liste de onze apparaît, par ordre alphabétique, le thème que vous utilisez étant déjà mis en évidence :
175+
176+```
177+borland-light
178+cappuccino
179+catppuccin-frappe
180+catppuccin-latte
181+cobalt
182+darcula
183+intellij-light
184+monochrome-dark
185+monochrome-light
186+turbo-classic
187+turbo-dark
188+```
189+
190+`turbo-classic` est la ligne en évidence, puisque c'est le thème dans lequel vous êtes. Appuyez une fois sur **↓** pour aller sur `turbo-dark`, puis sur **Entrée**.
191+
192+Tout l'éditeur se repeint en gris sombre, et la barre d'état affiche `Theme: Turbo Dark`.
193+
194+Appuyez sur **Alt-X** pour quitter.
195+
196+## Et maintenant ?
197+
198+Vous avez compilé l'éditeur, écrit un programme Go dedans, l'avez enregistré, exécuté, et vous avez changé son apparence.
199+
200+- Pour faire des choses précises — activer la complétion, écrire votre propre thème, chercher dans un fichier → voir les [guides pratiques](../how-to/)
201+- Pour retrouver une touche ou une entrée de menu → voir la [référence](../reference/)
202+- Pour comprendre comment fonctionnent réellement la coloration et la complétion → voir les [explications](../explanation/)
added git.sh +136 -0
new file mode 100755
@@ -0,0 +1,136 @@
1+#!/bin/bash
2+message=""
3+case $1 in
4+
5+ # 🎨: art
6+ art)
7+ message="Improve structure / format of the code"
8+ emoji="🎨"
9+ ;;
10+
11+ # 🐛: bug
12+ bug|fix)
13+ message="Fix a bug"
14+ emoji="🐛"
15+ ;;
16+
17+ # 🤓: geek
18+ human|human-fixed)
19+ message="Human Fixed"
20+ emoji="🤓"
21+ ;;
22+
23+ # 🤖: robot
24+ ai|ai-generated)
25+ message="AI generated"
26+ emoji="🤖"
27+ ;;
28+
29+ # ✨: sparkles
30+ sparkles|feature)
31+ message="Introduce new feature(s)"
32+ emoji="✨"
33+ ;;
34+
35+ # 🧩: jigsaw
36+ jigsaw|example|examples|demo|demos)
37+ message="Introduce new example(s)"
38+ emoji="🧩"
39+ ;;
40+
41+
42+ # 📝: memo
43+ memo|doc|documentation)
44+ message="Add or update documentation"
45+ emoji="📝"
46+ ;;
47+
48+ # 🌸: cherry_blossom
49+ gardening|garden|clean|cleaning)
50+ message="Gardening"
51+ emoji="🌸"
52+ ;;
53+
54+ # 🚀: rocket
55+ rocket|deploy)
56+ message="Deploy stuff"
57+ emoji="🚀"
58+ ;;
59+
60+ # 🎉: tada
61+ tada|first)
62+ message="Begin a project"
63+ emoji="🎉"
64+ ;;
65+
66+ # 🚧: construction
67+ construction|wip)
68+ message="Work in progress"
69+ emoji="🚧"
70+ ;;
71+
72+ # 📦️: package
73+ package|build)
74+ message="Add or update compiled files or packages"
75+ emoji="📦️"
76+ ;;
77+
78+ # 📦️: package
79+ release)
80+ message="Create a release"
81+ emoji="📦️"
82+ ;;
83+
84+ # 👽️: alien
85+ alien|api)
86+ message="Update code due to external API changes"
87+ emoji="👽️"
88+ ;;
89+
90+ # 🐳: whale
91+ docker|container)
92+ message="Docker"
93+ emoji="🐳"
94+ ;;
95+
96+ # 🍊: tangerine
97+ gitpod|gitpodify)
98+ message="Gitpodify"
99+ emoji="🍊"
100+ ;;
101+
102+ # 🧪: test tube
103+ alembic|experiments|experiment|xp)
104+ message="Perform experiments"
105+ emoji="🧪"
106+ ;;
107+
108+ # ✅: check mark
109+ test|tests|testing)
110+ message="Add or update tests"
111+ emoji="✅"
112+ ;;
113+
114+ # 💾: floppy-disk
115+ save)
116+ message="Saved"
117+ emoji="💾"
118+ ;;
119+
120+ *)
121+ message="Updated"
122+ emoji="🛟"
123+ ;;
124+
125+esac
126+
127+find . -name '.DS_Store' -type f -delete
128+
129+if [ -z "$2" ]
130+then
131+ # empty
132+ git add .; git commit -m "$emoji $message."; git push
133+else
134+ # not empty
135+ git add .; git commit -m "$emoji $message: $2"; git push
136+fi
new file mode 100755
@@ -0,0 +1,136 @@
1+#!/bin/bash
2+message=""
3+case $1 in
4+
5+ # 🎨: art
6+ art)
7+ message="Improve structure / format of the code"
8+ emoji="🎨"
9+ ;;
10+
11+ # 🐛: bug
12+ bug|fix)
13+ message="Fix a bug"
14+ emoji="🐛"
15+ ;;
16+
17+ # 🤓: geek
18+ human|human-fixed)
19+ message="Human Fixed"
20+ emoji="🤓"
21+ ;;
22+
23+ # 🤖: robot
24+ ai|ai-generated)
25+ message="AI generated"
26+ emoji="🤖"
27+ ;;
28+
29+ # ✨: sparkles
30+ sparkles|feature)
31+ message="Introduce new feature(s)"
32+ emoji="✨"
33+ ;;
34+
35+ # 🧩: jigsaw
36+ jigsaw|example|examples|demo|demos)
37+ message="Introduce new example(s)"
38+ emoji="🧩"
39+ ;;
40+
41+
42+ # 📝: memo
43+ memo|doc|documentation)
44+ message="Add or update documentation"
45+ emoji="📝"
46+ ;;
47+
48+ # 🌸: cherry_blossom
49+ gardening|garden|clean|cleaning)
50+ message="Gardening"
51+ emoji="🌸"
52+ ;;
53+
54+ # 🚀: rocket
55+ rocket|deploy)
56+ message="Deploy stuff"
57+ emoji="🚀"
58+ ;;
59+
60+ # 🎉: tada
61+ tada|first)
62+ message="Begin a project"
63+ emoji="🎉"
64+ ;;
65+
66+ # 🚧: construction
67+ construction|wip)
68+ message="Work in progress"
69+ emoji="🚧"
70+ ;;
71+
72+ # 📦️: package
73+ package|build)
74+ message="Add or update compiled files or packages"
75+ emoji="📦️"
76+ ;;
77+
78+ # 📦️: package
79+ release)
80+ message="Create a release"
81+ emoji="📦️"
82+ ;;
83+
84+ # 👽️: alien
85+ alien|api)
86+ message="Update code due to external API changes"
87+ emoji="👽️"
88+ ;;
89+
90+ # 🐳: whale
91+ docker|container)
92+ message="Docker"
93+ emoji="🐳"
94+ ;;
95+
96+ # 🍊: tangerine
97+ gitpod|gitpodify)
98+ message="Gitpodify"
99+ emoji="🍊"
100+ ;;
101+
102+ # 🧪: test tube
103+ alembic|experiments|experiment|xp)
104+ message="Perform experiments"
105+ emoji="🧪"
106+ ;;
107+
108+ # ✅: check mark
109+ test|tests|testing)
110+ message="Add or update tests"
111+ emoji="✅"
112+ ;;
113+
114+ # 💾: floppy-disk
115+ save)
116+ message="Saved"
117+ emoji="💾"
118+ ;;
119+
120+ *)
121+ message="Updated"
122+ emoji="🛟"
123+ ;;
124+
125+esac
126+
127+find . -name '.DS_Store' -type f -delete
128+
129+if [ -z "$2" ]
130+then
131+ # empty
132+ git add .; git commit -m "$emoji $message."; git push
133+else
134+ # not empty
135+ git add .; git commit -m "$emoji $message: $2"; git push
136+fi
added go.mod +23 -0
new file mode 100644
@@ -0,0 +1,23 @@
1+module rickub.com/turbo-editors/turbo-go
2+
3+go 1.26.1
4+
5+require (
6+ github.com/gdamore/tcell/v2 v2.13.10
7+ rickub.com/turbo-editors/turbo-core v1.0.0
8+)
9+
10+require (
11+ github.com/BurntSushi/toml v1.6.0 // indirect
12+ github.com/gdamore/encoding v1.0.1 // indirect
13+ github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
14+ github.com/rivo/uniseg v0.4.7 // indirect
15+ golang.org/x/sys v0.38.0 // indirect
16+ golang.org/x/term v0.37.0 // indirect
17+ golang.org/x/text v0.31.0 // indirect
18+)
19+
20+// turbo-core is developed alongside the editors that use it. Point this at the
21+// checkout beside this one so the whole family builds from a clean clone of the
22+// three repositories; drop it once the version above is tagged and published.
23+// replace rickub.com/turbo-editors/turbo-core => ../turbo-core
new file mode 100644
@@ -0,0 +1,23 @@
1+module rickub.com/turbo-editors/turbo-go
2+
3+go 1.26.1
4+
5+require (
6+ github.com/gdamore/tcell/v2 v2.13.10
7+ rickub.com/turbo-editors/turbo-core v1.0.0
8+)
9+
10+require (
11+ github.com/BurntSushi/toml v1.6.0 // indirect
12+ github.com/gdamore/encoding v1.0.1 // indirect
13+ github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
14+ github.com/rivo/uniseg v0.4.7 // indirect
15+ golang.org/x/sys v0.38.0 // indirect
16+ golang.org/x/term v0.37.0 // indirect
17+ golang.org/x/text v0.31.0 // indirect
18+)
19+
20+// turbo-core is developed alongside the editors that use it. Point this at the
21+// checkout beside this one so the whole family builds from a clean clone of the
22+// three repositories; drop it once the version above is tagged and published.
23+// replace rickub.com/turbo-editors/turbo-core => ../turbo-core
added go.sum +49 -0
new file mode 100644
@@ -0,0 +1,49 @@
1+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
2+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
3+github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
4+github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
5+github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA=
6+github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
7+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
8+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
9+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
10+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
11+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
12+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
13+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
14+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
15+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
16+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
17+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
18+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
19+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
20+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
21+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
22+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
23+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
24+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
25+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
26+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
27+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
30+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
31+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
32+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
33+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
34+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
35+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
36+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
37+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
38+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
39+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
40+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
41+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
42+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
43+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
44+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
45+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
46+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
47+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
48+rickub.com/turbo-editors/turbo-core v1.0.0 h1:+tzwwONYXO46o+JpWHFR8PsGHTPLKtgd5NLZx9g3cdY=
49+rickub.com/turbo-editors/turbo-core v1.0.0/go.mod h1:rmfIY5gsFEo3sC5IIJFapwsGvdKG6NjrD7ACmnNTKq8=
new file mode 100644
@@ -0,0 +1,49 @@
1+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
2+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
3+github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
4+github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
5+github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA=
6+github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
7+github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
8+github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
9+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
10+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
11+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
12+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
13+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
14+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
15+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
16+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
17+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
18+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
19+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
20+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
21+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
22+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
23+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
24+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
25+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
26+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
27+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
30+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
31+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
32+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
33+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
34+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
35+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
36+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
37+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
38+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
39+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
40+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
41+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
42+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
43+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
44+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
45+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
46+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
47+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
48+rickub.com/turbo-editors/turbo-core v1.0.0 h1:+tzwwONYXO46o+JpWHFR8PsGHTPLKtgd5NLZx9g3cdY=
49+rickub.com/turbo-editors/turbo-core v1.0.0/go.mod h1:rmfIY5gsFEo3sC5IIJFapwsGvdKG6NjrD7ACmnNTKq8=
added install_test.go +348 -0
new file mode 100644
@@ -0,0 +1,348 @@
1+package main
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "runtime"
8+ "strings"
9+ "syscall"
10+ "testing"
11+)
12+
13+// runInstaller runs scripts/install.sh with the given arguments and returns
14+// everything it printed, failing the test if it did not exit cleanly.
15+func runInstaller(t *testing.T, args ...string) string {
16+ t.Helper()
17+
18+ output, err := exec.Command("bash", append([]string{"scripts/install.sh"}, args...)...).CombinedOutput()
19+ if err != nil {
20+ t.Fatalf("scripts/install.sh %v failed: %v\n%s", args, err, output)
21+ }
22+ return string(output)
23+}
24+
25+// skipUnlessShellIsAvailable skips a test where the installer cannot run.
26+func skipUnlessShellIsAvailable(t *testing.T) {
27+ t.Helper()
28+
29+ if testing.Short() {
30+ t.Skip("-short: the installer compiles the whole editor")
31+ }
32+ if runtime.GOOS == "windows" {
33+ t.Skip("the installer is a shell script")
34+ }
35+ if _, err := exec.LookPath("bash"); err != nil {
36+ t.Skip("bash is not available")
37+ }
38+}
39+
40+func TestTheInstallerBuildsAWorkingBinary(t *testing.T) {
41+ skipUnlessShellIsAvailable(t)
42+ prefix := t.TempDir()
43+
44+ output := runInstaller(t, "--prefix", prefix)
45+
46+ binary := filepath.Join(prefix, "turbo-go")
47+ info, err := os.Stat(binary)
48+ if err != nil {
49+ t.Fatalf("nothing was installed at %s: %v\n%s", binary, err, output)
50+ }
51+ if info.Mode().Perm()&0o111 == 0 {
52+ t.Errorf("the installed file has permissions %o, want it executable", info.Mode().Perm())
53+ }
54+
55+ version, err := exec.Command(binary, "-version").Output()
56+ if err != nil {
57+ t.Fatalf("the installed binary does not run: %v", err)
58+ }
59+ if !strings.Contains(string(version), "Turbo Go") {
60+ t.Errorf("-version printed %q", version)
61+ }
62+}
63+
64+func TestTheInstallerSaysWhereItPutThings(t *testing.T) {
65+ skipUnlessShellIsAvailable(t)
66+ prefix := t.TempDir()
67+
68+ output := runInstaller(t, "--prefix", prefix)
69+
70+ for _, want := range []string{"Turbo Go", prefix, "PATH", "gopls"} {
71+ if !strings.Contains(output, want) {
72+ t.Errorf("the installer never mentions %q:\n%s", want, output)
73+ }
74+ }
75+}
76+
77+func TestTheInstallerWarnsWhenThePrefixIsNotOnPath(t *testing.T) {
78+ skipUnlessShellIsAvailable(t)
79+ prefix := t.TempDir() // a fresh temporary directory is never on PATH
80+
81+ output := runInstaller(t, "--prefix", prefix)
82+
83+ if !strings.Contains(output, "not on your PATH") {
84+ t.Errorf("the installer did not warn about the PATH:\n%s", output)
85+ }
86+ if !strings.Contains(output, "export PATH=") {
87+ t.Errorf("the installer warned without saying how to fix it:\n%s", output)
88+ }
89+}
90+
91+func TestTheInstallerRemovesWhatItInstalled(t *testing.T) {
92+ skipUnlessShellIsAvailable(t)
93+ prefix := t.TempDir()
94+ runInstaller(t, "--prefix", prefix)
95+
96+ runInstaller(t, "--prefix", prefix, "--uninstall")
97+
98+ if _, err := os.Stat(filepath.Join(prefix, "turbo-go")); !os.IsNotExist(err) {
99+ t.Error("the binary is still there after --uninstall")
100+ }
101+}
102+
103+func TestUninstallingNothingIsNotAFailure(t *testing.T) {
104+ skipUnlessShellIsAvailable(t)
105+
106+ output := runInstaller(t, "--prefix", t.TempDir(), "--uninstall")
107+
108+ if !strings.Contains(output, "nothing installed") {
109+ t.Errorf("the installer did not say there was nothing to remove:\n%s", output)
110+ }
111+}
112+
113+func TestTheInstallerExplainsItself(t *testing.T) {
114+ skipUnlessShellIsAvailable(t)
115+
116+ output := runInstaller(t, "--help")
117+
118+ for _, want := range []string{"--prefix", "--with-gopls", "--uninstall"} {
119+ if !strings.Contains(output, want) {
120+ t.Errorf("--help does not document %q:\n%s", want, output)
121+ }
122+ }
123+}
124+
125+func TestTheInstallerRefusesAnUnknownOption(t *testing.T) {
126+ skipUnlessShellIsAvailable(t)
127+
128+ output, err := exec.Command("bash", "scripts/install.sh", "--nonsense").CombinedOutput()
129+
130+ if err == nil {
131+ t.Fatal("the installer accepted an option it does not have")
132+ }
133+ if !strings.Contains(string(output), "unknown option") {
134+ t.Errorf("the installer did not say what was wrong:\n%s", output)
135+ }
136+}
137+
138+func TestTheInstallerNeedsADirectoryAfterPrefix(t *testing.T) {
139+ skipUnlessShellIsAvailable(t)
140+
141+ output, err := exec.Command("bash", "scripts/install.sh", "--prefix").CombinedOutput()
142+
143+ if err == nil {
144+ t.Fatal("--prefix was accepted with nothing after it")
145+ }
146+ if !strings.Contains(string(output), "needs a directory") {
147+ t.Errorf("the installer did not say what was wrong:\n%s", output)
148+ }
149+}
150+
151+func TestTheInstallerRunsFromAnyDirectory(t *testing.T) {
152+ skipUnlessShellIsAvailable(t)
153+ repo, err := filepath.Abs(".")
154+ if err != nil {
155+ t.Fatalf("Abs() error = %v", err)
156+ }
157+ prefix := t.TempDir()
158+
159+ // It is invoked by absolute path from somewhere else entirely, as it would
160+ // be from a shell alias or another script.
161+ command := exec.Command("bash", filepath.Join(repo, "scripts", "install.sh"), "--prefix", prefix)
162+ command.Dir = t.TempDir()
163+ if output, err := command.CombinedOutput(); err != nil {
164+ t.Fatalf("the installer failed when run from elsewhere: %v\n%s", err, output)
165+ }
166+
167+ if _, err := os.Stat(filepath.Join(prefix, "turbo-go")); err != nil {
168+ t.Errorf("nothing was installed: %v", err)
169+ }
170+}
171+
172+func TestAFailedBuildLeavesTheInstalledBinaryAlone(t *testing.T) {
173+ skipUnlessShellIsAvailable(t)
174+ prefix := t.TempDir()
175+ runInstaller(t, "--prefix", prefix)
176+
177+ binary := filepath.Join(prefix, "turbo-go")
178+ before, err := os.Stat(binary)
179+ if err != nil {
180+ t.Fatalf("the first install produced nothing: %v", err)
181+ }
182+
183+ // A stray file in package main is exactly what a user's own scratch file
184+ // does to this repository, and it must not cost them their installation.
185+ stray := filepath.Join("scripts", "..", "zz_broken_on_purpose.go")
186+ if err := os.WriteFile(stray, []byte("package main\n\nfunc main() {}\n"), 0o644); err != nil {
187+ t.Fatalf("writing the stray file: %v", err)
188+ }
189+ t.Cleanup(func() { os.Remove(stray) })
190+
191+ output, err := exec.Command("bash", "scripts/install.sh", "--prefix", prefix).CombinedOutput()
192+
193+ if err == nil {
194+ t.Fatal("the installer reported success on a build that cannot succeed")
195+ }
196+ if !strings.Contains(string(output), "nothing was installed") {
197+ t.Errorf("the installer did not say the installation was untouched:\n%s", output)
198+ }
199+ after, err := os.Stat(binary)
200+ if err != nil {
201+ t.Fatalf("the failed build removed the installed binary: %v", err)
202+ }
203+ if !after.ModTime().Equal(before.ModTime()) {
204+ t.Error("the failed build replaced the installed binary")
205+ }
206+}
207+
208+func TestReinstallingReplacesTheFileRatherThanOverwritingIt(t *testing.T) {
209+ // macOS caches a binary's code signature against its inode. Writing new
210+ // bytes into the same inode — which is what cp does — leaves the cached
211+ // signature describing something else, and the kernel then refuses to
212+ // execute it: builds fine, installs fine, "does not run". Replacing the
213+ // directory entry with a fresh inode is what avoids that, and it makes the
214+ // install atomic besides.
215+ skipUnlessShellIsAvailable(t)
216+ prefix := t.TempDir()
217+ binary := filepath.Join(prefix, "turbo-go")
218+
219+ runInstaller(t, "--prefix", prefix)
220+ first := inodeOf(t, binary)
221+
222+ runInstaller(t, "--prefix", prefix)
223+ second := inodeOf(t, binary)
224+
225+ if first == second {
226+ t.Errorf("the reinstall wrote into the same inode (%d); it must replace the file", first)
227+ }
228+}
229+
230+func TestReinstallingLeavesAWorkingBinary(t *testing.T) {
231+ skipUnlessShellIsAvailable(t)
232+ prefix := t.TempDir()
233+ binary := filepath.Join(prefix, "turbo-go")
234+
235+ runInstaller(t, "--prefix", prefix)
236+ runInstaller(t, "--prefix", prefix)
237+
238+ if _, err := exec.Command(binary, "-version").Output(); err != nil {
239+ t.Fatalf("the reinstalled binary does not run: %v", err)
240+ }
241+}
242+
243+func TestABinaryThatWillNotRunIsReportedWithItsOwnError(t *testing.T) {
244+ // "the installed binary does not run" on its own tells whoever hit it
245+ // nothing they can act on. Whatever the system said has to come through.
246+ skipUnlessShellIsAvailable(t)
247+
248+ if !strings.Contains(readInstaller(t), "$verify") {
249+ t.Error("the installer discards what the binary said when it will not run")
250+ }
251+}
252+
253+// readInstaller returns the installer's source.
254+func readInstaller(t *testing.T) string {
255+ t.Helper()
256+
257+ data, err := os.ReadFile("scripts/install.sh")
258+ if err != nil {
259+ t.Fatalf("reading the installer: %v", err)
260+ }
261+ return string(data)
262+}
263+
264+// inodeOf returns a file's inode number.
265+func inodeOf(t *testing.T, path string) uint64 {
266+ t.Helper()
267+
268+ info, err := os.Stat(path)
269+ if err != nil {
270+ t.Fatalf("stat %s: %v", path, err)
271+ }
272+ stat, ok := info.Sys().(*syscall.Stat_t)
273+ if !ok {
274+ t.Skip("inode numbers are not available on this platform")
275+ }
276+ return uint64(stat.Ino)
277+}
278+
279+func TestTheInstalledBinaryReportsTheCommitItWasBuiltFrom(t *testing.T) {
280+ // The point of stamping: an installed editor must name the commit it came
281+ // from, not a constant somebody forgot to bump before releasing.
282+ skipUnlessShellIsAvailable(t)
283+ prefix := t.TempDir()
284+
285+ runInstaller(t, "--prefix", prefix)
286+
287+ reported, err := exec.Command(filepath.Join(prefix, "turbo-go"), "-version").Output()
288+ if err != nil {
289+ t.Fatalf("the installed binary does not run: %v", err)
290+ }
291+
292+ commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
293+ if err != nil {
294+ t.Skip("not a git checkout, so there is no commit to stamp")
295+ }
296+ if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) {
297+ t.Errorf("-version printed %q, which never mentions the commit %s", reported, want)
298+ }
299+}
300+
301+func TestTheInstalledBinaryDoesNotReportAnUnknownVersion(t *testing.T) {
302+ // "unknown" is what the binary says when *no* source could name it, and
303+ // seeing it here would mean the installer's ldflags never reached the
304+ // linker. "devel" is a different thing: it is what a correct build of a
305+ // checkout with no tags reports, so a checkout that has never been tagged
306+ // must not fail this.
307+ //
308+ // What proves the stamp arrived either way is the commit, which only the
309+ // linker can have supplied.
310+ skipUnlessShellIsAvailable(t)
311+
312+ // Outside a git checkout the installer has nothing to stamp *with*, and
313+ // "unknown" is then the correct answer rather than a failure — so the
314+ // premise is checked before anything is asserted on.
315+ commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
316+ if err != nil {
317+ t.Skip("not a git checkout, so there is nothing for the installer to stamp")
318+ }
319+ prefix := t.TempDir()
320+
321+ runInstaller(t, "--prefix", prefix)
322+
323+ reported, err := exec.Command(filepath.Join(prefix, "turbo-go"), "-version").Output()
324+ if err != nil {
325+ t.Fatalf("the installed binary does not run: %v", err)
326+ }
327+ if strings.Contains(string(reported), "unknown") {
328+ t.Errorf("-version printed %q, so nothing reached the linker at all", reported)
329+ }
330+ if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) {
331+ t.Errorf("-version printed %q, want it to carry the commit %q", reported, want)
332+ }
333+}
334+
335+func TestTheInstallerStampsThroughTheLinker(t *testing.T) {
336+ // A build outside a git checkout has nothing to describe, and must still
337+ // build rather than passing a half-built -X flag to the linker.
338+ script := readInstaller(t)
339+
340+ for _, want := range []string{"turbo-core/version", "-ldflags", "describe --tags --dirty"} {
341+ if !strings.Contains(script, want) {
342+ t.Errorf("the installer never mentions %q", want)
343+ }
344+ }
345+ if !strings.Contains(script, `ldflags=""`) {
346+ t.Error("the installer has no path for a checkout git cannot describe")
347+ }
348+}
new file mode 100644
@@ -0,0 +1,348 @@
1+package main
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "runtime"
8+ "strings"
9+ "syscall"
10+ "testing"
11+)
12+
13+// runInstaller runs scripts/install.sh with the given arguments and returns
14+// everything it printed, failing the test if it did not exit cleanly.
15+func runInstaller(t *testing.T, args ...string) string {
16+ t.Helper()
17+
18+ output, err := exec.Command("bash", append([]string{"scripts/install.sh"}, args...)...).CombinedOutput()
19+ if err != nil {
20+ t.Fatalf("scripts/install.sh %v failed: %v\n%s", args, err, output)
21+ }
22+ return string(output)
23+}
24+
25+// skipUnlessShellIsAvailable skips a test where the installer cannot run.
26+func skipUnlessShellIsAvailable(t *testing.T) {
27+ t.Helper()
28+
29+ if testing.Short() {
30+ t.Skip("-short: the installer compiles the whole editor")
31+ }
32+ if runtime.GOOS == "windows" {
33+ t.Skip("the installer is a shell script")
34+ }
35+ if _, err := exec.LookPath("bash"); err != nil {
36+ t.Skip("bash is not available")
37+ }
38+}
39+
40+func TestTheInstallerBuildsAWorkingBinary(t *testing.T) {
41+ skipUnlessShellIsAvailable(t)
42+ prefix := t.TempDir()
43+
44+ output := runInstaller(t, "--prefix", prefix)
45+
46+ binary := filepath.Join(prefix, "turbo-go")
47+ info, err := os.Stat(binary)
48+ if err != nil {
49+ t.Fatalf("nothing was installed at %s: %v\n%s", binary, err, output)
50+ }
51+ if info.Mode().Perm()&0o111 == 0 {
52+ t.Errorf("the installed file has permissions %o, want it executable", info.Mode().Perm())
53+ }
54+
55+ version, err := exec.Command(binary, "-version").Output()
56+ if err != nil {
57+ t.Fatalf("the installed binary does not run: %v", err)
58+ }
59+ if !strings.Contains(string(version), "Turbo Go") {
60+ t.Errorf("-version printed %q", version)
61+ }
62+}
63+
64+func TestTheInstallerSaysWhereItPutThings(t *testing.T) {
65+ skipUnlessShellIsAvailable(t)
66+ prefix := t.TempDir()
67+
68+ output := runInstaller(t, "--prefix", prefix)
69+
70+ for _, want := range []string{"Turbo Go", prefix, "PATH", "gopls"} {
71+ if !strings.Contains(output, want) {
72+ t.Errorf("the installer never mentions %q:\n%s", want, output)
73+ }
74+ }
75+}
76+
77+func TestTheInstallerWarnsWhenThePrefixIsNotOnPath(t *testing.T) {
78+ skipUnlessShellIsAvailable(t)
79+ prefix := t.TempDir() // a fresh temporary directory is never on PATH
80+
81+ output := runInstaller(t, "--prefix", prefix)
82+
83+ if !strings.Contains(output, "not on your PATH") {
84+ t.Errorf("the installer did not warn about the PATH:\n%s", output)
85+ }
86+ if !strings.Contains(output, "export PATH=") {
87+ t.Errorf("the installer warned without saying how to fix it:\n%s", output)
88+ }
89+}
90+
91+func TestTheInstallerRemovesWhatItInstalled(t *testing.T) {
92+ skipUnlessShellIsAvailable(t)
93+ prefix := t.TempDir()
94+ runInstaller(t, "--prefix", prefix)
95+
96+ runInstaller(t, "--prefix", prefix, "--uninstall")
97+
98+ if _, err := os.Stat(filepath.Join(prefix, "turbo-go")); !os.IsNotExist(err) {
99+ t.Error("the binary is still there after --uninstall")
100+ }
101+}
102+
103+func TestUninstallingNothingIsNotAFailure(t *testing.T) {
104+ skipUnlessShellIsAvailable(t)
105+
106+ output := runInstaller(t, "--prefix", t.TempDir(), "--uninstall")
107+
108+ if !strings.Contains(output, "nothing installed") {
109+ t.Errorf("the installer did not say there was nothing to remove:\n%s", output)
110+ }
111+}
112+
113+func TestTheInstallerExplainsItself(t *testing.T) {
114+ skipUnlessShellIsAvailable(t)
115+
116+ output := runInstaller(t, "--help")
117+
118+ for _, want := range []string{"--prefix", "--with-gopls", "--uninstall"} {
119+ if !strings.Contains(output, want) {
120+ t.Errorf("--help does not document %q:\n%s", want, output)
121+ }
122+ }
123+}
124+
125+func TestTheInstallerRefusesAnUnknownOption(t *testing.T) {
126+ skipUnlessShellIsAvailable(t)
127+
128+ output, err := exec.Command("bash", "scripts/install.sh", "--nonsense").CombinedOutput()
129+
130+ if err == nil {
131+ t.Fatal("the installer accepted an option it does not have")
132+ }
133+ if !strings.Contains(string(output), "unknown option") {
134+ t.Errorf("the installer did not say what was wrong:\n%s", output)
135+ }
136+}
137+
138+func TestTheInstallerNeedsADirectoryAfterPrefix(t *testing.T) {
139+ skipUnlessShellIsAvailable(t)
140+
141+ output, err := exec.Command("bash", "scripts/install.sh", "--prefix").CombinedOutput()
142+
143+ if err == nil {
144+ t.Fatal("--prefix was accepted with nothing after it")
145+ }
146+ if !strings.Contains(string(output), "needs a directory") {
147+ t.Errorf("the installer did not say what was wrong:\n%s", output)
148+ }
149+}
150+
151+func TestTheInstallerRunsFromAnyDirectory(t *testing.T) {
152+ skipUnlessShellIsAvailable(t)
153+ repo, err := filepath.Abs(".")
154+ if err != nil {
155+ t.Fatalf("Abs() error = %v", err)
156+ }
157+ prefix := t.TempDir()
158+
159+ // It is invoked by absolute path from somewhere else entirely, as it would
160+ // be from a shell alias or another script.
161+ command := exec.Command("bash", filepath.Join(repo, "scripts", "install.sh"), "--prefix", prefix)
162+ command.Dir = t.TempDir()
163+ if output, err := command.CombinedOutput(); err != nil {
164+ t.Fatalf("the installer failed when run from elsewhere: %v\n%s", err, output)
165+ }
166+
167+ if _, err := os.Stat(filepath.Join(prefix, "turbo-go")); err != nil {
168+ t.Errorf("nothing was installed: %v", err)
169+ }
170+}
171+
172+func TestAFailedBuildLeavesTheInstalledBinaryAlone(t *testing.T) {
173+ skipUnlessShellIsAvailable(t)
174+ prefix := t.TempDir()
175+ runInstaller(t, "--prefix", prefix)
176+
177+ binary := filepath.Join(prefix, "turbo-go")
178+ before, err := os.Stat(binary)
179+ if err != nil {
180+ t.Fatalf("the first install produced nothing: %v", err)
181+ }
182+
183+ // A stray file in package main is exactly what a user's own scratch file
184+ // does to this repository, and it must not cost them their installation.
185+ stray := filepath.Join("scripts", "..", "zz_broken_on_purpose.go")
186+ if err := os.WriteFile(stray, []byte("package main\n\nfunc main() {}\n"), 0o644); err != nil {
187+ t.Fatalf("writing the stray file: %v", err)
188+ }
189+ t.Cleanup(func() { os.Remove(stray) })
190+
191+ output, err := exec.Command("bash", "scripts/install.sh", "--prefix", prefix).CombinedOutput()
192+
193+ if err == nil {
194+ t.Fatal("the installer reported success on a build that cannot succeed")
195+ }
196+ if !strings.Contains(string(output), "nothing was installed") {
197+ t.Errorf("the installer did not say the installation was untouched:\n%s", output)
198+ }
199+ after, err := os.Stat(binary)
200+ if err != nil {
201+ t.Fatalf("the failed build removed the installed binary: %v", err)
202+ }
203+ if !after.ModTime().Equal(before.ModTime()) {
204+ t.Error("the failed build replaced the installed binary")
205+ }
206+}
207+
208+func TestReinstallingReplacesTheFileRatherThanOverwritingIt(t *testing.T) {
209+ // macOS caches a binary's code signature against its inode. Writing new
210+ // bytes into the same inode — which is what cp does — leaves the cached
211+ // signature describing something else, and the kernel then refuses to
212+ // execute it: builds fine, installs fine, "does not run". Replacing the
213+ // directory entry with a fresh inode is what avoids that, and it makes the
214+ // install atomic besides.
215+ skipUnlessShellIsAvailable(t)
216+ prefix := t.TempDir()
217+ binary := filepath.Join(prefix, "turbo-go")
218+
219+ runInstaller(t, "--prefix", prefix)
220+ first := inodeOf(t, binary)
221+
222+ runInstaller(t, "--prefix", prefix)
223+ second := inodeOf(t, binary)
224+
225+ if first == second {
226+ t.Errorf("the reinstall wrote into the same inode (%d); it must replace the file", first)
227+ }
228+}
229+
230+func TestReinstallingLeavesAWorkingBinary(t *testing.T) {
231+ skipUnlessShellIsAvailable(t)
232+ prefix := t.TempDir()
233+ binary := filepath.Join(prefix, "turbo-go")
234+
235+ runInstaller(t, "--prefix", prefix)
236+ runInstaller(t, "--prefix", prefix)
237+
238+ if _, err := exec.Command(binary, "-version").Output(); err != nil {
239+ t.Fatalf("the reinstalled binary does not run: %v", err)
240+ }
241+}
242+
243+func TestABinaryThatWillNotRunIsReportedWithItsOwnError(t *testing.T) {
244+ // "the installed binary does not run" on its own tells whoever hit it
245+ // nothing they can act on. Whatever the system said has to come through.
246+ skipUnlessShellIsAvailable(t)
247+
248+ if !strings.Contains(readInstaller(t), "$verify") {
249+ t.Error("the installer discards what the binary said when it will not run")
250+ }
251+}
252+
253+// readInstaller returns the installer's source.
254+func readInstaller(t *testing.T) string {
255+ t.Helper()
256+
257+ data, err := os.ReadFile("scripts/install.sh")
258+ if err != nil {
259+ t.Fatalf("reading the installer: %v", err)
260+ }
261+ return string(data)
262+}
263+
264+// inodeOf returns a file's inode number.
265+func inodeOf(t *testing.T, path string) uint64 {
266+ t.Helper()
267+
268+ info, err := os.Stat(path)
269+ if err != nil {
270+ t.Fatalf("stat %s: %v", path, err)
271+ }
272+ stat, ok := info.Sys().(*syscall.Stat_t)
273+ if !ok {
274+ t.Skip("inode numbers are not available on this platform")
275+ }
276+ return uint64(stat.Ino)
277+}
278+
279+func TestTheInstalledBinaryReportsTheCommitItWasBuiltFrom(t *testing.T) {
280+ // The point of stamping: an installed editor must name the commit it came
281+ // from, not a constant somebody forgot to bump before releasing.
282+ skipUnlessShellIsAvailable(t)
283+ prefix := t.TempDir()
284+
285+ runInstaller(t, "--prefix", prefix)
286+
287+ reported, err := exec.Command(filepath.Join(prefix, "turbo-go"), "-version").Output()
288+ if err != nil {
289+ t.Fatalf("the installed binary does not run: %v", err)
290+ }
291+
292+ commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
293+ if err != nil {
294+ t.Skip("not a git checkout, so there is no commit to stamp")
295+ }
296+ if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) {
297+ t.Errorf("-version printed %q, which never mentions the commit %s", reported, want)
298+ }
299+}
300+
301+func TestTheInstalledBinaryDoesNotReportAnUnknownVersion(t *testing.T) {
302+ // "unknown" is what the binary says when *no* source could name it, and
303+ // seeing it here would mean the installer's ldflags never reached the
304+ // linker. "devel" is a different thing: it is what a correct build of a
305+ // checkout with no tags reports, so a checkout that has never been tagged
306+ // must not fail this.
307+ //
308+ // What proves the stamp arrived either way is the commit, which only the
309+ // linker can have supplied.
310+ skipUnlessShellIsAvailable(t)
311+
312+ // Outside a git checkout the installer has nothing to stamp *with*, and
313+ // "unknown" is then the correct answer rather than a failure — so the
314+ // premise is checked before anything is asserted on.
315+ commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
316+ if err != nil {
317+ t.Skip("not a git checkout, so there is nothing for the installer to stamp")
318+ }
319+ prefix := t.TempDir()
320+
321+ runInstaller(t, "--prefix", prefix)
322+
323+ reported, err := exec.Command(filepath.Join(prefix, "turbo-go"), "-version").Output()
324+ if err != nil {
325+ t.Fatalf("the installed binary does not run: %v", err)
326+ }
327+ if strings.Contains(string(reported), "unknown") {
328+ t.Errorf("-version printed %q, so nothing reached the linker at all", reported)
329+ }
330+ if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) {
331+ t.Errorf("-version printed %q, want it to carry the commit %q", reported, want)
332+ }
333+}
334+
335+func TestTheInstallerStampsThroughTheLinker(t *testing.T) {
336+ // A build outside a git checkout has nothing to describe, and must still
337+ // build rather than passing a half-built -X flag to the linker.
338+ script := readInstaller(t)
339+
340+ for _, want := range []string{"turbo-core/version", "-ldflags", "describe --tags --dirty"} {
341+ if !strings.Contains(script, want) {
342+ t.Errorf("the installer never mentions %q", want)
343+ }
344+ }
345+ if !strings.Contains(script, `ldflags=""`) {
346+ t.Error("the installer has no path for a checkout git cannot describe")
347+ }
348+}
added internal/golang/acp.toml.tmpl +70 -0
new file mode 100644
@@ -0,0 +1,70 @@
1+# turbo-go agents.
2+#
3+# Each [[agent]] becomes one line of the Agent menu (Alt-A). Choosing it starts
4+# that program and opens a window on the conversation; closing the window stops
5+# it again. Open the same agent twice and you get two independent conversations.
6+#
7+# The editor is a client for the Agent Client Protocol — https://agentclientprotocol.com —
8+# so it holds no API key and knows no model. All of that is your agent's own
9+# configuration, in a file this editor does not read.
10+#
11+# name what the menu shows, and what the window is called. Required, and
12+# unique: a project's agent of the same name replaces one of yours.
13+# command the program to run. Required. Looked up on PATH.
14+# args its arguments, passed as given. There is no shell here, so no
15+# quoting, no globs and no && — use command = "sh", args = ["-c", …]
16+# when you really want one.
17+# env extra environment variables. The agent also inherits the ones the
18+# editor was started with, so a credential already exported reaches
19+# it without being written down here.
20+# cwd where to run it, relative to the project. Left out, it is the
21+# project itself.
22+#
23+# A key this file does not define is refused rather than ignored: a misspelt
24+# `comand` would otherwise look exactly like one that had no effect.
25+#
26+# The same file may also live in %[2]s, where it applies to every project you open.
27+# This one is read afterwards and wins where a name appears in both.
28+
29+# Docker's agent runtime, talking to a llama.cpp server on this machine.
30+# `docker agent serve acp <file>` speaks the protocol on its standard input and
31+# output, which is exactly what the editor wants.
32+#
33+# The YAML beside this file is the agent's own: which provider, which model,
34+# which tools. Write it yourself, or run `docker agent new` to start one.
35+
36+[[agent]]
37+name = "Local agent"
38+command = "docker"
39+args = ["agent", "serve", "acp", "%[1]s/agent.yaml"]
40+env = { TELEMETRY_ENABLED = "false" }
41+
42+# A second agent is one more block. Two windows side by side — Window ▸ Tile —
43+# is how a fast local model and a slow careful one get compared.
44+#
45+# [[agent]]
46+# name = "Reviewer"
47+# command = "my-agent"
48+# args = ["--acp", "--profile", "review"]
49+# cwd = "."
50+
51+# Once a window is open:
52+#
53+# Enter send what you have typed
54+# Alt-Enter a new line instead of sending
55+# Tab move between the conversation and the box
56+# / at the start of the box, list the agent's own commands
57+# @ list the project's files; the one you pick goes to the agent
58+# PgUp PgDn read back through the conversation
59+# Esc stop the turn in progress
60+# Ctrl-W close the window, and stop the agent with it
61+#
62+# Code the agent sends inside a ```go fence is coloured by the same scanner
63+# this editor colours .go files with. A fence naming a language it does not
64+# know is left plain rather than guessed at.
65+#
66+# An agent with a shell or a filesystem tool asks before it uses one, and the
67+# box that appears carries the agent's own choices. Nothing runs until you
68+# answer. When it reads a file you have open and have not saved, it is given
69+# what you can see rather than what is on disk; when it writes one, the change
70+# lands in the buffer for you to undo with Ctrl-Z or keep with F2.
new file mode 100644
@@ -0,0 +1,70 @@
1+# turbo-go agents.
2+#
3+# Each [[agent]] becomes one line of the Agent menu (Alt-A). Choosing it starts
4+# that program and opens a window on the conversation; closing the window stops
5+# it again. Open the same agent twice and you get two independent conversations.
6+#
7+# The editor is a client for the Agent Client Protocol — https://agentclientprotocol.com —
8+# so it holds no API key and knows no model. All of that is your agent's own
9+# configuration, in a file this editor does not read.
10+#
11+# name what the menu shows, and what the window is called. Required, and
12+# unique: a project's agent of the same name replaces one of yours.
13+# command the program to run. Required. Looked up on PATH.
14+# args its arguments, passed as given. There is no shell here, so no
15+# quoting, no globs and no && — use command = "sh", args = ["-c", …]
16+# when you really want one.
17+# env extra environment variables. The agent also inherits the ones the
18+# editor was started with, so a credential already exported reaches
19+# it without being written down here.
20+# cwd where to run it, relative to the project. Left out, it is the
21+# project itself.
22+#
23+# A key this file does not define is refused rather than ignored: a misspelt
24+# `comand` would otherwise look exactly like one that had no effect.
25+#
26+# The same file may also live in %[2]s, where it applies to every project you open.
27+# This one is read afterwards and wins where a name appears in both.
28+
29+# Docker's agent runtime, talking to a llama.cpp server on this machine.
30+# `docker agent serve acp <file>` speaks the protocol on its standard input and
31+# output, which is exactly what the editor wants.
32+#
33+# The YAML beside this file is the agent's own: which provider, which model,
34+# which tools. Write it yourself, or run `docker agent new` to start one.
35+
36+[[agent]]
37+name = "Local agent"
38+command = "docker"
39+args = ["agent", "serve", "acp", "%[1]s/agent.yaml"]
40+env = { TELEMETRY_ENABLED = "false" }
41+
42+# A second agent is one more block. Two windows side by side — Window ▸ Tile —
43+# is how a fast local model and a slow careful one get compared.
44+#
45+# [[agent]]
46+# name = "Reviewer"
47+# command = "my-agent"
48+# args = ["--acp", "--profile", "review"]
49+# cwd = "."
50+
51+# Once a window is open:
52+#
53+# Enter send what you have typed
54+# Alt-Enter a new line instead of sending
55+# Tab move between the conversation and the box
56+# / at the start of the box, list the agent's own commands
57+# @ list the project's files; the one you pick goes to the agent
58+# PgUp PgDn read back through the conversation
59+# Esc stop the turn in progress
60+# Ctrl-W close the window, and stop the agent with it
61+#
62+# Code the agent sends inside a ```go fence is coloured by the same scanner
63+# this editor colours .go files with. A fence naming a language it does not
64+# know is left plain rather than guessed at.
65+#
66+# An agent with a shell or a filesystem tool asks before it uses one, and the
67+# box that appears carries the agent's own choices. Nothing runs until you
68+# answer. When it reads a file you have open and have not saved, it is given
69+# what you can see rather than what is on disk; when it writes one, the change
70+# lands in the buffer for you to undo with Ctrl-Z or keep with F2.
added internal/golang/editor_test.go +185 -0
new file mode 100644
@@ -0,0 +1,185 @@
1+package golang_test
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/gdamore/tcell/v2"
13+
14+ "rickub.com/turbo-editors/turbo-core/app"
15+ "rickub.com/turbo-editors/turbo-core/buffer"
16+ "rickub.com/turbo-editors/turbo-core/lsp"
17+ "rickub.com/turbo-editors/turbo-core/syntax"
18+
19+ "rickub.com/turbo-editors/turbo-go/internal/golang"
20+)
21+
22+// TestCompletionEndToEndWithRealGopls drives the exact sequence the command
23+// does at start-up: open the files first, start the language server second,
24+// then ask for a completion.
25+//
26+// That order is the whole point. The editor's earlier version announced its
27+// open documents to a server that did not exist yet and never mentioned them
28+// again, so gopls answered every completion about a file it had never heard
29+// of — which looks, from the outside, exactly like completion not working.
30+//
31+// It lives in Turbo Go rather than in turbo-core because gopls is Turbo Go's
32+// server: the library has no language server of its own to be driven against.
33+//
34+// It skips itself when gopls is not installed, and under -short.
35+func TestCompletionEndToEndWithRealGopls(t *testing.T) {
36+ if testing.Short() {
37+ t.Skip("-short: not starting a language server")
38+ }
39+ if _, err := lsp.FindServer(golang.Profile().Server); errors.Is(err, lsp.ErrServerNotFound) {
40+ t.Skipf("%s is not installed; %s", golang.ServerCommand, golang.InstallHint)
41+ }
42+
43+ root := t.TempDir()
44+ writeFile(t, filepath.Join(root, "go.mod"), "module example.test\n\ngo 1.24\n")
45+
46+ // The file on disk stops short of the dot. The text the completion is
47+ // about gets *typed* below, so the answer can only come from what the
48+ // editor told the server — which is the whole point of this test. A
49+ // fixture already containing "strings." would be answered from disk, and
50+ // would pass whether or not the editor said anything at all.
51+ source := "package main\n\nimport \"strings\"\n\nfunc main() {\n\t\n}\n"
52+ path := filepath.Join(root, "main.go")
53+ writeFile(t, path, source)
54+
55+ editor := newTestEditor(t)
56+
57+ // 1. Open the file, exactly as main does — before there is any server.
58+ editor.Open(path)
59+
60+ // 2. Start the language server, exactly as main does — afterwards.
61+ ctx, cancel := context.WithCancel(t.Context())
62+ defer cancel()
63+ editor.StartLanguageServer(ctx, root)
64+ t.Cleanup(func() { editor.Language().Stop(context.Background()) })
65+
66+ waitUntilReady(t, editor)
67+
68+ // 3. Let the event loop notice the server is ready, as Run does on every
69+ // turn. This is what announces the file that was already open.
70+ editor.Tick()
71+
72+ // 4. Type "strings." into the buffer, so that only the editor knows it is
73+ // there, then ask for a completion.
74+ view := editor.ActiveView()
75+ view.Buffer().SetCursor(buffer.Position{Line: 5, Col: 1})
76+ typeText(editor, "strings.")
77+
78+ if !editor.Completion().Visible() {
79+ // Typing the dot asks for a completion by itself; ask again explicitly
80+ // so a failure reports the status rather than the popup's absence.
81+ editor.RequestCompletion()
82+ }
83+ if !editor.Completion().Visible() {
84+ t.Fatalf("no completion list opened; the status bar says %q", editor.StatusBar().Message())
85+ }
86+ if !completionOffers(editor, "Contains") {
87+ t.Errorf("the list does not offer strings.Contains; it has %d entries", editor.Completion().Count())
88+ }
89+}
90+
91+func TestTheEditorColoursGoSourceItOpens(t *testing.T) {
92+ // The whole path in one test: Register taught the library about Go, the
93+ // profile named the editor, and a .go file opened through the public API
94+ // comes out coloured.
95+ root := t.TempDir()
96+ path := filepath.Join(root, "main.go")
97+ writeFile(t, path, "package main\n")
98+
99+ editor := newTestEditor(t)
100+ editor.Open(path)
101+
102+ if got := editor.ActiveView().Language(); got != golang.Language {
103+ t.Fatalf("the view colours the file as %q, want %q", got, golang.Language)
104+ }
105+ if spans := syntax.Highlight(golang.Language, "package main"); len(spans[0]) == 0 {
106+ t.Error("the registered Go scanner colours nothing")
107+ }
108+}
109+
110+func TestTheEditorCallsItselfTurboGo(t *testing.T) {
111+ editor := newTestEditor(t)
112+
113+ if got := editor.Profile().Name; got != golang.Name {
114+ t.Errorf("Profile().Name = %q, want %q", got, golang.Name)
115+ }
116+ if got := editor.Profile().ProjectDir(); got != ".turbo-go" {
117+ t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-go")
118+ }
119+}
120+
121+// newTestEditor returns Turbo Go drawing on a simulated terminal, set up the
122+// way the command sets it up.
123+func newTestEditor(t *testing.T) *app.App {
124+ t.Helper()
125+
126+ golang.Register()
127+ screen := tcell.NewSimulationScreen("UTF-8")
128+ if err := screen.Init(); err != nil {
129+ t.Fatalf("initialising the simulation screen: %v", err)
130+ }
131+ t.Cleanup(screen.Fini)
132+ screen.SetSize(80, 24)
133+
134+ // Never read the themes or snippets of whoever is running the tests.
135+ p := golang.Profile()
136+ t.Setenv(p.ThemeDirEnvVar(), t.TempDir())
137+ t.Setenv(p.SnippetDirEnvVar(), t.TempDir())
138+
139+ editor := app.New(screen, "turbo-classic", p)
140+ editor.Render()
141+ return editor
142+}
143+
144+// typeText sends a run of printable characters through the whole routing chain.
145+func typeText(editor *app.App, text string) {
146+ for _, r := range text {
147+ editor.Handle(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
148+ }
149+}
150+
151+// completionOffers reports whether the open popup holds an entry starting with
152+// a label.
153+func completionOffers(editor *app.App, label string) bool {
154+ for _, item := range editor.Completion().Matches() {
155+ if strings.HasPrefix(item.Label, label) {
156+ return true
157+ }
158+ }
159+ return false
160+}
161+
162+// waitUntilReady blocks until the language server has finished starting.
163+func waitUntilReady(t *testing.T, editor *app.App) {
164+ t.Helper()
165+
166+ deadline := time.After(lsp.InitializeTimeout)
167+ for !editor.Language().Ready() {
168+ select {
169+ case <-deadline:
170+ t.Fatalf("the language server never became ready: %s", editor.Language().Status())
171+ case <-time.After(10 * time.Millisecond):
172+ }
173+ }
174+}
175+
176+// writeFile creates a file, making its directory first.
177+func writeFile(t *testing.T, path, content string) {
178+ t.Helper()
179+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
180+ t.Fatalf("creating %s: %v", filepath.Dir(path), err)
181+ }
182+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
183+ t.Fatalf("writing %s: %v", path, err)
184+ }
185+}
new file mode 100644
@@ -0,0 +1,185 @@
1+package golang_test
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "path/filepath"
8+ "strings"
9+ "testing"
10+ "time"
11+
12+ "github.com/gdamore/tcell/v2"
13+
14+ "rickub.com/turbo-editors/turbo-core/app"
15+ "rickub.com/turbo-editors/turbo-core/buffer"
16+ "rickub.com/turbo-editors/turbo-core/lsp"
17+ "rickub.com/turbo-editors/turbo-core/syntax"
18+
19+ "rickub.com/turbo-editors/turbo-go/internal/golang"
20+)
21+
22+// TestCompletionEndToEndWithRealGopls drives the exact sequence the command
23+// does at start-up: open the files first, start the language server second,
24+// then ask for a completion.
25+//
26+// That order is the whole point. The editor's earlier version announced its
27+// open documents to a server that did not exist yet and never mentioned them
28+// again, so gopls answered every completion about a file it had never heard
29+// of — which looks, from the outside, exactly like completion not working.
30+//
31+// It lives in Turbo Go rather than in turbo-core because gopls is Turbo Go's
32+// server: the library has no language server of its own to be driven against.
33+//
34+// It skips itself when gopls is not installed, and under -short.
35+func TestCompletionEndToEndWithRealGopls(t *testing.T) {
36+ if testing.Short() {
37+ t.Skip("-short: not starting a language server")
38+ }
39+ if _, err := lsp.FindServer(golang.Profile().Server); errors.Is(err, lsp.ErrServerNotFound) {
40+ t.Skipf("%s is not installed; %s", golang.ServerCommand, golang.InstallHint)
41+ }
42+
43+ root := t.TempDir()
44+ writeFile(t, filepath.Join(root, "go.mod"), "module example.test\n\ngo 1.24\n")
45+
46+ // The file on disk stops short of the dot. The text the completion is
47+ // about gets *typed* below, so the answer can only come from what the
48+ // editor told the server — which is the whole point of this test. A
49+ // fixture already containing "strings." would be answered from disk, and
50+ // would pass whether or not the editor said anything at all.
51+ source := "package main\n\nimport \"strings\"\n\nfunc main() {\n\t\n}\n"
52+ path := filepath.Join(root, "main.go")
53+ writeFile(t, path, source)
54+
55+ editor := newTestEditor(t)
56+
57+ // 1. Open the file, exactly as main does — before there is any server.
58+ editor.Open(path)
59+
60+ // 2. Start the language server, exactly as main does — afterwards.
61+ ctx, cancel := context.WithCancel(t.Context())
62+ defer cancel()
63+ editor.StartLanguageServer(ctx, root)
64+ t.Cleanup(func() { editor.Language().Stop(context.Background()) })
65+
66+ waitUntilReady(t, editor)
67+
68+ // 3. Let the event loop notice the server is ready, as Run does on every
69+ // turn. This is what announces the file that was already open.
70+ editor.Tick()
71+
72+ // 4. Type "strings." into the buffer, so that only the editor knows it is
73+ // there, then ask for a completion.
74+ view := editor.ActiveView()
75+ view.Buffer().SetCursor(buffer.Position{Line: 5, Col: 1})
76+ typeText(editor, "strings.")
77+
78+ if !editor.Completion().Visible() {
79+ // Typing the dot asks for a completion by itself; ask again explicitly
80+ // so a failure reports the status rather than the popup's absence.
81+ editor.RequestCompletion()
82+ }
83+ if !editor.Completion().Visible() {
84+ t.Fatalf("no completion list opened; the status bar says %q", editor.StatusBar().Message())
85+ }
86+ if !completionOffers(editor, "Contains") {
87+ t.Errorf("the list does not offer strings.Contains; it has %d entries", editor.Completion().Count())
88+ }
89+}
90+
91+func TestTheEditorColoursGoSourceItOpens(t *testing.T) {
92+ // The whole path in one test: Register taught the library about Go, the
93+ // profile named the editor, and a .go file opened through the public API
94+ // comes out coloured.
95+ root := t.TempDir()
96+ path := filepath.Join(root, "main.go")
97+ writeFile(t, path, "package main\n")
98+
99+ editor := newTestEditor(t)
100+ editor.Open(path)
101+
102+ if got := editor.ActiveView().Language(); got != golang.Language {
103+ t.Fatalf("the view colours the file as %q, want %q", got, golang.Language)
104+ }
105+ if spans := syntax.Highlight(golang.Language, "package main"); len(spans[0]) == 0 {
106+ t.Error("the registered Go scanner colours nothing")
107+ }
108+}
109+
110+func TestTheEditorCallsItselfTurboGo(t *testing.T) {
111+ editor := newTestEditor(t)
112+
113+ if got := editor.Profile().Name; got != golang.Name {
114+ t.Errorf("Profile().Name = %q, want %q", got, golang.Name)
115+ }
116+ if got := editor.Profile().ProjectDir(); got != ".turbo-go" {
117+ t.Errorf("ProjectDir() = %q, want %q", got, ".turbo-go")
118+ }
119+}
120+
121+// newTestEditor returns Turbo Go drawing on a simulated terminal, set up the
122+// way the command sets it up.
123+func newTestEditor(t *testing.T) *app.App {
124+ t.Helper()
125+
126+ golang.Register()
127+ screen := tcell.NewSimulationScreen("UTF-8")
128+ if err := screen.Init(); err != nil {
129+ t.Fatalf("initialising the simulation screen: %v", err)
130+ }
131+ t.Cleanup(screen.Fini)
132+ screen.SetSize(80, 24)
133+
134+ // Never read the themes or snippets of whoever is running the tests.
135+ p := golang.Profile()
136+ t.Setenv(p.ThemeDirEnvVar(), t.TempDir())
137+ t.Setenv(p.SnippetDirEnvVar(), t.TempDir())
138+
139+ editor := app.New(screen, "turbo-classic", p)
140+ editor.Render()
141+ return editor
142+}
143+
144+// typeText sends a run of printable characters through the whole routing chain.
145+func typeText(editor *app.App, text string) {
146+ for _, r := range text {
147+ editor.Handle(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
148+ }
149+}
150+
151+// completionOffers reports whether the open popup holds an entry starting with
152+// a label.
153+func completionOffers(editor *app.App, label string) bool {
154+ for _, item := range editor.Completion().Matches() {
155+ if strings.HasPrefix(item.Label, label) {
156+ return true
157+ }
158+ }
159+ return false
160+}
161+
162+// waitUntilReady blocks until the language server has finished starting.
163+func waitUntilReady(t *testing.T, editor *app.App) {
164+ t.Helper()
165+
166+ deadline := time.After(lsp.InitializeTimeout)
167+ for !editor.Language().Ready() {
168+ select {
169+ case <-deadline:
170+ t.Fatalf("the language server never became ready: %s", editor.Language().Status())
171+ case <-time.After(10 * time.Millisecond):
172+ }
173+ }
174+}
175+
176+// writeFile creates a file, making its directory first.
177+func writeFile(t *testing.T, path, content string) {
178+ t.Helper()
179+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
180+ t.Fatalf("creating %s: %v", filepath.Dir(path), err)
181+ }
182+ if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
183+ t.Fatalf("writing %s: %v", path, err)
184+ }
185+}
added internal/golang/golang.go +118 -0
new file mode 100644
@@ -0,0 +1,118 @@
1+// Package golang is everything about Turbo Go that is about *Go*: how the
2+// editor names itself, which language server it talks to, what a Go project's
3+// starter files say, and how Go source is coloured.
4+//
5+// Everything else the editor does lives in turbo-core, which knows nothing
6+// about Go. This package is the whole of the difference between Turbo Go and
7+// Turbo Rust, which is what makes a third editor a matter of writing one of
8+// these rather than forking anything.
9+//
10+// golang.Register() // teach the library to colour Go
11+// editor := app.New(screen, name, golang.Profile())
12+package golang
13+
14+import (
15+ "os"
16+ "path/filepath"
17+
18+ "rickub.com/turbo-editors/turbo-core/profile"
19+ "rickub.com/turbo-editors/turbo-core/syntax"
20+)
21+
22+// Name and Slug are what the editor calls itself. The slug is also its binary,
23+// its project directory (as .turbo-go) and the stem of its environment
24+// variables (as TURBO_GO_…), so it is not free to change.
25+const (
26+ Name = "Turbo Go"
27+ Slug = "turbo-go"
28+)
29+
30+// Language is the name Go is known by: the value LanguageOf returns for a .go
31+// file, and what a snippets file writes in its languages key.
32+const Language syntax.Language = "go"
33+
34+// ServerCommand is the language server Turbo Go talks to, and InstallHint the
35+// single command that installs it.
36+const (
37+ ServerCommand = "gopls"
38+ InstallHint = "go install golang.org/x/tools/gopls@latest"
39+)
40+
41+// Profile returns the editor Turbo Go is.
42+//
43+// It is a function rather than a variable because Server.Dirs is worked out
44+// from the environment, and a variable would freeze whatever GOPATH said when
45+// the package was linked.
46+func Profile() profile.Profile {
47+ return profile.Profile{
48+ Name: Name,
49+ Slug: Slug,
50+ Language: "Go",
51+ // Go is fixed on the bar and takes G, which no other menu claims.
52+ ToolsMenu: "~G~o",
53+ // go.mod is the module's boundary, and the module is what gopls loads.
54+ RootMarkers: []string{"go.mod"},
55+ Server: profile.Server{
56+ Command: ServerCommand,
57+ // gopls needs a subcommand; most language servers do not.
58+ Args: []string{"serve"},
59+ InstallHint: InstallHint,
60+ Dirs: []string{BinDir()},
61+ },
62+ Templates: profile.Templates{
63+ Settings: settingsTemplate,
64+ Snippets: snippetsTemplate,
65+ Tools: toolsTemplate,
66+ Agents: agentsTemplate,
67+ },
68+ }
69+}
70+
71+// Register teaches turbo-core to colour Go.
72+//
73+// It is called explicitly at start-up rather than from an init function so that
74+// "which languages does this editor know?" is answered by reading main, not by
75+// working out which packages were imported.
76+func Register() {
77+ syntax.Register(syntax.Definition{
78+ Language: Language,
79+ Extensions: []string{".go"},
80+ Highlight: highlight,
81+ })
82+}
83+
84+// highlight colours Go source with go/scanner — the same lexer the Go toolchain
85+// uses, so the editor and the compiler agree about what a token is.
86+//
87+// It works in byte offsets rather than a line at a time, which is why it goes
88+// through syntax.LineIndex instead of syntax.LineScanner.
89+func highlight(src string) [][]syntax.Span {
90+ lines := syntax.NewLineIndex(src)
91+ out := make([][]syntax.Span, lines.Count())
92+
93+ tokens := scanTokens(src)
94+ for i, class := range classify(tokens) {
95+ lines.AppendSpans(out, tokens[i].start, tokens[i].end, class)
96+ }
97+ return out
98+}
99+
100+// BinDir returns where "go install" puts binaries: GOBIN when it is set,
101+// GOPATH/bin otherwise, and the conventional ~/go/bin when neither is.
102+//
103+// It is where gopls is looked for after PATH, because `go install` puts it
104+// somewhere that is very often not on PATH — which is the single most common
105+// reason completion is missing on a machine that has gopls.
106+func BinDir() string {
107+ if gobin := os.Getenv("GOBIN"); gobin != "" {
108+ return gobin
109+ }
110+ if gopath := os.Getenv("GOPATH"); gopath != "" {
111+ return filepath.Join(gopath, "bin")
112+ }
113+ home, err := os.UserHomeDir()
114+ if err != nil {
115+ return ""
116+ }
117+ return filepath.Join(home, "go", "bin")
118+}
new file mode 100644
@@ -0,0 +1,118 @@
1+// Package golang is everything about Turbo Go that is about *Go*: how the
2+// editor names itself, which language server it talks to, what a Go project's
3+// starter files say, and how Go source is coloured.
4+//
5+// Everything else the editor does lives in turbo-core, which knows nothing
6+// about Go. This package is the whole of the difference between Turbo Go and
7+// Turbo Rust, which is what makes a third editor a matter of writing one of
8+// these rather than forking anything.
9+//
10+// golang.Register() // teach the library to colour Go
11+// editor := app.New(screen, name, golang.Profile())
12+package golang
13+
14+import (
15+ "os"
16+ "path/filepath"
17+
18+ "rickub.com/turbo-editors/turbo-core/profile"
19+ "rickub.com/turbo-editors/turbo-core/syntax"
20+)
21+
22+// Name and Slug are what the editor calls itself. The slug is also its binary,
23+// its project directory (as .turbo-go) and the stem of its environment
24+// variables (as TURBO_GO_…), so it is not free to change.
25+const (
26+ Name = "Turbo Go"
27+ Slug = "turbo-go"
28+)
29+
30+// Language is the name Go is known by: the value LanguageOf returns for a .go
31+// file, and what a snippets file writes in its languages key.
32+const Language syntax.Language = "go"
33+
34+// ServerCommand is the language server Turbo Go talks to, and InstallHint the
35+// single command that installs it.
36+const (
37+ ServerCommand = "gopls"
38+ InstallHint = "go install golang.org/x/tools/gopls@latest"
39+)
40+
41+// Profile returns the editor Turbo Go is.
42+//
43+// It is a function rather than a variable because Server.Dirs is worked out
44+// from the environment, and a variable would freeze whatever GOPATH said when
45+// the package was linked.
46+func Profile() profile.Profile {
47+ return profile.Profile{
48+ Name: Name,
49+ Slug: Slug,
50+ Language: "Go",
51+ // Go is fixed on the bar and takes G, which no other menu claims.
52+ ToolsMenu: "~G~o",
53+ // go.mod is the module's boundary, and the module is what gopls loads.
54+ RootMarkers: []string{"go.mod"},
55+ Server: profile.Server{
56+ Command: ServerCommand,
57+ // gopls needs a subcommand; most language servers do not.
58+ Args: []string{"serve"},
59+ InstallHint: InstallHint,
60+ Dirs: []string{BinDir()},
61+ },
62+ Templates: profile.Templates{
63+ Settings: settingsTemplate,
64+ Snippets: snippetsTemplate,
65+ Tools: toolsTemplate,
66+ Agents: agentsTemplate,
67+ },
68+ }
69+}
70+
71+// Register teaches turbo-core to colour Go.
72+//
73+// It is called explicitly at start-up rather than from an init function so that
74+// "which languages does this editor know?" is answered by reading main, not by
75+// working out which packages were imported.
76+func Register() {
77+ syntax.Register(syntax.Definition{
78+ Language: Language,
79+ Extensions: []string{".go"},
80+ Highlight: highlight,
81+ })
82+}
83+
84+// highlight colours Go source with go/scanner — the same lexer the Go toolchain
85+// uses, so the editor and the compiler agree about what a token is.
86+//
87+// It works in byte offsets rather than a line at a time, which is why it goes
88+// through syntax.LineIndex instead of syntax.LineScanner.
89+func highlight(src string) [][]syntax.Span {
90+ lines := syntax.NewLineIndex(src)
91+ out := make([][]syntax.Span, lines.Count())
92+
93+ tokens := scanTokens(src)
94+ for i, class := range classify(tokens) {
95+ lines.AppendSpans(out, tokens[i].start, tokens[i].end, class)
96+ }
97+ return out
98+}
99+
100+// BinDir returns where "go install" puts binaries: GOBIN when it is set,
101+// GOPATH/bin otherwise, and the conventional ~/go/bin when neither is.
102+//
103+// It is where gopls is looked for after PATH, because `go install` puts it
104+// somewhere that is very often not on PATH — which is the single most common
105+// reason completion is missing on a machine that has gopls.
106+func BinDir() string {
107+ if gobin := os.Getenv("GOBIN"); gobin != "" {
108+ return gobin
109+ }
110+ if gopath := os.Getenv("GOPATH"); gopath != "" {
111+ return filepath.Join(gopath, "bin")
112+ }
113+ home, err := os.UserHomeDir()
114+ if err != nil {
115+ return ""
116+ }
117+ return filepath.Join(home, "go", "bin")
118+}
added internal/golang/gopls_test.go +93 -0
new file mode 100644
@@ -0,0 +1,93 @@
1+package golang_test
2+
3+import (
4+ "context"
5+ "errors"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "rickub.com/turbo-editors/turbo-core/lsp"
11+
12+ "rickub.com/turbo-editors/turbo-go/internal/golang"
13+)
14+
15+// TestAgainstRealGopls drives turbo-core's LSP client against an actual
16+// language server: process, pipes, handshake, completion and shutdown.
17+//
18+// It lives here rather than in turbo-core because gopls is Turbo Go's server;
19+// the library has no language server of its own to test against.
20+//
21+// The original comment follows.
22+//
23+// It drives the whole client against an actual language
24+// server: process, pipes, handshake, completion and shutdown.
25+//
26+// It is skipped when gopls is not installed and under -short, so a checkout
27+// with no language server still has a green suite — which is exactly the
28+// situation the editor itself is built to cope with.
29+func TestAgainstRealGopls(t *testing.T) {
30+ if testing.Short() {
31+ t.Skip("-short: not starting a language server")
32+ }
33+ if _, err := lsp.FindServer(golang.Profile().Server); errors.Is(err, lsp.ErrServerNotFound) {
34+ t.Skipf("%s is not installed; %s", golang.ServerCommand, golang.InstallHint)
35+ }
36+
37+ root := t.TempDir()
38+ writeFile(t, filepath.Join(root, "go.mod"), "module example.test\n\ngo 1.24\n")
39+ source := "package main\n\nimport \"strings\"\n\nfunc main() {\n\tstrings.\n}\n"
40+ path := filepath.Join(root, "main.go")
41+ writeFile(t, path, source)
42+
43+ server, err := lsp.StartServer(t.Context(), golang.Profile().Server, root, golang.Name)
44+ if err != nil {
45+ t.Fatalf("StartServer() error = %v", err)
46+ }
47+ client := server.Client()
48+ t.Cleanup(func() {
49+ // Not t.Context(): that one is already cancelled by the time cleanups
50+ // run, and shutting a server down needs a context that is still alive.
51+ if err := server.Stop(context.Background()); err != nil {
52+ t.Errorf("Stop() error = %v", err)
53+ }
54+ })
55+
56+ if !client.Ready() {
57+ t.Fatal("the client is not ready after StartServer returned")
58+ }
59+ if err := client.DidOpen(path, source); err != nil {
60+ t.Fatalf("DidOpen() error = %v", err)
61+ }
62+
63+ // Line 5, just after "strings." — the column counts the leading tab as one
64+ // rune, as the editor does everywhere.
65+ items, err := client.Complete(t.Context(), path, 5, 9, "\tstrings.")
66+ if err != nil {
67+ t.Fatalf("Complete() error = %v", err)
68+ }
69+ if len(items) == 0 {
70+ t.Fatal("gopls offered no completions after \"strings.\"")
71+ }
72+
73+ if !containsLabel(items, "Contains") {
74+ t.Errorf("the completions do not include strings.Contains: %v", labelsOf(items))
75+ }
76+}
77+
78+func containsLabel(items []lsp.CompletionItem, want string) bool {
79+ for _, item := range items {
80+ if strings.HasPrefix(item.Label, want) {
81+ return true
82+ }
83+ }
84+ return false
85+}
86+
87+func labelsOf(items []lsp.CompletionItem) []string {
88+ labels := make([]string, 0, min(len(items), 10))
89+ for _, item := range items[:min(len(items), 10)] {
90+ labels = append(labels, item.Label)
91+ }
92+ return labels
93+}
new file mode 100644
@@ -0,0 +1,93 @@
1+package golang_test
2+
3+import (
4+ "context"
5+ "errors"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "rickub.com/turbo-editors/turbo-core/lsp"
11+
12+ "rickub.com/turbo-editors/turbo-go/internal/golang"
13+)
14+
15+// TestAgainstRealGopls drives turbo-core's LSP client against an actual
16+// language server: process, pipes, handshake, completion and shutdown.
17+//
18+// It lives here rather than in turbo-core because gopls is Turbo Go's server;
19+// the library has no language server of its own to test against.
20+//
21+// The original comment follows.
22+//
23+// It drives the whole client against an actual language
24+// server: process, pipes, handshake, completion and shutdown.
25+//
26+// It is skipped when gopls is not installed and under -short, so a checkout
27+// with no language server still has a green suite — which is exactly the
28+// situation the editor itself is built to cope with.
29+func TestAgainstRealGopls(t *testing.T) {
30+ if testing.Short() {
31+ t.Skip("-short: not starting a language server")
32+ }
33+ if _, err := lsp.FindServer(golang.Profile().Server); errors.Is(err, lsp.ErrServerNotFound) {
34+ t.Skipf("%s is not installed; %s", golang.ServerCommand, golang.InstallHint)
35+ }
36+
37+ root := t.TempDir()
38+ writeFile(t, filepath.Join(root, "go.mod"), "module example.test\n\ngo 1.24\n")
39+ source := "package main\n\nimport \"strings\"\n\nfunc main() {\n\tstrings.\n}\n"
40+ path := filepath.Join(root, "main.go")
41+ writeFile(t, path, source)
42+
43+ server, err := lsp.StartServer(t.Context(), golang.Profile().Server, root, golang.Name)
44+ if err != nil {
45+ t.Fatalf("StartServer() error = %v", err)
46+ }
47+ client := server.Client()
48+ t.Cleanup(func() {
49+ // Not t.Context(): that one is already cancelled by the time cleanups
50+ // run, and shutting a server down needs a context that is still alive.
51+ if err := server.Stop(context.Background()); err != nil {
52+ t.Errorf("Stop() error = %v", err)
53+ }
54+ })
55+
56+ if !client.Ready() {
57+ t.Fatal("the client is not ready after StartServer returned")
58+ }
59+ if err := client.DidOpen(path, source); err != nil {
60+ t.Fatalf("DidOpen() error = %v", err)
61+ }
62+
63+ // Line 5, just after "strings." — the column counts the leading tab as one
64+ // rune, as the editor does everywhere.
65+ items, err := client.Complete(t.Context(), path, 5, 9, "\tstrings.")
66+ if err != nil {
67+ t.Fatalf("Complete() error = %v", err)
68+ }
69+ if len(items) == 0 {
70+ t.Fatal("gopls offered no completions after \"strings.\"")
71+ }
72+
73+ if !containsLabel(items, "Contains") {
74+ t.Errorf("the completions do not include strings.Contains: %v", labelsOf(items))
75+ }
76+}
77+
78+func containsLabel(items []lsp.CompletionItem, want string) bool {
79+ for _, item := range items {
80+ if strings.HasPrefix(item.Label, want) {
81+ return true
82+ }
83+ }
84+ return false
85+}
86+
87+func labelsOf(items []lsp.CompletionItem) []string {
88+ labels := make([]string, 0, min(len(items), 10))
89+ for _, item := range items[:min(len(items), 10)] {
90+ labels = append(labels, item.Label)
91+ }
92+ return labels
93+}
added internal/golang/scan.go +182 -0
new file mode 100644
@@ -0,0 +1,182 @@
1+package golang
2+
3+import (
4+ "go/scanner"
5+ "go/token"
6+
7+ "rickub.com/turbo-editors/turbo-core/syntax"
8+)
9+
10+// rawToken is one token as go/scanner reports it, reduced to what colouring
11+// needs: a byte range and the class it belongs to.
12+type rawToken struct {
13+ start int
14+ end int
15+ tok token.Token
16+ lit string
17+}
18+
19+// scanTokens tokenises src, ignoring every syntax error.
20+//
21+// Errors are expected: the file is being typed into. The scanner still returns
22+// a usable token for broken input — an unterminated string comes back as a
23+// STRING running to the end of the line — which is exactly what keeps the
24+// colours steady while the user types.
25+func scanTokens(src string) []rawToken {
26+ fileSet := token.NewFileSet()
27+ file := fileSet.AddFile("", fileSet.Base(), len(src))
28+
29+ var s scanner.Scanner
30+ s.Init(file, []byte(src), func(token.Position, string) {}, scanner.ScanComments)
31+
32+ var tokens []rawToken
33+ for {
34+ pos, tok, lit := s.Scan()
35+ if tok == token.EOF {
36+ return tokens
37+ }
38+
39+ start := file.Offset(pos)
40+ width := tokenWidth(tok, lit)
41+ if width == 0 {
42+ continue // an automatically inserted semicolon covers no text
43+ }
44+ tokens = append(tokens, rawToken{start: start, end: start + width, tok: tok, lit: lit})
45+ }
46+}
47+
48+// tokenWidth returns how many bytes of source a token covers.
49+//
50+// Operators and punctuation come back with an empty literal, so their width is
51+// that of their spelling; a semicolon the scanner inserted itself covers
52+// nothing at all.
53+func tokenWidth(tok token.Token, lit string) int {
54+ if tok == token.SEMICOLON && lit == "\n" {
55+ return 0
56+ }
57+ if lit != "" {
58+ return len(lit)
59+ }
60+ return len(tok.String())
61+}
62+
63+// classify assigns a colouring class to each token, using the token that
64+// follows and the one before where that is what distinguishes them: an
65+// identifier before "(" is a call, and one after "type" is a type name.
66+func classify(tokens []rawToken) []syntax.Class {
67+ classes := make([]syntax.Class, len(tokens))
68+ for i, t := range tokens {
69+ classes[i] = classOf(t, previous(tokens, i), next(tokens, i))
70+ }
71+ return classes
72+}
73+
74+// literalClasses are the tokens whose class follows from the token alone.
75+var literalClasses = map[token.Token]syntax.Class{
76+ token.COMMENT: syntax.ClassComment,
77+ token.STRING: syntax.ClassString,
78+ token.CHAR: syntax.ClassChar,
79+ token.INT: syntax.ClassNumber,
80+ token.FLOAT: syntax.ClassNumber,
81+ token.IMAG: syntax.ClassNumber,
82+}
83+
84+// classOf returns the class of a single token, given its neighbours.
85+func classOf(t rawToken, before, after token.Token) syntax.Class {
86+ if class, ok := literalClasses[t.tok]; ok {
87+ return class
88+ }
89+ if t.tok == token.IDENT {
90+ return identifierClass(t.lit, before, after)
91+ }
92+ return symbolClass(t.tok)
93+}
94+
95+// symbolClass returns the class of anything that is neither a literal nor an
96+// identifier: a keyword, a bracket, or an operator.
97+func symbolClass(tok token.Token) syntax.Class {
98+ switch {
99+ case tok.IsKeyword():
100+ return syntax.ClassKeyword
101+ case isPunctuation(tok):
102+ return syntax.ClassPunctuation
103+ case tok.IsOperator():
104+ return syntax.ClassOperator
105+ }
106+ return syntax.ClassIdentifier
107+}
108+
109+// predeclaredClasses are the identifiers the language itself provides, and
110+// what each of them is. They are recognised by name because they are not
111+// keywords: a file may shadow "len" or "any", and colouring it as the
112+// predeclared one anyway is what every other Go editor does too.
113+var predeclaredClasses = map[string]syntax.Class{
114+ // Types.
115+ "any": syntax.ClassType, "bool": syntax.ClassType, "byte": syntax.ClassType,
116+ "comparable": syntax.ClassType, "complex64": syntax.ClassType, "complex128": syntax.ClassType,
117+ "error": syntax.ClassType, "float32": syntax.ClassType, "float64": syntax.ClassType,
118+ "int": syntax.ClassType, "int8": syntax.ClassType, "int16": syntax.ClassType,
119+ "int32": syntax.ClassType, "int64": syntax.ClassType, "rune": syntax.ClassType,
120+ "string": syntax.ClassType, "uint": syntax.ClassType, "uint8": syntax.ClassType,
121+ "uint16": syntax.ClassType, "uint32": syntax.ClassType, "uint64": syntax.ClassType,
122+ "uintptr": syntax.ClassType,
123+
124+ // Constants.
125+ "true": syntax.ClassConstant, "false": syntax.ClassConstant,
126+ "iota": syntax.ClassConstant, "nil": syntax.ClassConstant,
127+
128+ // Functions.
129+ "append": syntax.ClassBuiltin, "cap": syntax.ClassBuiltin, "clear": syntax.ClassBuiltin,
130+ "close": syntax.ClassBuiltin, "complex": syntax.ClassBuiltin, "copy": syntax.ClassBuiltin,
131+ "delete": syntax.ClassBuiltin, "imag": syntax.ClassBuiltin, "len": syntax.ClassBuiltin,
132+ "make": syntax.ClassBuiltin, "max": syntax.ClassBuiltin, "min": syntax.ClassBuiltin,
133+ "new": syntax.ClassBuiltin, "panic": syntax.ClassBuiltin, "print": syntax.ClassBuiltin,
134+ "println": syntax.ClassBuiltin, "real": syntax.ClassBuiltin, "recover": syntax.ClassBuiltin,
135+}
136+
137+// identifierClass tells apart the several things an identifier can be.
138+func identifierClass(name string, before, after token.Token) syntax.Class {
139+ if class, ok := predeclaredClasses[name]; ok {
140+ return class
141+ }
142+
143+ class := syntax.ClassIdentifier
144+ switch {
145+ case before == token.TYPE, before == token.STRUCT, before == token.INTERFACE:
146+ class = syntax.ClassType
147+ case after == token.LPAREN, before == token.FUNC:
148+ class = syntax.ClassFunction
149+ }
150+ return class
151+}
152+
153+// previous returns the token before index i, or ILLEGAL at the start.
154+func previous(tokens []rawToken, i int) token.Token {
155+ if i == 0 {
156+ return token.ILLEGAL
157+ }
158+ return tokens[i-1].tok
159+}
160+
161+// next returns the token after index i, or ILLEGAL at the end.
162+func next(tokens []rawToken, i int) token.Token {
163+ if i+1 >= len(tokens) {
164+ return token.ILLEGAL
165+ }
166+ return tokens[i+1].tok
167+}
168+
169+// isPunctuation reports whether a token is structure rather than computation.
170+// Brackets, commas and the like are usually themed more quietly than the
171+// operators that actually do something.
172+func isPunctuation(tok token.Token) bool {
173+ switch tok {
174+ case token.LPAREN, token.RPAREN,
175+ token.LBRACK, token.RBRACK,
176+ token.LBRACE, token.RBRACE,
177+ token.COMMA, token.SEMICOLON, token.COLON, token.PERIOD:
178+ return true
179+ default:
180+ return false
181+ }
182+}
new file mode 100644
@@ -0,0 +1,182 @@
1+package golang
2+
3+import (
4+ "go/scanner"
5+ "go/token"
6+
7+ "rickub.com/turbo-editors/turbo-core/syntax"
8+)
9+
10+// rawToken is one token as go/scanner reports it, reduced to what colouring
11+// needs: a byte range and the class it belongs to.
12+type rawToken struct {
13+ start int
14+ end int
15+ tok token.Token
16+ lit string
17+}
18+
19+// scanTokens tokenises src, ignoring every syntax error.
20+//
21+// Errors are expected: the file is being typed into. The scanner still returns
22+// a usable token for broken input — an unterminated string comes back as a
23+// STRING running to the end of the line — which is exactly what keeps the
24+// colours steady while the user types.
25+func scanTokens(src string) []rawToken {
26+ fileSet := token.NewFileSet()
27+ file := fileSet.AddFile("", fileSet.Base(), len(src))
28+
29+ var s scanner.Scanner
30+ s.Init(file, []byte(src), func(token.Position, string) {}, scanner.ScanComments)
31+
32+ var tokens []rawToken
33+ for {
34+ pos, tok, lit := s.Scan()
35+ if tok == token.EOF {
36+ return tokens
37+ }
38+
39+ start := file.Offset(pos)
40+ width := tokenWidth(tok, lit)
41+ if width == 0 {
42+ continue // an automatically inserted semicolon covers no text
43+ }
44+ tokens = append(tokens, rawToken{start: start, end: start + width, tok: tok, lit: lit})
45+ }
46+}
47+
48+// tokenWidth returns how many bytes of source a token covers.
49+//
50+// Operators and punctuation come back with an empty literal, so their width is
51+// that of their spelling; a semicolon the scanner inserted itself covers
52+// nothing at all.
53+func tokenWidth(tok token.Token, lit string) int {
54+ if tok == token.SEMICOLON && lit == "\n" {
55+ return 0
56+ }
57+ if lit != "" {
58+ return len(lit)
59+ }
60+ return len(tok.String())
61+}
62+
63+// classify assigns a colouring class to each token, using the token that
64+// follows and the one before where that is what distinguishes them: an
65+// identifier before "(" is a call, and one after "type" is a type name.
66+func classify(tokens []rawToken) []syntax.Class {
67+ classes := make([]syntax.Class, len(tokens))
68+ for i, t := range tokens {
69+ classes[i] = classOf(t, previous(tokens, i), next(tokens, i))
70+ }
71+ return classes
72+}
73+
74+// literalClasses are the tokens whose class follows from the token alone.
75+var literalClasses = map[token.Token]syntax.Class{
76+ token.COMMENT: syntax.ClassComment,
77+ token.STRING: syntax.ClassString,
78+ token.CHAR: syntax.ClassChar,
79+ token.INT: syntax.ClassNumber,
80+ token.FLOAT: syntax.ClassNumber,
81+ token.IMAG: syntax.ClassNumber,
82+}
83+
84+// classOf returns the class of a single token, given its neighbours.
85+func classOf(t rawToken, before, after token.Token) syntax.Class {
86+ if class, ok := literalClasses[t.tok]; ok {
87+ return class
88+ }
89+ if t.tok == token.IDENT {
90+ return identifierClass(t.lit, before, after)
91+ }
92+ return symbolClass(t.tok)
93+}
94+
95+// symbolClass returns the class of anything that is neither a literal nor an
96+// identifier: a keyword, a bracket, or an operator.
97+func symbolClass(tok token.Token) syntax.Class {
98+ switch {
99+ case tok.IsKeyword():
100+ return syntax.ClassKeyword
101+ case isPunctuation(tok):
102+ return syntax.ClassPunctuation
103+ case tok.IsOperator():
104+ return syntax.ClassOperator
105+ }
106+ return syntax.ClassIdentifier
107+}
108+
109+// predeclaredClasses are the identifiers the language itself provides, and
110+// what each of them is. They are recognised by name because they are not
111+// keywords: a file may shadow "len" or "any", and colouring it as the
112+// predeclared one anyway is what every other Go editor does too.
113+var predeclaredClasses = map[string]syntax.Class{
114+ // Types.
115+ "any": syntax.ClassType, "bool": syntax.ClassType, "byte": syntax.ClassType,
116+ "comparable": syntax.ClassType, "complex64": syntax.ClassType, "complex128": syntax.ClassType,
117+ "error": syntax.ClassType, "float32": syntax.ClassType, "float64": syntax.ClassType,
118+ "int": syntax.ClassType, "int8": syntax.ClassType, "int16": syntax.ClassType,
119+ "int32": syntax.ClassType, "int64": syntax.ClassType, "rune": syntax.ClassType,
120+ "string": syntax.ClassType, "uint": syntax.ClassType, "uint8": syntax.ClassType,
121+ "uint16": syntax.ClassType, "uint32": syntax.ClassType, "uint64": syntax.ClassType,
122+ "uintptr": syntax.ClassType,
123+
124+ // Constants.
125+ "true": syntax.ClassConstant, "false": syntax.ClassConstant,
126+ "iota": syntax.ClassConstant, "nil": syntax.ClassConstant,
127+
128+ // Functions.
129+ "append": syntax.ClassBuiltin, "cap": syntax.ClassBuiltin, "clear": syntax.ClassBuiltin,
130+ "close": syntax.ClassBuiltin, "complex": syntax.ClassBuiltin, "copy": syntax.ClassBuiltin,
131+ "delete": syntax.ClassBuiltin, "imag": syntax.ClassBuiltin, "len": syntax.ClassBuiltin,
132+ "make": syntax.ClassBuiltin, "max": syntax.ClassBuiltin, "min": syntax.ClassBuiltin,
133+ "new": syntax.ClassBuiltin, "panic": syntax.ClassBuiltin, "print": syntax.ClassBuiltin,
134+ "println": syntax.ClassBuiltin, "real": syntax.ClassBuiltin, "recover": syntax.ClassBuiltin,
135+}
136+
137+// identifierClass tells apart the several things an identifier can be.
138+func identifierClass(name string, before, after token.Token) syntax.Class {
139+ if class, ok := predeclaredClasses[name]; ok {
140+ return class
141+ }
142+
143+ class := syntax.ClassIdentifier
144+ switch {
145+ case before == token.TYPE, before == token.STRUCT, before == token.INTERFACE:
146+ class = syntax.ClassType
147+ case after == token.LPAREN, before == token.FUNC:
148+ class = syntax.ClassFunction
149+ }
150+ return class
151+}
152+
153+// previous returns the token before index i, or ILLEGAL at the start.
154+func previous(tokens []rawToken, i int) token.Token {
155+ if i == 0 {
156+ return token.ILLEGAL
157+ }
158+ return tokens[i-1].tok
159+}
160+
161+// next returns the token after index i, or ILLEGAL at the end.
162+func next(tokens []rawToken, i int) token.Token {
163+ if i+1 >= len(tokens) {
164+ return token.ILLEGAL
165+ }
166+ return tokens[i+1].tok
167+}
168+
169+// isPunctuation reports whether a token is structure rather than computation.
170+// Brackets, commas and the like are usually themed more quietly than the
171+// operators that actually do something.
172+func isPunctuation(tok token.Token) bool {
173+ switch tok {
174+ case token.LPAREN, token.RPAREN,
175+ token.LBRACK, token.RBRACK,
176+ token.LBRACE, token.RBRACE,
177+ token.COMMA, token.SEMICOLON, token.COLON, token.PERIOD:
178+ return true
179+ default:
180+ return false
181+ }
182+}
added internal/golang/scan_test.go +249 -0
new file mode 100644
@@ -0,0 +1,249 @@
1+package golang
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "rickub.com/turbo-editors/turbo-core/syntax"
8+)
9+
10+// The tests below came from turbo-core, where the Go scanner used to live. They
11+// drive it from the outside now — through syntax.Highlight, after Register —
12+// which is how the editor reaches it too.
13+
14+func init() { Register() }
15+
16+// classAt returns the class covering a rune column on a line, and whether any
17+// span covers it at all.
18+func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) {
19+ if line < 0 || line >= len(spans) {
20+ return 0, false
21+ }
22+ for _, s := range spans[line] {
23+ if col >= s.Start && col < s.End {
24+ return s.Class, true
25+ }
26+ }
27+ return 0, false
28+}
29+
30+// classOfWord returns the class of the first occurrence of word in src.
31+func classOfWord(t *testing.T, src, word string) syntax.Class {
32+ t.Helper()
33+
34+ index := strings.Index(src, word)
35+ if index < 0 {
36+ t.Fatalf("%q does not appear in the source", word)
37+ }
38+ line := strings.Count(src[:index], "\n")
39+ col := index - (strings.LastIndex(src[:index], "\n") + 1)
40+
41+ class, ok := classAt(syntax.Highlight(Language, src), line, col)
42+ if !ok {
43+ t.Fatalf("no span covers %q at line %d column %d", word, line, col)
44+ }
45+ return class
46+}
47+
48+func TestHighlightReturnsOneEntryPerLine(t *testing.T) {
49+ tests := []struct {
50+ name string
51+ src string
52+ want int
53+ }{
54+ {"empty", "", 1},
55+ {"one line without a terminator", "package main", 1},
56+ {"one line with a terminator", "package main\n", 2},
57+ {"three lines", "a\nb\nc", 3},
58+ }
59+
60+ for _, tc := range tests {
61+ t.Run(tc.name, func(t *testing.T) {
62+ if got := len(syntax.Highlight(Language, tc.src)); got != tc.want {
63+ t.Errorf("syntax.Highlight(Language, %q) returned %d lines, want %d", tc.src, got, tc.want)
64+ }
65+ })
66+ }
67+}
68+
69+func TestEachTokenClass(t *testing.T) {
70+ const src = `package main
71+
72+// a comment
73+import "fmt"
74+
75+type Point struct{ X, Y int }
76+
77+func main() {
78+ var name string = "hello"
79+ const c = 'x'
80+ n := 42 + 3.5
81+ ok := true
82+ _ = len(name)
83+ fmt.Println(name, n, ok, nil)
84+}
85+`
86+
87+ tests := []struct {
88+ word string
89+ want syntax.Class
90+ }{
91+ {"package", syntax.ClassKeyword},
92+ {"func", syntax.ClassKeyword},
93+ {"// a comment", syntax.ClassComment},
94+ {`"fmt"`, syntax.ClassString},
95+ {"Point", syntax.ClassType},
96+ {"string", syntax.ClassType},
97+ {"int", syntax.ClassType},
98+ {"'x'", syntax.ClassChar},
99+ {"42", syntax.ClassNumber},
100+ {"3.5", syntax.ClassNumber},
101+ {"true", syntax.ClassConstant},
102+ {"nil", syntax.ClassConstant},
103+ {"len", syntax.ClassBuiltin},
104+ {"Println", syntax.ClassFunction},
105+ {"name", syntax.ClassIdentifier},
106+ {":=", syntax.ClassOperator},
107+ {"+", syntax.ClassOperator},
108+ }
109+
110+ for _, tc := range tests {
111+ t.Run(tc.word, func(t *testing.T) {
112+ if got := classOfWord(t, src, tc.word); got != tc.want {
113+ t.Errorf("%q is coloured as %v, want %v", tc.word, got, tc.want)
114+ }
115+ })
116+ }
117+}
118+
119+func TestADeclaredFunctionNameIsAFunction(t *testing.T) {
120+ if got := classOfWord(t, "func add(a, b int) int { return a + b }", "add"); got != syntax.ClassFunction {
121+ t.Errorf("the declared name is %v, want function", got)
122+ }
123+}
124+
125+func TestAPackageNameIsAPlainIdentifier(t *testing.T) {
126+ // "main" here is neither a call nor a declaration, so it must not be
127+ // dressed up as a function just because it follows a keyword.
128+ if got := classOfWord(t, "package main\n", "main"); got != syntax.ClassIdentifier {
129+ t.Errorf("the package name is %v, want identifier", got)
130+ }
131+}
132+
133+func TestPunctuationIsSeparateFromOperators(t *testing.T) {
134+ const src = "f(a, b)"
135+
136+ for _, col := range []int{1, 3, 6} { // '(' ',' ')'
137+ if got, _ := classAt(syntax.Highlight(Language, src), 0, col); got != syntax.ClassPunctuation {
138+ t.Errorf("column %d is %v, want punctuation", col, got)
139+ }
140+ }
141+}
142+
143+func TestSpansNeverStraddleALineBreak(t *testing.T) {
144+ const src = "/* a comment\nspanning three\nlines */\nx := 1"
145+
146+ spans := syntax.Highlight(Language, src)
147+
148+ for line := range 3 {
149+ if got, ok := classAt(spans, line, 0); !ok || got != syntax.ClassComment {
150+ t.Errorf("line %d starts with %v (covered=%v), want comment", line, got, ok)
151+ }
152+ }
153+ for _, s := range spans[0] {
154+ if s.End > len([]rune("/* a comment")) {
155+ t.Errorf("a span on line 0 ends at column %d, past the end of the line", s.End)
156+ }
157+ }
158+ if got, _ := classAt(spans, 3, 0); got != syntax.ClassIdentifier {
159+ t.Errorf("the line after the comment is %v, want identifier", got)
160+ }
161+}
162+
163+func TestRawStringsSpanningLinesAreColoured(t *testing.T) {
164+ src := "s := `line one\nline two`\n"
165+
166+ spans := syntax.Highlight(Language, src)
167+
168+ if got, _ := classAt(spans, 0, 5); got != syntax.ClassString {
169+ t.Errorf("the opening of the raw string is %v, want string", got)
170+ }
171+ if got, _ := classAt(spans, 1, 0); got != syntax.ClassString {
172+ t.Errorf("the continuation of the raw string is %v, want string", got)
173+ }
174+}
175+
176+func TestBrokenSourceIsStillColoured(t *testing.T) {
177+ tests := []struct {
178+ name string
179+ src string
180+ }{
181+ {"unterminated string", `x := "hello`},
182+ {"unterminated comment", "/* never closed"},
183+ {"unterminated rune", "c := 'a"},
184+ {"stray brace", "func main() { }}}"},
185+ {"half-typed declaration", "func "},
186+ {"nothing but an operator", "=="},
187+ {"an illegal character", "x := #"},
188+ }
189+
190+ for _, tc := range tests {
191+ t.Run(tc.name, func(t *testing.T) {
192+ spans := syntax.Highlight(Language, tc.src) // must not panic
193+ if len(spans) == 0 {
194+ t.Error("Highlight returned no lines at all")
195+ }
196+ })
197+ }
198+}
199+
200+func TestUnterminatedStringIsStillAString(t *testing.T) {
201+ spans := syntax.Highlight(Language, `x := "hello`)
202+
203+ if got, _ := classAt(spans, 0, 6); got != syntax.ClassString {
204+ t.Errorf("the text after the quote is %v, want string — colours must not flicker while typing", got)
205+ }
206+}
207+
208+func TestColumnsAreCountedInRunesNotBytes(t *testing.T) {
209+ // The comment holds multi-byte characters, so a span measured in bytes
210+ // would run past the end of the following line.
211+ const src = "// héllo wörld\nfunc main() {}"
212+
213+ spans := syntax.Highlight(Language, src)
214+
215+ if got, _ := classAt(spans, 0, 13); got != syntax.ClassComment {
216+ t.Errorf("column 13 of the comment is %v, want comment", got)
217+ }
218+ if _, ok := classAt(spans, 0, 14); ok {
219+ t.Error("a span covers column 14, past the 14 runes of the comment")
220+ }
221+ if got, _ := classAt(spans, 1, 0); got != syntax.ClassKeyword {
222+ t.Errorf("the line after the accented comment is %v, want keyword", got)
223+ }
224+}
225+
226+func TestIdentifiersInsideStringsAreNotColouredAsCode(t *testing.T) {
227+ spans := syntax.Highlight(Language, `s := "func main"`)
228+
229+ if got, _ := classAt(spans, 0, 6); got != syntax.ClassString {
230+ t.Errorf("a keyword inside a string is %v, want string", got)
231+ }
232+}
233+
234+func TestSpansOnALineAreOrderedAndDoNotOverlap(t *testing.T) {
235+ const src = "func add(a, b int) int { return a + b }"
236+
237+ for _, line := range syntax.Highlight(Language, src) {
238+ previousEnd := 0
239+ for _, s := range line {
240+ if s.Start < previousEnd {
241+ t.Errorf("span %+v starts before the previous one ended at %d", s, previousEnd)
242+ }
243+ if s.End <= s.Start {
244+ t.Errorf("span %+v is empty or reversed", s)
245+ }
246+ previousEnd = s.End
247+ }
248+ }
249+}
new file mode 100644
@@ -0,0 +1,249 @@
1+package golang
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "rickub.com/turbo-editors/turbo-core/syntax"
8+)
9+
10+// The tests below came from turbo-core, where the Go scanner used to live. They
11+// drive it from the outside now — through syntax.Highlight, after Register —
12+// which is how the editor reaches it too.
13+
14+func init() { Register() }
15+
16+// classAt returns the class covering a rune column on a line, and whether any
17+// span covers it at all.
18+func classAt(spans [][]syntax.Span, line, col int) (syntax.Class, bool) {
19+ if line < 0 || line >= len(spans) {
20+ return 0, false
21+ }
22+ for _, s := range spans[line] {
23+ if col >= s.Start && col < s.End {
24+ return s.Class, true
25+ }
26+ }
27+ return 0, false
28+}
29+
30+// classOfWord returns the class of the first occurrence of word in src.
31+func classOfWord(t *testing.T, src, word string) syntax.Class {
32+ t.Helper()
33+
34+ index := strings.Index(src, word)
35+ if index < 0 {
36+ t.Fatalf("%q does not appear in the source", word)
37+ }
38+ line := strings.Count(src[:index], "\n")
39+ col := index - (strings.LastIndex(src[:index], "\n") + 1)
40+
41+ class, ok := classAt(syntax.Highlight(Language, src), line, col)
42+ if !ok {
43+ t.Fatalf("no span covers %q at line %d column %d", word, line, col)
44+ }
45+ return class
46+}
47+
48+func TestHighlightReturnsOneEntryPerLine(t *testing.T) {
49+ tests := []struct {
50+ name string
51+ src string
52+ want int
53+ }{
54+ {"empty", "", 1},
55+ {"one line without a terminator", "package main", 1},
56+ {"one line with a terminator", "package main\n", 2},
57+ {"three lines", "a\nb\nc", 3},
58+ }
59+
60+ for _, tc := range tests {
61+ t.Run(tc.name, func(t *testing.T) {
62+ if got := len(syntax.Highlight(Language, tc.src)); got != tc.want {
63+ t.Errorf("syntax.Highlight(Language, %q) returned %d lines, want %d", tc.src, got, tc.want)
64+ }
65+ })
66+ }
67+}
68+
69+func TestEachTokenClass(t *testing.T) {
70+ const src = `package main
71+
72+// a comment
73+import "fmt"
74+
75+type Point struct{ X, Y int }
76+
77+func main() {
78+ var name string = "hello"
79+ const c = 'x'
80+ n := 42 + 3.5
81+ ok := true
82+ _ = len(name)
83+ fmt.Println(name, n, ok, nil)
84+}
85+`
86+
87+ tests := []struct {
88+ word string
89+ want syntax.Class
90+ }{
91+ {"package", syntax.ClassKeyword},
92+ {"func", syntax.ClassKeyword},
93+ {"// a comment", syntax.ClassComment},
94+ {`"fmt"`, syntax.ClassString},
95+ {"Point", syntax.ClassType},
96+ {"string", syntax.ClassType},
97+ {"int", syntax.ClassType},
98+ {"'x'", syntax.ClassChar},
99+ {"42", syntax.ClassNumber},
100+ {"3.5", syntax.ClassNumber},
101+ {"true", syntax.ClassConstant},
102+ {"nil", syntax.ClassConstant},
103+ {"len", syntax.ClassBuiltin},
104+ {"Println", syntax.ClassFunction},
105+ {"name", syntax.ClassIdentifier},
106+ {":=", syntax.ClassOperator},
107+ {"+", syntax.ClassOperator},
108+ }
109+
110+ for _, tc := range tests {
111+ t.Run(tc.word, func(t *testing.T) {
112+ if got := classOfWord(t, src, tc.word); got != tc.want {
113+ t.Errorf("%q is coloured as %v, want %v", tc.word, got, tc.want)
114+ }
115+ })
116+ }
117+}
118+
119+func TestADeclaredFunctionNameIsAFunction(t *testing.T) {
120+ if got := classOfWord(t, "func add(a, b int) int { return a + b }", "add"); got != syntax.ClassFunction {
121+ t.Errorf("the declared name is %v, want function", got)
122+ }
123+}
124+
125+func TestAPackageNameIsAPlainIdentifier(t *testing.T) {
126+ // "main" here is neither a call nor a declaration, so it must not be
127+ // dressed up as a function just because it follows a keyword.
128+ if got := classOfWord(t, "package main\n", "main"); got != syntax.ClassIdentifier {
129+ t.Errorf("the package name is %v, want identifier", got)
130+ }
131+}
132+
133+func TestPunctuationIsSeparateFromOperators(t *testing.T) {
134+ const src = "f(a, b)"
135+
136+ for _, col := range []int{1, 3, 6} { // '(' ',' ')'
137+ if got, _ := classAt(syntax.Highlight(Language, src), 0, col); got != syntax.ClassPunctuation {
138+ t.Errorf("column %d is %v, want punctuation", col, got)
139+ }
140+ }
141+}
142+
143+func TestSpansNeverStraddleALineBreak(t *testing.T) {
144+ const src = "/* a comment\nspanning three\nlines */\nx := 1"
145+
146+ spans := syntax.Highlight(Language, src)
147+
148+ for line := range 3 {
149+ if got, ok := classAt(spans, line, 0); !ok || got != syntax.ClassComment {
150+ t.Errorf("line %d starts with %v (covered=%v), want comment", line, got, ok)
151+ }
152+ }
153+ for _, s := range spans[0] {
154+ if s.End > len([]rune("/* a comment")) {
155+ t.Errorf("a span on line 0 ends at column %d, past the end of the line", s.End)
156+ }
157+ }
158+ if got, _ := classAt(spans, 3, 0); got != syntax.ClassIdentifier {
159+ t.Errorf("the line after the comment is %v, want identifier", got)
160+ }
161+}
162+
163+func TestRawStringsSpanningLinesAreColoured(t *testing.T) {
164+ src := "s := `line one\nline two`\n"
165+
166+ spans := syntax.Highlight(Language, src)
167+
168+ if got, _ := classAt(spans, 0, 5); got != syntax.ClassString {
169+ t.Errorf("the opening of the raw string is %v, want string", got)
170+ }
171+ if got, _ := classAt(spans, 1, 0); got != syntax.ClassString {
172+ t.Errorf("the continuation of the raw string is %v, want string", got)
173+ }
174+}
175+
176+func TestBrokenSourceIsStillColoured(t *testing.T) {
177+ tests := []struct {
178+ name string
179+ src string
180+ }{
181+ {"unterminated string", `x := "hello`},
182+ {"unterminated comment", "/* never closed"},
183+ {"unterminated rune", "c := 'a"},
184+ {"stray brace", "func main() { }}}"},
185+ {"half-typed declaration", "func "},
186+ {"nothing but an operator", "=="},
187+ {"an illegal character", "x := #"},
188+ }
189+
190+ for _, tc := range tests {
191+ t.Run(tc.name, func(t *testing.T) {
192+ spans := syntax.Highlight(Language, tc.src) // must not panic
193+ if len(spans) == 0 {
194+ t.Error("Highlight returned no lines at all")
195+ }
196+ })
197+ }
198+}
199+
200+func TestUnterminatedStringIsStillAString(t *testing.T) {
201+ spans := syntax.Highlight(Language, `x := "hello`)
202+
203+ if got, _ := classAt(spans, 0, 6); got != syntax.ClassString {
204+ t.Errorf("the text after the quote is %v, want string — colours must not flicker while typing", got)
205+ }
206+}
207+
208+func TestColumnsAreCountedInRunesNotBytes(t *testing.T) {
209+ // The comment holds multi-byte characters, so a span measured in bytes
210+ // would run past the end of the following line.
211+ const src = "// héllo wörld\nfunc main() {}"
212+
213+ spans := syntax.Highlight(Language, src)
214+
215+ if got, _ := classAt(spans, 0, 13); got != syntax.ClassComment {
216+ t.Errorf("column 13 of the comment is %v, want comment", got)
217+ }
218+ if _, ok := classAt(spans, 0, 14); ok {
219+ t.Error("a span covers column 14, past the 14 runes of the comment")
220+ }
221+ if got, _ := classAt(spans, 1, 0); got != syntax.ClassKeyword {
222+ t.Errorf("the line after the accented comment is %v, want keyword", got)
223+ }
224+}
225+
226+func TestIdentifiersInsideStringsAreNotColouredAsCode(t *testing.T) {
227+ spans := syntax.Highlight(Language, `s := "func main"`)
228+
229+ if got, _ := classAt(spans, 0, 6); got != syntax.ClassString {
230+ t.Errorf("a keyword inside a string is %v, want string", got)
231+ }
232+}
233+
234+func TestSpansOnALineAreOrderedAndDoNotOverlap(t *testing.T) {
235+ const src = "func add(a, b int) int { return a + b }"
236+
237+ for _, line := range syntax.Highlight(Language, src) {
238+ previousEnd := 0
239+ for _, s := range line {
240+ if s.Start < previousEnd {
241+ t.Errorf("span %+v starts before the previous one ended at %d", s, previousEnd)
242+ }
243+ if s.End <= s.Start {
244+ t.Errorf("span %+v is empty or reversed", s)
245+ }
246+ previousEnd = s.End
247+ }
248+ }
249+}
added internal/golang/settings.toml.tmpl +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+# turbo-go project settings.
2+#
3+# These apply to everyone who opens this project in turbo-go. Delete this file
4+# and the editor falls back to its own defaults.
5+
6+[editor]
7+
8+# The colour theme to start in. `turbo-go -list-themes` lists them all.
9+# A -theme flag on the command line overrides this.
10+theme = %q
11+
12+# Write modified files by themselves, a short while after you stop typing.
13+# On, because a project that has gone to the trouble of having a settings file
14+# has said what it wants; set it to false and save, and it stops at once.
15+autosave = true
16+
17+# How long that while is. Any Go duration: "500ms", "2s", "1m".
18+autosave_delay = %q
new file mode 100644
@@ -0,0 +1,18 @@
1+# turbo-go project settings.
2+#
3+# These apply to everyone who opens this project in turbo-go. Delete this file
4+# and the editor falls back to its own defaults.
5+
6+[editor]
7+
8+# The colour theme to start in. `turbo-go -list-themes` lists them all.
9+# A -theme flag on the command line overrides this.
10+theme = %q
11+
12+# Write modified files by themselves, a short while after you stop typing.
13+# On, because a project that has gone to the trouble of having a settings file
14+# has said what it wants; set it to false and save, and it stops at once.
15+autosave = true
16+
17+# How long that while is. Any Go duration: "500ms", "2s", "1m".
18+autosave_delay = %q
added internal/golang/snippets.toml.tmpl +53 -0
new file mode 100644
@@ -0,0 +1,53 @@
1+# turbo-go snippets.
2+#
3+# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a
4+# group appear together in a submenu of that name; one with no group goes into
5+# %s. A snippet is inserted at the cursor, and every line after the
6+# first is indented to match the line you inserted it on.
7+#
8+# languages restricts a snippet to files of those kinds, by the names the
9+# editor uses: go, toml, yaml, markdown, javascript, html, xml, dockerfile,
10+# bash. Leave it out and the snippet is offered everywhere.
11+#
12+# Your own snippets, shared across every project, go in:
13+# %s
14+
15+[[snippet]]
16+group = "Go"
17+name = "main"
18+languages = ["go"]
19+body = """package main
20+
21+import "fmt"
22+
23+func main() {
24+ fmt.Println("hello world")
25+}"""
26+
27+[[snippet]]
28+group = "Go"
29+name = "switch"
30+languages = ["go"]
31+body = """
32+ i := 1
33+
34+ switch i {
35+ case 1:
36+ fmt.Println("one")
37+ case 2:
38+ fmt.Println("two")
39+ case 3:
40+ fmt.Println("three")
41+ }
42+"""
43+
44+[[snippet]]
45+group = "General"
46+name = "Hello"
47+body = "Hello!!!"
48+
49+[[snippet]]
50+group = "Markdown"
51+name = "Image"
52+languages = ["markdown"]
53+body = "![img](./pictures)"
new file mode 100644
@@ -0,0 +1,53 @@
1+# turbo-go snippets.
2+#
3+# Each [[snippet]] becomes one line of the Snippets menu. Snippets sharing a
4+# group appear together in a submenu of that name; one with no group goes into
5+# %s. A snippet is inserted at the cursor, and every line after the
6+# first is indented to match the line you inserted it on.
7+#
8+# languages restricts a snippet to files of those kinds, by the names the
9+# editor uses: go, toml, yaml, markdown, javascript, html, xml, dockerfile,
10+# bash. Leave it out and the snippet is offered everywhere.
11+#
12+# Your own snippets, shared across every project, go in:
13+# %s
14+
15+[[snippet]]
16+group = "Go"
17+name = "main"
18+languages = ["go"]
19+body = """package main
20+
21+import "fmt"
22+
23+func main() {
24+ fmt.Println("hello world")
25+}"""
26+
27+[[snippet]]
28+group = "Go"
29+name = "switch"
30+languages = ["go"]
31+body = """
32+ i := 1
33+
34+ switch i {
35+ case 1:
36+ fmt.Println("one")
37+ case 2:
38+ fmt.Println("two")
39+ case 3:
40+ fmt.Println("three")
41+ }
42+"""
43+
44+[[snippet]]
45+group = "General"
46+name = "Hello"
47+body = "Hello!!!"
48+
49+[[snippet]]
50+group = "Markdown"
51+name = "Image"
52+languages = ["markdown"]
53+body = "![img](./pictures)"
added internal/golang/templates.go +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+package golang
2+
3+import _ "embed"
4+
5+// The starter files Turbo Go writes into a project's .turbo-go directory.
6+//
7+// They live in four files beside this one and are embedded into the binary at
8+// compile time. Written out as text rather than encoded from structs because
9+// they are meant to be read and edited by a person: the comments in them say
10+// what each key is for, which is the whole reason the editor offers to create
11+// them at all rather than only to read them.
12+//
13+// Their contents are the one part of these four files that is about Go
14+// rather than about editing, which is why they live here and not in turbo-core.
15+//
16+// **The .tmpl suffix is not decoration.** Each file is formatted with
17+// fmt.Sprintf before it is written, and settings.toml.tmpl holds `theme = %q`
18+// — which is not valid TOML. Naming it settings.toml would be a claim it
19+// cannot meet: a TOML linter would reject it, and Turbo Go itself would colour it
20+// as TOML and draw it as broken. The blanks each one takes are documented on
21+// profile.Templates, and templates_test.go holds them to it.
22+
23+// settingsTemplate is the settings file a project gets when it asks for one.
24+//
25+// autosave is on: a project that has gone to the trouble of creating a
26+// settings file has said what it wants, and the file is the visible, editable
27+// place to say otherwise. settings.Default() — what applies with no file at
28+// all — stays off.
29+//
30+//go:embed settings.toml.tmpl
31+var settingsTemplate string
32+
33+// snippetsTemplate is the snippets file a project gets when it asks for one.
34+//
35+// It lists every language name the editor knows in its `languages` comment,
36+// because that comment is where a user finds out what they may write there. A
37+// test iterates syntax.Registered() rather than a hardcoded list, so the
38+// comment cannot fall behind the registry.
39+//
40+//go:embed snippets.toml.tmpl
41+var snippetsTemplate string
42+
43+// toolsTemplate is the tools file a project gets when it asks for one.
44+//
45+// Five commands, and the two features that are invisible otherwise: a
46+// {{placeholder}} that asks for a value before the command runs, and the
47+// `menu` key that puts a tool in a menu of its own.
48+//
49+//go:embed tools.toml.tmpl
50+var toolsTemplate string
51+
52+// agentsTemplate is the agents file a project gets when it asks for one.
53+//
54+// It takes one blank, used twice: the editor's own project directory. The
55+// example agent's arguments point at a configuration file kept beside this one,
56+// and the comment says where a user-level copy of the file would live.
57+//
58+//go:embed acp.toml.tmpl
59+var agentsTemplate string
new file mode 100644
@@ -0,0 +1,59 @@
1+package golang
2+
3+import _ "embed"
4+
5+// The starter files Turbo Go writes into a project's .turbo-go directory.
6+//
7+// They live in four files beside this one and are embedded into the binary at
8+// compile time. Written out as text rather than encoded from structs because
9+// they are meant to be read and edited by a person: the comments in them say
10+// what each key is for, which is the whole reason the editor offers to create
11+// them at all rather than only to read them.
12+//
13+// Their contents are the one part of these four files that is about Go
14+// rather than about editing, which is why they live here and not in turbo-core.
15+//
16+// **The .tmpl suffix is not decoration.** Each file is formatted with
17+// fmt.Sprintf before it is written, and settings.toml.tmpl holds `theme = %q`
18+// — which is not valid TOML. Naming it settings.toml would be a claim it
19+// cannot meet: a TOML linter would reject it, and Turbo Go itself would colour it
20+// as TOML and draw it as broken. The blanks each one takes are documented on
21+// profile.Templates, and templates_test.go holds them to it.
22+
23+// settingsTemplate is the settings file a project gets when it asks for one.
24+//
25+// autosave is on: a project that has gone to the trouble of creating a
26+// settings file has said what it wants, and the file is the visible, editable
27+// place to say otherwise. settings.Default() — what applies with no file at
28+// all — stays off.
29+//
30+//go:embed settings.toml.tmpl
31+var settingsTemplate string
32+
33+// snippetsTemplate is the snippets file a project gets when it asks for one.
34+//
35+// It lists every language name the editor knows in its `languages` comment,
36+// because that comment is where a user finds out what they may write there. A
37+// test iterates syntax.Registered() rather than a hardcoded list, so the
38+// comment cannot fall behind the registry.
39+//
40+//go:embed snippets.toml.tmpl
41+var snippetsTemplate string
42+
43+// toolsTemplate is the tools file a project gets when it asks for one.
44+//
45+// Five commands, and the two features that are invisible otherwise: a
46+// {{placeholder}} that asks for a value before the command runs, and the
47+// `menu` key that puts a tool in a menu of its own.
48+//
49+//go:embed tools.toml.tmpl
50+var toolsTemplate string
51+
52+// agentsTemplate is the agents file a project gets when it asks for one.
53+//
54+// It takes one blank, used twice: the editor's own project directory. The
55+// example agent's arguments point at a configuration file kept beside this one,
56+// and the comment says where a user-level copy of the file would live.
57+//
58+//go:embed acp.toml.tmpl
59+var agentsTemplate string
added internal/golang/templates_test.go +507 -0
new file mode 100644
@@ -0,0 +1,507 @@
1+package golang
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "slices"
8+ "strings"
9+ "testing"
10+
11+ "rickub.com/turbo-editors/turbo-core/acp"
12+ "rickub.com/turbo-editors/turbo-core/settings"
13+ "rickub.com/turbo-editors/turbo-core/snippets"
14+ "rickub.com/turbo-editors/turbo-core/syntax"
15+ "rickub.com/turbo-editors/turbo-core/tools"
16+)
17+
18+// The starter files Turbo Go writes are the one part of a project's .turbo-go
19+// directory that is about Go, so this is where what is *in* them is checked.
20+// That the file written is the profile's template at all is turbo-core's test.
21+
22+// noUserSnippets points the user's own snippets at an empty directory, so a
23+// test never reads whoever is running it.
24+func noUserSnippets(t *testing.T) {
25+ t.Helper()
26+ t.Setenv(Profile().SnippetDirEnvVar(), t.TempDir())
27+}
28+
29+// loadTools reads a project's tools, failing the test if it cannot.
30+func loadTools(t *testing.T, dir string) tools.List {
31+ t.Helper()
32+
33+ list, err := tools.Load(Profile(), dir)
34+ if err != nil {
35+ t.Fatalf("tools.Load(%q) error = %v", dir, err)
36+ }
37+ return list
38+}
39+
40+// loadSnippets reads a project's snippets, failing the test if it cannot.
41+func loadSnippets(t *testing.T, dir string) snippets.List {
42+ t.Helper()
43+
44+ list, err := snippets.Load(Profile(), dir)
45+ if err != nil {
46+ t.Fatalf("snippets.Load(%q) error = %v", dir, err)
47+ }
48+ return list
49+}
50+
51+// readFile returns a file's contents.
52+func readFile(t *testing.T, path string) string {
53+ t.Helper()
54+
55+ data, err := os.ReadFile(path)
56+ if err != nil {
57+ t.Fatalf("reading %s: %v", path, err)
58+ }
59+ return string(data)
60+}
61+
62+// plain strips the tilde hot-key markers from a label.
63+func plain(label string) string { return strings.ReplaceAll(label, "~", "") }
64+
65+// hotKey returns the character between the tildes, or 0 when there is none.
66+func hotKey(label string) rune {
67+ first := strings.IndexByte(label, '~')
68+ if first < 0 || first+1 >= len(label) {
69+ return 0
70+ }
71+ return rune(label[first+1])
72+}
73+
74+func TestTheCreatedToolsFileHoldsTheGoCommandsAndTheExamplesThatTeachTheFormat(t *testing.T) {
75+ // The first five are what a Go project runs before it commits, and they are
76+ // the reason the file exists at all. The three after them are there to
77+ // teach the format itself — a value the editor asks for, a menu of the
78+ // tool's own, an output that is not the default — and a starter file that
79+ // only listed the five would leave all three undiscoverable.
80+ dir := t.TempDir()
81+ if _, err := tools.Create(Profile(), dir); err != nil {
82+ t.Fatalf("tools.Create() error = %v", err)
83+ }
84+
85+ byName := map[string]string{}
86+ for _, tool := range loadTools(t, dir).Tools() {
87+ byName[plain(tool.Name)] = tool.Command
88+ }
89+
90+ want := map[string]string{
91+ "Format": "gofmt -l -w .",
92+ "Lint": "go vet ./...",
93+ "Build": "go build ./...",
94+ "Test": "go test ./...",
95+ "Run": "go run .",
96+ "Grep": "grep -rn {{pattern}} --include='*.go' .",
97+ "Init module": "go mod init {{module path}}",
98+ "Echo": "echo 🎉 tada!",
99+ }
100+ for name, command := range want {
101+ if got := byName[name]; got != command {
102+ t.Errorf("%s runs %q, want %q", name, got, command)
103+ }
104+ }
105+ for name := range byName {
106+ if _, ok := want[name]; !ok {
107+ t.Errorf("the created file holds a tool this test does not know about: %q", name)
108+ }
109+ }
110+}
111+
112+func TestTheCreatedToolsCarryHotKeys(t *testing.T) {
113+ // Five items in a menu are worth reaching with one keystroke each.
114+ dir := t.TempDir()
115+ if _, err := tools.Create(Profile(), dir); err != nil {
116+ t.Fatalf("tools.Create() error = %v", err)
117+ }
118+
119+ seen := map[rune]string{}
120+ for _, tool := range loadTools(t, dir).Tools() {
121+ key := hotKey(tool.Name)
122+ if key == 0 {
123+ t.Errorf("%q has no hot key", tool.Name)
124+ continue
125+ }
126+ if other, clash := seen[key]; clash {
127+ t.Errorf("%q and %q both answer to %c", other, tool.Name, key)
128+ }
129+ seen[key] = tool.Name
130+ }
131+}
132+
133+func TestTheCreatedToolsFileDoesNotClaimEverythingRunsInATerminal(t *testing.T) {
134+ // The header said so before output existed, and left the file contradicting
135+ // itself two lines above the key that says otherwise.
136+ dir := t.TempDir()
137+ if _, err := tools.Create(Profile(), dir); err != nil {
138+ t.Fatalf("tools.Create() error = %v", err)
139+ }
140+
141+ contents := readFile(t, tools.Path(Profile(), dir))
142+ if strings.Contains(contents, "runs it in a terminal window of its own") {
143+ t.Errorf("the header still claims every tool runs in a terminal:\n%s", contents)
144+ }
145+}
146+
147+func TestTheCreatedToolsFileExplainsItself(t *testing.T) {
148+ dir := t.TempDir()
149+ if _, err := tools.Create(Profile(), dir); err != nil {
150+ t.Fatalf("tools.Create() error = %v", err)
151+ }
152+
153+ contents := readFile(t, tools.Path(Profile(), dir))
154+ for _, want := range []string{"[[tool]]", "sh -c", "hot key", "popup", "terminal", "editor", "./..."} {
155+ if !strings.Contains(contents, want) {
156+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
157+ }
158+ }
159+}
160+
161+func TestTheCreatedToolsFileNamesAnOutputForEveryTool(t *testing.T) {
162+ // The key is the interesting part of the format, and a file where it only
163+ // appears once is a file where nobody notices it exists.
164+ dir := t.TempDir()
165+ if _, err := tools.Create(Profile(), dir); err != nil {
166+ t.Fatalf("tools.Create() error = %v", err)
167+ }
168+
169+ for _, tool := range loadTools(t, dir).Tools() {
170+ if tool.Output == "" {
171+ t.Errorf("%q leaves its output to the default rather than saying it", tool.Name)
172+ }
173+ }
174+}
175+
176+func TestEachToolGoesWhereItsOwnOutputBelongs(t *testing.T) {
177+ // All three destinations appear, because a starter file is where somebody
178+ // finds out that the key has more than one value. Run and Echo are
179+ // terminals: a program that reads the keyboard has to be able to be
180+ // answered, and a popup cannot do that. Grep prints a list worth keeping
181+ // beside the code and searching with Ctrl-F, which is what an editing
182+ // window is for. The rest say something short and are read once.
183+ dir := t.TempDir()
184+ if _, err := tools.Create(Profile(), dir); err != nil {
185+ t.Fatalf("tools.Create() error = %v", err)
186+ }
187+
188+ want := map[string]tools.Output{
189+ "Format": tools.OutputPopup,
190+ "Lint": tools.OutputPopup,
191+ "Build": tools.OutputPopup,
192+ "Test": tools.OutputPopup,
193+ "Run": tools.OutputTerminal,
194+ "Grep": tools.OutputEditor,
195+ "Init module": tools.OutputPopup,
196+ "Echo": tools.OutputTerminal,
197+ }
198+ for _, tool := range loadTools(t, dir).Tools() {
199+ name := plain(tool.Name)
200+ if got := tool.Where(); got != want[name] {
201+ t.Errorf("%s goes to %q, want %q", name, got, want[name])
202+ }
203+ }
204+}
205+
206+func TestTheCreatedToolsFileShowsHowToUseAnotherMenu(t *testing.T) {
207+ dir := t.TempDir()
208+ if _, err := tools.Create(Profile(), dir); err != nil {
209+ t.Fatalf("tools.Create() error = %v", err)
210+ }
211+
212+ contents := readFile(t, tools.Path(Profile(), dir))
213+ for _, want := range []string{"menu says which menu", `menu = "Tools"`} {
214+ if !strings.Contains(contents, want) {
215+ t.Errorf("the created file never shows %q:\n%s", want, contents)
216+ }
217+ }
218+}
219+
220+func TestTheCreatedSnippetsFilesTabsSurviveTOML(t *testing.T) {
221+ // A Go snippet is indented with tabs, and every step between the template
222+ // and the editor is somewhere one can be lost: the multi-line TOML string,
223+ // the decoder, and the editor's own re-indentation on insertion. Asserting
224+ // on a tab the body is known to contain is what catches that.
225+ noUserSnippets(t)
226+ dir := t.TempDir()
227+ if _, err := snippets.Create(Profile(), dir); err != nil {
228+ t.Fatalf("snippets.Create() error = %v", err)
229+ }
230+
231+ for _, group := range loadSnippets(t, dir).Groups("go") {
232+ for _, snippet := range group.Snippets {
233+ if snippet.Name != "main" {
234+ continue
235+ }
236+ if !strings.Contains(snippet.Body, "\tfmt.Println") {
237+ t.Errorf("the body is %q; the tab did not survive", snippet.Body)
238+ }
239+ return
240+ }
241+ }
242+ t.Fatal("the created file has no \"main\" snippet")
243+}
244+
245+func TestTheCreatedSnippetsFileExplainsItself(t *testing.T) {
246+ noUserSnippets(t)
247+ dir := t.TempDir()
248+ if _, err := snippets.Create(Profile(), dir); err != nil {
249+ t.Fatalf("snippets.Create() error = %v", err)
250+ }
251+
252+ contents := readFile(t, snippets.ProjectPath(Profile(), dir))
253+ for _, want := range []string{"[[snippet]]", "languages", "group", "General", snippets.UserPath(Profile())} {
254+ if !strings.Contains(contents, want) {
255+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
256+ }
257+ }
258+}
259+
260+func TestTheCreatedSettingsFileExplainsItself(t *testing.T) {
261+ project := t.TempDir()
262+ if _, err := settings.Create(Profile(), project, "turbo-classic"); err != nil {
263+ t.Fatalf("settings.Create() error = %v", err)
264+ }
265+
266+ contents := readFile(t, settings.Path(Profile(), project))
267+ for _, want := range []string{"-list-themes", "autosave_delay", "-theme flag"} {
268+ if !strings.Contains(contents, want) {
269+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
270+ }
271+ }
272+}
273+
274+func TestTheCreatedToolsFileExplainsHowToAskForAValue(t *testing.T) {
275+ // A parameterised tool is only discoverable if the file people get says the
276+ // syntax exists. The double-brace warning is here too, because somebody
277+ // reading this file may well have an awk one-liner in mind.
278+ dir := t.TempDir()
279+ if _, err := tools.Create(Profile(), dir); err != nil {
280+ t.Fatalf("tools.Create() error = %v", err)
281+ }
282+
283+ contents := readFile(t, tools.Path(Profile(), dir))
284+ for _, want := range []string{
285+ "{{label}}",
286+ "go mod init {{module path}}",
287+ "{{extra flags...}}",
288+ "Double braces, not single",
289+ } {
290+ if !strings.Contains(contents, want) {
291+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
292+ }
293+ }
294+}
295+
296+func TestTheCreatedToolsFileStillLoadsWithItsPlaceholderExamples(t *testing.T) {
297+ // Two of the starter tools ask for a value, on purpose: a syntax explained
298+ // only in a comment is a syntax nobody tries. What they ask for is checked
299+ // here rather than left to the prose, because the braces also appear in the
300+ // file's *comments* — `{{label}}`, `{{extra flags...}}`, and an awk
301+ // one-liner warning against single ones — and a loader that read those as
302+ // tools would ask for something nobody wrote a command for.
303+ dir := t.TempDir()
304+ if _, err := tools.Create(Profile(), dir); err != nil {
305+ t.Fatalf("tools.Create() error = %v", err)
306+ }
307+
308+ want := map[string][]tools.Placeholder{
309+ "Grep": {{Label: "pattern"}},
310+ "Init module": {{Label: "module path"}},
311+ }
312+ for _, tool := range loadTools(t, dir).Tools() {
313+ name := plain(tool.Name)
314+ got := tool.Placeholders()
315+ if !slices.Equal(got, want[name]) {
316+ t.Errorf("%q asks for %v, want %v", name, got, want[name])
317+ }
318+ }
319+}
320+
321+func TestTheCreatedSnippetsFileListsEveryLanguageTheEditorKnows(t *testing.T) {
322+ // The comment is where a user finds out what they may write in a languages
323+ // key. One that omits a language the editor colours sends them looking for
324+ // a feature that is already there.
325+ noUserSnippets(t)
326+ dir := t.TempDir()
327+ if _, err := snippets.Create(Profile(), dir); err != nil {
328+ t.Fatalf("snippets.Create() error = %v", err)
329+ }
330+
331+ contents := readFile(t, snippets.ProjectPath(Profile(), dir))
332+ for _, language := range syntax.Registered() {
333+ if !strings.Contains(contents, string(language)) {
334+ t.Errorf("the created file never mentions the %q language:\n%s", language, contents)
335+ }
336+ }
337+}
338+
339+func TestTheCreatedSettingsFileTurnsAutosaveOn(t *testing.T) {
340+ // A project that has gone to the trouble of creating a settings file has
341+ // said what it wants. The file is the visible, editable place to say
342+ // otherwise, which is why the default lives here and not in the library.
343+ project := t.TempDir()
344+ if _, err := settings.Create(Profile(), project, "turbo-classic"); err != nil {
345+ t.Fatalf("settings.Create() error = %v", err)
346+ }
347+
348+ loaded, err := settings.Load(Profile(), project)
349+ if err != nil {
350+ t.Fatalf("settings.Load() error = %v", err)
351+ }
352+ if !loaded.Autosave {
353+ t.Errorf("the created settings file leaves autosave off:\n%s", readFile(t, settings.Path(Profile(), project)))
354+ }
355+ if loaded.AutosaveDelay != settings.DefaultAutosaveDelay {
356+ t.Errorf("AutosaveDelay = %v, want the library default %v", loaded.AutosaveDelay, settings.DefaultAutosaveDelay)
357+ }
358+}
359+
360+func TestAProjectWithNoSettingsFileStillDoesNotAutosave(t *testing.T) {
361+ // The other half of the decision. Turning autosave on for a project that
362+ // never opted in would mean the editor writing to disk in any directory it
363+ // is started in, which is a different and much larger claim.
364+ if settings.Default().Autosave {
365+ t.Error("settings.Default() autosaves; a project with no settings file never opted in")
366+ }
367+}
368+
369+// The three embedded templates and the blanks profile.Templates says each one
370+// takes. Kept together so that adding a verb to a .tmpl file without saying so
371+// here fails, which is the guard the constants used to get for free by sitting
372+// next to the contract.
373+var embeddedTemplates = []struct {
374+ name string
375+ body string
376+ verb string
377+ blanks int
378+ filledBy []any
379+}{
380+ {"settings.toml.tmpl", settingsTemplate, "%q", 2, []any{"turbo-classic", "2s"}},
381+ {"snippets.toml.tmpl", snippetsTemplate, "%s", 2, []any{"General", "/tmp/snippets.toml"}},
382+ {"tools.toml.tmpl", toolsTemplate, "%", 0, nil},
383+}
384+
385+func TestEveryTemplateIsEmbeddedAndNotEmpty(t *testing.T) {
386+ // go:embed fails to compile when a file is missing, but an empty file
387+ // compiles happily and writes an empty starter file into somebody's
388+ // project.
389+ for _, template := range embeddedTemplates {
390+ if len(template.body) == 0 {
391+ t.Errorf("%s embedded as nothing", template.name)
392+ }
393+ }
394+}
395+
396+func TestEveryTemplateTakesTheBlanksItsContractPromises(t *testing.T) {
397+ // profile.Templates documents the count and the verb of each. The
398+ // templates now live in files of their own, so nothing but this notices a
399+ // verb added, removed, or changed.
400+ for _, template := range embeddedTemplates {
401+ if got := strings.Count(template.body, template.verb); got != template.blanks {
402+ t.Errorf("%s holds %d %q, want %d", template.name, got, template.verb, template.blanks)
403+ }
404+ }
405+}
406+
407+func TestFillingATemplateLeavesNoFormattingMarker(t *testing.T) {
408+ // Go writes %!q(MISSING) or %!(EXTRA …) into the output rather than
409+ // failing, so a template with the wrong number of blanks produces a file
410+ // that is written, opened, and wrong.
411+ for _, template := range embeddedTemplates {
412+ filled := template.body
413+ if template.filledBy != nil {
414+ filled = fmt.Sprintf(template.body, template.filledBy...)
415+ }
416+ if strings.Contains(filled, "%!") {
417+ t.Errorf("%s filled to:\n%s", template.name, filled)
418+ }
419+ }
420+}
421+
422+func TestTheCreatedAgentsFileFillsBothOfItsBlanks(t *testing.T) {
423+ // The template takes two different values — the project directory, which
424+ // the example agent's arguments point into, and the user's own file, which
425+ // a comment names. Go writes %!s(MISSING) into the output rather than
426+ // failing, so a miscounted verb produces a starter file that is written,
427+ // opened, and wrong.
428+ dir := t.TempDir()
429+ if _, err := acp.Create(Profile(), dir); err != nil {
430+ t.Fatalf("acp.Create() error = %v", err)
431+ }
432+
433+ contents := readFile(t, acp.ProjectPath(Profile(), dir))
434+ if strings.Contains(contents, "%!") {
435+ t.Errorf("the created file has an unfilled verb in it:\n%s", contents)
436+ }
437+ if want := Profile().ProjectDir() + "/agent.yaml"; !strings.Contains(contents, want) {
438+ t.Errorf("the example agent does not point at %q:\n%s", want, contents)
439+ }
440+ if want := acp.UserPath(Profile()); want != "" && !strings.Contains(contents, want) {
441+ t.Errorf("the created file never names the user's own file %q:\n%s", want, contents)
442+ }
443+}
444+
445+func TestTheCreatedAgentsFileLoadsBackAsOneAgent(t *testing.T) {
446+ // The file is mostly comments, and a comment carrying a [[agent]] example
447+ // that the loader read as real would put an agent nobody configured into
448+ // the menu.
449+ dir := t.TempDir()
450+ if _, err := acp.Create(Profile(), dir); err != nil {
451+ t.Fatalf("acp.Create() error = %v", err)
452+ }
453+
454+ list, err := acp.Load(Profile(), dir)
455+ if err != nil {
456+ t.Fatalf("acp.Load() error = %v", err)
457+ }
458+ if list.Len() != 1 {
459+ t.Fatalf("the created file holds %d agents, want 1: %v", list.Len(), list.Agents())
460+ }
461+
462+ agent := list.Agents()[0]
463+ if agent.Command != "docker" {
464+ t.Errorf("the example agent runs %q, want docker", agent.Command)
465+ }
466+ if want := "agent serve acp"; !strings.Contains(agent.CommandLine(), want) {
467+ t.Errorf("the example command line is %q, want %q in it", agent.CommandLine(), want)
468+ }
469+}
470+
471+func TestTheCreatedAgentsFileExplainsItself(t *testing.T) {
472+ // The keys and the window's keyboard are both invisible otherwise: this is
473+ // the only document a user is handed by the editor itself.
474+ dir := t.TempDir()
475+ if _, err := acp.Create(Profile(), dir); err != nil {
476+ t.Fatalf("acp.Create() error = %v", err)
477+ }
478+
479+ contents := readFile(t, acp.ProjectPath(Profile(), dir))
480+ for _, want := range []string{
481+ "[[agent]]", "name", "command", "args", "env", "cwd",
482+ "agentclientprotocol.com",
483+ "Alt-Enter", "Ctrl-W", "Esc",
484+ } {
485+ if !strings.Contains(contents, want) {
486+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
487+ }
488+ }
489+}
490+
491+func TestCreatingAgentsTwiceLeavesTheFirstAlone(t *testing.T) {
492+ dir := t.TempDir()
493+ path, err := acp.Create(Profile(), dir)
494+ if err != nil {
495+ t.Fatalf("acp.Create() error = %v", err)
496+ }
497+ if err := os.WriteFile(path, []byte("# mine\n"), 0o644); err != nil {
498+ t.Fatalf("writing over it: %v", err)
499+ }
500+
501+ if _, err := acp.Create(Profile(), dir); !errors.Is(err, acp.ErrExists) {
502+ t.Errorf("acp.Create() error = %v, want ErrExists", err)
503+ }
504+ if got := readFile(t, path); got != "# mine\n" {
505+ t.Errorf("the file was overwritten: %q", got)
506+ }
507+}
new file mode 100644
@@ -0,0 +1,507 @@
1+package golang
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "slices"
8+ "strings"
9+ "testing"
10+
11+ "rickub.com/turbo-editors/turbo-core/acp"
12+ "rickub.com/turbo-editors/turbo-core/settings"
13+ "rickub.com/turbo-editors/turbo-core/snippets"
14+ "rickub.com/turbo-editors/turbo-core/syntax"
15+ "rickub.com/turbo-editors/turbo-core/tools"
16+)
17+
18+// The starter files Turbo Go writes are the one part of a project's .turbo-go
19+// directory that is about Go, so this is where what is *in* them is checked.
20+// That the file written is the profile's template at all is turbo-core's test.
21+
22+// noUserSnippets points the user's own snippets at an empty directory, so a
23+// test never reads whoever is running it.
24+func noUserSnippets(t *testing.T) {
25+ t.Helper()
26+ t.Setenv(Profile().SnippetDirEnvVar(), t.TempDir())
27+}
28+
29+// loadTools reads a project's tools, failing the test if it cannot.
30+func loadTools(t *testing.T, dir string) tools.List {
31+ t.Helper()
32+
33+ list, err := tools.Load(Profile(), dir)
34+ if err != nil {
35+ t.Fatalf("tools.Load(%q) error = %v", dir, err)
36+ }
37+ return list
38+}
39+
40+// loadSnippets reads a project's snippets, failing the test if it cannot.
41+func loadSnippets(t *testing.T, dir string) snippets.List {
42+ t.Helper()
43+
44+ list, err := snippets.Load(Profile(), dir)
45+ if err != nil {
46+ t.Fatalf("snippets.Load(%q) error = %v", dir, err)
47+ }
48+ return list
49+}
50+
51+// readFile returns a file's contents.
52+func readFile(t *testing.T, path string) string {
53+ t.Helper()
54+
55+ data, err := os.ReadFile(path)
56+ if err != nil {
57+ t.Fatalf("reading %s: %v", path, err)
58+ }
59+ return string(data)
60+}
61+
62+// plain strips the tilde hot-key markers from a label.
63+func plain(label string) string { return strings.ReplaceAll(label, "~", "") }
64+
65+// hotKey returns the character between the tildes, or 0 when there is none.
66+func hotKey(label string) rune {
67+ first := strings.IndexByte(label, '~')
68+ if first < 0 || first+1 >= len(label) {
69+ return 0
70+ }
71+ return rune(label[first+1])
72+}
73+
74+func TestTheCreatedToolsFileHoldsTheGoCommandsAndTheExamplesThatTeachTheFormat(t *testing.T) {
75+ // The first five are what a Go project runs before it commits, and they are
76+ // the reason the file exists at all. The three after them are there to
77+ // teach the format itself — a value the editor asks for, a menu of the
78+ // tool's own, an output that is not the default — and a starter file that
79+ // only listed the five would leave all three undiscoverable.
80+ dir := t.TempDir()
81+ if _, err := tools.Create(Profile(), dir); err != nil {
82+ t.Fatalf("tools.Create() error = %v", err)
83+ }
84+
85+ byName := map[string]string{}
86+ for _, tool := range loadTools(t, dir).Tools() {
87+ byName[plain(tool.Name)] = tool.Command
88+ }
89+
90+ want := map[string]string{
91+ "Format": "gofmt -l -w .",
92+ "Lint": "go vet ./...",
93+ "Build": "go build ./...",
94+ "Test": "go test ./...",
95+ "Run": "go run .",
96+ "Grep": "grep -rn {{pattern}} --include='*.go' .",
97+ "Init module": "go mod init {{module path}}",
98+ "Echo": "echo 🎉 tada!",
99+ }
100+ for name, command := range want {
101+ if got := byName[name]; got != command {
102+ t.Errorf("%s runs %q, want %q", name, got, command)
103+ }
104+ }
105+ for name := range byName {
106+ if _, ok := want[name]; !ok {
107+ t.Errorf("the created file holds a tool this test does not know about: %q", name)
108+ }
109+ }
110+}
111+
112+func TestTheCreatedToolsCarryHotKeys(t *testing.T) {
113+ // Five items in a menu are worth reaching with one keystroke each.
114+ dir := t.TempDir()
115+ if _, err := tools.Create(Profile(), dir); err != nil {
116+ t.Fatalf("tools.Create() error = %v", err)
117+ }
118+
119+ seen := map[rune]string{}
120+ for _, tool := range loadTools(t, dir).Tools() {
121+ key := hotKey(tool.Name)
122+ if key == 0 {
123+ t.Errorf("%q has no hot key", tool.Name)
124+ continue
125+ }
126+ if other, clash := seen[key]; clash {
127+ t.Errorf("%q and %q both answer to %c", other, tool.Name, key)
128+ }
129+ seen[key] = tool.Name
130+ }
131+}
132+
133+func TestTheCreatedToolsFileDoesNotClaimEverythingRunsInATerminal(t *testing.T) {
134+ // The header said so before output existed, and left the file contradicting
135+ // itself two lines above the key that says otherwise.
136+ dir := t.TempDir()
137+ if _, err := tools.Create(Profile(), dir); err != nil {
138+ t.Fatalf("tools.Create() error = %v", err)
139+ }
140+
141+ contents := readFile(t, tools.Path(Profile(), dir))
142+ if strings.Contains(contents, "runs it in a terminal window of its own") {
143+ t.Errorf("the header still claims every tool runs in a terminal:\n%s", contents)
144+ }
145+}
146+
147+func TestTheCreatedToolsFileExplainsItself(t *testing.T) {
148+ dir := t.TempDir()
149+ if _, err := tools.Create(Profile(), dir); err != nil {
150+ t.Fatalf("tools.Create() error = %v", err)
151+ }
152+
153+ contents := readFile(t, tools.Path(Profile(), dir))
154+ for _, want := range []string{"[[tool]]", "sh -c", "hot key", "popup", "terminal", "editor", "./..."} {
155+ if !strings.Contains(contents, want) {
156+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
157+ }
158+ }
159+}
160+
161+func TestTheCreatedToolsFileNamesAnOutputForEveryTool(t *testing.T) {
162+ // The key is the interesting part of the format, and a file where it only
163+ // appears once is a file where nobody notices it exists.
164+ dir := t.TempDir()
165+ if _, err := tools.Create(Profile(), dir); err != nil {
166+ t.Fatalf("tools.Create() error = %v", err)
167+ }
168+
169+ for _, tool := range loadTools(t, dir).Tools() {
170+ if tool.Output == "" {
171+ t.Errorf("%q leaves its output to the default rather than saying it", tool.Name)
172+ }
173+ }
174+}
175+
176+func TestEachToolGoesWhereItsOwnOutputBelongs(t *testing.T) {
177+ // All three destinations appear, because a starter file is where somebody
178+ // finds out that the key has more than one value. Run and Echo are
179+ // terminals: a program that reads the keyboard has to be able to be
180+ // answered, and a popup cannot do that. Grep prints a list worth keeping
181+ // beside the code and searching with Ctrl-F, which is what an editing
182+ // window is for. The rest say something short and are read once.
183+ dir := t.TempDir()
184+ if _, err := tools.Create(Profile(), dir); err != nil {
185+ t.Fatalf("tools.Create() error = %v", err)
186+ }
187+
188+ want := map[string]tools.Output{
189+ "Format": tools.OutputPopup,
190+ "Lint": tools.OutputPopup,
191+ "Build": tools.OutputPopup,
192+ "Test": tools.OutputPopup,
193+ "Run": tools.OutputTerminal,
194+ "Grep": tools.OutputEditor,
195+ "Init module": tools.OutputPopup,
196+ "Echo": tools.OutputTerminal,
197+ }
198+ for _, tool := range loadTools(t, dir).Tools() {
199+ name := plain(tool.Name)
200+ if got := tool.Where(); got != want[name] {
201+ t.Errorf("%s goes to %q, want %q", name, got, want[name])
202+ }
203+ }
204+}
205+
206+func TestTheCreatedToolsFileShowsHowToUseAnotherMenu(t *testing.T) {
207+ dir := t.TempDir()
208+ if _, err := tools.Create(Profile(), dir); err != nil {
209+ t.Fatalf("tools.Create() error = %v", err)
210+ }
211+
212+ contents := readFile(t, tools.Path(Profile(), dir))
213+ for _, want := range []string{"menu says which menu", `menu = "Tools"`} {
214+ if !strings.Contains(contents, want) {
215+ t.Errorf("the created file never shows %q:\n%s", want, contents)
216+ }
217+ }
218+}
219+
220+func TestTheCreatedSnippetsFilesTabsSurviveTOML(t *testing.T) {
221+ // A Go snippet is indented with tabs, and every step between the template
222+ // and the editor is somewhere one can be lost: the multi-line TOML string,
223+ // the decoder, and the editor's own re-indentation on insertion. Asserting
224+ // on a tab the body is known to contain is what catches that.
225+ noUserSnippets(t)
226+ dir := t.TempDir()
227+ if _, err := snippets.Create(Profile(), dir); err != nil {
228+ t.Fatalf("snippets.Create() error = %v", err)
229+ }
230+
231+ for _, group := range loadSnippets(t, dir).Groups("go") {
232+ for _, snippet := range group.Snippets {
233+ if snippet.Name != "main" {
234+ continue
235+ }
236+ if !strings.Contains(snippet.Body, "\tfmt.Println") {
237+ t.Errorf("the body is %q; the tab did not survive", snippet.Body)
238+ }
239+ return
240+ }
241+ }
242+ t.Fatal("the created file has no \"main\" snippet")
243+}
244+
245+func TestTheCreatedSnippetsFileExplainsItself(t *testing.T) {
246+ noUserSnippets(t)
247+ dir := t.TempDir()
248+ if _, err := snippets.Create(Profile(), dir); err != nil {
249+ t.Fatalf("snippets.Create() error = %v", err)
250+ }
251+
252+ contents := readFile(t, snippets.ProjectPath(Profile(), dir))
253+ for _, want := range []string{"[[snippet]]", "languages", "group", "General", snippets.UserPath(Profile())} {
254+ if !strings.Contains(contents, want) {
255+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
256+ }
257+ }
258+}
259+
260+func TestTheCreatedSettingsFileExplainsItself(t *testing.T) {
261+ project := t.TempDir()
262+ if _, err := settings.Create(Profile(), project, "turbo-classic"); err != nil {
263+ t.Fatalf("settings.Create() error = %v", err)
264+ }
265+
266+ contents := readFile(t, settings.Path(Profile(), project))
267+ for _, want := range []string{"-list-themes", "autosave_delay", "-theme flag"} {
268+ if !strings.Contains(contents, want) {
269+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
270+ }
271+ }
272+}
273+
274+func TestTheCreatedToolsFileExplainsHowToAskForAValue(t *testing.T) {
275+ // A parameterised tool is only discoverable if the file people get says the
276+ // syntax exists. The double-brace warning is here too, because somebody
277+ // reading this file may well have an awk one-liner in mind.
278+ dir := t.TempDir()
279+ if _, err := tools.Create(Profile(), dir); err != nil {
280+ t.Fatalf("tools.Create() error = %v", err)
281+ }
282+
283+ contents := readFile(t, tools.Path(Profile(), dir))
284+ for _, want := range []string{
285+ "{{label}}",
286+ "go mod init {{module path}}",
287+ "{{extra flags...}}",
288+ "Double braces, not single",
289+ } {
290+ if !strings.Contains(contents, want) {
291+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
292+ }
293+ }
294+}
295+
296+func TestTheCreatedToolsFileStillLoadsWithItsPlaceholderExamples(t *testing.T) {
297+ // Two of the starter tools ask for a value, on purpose: a syntax explained
298+ // only in a comment is a syntax nobody tries. What they ask for is checked
299+ // here rather than left to the prose, because the braces also appear in the
300+ // file's *comments* — `{{label}}`, `{{extra flags...}}`, and an awk
301+ // one-liner warning against single ones — and a loader that read those as
302+ // tools would ask for something nobody wrote a command for.
303+ dir := t.TempDir()
304+ if _, err := tools.Create(Profile(), dir); err != nil {
305+ t.Fatalf("tools.Create() error = %v", err)
306+ }
307+
308+ want := map[string][]tools.Placeholder{
309+ "Grep": {{Label: "pattern"}},
310+ "Init module": {{Label: "module path"}},
311+ }
312+ for _, tool := range loadTools(t, dir).Tools() {
313+ name := plain(tool.Name)
314+ got := tool.Placeholders()
315+ if !slices.Equal(got, want[name]) {
316+ t.Errorf("%q asks for %v, want %v", name, got, want[name])
317+ }
318+ }
319+}
320+
321+func TestTheCreatedSnippetsFileListsEveryLanguageTheEditorKnows(t *testing.T) {
322+ // The comment is where a user finds out what they may write in a languages
323+ // key. One that omits a language the editor colours sends them looking for
324+ // a feature that is already there.
325+ noUserSnippets(t)
326+ dir := t.TempDir()
327+ if _, err := snippets.Create(Profile(), dir); err != nil {
328+ t.Fatalf("snippets.Create() error = %v", err)
329+ }
330+
331+ contents := readFile(t, snippets.ProjectPath(Profile(), dir))
332+ for _, language := range syntax.Registered() {
333+ if !strings.Contains(contents, string(language)) {
334+ t.Errorf("the created file never mentions the %q language:\n%s", language, contents)
335+ }
336+ }
337+}
338+
339+func TestTheCreatedSettingsFileTurnsAutosaveOn(t *testing.T) {
340+ // A project that has gone to the trouble of creating a settings file has
341+ // said what it wants. The file is the visible, editable place to say
342+ // otherwise, which is why the default lives here and not in the library.
343+ project := t.TempDir()
344+ if _, err := settings.Create(Profile(), project, "turbo-classic"); err != nil {
345+ t.Fatalf("settings.Create() error = %v", err)
346+ }
347+
348+ loaded, err := settings.Load(Profile(), project)
349+ if err != nil {
350+ t.Fatalf("settings.Load() error = %v", err)
351+ }
352+ if !loaded.Autosave {
353+ t.Errorf("the created settings file leaves autosave off:\n%s", readFile(t, settings.Path(Profile(), project)))
354+ }
355+ if loaded.AutosaveDelay != settings.DefaultAutosaveDelay {
356+ t.Errorf("AutosaveDelay = %v, want the library default %v", loaded.AutosaveDelay, settings.DefaultAutosaveDelay)
357+ }
358+}
359+
360+func TestAProjectWithNoSettingsFileStillDoesNotAutosave(t *testing.T) {
361+ // The other half of the decision. Turning autosave on for a project that
362+ // never opted in would mean the editor writing to disk in any directory it
363+ // is started in, which is a different and much larger claim.
364+ if settings.Default().Autosave {
365+ t.Error("settings.Default() autosaves; a project with no settings file never opted in")
366+ }
367+}
368+
369+// The three embedded templates and the blanks profile.Templates says each one
370+// takes. Kept together so that adding a verb to a .tmpl file without saying so
371+// here fails, which is the guard the constants used to get for free by sitting
372+// next to the contract.
373+var embeddedTemplates = []struct {
374+ name string
375+ body string
376+ verb string
377+ blanks int
378+ filledBy []any
379+}{
380+ {"settings.toml.tmpl", settingsTemplate, "%q", 2, []any{"turbo-classic", "2s"}},
381+ {"snippets.toml.tmpl", snippetsTemplate, "%s", 2, []any{"General", "/tmp/snippets.toml"}},
382+ {"tools.toml.tmpl", toolsTemplate, "%", 0, nil},
383+}
384+
385+func TestEveryTemplateIsEmbeddedAndNotEmpty(t *testing.T) {
386+ // go:embed fails to compile when a file is missing, but an empty file
387+ // compiles happily and writes an empty starter file into somebody's
388+ // project.
389+ for _, template := range embeddedTemplates {
390+ if len(template.body) == 0 {
391+ t.Errorf("%s embedded as nothing", template.name)
392+ }
393+ }
394+}
395+
396+func TestEveryTemplateTakesTheBlanksItsContractPromises(t *testing.T) {
397+ // profile.Templates documents the count and the verb of each. The
398+ // templates now live in files of their own, so nothing but this notices a
399+ // verb added, removed, or changed.
400+ for _, template := range embeddedTemplates {
401+ if got := strings.Count(template.body, template.verb); got != template.blanks {
402+ t.Errorf("%s holds %d %q, want %d", template.name, got, template.verb, template.blanks)
403+ }
404+ }
405+}
406+
407+func TestFillingATemplateLeavesNoFormattingMarker(t *testing.T) {
408+ // Go writes %!q(MISSING) or %!(EXTRA …) into the output rather than
409+ // failing, so a template with the wrong number of blanks produces a file
410+ // that is written, opened, and wrong.
411+ for _, template := range embeddedTemplates {
412+ filled := template.body
413+ if template.filledBy != nil {
414+ filled = fmt.Sprintf(template.body, template.filledBy...)
415+ }
416+ if strings.Contains(filled, "%!") {
417+ t.Errorf("%s filled to:\n%s", template.name, filled)
418+ }
419+ }
420+}
421+
422+func TestTheCreatedAgentsFileFillsBothOfItsBlanks(t *testing.T) {
423+ // The template takes two different values — the project directory, which
424+ // the example agent's arguments point into, and the user's own file, which
425+ // a comment names. Go writes %!s(MISSING) into the output rather than
426+ // failing, so a miscounted verb produces a starter file that is written,
427+ // opened, and wrong.
428+ dir := t.TempDir()
429+ if _, err := acp.Create(Profile(), dir); err != nil {
430+ t.Fatalf("acp.Create() error = %v", err)
431+ }
432+
433+ contents := readFile(t, acp.ProjectPath(Profile(), dir))
434+ if strings.Contains(contents, "%!") {
435+ t.Errorf("the created file has an unfilled verb in it:\n%s", contents)
436+ }
437+ if want := Profile().ProjectDir() + "/agent.yaml"; !strings.Contains(contents, want) {
438+ t.Errorf("the example agent does not point at %q:\n%s", want, contents)
439+ }
440+ if want := acp.UserPath(Profile()); want != "" && !strings.Contains(contents, want) {
441+ t.Errorf("the created file never names the user's own file %q:\n%s", want, contents)
442+ }
443+}
444+
445+func TestTheCreatedAgentsFileLoadsBackAsOneAgent(t *testing.T) {
446+ // The file is mostly comments, and a comment carrying a [[agent]] example
447+ // that the loader read as real would put an agent nobody configured into
448+ // the menu.
449+ dir := t.TempDir()
450+ if _, err := acp.Create(Profile(), dir); err != nil {
451+ t.Fatalf("acp.Create() error = %v", err)
452+ }
453+
454+ list, err := acp.Load(Profile(), dir)
455+ if err != nil {
456+ t.Fatalf("acp.Load() error = %v", err)
457+ }
458+ if list.Len() != 1 {
459+ t.Fatalf("the created file holds %d agents, want 1: %v", list.Len(), list.Agents())
460+ }
461+
462+ agent := list.Agents()[0]
463+ if agent.Command != "docker" {
464+ t.Errorf("the example agent runs %q, want docker", agent.Command)
465+ }
466+ if want := "agent serve acp"; !strings.Contains(agent.CommandLine(), want) {
467+ t.Errorf("the example command line is %q, want %q in it", agent.CommandLine(), want)
468+ }
469+}
470+
471+func TestTheCreatedAgentsFileExplainsItself(t *testing.T) {
472+ // The keys and the window's keyboard are both invisible otherwise: this is
473+ // the only document a user is handed by the editor itself.
474+ dir := t.TempDir()
475+ if _, err := acp.Create(Profile(), dir); err != nil {
476+ t.Fatalf("acp.Create() error = %v", err)
477+ }
478+
479+ contents := readFile(t, acp.ProjectPath(Profile(), dir))
480+ for _, want := range []string{
481+ "[[agent]]", "name", "command", "args", "env", "cwd",
482+ "agentclientprotocol.com",
483+ "Alt-Enter", "Ctrl-W", "Esc",
484+ } {
485+ if !strings.Contains(contents, want) {
486+ t.Errorf("the created file never mentions %q:\n%s", want, contents)
487+ }
488+ }
489+}
490+
491+func TestCreatingAgentsTwiceLeavesTheFirstAlone(t *testing.T) {
492+ dir := t.TempDir()
493+ path, err := acp.Create(Profile(), dir)
494+ if err != nil {
495+ t.Fatalf("acp.Create() error = %v", err)
496+ }
497+ if err := os.WriteFile(path, []byte("# mine\n"), 0o644); err != nil {
498+ t.Fatalf("writing over it: %v", err)
499+ }
500+
501+ if _, err := acp.Create(Profile(), dir); !errors.Is(err, acp.ErrExists) {
502+ t.Errorf("acp.Create() error = %v, want ErrExists", err)
503+ }
504+ if got := readFile(t, path); got != "# mine\n" {
505+ t.Errorf("the file was overwritten: %q", got)
506+ }
507+}
added internal/golang/tools.toml.tmpl +94 -0
new file mode 100644
@@ -0,0 +1,94 @@
1+# turbo-go tools.
2+#
3+# Each [[tool]] becomes one line of the Go menu, in the order they appear here.
4+# name is what the menu shows; a letter between tildes is its hot key, and no
5+# two tools should claim the same one.
6+#
7+# command goes to "sh -c", so pipes, globs and && work: one entry can be a
8+# whole sequence.
9+#
10+# menu says which menu it appears in. Leave it out and the tool goes into the Go
11+# menu; name anything else and that menu is created for you, in the order the
12+# names first appear here. A tool that has nothing to do with Go belongs in one
13+# of your own:
14+#
15+# [[tool]]
16+# name = "~E~cho"
17+# command = "echo TADA"
18+# menu = "Tools"
19+#
20+# A {{label}} in a command is a value the editor asks for before running it, in
21+# a box titled after the tool. The text between the braces is what it asks for:
22+#
23+# [[tool]]
24+# name = "~I~nit module"
25+# command = "go mod init {{module path}}"
26+#
27+# The value is quoted, so a path with a space in it stays one argument. Add ...
28+# inside the braces when you mean several arguments rather than one value:
29+#
30+# [[tool]]
31+# name = "Test ~o~ne"
32+# command = "go test {{extra flags...}} ./..."
33+#
34+# Double braces, not single. Single ones appear in real commands — awk '{print
35+# $1}' and find . -exec rm {} + are both ordinary things to put here — and
36+# neither is asking you for anything.
37+#
38+# output says where what the command prints goes:
39+# popup a dialog that fills in as it runs, and says the exit code (default)
40+# terminal a terminal window, for anything that reads the keyboard or runs long
41+# editor an editing window once it has finished, to search with Ctrl-F
42+#
43+# Commands run in the directory the editor was started in, which is why ./...
44+# means the whole module when you start from the project root.
45+
46+[[tool]]
47+name = "~F~ormat"
48+command = "gofmt -l -w ."
49+output = "popup"
50+
51+[[tool]]
52+name = "~L~int"
53+command = "go vet ./..."
54+output = "popup"
55+
56+[[tool]]
57+name = "~B~uild"
58+command = "go build ./..."
59+output = "popup"
60+
61+[[tool]]
62+name = "~T~est"
63+command = "go test ./..."
64+output = "popup"
65+
66+[[tool]]
67+name = "~R~un"
68+command = "go run ."
69+# A terminal, not a popup: a program that reads the keyboard has to be able to
70+# be answered, and one that runs long has to be able to be interrupted.
71+output = "terminal"
72+
73+# A tool that asks for a value: {{double braces}} opens a box before it runs,
74+# and what you type is shell-quoted, so a name with a space stays one argument.
75+
76+[[tool]]
77+name = "~G~rep"
78+command = "grep -rn {{pattern}} --include='*.go' ."
79+output = "editor"
80+
81+[[tool]]
82+name = "~I~nit module"
83+command = "go mod init {{module path}}"
84+output = "popup"
85+
86+# A tool naming a `menu` gets a menu of its own on the bar. Nothing here does,
87+# so every tool below is in the Go menu. Add `menu = "Doc~k~er"` to one and a
88+# Docker menu appears between Go and Help — that is the whole mechanism.
89+
90+[[tool]]
91+name = "~E~cho"
92+command = "echo 🎉 tada!"
93+menu = "Tools"
94+output = "terminal"
new file mode 100644
@@ -0,0 +1,94 @@
1+# turbo-go tools.
2+#
3+# Each [[tool]] becomes one line of the Go menu, in the order they appear here.
4+# name is what the menu shows; a letter between tildes is its hot key, and no
5+# two tools should claim the same one.
6+#
7+# command goes to "sh -c", so pipes, globs and && work: one entry can be a
8+# whole sequence.
9+#
10+# menu says which menu it appears in. Leave it out and the tool goes into the Go
11+# menu; name anything else and that menu is created for you, in the order the
12+# names first appear here. A tool that has nothing to do with Go belongs in one
13+# of your own:
14+#
15+# [[tool]]
16+# name = "~E~cho"
17+# command = "echo TADA"
18+# menu = "Tools"
19+#
20+# A {{label}} in a command is a value the editor asks for before running it, in
21+# a box titled after the tool. The text between the braces is what it asks for:
22+#
23+# [[tool]]
24+# name = "~I~nit module"
25+# command = "go mod init {{module path}}"
26+#
27+# The value is quoted, so a path with a space in it stays one argument. Add ...
28+# inside the braces when you mean several arguments rather than one value:
29+#
30+# [[tool]]
31+# name = "Test ~o~ne"
32+# command = "go test {{extra flags...}} ./..."
33+#
34+# Double braces, not single. Single ones appear in real commands — awk '{print
35+# $1}' and find . -exec rm {} + are both ordinary things to put here — and
36+# neither is asking you for anything.
37+#
38+# output says where what the command prints goes:
39+# popup a dialog that fills in as it runs, and says the exit code (default)
40+# terminal a terminal window, for anything that reads the keyboard or runs long
41+# editor an editing window once it has finished, to search with Ctrl-F
42+#
43+# Commands run in the directory the editor was started in, which is why ./...
44+# means the whole module when you start from the project root.
45+
46+[[tool]]
47+name = "~F~ormat"
48+command = "gofmt -l -w ."
49+output = "popup"
50+
51+[[tool]]
52+name = "~L~int"
53+command = "go vet ./..."
54+output = "popup"
55+
56+[[tool]]
57+name = "~B~uild"
58+command = "go build ./..."
59+output = "popup"
60+
61+[[tool]]
62+name = "~T~est"
63+command = "go test ./..."
64+output = "popup"
65+
66+[[tool]]
67+name = "~R~un"
68+command = "go run ."
69+# A terminal, not a popup: a program that reads the keyboard has to be able to
70+# be answered, and one that runs long has to be able to be interrupted.
71+output = "terminal"
72+
73+# A tool that asks for a value: {{double braces}} opens a box before it runs,
74+# and what you type is shell-quoted, so a name with a space stays one argument.
75+
76+[[tool]]
77+name = "~G~rep"
78+command = "grep -rn {{pattern}} --include='*.go' ."
79+output = "editor"
80+
81+[[tool]]
82+name = "~I~nit module"
83+command = "go mod init {{module path}}"
84+output = "popup"
85+
86+# A tool naming a `menu` gets a menu of its own on the bar. Nothing here does,
87+# so every tool below is in the Go menu. Add `menu = "Doc~k~er"` to one and a
88+# Docker menu appears between Go and Help — that is the whole mechanism.
89+
90+[[tool]]
91+name = "~E~cho"
92+command = "echo 🎉 tada!"
93+menu = "Tools"
94+output = "terminal"
added main.go +203 -0
new file mode 100644
@@ -0,0 +1,203 @@
1+// Command turbo-go is a Turbo C-style editor for Go: a full-screen terminal
2+// IDE with menus, movable windows, syntax colouring and gopls completion.
3+//
4+// Almost all of it is turbo-core, the library every Turbo editor is built on.
5+// What is here is the command line, the terminal, and internal/golang — the
6+// profile that says this one is for Go.
7+//
8+// Usage:
9+//
10+// turbo-go [flags] [file...]
11+//
12+// Flags:
13+//
14+// -theme name the colour theme to start with, overriding the project's
15+// -list-themes print the available themes and exit
16+// -no-lsp do not start a language server
17+// -version print the version and exit
18+package main
19+
20+import (
21+ "context"
22+ "errors"
23+ "flag"
24+ "fmt"
25+ "os"
26+
27+ "github.com/gdamore/tcell/v2"
28+
29+ "rickub.com/turbo-editors/turbo-core/app"
30+ "rickub.com/turbo-editors/turbo-core/profile"
31+ "rickub.com/turbo-editors/turbo-core/settings"
32+ "rickub.com/turbo-editors/turbo-core/theme"
33+ "rickub.com/turbo-editors/turbo-core/version"
34+
35+ "rickub.com/turbo-editors/turbo-go/internal/golang"
36+)
37+
38+func main() {
39+ if err := run(); err != nil {
40+ fmt.Fprintf(os.Stderr, "%s: %v\n", golang.Slug, err)
41+ os.Exit(1)
42+ }
43+}
44+
45+// options are what the command line asked for.
46+type options struct {
47+ theme string
48+ listThemes bool
49+ noLSP bool
50+ version bool
51+ files []string
52+}
53+
54+// parseFlags reads the command line.
55+func parseFlags() options {
56+ var opts options
57+
58+ // The default is empty rather than the theme's name so that "was -theme
59+ // given?" can still be answered afterwards, which is what lets the project
60+ // settings fill it in without overriding an explicit choice.
61+ flag.StringVar(&opts.theme, "theme", "", "colour theme to start with (default from the project, else "+theme.DefaultName+")")
62+ flag.BoolVar(&opts.listThemes, "list-themes", false, "print the available themes and exit")
63+ flag.BoolVar(&opts.noLSP, "no-lsp", false, "do not start a language server")
64+ flag.BoolVar(&opts.version, "version", false, "print the version and exit")
65+ flag.Parse()
66+
67+ opts.files = flag.Args()
68+ return opts
69+}
70+
71+// run does the work, so that main is nothing but error reporting.
72+func run() error {
73+ opts := parseFlags()
74+ // Registering here rather than from an init function is what makes "this
75+ // editor knows Go" a line somebody can read.
76+ golang.Register()
77+ p := golang.Profile()
78+
79+ switch {
80+ case opts.version:
81+ fmt.Printf("%s %s\n", p.Name, version.Current())
82+ return nil
83+ case opts.listThemes:
84+ return listThemes(p)
85+ }
86+
87+ return edit(opts, p)
88+}
89+
90+// listThemes prints every theme that can be loaded, with its description.
91+func listThemes(p profile.Profile) error {
92+ userDir := p.ThemeDir()
93+
94+ for _, name := range theme.Available(userDir) {
95+ loaded, err := theme.Load(name, userDir)
96+ if err != nil {
97+ fmt.Printf("%-16s (cannot be loaded: %v)\n", name, err)
98+ continue
99+ }
100+ fmt.Printf("%-16s %s\n", name, loaded.Description())
101+ }
102+
103+ if userDir != "" {
104+ fmt.Printf("\nYour own themes go in %s\n", userDir)
105+ }
106+ return nil
107+}
108+
109+// edit opens the terminal and runs the editor until the user leaves.
110+func edit(opts options, p profile.Profile) error {
111+ project, projectSettings := loadProjectSettings(p)
112+
113+ screen, err := newScreen()
114+ if err != nil {
115+ return err
116+ }
117+ // The screen must be given back whatever happens, or a crash leaves the
118+ // terminal in raw mode with no cursor.
119+ defer screen.Fini()
120+
121+ editor := app.New(screen, themeName(opts, projectSettings), p)
122+ if settings.Exists(p, project) {
123+ editor.UseSettings(projectSettings, settings.Path(p, project))
124+ }
125+ openFiles(editor, opts.files)
126+
127+ ctx, cancel := context.WithCancel(context.Background())
128+ defer cancel()
129+ if !opts.noLSP {
130+ editor.StartLanguageServer(ctx, app.ProjectRoot(p, opts.files))
131+ }
132+ defer editor.Language().Stop(context.Background())
133+
134+ return editor.Run()
135+}
136+
137+// loadProjectSettings reads .turbo-go/settings.toml from the working
138+// directory, and returns that directory along with what it found.
139+//
140+// The working directory alone is looked in, with no walk up towards the root:
141+// "the project" is where you started the editor, which is a rule you can hold
142+// in your head. A file that is there but unreadable is reported on standard
143+// error and then ignored — a broken settings file must not stop the editor
144+// opening, because the editor is how you would fix it.
145+func loadProjectSettings(p profile.Profile) (string, settings.Settings) {
146+ project, err := os.Getwd()
147+ if err != nil {
148+ project = "."
149+ }
150+
151+ loaded, err := settings.Load(p, project)
152+ switch {
153+ case errors.Is(err, settings.ErrNotFound):
154+ return project, settings.Default()
155+ case err != nil:
156+ fmt.Fprintf(os.Stderr, "%s: %v\n", golang.Slug, err)
157+ return project, settings.Default()
158+ }
159+ return project, loaded
160+}
161+
162+// themeName decides which theme to start in.
163+//
164+// A -theme flag wins, because it is the more explicit statement of the two and
165+// is how you try a theme without editing a file everyone shares. The project's
166+// settings come next, and the built-in default last.
167+func themeName(opts options, projectSettings settings.Settings) string {
168+ switch {
169+ case opts.theme != "":
170+ return opts.theme
171+ case projectSettings.Theme != "":
172+ return projectSettings.Theme
173+ default:
174+ return theme.DefaultName
175+ }
176+}
177+
178+// newScreen opens the terminal and turns on what the editor needs from it.
179+func newScreen() (tcell.Screen, error) {
180+ screen, err := tcell.NewScreen()
181+ if err != nil {
182+ return nil, fmt.Errorf("opening the terminal: %w", err)
183+ }
184+ if err := screen.Init(); err != nil {
185+ return nil, fmt.Errorf("initialising the terminal: %w", err)
186+ }
187+
188+ screen.EnableMouse()
189+ screen.EnablePaste()
190+ return screen, nil
191+}
192+
193+// openFiles opens the files named on the command line, or an empty window when
194+// none were.
195+func openFiles(editor *app.App, files []string) {
196+ if len(files) == 0 {
197+ editor.NewFile()
198+ return
199+ }
200+ for _, file := range files {
201+ editor.Open(file)
202+ }
203+}
new file mode 100644
@@ -0,0 +1,203 @@
1+// Command turbo-go is a Turbo C-style editor for Go: a full-screen terminal
2+// IDE with menus, movable windows, syntax colouring and gopls completion.
3+//
4+// Almost all of it is turbo-core, the library every Turbo editor is built on.
5+// What is here is the command line, the terminal, and internal/golang — the
6+// profile that says this one is for Go.
7+//
8+// Usage:
9+//
10+// turbo-go [flags] [file...]
11+//
12+// Flags:
13+//
14+// -theme name the colour theme to start with, overriding the project's
15+// -list-themes print the available themes and exit
16+// -no-lsp do not start a language server
17+// -version print the version and exit
18+package main
19+
20+import (
21+ "context"
22+ "errors"
23+ "flag"
24+ "fmt"
25+ "os"
26+
27+ "github.com/gdamore/tcell/v2"
28+
29+ "rickub.com/turbo-editors/turbo-core/app"
30+ "rickub.com/turbo-editors/turbo-core/profile"
31+ "rickub.com/turbo-editors/turbo-core/settings"
32+ "rickub.com/turbo-editors/turbo-core/theme"
33+ "rickub.com/turbo-editors/turbo-core/version"
34+
35+ "rickub.com/turbo-editors/turbo-go/internal/golang"
36+)
37+
38+func main() {
39+ if err := run(); err != nil {
40+ fmt.Fprintf(os.Stderr, "%s: %v\n", golang.Slug, err)
41+ os.Exit(1)
42+ }
43+}
44+
45+// options are what the command line asked for.
46+type options struct {
47+ theme string
48+ listThemes bool
49+ noLSP bool
50+ version bool
51+ files []string
52+}
53+
54+// parseFlags reads the command line.
55+func parseFlags() options {
56+ var opts options
57+
58+ // The default is empty rather than the theme's name so that "was -theme
59+ // given?" can still be answered afterwards, which is what lets the project
60+ // settings fill it in without overriding an explicit choice.
61+ flag.StringVar(&opts.theme, "theme", "", "colour theme to start with (default from the project, else "+theme.DefaultName+")")
62+ flag.BoolVar(&opts.listThemes, "list-themes", false, "print the available themes and exit")
63+ flag.BoolVar(&opts.noLSP, "no-lsp", false, "do not start a language server")
64+ flag.BoolVar(&opts.version, "version", false, "print the version and exit")
65+ flag.Parse()
66+
67+ opts.files = flag.Args()
68+ return opts
69+}
70+
71+// run does the work, so that main is nothing but error reporting.
72+func run() error {
73+ opts := parseFlags()
74+ // Registering here rather than from an init function is what makes "this
75+ // editor knows Go" a line somebody can read.
76+ golang.Register()
77+ p := golang.Profile()
78+
79+ switch {
80+ case opts.version:
81+ fmt.Printf("%s %s\n", p.Name, version.Current())
82+ return nil
83+ case opts.listThemes:
84+ return listThemes(p)
85+ }
86+
87+ return edit(opts, p)
88+}
89+
90+// listThemes prints every theme that can be loaded, with its description.
91+func listThemes(p profile.Profile) error {
92+ userDir := p.ThemeDir()
93+
94+ for _, name := range theme.Available(userDir) {
95+ loaded, err := theme.Load(name, userDir)
96+ if err != nil {
97+ fmt.Printf("%-16s (cannot be loaded: %v)\n", name, err)
98+ continue
99+ }
100+ fmt.Printf("%-16s %s\n", name, loaded.Description())
101+ }
102+
103+ if userDir != "" {
104+ fmt.Printf("\nYour own themes go in %s\n", userDir)
105+ }
106+ return nil
107+}
108+
109+// edit opens the terminal and runs the editor until the user leaves.
110+func edit(opts options, p profile.Profile) error {
111+ project, projectSettings := loadProjectSettings(p)
112+
113+ screen, err := newScreen()
114+ if err != nil {
115+ return err
116+ }
117+ // The screen must be given back whatever happens, or a crash leaves the
118+ // terminal in raw mode with no cursor.
119+ defer screen.Fini()
120+
121+ editor := app.New(screen, themeName(opts, projectSettings), p)
122+ if settings.Exists(p, project) {
123+ editor.UseSettings(projectSettings, settings.Path(p, project))
124+ }
125+ openFiles(editor, opts.files)
126+
127+ ctx, cancel := context.WithCancel(context.Background())
128+ defer cancel()
129+ if !opts.noLSP {
130+ editor.StartLanguageServer(ctx, app.ProjectRoot(p, opts.files))
131+ }
132+ defer editor.Language().Stop(context.Background())
133+
134+ return editor.Run()
135+}
136+
137+// loadProjectSettings reads .turbo-go/settings.toml from the working
138+// directory, and returns that directory along with what it found.
139+//
140+// The working directory alone is looked in, with no walk up towards the root:
141+// "the project" is where you started the editor, which is a rule you can hold
142+// in your head. A file that is there but unreadable is reported on standard
143+// error and then ignored — a broken settings file must not stop the editor
144+// opening, because the editor is how you would fix it.
145+func loadProjectSettings(p profile.Profile) (string, settings.Settings) {
146+ project, err := os.Getwd()
147+ if err != nil {
148+ project = "."
149+ }
150+
151+ loaded, err := settings.Load(p, project)
152+ switch {
153+ case errors.Is(err, settings.ErrNotFound):
154+ return project, settings.Default()
155+ case err != nil:
156+ fmt.Fprintf(os.Stderr, "%s: %v\n", golang.Slug, err)
157+ return project, settings.Default()
158+ }
159+ return project, loaded
160+}
161+
162+// themeName decides which theme to start in.
163+//
164+// A -theme flag wins, because it is the more explicit statement of the two and
165+// is how you try a theme without editing a file everyone shares. The project's
166+// settings come next, and the built-in default last.
167+func themeName(opts options, projectSettings settings.Settings) string {
168+ switch {
169+ case opts.theme != "":
170+ return opts.theme
171+ case projectSettings.Theme != "":
172+ return projectSettings.Theme
173+ default:
174+ return theme.DefaultName
175+ }
176+}
177+
178+// newScreen opens the terminal and turns on what the editor needs from it.
179+func newScreen() (tcell.Screen, error) {
180+ screen, err := tcell.NewScreen()
181+ if err != nil {
182+ return nil, fmt.Errorf("opening the terminal: %w", err)
183+ }
184+ if err := screen.Init(); err != nil {
185+ return nil, fmt.Errorf("initialising the terminal: %w", err)
186+ }
187+
188+ screen.EnableMouse()
189+ screen.EnablePaste()
190+ return screen, nil
191+}
192+
193+// openFiles opens the files named on the command line, or an empty window when
194+// none were.
195+func openFiles(editor *app.App, files []string) {
196+ if len(files) == 0 {
197+ editor.NewFile()
198+ return
199+ }
200+ for _, file := range files {
201+ editor.Open(file)
202+ }
203+}
added main_test.go +131 -0
new file mode 100644
@@ -0,0 +1,131 @@
1+package main
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+
8+ "rickub.com/turbo-editors/turbo-core/app"
9+ "rickub.com/turbo-editors/turbo-core/settings"
10+ "rickub.com/turbo-editors/turbo-core/theme"
11+
12+ "rickub.com/turbo-editors/turbo-go/internal/golang"
13+)
14+
15+func TestTheProjectRootIsTheModuleRoot(t *testing.T) {
16+ // gopls is given the module's boundary, which is what decides the package
17+ // set it loads. The walk itself is turbo-core's; what is checked here is
18+ // that Turbo Go's profile asks it to look for a go.mod.
19+ root := t.TempDir()
20+ if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module x\n"), 0o644); err != nil {
21+ t.Fatalf("writing go.mod: %v", err)
22+ }
23+ nested := filepath.Join(root, "internal", "deep")
24+ if err := os.MkdirAll(nested, 0o755); err != nil {
25+ t.Fatalf("creating the tree: %v", err)
26+ }
27+ file := filepath.Join(nested, "deep.go")
28+ if err := os.WriteFile(file, []byte("package deep\n"), 0o644); err != nil {
29+ t.Fatalf("writing the file: %v", err)
30+ }
31+
32+ if got := app.ProjectRoot(golang.Profile(), []string{file}); got != root {
33+ t.Errorf("ProjectRoot() = %q, want the module root %q", got, root)
34+ }
35+}
36+
37+func TestThemeNamePrefersTheFlagOverTheProject(t *testing.T) {
38+ got := themeName(options{theme: "borland-light"}, settings.Settings{Theme: "turbo-dark"})
39+
40+ if got != "borland-light" {
41+ t.Errorf("themeName() = %q; an explicit -theme must win over the project's", got)
42+ }
43+}
44+
45+func TestThemeNameUsesTheProjectWhenNoFlagWasGiven(t *testing.T) {
46+ got := themeName(options{}, settings.Settings{Theme: "turbo-dark"})
47+
48+ if got != "turbo-dark" {
49+ t.Errorf("themeName() = %q, want the project's theme", got)
50+ }
51+}
52+
53+func TestThemeNameFallsBackToTheDefault(t *testing.T) {
54+ got := themeName(options{}, settings.Settings{})
55+
56+ if got != theme.DefaultName {
57+ t.Errorf("themeName() = %q, want %q", got, theme.DefaultName)
58+ }
59+}
60+
61+func TestLoadProjectSettingsReadsTheWorkingDirectory(t *testing.T) {
62+ project := t.TempDir()
63+ t.Chdir(project)
64+ if err := os.MkdirAll(settings.Dir(golang.Profile(), project), 0o755); err != nil {
65+ t.Fatalf("creating the settings directory: %v", err)
66+ }
67+ contents := "[editor]\ntheme = \"turbo-dark\"\nautosave = true\n"
68+ if err := os.WriteFile(settings.Path(golang.Profile(), project), []byte(contents), 0o644); err != nil {
69+ t.Fatalf("writing the settings file: %v", err)
70+ }
71+
72+ _, loaded := loadProjectSettings(golang.Profile())
73+
74+ if loaded.Theme != "turbo-dark" {
75+ t.Errorf("Theme = %q, want turbo-dark", loaded.Theme)
76+ }
77+ if !loaded.Autosave {
78+ t.Error("Autosave = false, want the file's true")
79+ }
80+}
81+
82+func TestLoadProjectSettingsDoesNotWalkUpToAParent(t *testing.T) {
83+ // "The project is where you started the editor" is the rule; a settings
84+ // file one directory up belongs to a different project.
85+ parent := t.TempDir()
86+ if err := os.MkdirAll(settings.Dir(golang.Profile(), parent), 0o755); err != nil {
87+ t.Fatalf("creating the settings directory: %v", err)
88+ }
89+ if err := os.WriteFile(settings.Path(golang.Profile(), parent), []byte("[editor]\ntheme = \"turbo-dark\"\n"), 0o644); err != nil {
90+ t.Fatalf("writing the settings file: %v", err)
91+ }
92+ child := filepath.Join(parent, "internal")
93+ if err := os.Mkdir(child, 0o755); err != nil {
94+ t.Fatalf("creating the child directory: %v", err)
95+ }
96+ t.Chdir(child)
97+
98+ _, loaded := loadProjectSettings(golang.Profile())
99+
100+ if loaded.Theme != "" {
101+ t.Errorf("Theme = %q; settings were read from a parent directory", loaded.Theme)
102+ }
103+}
104+
105+func TestLoadProjectSettingsCarriesOnWithoutAFile(t *testing.T) {
106+ t.Chdir(t.TempDir())
107+
108+ _, loaded := loadProjectSettings(golang.Profile())
109+
110+ if loaded != settings.Default() {
111+ t.Errorf("loadProjectSettings(golang.Profile()) = %+v, want the defaults", loaded)
112+ }
113+}
114+
115+func TestABrokenSettingsFileDoesNotStopTheEditor(t *testing.T) {
116+ // The editor is how you would fix the file, so it has to open.
117+ project := t.TempDir()
118+ t.Chdir(project)
119+ if err := os.MkdirAll(settings.Dir(golang.Profile(), project), 0o755); err != nil {
120+ t.Fatalf("creating the settings directory: %v", err)
121+ }
122+ if err := os.WriteFile(settings.Path(golang.Profile(), project), []byte("[editor\nnot toml"), 0o644); err != nil {
123+ t.Fatalf("writing the settings file: %v", err)
124+ }
125+
126+ _, loaded := loadProjectSettings(golang.Profile())
127+
128+ if loaded != settings.Default() {
129+ t.Errorf("loadProjectSettings(golang.Profile()) = %+v, want the defaults", loaded)
130+ }
131+}
new file mode 100644
@@ -0,0 +1,131 @@
1+package main
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+
8+ "rickub.com/turbo-editors/turbo-core/app"
9+ "rickub.com/turbo-editors/turbo-core/settings"
10+ "rickub.com/turbo-editors/turbo-core/theme"
11+
12+ "rickub.com/turbo-editors/turbo-go/internal/golang"
13+)
14+
15+func TestTheProjectRootIsTheModuleRoot(t *testing.T) {
16+ // gopls is given the module's boundary, which is what decides the package
17+ // set it loads. The walk itself is turbo-core's; what is checked here is
18+ // that Turbo Go's profile asks it to look for a go.mod.
19+ root := t.TempDir()
20+ if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module x\n"), 0o644); err != nil {
21+ t.Fatalf("writing go.mod: %v", err)
22+ }
23+ nested := filepath.Join(root, "internal", "deep")
24+ if err := os.MkdirAll(nested, 0o755); err != nil {
25+ t.Fatalf("creating the tree: %v", err)
26+ }
27+ file := filepath.Join(nested, "deep.go")
28+ if err := os.WriteFile(file, []byte("package deep\n"), 0o644); err != nil {
29+ t.Fatalf("writing the file: %v", err)
30+ }
31+
32+ if got := app.ProjectRoot(golang.Profile(), []string{file}); got != root {
33+ t.Errorf("ProjectRoot() = %q, want the module root %q", got, root)
34+ }
35+}
36+
37+func TestThemeNamePrefersTheFlagOverTheProject(t *testing.T) {
38+ got := themeName(options{theme: "borland-light"}, settings.Settings{Theme: "turbo-dark"})
39+
40+ if got != "borland-light" {
41+ t.Errorf("themeName() = %q; an explicit -theme must win over the project's", got)
42+ }
43+}
44+
45+func TestThemeNameUsesTheProjectWhenNoFlagWasGiven(t *testing.T) {
46+ got := themeName(options{}, settings.Settings{Theme: "turbo-dark"})
47+
48+ if got != "turbo-dark" {
49+ t.Errorf("themeName() = %q, want the project's theme", got)
50+ }
51+}
52+
53+func TestThemeNameFallsBackToTheDefault(t *testing.T) {
54+ got := themeName(options{}, settings.Settings{})
55+
56+ if got != theme.DefaultName {
57+ t.Errorf("themeName() = %q, want %q", got, theme.DefaultName)
58+ }
59+}
60+
61+func TestLoadProjectSettingsReadsTheWorkingDirectory(t *testing.T) {
62+ project := t.TempDir()
63+ t.Chdir(project)
64+ if err := os.MkdirAll(settings.Dir(golang.Profile(), project), 0o755); err != nil {
65+ t.Fatalf("creating the settings directory: %v", err)
66+ }
67+ contents := "[editor]\ntheme = \"turbo-dark\"\nautosave = true\n"
68+ if err := os.WriteFile(settings.Path(golang.Profile(), project), []byte(contents), 0o644); err != nil {
69+ t.Fatalf("writing the settings file: %v", err)
70+ }
71+
72+ _, loaded := loadProjectSettings(golang.Profile())
73+
74+ if loaded.Theme != "turbo-dark" {
75+ t.Errorf("Theme = %q, want turbo-dark", loaded.Theme)
76+ }
77+ if !loaded.Autosave {
78+ t.Error("Autosave = false, want the file's true")
79+ }
80+}
81+
82+func TestLoadProjectSettingsDoesNotWalkUpToAParent(t *testing.T) {
83+ // "The project is where you started the editor" is the rule; a settings
84+ // file one directory up belongs to a different project.
85+ parent := t.TempDir()
86+ if err := os.MkdirAll(settings.Dir(golang.Profile(), parent), 0o755); err != nil {
87+ t.Fatalf("creating the settings directory: %v", err)
88+ }
89+ if err := os.WriteFile(settings.Path(golang.Profile(), parent), []byte("[editor]\ntheme = \"turbo-dark\"\n"), 0o644); err != nil {
90+ t.Fatalf("writing the settings file: %v", err)
91+ }
92+ child := filepath.Join(parent, "internal")
93+ if err := os.Mkdir(child, 0o755); err != nil {
94+ t.Fatalf("creating the child directory: %v", err)
95+ }
96+ t.Chdir(child)
97+
98+ _, loaded := loadProjectSettings(golang.Profile())
99+
100+ if loaded.Theme != "" {
101+ t.Errorf("Theme = %q; settings were read from a parent directory", loaded.Theme)
102+ }
103+}
104+
105+func TestLoadProjectSettingsCarriesOnWithoutAFile(t *testing.T) {
106+ t.Chdir(t.TempDir())
107+
108+ _, loaded := loadProjectSettings(golang.Profile())
109+
110+ if loaded != settings.Default() {
111+ t.Errorf("loadProjectSettings(golang.Profile()) = %+v, want the defaults", loaded)
112+ }
113+}
114+
115+func TestABrokenSettingsFileDoesNotStopTheEditor(t *testing.T) {
116+ // The editor is how you would fix the file, so it has to open.
117+ project := t.TempDir()
118+ t.Chdir(project)
119+ if err := os.MkdirAll(settings.Dir(golang.Profile(), project), 0o755); err != nil {
120+ t.Fatalf("creating the settings directory: %v", err)
121+ }
122+ if err := os.WriteFile(settings.Path(golang.Profile(), project), []byte("[editor\nnot toml"), 0o644); err != nil {
123+ t.Fatalf("writing the settings file: %v", err)
124+ }
125+
126+ _, loaded := loadProjectSettings(golang.Profile())
127+
128+ if loaded != settings.Default() {
129+ t.Errorf("loadProjectSettings(golang.Profile()) = %+v, want the defaults", loaded)
130+ }
131+}
added new.branch.sh +3 -0
new file mode 100755
@@ -0,0 +1,3 @@
1+#!/bin/bash
2+git switch -c "$1"
3+git push -u origin "$1"
new file mode 100755
@@ -0,0 +1,3 @@
1+#!/bin/bash
2+git switch -c "$1"
3+git push -u origin "$1"
added new.feature.sh +6 -0
new file mode 100755
@@ -0,0 +1,6 @@
1+#!/bin/bash
2+git switch -c feature/"$1"
3+# touch new.feature.txt
4+# git add .
5+# git commit -m "$1"
6+git push -u origin feature/"$1"
new file mode 100755
@@ -0,0 +1,6 @@
1+#!/bin/bash
2+git switch -c feature/"$1"
3+# touch new.feature.txt
4+# git add .
5+# git commit -m "$1"
6+git push -u origin feature/"$1"
added new.fix.sh +6 -0
new file mode 100755
@@ -0,0 +1,6 @@
1+#!/bin/bash
2+git switch -c fix/"$1"
3+#touch new.fix.txt
4+# git add .
5+# git commit -m "$1"
6+git push -u origin fix/"$1"
new file mode 100755
@@ -0,0 +1,6 @@
1+#!/bin/bash
2+git switch -c fix/"$1"
3+#touch new.fix.txt
4+# git add .
5+# git commit -m "$1"
6+git push -u origin fix/"$1"
added release_test.go +594 -0
new file mode 100644
@@ -0,0 +1,594 @@
1+package main
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "runtime"
8+ "strings"
9+ "testing"
10+)
11+
12+// readReleaseScript returns the release builder, so its rules can be asserted
13+// without running it: running it cross-compiles five binaries, which is not a
14+// unit test. (Running the tagging script, on the other hand, is done below,
15+// against a throwaway clone.)
16+func readReleaseScript(t *testing.T) string {
17+ t.Helper()
18+
19+ script, err := os.ReadFile("02-build-releases.sh")
20+ if err != nil {
21+ t.Fatalf("cannot read the release script: %v", err)
22+ }
23+ return string(script)
24+}
25+
26+func TestTheReleaseScriptStampsTheBinariesItShips(t *testing.T) {
27+ // Without -ldflags on the cross-compile, every downloaded binary reports
28+ // "devel" while the release page names a version. The host binary would
29+ // still be right, so nothing but this notices.
30+ script := readReleaseScript(t)
31+
32+ build := commandContaining(t, script, "GOARCH=")
33+ if !strings.Contains(build, "-ldflags") {
34+ t.Errorf("the cross-compile does not stamp a version:\n%s", build)
35+ }
36+}
37+
38+func TestTheReleaseScriptTakesTheStampFromTheMakefile(t *testing.T) {
39+ // Repeating the -X paths in the script is how the host binary and the
40+ // downloads would come to disagree about which package holds the version.
41+ script := readReleaseScript(t)
42+
43+ if !strings.Contains(script, "make --no-print-directory ldflags") {
44+ t.Error("the script does not read the linker flags from the Makefile")
45+ }
46+ if strings.Contains(script, "version.stamp=") {
47+ t.Error("the script spells out the -X path, which the Makefile already owns")
48+ }
49+}
50+
51+func TestTheReleaseScriptStampsTheTagItIsReleasing(t *testing.T) {
52+ // The release *is* ${TAG}, so that is what the binaries say. Letting the
53+ // Makefile's default stand would stamp `git describe`, which answers a
54+ // different question — where HEAD is — and disagrees the moment anyone
55+ // commits after tagging.
56+ script := readReleaseScript(t)
57+
58+ flags := commandContaining(t, script, "ldflags")
59+ if !strings.Contains(flags, `VERSION="${TAG}"`) {
60+ t.Errorf("the stamp does not come from TAG:\n%s", flags)
61+ }
62+ if build := commandContaining(t, script, "make build"); !strings.Contains(build, `VERSION="${TAG}"`) {
63+ t.Errorf("the host build carries a different version from the assets:\n%s", build)
64+ }
65+}
66+
67+func TestTheReleaseScriptDoesNotParseTheVersionOutOfProse(t *testing.T) {
68+ // `-version` is written for a person and has changed shape once already;
69+ // awk '{print $NF}' on it read a timestamp and failed a release.
70+ script := readReleaseScript(t)
71+
72+ if strings.Contains(script, "$NF") {
73+ t.Error("the script reads a field out of the -version line, which is prose")
74+ }
75+}
76+
77+func TestTheMakefileHandsOutTheFlagsThatStampABuild(t *testing.T) {
78+ // The contract the release script depends on: `make ldflags` prints flags
79+ // that actually put *the Makefile's own version* into a binary.
80+ //
81+ // It is checked against `make version` rather than against "not devel",
82+ // because a checkout with no tags — a fresh clone, or a repository that has
83+ // never had a release — correctly reports devel, and a test that called
84+ // that a failure would be testing the tags rather than the flags.
85+ version, err := exec.Command("make", "--no-print-directory", "version").Output()
86+ if err != nil {
87+ t.Fatalf("make version: %v", err)
88+ }
89+ // internal/version drops the leading v of a tag, so the comparison has to
90+ // as well: `make version` says v0.2.1 and the binary says 0.2.1.
91+ number := strings.TrimPrefix(strings.Fields(strings.TrimSpace(string(version)))[0], "v")
92+
93+ flags, err := exec.Command("make", "--no-print-directory", "ldflags").Output()
94+ if err != nil {
95+ t.Fatalf("make ldflags: %v", err)
96+ }
97+
98+ binary := filepath.Join(t.TempDir(), "turbo-go")
99+ build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".")
100+ build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH)
101+ if out, err := build.CombinedOutput(); err != nil {
102+ t.Fatalf("building with those flags failed: %v\n%s", err, out)
103+ }
104+
105+ reported, err := exec.Command(binary, "-version").Output()
106+ if err != nil {
107+ t.Fatalf("the stamped binary does not run: %v", err)
108+ }
109+ if !strings.Contains(string(reported), number) {
110+ t.Errorf("-version printed %q, want it to carry the Makefile's version %q", reported, number)
111+ }
112+ if strings.Contains(string(reported), "unknown") {
113+ t.Errorf("-version printed %q, so nothing reached the linker at all", reported)
114+ }
115+}
116+
117+func TestMakeLdflagsTakesTheVersionItIsGiven(t *testing.T) {
118+ // The release script overrides VERSION with the tag it is releasing, and
119+ // everything downstream rests on that override reaching the linker.
120+ flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION=v9.9.9").Output()
121+ if err != nil {
122+ t.Fatalf("make ldflags: %v", err)
123+ }
124+
125+ binary := filepath.Join(t.TempDir(), "turbo-go")
126+ build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".")
127+ build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH)
128+ if out, err := build.CombinedOutput(); err != nil {
129+ t.Fatalf("building with those flags failed: %v\n%s", err, out)
130+ }
131+
132+ reported, err := exec.Command(binary, "-version").Output()
133+ if err != nil {
134+ t.Fatalf("the stamped binary does not run: %v", err)
135+ }
136+ if !strings.Contains(string(reported), "9.9.9") {
137+ t.Errorf("-version printed %q, so VERSION=v9.9.9 never reached the linker", reported)
138+ }
139+}
140+
141+// commandContaining returns the first shell command of a script holding a
142+// fragment, with backslash continuations joined: a command's flags are often
143+// on the line after the one that names it, and a test about the command should
144+// not depend on where it happens to wrap.
145+func commandContaining(t *testing.T, script, fragment string) string {
146+ t.Helper()
147+
148+ joined := strings.ReplaceAll(script, "\\\n", " ")
149+ for _, line := range strings.Split(joined, "\n") {
150+ if strings.Contains(line, fragment) {
151+ return strings.TrimSpace(line)
152+ }
153+ }
154+ t.Fatalf("no command in the script contains %q", fragment)
155+ return ""
156+}
157+
158+// readTagScript returns the tagging script, whose failure modes are what the
159+// release builder is left to notice when they are not caught here.
160+func readTagScript(t *testing.T) string {
161+ t.Helper()
162+
163+ script, err := os.ReadFile("01-release.tag.sh")
164+ if err != nil {
165+ t.Fatalf("cannot read the tagging script: %v", err)
166+ }
167+ return string(script)
168+}
169+
170+func TestTheTagScriptStopsOnTheFirstFailure(t *testing.T) {
171+ // Without this, `git tag` refusing a tag that already existed was skipped
172+ // in silence and the `git push` after it pushed the OLD tag, cutting a
173+ // release from a commit nobody meant.
174+ if !strings.Contains(readTagScript(t), "set -euo pipefail") {
175+ t.Error("the tagging script does not stop on a failing step")
176+ }
177+}
178+
179+func TestTheTagScriptRefusesATagThatAlreadyExists(t *testing.T) {
180+ script := readTagScript(t)
181+
182+ for _, want := range []string{
183+ "git rev-parse -q --verify", // taken locally
184+ "git ls-remote --tags origin", // taken on the remote, after a local delete
185+ } {
186+ if !strings.Contains(script, want) {
187+ t.Errorf("the tagging script never checks %q", want)
188+ }
189+ }
190+}
191+
192+func TestTheTagScriptSurvivesHavingNothingToCommit(t *testing.T) {
193+ // Under `set -e` a plain `git commit` with a clean tree ends the release,
194+ // which is wrong: the work being already committed is the normal case on a
195+ // second run.
196+ script := readTagScript(t)
197+
198+ if !strings.Contains(script, "git diff --cached --quiet") {
199+ t.Error("the tagging script commits without checking there is anything to commit")
200+ }
201+}
202+
203+func TestTheTagScriptTagsOnlyAfterThePushSucceeded(t *testing.T) {
204+ // A tag left behind pointing at a commit the remote has never seen is the
205+ // state that needs a force push to escape.
206+ script := readTagScript(t)
207+
208+ push := strings.Index(script, `git push origin "$(git rev-parse`)
209+ tag := strings.Index(script, `git tag -a "${TAG}"`)
210+ if push < 0 || tag < 0 {
211+ t.Fatal("the tagging script no longer pushes and tags")
212+ }
213+ if tag < push {
214+ t.Error("the script tags before pushing, so a rejected push leaves a stray tag")
215+ }
216+}
217+
218+// skipInsideARelease stops a test that runs the tagging script from running
219+// while the tagging script is running it.
220+//
221+// The script sets this before `make check`, and `make check` runs this suite.
222+// Without the guard the two call each other forever — which is not a test-only
223+// hazard: a real release would recurse in exactly the same way. The Release
224+// workflow sets it too, for the same reason.
225+func skipInsideARelease(t *testing.T) {
226+ t.Helper()
227+ if os.Getenv("TURBO_GO_RELEASING") != "" {
228+ t.Skip("running inside a release; not starting another one")
229+ }
230+}
231+
232+func TestTheTagScriptRunsTheSuiteBeforePublishing(t *testing.T) {
233+ // A version people will download, and the proxy will cache, is the wrong
234+ // place to find out the suite was red.
235+ if !strings.Contains(readTagScript(t), "make --no-print-directory check") {
236+ t.Error("the tagging script publishes without running make check")
237+ }
238+}
239+
240+func TestTheTagScriptRefusesAReplaceDirective(t *testing.T) {
241+ // The proxy serves go.mod as written, so `go install …@TAG` on a module
242+ // carrying a replace looks for turbo-core in a directory that does not
243+ // exist on the installer's machine.
244+ if !strings.Contains(readTagScript(t), "replace") {
245+ t.Error("the tagging script does not check go.mod for a replace directive")
246+ }
247+}
248+
249+func TestThisModuleHasNoReplaceDirective(t *testing.T) {
250+ // The check above only helps if it is true today as well.
251+ data, err := os.ReadFile("go.mod")
252+ if err != nil {
253+ t.Fatalf("reading go.mod: %v", err)
254+ }
255+ for _, line := range strings.Split(string(data), "\n") {
256+ if strings.HasPrefix(strings.TrimSpace(line), "replace ") {
257+ t.Errorf("go.mod carries %q; a published module must not", line)
258+ }
259+ }
260+}
261+
262+func TestTheReleaseToolingNeedsNoPersonalToken(t *testing.T) {
263+ // The Release workflow publishes with the job's own GITHUB_TOKEN, which is
264+ // the only credential Rickub's release API accepts. A script still reading
265+ // a token file is a credential that cannot work and has to be kept
266+ // somewhere all the same — and a 02 or 04 left in the tree is a second
267+ // pipeline somebody will run by mistake.
268+ for _, script := range []string{"01-release.tag.sh", "02-build-releases.sh"} {
269+ data, err := os.ReadFile(script)
270+ if err != nil {
271+ t.Fatalf("reading %s: %v", script, err)
272+ }
273+ for _, secret := range []string{"token.env", "${TOKEN}"} {
274+ if strings.Contains(string(data), secret) {
275+ t.Errorf("%s still reads %s", script, secret)
276+ }
277+ }
278+ }
279+ for _, gone := range []string{"02-release.publish.sh", "04-release.upload-binaries.sh"} {
280+ if _, err := os.Stat(gone); err == nil {
281+ t.Errorf("%s is still there; the workflow publishes and attaches the binaries now", gone)
282+ }
283+ }
284+}
285+
286+func TestTheTagScriptTagsAndPushesForReal(t *testing.T) {
287+ // The whole flow, in a throwaway clone with its own bare remote, so no tag
288+ // is ever created in the real repository. Reading the script is not the
289+ // same as running it: every guard above was added because one of them was
290+ // wrong once.
291+ skipInsideARelease(t)
292+ if _, err := exec.LookPath("git"); err != nil {
293+ t.Skip("git is not available")
294+ }
295+
296+ remote, clone := throwawayClone(t)
297+
298+ out, err := runAllowingFailure(t, clone, "./01-release.tag.sh")
299+ if err != nil {
300+ t.Fatalf("the tagging script failed:\n%s", out)
301+ }
302+ if !strings.Contains(out, "published") {
303+ t.Errorf("the script did not report publishing:\n%s", out)
304+ }
305+
306+ tags, _ := runAllowingFailure(t, remote, "git", "tag")
307+ if !strings.Contains(tags, "v0.0.1-test") {
308+ t.Errorf("the remote has tags %q, want v0.0.1-test", strings.TrimSpace(tags))
309+ }
310+}
311+
312+func TestTheTagScriptRefusesATagItAlreadyPublished(t *testing.T) {
313+ // Moving a published version is not an option: the proxy caches what it
314+ // fetched, and the release page already carries binaries with that number.
315+ skipInsideARelease(t)
316+ if _, err := exec.LookPath("git"); err != nil {
317+ t.Skip("git is not available")
318+ }
319+
320+ _, clone := throwawayClone(t)
321+
322+ if out, err := runAllowingFailure(t, clone, "./01-release.tag.sh"); err != nil {
323+ t.Fatalf("the first release failed:\n%s", out)
324+ }
325+
326+ out, err := runAllowingFailure(t, clone, "./01-release.tag.sh")
327+
328+ if err == nil {
329+ t.Fatalf("the script published the same tag twice:\n%s", out)
330+ }
331+ if !strings.Contains(out, "already exists") {
332+ t.Errorf("the refusal does not say the tag is taken:\n%s", out)
333+ }
334+}
335+
336+// throwawayClone sets up a bare remote and a clone of it holding a copy of
337+// this module and a release.env naming a test version, and returns both paths.
338+func throwawayClone(t *testing.T) (remote, clone string) {
339+ t.Helper()
340+
341+ root := t.TempDir()
342+ remote = filepath.Join(root, "remote.git")
343+ clone = filepath.Join(root, "clone")
344+
345+ runOrFail(t, root, "git", "init", "--bare", "--initial-branch=main", remote)
346+ runOrFail(t, root, "git", "clone", remote, clone)
347+ copyModuleInto(t, clone)
348+ runOrFail(t, clone, "git", "config", "user.email", "test@example.test")
349+ runOrFail(t, clone, "git", "config", "user.name", "Release Test")
350+ writeFile(t, filepath.Join(clone, "release.env"), "TAG=v0.0.1-test\nABOUT=\"a throwaway release\"\n")
351+ return remote, clone
352+}
353+
354+// copyModuleInto copies the module's source into a directory, so the script can
355+// be run against a real checkout without touching this one.
356+//
357+// .git is left out because the target has its own; *.env because a test writes
358+// its own release.env — copying this checkout's would release whatever version
359+// happens to be in it; go.work because it would point the copy at a turbo-core
360+// checkout that is not what a release builds against; and the build outputs
361+// (bin, release, kits) and the demo project because they are hundreds of
362+// megabytes the script never reads.
363+//
364+// The copying is done here rather than by shelling out to cp, which on a
365+// network-backed working copy has been seen to write the right number of bytes
366+// and the wrong ones: every file in the copy came out NUL-filled.
367+func copyModuleInto(t *testing.T, target string) {
368+ t.Helper()
369+
370+ entries, err := os.ReadDir(".")
371+ if err != nil {
372+ t.Fatalf("reading the module: %v", err)
373+ }
374+ for _, entry := range entries {
375+ if leftOutOfTheCopy(entry.Name()) {
376+ continue
377+ }
378+ copyTree(t, entry.Name(), filepath.Join(target, entry.Name()))
379+ }
380+}
381+
382+// leftOutOfTheCopy reports whether a top-level entry stays out of a throwaway
383+// copy of the module.
384+func leftOutOfTheCopy(name string) bool {
385+ switch name {
386+ case ".git", "bin", "release", "kits", "demo", "go.work", "go.work.sum":
387+ return true
388+ }
389+ return strings.HasSuffix(name, ".env")
390+}
391+
392+// copyTree copies a file or a directory to a new path.
393+//
394+// Anything that is neither a regular file nor a directory is skipped: the tool
395+// directories beside the source hold symlinks into caches that do not exist in
396+// a temporary copy, and the release scripts have no use for them.
397+func copyTree(t *testing.T, from, to string) {
398+ t.Helper()
399+
400+ err := filepath.WalkDir(from, func(path string, entry os.DirEntry, err error) error {
401+ if err != nil {
402+ return err
403+ }
404+ relative, err := filepath.Rel(from, path)
405+ if err != nil {
406+ return err
407+ }
408+ destination := filepath.Join(to, relative)
409+
410+ if entry.IsDir() {
411+ return os.MkdirAll(destination, 0o755)
412+ }
413+ if !entry.Type().IsRegular() {
414+ return nil
415+ }
416+ info, err := entry.Info()
417+ if err != nil {
418+ return err
419+ }
420+ data, err := os.ReadFile(path)
421+ if err != nil {
422+ return err
423+ }
424+ if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
425+ return err
426+ }
427+ // The mode carries the execute bit, without which the scripts these
428+ // tests exist to run cannot be run.
429+ return os.WriteFile(destination, data, info.Mode().Perm())
430+ })
431+ if err != nil {
432+ t.Fatalf("copying %s: %v", from, err)
433+ }
434+}
435+
436+// runOrFail executes a command in a directory, failing the test if it does not
437+// succeed.
438+func runOrFail(t *testing.T, dir string, name string, args ...string) {
439+ t.Helper()
440+
441+ if out, err := runAllowingFailure(t, dir, name, args...); err != nil {
442+ t.Fatalf("%s %v: %v\n%s", name, args, err, out)
443+ }
444+}
445+
446+// runAllowingFailure executes a command and returns its combined output along
447+// with whether it succeeded.
448+//
449+// GOWORK is switched off for the child: a go.work beside this checkout points
450+// at a turbo-core working tree, and a release is built against the published
451+// module, which is what a clean clone would see.
452+func runAllowingFailure(t *testing.T, dir string, name string, args ...string) (string, error) {
453+ t.Helper()
454+
455+ command := exec.Command(name, args...)
456+ command.Dir = dir
457+ command.Env = append(os.Environ(), "GOWORK=off")
458+ out, err := command.CombinedOutput()
459+ return string(out), err
460+}
461+
462+// writeFile creates a file, failing the test if it cannot.
463+func writeFile(t *testing.T, path, contents string) {
464+ t.Helper()
465+
466+ if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
467+ t.Fatalf("writing %s: %v", path, err)
468+ }
469+}
470+
471+func TestTheBuildScriptTakesTheTagFromTheCommandLine(t *testing.T) {
472+ // release.env is git-ignored, so the workflow has none: it passes the tag
473+ // it was started by. A script that only reads the file builds nothing in
474+ // CI, or builds whatever version the file last named.
475+ script := readReleaseScript(t)
476+
477+ if !strings.Contains(script, `TAG="${1:-${TAG:-}}"`) {
478+ t.Error("the build script does not take the tag from its first argument")
479+ }
480+ if !strings.Contains(script, `[ -f release.env ]`) {
481+ t.Error("the build script requires release.env, which CI does not have")
482+ }
483+}
484+
485+func TestTheBuildScriptRefusesATagThatIsNotAVersion(t *testing.T) {
486+ // The proxy will not serve a tag it cannot read as a version, so a typo
487+ // here builds perfectly and then fails at every `go install`.
488+ //
489+ // The refusal comes before anything is built or written, so the script
490+ // alone is enough: it is run from an empty directory with no release.env.
491+ dir := t.TempDir()
492+ script, err := os.ReadFile("02-build-releases.sh")
493+ if err != nil {
494+ t.Fatalf("reading the build script: %v", err)
495+ }
496+ writeFile(t, filepath.Join(dir, "02-build-releases.sh"), string(script))
497+
498+ out, err := runAllowingFailure(t, dir, "bash", "./02-build-releases.sh", "v0.o.0")
499+
500+ if err == nil {
501+ t.Fatalf("the script accepted a tag that is not a version:\n%s", out)
502+ }
503+ if !strings.Contains(out, "v1.2.3") {
504+ t.Errorf("the refusal does not say what a tag should look like:\n%s", out)
505+ }
506+}
507+
508+func TestTheBuildScriptDoesNotHandOffToAnUploadScript(t *testing.T) {
509+ // 04 attached the binaries to a release page a personal token had created.
510+ // The workflow does both now; a script still pointing at 04 sends the
511+ // reader to run something that is not there.
512+ if strings.Contains(readReleaseScript(t), "04-release") {
513+ t.Error("the build script still hands off to 04-release.upload-binaries.sh")
514+ }
515+}
516+
517+// readWorkflow returns the release workflow's text.
518+func readWorkflow(t *testing.T) string {
519+ t.Helper()
520+
521+ data, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml"))
522+ if err != nil {
523+ t.Fatalf("reading the release workflow: %v", err)
524+ }
525+ return string(data)
526+}
527+
528+func TestTheWorkflowPublishesOnATagPush(t *testing.T) {
529+ // The tag push is the trigger: ./01-release.tag.sh ends by pushing one,
530+ // and nothing else starts a release.
531+ workflow := readWorkflow(t)
532+
533+ for _, want := range []string{"push:", "tags:", `- "v*"`} {
534+ if !strings.Contains(workflow, want) {
535+ t.Errorf("the workflow never declares %q", want)
536+ }
537+ }
538+ // A workflow with the default read-only token cannot create a release, and
539+ // fails at its last step after doing all the work.
540+ if !strings.Contains(workflow, "contents: write") {
541+ t.Error("the workflow does not ask for contents: write")
542+ }
543+}
544+
545+func TestTheWorkflowBuildsWithTheSameScriptAPersonRuns(t *testing.T) {
546+ // A CI job that builds its own way is a second pipeline nobody tests, and
547+ // the local one is then only ever exercised by accident.
548+ if !strings.Contains(readWorkflow(t), "./02-build-releases.sh") {
549+ t.Error("the workflow does not build the release with ./02-build-releases.sh")
550+ }
551+}
552+
553+func TestTheWorkflowAttachesWhatWasBuilt(t *testing.T) {
554+ // Publishing a release page with no files attached is a silent half-job:
555+ // the page exists and the downloads are not there.
556+ workflow := readWorkflow(t)
557+
558+ for _, want := range []string{"turbo-go-*", "SHA256SUMS", "fail_on_unmatched_files: true"} {
559+ if !strings.Contains(workflow, want) {
560+ t.Errorf("the workflow never mentions %q", want)
561+ }
562+ }
563+}
564+
565+func TestTheWorkflowLinksToTheDocumentationAtThatTag(t *testing.T) {
566+ // A release page is not inside the repository tree, so a relative path
567+ // from it 404s — and a link to the branch would rot as the branch moves.
568+ workflow := readWorkflow(t)
569+
570+ if !strings.Contains(workflow, "blob/${GITHUB_REF_NAME}") {
571+ t.Error("the release notes do not link into the repository at the released tag")
572+ }
573+ if !strings.Contains(workflow, "/docs/en/README.md") {
574+ t.Error("the release notes do not link to the documentation")
575+ }
576+}
577+
578+func TestTheWorkflowNeedsNoPersonalToken(t *testing.T) {
579+ // The release API behind Rickub's /gh shim accepts the job's own
580+ // GITHUB_TOKEN and refuses a personal one, so a secret referenced here is
581+ // a credential that cannot work and still has to be kept somewhere.
582+ if strings.Contains(readWorkflow(t), "secrets.") {
583+ t.Error("the workflow reads a secret; the job's own token is the only credential the release API takes")
584+ }
585+}
586+
587+func TestTheWorkflowDoesNotStartAReleaseInsideItself(t *testing.T) {
588+ // The suite it runs includes tests that run ./01-release.tag.sh against a
589+ // throwaway clone. Locally the script exports this before calling make;
590+ // in CI nothing calls the script, so the job has to set it itself.
591+ if !strings.Contains(readWorkflow(t), "TURBO_GO_RELEASING") {
592+ t.Error("the workflow runs the suite without TURBO_GO_RELEASING set")
593+ }
594+}
new file mode 100644
@@ -0,0 +1,594 @@
1+package main
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "runtime"
8+ "strings"
9+ "testing"
10+)
11+
12+// readReleaseScript returns the release builder, so its rules can be asserted
13+// without running it: running it cross-compiles five binaries, which is not a
14+// unit test. (Running the tagging script, on the other hand, is done below,
15+// against a throwaway clone.)
16+func readReleaseScript(t *testing.T) string {
17+ t.Helper()
18+
19+ script, err := os.ReadFile("02-build-releases.sh")
20+ if err != nil {
21+ t.Fatalf("cannot read the release script: %v", err)
22+ }
23+ return string(script)
24+}
25+
26+func TestTheReleaseScriptStampsTheBinariesItShips(t *testing.T) {
27+ // Without -ldflags on the cross-compile, every downloaded binary reports
28+ // "devel" while the release page names a version. The host binary would
29+ // still be right, so nothing but this notices.
30+ script := readReleaseScript(t)
31+
32+ build := commandContaining(t, script, "GOARCH=")
33+ if !strings.Contains(build, "-ldflags") {
34+ t.Errorf("the cross-compile does not stamp a version:\n%s", build)
35+ }
36+}
37+
38+func TestTheReleaseScriptTakesTheStampFromTheMakefile(t *testing.T) {
39+ // Repeating the -X paths in the script is how the host binary and the
40+ // downloads would come to disagree about which package holds the version.
41+ script := readReleaseScript(t)
42+
43+ if !strings.Contains(script, "make --no-print-directory ldflags") {
44+ t.Error("the script does not read the linker flags from the Makefile")
45+ }
46+ if strings.Contains(script, "version.stamp=") {
47+ t.Error("the script spells out the -X path, which the Makefile already owns")
48+ }
49+}
50+
51+func TestTheReleaseScriptStampsTheTagItIsReleasing(t *testing.T) {
52+ // The release *is* ${TAG}, so that is what the binaries say. Letting the
53+ // Makefile's default stand would stamp `git describe`, which answers a
54+ // different question — where HEAD is — and disagrees the moment anyone
55+ // commits after tagging.
56+ script := readReleaseScript(t)
57+
58+ flags := commandContaining(t, script, "ldflags")
59+ if !strings.Contains(flags, `VERSION="${TAG}"`) {
60+ t.Errorf("the stamp does not come from TAG:\n%s", flags)
61+ }
62+ if build := commandContaining(t, script, "make build"); !strings.Contains(build, `VERSION="${TAG}"`) {
63+ t.Errorf("the host build carries a different version from the assets:\n%s", build)
64+ }
65+}
66+
67+func TestTheReleaseScriptDoesNotParseTheVersionOutOfProse(t *testing.T) {
68+ // `-version` is written for a person and has changed shape once already;
69+ // awk '{print $NF}' on it read a timestamp and failed a release.
70+ script := readReleaseScript(t)
71+
72+ if strings.Contains(script, "$NF") {
73+ t.Error("the script reads a field out of the -version line, which is prose")
74+ }
75+}
76+
77+func TestTheMakefileHandsOutTheFlagsThatStampABuild(t *testing.T) {
78+ // The contract the release script depends on: `make ldflags` prints flags
79+ // that actually put *the Makefile's own version* into a binary.
80+ //
81+ // It is checked against `make version` rather than against "not devel",
82+ // because a checkout with no tags — a fresh clone, or a repository that has
83+ // never had a release — correctly reports devel, and a test that called
84+ // that a failure would be testing the tags rather than the flags.
85+ version, err := exec.Command("make", "--no-print-directory", "version").Output()
86+ if err != nil {
87+ t.Fatalf("make version: %v", err)
88+ }
89+ // internal/version drops the leading v of a tag, so the comparison has to
90+ // as well: `make version` says v0.2.1 and the binary says 0.2.1.
91+ number := strings.TrimPrefix(strings.Fields(strings.TrimSpace(string(version)))[0], "v")
92+
93+ flags, err := exec.Command("make", "--no-print-directory", "ldflags").Output()
94+ if err != nil {
95+ t.Fatalf("make ldflags: %v", err)
96+ }
97+
98+ binary := filepath.Join(t.TempDir(), "turbo-go")
99+ build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".")
100+ build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH)
101+ if out, err := build.CombinedOutput(); err != nil {
102+ t.Fatalf("building with those flags failed: %v\n%s", err, out)
103+ }
104+
105+ reported, err := exec.Command(binary, "-version").Output()
106+ if err != nil {
107+ t.Fatalf("the stamped binary does not run: %v", err)
108+ }
109+ if !strings.Contains(string(reported), number) {
110+ t.Errorf("-version printed %q, want it to carry the Makefile's version %q", reported, number)
111+ }
112+ if strings.Contains(string(reported), "unknown") {
113+ t.Errorf("-version printed %q, so nothing reached the linker at all", reported)
114+ }
115+}
116+
117+func TestMakeLdflagsTakesTheVersionItIsGiven(t *testing.T) {
118+ // The release script overrides VERSION with the tag it is releasing, and
119+ // everything downstream rests on that override reaching the linker.
120+ flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION=v9.9.9").Output()
121+ if err != nil {
122+ t.Fatalf("make ldflags: %v", err)
123+ }
124+
125+ binary := filepath.Join(t.TempDir(), "turbo-go")
126+ build := exec.Command("go", "build", "-trimpath", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".")
127+ build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH)
128+ if out, err := build.CombinedOutput(); err != nil {
129+ t.Fatalf("building with those flags failed: %v\n%s", err, out)
130+ }
131+
132+ reported, err := exec.Command(binary, "-version").Output()
133+ if err != nil {
134+ t.Fatalf("the stamped binary does not run: %v", err)
135+ }
136+ if !strings.Contains(string(reported), "9.9.9") {
137+ t.Errorf("-version printed %q, so VERSION=v9.9.9 never reached the linker", reported)
138+ }
139+}
140+
141+// commandContaining returns the first shell command of a script holding a
142+// fragment, with backslash continuations joined: a command's flags are often
143+// on the line after the one that names it, and a test about the command should
144+// not depend on where it happens to wrap.
145+func commandContaining(t *testing.T, script, fragment string) string {
146+ t.Helper()
147+
148+ joined := strings.ReplaceAll(script, "\\\n", " ")
149+ for _, line := range strings.Split(joined, "\n") {
150+ if strings.Contains(line, fragment) {
151+ return strings.TrimSpace(line)
152+ }
153+ }
154+ t.Fatalf("no command in the script contains %q", fragment)
155+ return ""
156+}
157+
158+// readTagScript returns the tagging script, whose failure modes are what the
159+// release builder is left to notice when they are not caught here.
160+func readTagScript(t *testing.T) string {
161+ t.Helper()
162+
163+ script, err := os.ReadFile("01-release.tag.sh")
164+ if err != nil {
165+ t.Fatalf("cannot read the tagging script: %v", err)
166+ }
167+ return string(script)
168+}
169+
170+func TestTheTagScriptStopsOnTheFirstFailure(t *testing.T) {
171+ // Without this, `git tag` refusing a tag that already existed was skipped
172+ // in silence and the `git push` after it pushed the OLD tag, cutting a
173+ // release from a commit nobody meant.
174+ if !strings.Contains(readTagScript(t), "set -euo pipefail") {
175+ t.Error("the tagging script does not stop on a failing step")
176+ }
177+}
178+
179+func TestTheTagScriptRefusesATagThatAlreadyExists(t *testing.T) {
180+ script := readTagScript(t)
181+
182+ for _, want := range []string{
183+ "git rev-parse -q --verify", // taken locally
184+ "git ls-remote --tags origin", // taken on the remote, after a local delete
185+ } {
186+ if !strings.Contains(script, want) {
187+ t.Errorf("the tagging script never checks %q", want)
188+ }
189+ }
190+}
191+
192+func TestTheTagScriptSurvivesHavingNothingToCommit(t *testing.T) {
193+ // Under `set -e` a plain `git commit` with a clean tree ends the release,
194+ // which is wrong: the work being already committed is the normal case on a
195+ // second run.
196+ script := readTagScript(t)
197+
198+ if !strings.Contains(script, "git diff --cached --quiet") {
199+ t.Error("the tagging script commits without checking there is anything to commit")
200+ }
201+}
202+
203+func TestTheTagScriptTagsOnlyAfterThePushSucceeded(t *testing.T) {
204+ // A tag left behind pointing at a commit the remote has never seen is the
205+ // state that needs a force push to escape.
206+ script := readTagScript(t)
207+
208+ push := strings.Index(script, `git push origin "$(git rev-parse`)
209+ tag := strings.Index(script, `git tag -a "${TAG}"`)
210+ if push < 0 || tag < 0 {
211+ t.Fatal("the tagging script no longer pushes and tags")
212+ }
213+ if tag < push {
214+ t.Error("the script tags before pushing, so a rejected push leaves a stray tag")
215+ }
216+}
217+
218+// skipInsideARelease stops a test that runs the tagging script from running
219+// while the tagging script is running it.
220+//
221+// The script sets this before `make check`, and `make check` runs this suite.
222+// Without the guard the two call each other forever — which is not a test-only
223+// hazard: a real release would recurse in exactly the same way. The Release
224+// workflow sets it too, for the same reason.
225+func skipInsideARelease(t *testing.T) {
226+ t.Helper()
227+ if os.Getenv("TURBO_GO_RELEASING") != "" {
228+ t.Skip("running inside a release; not starting another one")
229+ }
230+}
231+
232+func TestTheTagScriptRunsTheSuiteBeforePublishing(t *testing.T) {
233+ // A version people will download, and the proxy will cache, is the wrong
234+ // place to find out the suite was red.
235+ if !strings.Contains(readTagScript(t), "make --no-print-directory check") {
236+ t.Error("the tagging script publishes without running make check")
237+ }
238+}
239+
240+func TestTheTagScriptRefusesAReplaceDirective(t *testing.T) {
241+ // The proxy serves go.mod as written, so `go install …@TAG` on a module
242+ // carrying a replace looks for turbo-core in a directory that does not
243+ // exist on the installer's machine.
244+ if !strings.Contains(readTagScript(t), "replace") {
245+ t.Error("the tagging script does not check go.mod for a replace directive")
246+ }
247+}
248+
249+func TestThisModuleHasNoReplaceDirective(t *testing.T) {
250+ // The check above only helps if it is true today as well.
251+ data, err := os.ReadFile("go.mod")
252+ if err != nil {
253+ t.Fatalf("reading go.mod: %v", err)
254+ }
255+ for _, line := range strings.Split(string(data), "\n") {
256+ if strings.HasPrefix(strings.TrimSpace(line), "replace ") {
257+ t.Errorf("go.mod carries %q; a published module must not", line)
258+ }
259+ }
260+}
261+
262+func TestTheReleaseToolingNeedsNoPersonalToken(t *testing.T) {
263+ // The Release workflow publishes with the job's own GITHUB_TOKEN, which is
264+ // the only credential Rickub's release API accepts. A script still reading
265+ // a token file is a credential that cannot work and has to be kept
266+ // somewhere all the same — and a 02 or 04 left in the tree is a second
267+ // pipeline somebody will run by mistake.
268+ for _, script := range []string{"01-release.tag.sh", "02-build-releases.sh"} {
269+ data, err := os.ReadFile(script)
270+ if err != nil {
271+ t.Fatalf("reading %s: %v", script, err)
272+ }
273+ for _, secret := range []string{"token.env", "${TOKEN}"} {
274+ if strings.Contains(string(data), secret) {
275+ t.Errorf("%s still reads %s", script, secret)
276+ }
277+ }
278+ }
279+ for _, gone := range []string{"02-release.publish.sh", "04-release.upload-binaries.sh"} {
280+ if _, err := os.Stat(gone); err == nil {
281+ t.Errorf("%s is still there; the workflow publishes and attaches the binaries now", gone)
282+ }
283+ }
284+}
285+
286+func TestTheTagScriptTagsAndPushesForReal(t *testing.T) {
287+ // The whole flow, in a throwaway clone with its own bare remote, so no tag
288+ // is ever created in the real repository. Reading the script is not the
289+ // same as running it: every guard above was added because one of them was
290+ // wrong once.
291+ skipInsideARelease(t)
292+ if _, err := exec.LookPath("git"); err != nil {
293+ t.Skip("git is not available")
294+ }
295+
296+ remote, clone := throwawayClone(t)
297+
298+ out, err := runAllowingFailure(t, clone, "./01-release.tag.sh")
299+ if err != nil {
300+ t.Fatalf("the tagging script failed:\n%s", out)
301+ }
302+ if !strings.Contains(out, "published") {
303+ t.Errorf("the script did not report publishing:\n%s", out)
304+ }
305+
306+ tags, _ := runAllowingFailure(t, remote, "git", "tag")
307+ if !strings.Contains(tags, "v0.0.1-test") {
308+ t.Errorf("the remote has tags %q, want v0.0.1-test", strings.TrimSpace(tags))
309+ }
310+}
311+
312+func TestTheTagScriptRefusesATagItAlreadyPublished(t *testing.T) {
313+ // Moving a published version is not an option: the proxy caches what it
314+ // fetched, and the release page already carries binaries with that number.
315+ skipInsideARelease(t)
316+ if _, err := exec.LookPath("git"); err != nil {
317+ t.Skip("git is not available")
318+ }
319+
320+ _, clone := throwawayClone(t)
321+
322+ if out, err := runAllowingFailure(t, clone, "./01-release.tag.sh"); err != nil {
323+ t.Fatalf("the first release failed:\n%s", out)
324+ }
325+
326+ out, err := runAllowingFailure(t, clone, "./01-release.tag.sh")
327+
328+ if err == nil {
329+ t.Fatalf("the script published the same tag twice:\n%s", out)
330+ }
331+ if !strings.Contains(out, "already exists") {
332+ t.Errorf("the refusal does not say the tag is taken:\n%s", out)
333+ }
334+}
335+
336+// throwawayClone sets up a bare remote and a clone of it holding a copy of
337+// this module and a release.env naming a test version, and returns both paths.
338+func throwawayClone(t *testing.T) (remote, clone string) {
339+ t.Helper()
340+
341+ root := t.TempDir()
342+ remote = filepath.Join(root, "remote.git")
343+ clone = filepath.Join(root, "clone")
344+
345+ runOrFail(t, root, "git", "init", "--bare", "--initial-branch=main", remote)
346+ runOrFail(t, root, "git", "clone", remote, clone)
347+ copyModuleInto(t, clone)
348+ runOrFail(t, clone, "git", "config", "user.email", "test@example.test")
349+ runOrFail(t, clone, "git", "config", "user.name", "Release Test")
350+ writeFile(t, filepath.Join(clone, "release.env"), "TAG=v0.0.1-test\nABOUT=\"a throwaway release\"\n")
351+ return remote, clone
352+}
353+
354+// copyModuleInto copies the module's source into a directory, so the script can
355+// be run against a real checkout without touching this one.
356+//
357+// .git is left out because the target has its own; *.env because a test writes
358+// its own release.env — copying this checkout's would release whatever version
359+// happens to be in it; go.work because it would point the copy at a turbo-core
360+// checkout that is not what a release builds against; and the build outputs
361+// (bin, release, kits) and the demo project because they are hundreds of
362+// megabytes the script never reads.
363+//
364+// The copying is done here rather than by shelling out to cp, which on a
365+// network-backed working copy has been seen to write the right number of bytes
366+// and the wrong ones: every file in the copy came out NUL-filled.
367+func copyModuleInto(t *testing.T, target string) {
368+ t.Helper()
369+
370+ entries, err := os.ReadDir(".")
371+ if err != nil {
372+ t.Fatalf("reading the module: %v", err)
373+ }
374+ for _, entry := range entries {
375+ if leftOutOfTheCopy(entry.Name()) {
376+ continue
377+ }
378+ copyTree(t, entry.Name(), filepath.Join(target, entry.Name()))
379+ }
380+}
381+
382+// leftOutOfTheCopy reports whether a top-level entry stays out of a throwaway
383+// copy of the module.
384+func leftOutOfTheCopy(name string) bool {
385+ switch name {
386+ case ".git", "bin", "release", "kits", "demo", "go.work", "go.work.sum":
387+ return true
388+ }
389+ return strings.HasSuffix(name, ".env")
390+}
391+
392+// copyTree copies a file or a directory to a new path.
393+//
394+// Anything that is neither a regular file nor a directory is skipped: the tool
395+// directories beside the source hold symlinks into caches that do not exist in
396+// a temporary copy, and the release scripts have no use for them.
397+func copyTree(t *testing.T, from, to string) {
398+ t.Helper()
399+
400+ err := filepath.WalkDir(from, func(path string, entry os.DirEntry, err error) error {
401+ if err != nil {
402+ return err
403+ }
404+ relative, err := filepath.Rel(from, path)
405+ if err != nil {
406+ return err
407+ }
408+ destination := filepath.Join(to, relative)
409+
410+ if entry.IsDir() {
411+ return os.MkdirAll(destination, 0o755)
412+ }
413+ if !entry.Type().IsRegular() {
414+ return nil
415+ }
416+ info, err := entry.Info()
417+ if err != nil {
418+ return err
419+ }
420+ data, err := os.ReadFile(path)
421+ if err != nil {
422+ return err
423+ }
424+ if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
425+ return err
426+ }
427+ // The mode carries the execute bit, without which the scripts these
428+ // tests exist to run cannot be run.
429+ return os.WriteFile(destination, data, info.Mode().Perm())
430+ })
431+ if err != nil {
432+ t.Fatalf("copying %s: %v", from, err)
433+ }
434+}
435+
436+// runOrFail executes a command in a directory, failing the test if it does not
437+// succeed.
438+func runOrFail(t *testing.T, dir string, name string, args ...string) {
439+ t.Helper()
440+
441+ if out, err := runAllowingFailure(t, dir, name, args...); err != nil {
442+ t.Fatalf("%s %v: %v\n%s", name, args, err, out)
443+ }
444+}
445+
446+// runAllowingFailure executes a command and returns its combined output along
447+// with whether it succeeded.
448+//
449+// GOWORK is switched off for the child: a go.work beside this checkout points
450+// at a turbo-core working tree, and a release is built against the published
451+// module, which is what a clean clone would see.
452+func runAllowingFailure(t *testing.T, dir string, name string, args ...string) (string, error) {
453+ t.Helper()
454+
455+ command := exec.Command(name, args...)
456+ command.Dir = dir
457+ command.Env = append(os.Environ(), "GOWORK=off")
458+ out, err := command.CombinedOutput()
459+ return string(out), err
460+}
461+
462+// writeFile creates a file, failing the test if it cannot.
463+func writeFile(t *testing.T, path, contents string) {
464+ t.Helper()
465+
466+ if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
467+ t.Fatalf("writing %s: %v", path, err)
468+ }
469+}
470+
471+func TestTheBuildScriptTakesTheTagFromTheCommandLine(t *testing.T) {
472+ // release.env is git-ignored, so the workflow has none: it passes the tag
473+ // it was started by. A script that only reads the file builds nothing in
474+ // CI, or builds whatever version the file last named.
475+ script := readReleaseScript(t)
476+
477+ if !strings.Contains(script, `TAG="${1:-${TAG:-}}"`) {
478+ t.Error("the build script does not take the tag from its first argument")
479+ }
480+ if !strings.Contains(script, `[ -f release.env ]`) {
481+ t.Error("the build script requires release.env, which CI does not have")
482+ }
483+}
484+
485+func TestTheBuildScriptRefusesATagThatIsNotAVersion(t *testing.T) {
486+ // The proxy will not serve a tag it cannot read as a version, so a typo
487+ // here builds perfectly and then fails at every `go install`.
488+ //
489+ // The refusal comes before anything is built or written, so the script
490+ // alone is enough: it is run from an empty directory with no release.env.
491+ dir := t.TempDir()
492+ script, err := os.ReadFile("02-build-releases.sh")
493+ if err != nil {
494+ t.Fatalf("reading the build script: %v", err)
495+ }
496+ writeFile(t, filepath.Join(dir, "02-build-releases.sh"), string(script))
497+
498+ out, err := runAllowingFailure(t, dir, "bash", "./02-build-releases.sh", "v0.o.0")
499+
500+ if err == nil {
501+ t.Fatalf("the script accepted a tag that is not a version:\n%s", out)
502+ }
503+ if !strings.Contains(out, "v1.2.3") {
504+ t.Errorf("the refusal does not say what a tag should look like:\n%s", out)
505+ }
506+}
507+
508+func TestTheBuildScriptDoesNotHandOffToAnUploadScript(t *testing.T) {
509+ // 04 attached the binaries to a release page a personal token had created.
510+ // The workflow does both now; a script still pointing at 04 sends the
511+ // reader to run something that is not there.
512+ if strings.Contains(readReleaseScript(t), "04-release") {
513+ t.Error("the build script still hands off to 04-release.upload-binaries.sh")
514+ }
515+}
516+
517+// readWorkflow returns the release workflow's text.
518+func readWorkflow(t *testing.T) string {
519+ t.Helper()
520+
521+ data, err := os.ReadFile(filepath.Join(".github", "workflows", "release.yml"))
522+ if err != nil {
523+ t.Fatalf("reading the release workflow: %v", err)
524+ }
525+ return string(data)
526+}
527+
528+func TestTheWorkflowPublishesOnATagPush(t *testing.T) {
529+ // The tag push is the trigger: ./01-release.tag.sh ends by pushing one,
530+ // and nothing else starts a release.
531+ workflow := readWorkflow(t)
532+
533+ for _, want := range []string{"push:", "tags:", `- "v*"`} {
534+ if !strings.Contains(workflow, want) {
535+ t.Errorf("the workflow never declares %q", want)
536+ }
537+ }
538+ // A workflow with the default read-only token cannot create a release, and
539+ // fails at its last step after doing all the work.
540+ if !strings.Contains(workflow, "contents: write") {
541+ t.Error("the workflow does not ask for contents: write")
542+ }
543+}
544+
545+func TestTheWorkflowBuildsWithTheSameScriptAPersonRuns(t *testing.T) {
546+ // A CI job that builds its own way is a second pipeline nobody tests, and
547+ // the local one is then only ever exercised by accident.
548+ if !strings.Contains(readWorkflow(t), "./02-build-releases.sh") {
549+ t.Error("the workflow does not build the release with ./02-build-releases.sh")
550+ }
551+}
552+
553+func TestTheWorkflowAttachesWhatWasBuilt(t *testing.T) {
554+ // Publishing a release page with no files attached is a silent half-job:
555+ // the page exists and the downloads are not there.
556+ workflow := readWorkflow(t)
557+
558+ for _, want := range []string{"turbo-go-*", "SHA256SUMS", "fail_on_unmatched_files: true"} {
559+ if !strings.Contains(workflow, want) {
560+ t.Errorf("the workflow never mentions %q", want)
561+ }
562+ }
563+}
564+
565+func TestTheWorkflowLinksToTheDocumentationAtThatTag(t *testing.T) {
566+ // A release page is not inside the repository tree, so a relative path
567+ // from it 404s — and a link to the branch would rot as the branch moves.
568+ workflow := readWorkflow(t)
569+
570+ if !strings.Contains(workflow, "blob/${GITHUB_REF_NAME}") {
571+ t.Error("the release notes do not link into the repository at the released tag")
572+ }
573+ if !strings.Contains(workflow, "/docs/en/README.md") {
574+ t.Error("the release notes do not link to the documentation")
575+ }
576+}
577+
578+func TestTheWorkflowNeedsNoPersonalToken(t *testing.T) {
579+ // The release API behind Rickub's /gh shim accepts the job's own
580+ // GITHUB_TOKEN and refuses a personal one, so a secret referenced here is
581+ // a credential that cannot work and still has to be kept somewhere.
582+ if strings.Contains(readWorkflow(t), "secrets.") {
583+ t.Error("the workflow reads a secret; the job's own token is the only credential the release API takes")
584+ }
585+}
586+
587+func TestTheWorkflowDoesNotStartAReleaseInsideItself(t *testing.T) {
588+ // The suite it runs includes tests that run ./01-release.tag.sh against a
589+ // throwaway clone. Locally the script exports this before calling make;
590+ // in CI nothing calls the script, so the job has to set it itself.
591+ if !strings.Contains(readWorkflow(t), "TURBO_GO_RELEASING") {
592+ t.Error("the workflow runs the suite without TURBO_GO_RELEASING set")
593+ }
594+}
added scripts/check-version.sh +72 -0
new file mode 100755
@@ -0,0 +1,72 @@
1+#!/usr/bin/env bash
2+#
3+# Check that a freshly built binary reports the version the build meant to put
4+# into it.
5+#
6+# scripts/check-version.sh bin/turbo-go v0.2.0 88a4c38
7+# scripts/check-version.sh bin/turbo-go # unstamped build
8+#
9+# Linker flags are a string: a typo in one produces a binary that builds, links
10+# and runs, and quietly reports the wrong version — or "unknown". Nothing but
11+# running the binary catches that, so the build runs it.
12+#
13+# With a version to expect, the reported number must **equal** it. A substring
14+# test is not enough: "0.2.0" is a substring of "10.2.0" and of a commit hash
15+# that happens to contain it, and the case this exists to catch is a stamp that
16+# is nearly right.
17+#
18+# With no version to expect — a build outside a git checkout, where there is
19+# nothing to describe — the only claim left is that some source named it, so
20+# "unknown" is the failure.
21+
22+set -euo pipefail
23+
24+if [ $# -lt 1 ]; then
25+ echo "usage: $0 <binary> [expected-version] [expected-commit]" >&2
26+ exit 2
27+fi
28+
29+readonly BINARY="$1"
30+readonly EXPECTED_VERSION="${2:-}"
31+readonly EXPECTED_COMMIT="${3:-}"
32+
33+if [ ! -x "${BINARY}" ]; then
34+ echo "check-version: ${BINARY} is not an executable file" >&2
35+ exit 1
36+fi
37+
38+if ! reported="$("${BINARY}" -version 2>&1)"; then
39+ echo "check-version: ${BINARY} does not run:" >&2
40+ echo "${reported}" >&2
41+ exit 1
42+fi
43+
44+# The binary prints "<Name> <number>" or "<Name> <number> (<commit>, built …)",
45+# so the number is the last field before the parenthesis, if there is one. The
46+# name is two words in every editor built on turbo-core and one word in some
47+# future one, which is why it is read from the right rather than the left.
48+head="${reported%% (*}"
49+number="${head##* }"
50+
51+if [ -n "${EXPECTED_VERSION}" ]; then
52+ # The version package drops the leading v of a tag: the tag is v0.2.0 and
53+ # what a person reads is 0.2.0.
54+ want="${EXPECTED_VERSION#v}"
55+ if [ "${number}" != "${want}" ]; then
56+ echo "check-version: the build meant to stamp ${want} and the binary reports ${number}" >&2
57+ echo " ${reported}" >&2
58+ exit 1
59+ fi
60+elif [ "${number}" = "unknown" ]; then
61+ echo "check-version: the binary cannot name its own version" >&2
62+ echo " ${reported}" >&2
63+ exit 1
64+fi
65+
66+if [ -n "${EXPECTED_COMMIT}" ] && [ "${reported}" = "${reported#*"${EXPECTED_COMMIT}"}" ]; then
67+ echo "check-version: the build meant to stamp commit ${EXPECTED_COMMIT} and the binary reports:" >&2
68+ echo " ${reported}" >&2
69+ exit 1
70+fi
71+
72+echo "${reported}"
new file mode 100755
@@ -0,0 +1,72 @@
1+#!/usr/bin/env bash
2+#
3+# Check that a freshly built binary reports the version the build meant to put
4+# into it.
5+#
6+# scripts/check-version.sh bin/turbo-go v0.2.0 88a4c38
7+# scripts/check-version.sh bin/turbo-go # unstamped build
8+#
9+# Linker flags are a string: a typo in one produces a binary that builds, links
10+# and runs, and quietly reports the wrong version — or "unknown". Nothing but
11+# running the binary catches that, so the build runs it.
12+#
13+# With a version to expect, the reported number must **equal** it. A substring
14+# test is not enough: "0.2.0" is a substring of "10.2.0" and of a commit hash
15+# that happens to contain it, and the case this exists to catch is a stamp that
16+# is nearly right.
17+#
18+# With no version to expect — a build outside a git checkout, where there is
19+# nothing to describe — the only claim left is that some source named it, so
20+# "unknown" is the failure.
21+
22+set -euo pipefail
23+
24+if [ $# -lt 1 ]; then
25+ echo "usage: $0 <binary> [expected-version] [expected-commit]" >&2
26+ exit 2
27+fi
28+
29+readonly BINARY="$1"
30+readonly EXPECTED_VERSION="${2:-}"
31+readonly EXPECTED_COMMIT="${3:-}"
32+
33+if [ ! -x "${BINARY}" ]; then
34+ echo "check-version: ${BINARY} is not an executable file" >&2
35+ exit 1
36+fi
37+
38+if ! reported="$("${BINARY}" -version 2>&1)"; then
39+ echo "check-version: ${BINARY} does not run:" >&2
40+ echo "${reported}" >&2
41+ exit 1
42+fi
43+
44+# The binary prints "<Name> <number>" or "<Name> <number> (<commit>, built …)",
45+# so the number is the last field before the parenthesis, if there is one. The
46+# name is two words in every editor built on turbo-core and one word in some
47+# future one, which is why it is read from the right rather than the left.
48+head="${reported%% (*}"
49+number="${head##* }"
50+
51+if [ -n "${EXPECTED_VERSION}" ]; then
52+ # The version package drops the leading v of a tag: the tag is v0.2.0 and
53+ # what a person reads is 0.2.0.
54+ want="${EXPECTED_VERSION#v}"
55+ if [ "${number}" != "${want}" ]; then
56+ echo "check-version: the build meant to stamp ${want} and the binary reports ${number}" >&2
57+ echo " ${reported}" >&2
58+ exit 1
59+ fi
60+elif [ "${number}" = "unknown" ]; then
61+ echo "check-version: the binary cannot name its own version" >&2
62+ echo " ${reported}" >&2
63+ exit 1
64+fi
65+
66+if [ -n "${EXPECTED_COMMIT}" ] && [ "${reported}" = "${reported#*"${EXPECTED_COMMIT}"}" ]; then
67+ echo "check-version: the build meant to stamp commit ${EXPECTED_COMMIT} and the binary reports:" >&2
68+ echo " ${reported}" >&2
69+ exit 1
70+fi
71+
72+echo "${reported}"
added scripts/install.sh +276 -0
new file mode 100755
@@ -0,0 +1,276 @@
1+#!/usr/bin/env bash
2+#
3+# Build turbo-go and install it where your shell can find it.
4+#
5+# scripts/install.sh # install into GOBIN, or GOPATH/bin
6+# scripts/install.sh --prefix ~/bin # install somewhere else
7+# scripts/install.sh --with-gopls # install the language server too
8+# scripts/install.sh --uninstall # remove it again
9+#
10+# The build goes to a temporary file first, so a failed build never replaces a
11+# working installation, and the install itself is a rename rather than a write
12+# over the binary that is already there. The version is stamped in by the
13+# linker, so `turbo-go -version` names the commit it was built from.
14+
15+set -euo pipefail
16+
17+readonly BINARY=turbo-go
18+
19+# --- output -----------------------------------------------------------------
20+
21+if [ -t 1 ]; then
22+ readonly BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m'
23+else
24+ readonly BOLD='' DIM='' RED='' GREEN='' YELLOW='' RESET=''
25+fi
26+
27+info() { printf '%s\n' "$*"; }
28+step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; }
29+ok() { printf '%s ✓%s %s\n' "$GREEN" "$RESET" "$*"; }
30+warn() { printf '%s !%s %s\n' "$YELLOW" "$RESET" "$*"; }
31+die() {
32+ printf '%s ✗%s %s\n' "$RED" "$RESET" "$*" >&2
33+ exit 1
34+}
35+
36+usage() {
37+ cat <<EOF
38+${BOLD}$BINARY installer${RESET}
39+
40+ scripts/install.sh [options]
41+
42+Options:
43+ -p, --prefix DIR install into DIR (default: GOBIN, or GOPATH/bin)
44+ --with-gopls also install gopls, which completion needs
45+ --uninstall remove an installed $BINARY and stop
46+ -h, --help show this and stop
47+EOF
48+}
49+
50+# --- arguments --------------------------------------------------------------
51+
52+prefix=""
53+with_gopls=false
54+uninstall=false
55+
56+while [ $# -gt 0 ]; do
57+ case "$1" in
58+ -p | --prefix)
59+ [ $# -ge 2 ] || die "--prefix needs a directory"
60+ prefix="$2"
61+ shift 2
62+ ;;
63+ --with-gopls)
64+ with_gopls=true
65+ shift
66+ ;;
67+ --uninstall)
68+ uninstall=true
69+ shift
70+ ;;
71+ -h | --help)
72+ usage
73+ exit 0
74+ ;;
75+ *) die "unknown option: $1 (try --help)" ;;
76+ esac
77+done
78+
79+# --- where things are -------------------------------------------------------
80+
81+cd "$(dirname "${BASH_SOURCE[0]}")/.."
82+readonly REPO="$PWD"
83+
84+command -v go >/dev/null 2>&1 || die "go is not installed: https://go.dev/dl/"
85+
86+# default_prefix returns where "go install" would put a binary: GOBIN when it
87+# is set, GOPATH/bin otherwise. That is the directory a Go developer is most
88+# likely to already have on PATH.
89+default_prefix() {
90+ local gobin
91+ gobin="$(go env GOBIN)"
92+ if [ -n "$gobin" ]; then
93+ printf '%s\n' "$gobin"
94+ else
95+ printf '%s/bin\n' "$(go env GOPATH)"
96+ fi
97+}
98+
99+[ -n "$prefix" ] || prefix="$(default_prefix)"
100+[ -n "$prefix" ] || die "cannot work out where to install; use --prefix DIR"
101+readonly TARGET="$prefix/$BINARY"
102+
103+# --- uninstall --------------------------------------------------------------
104+
105+if $uninstall; then
106+ step "Removing $TARGET"
107+ if [ -e "$TARGET" ]; then
108+ rm -f "$TARGET"
109+ ok "removed"
110+ else
111+ warn "nothing installed at $TARGET"
112+ fi
113+ exit 0
114+fi
115+
116+# --- toolchain --------------------------------------------------------------
117+
118+step "Checking the Go toolchain"
119+
120+# The requirement lives in go.mod, so this check cannot drift from the build.
121+required="$(awk '/^go /{print $2; exit}' "$REPO/go.mod")"
122+installed="$(go env GOVERSION)"
123+installed="${installed#go}"
124+
125+if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then
126+ die "Go $required or later is needed, but $installed is installed"
127+fi
128+ok "go $installed (go.mod asks for $required or later)"
129+
130+# --- build ------------------------------------------------------------------
131+
132+step "Building $BINARY"
133+
134+readonly STAGING="$(mktemp -d)"
135+trap 'rm -rf "$STAGING"' EXIT
136+
137+# The version the binary reports is stamped in by the linker, so that an
138+# installed editor names the commit it was actually built from rather than a
139+# constant somebody forgot to bump. Outside a git checkout — installed from a
140+# tarball, say — there is nothing to describe and the binary works the version
141+# out from its own build information instead.
142+version_pkg="rickub.com/turbo-editors/turbo-core/version"
143+if describe="$(git -C "$REPO" describe --tags --dirty 2>/dev/null)"; then
144+ commit="$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || true)"
145+ built="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
146+ ldflags="-X '$version_pkg.stamp=$describe' -X '$version_pkg.commit=$commit' -X '$version_pkg.built=$built'"
147+else
148+ ldflags=""
149+fi
150+
151+if ! go build -ldflags "$ldflags" -o "$STAGING/$BINARY" "$REPO" 2>"$STAGING/build.log"; then
152+ cat "$STAGING/build.log" >&2
153+ info ""
154+ warn "A failure in the repository root is often a stray .go file that has"
155+ warn "landed in package main. 'go vet .' names it."
156+ die "build failed; nothing was installed"
157+fi
158+
159+# Running the staged binary is the only proof that the flags above reached the
160+# linker rather than merely looking right. It happens before the install, so a
161+# binary that cannot name its own version never replaces a working one.
162+if ! stamped="$("$REPO/scripts/check-version.sh" "$STAGING/$BINARY" "${describe:-}" "${commit:-}" 2>&1)"; then
163+ info "$stamped"
164+ die "the build did not carry its version; nothing was installed"
165+fi
166+ok "built — $stamped"
167+
168+# --- install ----------------------------------------------------------------
169+
170+step "Installing into $prefix"
171+
172+mkdir -p "$prefix" || die "cannot create $prefix"
173+
174+# Install by renaming a complete file over the target, never by writing into
175+# the one that is there.
176+#
177+# macOS caches a binary's code signature against its inode. cp writes new bytes
178+# into the existing inode, so the cached signature ends up describing something
179+# else and the kernel refuses to execute the result — a reinstall that builds,
180+# installs, and then will not run. A rename gives the name a fresh inode, so
181+# there is nothing stale to cache. It is atomic besides: no moment at which a
182+# half-written turbo-go is on the PATH.
183+#
184+# The temporary has to sit in $prefix, because a rename only works within one
185+# filesystem and $STAGING is somewhere else entirely.
186+readonly INCOMING="$prefix/.$BINARY.incoming.$$"
187+trap 'rm -rf "$STAGING"; rm -f "$INCOMING"' EXIT
188+
189+cp "$STAGING/$BINARY" "$INCOMING" || die "cannot write into $prefix"
190+chmod 0755 "$INCOMING"
191+mv -f "$INCOMING" "$TARGET" || die "cannot replace $TARGET"
192+
193+# Whatever the system said is the useful part: "does not run" on its own tells
194+# nobody anything they can act on.
195+if ! verify="$("$TARGET" -version 2>&1)"; then
196+ info "$verify"
197+ die "the installed binary does not run"
198+fi
199+version="$verify"
200+ok "$version → $TARGET"
201+
202+# --- PATH -------------------------------------------------------------------
203+
204+# on_path reports whether a directory is one the shell searches.
205+on_path() {
206+ case ":${PATH:-}:" in
207+ *":$1:"*) return 0 ;;
208+ *) return 1 ;;
209+ esac
210+}
211+
212+# shell_profile guesses the file that sets PATH for the user's shell.
213+shell_profile() {
214+ case "${SHELL:-}" in
215+ */zsh) printf '~/.zshrc\n' ;;
216+ */fish) printf '~/.config/fish/config.fish\n' ;;
217+ *) printf '~/.bashrc\n' ;;
218+ esac
219+}
220+
221+step "Checking your PATH"
222+if on_path "$prefix"; then
223+ ok "$prefix is on your PATH"
224+else
225+ warn "$prefix is not on your PATH. Add it:"
226+ info ""
227+ info " echo 'export PATH=\"\$PATH:$prefix\"' >> $(shell_profile)"
228+ info " exec \$SHELL"
229+fi
230+
231+# --- the language server ----------------------------------------------------
232+
233+# find_gopls looks where the editor itself looks: PATH, then GOBIN, then
234+# GOPATH/bin.
235+find_gopls() {
236+ command -v gopls 2>/dev/null && return 0
237+ local candidate
238+ for candidate in "$(go env GOBIN)/gopls" "$(go env GOPATH)/bin/gopls"; do
239+ [ -x "$candidate" ] && {
240+ printf '%s\n' "$candidate"
241+ return 0
242+ }
243+ done
244+ return 1
245+}
246+
247+step "Checking the language server"
248+
249+if $with_gopls && ! find_gopls >/dev/null; then
250+ info " installing gopls, which takes a minute…"
251+ go install golang.org/x/tools/gopls@latest || die "could not install gopls"
252+fi
253+
254+if gopls_path="$(find_gopls)"; then
255+ ok "gopls at $gopls_path"
256+else
257+ warn "gopls is not installed, so there will be no completion."
258+ warn "Editing, colouring and themes all work without it."
259+ info ""
260+ info " go install golang.org/x/tools/gopls@latest"
261+ info " ${DIM}or re-run this script with --with-gopls${RESET}"
262+fi
263+
264+# --- what to do next --------------------------------------------------------
265+
266+info ""
267+step "Ready"
268+info ""
269+info " Open a file ${BOLD}inside a Go module${RESET} — completion needs one:"
270+info ""
271+info " cd /path/to/your/project"
272+info " $BINARY main.go"
273+info ""
274+info " ${DIM}F10 menu · F2 save · Alt-X exit · type a '.' for completion${RESET}"
275+info " ${DIM}themes: $BINARY -list-themes${RESET}"
276+info ""
new file mode 100755
@@ -0,0 +1,276 @@
1+#!/usr/bin/env bash
2+#
3+# Build turbo-go and install it where your shell can find it.
4+#
5+# scripts/install.sh # install into GOBIN, or GOPATH/bin
6+# scripts/install.sh --prefix ~/bin # install somewhere else
7+# scripts/install.sh --with-gopls # install the language server too
8+# scripts/install.sh --uninstall # remove it again
9+#
10+# The build goes to a temporary file first, so a failed build never replaces a
11+# working installation, and the install itself is a rename rather than a write
12+# over the binary that is already there. The version is stamped in by the
13+# linker, so `turbo-go -version` names the commit it was built from.
14+
15+set -euo pipefail
16+
17+readonly BINARY=turbo-go
18+
19+# --- output -----------------------------------------------------------------
20+
21+if [ -t 1 ]; then
22+ readonly BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m'
23+else
24+ readonly BOLD='' DIM='' RED='' GREEN='' YELLOW='' RESET=''
25+fi
26+
27+info() { printf '%s\n' "$*"; }
28+step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; }
29+ok() { printf '%s ✓%s %s\n' "$GREEN" "$RESET" "$*"; }
30+warn() { printf '%s !%s %s\n' "$YELLOW" "$RESET" "$*"; }
31+die() {
32+ printf '%s ✗%s %s\n' "$RED" "$RESET" "$*" >&2
33+ exit 1
34+}
35+
36+usage() {
37+ cat <<EOF
38+${BOLD}$BINARY installer${RESET}
39+
40+ scripts/install.sh [options]
41+
42+Options:
43+ -p, --prefix DIR install into DIR (default: GOBIN, or GOPATH/bin)
44+ --with-gopls also install gopls, which completion needs
45+ --uninstall remove an installed $BINARY and stop
46+ -h, --help show this and stop
47+EOF
48+}
49+
50+# --- arguments --------------------------------------------------------------
51+
52+prefix=""
53+with_gopls=false
54+uninstall=false
55+
56+while [ $# -gt 0 ]; do
57+ case "$1" in
58+ -p | --prefix)
59+ [ $# -ge 2 ] || die "--prefix needs a directory"
60+ prefix="$2"
61+ shift 2
62+ ;;
63+ --with-gopls)
64+ with_gopls=true
65+ shift
66+ ;;
67+ --uninstall)
68+ uninstall=true
69+ shift
70+ ;;
71+ -h | --help)
72+ usage
73+ exit 0
74+ ;;
75+ *) die "unknown option: $1 (try --help)" ;;
76+ esac
77+done
78+
79+# --- where things are -------------------------------------------------------
80+
81+cd "$(dirname "${BASH_SOURCE[0]}")/.."
82+readonly REPO="$PWD"
83+
84+command -v go >/dev/null 2>&1 || die "go is not installed: https://go.dev/dl/"
85+
86+# default_prefix returns where "go install" would put a binary: GOBIN when it
87+# is set, GOPATH/bin otherwise. That is the directory a Go developer is most
88+# likely to already have on PATH.
89+default_prefix() {
90+ local gobin
91+ gobin="$(go env GOBIN)"
92+ if [ -n "$gobin" ]; then
93+ printf '%s\n' "$gobin"
94+ else
95+ printf '%s/bin\n' "$(go env GOPATH)"
96+ fi
97+}
98+
99+[ -n "$prefix" ] || prefix="$(default_prefix)"
100+[ -n "$prefix" ] || die "cannot work out where to install; use --prefix DIR"
101+readonly TARGET="$prefix/$BINARY"
102+
103+# --- uninstall --------------------------------------------------------------
104+
105+if $uninstall; then
106+ step "Removing $TARGET"
107+ if [ -e "$TARGET" ]; then
108+ rm -f "$TARGET"
109+ ok "removed"
110+ else
111+ warn "nothing installed at $TARGET"
112+ fi
113+ exit 0
114+fi
115+
116+# --- toolchain --------------------------------------------------------------
117+
118+step "Checking the Go toolchain"
119+
120+# The requirement lives in go.mod, so this check cannot drift from the build.
121+required="$(awk '/^go /{print $2; exit}' "$REPO/go.mod")"
122+installed="$(go env GOVERSION)"
123+installed="${installed#go}"
124+
125+if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then
126+ die "Go $required or later is needed, but $installed is installed"
127+fi
128+ok "go $installed (go.mod asks for $required or later)"
129+
130+# --- build ------------------------------------------------------------------
131+
132+step "Building $BINARY"
133+
134+readonly STAGING="$(mktemp -d)"
135+trap 'rm -rf "$STAGING"' EXIT
136+
137+# The version the binary reports is stamped in by the linker, so that an
138+# installed editor names the commit it was actually built from rather than a
139+# constant somebody forgot to bump. Outside a git checkout — installed from a
140+# tarball, say — there is nothing to describe and the binary works the version
141+# out from its own build information instead.
142+version_pkg="rickub.com/turbo-editors/turbo-core/version"
143+if describe="$(git -C "$REPO" describe --tags --dirty 2>/dev/null)"; then
144+ commit="$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || true)"
145+ built="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
146+ ldflags="-X '$version_pkg.stamp=$describe' -X '$version_pkg.commit=$commit' -X '$version_pkg.built=$built'"
147+else
148+ ldflags=""
149+fi
150+
151+if ! go build -ldflags "$ldflags" -o "$STAGING/$BINARY" "$REPO" 2>"$STAGING/build.log"; then
152+ cat "$STAGING/build.log" >&2
153+ info ""
154+ warn "A failure in the repository root is often a stray .go file that has"
155+ warn "landed in package main. 'go vet .' names it."
156+ die "build failed; nothing was installed"
157+fi
158+
159+# Running the staged binary is the only proof that the flags above reached the
160+# linker rather than merely looking right. It happens before the install, so a
161+# binary that cannot name its own version never replaces a working one.
162+if ! stamped="$("$REPO/scripts/check-version.sh" "$STAGING/$BINARY" "${describe:-}" "${commit:-}" 2>&1)"; then
163+ info "$stamped"
164+ die "the build did not carry its version; nothing was installed"
165+fi
166+ok "built — $stamped"
167+
168+# --- install ----------------------------------------------------------------
169+
170+step "Installing into $prefix"
171+
172+mkdir -p "$prefix" || die "cannot create $prefix"
173+
174+# Install by renaming a complete file over the target, never by writing into
175+# the one that is there.
176+#
177+# macOS caches a binary's code signature against its inode. cp writes new bytes
178+# into the existing inode, so the cached signature ends up describing something
179+# else and the kernel refuses to execute the result — a reinstall that builds,
180+# installs, and then will not run. A rename gives the name a fresh inode, so
181+# there is nothing stale to cache. It is atomic besides: no moment at which a
182+# half-written turbo-go is on the PATH.
183+#
184+# The temporary has to sit in $prefix, because a rename only works within one
185+# filesystem and $STAGING is somewhere else entirely.
186+readonly INCOMING="$prefix/.$BINARY.incoming.$$"
187+trap 'rm -rf "$STAGING"; rm -f "$INCOMING"' EXIT
188+
189+cp "$STAGING/$BINARY" "$INCOMING" || die "cannot write into $prefix"
190+chmod 0755 "$INCOMING"
191+mv -f "$INCOMING" "$TARGET" || die "cannot replace $TARGET"
192+
193+# Whatever the system said is the useful part: "does not run" on its own tells
194+# nobody anything they can act on.
195+if ! verify="$("$TARGET" -version 2>&1)"; then
196+ info "$verify"
197+ die "the installed binary does not run"
198+fi
199+version="$verify"
200+ok "$version → $TARGET"
201+
202+# --- PATH -------------------------------------------------------------------
203+
204+# on_path reports whether a directory is one the shell searches.
205+on_path() {
206+ case ":${PATH:-}:" in
207+ *":$1:"*) return 0 ;;
208+ *) return 1 ;;
209+ esac
210+}
211+
212+# shell_profile guesses the file that sets PATH for the user's shell.
213+shell_profile() {
214+ case "${SHELL:-}" in
215+ */zsh) printf '~/.zshrc\n' ;;
216+ */fish) printf '~/.config/fish/config.fish\n' ;;
217+ *) printf '~/.bashrc\n' ;;
218+ esac
219+}
220+
221+step "Checking your PATH"
222+if on_path "$prefix"; then
223+ ok "$prefix is on your PATH"
224+else
225+ warn "$prefix is not on your PATH. Add it:"
226+ info ""
227+ info " echo 'export PATH=\"\$PATH:$prefix\"' >> $(shell_profile)"
228+ info " exec \$SHELL"
229+fi
230+
231+# --- the language server ----------------------------------------------------
232+
233+# find_gopls looks where the editor itself looks: PATH, then GOBIN, then
234+# GOPATH/bin.
235+find_gopls() {
236+ command -v gopls 2>/dev/null && return 0
237+ local candidate
238+ for candidate in "$(go env GOBIN)/gopls" "$(go env GOPATH)/bin/gopls"; do
239+ [ -x "$candidate" ] && {
240+ printf '%s\n' "$candidate"
241+ return 0
242+ }
243+ done
244+ return 1
245+}
246+
247+step "Checking the language server"
248+
249+if $with_gopls && ! find_gopls >/dev/null; then
250+ info " installing gopls, which takes a minute…"
251+ go install golang.org/x/tools/gopls@latest || die "could not install gopls"
252+fi
253+
254+if gopls_path="$(find_gopls)"; then
255+ ok "gopls at $gopls_path"
256+else
257+ warn "gopls is not installed, so there will be no completion."
258+ warn "Editing, colouring and themes all work without it."
259+ info ""
260+ info " go install golang.org/x/tools/gopls@latest"
261+ info " ${DIM}or re-run this script with --with-gopls${RESET}"
262+fi
263+
264+# --- what to do next --------------------------------------------------------
265+
266+info ""
267+step "Ready"
268+info ""
269+info " Open a file ${BOLD}inside a Go module${RESET} — completion needs one:"
270+info ""
271+info " cd /path/to/your/project"
272+info " $BINARY main.go"
273+info ""
274+info " ${DIM}F10 menu · F2 save · Alt-X exit · type a '.' for completion${RESET}"
275+info " ${DIM}themes: $BINARY -list-themes${RESET}"
276+info ""
added version_check_test.go +174 -0
new file mode 100644
@@ -0,0 +1,174 @@
1+package main
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+// Linker flags are a string, and a wrong one is not an error: `-X` naming a
12+// symbol that does not exist links happily and stamps nothing, so the binary
13+// falls back to whatever the Go build system knows and reports a version the
14+// build never meant. Nothing but running the binary catches that, which is why
15+// `make build` runs it and why these tests do too.
16+
17+// buildStampedWith compiles the editor with the flags `make ldflags` produces
18+// for a version, and returns the path to the binary.
19+func buildStampedWith(t *testing.T, version string) string {
20+ t.Helper()
21+
22+ flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION="+version).Output()
23+ if err != nil {
24+ t.Fatalf("make ldflags VERSION=%s: %v", version, err)
25+ }
26+
27+ binary := filepath.Join(t.TempDir(), "turbo-go")
28+ build := exec.Command("go", "build", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".")
29+ if out, err := build.CombinedOutput(); err != nil {
30+ t.Fatalf("building with those flags failed: %v\n%s", err, out)
31+ }
32+ return binary
33+}
34+
35+// checkVersion runs the build's version check and returns what it said and
36+// whether it was satisfied.
37+func checkVersion(t *testing.T, args ...string) (string, bool) {
38+ t.Helper()
39+
40+ out, err := exec.Command("./scripts/check-version.sh", args...).CombinedOutput()
41+ return string(out), err == nil
42+}
43+
44+func TestTheVersionCheckAcceptsTheVersionTheBuildStamped(t *testing.T) {
45+ binary := buildStampedWith(t, "v9.9.9")
46+
47+ out, ok := checkVersion(t, binary, "v9.9.9")
48+ if !ok {
49+ t.Fatalf("the check refused a correctly stamped binary:\n%s", out)
50+ }
51+ if !strings.Contains(out, "9.9.9") {
52+ t.Errorf("the check reported %q, want it to name the version", out)
53+ }
54+}
55+
56+func TestTheVersionCheckRefusesAVersionThatMerelyContainsTheRightOne(t *testing.T) {
57+ // The reason this is an equality test and not a grep: "0.2.0" is a
58+ // substring of "10.2.0", so a substring check passes a release that ships
59+ // a binary naming an entirely different version.
60+ binary := buildStampedWith(t, "v10.2.0")
61+
62+ out, ok := checkVersion(t, binary, "v0.2.0")
63+ if ok {
64+ t.Fatalf("the check accepted 10.2.0 as 0.2.0:\n%s", out)
65+ }
66+ if !strings.Contains(out, "10.2.0") {
67+ t.Errorf("the failure does not say what the binary actually reports:\n%s", out)
68+ }
69+}
70+
71+func TestTheVersionCheckRefusesAStampThatNeverReachedTheLinker(t *testing.T) {
72+ // The failure this exists for. `-X` naming a symbol that is not there is
73+ // not an error: the binary links, runs, and reports the wrong thing.
74+ binary := filepath.Join(t.TempDir(), "turbo-go")
75+ build := exec.Command("go", "build",
76+ "-ldflags", "-X 'rickub.com/turbo-editors/turbo-core/version.stampX=v9.9.9'",
77+ "-o", binary, ".")
78+ if out, err := build.CombinedOutput(); err != nil {
79+ t.Fatalf("the build with a misspelt -X failed, so there is nothing to catch: %v\n%s", err, out)
80+ }
81+
82+ out, ok := checkVersion(t, binary, "v9.9.9")
83+ if ok {
84+ t.Fatalf("the check accepted a binary nothing was stamped into:\n%s", out)
85+ }
86+}
87+
88+func TestTheVersionCheckRefusesABinaryThatDoesNotRun(t *testing.T) {
89+ binary := filepath.Join(t.TempDir(), "turbo-go")
90+ if err := os.WriteFile(binary, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
91+ t.Fatalf("cannot write the stand-in: %v", err)
92+ }
93+
94+ if out, ok := checkVersion(t, binary, "v9.9.9"); ok {
95+ t.Fatalf("the check accepted a binary that does not run:\n%s", out)
96+ }
97+}
98+
99+func TestTheVersionCheckNeedsSomethingToCheck(t *testing.T) {
100+ if out, ok := checkVersion(t); ok {
101+ t.Fatalf("the check accepted no arguments at all:\n%s", out)
102+ }
103+ if out, ok := checkVersion(t, filepath.Join(t.TempDir(), "not-there")); ok {
104+ t.Fatalf("the check accepted a path with no binary at it:\n%s", out)
105+ }
106+}
107+
108+func TestAnUnstampedBuildIsAcceptedButAnUnnameableOneIsNot(t *testing.T) {
109+ // Installing from a tarball has no git checkout to describe, so there is no
110+ // version to expect. The claim left is that *some* source named it.
111+ binary := filepath.Join(t.TempDir(), "turbo-go")
112+ build := exec.Command("go", "build", "-o", binary, ".")
113+ if out, err := build.CombinedOutput(); err != nil {
114+ t.Fatalf("an unstamped build failed: %v\n%s", err, out)
115+ }
116+
117+ out, ok := checkVersion(t, binary)
118+ if !ok {
119+ t.Fatalf("the check refused an unstamped build, which is a legitimate one:\n%s", out)
120+ }
121+ if strings.Contains(out, "unknown") {
122+ t.Errorf("the binary cannot name its version and the check passed anyway:\n%s", out)
123+ }
124+}
125+
126+func TestTheBuildTargetChecksWhatItStamped(t *testing.T) {
127+ // The wiring, not the script: a check nothing calls protects nothing.
128+ makefile, err := os.ReadFile("Makefile")
129+ if err != nil {
130+ t.Fatalf("cannot read the Makefile: %v", err)
131+ }
132+ recipe := buildRecipe(t, string(makefile))
133+
134+ if !strings.Contains(recipe, "check-version.sh") {
135+ t.Errorf("the build target never checks the version it stamped:\n%s", recipe)
136+ }
137+}
138+
139+func TestTheInstallerChecksWhatItStampedBeforeItInstalls(t *testing.T) {
140+ // Before, not after: a binary that cannot name its own version must never
141+ // replace one that can.
142+ script, err := os.ReadFile("scripts/install.sh")
143+ if err != nil {
144+ t.Fatalf("cannot read the installer: %v", err)
145+ }
146+ text := string(script)
147+
148+ check := strings.Index(text, "check-version.sh")
149+ install := strings.Index(text, "mv -f")
150+ switch {
151+ case check < 0:
152+ t.Fatal("the installer never checks the version it stamped")
153+ case install < 0:
154+ t.Fatal("the installer no longer installs by rename; this test is out of date")
155+ case check > install:
156+ t.Error("the installer checks the version after installing, so a bad build replaces a good one")
157+ }
158+}
159+
160+// buildRecipe returns the lines of the Makefile's build target.
161+func buildRecipe(t *testing.T, makefile string) string {
162+ t.Helper()
163+
164+ start := strings.Index(makefile, "\nbuild:")
165+ if start < 0 {
166+ t.Fatal("the Makefile has no build target")
167+ }
168+ rest := makefile[start+1:]
169+ end := strings.Index(rest, "\n\n")
170+ if end < 0 {
171+ end = len(rest)
172+ }
173+ return rest[:end]
174+}
new file mode 100644
@@ -0,0 +1,174 @@
1+package main
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+// Linker flags are a string, and a wrong one is not an error: `-X` naming a
12+// symbol that does not exist links happily and stamps nothing, so the binary
13+// falls back to whatever the Go build system knows and reports a version the
14+// build never meant. Nothing but running the binary catches that, which is why
15+// `make build` runs it and why these tests do too.
16+
17+// buildStampedWith compiles the editor with the flags `make ldflags` produces
18+// for a version, and returns the path to the binary.
19+func buildStampedWith(t *testing.T, version string) string {
20+ t.Helper()
21+
22+ flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION="+version).Output()
23+ if err != nil {
24+ t.Fatalf("make ldflags VERSION=%s: %v", version, err)
25+ }
26+
27+ binary := filepath.Join(t.TempDir(), "turbo-go")
28+ build := exec.Command("go", "build", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".")
29+ if out, err := build.CombinedOutput(); err != nil {
30+ t.Fatalf("building with those flags failed: %v\n%s", err, out)
31+ }
32+ return binary
33+}
34+
35+// checkVersion runs the build's version check and returns what it said and
36+// whether it was satisfied.
37+func checkVersion(t *testing.T, args ...string) (string, bool) {
38+ t.Helper()
39+
40+ out, err := exec.Command("./scripts/check-version.sh", args...).CombinedOutput()
41+ return string(out), err == nil
42+}
43+
44+func TestTheVersionCheckAcceptsTheVersionTheBuildStamped(t *testing.T) {
45+ binary := buildStampedWith(t, "v9.9.9")
46+
47+ out, ok := checkVersion(t, binary, "v9.9.9")
48+ if !ok {
49+ t.Fatalf("the check refused a correctly stamped binary:\n%s", out)
50+ }
51+ if !strings.Contains(out, "9.9.9") {
52+ t.Errorf("the check reported %q, want it to name the version", out)
53+ }
54+}
55+
56+func TestTheVersionCheckRefusesAVersionThatMerelyContainsTheRightOne(t *testing.T) {
57+ // The reason this is an equality test and not a grep: "0.2.0" is a
58+ // substring of "10.2.0", so a substring check passes a release that ships
59+ // a binary naming an entirely different version.
60+ binary := buildStampedWith(t, "v10.2.0")
61+
62+ out, ok := checkVersion(t, binary, "v0.2.0")
63+ if ok {
64+ t.Fatalf("the check accepted 10.2.0 as 0.2.0:\n%s", out)
65+ }
66+ if !strings.Contains(out, "10.2.0") {
67+ t.Errorf("the failure does not say what the binary actually reports:\n%s", out)
68+ }
69+}
70+
71+func TestTheVersionCheckRefusesAStampThatNeverReachedTheLinker(t *testing.T) {
72+ // The failure this exists for. `-X` naming a symbol that is not there is
73+ // not an error: the binary links, runs, and reports the wrong thing.
74+ binary := filepath.Join(t.TempDir(), "turbo-go")
75+ build := exec.Command("go", "build",
76+ "-ldflags", "-X 'rickub.com/turbo-editors/turbo-core/version.stampX=v9.9.9'",
77+ "-o", binary, ".")
78+ if out, err := build.CombinedOutput(); err != nil {
79+ t.Fatalf("the build with a misspelt -X failed, so there is nothing to catch: %v\n%s", err, out)
80+ }
81+
82+ out, ok := checkVersion(t, binary, "v9.9.9")
83+ if ok {
84+ t.Fatalf("the check accepted a binary nothing was stamped into:\n%s", out)
85+ }
86+}
87+
88+func TestTheVersionCheckRefusesABinaryThatDoesNotRun(t *testing.T) {
89+ binary := filepath.Join(t.TempDir(), "turbo-go")
90+ if err := os.WriteFile(binary, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
91+ t.Fatalf("cannot write the stand-in: %v", err)
92+ }
93+
94+ if out, ok := checkVersion(t, binary, "v9.9.9"); ok {
95+ t.Fatalf("the check accepted a binary that does not run:\n%s", out)
96+ }
97+}
98+
99+func TestTheVersionCheckNeedsSomethingToCheck(t *testing.T) {
100+ if out, ok := checkVersion(t); ok {
101+ t.Fatalf("the check accepted no arguments at all:\n%s", out)
102+ }
103+ if out, ok := checkVersion(t, filepath.Join(t.TempDir(), "not-there")); ok {
104+ t.Fatalf("the check accepted a path with no binary at it:\n%s", out)
105+ }
106+}
107+
108+func TestAnUnstampedBuildIsAcceptedButAnUnnameableOneIsNot(t *testing.T) {
109+ // Installing from a tarball has no git checkout to describe, so there is no
110+ // version to expect. The claim left is that *some* source named it.
111+ binary := filepath.Join(t.TempDir(), "turbo-go")
112+ build := exec.Command("go", "build", "-o", binary, ".")
113+ if out, err := build.CombinedOutput(); err != nil {
114+ t.Fatalf("an unstamped build failed: %v\n%s", err, out)
115+ }
116+
117+ out, ok := checkVersion(t, binary)
118+ if !ok {
119+ t.Fatalf("the check refused an unstamped build, which is a legitimate one:\n%s", out)
120+ }
121+ if strings.Contains(out, "unknown") {
122+ t.Errorf("the binary cannot name its version and the check passed anyway:\n%s", out)
123+ }
124+}
125+
126+func TestTheBuildTargetChecksWhatItStamped(t *testing.T) {
127+ // The wiring, not the script: a check nothing calls protects nothing.
128+ makefile, err := os.ReadFile("Makefile")
129+ if err != nil {
130+ t.Fatalf("cannot read the Makefile: %v", err)
131+ }
132+ recipe := buildRecipe(t, string(makefile))
133+
134+ if !strings.Contains(recipe, "check-version.sh") {
135+ t.Errorf("the build target never checks the version it stamped:\n%s", recipe)
136+ }
137+}
138+
139+func TestTheInstallerChecksWhatItStampedBeforeItInstalls(t *testing.T) {
140+ // Before, not after: a binary that cannot name its own version must never
141+ // replace one that can.
142+ script, err := os.ReadFile("scripts/install.sh")
143+ if err != nil {
144+ t.Fatalf("cannot read the installer: %v", err)
145+ }
146+ text := string(script)
147+
148+ check := strings.Index(text, "check-version.sh")
149+ install := strings.Index(text, "mv -f")
150+ switch {
151+ case check < 0:
152+ t.Fatal("the installer never checks the version it stamped")
153+ case install < 0:
154+ t.Fatal("the installer no longer installs by rename; this test is out of date")
155+ case check > install:
156+ t.Error("the installer checks the version after installing, so a bad build replaces a good one")
157+ }
158+}
159+
160+// buildRecipe returns the lines of the Makefile's build target.
161+func buildRecipe(t *testing.T, makefile string) string {
162+ t.Helper()
163+
164+ start := strings.Index(makefile, "\nbuild:")
165+ if start < 0 {
166+ t.Fatal("the Makefile has no build target")
167+ }
168+ rest := makefile[start+1:]
169+ end := strings.Index(rest, "\n\n")
170+ if end < 0 {
171+ end = len(rest)
172+ }
173+ return rest[:end]
174+}