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

428 lines
7.0 KiB
Go

package exhibit
import (
"golang.org/x/sys/unix"
"bytes"
"fmt"
"io"
"log"
"os"
"sync"
"time"
"unicode/utf8"
)
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
in *os.File
outLock sync.Mutex
out *os.File
bufLock sync.Mutex
buffer *bytes.Buffer
sizeLock sync.Mutex
size Size
displayLock sync.Mutex
display [][]Cell
interLock sync.Mutex
interBuf []Cell
currentAttributes Attributes
cursorPosition Position
cursorVisible bool
termios unix.Termios
doneLock sync.Mutex
done bool
shutdown chan struct{}
event chan Event
}
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()), unix.TCGETS)
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
x, y := t.sizeInternal()
if x != 0 && y != 0 {
t.setSize(x, y)
}
t.enterAlt()
t.enterRaw()
t.SetCursor(0, 0)
t.Render()
t.watchSize()
t.watchInput()
return &t
}
func (t *Terminal) Shutdown() {
t.shutdown <- struct{}{}
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()
close(t.event)
}
func (t *Terminal) Clear() {
t.writeBuffer([]byte(clear))
}
func (t *Terminal) SetCursor(x, y int) {
t.writeBuffer([]byte(fmt.Sprintf(cup, y+1, x+1)))
t.cursorPosition.X = x
t.cursorPosition.Y = y
}
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() Size {
t.sizeLock.Lock()
defer t.sizeLock.Unlock()
return t.size
}
func (t *Terminal) setSize(x, y int) {
t.sizeLock.Lock()
defer t.sizeLock.Unlock()
if t.size.X == x && t.size.Y == y {
return
}
t.displayLock.Lock()
defer t.displayLock.Unlock()
t.size.X = x
t.size.Y = y
if t.display == nil {
t.display = make([][]Cell, x)
} else if len(t.display) < x {
t.display = append(t.display, make([][]Cell, x-len(t.display))...)
}
for i := 0; i < x; i++ {
if t.display[i] == nil {
t.display[i] = make([]Cell, y)
} else if len(t.display[i]) < y {
t.display[i] = append(t.display[i],
make([]Cell, y-len(t.display[i]))...)
}
}
t.Clear()
}
func (t *Terminal) WriteString(s string, x, y int, attrs Attributes) {
if len(s) == 0 || len(t.display) < x+1 || len(t.display[0]) < y+1 {
return
}
var j int
for i := 0; i < len(s); i++ {
r, sz := utf8.DecodeRuneInString(s[j:])
if sz > 0 {
cell := Cell{Position{x + i, y}, r, attrs}
if t.display[x+i][y] != cell {
t.interBuf = append(t.interBuf, cell)
}
j = j + sz
}
}
}
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.Size()
for _, c := range t.interBuf {
if c.Pos.X >= sz.X || c.Pos.Y >= sz.Y {
continue
}
t.display[c.Pos.X][c.Pos.Y] = c
if t.currentAttributes != c.Attrs {
changed = true
t.writeAttrs(c.Attrs)
}
if t.cursorPosition.X+1 != c.Pos.X {
t.SetCursor(c.Pos.X, c.Pos.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)))
if t.cursorPosition.X+1 <= t.size.X {
t.cursorPosition.X = 1
} else {
t.cursorPosition.X++
}
if t.cursorPosition.Y+1 <= t.size.Y {
t.cursorPosition.Y = 1
} else {
t.cursorPosition.Y++
}
}
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:
}
}
}()
}
func (t *Terminal) watchSize() {
go func() {
timer := time.NewTicker(1 * time.Second)
for {
select {
case <-t.shutdown:
timer.Stop()
break
case <-timer.C:
x, y := t.sizeInternal()
if x != 0 && y != 0 {
t.setSize(x, y)
}
continue
}
break
}
}()
}
func (t *Terminal) sizeInternal() (x, y int) {
sz, err := unix.IoctlGetWinsize(int(t.in.Fd()), unix.TIOCGWINSZ)
if err != nil {
return 0, 0
}
return 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(), unix.TCSETS, &termios)
if err != nil {
t.exitAlt()
t.out.Close()
t.in.Close()
log.Panic(err)
}
}
func (t *Terminal) exitRaw() {
err := unix.IoctlSetTermios(t.outFd(), unix.TCSETS, &t.termios)
if err != nil {
t.out.Close()
t.in.Close()
log.Panic(err)
}
}