1// Copyright 2014 The gocui Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package gocui
6
7import (
8 "errors"
9
10 "github.com/nsf/termbox-go"
11)
12
13var (
14 // ErrQuit is used to decide if the MainLoop finished successfully.
15 ErrQuit = errors.New("quit")
16
17 // ErrUnknownView allows to assert if a View must be initialized.
18 ErrUnknownView = errors.New("unknown view")
19)
20
21// OutputMode represents the terminal's output mode (8 or 256 colors).
22type OutputMode termbox.OutputMode
23
24const (
25 // OutputNormal provides 8-colors terminal mode.
26 OutputNormal = OutputMode(termbox.OutputNormal)
27
28 // Output256 provides 256-colors terminal mode.
29 Output256 = OutputMode(termbox.Output256)
30)
31
32// Gui represents the whole User Interface, including the views, layouts
33// and keybindings.
34type Gui struct {
35 tbEvents chan termbox.Event
36 userEvents chan userEvent
37 views []*View
38 currentView *View
39 managers []Manager
40 keybindings []*keybinding
41 maxX, maxY int
42 outputMode OutputMode
43
44 // BgColor and FgColor allow to configure the background and foreground
45 // colors of the GUI.
46 BgColor, FgColor Attribute
47
48 // SelBgColor and SelFgColor allow to configure the background and
49 // foreground colors of the frame of the current view.
50 SelBgColor, SelFgColor Attribute
51
52 // If Highlight is true, Sel{Bg,Fg}Colors will be used to draw the
53 // frame of the current view.
54 Highlight bool
55
56 // If Cursor is true then the cursor is enabled.
57 Cursor bool
58
59 // If Mouse is true then mouse events will be enabled.
60 Mouse bool
61
62 // If InputEsc is true, when ESC sequence is in the buffer and it doesn't
63 // match any known sequence, ESC means KeyEsc.
64 InputEsc bool
65
66 // If ASCII is true then use ASCII instead of unicode to draw the
67 // interface. Using ASCII is more portable.
68 ASCII bool
69}
70
71// NewGui returns a new Gui object with a given output mode.
72func NewGui(mode OutputMode) (*Gui, error) {
73 if err := termbox.Init(); err != nil {
74 return nil, err
75 }
76
77 g := &Gui{}
78
79 g.outputMode = mode
80 termbox.SetOutputMode(termbox.OutputMode(mode))
81
82 g.tbEvents = make(chan termbox.Event, 20)
83 g.userEvents = make(chan userEvent, 20)
84
85 g.maxX, g.maxY = termbox.Size()
86
87 g.BgColor, g.FgColor = ColorDefault, ColorDefault
88 g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault
89
90 return g, nil
91}
92
93// Close finalizes the library. It should be called after a successful
94// initialization and when gocui is not needed anymore.
95func (g *Gui) Close() {
96 termbox.Close()
97}
98
99// Size returns the terminal's size.
100func (g *Gui) Size() (x, y int) {
101 return g.maxX, g.maxY
102}
103
104// SetRune writes a rune at the given point, relative to the top-left
105// corner of the terminal. It checks if the position is valid and applies
106// the given colors.
107func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
108 if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
109 return errors.New("invalid point")
110 }
111 termbox.SetCell(x, y, ch, termbox.Attribute(fgColor), termbox.Attribute(bgColor))
112 return nil
113}
114
115// Rune returns the rune contained in the cell at the given position.
116// It checks if the position is valid.
117func (g *Gui) Rune(x, y int) (rune, error) {
118 if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
119 return ' ', errors.New("invalid point")
120 }
121 c := termbox.CellBuffer()[y*g.maxX+x]
122 return c.Ch, nil
123}
124
125// SetView creates a new view with its top-left corner at (x0, y0)
126// and the bottom-right one at (x1, y1). If a view with the same name
127// already exists, its dimensions are updated; otherwise, the error
128// ErrUnknownView is returned, which allows to assert if the View must
129// be initialized. It checks if the position is valid.
130func (g *Gui) SetView(name string, x0, y0, x1, y1 int) (*View, error) {
131 if x0 >= x1 || y0 >= y1 {
132 return nil, errors.New("invalid dimensions")
133 }
134 if name == "" {
135 return nil, errors.New("invalid name")
136 }
137
138 if v, err := g.View(name); err == nil {
139 v.x0 = x0
140 v.y0 = y0
141 v.x1 = x1
142 v.y1 = y1
143 v.tainted = true
144 return v, nil
145 }
146
147 v := newView(name, x0, y0, x1, y1, g.outputMode)
148 v.BgColor, v.FgColor = g.BgColor, g.FgColor
149 v.SelBgColor, v.SelFgColor = g.SelBgColor, g.SelFgColor
150 g.views = append(g.views, v)
151 return v, ErrUnknownView
152}
153
154// SetViewOnTop sets the given view on top of the existing ones.
155func (g *Gui) SetViewOnTop(name string) (*View, error) {
156 for i, v := range g.views {
157 if v.name == name {
158 s := append(g.views[:i], g.views[i+1:]...)
159 g.views = append(s, v)
160 return v, nil
161 }
162 }
163 return nil, ErrUnknownView
164}
165
166// SetViewOnBottom sets the given view on bottom of the existing ones.
167func (g *Gui) SetViewOnBottom(name string) (*View, error) {
168 for i, v := range g.views {
169 if v.name == name {
170 s := append(g.views[:i], g.views[i+1:]...)
171 g.views = append([]*View{v}, s...)
172 return v, nil
173 }
174 }
175 return nil, ErrUnknownView
176}
177
178// Views returns all the views in the GUI.
179func (g *Gui) Views() []*View {
180 return g.views
181}
182
183// View returns a pointer to the view with the given name, or error
184// ErrUnknownView if a view with that name does not exist.
185func (g *Gui) View(name string) (*View, error) {
186 for _, v := range g.views {
187 if v.name == name {
188 return v, nil
189 }
190 }
191 return nil, ErrUnknownView
192}
193
194// ViewByPosition returns a pointer to a view matching the given position, or
195// error ErrUnknownView if a view in that position does not exist.
196func (g *Gui) ViewByPosition(x, y int) (*View, error) {
197 // traverse views in reverse order checking top views first
198 for i := len(g.views); i > 0; i-- {
199 v := g.views[i-1]
200 if x > v.x0 && x < v.x1 && y > v.y0 && y < v.y1 {
201 return v, nil
202 }
203 }
204 return nil, ErrUnknownView
205}
206
207// ViewPosition returns the coordinates of the view with the given name, or
208// error ErrUnknownView if a view with that name does not exist.
209func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) {
210 for _, v := range g.views {
211 if v.name == name {
212 return v.x0, v.y0, v.x1, v.y1, nil
213 }
214 }
215 return 0, 0, 0, 0, ErrUnknownView
216}
217
218// DeleteView deletes a view by name.
219func (g *Gui) DeleteView(name string) error {
220 for i, v := range g.views {
221 if v.name == name {
222 g.views = append(g.views[:i], g.views[i+1:]...)
223 return nil
224 }
225 }
226 return ErrUnknownView
227}
228
229// SetCurrentView gives the focus to a given view.
230func (g *Gui) SetCurrentView(name string) (*View, error) {
231 for _, v := range g.views {
232 if v.name == name {
233 g.currentView = v
234 return v, nil
235 }
236 }
237 return nil, ErrUnknownView
238}
239
240// CurrentView returns the currently focused view, or nil if no view
241// owns the focus.
242func (g *Gui) CurrentView() *View {
243 return g.currentView
244}
245
246// SetKeybinding creates a new keybinding. If viewname equals to ""
247// (empty string) then the keybinding will apply to all views. key must
248// be a rune or a Key.
249func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error {
250 var kb *keybinding
251
252 k, ch, err := getKey(key)
253 if err != nil {
254 return err
255 }
256 kb = newKeybinding(viewname, k, ch, mod, handler)
257 g.keybindings = append(g.keybindings, kb)
258 return nil
259}
260
261// DeleteKeybinding deletes a keybinding.
262func (g *Gui) DeleteKeybinding(viewname string, key interface{}, mod Modifier) error {
263 k, ch, err := getKey(key)
264 if err != nil {
265 return err
266 }
267
268 for i, kb := range g.keybindings {
269 if kb.viewName == viewname && kb.ch == ch && kb.key == k && kb.mod == mod {
270 g.keybindings = append(g.keybindings[:i], g.keybindings[i+1:]...)
271 return nil
272 }
273 }
274 return errors.New("keybinding not found")
275}
276
277// DeleteKeybindings deletes all keybindings of view.
278func (g *Gui) DeleteKeybindings(viewname string) {
279 var s []*keybinding
280 for _, kb := range g.keybindings {
281 if kb.viewName != viewname {
282 s = append(s, kb)
283 }
284 }
285 g.keybindings = s
286}
287
288// getKey takes an empty interface with a key and returns the corresponding
289// typed Key or rune.
290func getKey(key interface{}) (Key, rune, error) {
291 switch t := key.(type) {
292 case Key:
293 return t, 0, nil
294 case rune:
295 return 0, t, nil
296 default:
297 return 0, 0, errors.New("unknown type")
298 }
299}
300
301// userEvent represents an event triggered by the user.
302type userEvent struct {
303 f func(*Gui) error
304}
305
306// Update executes the passed function. This method can be called safely from a
307// goroutine in order to update the GUI. It is important to note that the
308// passed function won't be executed immediately, instead it will be added to
309// the user events queue. Given that Update spawns a goroutine, the order in
310// which the user events will be handled is not guaranteed.
311func (g *Gui) Update(f func(*Gui) error) {
312 go func() { g.userEvents <- userEvent{f: f} }()
313}
314
315// A Manager is in charge of GUI's layout and can be used to build widgets.
316type Manager interface {
317 // Layout is called every time the GUI is redrawn, it must contain the
318 // base views and its initializations.
319 Layout(*Gui) error
320}
321
322// The ManagerFunc type is an adapter to allow the use of ordinary functions as
323// Managers. If f is a function with the appropriate signature, ManagerFunc(f)
324// is an Manager object that calls f.
325type ManagerFunc func(*Gui) error
326
327// Layout calls f(g)
328func (f ManagerFunc) Layout(g *Gui) error {
329 return f(g)
330}
331
332// SetManager sets the given GUI managers. It deletes all views and
333// keybindings.
334func (g *Gui) SetManager(managers ...Manager) {
335 g.managers = managers
336 g.currentView = nil
337 g.views = nil
338 g.keybindings = nil
339
340 go func() { g.tbEvents <- termbox.Event{Type: termbox.EventResize} }()
341}
342
343// SetManagerFunc sets the given manager function. It deletes all views and
344// keybindings.
345func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
346 g.SetManager(ManagerFunc(manager))
347}
348
349// MainLoop runs the main loop until an error is returned. A successful
350// finish should return ErrQuit.
351func (g *Gui) MainLoop() error {
352 go func() {
353 for {
354 g.tbEvents <- termbox.PollEvent()
355 }
356 }()
357
358 inputMode := termbox.InputAlt
359 if g.InputEsc {
360 inputMode = termbox.InputEsc
361 }
362 if g.Mouse {
363 inputMode |= termbox.InputMouse
364 }
365 termbox.SetInputMode(inputMode)
366
367 if err := g.flush(); err != nil {
368 return err
369 }
370 for {
371 select {
372 case ev := <-g.tbEvents:
373 if err := g.handleEvent(&ev); err != nil {
374 return err
375 }
376 case ev := <-g.userEvents:
377 if err := ev.f(g); err != nil {
378 return err
379 }
380 }
381 if err := g.consumeevents(); err != nil {
382 return err
383 }
384 if err := g.flush(); err != nil {
385 return err
386 }
387 }
388}
389
390// consumeevents handles the remaining events in the events pool.
391func (g *Gui) consumeevents() error {
392 for {
393 select {
394 case ev := <-g.tbEvents:
395 if err := g.handleEvent(&ev); err != nil {
396 return err
397 }
398 case ev := <-g.userEvents:
399 if err := ev.f(g); err != nil {
400 return err
401 }
402 default:
403 return nil
404 }
405 }
406}
407
408// handleEvent handles an event, based on its type (key-press, error,
409// etc.)
410func (g *Gui) handleEvent(ev *termbox.Event) error {
411 switch ev.Type {
412 case termbox.EventKey, termbox.EventMouse:
413 return g.onKey(ev)
414 case termbox.EventError:
415 return ev.Err
416 default:
417 return nil
418 }
419}
420
421// flush updates the gui, re-drawing frames and buffers.
422func (g *Gui) flush() error {
423 termbox.Clear(termbox.Attribute(g.FgColor), termbox.Attribute(g.BgColor))
424
425 maxX, maxY := termbox.Size()
426 // if GUI's size has changed, we need to redraw all views
427 if maxX != g.maxX || maxY != g.maxY {
428 for _, v := range g.views {
429 v.tainted = true
430 }
431 }
432 g.maxX, g.maxY = maxX, maxY
433
434 for _, m := range g.managers {
435 if err := m.Layout(g); err != nil {
436 return err
437 }
438 }
439 for _, v := range g.views {
440 if v.Frame {
441 var fgColor, bgColor Attribute
442 if g.Highlight && v == g.currentView {
443 fgColor = g.SelFgColor
444 bgColor = g.SelBgColor
445 } else {
446 fgColor = g.FgColor
447 bgColor = g.BgColor
448 }
449
450 if err := g.drawFrameEdges(v, fgColor, bgColor); err != nil {
451 return err
452 }
453 if err := g.drawFrameCorners(v, fgColor, bgColor); err != nil {
454 return err
455 }
456 if v.Title != "" {
457 if err := g.drawTitle(v, fgColor, bgColor); err != nil {
458 return err
459 }
460 }
461 }
462 if err := g.draw(v); err != nil {
463 return err
464 }
465 }
466 termbox.Flush()
467 return nil
468}
469
470// drawFrameEdges draws the horizontal and vertical edges of a view.
471func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
472 runeH, runeV := '─', '│'
473 if g.ASCII {
474 runeH, runeV = '-', '|'
475 }
476
477 for x := v.x0 + 1; x < v.x1 && x < g.maxX; x++ {
478 if x < 0 {
479 continue
480 }
481 if v.y0 > -1 && v.y0 < g.maxY {
482 if err := g.SetRune(x, v.y0, runeH, fgColor, bgColor); err != nil {
483 return err
484 }
485 }
486 if v.y1 > -1 && v.y1 < g.maxY {
487 if err := g.SetRune(x, v.y1, runeH, fgColor, bgColor); err != nil {
488 return err
489 }
490 }
491 }
492 for y := v.y0 + 1; y < v.y1 && y < g.maxY; y++ {
493 if y < 0 {
494 continue
495 }
496 if v.x0 > -1 && v.x0 < g.maxX {
497 if err := g.SetRune(v.x0, y, runeV, fgColor, bgColor); err != nil {
498 return err
499 }
500 }
501 if v.x1 > -1 && v.x1 < g.maxX {
502 if err := g.SetRune(v.x1, y, runeV, fgColor, bgColor); err != nil {
503 return err
504 }
505 }
506 }
507 return nil
508}
509
510// drawFrameCorners draws the corners of the view.
511func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
512 runeTL, runeTR, runeBL, runeBR := '┌', '┐', '└', '┘'
513 if g.ASCII {
514 runeTL, runeTR, runeBL, runeBR = '+', '+', '+', '+'
515 }
516
517 corners := []struct {
518 x, y int
519 ch rune
520 }{{v.x0, v.y0, runeTL}, {v.x1, v.y0, runeTR}, {v.x0, v.y1, runeBL}, {v.x1, v.y1, runeBR}}
521
522 for _, c := range corners {
523 if c.x >= 0 && c.y >= 0 && c.x < g.maxX && c.y < g.maxY {
524 if err := g.SetRune(c.x, c.y, c.ch, fgColor, bgColor); err != nil {
525 return err
526 }
527 }
528 }
529 return nil
530}
531
532// drawTitle draws the title of the view.
533func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
534 if v.y0 < 0 || v.y0 >= g.maxY {
535 return nil
536 }
537
538 for i, ch := range v.Title {
539 x := v.x0 + i + 2
540 if x < 0 {
541 continue
542 } else if x > v.x1-2 || x >= g.maxX {
543 break
544 }
545 if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil {
546 return err
547 }
548 }
549 return nil
550}
551
552// draw manages the cursor and calls the draw function of a view.
553func (g *Gui) draw(v *View) error {
554 if g.Cursor {
555 if curview := g.currentView; curview != nil {
556 vMaxX, vMaxY := curview.Size()
557 if curview.cx < 0 {
558 curview.cx = 0
559 } else if curview.cx >= vMaxX {
560 curview.cx = vMaxX - 1
561 }
562 if curview.cy < 0 {
563 curview.cy = 0
564 } else if curview.cy >= vMaxY {
565 curview.cy = vMaxY - 1
566 }
567
568 gMaxX, gMaxY := g.Size()
569 cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
570 if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
571 termbox.SetCursor(cx, cy)
572 } else {
573 termbox.HideCursor()
574 }
575 }
576 } else {
577 termbox.HideCursor()
578 }
579
580 v.clearRunes()
581 if err := v.draw(); err != nil {
582 return err
583 }
584 return nil
585}
586
587// onKey manages key-press events. A keybinding handler is called when
588// a key-press or mouse event satisfies a configured keybinding. Furthermore,
589// currentView's internal buffer is modified if currentView.Editable is true.
590func (g *Gui) onKey(ev *termbox.Event) error {
591 switch ev.Type {
592 case termbox.EventKey:
593 matched, err := g.execKeybindings(g.currentView, ev)
594 if err != nil {
595 return err
596 }
597 if matched {
598 break
599 }
600 if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil {
601 g.currentView.Editor.Edit(g.currentView, Key(ev.Key), ev.Ch, Modifier(ev.Mod))
602 }
603 case termbox.EventMouse:
604 mx, my := ev.MouseX, ev.MouseY
605 v, err := g.ViewByPosition(mx, my)
606 if err != nil {
607 break
608 }
609 if err := v.SetCursor(mx-v.x0-1, my-v.y0-1); err != nil {
610 return err
611 }
612 if _, err := g.execKeybindings(v, ev); err != nil {
613 return err
614 }
615 }
616
617 return nil
618}
619
620// execKeybindings executes the keybinding handlers that match the passed view
621// and event. The value of matched is true if there is a match and no errors.
622func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err error) {
623 matched = false
624 for _, kb := range g.keybindings {
625 if kb.handler == nil {
626 continue
627 }
628 if kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) && kb.matchView(v) {
629 if err := kb.handler(g, v); err != nil {
630 return false, err
631 }
632 matched = true
633 }
634 }
635 return matched, nil
636}