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:
onLineis a parameter ofStart, not a field.Startbegins the goroutine that calls it, so a field assigned afterwards would be a data race — the exact oneterminal.ViewOptionswas 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 "<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.
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 |
<projectDir>/<p.ProjectDir()>/tools.toml |
ErrExists |
The one condition callers act on rather than report |
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
make test
go test ./tools/
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 118 119 120 121 122 123 124 125 126 127 |
|