1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package cui
5
6import (
7	"bufio"
8	"bytes"
9	"context"
10	"errors"
11	"sync"
12
13	termbox "github.com/nsf/termbox-go"
14)
15
16var initialized = false
17
18const padding = 2
19
20// Point is a 2D coordinate in console.
21//   X is the column
22//   Y is the row
23type Point struct{ X, Y int }
24
25// Rect is a 2D rectangle in console, excluding Max edge.
26type Rect struct{ Min, Max Point }
27
28// Screen is a writable area on screen.
29type Screen struct {
30	rendering sync.Mutex
31
32	blitting sync.Mutex
33	closed   bool
34	flushed  frame
35	pending  frame
36}
37
38type frame struct {
39	size    Point
40	content []byte
41}
42
43// NewScreen returns a new screen, only one screen can be use at a time.
44func NewScreen() (*Screen, error) {
45	if initialized {
46		return nil, errors.New("only one screen allowed at a time")
47	}
48	initialized = true
49	if err := termbox.Init(); err != nil {
50		initialized = false
51		return nil, err
52	}
53
54	termbox.SetInputMode(termbox.InputEsc)
55	termbox.HideCursor()
56	screen := &Screen{}
57	screen.flushed.size.X, screen.flushed.size.Y = termbox.Size()
58	screen.pending.size = screen.flushed.size
59	return screen, nil
60}
61
62func (screen *Screen) markClosed() {
63	screen.blitting.Lock()
64	screen.closed = true
65	screen.blitting.Unlock()
66}
67
68func (screen *Screen) isClosed() bool {
69	screen.blitting.Lock()
70	defer screen.blitting.Unlock()
71	return screen.closed
72}
73
74// Close closes the screen.
75func (screen *Screen) Close() error {
76	screen.markClosed()
77
78	// shutdown termbox
79	termbox.Close()
80	initialized = false
81	return nil
82}
83
84// Run runs the event loop.
85func (screen *Screen) Run() error {
86	defer screen.markClosed()
87
88	for !screen.isClosed() {
89		switch ev := termbox.PollEvent(); ev.Type {
90		case termbox.EventInterrupt:
91			// either screen refresh or close
92		case termbox.EventKey:
93			switch ev.Key {
94			case termbox.KeyCtrlC, termbox.KeyEsc:
95				return nil
96			default:
97				// ignore key presses
98			}
99		case termbox.EventError:
100			return ev.Err
101		case termbox.EventResize:
102			screen.blitting.Lock()
103			screen.flushed.size.X, screen.flushed.size.Y = ev.Width, ev.Height
104			err := screen.blit(&screen.flushed)
105			screen.blitting.Unlock()
106			if err != nil {
107				return err
108			}
109		}
110	}
111
112	return nil
113}
114
115// Size returns the current size of the screen.
116func (screen *Screen) Size() (width, height int) {
117	width, height = screen.pending.size.X-2*padding, screen.pending.size.Y-2*padding
118	if width < 0 {
119		width = 0
120	}
121	if height < 0 {
122		height = 0
123	}
124	return width, height
125}
126
127// Lock screen for exclusive rendering.
128func (screen *Screen) Lock() { screen.rendering.Lock() }
129
130// Unlock screen.
131func (screen *Screen) Unlock() { screen.rendering.Unlock() }
132
133// Write writes to the screen.
134func (screen *Screen) Write(data []byte) (int, error) {
135	screen.pending.content = append(screen.pending.content, data...)
136	return len(data), nil
137}
138
139// Flush flushes pending content to the console and clears for new frame.
140func (screen *Screen) Flush() error {
141	screen.blitting.Lock()
142	var err error
143	if !screen.closed {
144		err = screen.blit(&screen.pending)
145	} else {
146		err = context.Canceled
147	}
148	screen.pending.content = nil
149	screen.pending.size = screen.flushed.size
150	screen.blitting.Unlock()
151
152	return err
153}
154
155// blit writes content to the console.
156func (screen *Screen) blit(frame *frame) error {
157	screen.flushed.content = frame.content
158	size := screen.flushed.size
159
160	if err := termbox.Clear(termbox.ColorDefault, termbox.ColorDefault); err != nil {
161		return err
162	}
163
164	drawRect(Rect{
165		Min: Point{0, 0},
166		Max: size,
167	}, lightStyle)
168
169	scanner := bufio.NewScanner(bytes.NewReader(frame.content))
170	y := padding
171	for scanner.Scan() && y <= size.Y-2*padding {
172		x := padding
173		for _, r := range scanner.Text() {
174			if x > size.X-2*padding {
175				break
176			}
177			termbox.SetCell(x, y, r, termbox.ColorDefault, termbox.ColorDefault)
178			x++
179		}
180		y++
181	}
182
183	return termbox.Flush()
184}
185
186type rectStyle [3][3]rune
187
188var lightStyle = rectStyle{
189	{'┌', '─', '┐'},
190	{'│', ' ', '│'},
191	{'└', '─', '┘'},
192}
193
194// drawRect draws a rectangle using termbox.
195func drawRect(r Rect, style rectStyle) {
196	attr := termbox.ColorDefault
197
198	termbox.SetCell(r.Min.X, r.Min.Y, style[0][0], attr, attr)
199	termbox.SetCell(r.Max.X-1, r.Min.Y, style[0][2], attr, attr)
200	termbox.SetCell(r.Max.X-1, r.Max.Y-1, style[2][2], attr, attr)
201	termbox.SetCell(r.Min.X, r.Max.Y-1, style[2][0], attr, attr)
202
203	for x := r.Min.X + 1; x < r.Max.X-1; x++ {
204		termbox.SetCell(x, r.Min.Y, style[0][1], attr, attr)
205		termbox.SetCell(x, r.Max.Y-1, style[2][1], attr, attr)
206	}
207
208	for y := r.Min.Y + 1; y < r.Max.Y-1; y++ {
209		termbox.SetCell(r.Min.X, y, style[1][0], attr, attr)
210		termbox.SetCell(r.Max.X-1, y, style[1][2], attr, attr)
211	}
212}
213