# History *Append only. One dated entry per session. Never rewrite or delete an entry, including your own from an earlier turn.* ## 2026-08-30 — Turbo C-style Go editor, built from an empty repository - **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. - **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/`. - **Decisions**: - **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. - **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. - **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. - **LSP client written by hand** rather than taking `go.lsp.dev/jsonrpc2` — about 300 lines, and it keeps the total dependency count at two. - **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. - **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. - **`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. - **Bugs found and fixed while building**: - `MoveWordLeft` stepped left before scanning, so it skipped a word when the cursor sat just after one. - `theme.LoadFile` restarted the inheritance depth at zero, so a two-theme `inherits` loop blew the stack instead of being reported. - `closeBoxLabel = "[■]"` was measured with `len()` — 5 bytes for 3 columns — so clicks two cells past the close box closed the window. - `InputLine` swallowed `Alt`-letter, which stopped a dialog's buttons from ever seeing their own shortcuts. - The status bar drew its hints and its right-aligned text over each other. - `buffer.New()` claimed an empty buffer ends with a newline, so every new file gained a stray `"\n"`. - **`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. - **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`. - **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`. - **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. - **Not done**: never run in a real terminal; only Linux/arm64 exercised; no CI; diagnostics stored but only surfaced on the status bar. ## 2026-08-30 (later) — two defects found by running it for real - **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.`. - **Changes**: - `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. - `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. - `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. - `internal/editor/view.go` — the cursor cell is painted in a new `editor.cursor` theme key; `View` embeds `ui.FocusBox`. - `internal/ui/window.go` — `SetActive` propagates the focus to a content widget that can hold it. - `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. - Three theme files, the theme reference and the theme how-to, in both languages. - **Decisions**: - **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. - **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. - **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". - **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. - **Quality**: gate back to **PASS** after restating `Dialog.HandleKey`. Run 6: 0 errors, 0 warnings, 0 smells. - **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. ## 2026-08-30 (third) — the completion fix was still droppable, and an empty list said nothing - **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. - **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. - **Changes**: - `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. - `internal/app/complete.go` — an empty list now names the reason: `No completions — this file does not compile: `. - `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. - `internal/app/language.go` — records the server path, the root, and which documents have been announced; `Report()` and `Knows()` expose it. - Both `enable-completion` how-to guides gained a section on the two ways completion looks broken when it is not. - **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. - **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. - **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. - **Quality**: gate PASS, run 7. 0 errors, 0 warnings, 0 smells. ## 2026-08-30 (fourth) — an installer, so the editor can be used on real projects - **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. - **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`. - **Decisions**: - **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. - **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. - **The required Go version is read from `go.mod`**, not hardcoded, so the check cannot drift from the build. - **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. - **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. - **Note**: the user removed their own `hello.go` from the repository root, so `go build ./...` compiles again and the root package's tests run. ## 2026-08-30 (fifth) — windows follow the terminal - **Goal**: the user asked that the main window be resizable when the terminal changes size. - **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. - **Changes**: - `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. - `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. - `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. - `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. - **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. - **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. - **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. - **Quality**: gate PASS. ## 2026-08-30 (sixth) — the cursor, properly this time - **Goal**: the user reported that the cursor is still invisible under `turbo-dark`, after the earlier fix. - **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. - **Changes**: - `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;`, so the theme now decides the terminal's own cursor. The painted cell stays as a fallback for terminals that support neither. - `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`. - `internal/theme/themes/borland-light.toml` — same defect, `#f4f4f4` on `#ffffff`, eleven values. Now `#e8e8e8`. - **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. - **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. - **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. ## 2026-08-31 — the release build staged nothing, then built for five platforms - **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. - **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. - **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---`, 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. - **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. - **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. ## 2026-08-31 (later) — the upload script, adapted to a Go release - **Goal**: the user asked that `04-release.upload-binaries.sh` be fixed and adapted, after two rounds of flagging that it uploaded nothing. - **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*. - **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. - **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. - **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. - **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. ## 2026-08-31 — Terminal windows (ticket 0007) - **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. - **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. - **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). - **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. - **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. - **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. - **Also**: created ticket `0015` for the Windows/ConPTY port. Ticket `0007` was left `open` — closing it is the user's call. Nothing was committed. ## 2026-08-31 — Project settings, autosave and TOML colouring (ticket 0002) - **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. - **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. - **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. - **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. - **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. - **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. - **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. - **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. ## 2026-08-31 — Window frame boxes: [x] to close, [■] to maximise - **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** (`[■]` → `[▬]`). - **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. - **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. - **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). - **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`. - **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`. - **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. - **Quality**: PASS first time. 0 errors, 0 warnings, 0 smells, complexity 1156. - **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. - **Context**: project settings were merged to `main` as PR #2 before this session; this work sits on `feature/windows-buttons`, uncommitted. ## 2026-08-31 — Bug fix: OK did nothing in the Open dialog - **Goal**: user report — "quand on ouvre un fichier, dans la popup le bouton OK ne semble pas fonctionner (click souris ou focus et entree)". - **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. - **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)`. - **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. - **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 %. - **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. - **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. - **Quality**: PASS. 0 errors, 0 warnings, 0 smells, complexity 1160. - **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). ## 2026-08-31 — Project tree window (ticket 0003) - **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. - **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. - **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). - **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. - **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`. - **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. - **Quality**: PASS first time. 0 errors, 0 warnings, 0 smells, complexity 1228. - **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. ## 2026-08-31 — Markdown, JavaScript, HTML and shell colouring (tickets 0009, 0013, 0014) - **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**. - **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)`. - **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 `