turbo-editors/turbo-corepublic Fork 0
v0.9.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.

🛟 Updated. 28d5985 · on v0.9.0 · k33g · 18h ago
menus.go · 194 lines · 8.5 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package app

import "codeberg.org/turbo-editors/turbo-core/ui"

// buildMenus returns the editor's menu bar: File, Edit, Search, Run, Options,
// Window and Help, laid out the way Turbo C laid its own out, with Snippets,
// the editor's own toolchain menu, and whatever menus the project's tools file
// asks for.
//
// Every item is wired straight to a method of the app, and the ones that only
// make sense with a file open say so through Enabled, so the menu greys them
// out instead of doing nothing when chosen.
//
// allMenus is where the order lives, because the bar is rebuilt from it every
// time the tools file changes.
func (a *App) buildMenus() *ui.MenuBar {
	return ui.NewMenuBar(a.allMenus()...)
}

// hasWindow reports whether there is a file open to act on.
func (a *App) hasWindow() bool { return a.desktop.Active() != nil }

func (a *App) fileMenu() *ui.Menu {
	return &ui.Menu{Label: "~F~ile", Items: []*ui.MenuItem{
		{Label: "~N~ew", Shortcut: "F4", Action: a.NewFile},
		{Label: "~O~pen…", Shortcut: "F3", Action: a.OpenFile},
		{Label: "~S~ave", Shortcut: "F2", Action: a.SaveFile, Enabled: a.hasWindow},
		{Label: "Save ~a~s…", Action: a.SaveFileAs, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "~C~lose", Shortcut: "Ctrl-W", Action: a.CloseFile, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "E~x~it", Shortcut: "Alt-X", Action: a.Quit},
	}}
}

func (a *App) editMenu() *ui.Menu {
	return &ui.Menu{Label: "~E~dit", Items: []*ui.MenuItem{
		{Label: "~U~ndo", Shortcut: "Ctrl-Z", Action: a.Undo, Enabled: a.hasWindow},
		// Ctrl-R, not the Ctrl-Y it used to be: Ctrl-Y is Turbo C's
		// delete-line, and between an editor that looks like Turbo C and a
		// habit picked up here, the first has the better claim. Ctrl-Shift-Z
		// is not an option — a terminal delivers it as plain Ctrl-Z.
		{Label: "~R~edo", Shortcut: "Ctrl-R", Action: a.Redo, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "~I~nsert line", Shortcut: "Ctrl-N", Action: a.InsertLine, Enabled: a.hasWindow},
		{Label: "~D~elete line", Shortcut: "Ctrl-Y", Action: a.DeleteLine, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "Cu~t~", Shortcut: "Shift-Del", Action: a.Cut, Enabled: a.hasWindow},
		{Label: "~C~opy", Shortcut: "Ctrl-Ins", Action: a.Copy, Enabled: a.hasWindow},
		{Label: "~P~aste", Shortcut: "Shift-Ins", Action: a.Paste, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "Select ~a~ll", Shortcut: "Ctrl-A", Action: a.SelectAll, Enabled: a.hasWindow},
	}}
}

func (a *App) searchMenu() *ui.Menu {
	return &ui.Menu{Label: "~S~earch", Items: []*ui.MenuItem{
		{Label: "~F~ind…", Shortcut: "Ctrl-F", Action: a.Find, Enabled: a.hasWindow},
		{Label: "Find ~n~ext", Shortcut: "F7", Action: a.FindNext, Enabled: a.hasWindow},
		{Label: "Find ~p~revious", Shortcut: "Shift-F7", Action: a.FindPrevious, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "~G~o to line…", Shortcut: "Ctrl-G", Action: a.GoToLine, Enabled: a.hasWindow},
	}}
}

// agentMenu lists the coding agents the project's acp.toml knows about, one
// window each.
//
// It is on the bar whether or not any agent is configured, because that is
// where Create agents file has to be reachable from — the same reason the
// toolchain menu stays fixed even in a project with no tools file.
//
// A file that cannot be read shows the error as a greyed line rather than an
// empty menu: a silent drop looks exactly like having no agents.
func (a *App) agentMenu() *ui.Menu {
	menu := &ui.Menu{Label: "~A~gent"}
	menu.OnOpen = func() { menu.Items = a.agentItems() }
	menu.Items = a.agentItems()
	return menu
}

// agentItems builds the menu's contents, which are read from the file every
// time it drops down so that editing acp.toml never needs a restart.
func (a *App) agentItems() []*ui.MenuItem {
	var items []*ui.MenuItem

	list, err := a.loadAgents()
	switch {
	case err != nil:
		items = append(items, &ui.MenuItem{Label: "(the agents file has an error)", Enabled: never})
	case list.Len() == 0:
		items = append(items, &ui.MenuItem{Label: "(no agents configured)", Enabled: never})
	default:
		for _, agent := range list.Agents() {
			name := agent.Name
			items = append(items, &ui.MenuItem{Label: name, Action: func() { a.NewAgent(name) }})
		}
	}

	return append(items,
		&ui.MenuItem{Separator: true},
		&ui.MenuItem{Label: "~C~ancel turn", Shortcut: "Esc", Action: a.CancelAgentTurn, Enabled: a.agentTurnRunning},
		&ui.MenuItem{Label: "Create agents ~f~ile", Action: a.CreateAgentsFile, Enabled: a.hasNoAgentsFile},
		&ui.MenuItem{Label: "Agent ~s~tatus", Action: a.AgentStatus},
	)
}

