turbo-editors/turbo-gopublic Fork 0
3d7798bf187500c07c52e35cbc80f30990dbd391
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 3d7798b · on 3d7798bf187500c07c52e35cbc80f30990dbd391 · k33g · 13h ago
go-tools.md · 117 lines · 13.4 KBmarkdown
Blame HistoryOpen raw

Go tools — explanation

What is this about?

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.

Why the output has three places to go, and a popup by default

The first version put every command in a terminal window, and it was the wrong default for four of the five.

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.

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: 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.

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.

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.

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.

Why a terminal window is still there

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.

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.

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.

Why the exit code is always in the title

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.

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.

Why the commands are in a file

Five commands hardwired into the editor would have answered the request. They would also have been wrong within a week.

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.

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.

Commands go to sh -ccmd.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.

Why there is no user-level tools file

Snippets are read from two files — yours and the project's — because your snippets are your habits and should follow you between projects.

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.

Why a tool may name its own menu

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.

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.

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.

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.

Why the hot key is not the file's to choose

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.

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.

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.

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.

Why the bar is rebuilt from a stat

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.

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.

Why open files are re-read, and only some of them

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.

So when a command finishes, the editor re-reads every open file. The interesting part is which ones it refuses to touch.

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.

Two smaller decisions inside that:

  • 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.
  • 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.

Why the reload happens on the event loop

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.

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.

Why a command can ask for a value, and why it asks in double braces

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.

So a {{label}} in a command is a value the editor asks for first, in a box titled after the tool.

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.

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.

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.

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.

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.

How it relates to the rest

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# Go tools — explanation

## What is this about?

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.

## Why the output has three places to go, and a popup by default

The first version put every command in a terminal window, and it was the wrong default for four of the five.

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.

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.

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.

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.

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.

## Why a terminal window is still there

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.

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.

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.

## Why the exit code is always in the title

`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.

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.

## Why the commands are in a file

Five commands hardwired into the editor would have answered the request. They would also have been wrong within a week.

`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.

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.

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.

## Why there is no user-level tools file

Snippets are read from two files — yours and the project's — because your snippets are your habits and should follow you between projects.

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.

## Why a tool may name its own menu

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.

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.

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.

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.

## Why the hot key is not the file's to choose

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.

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.

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.

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.

## Why the bar is rebuilt from a stat

`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.

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.

## Why open files are re-read, and only some of them

`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.

So when a command finishes, the editor re-reads every open file. The interesting part is which ones it refuses to touch.

**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.

Two smaller decisions inside that:

- **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.
- **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.

## Why the reload happens on the event loop

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.

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.

## Why a command can ask for a value, and why it asks in double braces

`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.

So a `{{label}}` in a command is a value the editor asks for first, in a box titled after the tool.

**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.

**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.

**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.

**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.

**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.

## How it relates to the rest

- Every key of the file and every rule: [Go tools reference](../reference/go-tools.md)
- Using it: [How to run go commands from the editor](../how-to/run-go-commands.md)
- The windows `output = "terminal"` uses, and why they are real terminals: [Terminal windows](terminal-windows.md)
- The other menu built from a file: [Snippets](snippets.md)