storj/private/cui/screen.go
paul cannon 37a4edbaff all: reformat comments as required by gofmt 1.19
I don't know why the go people thought this was a good idea, because
this automatic reformatting is bound to do the wrong thing sometimes,
which is very annoying. But I don't see a way to turn it off, so best to
get this change out of the way.

Change-Id: Ib5dbbca6a6f6fc944d76c9b511b8c904f796e4f3
2022-08-10 18:24:55 +00:00

214 lines
4.6 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package cui
import (
"bufio"
"bytes"
"context"
"errors"
"sync"
termbox "github.com/nsf/termbox-go"
)
var initialized = false
const padding = 2
// Point is a 2D coordinate in console.
//
// X is the column
// Y is the row
type Point struct{ X, Y int }
// Rect is a 2D rectangle in console, excluding Max edge.
type Rect struct{ Min, Max Point }
// Screen is a writable area on screen.
type Screen struct {
rendering sync.Mutex
blitting sync.Mutex
closed bool
flushed frame
pending frame
}
type frame struct {
size Point
content []byte
}
// NewScreen returns a new screen, only one screen can be use at a time.
func NewScreen() (*Screen, error) {
if initialized {
return nil, errors.New("only one screen allowed at a time")
}
initialized = true
if err := termbox.Init(); err != nil {
initialized = false
return nil, err
}
termbox.SetInputMode(termbox.InputEsc)
termbox.HideCursor()
screen := &Screen{}
screen.flushed.size.X, screen.flushed.size.Y = termbox.Size()
screen.pending.size = screen.flushed.size
return screen, nil
}
func (screen *Screen) markClosed() {
screen.blitting.Lock()
screen.closed = true
screen.blitting.Unlock()
}
func (screen *Screen) isClosed() bool {
screen.blitting.Lock()
defer screen.blitting.Unlock()
return screen.closed
}
// Close closes the screen.
func (screen *Screen) Close() error {
screen.markClosed()
// shutdown termbox
termbox.Close()
initialized = false
return nil
}
// Run runs the event loop.
func (screen *Screen) Run() error {
defer screen.markClosed()
for !screen.isClosed() {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventInterrupt:
// either screen refresh or close
case termbox.EventKey:
switch ev.Key {
case termbox.KeyCtrlC, termbox.KeyEsc:
return nil
default:
// ignore key presses
}
case termbox.EventError:
return ev.Err
case termbox.EventResize:
screen.blitting.Lock()
screen.flushed.size.X, screen.flushed.size.Y = ev.Width, ev.Height
err := screen.blit(&screen.flushed)
screen.blitting.Unlock()
if err != nil {
return err
}
}
}
return nil
}
// Size returns the current size of the screen.
func (screen *Screen) Size() (width, height int) {
width, height = screen.pending.size.X-2*padding, screen.pending.size.Y-2*padding
if width < 0 {
width = 0
}
if height < 0 {
height = 0
}
return width, height
}
// Lock screen for exclusive rendering.
func (screen *Screen) Lock() { screen.rendering.Lock() }
// Unlock screen.
func (screen *Screen) Unlock() { screen.rendering.Unlock() }
// Write writes to the screen.
func (screen *Screen) Write(data []byte) (int, error) {
screen.pending.content = append(screen.pending.content, data...)
return len(data), nil
}
// Flush flushes pending content to the console and clears for new frame.
func (screen *Screen) Flush() error {
screen.blitting.Lock()
var err error
if !screen.closed {
err = screen.blit(&screen.pending)
} else {
err = context.Canceled
}
screen.pending.content = nil
screen.pending.size = screen.flushed.size
screen.blitting.Unlock()
return err
}
// blit writes content to the console.
func (screen *Screen) blit(frame *frame) error {
screen.flushed.content = frame.content
size := screen.flushed.size
if err := termbox.Clear(termbox.ColorDefault, termbox.ColorDefault); err != nil {
return err
}
drawRect(Rect{
Min: Point{0, 0},
Max: size,
}, lightStyle)
scanner := bufio.NewScanner(bytes.NewReader(frame.content))
y := padding
for scanner.Scan() && y <= size.Y-2*padding {
x := padding
for _, r := range scanner.Text() {
if x > size.X-2*padding {
break
}
termbox.SetCell(x, y, r, termbox.ColorDefault, termbox.ColorDefault)
x++
}
y++
}
return termbox.Flush()
}
type rectStyle [3][3]rune
var lightStyle = rectStyle{
{'┌', '─', '┐'},
{'│', ' ', '│'},
{'└', '─', '┘'},
}
// drawRect draws a rectangle using termbox.
func drawRect(r Rect, style rectStyle) {
attr := termbox.ColorDefault
termbox.SetCell(r.Min.X, r.Min.Y, style[0][0], attr, attr)
termbox.SetCell(r.Max.X-1, r.Min.Y, style[0][2], attr, attr)
termbox.SetCell(r.Max.X-1, r.Max.Y-1, style[2][2], attr, attr)
termbox.SetCell(r.Min.X, r.Max.Y-1, style[2][0], attr, attr)
for x := r.Min.X + 1; x < r.Max.X-1; x++ {
termbox.SetCell(x, r.Min.Y, style[0][1], attr, attr)
termbox.SetCell(x, r.Max.Y-1, style[2][1], attr, attr)
}
for y := r.Min.Y + 1; y < r.Max.Y-1; y++ {
termbox.SetCell(r.Min.X, y, style[1][0], attr, attr)
termbox.SetCell(r.Max.X-1, y, style[1][2], attr, attr)
}
}