gui.go

  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}