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

toolmenus.go · 165 lines · 4.9 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1// The menus the project's tools file asks for: the editor's own toolchain menu,
2// and any others a tool names for itself.
3
4package app
5
6import (
7 "os"
8 "time"
9 "unicode"
10
📦 Turbo Core f3ade8d k33g 10h ago11 "rickub.com/turbo-editors/turbo-core/tools"
12 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g 17h ago13)
14
15// allMenus returns the whole bar: the fixed menus, then the ones the project's
16// tools file asks for, then Help.
17//
18// Help stays last because that is where Turbo C put it and where a reader
19// looks for it. The project's own menus land between it and Go, which is where
20// a menu grown out of the file belongs — after everything the editor itself
21// offers.
22func (a *App) allMenus() []*ui.Menu {
23 fixed := []*ui.Menu{
24 a.fileMenu(),
25 a.editMenu(),
26 a.searchMenu(),
27 a.runMenu(),
28 a.codeMenu(),
29 a.optionsMenu(),
30 a.windowMenu(),
31 a.snippetsMenu(),
32 a.agentMenu(),
33 a.toolchainMenu(),
34 }
35 help := a.helpMenu()
36
37 menus := make([]*ui.Menu, 0, len(fixed)+1)
38 menus = append(menus, fixed...)
39 menus = append(menus, a.toolMenus(takenHotKeys(append(fixed, help)))...)
40 return append(menus, help)
41}
42
43// toolMenus returns one menu per name the tools file uses, in the order the
44// names first appear in it, skipping the editor's own because it is already on
45// the bar.
46//
47// A file that cannot be read gives no menus at all. The toolchain menu shows
48// the error instead: a broken file should say so once, not once per menu it was
49// about to create.
50func (a *App) toolMenus(taken map[rune]bool) []*ui.Menu {
51 list, err := a.loadTools()
52 if err != nil {
53 return nil
54 }
55
56 var menus []*ui.Menu
57 for _, name := range list.MenuNames() {
58 if name == a.toolsMenuName() {
59 continue
60 }
61 menus = append(menus, a.toolMenu(name, hotKeyLabel(name, taken)))
62 }
63 return menus
64}
65
66// toolMenu returns a menu holding the tools that named it.
67//
68// Like the toolchain menu, its items come from OnOpen: they are read from a
69// file that changes while the editor runs.
70func (a *App) toolMenu(name, label string) *ui.Menu {
71 menu := &ui.Menu{Label: label}
72 menu.OnOpen = func() { menu.Items = a.commandItems(name) }
73 return menu
74}
75
76// takenHotKeys returns the hot keys the given menus already claim.
77func takenHotKeys(menus []*ui.Menu) map[rune]bool {
78 taken := make(map[rune]bool, len(menus))
79 for _, menu := range menus {
80 if _, key, index := ui.SplitHotKey(menu.Label); index >= 0 {
81 taken[unicode.ToLower(key)] = true
82 }
83 }
84 return taken
85}
86
87// hotKeyLabel marks a menu name's first letter that no other menu has claimed,
88// and reserves it.
89//
90// The bar answers the *first* menu whose hot key matches, so two menus sharing
91// one silently make the second unreachable. A name written with the marker
92// already in it — `menu = "~T~ools"` — keeps its own letter when it is free;
93// when it is not, and when every letter of the name is spoken for, the menu
94// gets no hot key at all rather than one that opens something else. F10 and
95// the mouse still reach it.
96//
97// taken := map[rune]bool{'f': true}
98// hotKeyLabel("Format", taken) // "F~o~rmat"
99func hotKeyLabel(name string, taken map[rune]bool) string {
100 if plain, key, index := ui.SplitHotKey(name); index >= 0 {
101 if lower := unicode.ToLower(key); !taken[lower] {
102 taken[lower] = true
103 return name
104 }
105 name = plain // their letter is spoken for; find another
106 }
107
108 runes := []rune(name)
109 for i, r := range runes {
110 lower := unicode.ToLower(r)
111 if !unicode.IsLetter(r) || taken[lower] {
112 continue
113 }
114 taken[lower] = true
115 marker := string(ui.HotKeyMarker)
116 return string(runes[:i]) + marker + string(r) + marker + string(runes[i+1:])
117 }
118 return name
119}
120
121// fileStamp is what a file looked like at a moment: enough to tell that it has
122// changed without reading it.
123type fileStamp struct {
124 exists bool
125 size int64
126 modTime time.Time
127}
128
129// stampOf describes a file. A file that is not there stamps as the zero value,
130// so appearing and disappearing both register as changes.
131func stampOf(path string) fileStamp {
132 info, err := os.Stat(path)
133 if err != nil {
134 return fileStamp{}
135 }
136 return fileStamp{exists: true, size: info.Size(), modTime: info.ModTime()}
137}
138
139// toolsFileStamp describes the project's tools file.
140func (a *App) toolsFileStamp() fileStamp {
141 working, err := os.Getwd()
142 if err != nil {
143 return fileStamp{}
144 }
145 return stampOf(tools.Path(a.profile, working))
146}
147
148// refreshToolMenus puts the bar back in step with the tools file.
149//
150// Menu.OnOpen refills a menu's items, but the *set* of menus comes from the
151// file as well: adding `menu = "Docker"` to it should put a Docker menu on the
152// bar without restarting the editor. Rebuilding is guarded by the file's size
153// and modification time, so the usual case — nothing changed — costs one stat
154// and no parsing.
155//
156// It runs at the top of the event loop, with the rest of the state-driven
157// work, because rebuilding the bar from a goroutine would race the draw.
158func (a *App) refreshToolMenus() {
159 stamp := a.toolsFileStamp()
160 if stamp == a.toolsStamp {
161 return
162 }
163 a.toolsStamp = stamp
164 a.menu.SetMenus(a.allMenus())
165}