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.

tools.go · 271 lines · 8.5 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 16h ago1// Package tools reads the commands a project keeps in its editor directory's
2// tools.toml — its formatter, its linter, its build, its tests — so that a menu
3// can offer them.
4//
5// It knows nothing about menus or terminals: it reads a file and returns
6// values, which is what lets it be tested by writing files and reading them
7// back.
8//
9// list, err := tools.Load(p, ".")
10// if err != nil {
11// return err
12// }
13// for _, tool := range list.Tools() {
14// fmt.Println(tool.Name, "→", tool.Command)
15// }
16package tools
17
18import (
19 "errors"
20 "fmt"
21 "os"
22 "path/filepath"
23 "strings"
24
25 "github.com/BurntSushi/toml"
26
📦 Turbo Core f3ade8d k33g 8h ago27 "rickub.com/turbo-editors/turbo-core/profile"
🛟 Updated. 28d5985 k33g 16h ago28)
29
30// FileName is the file a project keeps its commands in. It sits in the editor's
31// own directory, the same one settings.toml and snippets.toml live in.
32const FileName = "tools.toml"
33
34// ErrExists is returned by Create when the project already has a tools file,
35// so that creating one never silently overwrites what someone wrote.
36var ErrExists = errors.New("tools: project tools already exist")
37
38// Output says where a command's output goes.
39//
40// It is a string rather than a number so the file reads as prose, and an
41// unknown value is refused rather than quietly falling back — a typo in
42// "termnial" should say so, not silently change where the output went.
43type Output string
44
45// The places a command's output can go.
46const (
47 // OutputPopup shows it in a dialog that fills in as the command runs. It
48 // is the default: most commands say something short and are read once.
49 OutputPopup Output = "popup"
50 // OutputTerminal runs the command in a terminal window, which is what a
51 // program that reads the keyboard or takes a long time wants.
52 OutputTerminal Output = "terminal"
53 // OutputEditor puts the finished output in an editing window, so it can be
54 // searched with Ctrl-F and kept beside the code.
55 OutputEditor Output = "editor"
56)
57
58// outputs is every value the file may name, and is what an unknown one is
59// reported against.
60var outputs = []Output{OutputPopup, OutputTerminal, OutputEditor}
61
62// Tool is one command the editor's toolchain menu offers.
63type Tool struct {
64 // Name is what the menu shows.
65 Name string
66 // Command is the shell command to run. It goes to `sh -c`, so pipes and
67 // `&&` work and one entry can be a whole sequence.
68 //
69 // A `{{label}}` in it is a value the editor asks for before running — see
70 // Placeholders and Fill.
71 Command string
72 // Output says where its output goes. Empty means OutputPopup.
73 Output Output
74 // Menu is the menu it appears in. A tool that names none is put into the
75 // editor's own toolchain menu when the file is loaded, so this is never
76 // empty on a Tool that came out of Load.
77 Menu string
78}
79
80// Where returns the tool's output destination, filling in the default.
81//
82// switch tool.Where() {
83// case tools.OutputTerminal:
84// runInATerminalWindow(tool.Command)
85// }
86func (t Tool) Where() Output {
87 if t.Output == "" {
88 return OutputPopup
89 }
90 return t.Output
91}
92
93// List is a project's tools, in the order they were read.
94//
95// The order is the file's, so someone reordering the file sees the menu
96// reorder.
97type List struct {
98 tools []Tool
99 // defaultMenu is the editor's own toolchain menu — "Go", "Rust" — which a
100 // tool that names no menu was put into. It is remembered because that menu
101 // heads MenuNames whether or not any tool asked for it.
102 defaultMenu string
103}
104
105// DefaultMenu returns the menu a tool with no menu of its own was put into.
106func (l List) DefaultMenu() string { return l.defaultMenu }
107
108// Tools returns the commands, in file order.
109func (l List) Tools() []Tool { return l.tools }
110
111// Len returns how many there are.
112func (l List) Len() int { return len(l.tools) }
113
114// MenuNames returns the menus the tools ask for, in the order their first tool
115// appears in the file.
116//
117// The editor's own toolchain menu is always first, whether or not any tool
118// named it: the menu that creates the tools file has to exist even when there
119// is no file.
120//
121// for _, name := range list.MenuNames() {
122// addMenu(name, list.In(name))
123// }
124func (l List) MenuNames() []string {
125 names := []string{l.defaultMenu}
126 seen := map[string]bool{l.defaultMenu: true}
127
128 for _, tool := range l.tools {
129 if !seen[tool.Menu] {
130 seen[tool.Menu] = true
131 names = append(names, tool.Menu)
132 }
133 }
134 return names
135}
136
137// In returns the tools belonging to one menu, in file order.
138func (l List) In(menu string) []Tool {
139 var out []Tool
140 for _, tool := range l.tools {
141 if tool.Menu == menu {
142 out = append(out, tool)
143 }
144 }
145 return out
146}
147
148// Path returns where a project keeps its tools.
149//
150// tools.Path(turboGo, "/src/p") // "/src/p/.turbo-go/tools.toml"
151func Path(p profile.Profile, projectDir string) string {
152 return filepath.Join(projectDir, p.ProjectDir(), FileName)
153}
154
155// Exists reports whether the project has a tools file that can be read.
156//
157// A directory in its place counts as absent: it is not something Load could
158// have read.
159func Exists(p profile.Profile, projectDir string) bool {
160 info, err := os.Stat(Path(p, projectDir))
161 return err == nil && info.Mode().IsRegular()
162}
163
164// Load reads a project's tools.
165//
166// There is no user-level tools file, unlike snippets. Snippets are your habits
167// and should follow you between projects; a project's tools belong to its own
168// toolchain, and a global one would offer `go build` in a Rust repository.
169//
170// A missing file is not an error — a project that has never asked for one has
171// none. A file that is present but unreadable *is* an error, so a typo is
172// reported rather than silently leaving the menu empty.
173//
174// A tool that names no menu is put into the editor's own toolchain menu here,
175// so that nothing downstream has to remember what the default was.
176//
177// list, err := tools.Load(p, ".")
178func Load(p profile.Profile, projectDir string) (List, error) {
179 path := Path(p, projectDir)
180 defaultMenu := DefaultMenuName(p)
181
182 data, err := os.ReadFile(path)
183 if err != nil {
184 if errors.Is(err, os.ErrNotExist) {
185 return List{defaultMenu: defaultMenu}, nil
186 }
187 return List{defaultMenu: defaultMenu}, fmt.Errorf("reading %s: %w", path, err)
188 }
189
190 var f file
191 if _, err := toml.Decode(string(data), &f); err != nil {
192 return List{defaultMenu: defaultMenu}, fmt.Errorf("reading %s: %w", path, err)
193 }
194 if err := check(f.Tool, path); err != nil {
195 return List{defaultMenu: defaultMenu}, err
196 }
197
198 for i := range f.Tool {
199 if f.Tool[i].Menu == "" {
200 f.Tool[i].Menu = defaultMenu
201 }
202 }
203 return List{tools: f.Tool, defaultMenu: defaultMenu}, nil
204}
205
206// DefaultMenuName is the plain name of the editor's toolchain menu: the
207// profile's label with its hot-key markers taken out.
208//
209// A tools file writes the plain name — `menu = "Go"` — because a hot key is the
210// editor's to assign, not the file's.
211//
212// tools.DefaultMenuName(profile.Profile{ToolsMenu: "~G~o"}) // "Go"
213// tools.DefaultMenuName(profile.Profile{ToolsMenu: "Rus~t~"}) // "Rust"
214func DefaultMenuName(p profile.Profile) string {
215 return strings.ReplaceAll(p.ToolsMenu, "~", "")
216}
217
218// file mirrors the tools file's structure.
219type file struct {
220 Tool []Tool `toml:"tool"`
221}
222
223// check refuses a tool that could not be shown, could not be run, whose output
224// has nowhere to go, or whose command asks for a value it does not name.
225//
226// One with no name has nothing to put in a menu; one with no command has
227// nothing to do; one naming an output that does not exist is a typo whose
228// silent correction would send the output somewhere the file did not ask for;
229// and a half-typed placeholder would otherwise reach the shell with its braces
230// still in it.
231func check(list []Tool, path string) error {
232 for i, tool := range list {
233 if tool.Name == "" {
234 return fmt.Errorf("reading %s: tool %d has no name", path, i+1)
235 }
236 if tool.Command == "" {
237 return fmt.Errorf("reading %s: tool %q has no command", path, tool.Name)
238 }
239 if !knownOutput(tool.Output) {
240 return fmt.Errorf("reading %s: tool %q has output %q; want one of %s",
241 path, tool.Name, tool.Output, outputNames())
242 }
243 if err := checkPlaceholders(tool, path); err != nil {
244 return err
245 }
246 }
247 return nil
248}
249
250// knownOutput reports whether an output value is one this package understands.
251// The empty string is, and means the default.
252func knownOutput(output Output) bool {
253 if output == "" {
254 return true
255 }
256 for _, known := range outputs {
257 if output == known {
258 return true
259 }
260 }
261 return false
262}
263
264// outputNames lists the valid outputs for an error message.
265func outputNames() string {
266 names := make([]string, len(outputs))
267 for i, output := range outputs {
268 names[i] = string(output)
269 }
270 return strings.Join(names, ", ")
271}