// never is the Enabled of a line that is a message rather than a command.
func never() bool { return false }

func (a *App) runMenu() *ui.Menu {
	return &ui.Menu{Label: "~R~un", Items: []*ui.MenuItem{
		{Label: "~C~ompletion", Shortcut: "Ctrl-Space", Action: a.RequestCompletion, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "~L~anguage server status", Action: a.ShowLanguageStatus},
	}}
}

// codeMenu holds everything the editor asks the language server about the
// symbol under the cursor.
//
// It exists because that work was split across two menus — Describe symbol in
// Run, Go to definition in Search — and neither is where a reader looks for
// "what else knows about this name". Both keep their keys: only where they are
// listed has changed.
//
// Every item acts on the **cursor**, not on a selection. Almost every request
// in the protocol takes a position rather than a range, so asking a user to
// select something first would be an invented step that answers nothing extra.
func (a *App) codeMenu() *ui.Menu {
	return &ui.Menu{Label: "~C~ode", Items: []*ui.MenuItem{
		{Label: "~D~escribe symbol", Shortcut: "F1", Action: a.DescribeSymbol, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "~G~o to definition", Shortcut: "F12", Action: a.GoToDefinition, Enabled: a.hasWindow},
		{Label: "Go to ~t~ype definition", Action: a.GoToTypeDefinition, Enabled: a.hasWindow},
		{Label: "Find ~i~mplementations…", Action: a.FindImplementations, Enabled: a.hasWindow},
		{Label: "Find ~r~eferences…", Shortcut: "Shift-F12", Action: a.FindReferences, Enabled: a.hasWindow},
		{Separator: true},
		// Symbol in file has no key on purpose: the obvious one is Ctrl-Shift-O,
		// and a terminal cannot tell that from Ctrl-O — the shift is lost before
		// the editor sees it. A menu item that advertises a key which does
		// nothing is worse than one with no key at all.
		{Label: "Symbol in ~f~ile…", Action: a.SymbolInFile, Enabled: a.hasWindow},
		{Label: "Symbol in ~p~roject…", Shortcut: "Ctrl-T", Action: a.SymbolInProject},
		{Separator: true},
		{Label: "Pro~b~lems…", Action: a.ShowProblems},
	}}
}

func (a *App) optionsMenu() *ui.Menu {
	return &ui.Menu{Label: "~O~ptions", Items: []*ui.MenuItem{
		{Label: "~T~heme…", Action: a.ChooseTheme},
		{Label: "~L~ine numbers", Action: a.ToggleLineNumbers, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "~C~reate project settings", Action: a.CreateProjectSettings, Enabled: not(a.HasProjectSettings)},
		{Label: "~P~roject settings…", Action: a.OpenProjectSettings, Enabled: a.HasProjectSettings},
	}}
}

func (a *App) windowMenu() *ui.Menu {
	return &ui.Menu{Label: "~W~indow", Items: []*ui.MenuItem{
		{Label: "~N~ext", Shortcut: "F6", Action: a.NextWindow, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "New ~t~erminal", Shortcut: "F8", Action: a.NewTerminal},
		{Label: "~P~roject tree", Shortcut: "F9", Action: a.ProjectTree},
		{Separator: true},
		{Label: "~T~ile", Action: a.TileWindows, Enabled: a.hasWindow},
		{Label: "~C~ascade", Action: a.CascadeWindows, Enabled: a.hasWindow},
		{Label: "~M~aximise", Action: a.MaximizeWindow, Enabled: a.hasWindow},
		{Separator: true},
		{Label: "~L~ist…", Shortcut: "Alt-0", Action: a.ListWindows, Enabled: a.hasWindow},
	}}
}

func (a *App) helpMenu() *ui.Menu {
	return &ui.Menu{Label: "~H~elp", Items: []*ui.MenuItem{
		{Label: "~K~eyboard", Action: a.ShowKeyboardHelp},
		{Label: "~A~bout", Action: a.ShowAbout},
	}}
}

// statusItems returns the hints along the bottom of the screen. They double as
// buttons: clicking one runs it.
func (a *App) statusItems() []ui.StatusItem {
	return []ui.StatusItem{
		{Key: "F1", Label: "Describe", Action: a.DescribeSymbol},
		{Key: "F2", Label: "Save", Action: a.SaveFile},
		{Key: "F3", Label: "Open", Action: a.OpenFile},
		{Key: "F6", Label: "Window", Action: a.NextWindow},
		{Key: "F7", Label: "Next", Action: a.FindNext},
		{Key: "F10", Label: "Menu", Action: func() { a.menu.OpenMenu(0) }},
		{Key: "Alt-X", Label: "Exit", Action: a.Quit},
	}
}