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