turbo-editors/turbo-corepublic Fork 0
d662cebdb65b319885da903daf7eff9ab1bfbb78
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 d662cebdb65b319885da903daf7eff9ab1bfbb78 · k33g · 21h ago
toolmenus.go · 165 lines · 4.9 KBGo Blame HistoryRaw
  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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// The menus the project's tools file asks for: the editor's own toolchain menu,
// and any others a tool names for itself.

package app

import (
	"os"
	"time"
	"unicode"

	"codeberg.org/turbo-editors/turbo-core/tools"
	"codeberg.org/turbo-editors/turbo-core/ui"
)

// allMenus returns the whole bar: the fixed menus, then the ones the project's
// tools file asks for, then Help.
//
// Help stays last because that is where Turbo C put it and where a reader
// looks for it. The project's own menus land between it and Go, which is where
// a menu grown out of the file belongs — after everything the editor itself
// offers.
func (a *App) allMenus() []*ui.Menu {
	fixed := []*ui.Menu{
		a.fileMenu(),
		a.editMenu(),
		a.searchMenu(),
		a.runMenu(),
		a.codeMenu(),
		a.optionsMenu(),
		a.windowMenu(),
		a.snippetsMenu(),
		a.agentMenu(),
		a.toolchainMenu(),
	}
	help := a.helpMenu()

	menus := make([]*ui.Menu, 0, len(fixed)+1)
	menus = append(menus, fixed...)
	menus = append(menus, a.toolMenus(takenHotKeys(append(fixed, help)))...)
	return append(menus, help)
}

// toolMenus returns one menu per name the tools file uses, in the order the
// names first appear in it, skipping the editor's own because it is already on
// the bar.
//
// A file that cannot be read gives no menus at all. The toolchain menu shows
// the error instead: a broken file should say so once, not once per menu it was
// about to create.
func (a *App) toolMenus(taken map[rune]bool) []*ui.Menu {
	list, err := a.loadTools()
	if err != nil {
		return nil
	}

	var menus []*ui.Menu
	for _, name := range list.MenuNames() {
		if name == a.toolsMenuName() {
			continue
		}
		menus = append(menus, a.toolMenu(name, hotKeyLabel(name, taken)))
	}
	return menus
}

// toolMenu returns a menu holding the tools that named it.
//
// Like the toolchain menu, its items come from OnOpen: they are read from a
// file that changes while the editor runs.
func (a *App) toolMenu(name, label string) *ui.Menu {
	menu := &ui.Menu{Label: label}
	menu.OnOpen = func() { menu.Items = a.commandItems(name) }
	return menu
}

// takenHotKeys returns the hot keys the given menus already claim.
func takenHotKeys(menus []*ui.Menu) map[rune]bool {
	taken := make(map[rune]bool, len(menus))
	for _, menu := range menus {
		if _, key, index := ui.SplitHotKey(menu.Label); index >= 0 {
			taken[unicode.ToLower(key)] = true
		}
	}
	return taken
}

// hotKeyLabel marks a menu name's first letter that no other menu has claimed,
// and reserves it.
//
// The bar answers the *first* menu whose hot key matches, so two menus sharing
// one silently make the second unreachable. A name written with the marker
// already in it — `menu = "~T~ools"` — keeps its own letter when it is free;
// when it is not, and when every letter of the name is spoken for, the menu
// gets no hot key at all rather than one that opens something else. F10 and
// the mouse still reach it.
//
//	taken := map[rune]bool{'f': true}
//	hotKeyLabel("Format", taken) // "F~o~rmat"
func hotKeyLabel(name string, taken map[rune]bool) string {
	if plain, key, index := ui.SplitHotKey(name); index >= 0 {
		if lower := unicode.ToLower(key); !taken[lower] {
			taken[lower] = true
			return name
		}
		name = plain // their letter is spoken for; find another
	}

	runes := []rune(name)
	for i, r := range runes {
		lower := unicode.ToLower(r)
		if !unicode.IsLetter(r) || taken[lower] {
			continue
		}
		taken[lower] = true
		marker := string(ui.HotKeyMarker)
		return string(runes[:i]) + marker + string(r) + marker + string(runes[i+1:])
	}
	return name
}

// fileStamp is what a file looked like at a moment: enough to tell that it has
// changed without reading it.
type fileStamp struct {
	exists  bool
	size    int64
	modTime time.Time
}

// stampOf describes a file. A file that is not there stamps as the zero value,
// so appearing and disappearing both register as changes.
func stampOf(path string) fileStamp {
	info, err := os.Stat(path)
	if err != nil {
		return fileStamp{}
	}
	return fileStamp{exists: true, size: info.Size(), modTime: info.ModTime()}
}

// toolsFileStamp describes the project's tools file.
func (a *App) toolsFileStamp() fileStamp {
	working, err := os.Getwd()
	if err != nil {
		return fileStamp{}
	}
	return stampOf(tools.Path(a.profile, working))
}

// refreshToolMenus puts the bar back in step with the tools file.
//
// Menu.OnOpen refills a menu's items, but the *set* of menus comes from the
// file as well: adding `menu = "Docker"` to it should put a Docker menu on the
// bar without restarting the editor. Rebuilding is guarded by the file's size
// and modification time, so the usual case — nothing changed — costs one stat
// and no parsing.
//
// It runs at the top of the event loop, with the rest of the state-driven
// work, because rebuilding the bar from a goroutine would race the draw.
func (a *App) refreshToolMenus() {
	stamp := a.toolsFileStamp()
	if stamp == a.toolsStamp {
		return
	}
	a.toolsStamp = stamp
	a.menu.SetMenus(a.allMenus())
}