# tools 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. 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. ## One file, and no user-level one 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. 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. ## Order is the file's `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. ## Which menu a tool is in `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. 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. 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. ## The five defaults are data, not code `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. 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. Each carries a hot key written with tildes (`"~T~est"`), and `TestTheCreatedToolsCarryHotKeys` checks no two clash — the menu answers the first match it finds. ## Where the output goes `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. 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. ## Running one `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()`. Two things about it are deliberate: - **`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. - **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. `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. **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. **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 ""`, 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. ## A command is a command line `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. ## Public API | Name | What it does | | --- | --- | | `FileName` | `"tools.toml"` | | `Tool{Name, Command, Output, Menu}` | One command the menu offers | | `(Tool) Where() Output` | Its output destination, with the default filled in | | `(Tool) Menu` | The menu it belongs to. `Load` fills in the editor's own when the file names none. | | `DefaultMenuName(p) string` | `p.ToolsMenu` with its hot-key markers taken out: `"~G~o"` → `"Go"` | | `Output`, `OutputPopup`, `OutputTerminal`, `OutputEditor` | Where a command's output goes | | `Shell() string` | The program every command is handed to: `/bin/sh` on Unix, `%COMSPEC%` — cmd.exe — on Windows | | `ShellArgs(command) []string` | What makes that shell run one command line and exit: `["-c", command]` or `["/S", "/C", command]` | | `Start(command, dir string, onLine func()) (*Run, error)` | Runs a command with its output captured | | `(*Run) Lines() []string` | What it has printed, as a copy | | `(*Run) Done() (bool, int)` | Whether it ended, and with what code | | `(*Run) Dropped() int` | How many lines were thrown away for being too old | | `(*Run) Stop()` | Kill it; safe on one that has already ended | | `(*Run) Command() string` | The command line being run | | `List` | A project's tools, in file order | | `(List) Tools() []Tool` | Them, in order | | `(List) In(menu string) []Tool` | One menu's tools, in file order | | `(List) MenuNames() []string` | Every menu the file asks for, the editor's own first, then file order | | `(List) DefaultMenu() string` | The menu a tool with none of its own was put into | | `(List) Len() int` | How many there are | | `Load(projectDir) (List, error)` | Reads the file; a missing one is not an error | | `Create(projectDir) (string, error)` | Writes the starter file; `ErrExists` rather than overwriting | | `Exists(projectDir) bool` | Whether there is a regular file there to read | | `Path(p, projectDir) string` | `//tools.toml` | | `ErrExists` | The one condition callers act on rather than report | ```go list, err := tools.Load(".") if err != nil { return err // the file is there but unreadable, which is worth saying } for _, name := range list.MenuNames() { addMenu(name) } for _, tool := range list.Tools() { addMenuItem(tool.Menu, tool.Name, func() { if tool.Where() == tools.OutputTerminal { runInATerminalWindow(tool.Command) return } run, err := tools.Start(tool.Command, ".", wakeTheEventLoop) _ = run _ = err }) } ``` ## Asking for a value 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. | Name | What it does | | --- | --- | | `Placeholder{Label, Raw}` | One value a command asks for. `Raw` means it goes in verbatim rather than shell-quoted. | | `(Tool) Placeholders() []Placeholder` | What a command asks for, in order, a repeated label once | | `(Tool) Fill(values) string` | The command with every placeholder replaced | | `ShellQuote(value) string` | One argument, whatever is in it | 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. ## Tests ```sh make test go test ./tools/ ```