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

app.go · 690 lines · 22.2 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g yesterday1// Package app assembles the editor: it owns the screen, the desktop of
2// windows, the menu bar, the status bar and the modal stack, and it routes
3// every key and click to whichever of them should see it.
4//
5// Everything below it is reusable on its own; this package is where the
6// decisions about *this* editor live.
7package app
8
9import (
10 "context"
11 "fmt"
12 "sync/atomic"
13 "time"
14
15 "github.com/gdamore/tcell/v2"
16
📦 Turbo Core f3ade8d k33g yesterday17 "rickub.com/turbo-editors/turbo-core/editor"
18 "rickub.com/turbo-editors/turbo-core/filetree"
19 "rickub.com/turbo-editors/turbo-core/profile"
20 "rickub.com/turbo-editors/turbo-core/settings"
21 "rickub.com/turbo-editors/turbo-core/terminal"
22 "rickub.com/turbo-editors/turbo-core/theme"
23 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g yesterday24)
25
26// menuBarHeight and statusBarHeight are the rows the furniture takes off the
27// top and bottom of the screen.
28const (
29 menuBarHeight = 1
30 statusBarHeight = 1
31 newWindowOffset = 2 // each new window is offset from the last
32 maxWindowOffsets = 8
33)
34
35// modal is a dialog waiting for an answer, and what to do with it.
36type modal struct {
37 dialog *ui.Dialog
38 onClose func(ui.Result)
39}
40
41// App is the running editor.
42type App struct {
43 screen tcell.Screen
44
45 // profile is which editor this is: its name, its language, its own
46 // directory, its language server, the files it offers to create. Everything
47 // that would otherwise be a hardcoded "turbo-go" is read from here.
48 profile profile.Profile
49
50 theme *theme.Theme
51 themeName string
52
53 desktop *ui.Desktop
54 menu *ui.MenuBar
55 status *ui.StatusBar
56 modals []modal
57 completion *CompletionBox
58
59 clipboard *editor.Clipboard
60 language *Language
61
62 // terminals maps a window to the shell inside it. A window that is not in
63 // here holds a file.
64 terminals map[*ui.Window]*terminal.View
65
66 // agents maps a window to the conversation inside it. A window in neither
67 // this nor terminals holds a file.
68 agents map[*ui.Window]*agentWindow
69
70 // permissions are the agents waiting for somebody to answer them. It is
71 // written from each session's reading goroutine and read by the event
72 // loop, which is why it carries its own lock.
73 permissions pendingPermissions
74
75 // agentFiles guards the open buffers against being read by a session's
76 // goroutine while the event loop is writing them.
77 agentFiles agentFileLock
78
79 // running is the command whose output is showing in a dialog, when one is.
80 running *toolRun
81
82 // toolValues is what was last typed into a tool's parameters box, by tool
83 // name and then by label. It lives for the session only: a value somebody
84 // typed this afternoon is not a decision the project made, so it does not
85 // belong in the project's own directory.
86 toolValues map[string]map[string]string
87
88 // toolsRan says a command from the Go menu has finished, so the files it
89 // may have rewritten want re-reading. It is set from the reading goroutine
90 // and cleared by the event loop, which is why it is atomic.
91 toolsRan atomic.Bool
92
93 // toolsStamp is what the tools file looked like when the menu bar was last
94 // built. The menus the file asks for are part of the bar, so a change to it
95 // has to rebuild the bar and not only the items inside a menu.
96 toolsStamp fileStamp
97
98 // treeWindow and treeView are the project tree, when one is open. There is
99 // at most one of them, so a pair of fields says it better than a map.
100 treeWindow *ui.Window
101 treeView *filetree.View
102
103 // settingsPath is the project settings file in use, or "" when the project
104 // has none. It is where a setting changed in the editor is written back.
105 settingsPath string
106 // autosave is the pending automatic save, when the project asked for one.
107 autosave autosave
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago108 // fileStamps is what each open file looked like on disk when the loop
109 // last saw it, by canonical path — how a change made by another program
110 // is noticed. fileWatch shares it with the goroutine that wakes the loop
111 // for one, at fileWatchInterval, while the editor is idle.
112 fileStamps map[string]fileStamp
113 fileWatch fileWatch
114 fileWatchInterval time.Duration
🛟 Updated. 28d5985 k33g yesterday115 // now is the clock, so that autosave's deadlines can be tested without
116 // waiting for them.
117 now func() time.Time
118
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago119 // pasting says the terminal is in the middle of a bracketed paste, and
120 // pasted is what it has sent so far: a paste is handed to an agent window
121 // whole, once it has ended, rather than key by key.
122 pasting bool
123 pasted []*tcell.EventKey
124
🛟 Updated. 28d5985 k33g yesterday125 lastSearch string
126 lastMatchCase bool
127 windowsOpened int
128 announced bool // the open documents have been told to the server
129 quitting bool
130}
131
132// New returns an editor drawing on screen, with the named theme, being the
133// editor the profile describes.
134//
135// An unknown theme name falls back to the default rather than failing: a typo
136// in a configuration file should not stop the editor opening.
137//
138// editor := app.New(screen, "turbo-dark", golang.Profile())
139// editor.NewFile()
140// editor.Run()
141func New(screen tcell.Screen, themeName string, p profile.Profile) *App {
142 a := &App{
143 screen: screen,
144 profile: p,
145 desktop: ui.NewDesktop(),
146 status: ui.NewStatusBar(),
147 completion: &CompletionBox{},
148 clipboard: &editor.Clipboard{},
149 language: NewLanguage(p.Server, p.Name),
150 terminals: map[*ui.Window]*terminal.View{},
151 agents: map[*ui.Window]*agentWindow{},
152 now: time.Now,
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago153
154 fileStamps: map[string]fileStamp{},
155 fileWatchInterval: defaultFileWatchInterval,
🛟 Updated. 28d5985 k33g yesterday156 }
157 a.autosave.delay = settings.DefaultAutosaveDelay
158
159 a.setTheme(themeName)
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago160 // Bracketed paste, so that a paste from outside arrives between two
161 // EventPaste marks and can be told from typing. Without it forty lines
162 // pasted into an agent window are forty Enters, each one sending.
163 screen.EnablePaste()
🛟 Updated. 28d5985 k33g yesterday164 // Stamped before the bar is built, so a tools file written between the two
165 // is picked up by the next turn of the loop rather than missed.
166 a.toolsStamp = a.toolsFileStamp()
167 a.menu = a.buildMenus()
168 a.status.SetItems(a.statusItems()...)
169 a.completion.OnAccept = a.acceptCompletion
170 a.language.OnUpdate = a.wake
171 return a
172}
173
174// Profile returns which editor this is: its name, its language, and everything
175// else that differs between one editor built on this library and another.
176func (a *App) Profile() profile.Profile { return a.profile }
177
178// Theme returns the theme currently in use.
179func (a *App) Theme() *theme.Theme { return a.theme }
180
181// ThemeName returns the name of the theme currently in use.
182func (a *App) ThemeName() string { return a.themeName }
183
184// UseSettings applies a project's settings and remembers where they came
185// from, so that changing a setting in the editor can be written back.
186//
187// The theme is not applied here: the caller resolves it first, because a
188// -theme flag on the command line overrides the project's choice and only the
189// caller knows whether one was given.
190//
191// s, err := settings.Load(p, dir)
192// if err == nil {
193// editor.UseSettings(s, settings.Path(p, dir))
194// }
195func (a *App) UseSettings(s settings.Settings, path string) {
196 a.settingsPath = path
197 a.SetAutosave(s.Autosave, s.AutosaveDelay)
198}
199
200// SettingsPath returns the project settings file the editor is following, or
201// "" when the project has none.
202func (a *App) SettingsPath() string { return a.settingsPath }
203
204// setTheme loads a theme by name, falling back to the default.
205func (a *App) setTheme(name string) {
206 loaded, err := theme.Load(name, a.profile.ThemeDir())
207 if err != nil {
208 a.theme, a.themeName = theme.Default(a.profile.ThemeDir()), theme.DefaultName
209 } else {
210 a.theme, a.themeName = loaded, name
211 }
212 a.applyCursorStyle()
213}
214
215// applyCursorStyle tells the terminal to draw its cursor as a solid block in
216// the theme's own colour.
217//
218// This matters more than it sounds. A terminal draws its cursor *over* the
219// cell, in whatever colour the user configured for some other palette, and on
220// a dark theme that is very often a dark cursor on a dark background — a cell
221// painted underneath does not help, because the terminal's block covers it.
222// Naming the colour here is the only way the theme can win.
223//
224// Terminals that support neither the shape nor the colour ignore this, which
225// is why the cell underneath is still painted as a fallback.
226func (a *App) applyCursorStyle() {
227 a.screen.SetCursorStyle(tcell.CursorStyleSteadyBlock, cursorColor(a.theme))
228}
229
230// cursorColor returns the colour a block cursor should be filled with: the
231// background of the theme's cursor style, since that is what a filled block
232// shows.
233func cursorColor(th *theme.Theme) tcell.Color {
234 _, background, _ := th.Style(theme.KeyEditorCursor).Decompose()
235 return background
236}
237
238// StartLanguageServer starts the editor's language server in root, in the
239// background so that a slow start-up does not hold the first keystroke up.
240func (a *App) StartLanguageServer(ctx context.Context, root string) {
241 go a.language.Start(ctx, root)
242}
243
244// announceOpenDocuments tells the language server about every file that is
245// already open, the first time there is anybody to tell.
246//
247// The command opens the files named on its command line and *then* starts the
248// server, so the didOpen sent at that moment reaches nothing. Without this
249// second announcement the server never learns the documents exist, ignores
250// every didChange that follows, and answers completions from whatever is on
251// disk — which is to say, not from what has been typed.
252//
253// This is checked on every turn of the event loop rather than driven by an
254// event, because an event can be dropped: tcell's queue is bounded, PostEvent
255// fails when it is full, and start-up — when gopls publishes diagnostics for
256// the whole module — is exactly when it is fullest. Correctness must not
257// depend on a message that is allowed to go missing.
258func (a *App) announceOpenDocuments() {
259 if a.announced || !a.language.Ready() {
260 return
261 }
262 a.announced = true
263
264 for _, window := range a.desktop.Windows() {
265 view, ok := window.Content().(*editor.View)
266 if !ok {
267 continue
268 }
269 a.language.DidOpen(view.Buffer().Path(), view.Buffer().Text())
270 }
271}
272
273// Language returns the language-server side of the editor.
274func (a *App) Language() *Language { return a.language }
275
276// Run draws and handles events until the user asks to leave.
277func (a *App) Run() error {
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago278 stop := a.watchFiles()
279 defer stop()
280
🛟 Updated. 28d5985 k33g yesterday281 for !a.quitting {
282 a.tick()
283 a.layout()
284 a.draw()
285
286 event := a.screen.PollEvent()
287 if event == nil {
288 return nil // the screen was finalised from under us
289 }
290 a.handle(event)
291 }
292 return nil
293}
294
295// tick is the work the loop does before it draws, every turn.
296//
297// Everything here is **state-driven**: each of these looks at what is true now
298// rather than waiting to be told. That is deliberate and it is the same reason
299// each time — the only way to wake this loop from another goroutine is
300// PostEvent, which drops what does not fit in its queue, so anything that
301// depended on a message arriving would fail exactly when the editor is busiest.
302// A dropped wake-up here costs a late turn, never a lost one.
303//
304// It is a method rather than five lines inside Run so that a test can take one
305// turn of the loop, which is the only way to test any of it: Run itself blocks
306// in PollEvent.
307func (a *App) tick() {
308 a.announceOpenDocuments()
309 a.refreshToolMenus()
310 a.refreshRunningTool()
311 a.reloadAfterTools()
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago312 a.reloadChangedFiles()
🛟 Updated. 28d5985 k33g yesterday313 a.saveDueDocuments()
314 a.refreshTerminalTitles()
315 a.refreshAgentTitles()
316 a.askNextPermission()
317 a.refreshMarks()
318}
319
320// Tick does one turn of the event loop's state-driven work, without waiting for
321// an event.
322//
323// Run does this for itself before every frame; it is exported for the same
324// reason Render is, so that an editor built on this library can drive itself
325// end to end from a test — announcing open documents to a language server that
326// has just come up, say — with no terminal and no event loop.
327func (a *App) Tick() { a.tick() }
328
329// Handle routes one event through the whole chain, exactly as the event loop
330// does. It is exported alongside Tick and Render, and for the same reason.
331//
332// editor.Handle(tcell.NewEventKey(tcell.KeyRune, 'x', tcell.ModNone))
333// editor.Tick()
334// editor.Render()
335func (a *App) Handle(event tcell.Event) { a.handle(event) }
336
337// ActiveView returns the editing view of the window in front, or nil when the
338// front window is not a file — a terminal, or the project tree — and when there
339// is no window at all.
340func (a *App) ActiveView() *editor.View { return a.activeView() }
341
342// Quitting reports whether the editor is on its way out, which is what ends
343// the event loop.
344func (a *App) Quitting() bool { return a.quitting }
345
346// wake makes the event loop go round again, so that something which happened
347// off the main goroutine — a batch of diagnostics, say — reaches the screen.
348//
349// The post is allowed to fail: a full queue means a redraw is already coming.
350func (a *App) wake() {
351 _ = a.screen.PostEvent(tcell.NewEventInterrupt(nil))
352}
353
354// screenRect returns the whole terminal.
355func (a *App) screenRect() ui.Rect {
356 width, height := a.screen.Size()
357 return ui.Rect{W: width, H: height}
358}
359
360// desktopRect returns the area between the menu bar and the status bar.
361func (a *App) desktopRect() ui.Rect {
362 screen := a.screenRect()
363 return ui.Rect{
364 X: 0,
365 Y: menuBarHeight,
366 W: screen.W,
367 H: max(screen.H-menuBarHeight-statusBarHeight, 0),
368 }
369}
370
371// layout places the furniture, which must happen again after every resize.
372func (a *App) layout() {
373 screen := a.screenRect()
374 a.menu.SetBounds(ui.Rect{W: screen.W, H: menuBarHeight})
375 a.status.SetBounds(ui.Rect{Y: screen.H - statusBarHeight, W: screen.W, H: statusBarHeight})
376 a.desktop.SetBounds(a.desktopRect())
377}
378
379// draw paints the whole screen, back to front.
380func (a *App) draw() {
381 painter := ui.NewPainter(a.screen)
382 a.screen.HideCursor()
383
384 a.desktop.Draw(painter.Sub(a.desktop.Bounds()), a.theme)
385 a.updateStatus()
386 a.status.Draw(painter, a.theme)
387 a.menu.Draw(painter, a.theme)
388
389 for _, m := range a.modals {
390 m.dialog.Draw(painter, a.theme)
391 }
392 a.completion.Draw(painter, a.theme)
393
394 a.screen.Show()
395}
396
397// updateStatus refreshes what the status bar shows on the right: the cursor
398// position, and whatever the language server has to say.
399func (a *App) updateStatus() {
400 view := a.activeView()
401 if view == nil {
402 a.status.SetExtra(a.language.Status())
403 return
404 }
405
406 extra := view.CursorStatus()
407 if diagnostic, ok := a.language.FirstError(view.Buffer().Path()); ok {
408 extra = fmt.Sprintf("%s ⚠ %s", extra, diagnostic.Message)
409 } else {
410 extra = fmt.Sprintf("%s %s", extra, a.language.Status())
411 }
412 a.status.SetExtra(extra)
413}
414
415// handle routes one event.
416func (a *App) handle(event tcell.Event) {
417 switch typed := event.(type) {
418 case *tcell.EventResize:
419 a.resize()
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago420 case *tcell.EventPaste:
421 a.handlePaste(typed)
🛟 Updated. 28d5985 k33g yesterday422 case *tcell.EventKey:
423 a.handleKey(typed)
424 case *tcell.EventMouse:
425 a.handleMouse(typed)
426 }
427 a.announceOpenDocuments()
428 a.settleModals()
429}
430
431// resize takes everything that is not the desktop to the new terminal size.
432//
433// The desktop and its windows follow in layout, on the next turn of the loop.
434// What needs doing here is the floating furniture: a dialog placed in the
435// middle of the old screen would be off-centre or half outside the new one,
436// and the completion popup is anchored to a cursor that has just moved.
437func (a *App) resize() {
438 a.screen.Sync()
439 a.completion.Hide()
440
441 for _, m := range a.modals {
442 m.dialog.CenterIn(a.screenRect())
443 }
444}
445
446// handleKey offers a key to each layer in turn, front to back, and gives what
447// nobody claimed to the window on the desktop.
448func (a *App) handleKey(ev *tcell.EventKey) {
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 12h ago449 if a.pasting {
450 a.pasted = append(a.pasted, ev)
451 return
452 }
🛟 Updated. 28d5985 k33g yesterday453 a.status.SetMessage("") // any keystroke clears a transient message
454
455 for _, claims := range a.keyLayers() {
456 if claims(ev) {
457 return
458 }
459 }
460
461 if a.desktop.HandleKey(ev) {
462 a.refreshCompletionPrefix()
463 }
464}
465
466// keyLayers returns everything that may claim a key press before the window in
467// front does, in the order they are offered it.
468//
469// The order is the design. An open completion list is what the next key is
470// about; a modal dialog is exclusive by definition; an open menu owns the
471// keyboard for as long as it is down; a focused terminal wants nearly
472// everything, so it comes before the editor's own shortcuts; and the closed
473// menu's Alt-letters and F10 come after the terminal precisely so a shell can
474// have Alt-B and Alt-F.
475func (a *App) keyLayers() []func(*tcell.EventKey) bool {
476 return []func(*tcell.EventKey) bool{
477 a.completion.HandleKey,
478 a.handleModalKey,
479 a.handleOpenMenuKey,
480 a.handleTerminalKey,
481 a.menu.HandleKey,
482 a.handleShortcut,
483 }
484}
485
486// handleModalKey gives a key to the topmost dialog, and reports that it was
487// taken whether or not the dialog did anything with it: a modal swallows the
488// keyboard for as long as it is up.
489func (a *App) handleModalKey(ev *tcell.EventKey) bool {
490 if len(a.modals) == 0 {
491 return false
492 }
493 a.modals[len(a.modals)-1].dialog.HandleKey(ev)
494 return true
495}
496
497// handleOpenMenuKey gives a key to the menu bar only while a menu is down.
498func (a *App) handleOpenMenuKey(ev *tcell.EventKey) bool {
499 return a.menu.Open() && a.menu.HandleKey(ev)
500}
501
502// handleTerminalKey sends a key to the shell in the front window.
503func (a *App) handleTerminalKey(ev *tcell.EventKey) bool {
504 return a.terminalTakesKey(ev) && a.desktop.HandleKey(ev)
505}
506
507// terminalTakesKey reports whether a key belongs to the shell in the front
508// window rather than to the editor.
509//
510// A shell needs nearly every key: Ctrl-C interrupts, Ctrl-W deletes a word,
511// Ctrl-F moves forward, Alt-B moves back. Letting the editor keep those would
512// make the terminal useless. What the editor keeps is the handful of keys that
513// are the way *out* of a terminal — see editorOwnedKey.
514func (a *App) terminalTakesKey(ev *tcell.EventKey) bool {
515 return a.activeTerminal() != nil && !editorOwnedKey(ev)
516}
517
518// editorOwnedKey reports whether the editor keeps a key even when a terminal
519// has the focus: the function keys, Alt-X to leave, and Alt-digit to switch
520// windows.
521//
522// Without these there would be no way out of a full-screen program running in
523// a terminal window.
524func editorOwnedKey(ev *tcell.EventKey) bool {
525 if ev.Key() >= tcell.KeyF1 && ev.Key() <= tcell.KeyF12 {
526 return true
527 }
528 if ev.Key() != tcell.KeyRune || ev.Modifiers()&tcell.ModAlt == 0 {
529 return false
530 }
531
532 r := ev.Rune()
533 return r == 'x' || r == 'X' || (r >= '0' && r <= '9')
534}
535
536// handleMouse offers a click to each layer in turn, front to back.
537func (a *App) handleMouse(ev *tcell.EventMouse) {
538 if a.completion.HandleMouse(ev) {
539 return
540 }
541 if len(a.modals) > 0 {
542 a.modals[len(a.modals)-1].dialog.HandleMouse(ev)
543 return
544 }
545 if a.menu.HandleMouse(ev) {
546 return
547 }
548 if a.status.HandleMouse(ev) {
549 return
550 }
551 a.desktop.HandleMouse(ev)
552}
553
554// handleShortcut runs the editor's global keys and reports whether it did.
555func (a *App) handleShortcut(ev *tcell.EventKey) bool {
556 if a.handleWindowNumber(ev) {
557 return true
558 }
559
560 switch {
561 case ev.Key() == tcell.KeyF1:
562 a.DescribeSymbol()
563 case ev.Key() == tcell.KeyF2:
564 a.SaveFile()
565 case ev.Key() == tcell.KeyF3:
566 a.OpenFile()
567 case ev.Key() == tcell.KeyF4:
568 a.NewFile()
569 case ev.Key() == tcell.KeyF6:
570 a.NextWindow()
571 case ev.Key() == tcell.KeyF8:
572 a.NewTerminal()
573 case ev.Key() == tcell.KeyF9:
574 a.ProjectTree()
575 case ev.Key() == tcell.KeyF7 && ev.Modifiers()&tcell.ModShift != 0:
576 a.FindPrevious()
577 case ev.Key() == tcell.KeyF7:
578 a.FindNext()
579 case ev.Key() == tcell.KeyF12 && ev.Modifiers()&tcell.ModShift != 0:
580 a.FindReferences()
581 case ev.Key() == tcell.KeyF12:
582 a.GoToDefinition()
583 case ev.Key() == tcell.KeyCtrlT:
584 a.SymbolInProject()
585 case ev.Key() == tcell.KeyCtrlF:
586 a.Find()
587 case ev.Key() == tcell.KeyCtrlG:
588 a.GoToLine()
589 case ev.Key() == tcell.KeyCtrlW:
590 a.CloseFile()
591 case ev.Key() == tcell.KeyRune && ev.Modifiers()&tcell.ModAlt != 0 && (ev.Rune() == 'x' || ev.Rune() == 'X'):
592 a.Quit()
593 default:
594 return false
595 }
596 return true
597}
598
599// handleWindowNumber deals with Alt-1 … Alt-9, which pick a window, and Alt-0,
600// which lists them.
601func (a *App) handleWindowNumber(ev *tcell.EventKey) bool {
602 if ev.Key() != tcell.KeyRune || ev.Modifiers()&tcell.ModAlt == 0 {
603 return false
604 }
605 digit := ev.Rune() - '0'
606 if digit < 0 || digit > 9 {
607 return false
608 }
609
610 if digit == 0 {
611 a.ListWindows()
612 return true
613 }
614 return a.desktop.FocusNumber(int(digit))
615}
616
617// pushModal puts a dialog in front of everything else.
618func (a *App) pushModal(dialog *ui.Dialog, onClose func(ui.Result)) {
619 a.completion.Hide()
620 a.modals = append(a.modals, modal{dialog: dialog, onClose: onClose})
621}
622
623// settleModals closes the dialogs that have been answered and runs what they
624// were waiting on.
625//
626// It runs after every event rather than inside the handlers, so that a dialog
627// which opens another one does not nest event handling.
628func (a *App) settleModals() {
629 for len(a.modals) > 0 {
630 top := a.modals[len(a.modals)-1]
631 if !top.dialog.Done() {
632 return
633 }
634 a.modals = a.modals[:len(a.modals)-1]
635 if top.onClose != nil {
636 top.onClose(top.dialog.Result())
637 }
638 }
639}
640
641// Message shows a one-line note on the status bar.
642func (a *App) Message(text string) { a.status.SetMessage(text) }
643
644// ShowMessage opens a box with a message in it.
645func (a *App) ShowMessage(title, message string) {
646 a.pushModal(NewMessageDialog(title, message, a.screenRect()), nil)
647}
648
649// activeView returns the editor view of the front window, or nil when no file
650// is open.
651func (a *App) activeView() *editor.View {
652 window := a.desktop.Active()
653 if window == nil {
654 return nil
655 }
656 view, _ := editorViewOf(window)
657 return view
658}
659
660// Modals returns how many dialogs are waiting, which the tests use to see that
661// an action opened one.
662func (a *App) Modals() int { return len(a.modals) }
663
664// TopModal returns the dialog in front, or nil when there is none.
665func (a *App) TopModal() *ui.Dialog {
666 if len(a.modals) == 0 {
667 return nil
668 }
669 return a.modals[len(a.modals)-1].dialog
670}
671
672// Desktop returns the desktop, so that tests and the main program can look at
673// the windows without going through the screen.
674func (a *App) Desktop() *ui.Desktop { return a.desktop }
675
676// Completion returns the completion popup.
677func (a *App) Completion() *CompletionBox { return a.completion }
678
679// MenuBar returns the menu bar.
680func (a *App) MenuBar() *ui.MenuBar { return a.menu }
681
682// Render lays the editor out and paints one frame. The event loop does this
683// for itself; it is exported so a screenshot can be taken without one.
684func (a *App) Render() {
685 a.layout()
686 a.draw()
687}
688
689// StatusBar returns the status bar.
690func (a *App) StatusBar() *ui.StatusBar { return a.status }