387 lines
6.3 KiB
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)
|
|
}
|
|
}
|