This repository has been archived on 2022-11-30. You can view files and clone it, but cannot push or open issues or pull requests.
spectator/exhibit/terminal.go

387 lines
6.3 KiB
Go

package exhibit
import (
"bytes"
"fmt"
"image"
"io"
"log"
"os"
"sync"
"time"
"golang.org/x/sys/unix"
)
const (
smcup = "\x1b[?1049h"
rmcup = "\x1b[?1049l"
civis = "\x1b[?25l"
cvvis = "\x1b[?12;25h"
clear = "\x1b[2J"
sgr = "\x1b[%vm"
cup = "\x1b[%v;%vH"
)
type Terminal struct {
Event <-chan Event
SizeChange <-chan image.Point
in *os.File
outLock sync.Mutex
out *os.File
bufLock sync.Mutex
buffer *bytes.Buffer
displayLock sync.Mutex
display Block
interLock sync.Mutex
interBuf []Cell
currentAttributes Attributes
cursorVisible bool
termios unix.Termios
doneLock sync.Mutex
done bool
shutdown chan struct{}
event chan Event
sizeChange chan image.Point
}
func Init() *Terminal {
var t Terminal
out, err := os.OpenFile("/dev/tty", os.O_WRONLY|unix.O_NOCTTY, os.ModeCharDevice)
if err != nil {
log.Panic(err)
}
in, err := os.OpenFile("/dev/tty", os.O_RDONLY|unix.O_NOCTTY, os.ModeCharDevice)
if err != nil {
log.Panic(err)
}
termios, err := unix.IoctlGetTermios(int(out.Fd()), ioctlReadTermios)
if err != nil {
out.Close()
in.Close()
log.Panic(err)
}
t.buffer = &bytes.Buffer{}
t.out = out
t.in = in
t.termios = *termios
t.shutdown = make(chan struct{})
t.event = make(chan Event, 1024)
t.Event = t.event
t.sizeChange = make(chan image.Point, 32)
t.SizeChange = t.sizeChange
size := t.sizeInternal()
t.setSize(size)
t.enterAlt()
t.enterRaw()
t.SetCursor(0, 0)
t.Clear()
t.Render()
t.watchSize()
t.watchInput()
return &t
}
func (t *Terminal) Shutdown() {
t.doneLock.Lock()
t.done = true
t.doneLock.Unlock()
t.resetBuffer()
t.ShowCursor()
t.Render()
t.exitRaw()
t.exitAlt()
t.out.Close()
t.in.Close()
t.shutdown <- struct{}{}
}
func (t *Terminal) Clear() {
t.displayLock.Lock()
defer t.displayLock.Unlock()
t.display.Cells = make(map[image.Point]Cell)
t.writeBuffer([]byte(clear))
}
func (t *Terminal) SetCursor(x, y int) {
t.writeBuffer([]byte(fmt.Sprintf(cup, y+1, x+1)))
}
func (t *Terminal) ShowCursor() {
t.writeBuffer([]byte(cvvis))
t.cursorVisible = true
}
func (t *Terminal) HideCursor() {
t.writeBuffer([]byte(civis))
t.cursorVisible = false
}
func (t *Terminal) CursorVisible() bool {
return t.cursorVisible
}
func (t *Terminal) Size() image.Point {
t.displayLock.Lock()
defer t.displayLock.Unlock()
return t.display.Size()
}
func (t *Terminal) setSize(s image.Point) {
t.displayLock.Lock()
defer t.displayLock.Unlock()
if t.display.Size().X == s.X && t.display.Size().Y == s.Y {
return
}
t.display = NewBlock(0, 0, s.X, s.Y)
select {
case t.sizeChange <- s:
default:
}
}
func (t *Terminal) WriteCells(cells []Cell) {
if len(cells) == 0 {
return
}
t.interLock.Lock()
defer t.interLock.Unlock()
for _, cell := range cells {
t.interBuf = append(t.interBuf, cell)
}
}
func (t *Terminal) Render() {
t.doneLock.Lock()
defer t.doneLock.Unlock()
if t.done {
return
}
t.reconcileCells()
t.flush()
t.resetBuffer()
}
func (t *Terminal) writeBuffer(b []byte) {
t.bufLock.Lock()
defer t.bufLock.Unlock()
t.buffer.Write(b)
}
func (t *Terminal) resetBuffer() {
t.bufLock.Lock()
defer t.bufLock.Unlock()
t.buffer.Reset()
}
func (t *Terminal) flush() {
t.bufLock.Lock()
defer t.bufLock.Unlock()
t.outLock.Lock()
defer t.outLock.Unlock()
io.Copy(t.out, t.buffer)
}
func (t *Terminal) reconcileCells() {
t.displayLock.Lock()
defer t.displayLock.Unlock()
t.interLock.Lock()
defer t.interLock.Unlock()
var changed bool
sz := t.display.Size()
for _, c := range t.interBuf {
if c.Point.X >= sz.X || c.Point.Y >= sz.Y {
continue
}
if c.Point.X < 0 || c.Point.Y < 0 {
continue
}
current := t.display.Cells[c.Point]
if current.Value == c.Value && current.Attrs == c.Attrs {
continue
}
t.display.Cells[c.Point] = c
if t.currentAttributes != c.Attrs {
changed = true
t.writeAttrs(c.Attrs)
}
t.SetCursor(c.Point.X, c.Point.Y)
t.writeRune(c.Value)
}
if changed {
t.writeAttrs(Attributes{})
}
t.SetCursor(0, 0)
t.interBuf = make([]Cell, 0)
}
func (t *Terminal) writeRune(r rune) {
t.writeBuffer([]byte(string(r)))
}
func (t *Terminal) writeAttrs(attrs Attributes) {
if t.currentAttributes.ForegroundColor != attrs.ForegroundColor {
t.writeBuffer([]byte(fmt.Sprintf(sgr, attrs.ForegroundColor)))
t.currentAttributes.ForegroundColor = attrs.ForegroundColor
}
if t.currentAttributes.BackgroundColor != attrs.BackgroundColor {
t.writeBuffer([]byte(fmt.Sprintf(sgr, attrs.BackgroundColor)))
t.currentAttributes.BackgroundColor = attrs.BackgroundColor
}
}
func (t *Terminal) watchInput() {
go func() {
buf := make([]byte, 1)
for {
_, err := t.in.Read(buf)
if err != nil {
break
}
select {
case t.event <- Event(buf[0]):
default:
}
}
close(t.event)
}()
}
func (t *Terminal) watchSize() {
go func() {
timer := time.NewTicker(1 * time.Second)
for {
select {
case <-t.shutdown:
timer.Stop()
close(t.sizeChange)
break
case <-timer.C:
size := t.sizeInternal()
t.setSize(size)
continue
}
break
}
}()
}
func (t *Terminal) sizeInternal() image.Point {
sz, err := unix.IoctlGetWinsize(int(t.in.Fd()), unix.TIOCGWINSZ)
if err != nil {
return image.Point{}
}
return image.Pt(int(sz.Col), int(sz.Row))
}
func (t *Terminal) writeOut(b []byte) {
t.outLock.Lock()
defer t.outLock.Unlock()
t.out.Write(b)
}
func (t *Terminal) outFd() int {
t.outLock.Lock()
defer t.outLock.Unlock()
return int(t.out.Fd())
}
func (t *Terminal) enterAlt() {
t.writeOut([]byte(smcup))
t.resetBuffer()
}
func (t *Terminal) exitAlt() {
t.writeOut([]byte(rmcup))
t.resetBuffer()
}
func (t *Terminal) enterRaw() {
termios := t.termios
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP |
unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
termios.Oflag &^= unix.OPOST
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG |
unix.IEXTEN
termios.Cflag &^= unix.CSIZE | unix.PARENB
termios.Cflag |= unix.CS8
termios.Cc[unix.VMIN] = 1
termios.Cc[unix.VTIME] = 0
err := unix.IoctlSetTermios(t.outFd(), ioctlWriteTermios, &termios)
if err != nil {
t.exitAlt()
t.out.Close()
t.in.Close()
log.Panic(err)
}
}
func (t *Terminal) exitRaw() {
err := unix.IoctlSetTermios(t.outFd(), ioctlWriteTermios, &t.termios)
if err != nil {
t.out.Close()
t.in.Close()
log.Panic(err)
}
}