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