turbo-editors/turbo-corepublic Fork 0
v1.0.2
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.

🛟 Updated. 28d5985 · on v1.0.2 · k33g · 19h ago
README.md · 127 lines · 8.9 KBmarkdown
Blame HistoryOpen raw

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 "<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
# 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 "<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 |

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