| 🛟 Updated. 28d5985 k33g 16h ago | 1 | # tools |
| 2 | |
| 3 | Reads the commands a project runs on itself — its formatter, its linter, its build, its tests — from `tools.toml` inside the editor's own directory — `.turbo-go` for Turbo Go, `.turbo-rust` for Turbo Rust — and runs one. |
| 4 | |
| 5 | Imports the standard library, the TOML parser and `projectfile`, and nothing else: no tcell, no `ui`. The file half is tested by writing files and reading them back; the running half by running real commands and comparing what came out. |
| 6 | |
| 7 | ## One file, and no user-level one |
| 8 | |
| 9 | Unlike snippets, there is **no** user-level tools file. Snippets are your habits and should follow you between projects; a project's tools belong to its own toolchain, and a global one would offer `go build ./...` in a Rust repository. |
| 10 | |
| 11 | A missing file is not an error — a project that never asked for one has none. A file that is **present but unreadable is** an error, so a typo is reported rather than silently leaving the menu empty. |
| 12 | |
| 13 | ## Order is the file's |
| 14 | |
| 15 | `Tools()` returns them as read, so someone reordering the file sees the menu reorder. `MenuNames()` follows the same rule for menus: a name appears where its first tool does. |
| 16 | |
| 17 | ## Which menu a tool is in |
| 18 | |
| 19 | `menu` is a **free-form name**, and absent means the editor's own toolchain menu — `Go` in Turbo Go, `Rust` in Turbo Rust, whichever the profile names. `Load` fills that in, so a `Tool` that came out of it always has a `Menu`. A name nothing else uses simply creates a menu — there is no list of allowed values to check against, because a list would be a list of somebody else's projects. `In(name)` is one menu's tools; `MenuNames()` is every menu the file asks for. |
| 20 | |
| 21 | The editor's own menu is always first in `MenuNames()`, whether or not any tool named it. It holds the item that *creates* the tools file, so it has to exist in a project that has none — which is exactly the project that needs it. |
| 22 | |
| 23 | This package does not know what a menu looks like, or that hot keys exist. `app` assigns those, because only the bar knows which letters are taken. |
| 24 | |
| 25 | ## The five defaults are data, not code |
| 26 | |
| 27 | `Create` writes a starter file holding `gofmt -l -w .`, `go vet ./...`, `go build ./...`, `go test ./...` and `go run .` — the commands a Go project runs before it commits, in the order you would run them. |
| 28 | |
| 29 | They are written down rather than compiled in because they are wrong for plenty of projects: `go vet` is the default linter only because it ships with the toolchain, `go run .` assumes the main package is at the root, and a project with a `Makefile` wants `make check`. Changing one is editing a file. |
| 30 | |
| 31 | Each carries a hot key written with tildes (`"~T~est"`), and `TestTheCreatedToolsCarryHotKeys` checks no two clash — the menu answers the first match it finds. |
| 32 | |
| 33 | ## Where the output goes |
| 34 | |
| 35 | `output` names one of `popup`, `terminal` or `editor`, and an absent one means `popup`. An **unknown** one is refused rather than corrected: `"termnial"` falling back silently would look as though it had worked while sending the output somewhere the file did not ask for. |
| 36 | |
| 37 | This package does not know what any of those three are — it only says which was asked for. `app` decides what a popup looks like. |
| 38 | |
| 39 | ## Running one |
| 40 | |
| 41 | `Start` runs a command through the platform's shell — `sh -c` on Unix, `cmd.exe /S /C` on Windows — **without a pty**, merging standard error into standard output in the order the command wrote them, and reads it on a goroutine of its own. `Run` is that command in flight: `Lines()`, `Done()`, `Dropped()` and `Stop()`. |
| 42 | |
| 43 | Two things about it are deliberate: |
| 44 | |
| 45 | - **`onLine` is a parameter of `Start`, not a field.** `Start` begins the goroutine that calls it, so a field assigned afterwards would be a data race — the exact one `terminal.ViewOptions` was created to fix, and not one to reintroduce in a new package. |
| 46 | - **Output is capped at 10000 lines**, oldest first, with `Dropped()` reporting how many went. A runaway command would otherwise grow a dialog until it grew the editor; the tail of a failing build is the part that matters, so the head is what goes, and the caller is told rather than quietly shown less. |
| 47 | |
| 48 | `output = "terminal"` does not come through here at all: `app` hands that to `terminal` instead, using `Shell()` and `ShellArgs()` from this package so both spell the shell the same way. |
| 49 | |
| 50 | **Stopping a command stops what it started.** On Unix the command gets a process group of its own (`Setpgid`) and `Stop` kills the group; on Windows it is assigned to a **job object** created with `KILL_ON_JOB_CLOSE`, and `Stop` terminates the job. The Windows assignment happens after the process has started, so a child spawned in those first milliseconds escapes — `exec.Cmd` has no way to start a process suspended. Both live behind the `group` interface in `run.go`; `shell_unix.go`, `shell_windows.go` and `group_other.go` are the three implementations. |
| 51 | |
| 52 | **cmd.exe reads its command line by its own rules**, not the C runtime's, so on Windows the line is composed here — `"cmd.exe" /S /C "<command>"`, the command verbatim inside one pair of quotes that `/S` tells cmd.exe to strip — and handed to `exec` through `SysProcAttr.CmdLine`. Go's own composition would turn a `"` inside the command into `\"`, which cmd.exe reads as a backslash and a quote. The Windows path compiles and passes `go vet`, and has never been run by this project's authors. |
| 53 | |
| 54 | ## A command is a command line |
| 55 | |
| 56 | `Command` goes to `sh -c` (or `cmd.exe /S /C`), so pipes, globs, `&&` and `;` all work and one entry can be `gofmt -l -w . && go vet ./... && go test ./...`. This package does not split an argv, because that would mean inventing quoting rules for a string somebody wrote by hand. |
| 57 | |
| 58 | ## Public API |
| 59 | |
| 60 | | Name | What it does | |
| 61 | | --- | --- | |
| 62 | | `FileName` | `"tools.toml"` | |
| 63 | | `Tool{Name, Command, Output, Menu}` | One command the menu offers | |
| 64 | | `(Tool) Where() Output` | Its output destination, with the default filled in | |
| 65 | | `(Tool) Menu` | The menu it belongs to. `Load` fills in the editor's own when the file names none. | |
| 66 | | `DefaultMenuName(p) string` | `p.ToolsMenu` with its hot-key markers taken out: `"~G~o"` → `"Go"` | |
| 67 | | `Output`, `OutputPopup`, `OutputTerminal`, `OutputEditor` | Where a command's output goes | |
| 68 | | `Shell() string` | The program every command is handed to: `/bin/sh` on Unix, `%COMSPEC%` — cmd.exe — on Windows | |
| 69 | | `ShellArgs(command) []string` | What makes that shell run one command line and exit: `["-c", command]` or `["/S", "/C", command]` | |
| 70 | | `Start(command, dir string, onLine func()) (*Run, error)` | Runs a command with its output captured | |
| 71 | | `(*Run) Lines() []string` | What it has printed, as a copy | |
| 72 | | `(*Run) Done() (bool, int)` | Whether it ended, and with what code | |
| 73 | | `(*Run) Dropped() int` | How many lines were thrown away for being too old | |
| 74 | | `(*Run) Stop()` | Kill it; safe on one that has already ended | |
| 75 | | `(*Run) Command() string` | The command line being run | |
| 76 | | `List` | A project's tools, in file order | |
| 77 | | `(List) Tools() []Tool` | Them, in order | |
| 78 | | `(List) In(menu string) []Tool` | One menu's tools, in file order | |
| 79 | | `(List) MenuNames() []string` | Every menu the file asks for, the editor's own first, then file order | |
| 80 | | `(List) DefaultMenu() string` | The menu a tool with none of its own was put into | |
| 81 | | `(List) Len() int` | How many there are | |
| 82 | | `Load(projectDir) (List, error)` | Reads the file; a missing one is not an error | |
| 83 | | `Create(projectDir) (string, error)` | Writes the starter file; `ErrExists` rather than overwriting | |
| 84 | | `Exists(projectDir) bool` | Whether there is a regular file there to read | |
| 85 | | `Path(p, projectDir) string` | `<projectDir>/<p.ProjectDir()>/tools.toml` | |
| 86 | | `ErrExists` | The one condition callers act on rather than report | |
| 87 | |
| 88 | ```go |
| 89 | list, err := tools.Load(".") |
| 90 | if err != nil { |
| 91 | return err // the file is there but unreadable, which is worth saying |
| 92 | } |
| 93 | for _, name := range list.MenuNames() { |
| 94 | addMenu(name) |
| 95 | } |
| 96 | for _, tool := range list.Tools() { |
| 97 | addMenuItem(tool.Menu, tool.Name, func() { |
| 98 | if tool.Where() == tools.OutputTerminal { |
| 99 | runInATerminalWindow(tool.Command) |
| 100 | return |
| 101 | } |
| 102 | run, err := tools.Start(tool.Command, ".", wakeTheEventLoop) |
| 103 | _ = run |
| 104 | _ = err |
| 105 | }) |
| 106 | } |
| 107 | ``` |
| 108 | |
| 109 | ## Asking for a value |
| 110 | |
| 111 | A `{{label}}` in a command is a value the editor asks for before running it. This package parses them and fills them in; the box is `app`'s. |
| 112 | |
| 113 | | Name | What it does | |
| 114 | | --- | --- | |
| 115 | | `Placeholder{Label, Raw}` | One value a command asks for. `Raw` means it goes in verbatim rather than shell-quoted. | |
| 116 | | `(Tool) Placeholders() []Placeholder` | What a command asks for, in order, a repeated label once | |
| 117 | | `(Tool) Fill(values) string` | The command with every placeholder replaced | |
| 118 | | `ShellQuote(value) string` | One argument, whatever is in it | |
| 119 | |
| 120 | Double braces, because single ones appear in real commands — `awk '{print $1}'` and `find . -exec rm {} +` are both ordinary things to put in a tools file. `Load` refuses an unclosed `{{` or a placeholder with no label, so a `Tool` that came from a file always parses. |
| 121 | |
| 122 | ## Tests |
| 123 | |
| 124 | ```sh |
| 125 | make test |
| 126 | go test ./tools/ |
| 127 | ``` |