turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md · 127 lines · 8.9 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 13h ago1# tools
2
3Reads 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
5Imports 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
9Unlike 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
11A 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
21The 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
23This 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
29They 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
31Each 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
37This 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
43Two 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
89list, err := tools.Load(".")
90if err != nil {
91 return err // the file is there but unreadable, which is worth saying
92}
93for _, name := range list.MenuNames() {
94 addMenu(name)
95}
96for _, 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
111A `{{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
120Double 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
125make test
126go test ./tools/
127```