211 lines
4.7 KiB
Go
211 lines
4.7 KiB
Go
|
package terminal
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strconv"
|
||
|
"unicode"
|
||
|
|
||
|
"github.com/mediocregopher/ginger/gim/geo"
|
||
|
)
|
||
|
|
||
|
// Reset all custom styles
|
||
|
const ansiReset = "\033[0m"
|
||
|
|
||
|
// Color describes the foreground or background color of text
|
||
|
type Color int
|
||
|
|
||
|
// Available Color values
|
||
|
const (
|
||
|
// whatever the terminal's default color scheme is
|
||
|
Default = iota
|
||
|
|
||
|
Black
|
||
|
Red
|
||
|
Green
|
||
|
Yellow
|
||
|
Blue
|
||
|
Magenta
|
||
|
Cyan
|
||
|
White
|
||
|
)
|
||
|
|
||
|
type bufStyle struct {
|
||
|
fgColor Color
|
||
|
bgColor Color
|
||
|
}
|
||
|
|
||
|
// returns foreground and background ansi codes
|
||
|
func (bf bufStyle) ansi() (string, string) {
|
||
|
var fg, bg string
|
||
|
if bf.fgColor != Default {
|
||
|
fg = "\033[0;3" + strconv.Itoa(int(bf.fgColor)-1) + "m"
|
||
|
}
|
||
|
if bf.bgColor != Default {
|
||
|
bg = "\033[0;4" + strconv.Itoa(int(bf.bgColor)-1) + "m"
|
||
|
}
|
||
|
return fg, bg
|
||
|
}
|
||
|
|
||
|
// returns the ansi sequence which would modify the style to the given one
|
||
|
func (bf bufStyle) diffTo(bf2 bufStyle) string {
|
||
|
// this implementation is naive, but whatever
|
||
|
if bf == bf2 {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
fg, bg := bf2.ansi()
|
||
|
if (bf == bufStyle{}) {
|
||
|
return fg + bg
|
||
|
}
|
||
|
return ansiReset + fg + bg
|
||
|
}
|
||
|
|
||
|
type bufPoint struct {
|
||
|
r rune
|
||
|
bufStyle
|
||
|
}
|
||
|
|
||
|
// Buffer describes an infinitely sized terminal buffer to which anything may be
|
||
|
// drawn, and which will efficiently generate strings representing the drawn
|
||
|
// text.
|
||
|
type Buffer struct {
|
||
|
currStyle bufStyle
|
||
|
currPos geo.XY
|
||
|
m *mat
|
||
|
max geo.XY
|
||
|
}
|
||
|
|
||
|
// NewBuffer initializes and returns a new empty buffer. The proper way to clear
|
||
|
// a buffer is to toss the old one and generate a new one.
|
||
|
func NewBuffer() *Buffer {
|
||
|
return &Buffer{
|
||
|
m: newMat(),
|
||
|
max: geo.XY{-1, -1},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Copy creates a new identical instance of this Buffer and returns it.
|
||
|
func (b *Buffer) Copy() *Buffer {
|
||
|
b2 := NewBuffer()
|
||
|
b.m.iter(func(x, y int, v interface{}) bool {
|
||
|
b2.setRune(geo.XY{x, y}, v.(bufPoint))
|
||
|
return true
|
||
|
})
|
||
|
b2.currStyle = b.currStyle
|
||
|
b2.currPos = b.currPos
|
||
|
return b2
|
||
|
}
|
||
|
|
||
|
func (b *Buffer) setRune(at geo.XY, p bufPoint) {
|
||
|
b.m.set(at[0], at[1], p)
|
||
|
b.max = b.max.Max(at)
|
||
|
}
|
||
|
|
||
|
// WriteRune writes the given rune to the Buffer at whatever the current
|
||
|
// position is, with whatever the current styling is.
|
||
|
func (b *Buffer) WriteRune(r rune) {
|
||
|
if r == '\n' {
|
||
|
b.currPos[0], b.currPos[1] = 0, b.currPos[1]+1
|
||
|
return
|
||
|
} else if r == '\r' {
|
||
|
b.currPos[0] = 0
|
||
|
} else if !unicode.IsPrint(r) {
|
||
|
panic(fmt.Sprintf("character %q is not supported by terminal.Buffer", r))
|
||
|
}
|
||
|
|
||
|
b.setRune(b.currPos, bufPoint{
|
||
|
r: r,
|
||
|
bufStyle: b.currStyle,
|
||
|
})
|
||
|
b.currPos[0]++
|
||
|
}
|
||
|
|
||
|
// WriteString writes the given string to the Buffer at whatever the current
|
||
|
// position is, with whatever the current styling is.
|
||
|
func (b *Buffer) WriteString(s string) {
|
||
|
for _, r := range s {
|
||
|
b.WriteRune(r)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SetPos sets the cursor position in the Buffer, so Print operations will begin
|
||
|
// at that point. Remember that the origin is at point (0, 0).
|
||
|
func (b *Buffer) SetPos(xy geo.XY) {
|
||
|
b.currPos = xy
|
||
|
}
|
||
|
|
||
|
// SetFGColor sets subsequent text's foreground color.
|
||
|
func (b *Buffer) SetFGColor(c Color) {
|
||
|
b.currStyle.fgColor = c
|
||
|
}
|
||
|
|
||
|
// SetBGColor sets subsequent text's background color.
|
||
|
func (b *Buffer) SetBGColor(c Color) {
|
||
|
b.currStyle.bgColor = c
|
||
|
}
|
||
|
|
||
|
// ResetStyle unsets all text styling options which have been set.
|
||
|
func (b *Buffer) ResetStyle() {
|
||
|
b.currStyle = bufStyle{}
|
||
|
}
|
||
|
|
||
|
// String renders and returns a string which, when printed to a terminal, will
|
||
|
// print the Buffer's contents at the terminal's current cursor position.
|
||
|
func (b *Buffer) String() string {
|
||
|
s := ansiReset // always start with a reset
|
||
|
var style bufStyle
|
||
|
var pos geo.XY
|
||
|
move := func(to geo.XY) {
|
||
|
diff := to.Sub(pos)
|
||
|
if diff[0] > 0 {
|
||
|
s += "\033[" + strconv.Itoa(diff[0]) + "C"
|
||
|
} else if diff[0] < 0 {
|
||
|
s += "\033[" + strconv.Itoa(-diff[0]) + "D"
|
||
|
}
|
||
|
if diff[1] > 0 {
|
||
|
s += "\033[" + strconv.Itoa(diff[1]) + "B"
|
||
|
} else if diff[1] < 0 {
|
||
|
s += "\033[" + strconv.Itoa(-diff[1]) + "A"
|
||
|
}
|
||
|
pos = to
|
||
|
}
|
||
|
|
||
|
b.m.iter(func(x, y int, v interface{}) bool {
|
||
|
p := v.(bufPoint)
|
||
|
move(geo.XY{x, y})
|
||
|
s += style.diffTo(p.bufStyle)
|
||
|
style = p.bufStyle
|
||
|
s += string(p.r)
|
||
|
pos[0]++
|
||
|
return true
|
||
|
})
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// DrawBuffer copies the given Buffer onto this one, with the given's top-left
|
||
|
// corner being at the given position. The given buffer may be the same as this
|
||
|
// one.
|
||
|
//
|
||
|
// Calling this method does not affect this Buffer's current cursor position or
|
||
|
// style.
|
||
|
func (b *Buffer) DrawBuffer(at geo.XY, b2 *Buffer) {
|
||
|
if b == b2 {
|
||
|
b2 = b2.Copy()
|
||
|
}
|
||
|
b2.m.iter(func(x, y int, v interface{}) bool {
|
||
|
x += at[0]
|
||
|
y += at[1]
|
||
|
if x < 0 || y < 0 {
|
||
|
return true
|
||
|
}
|
||
|
b.setRune(geo.XY{x, y}, v.(bufPoint))
|
||
|
return true
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Size returns the dimensions of the Buffer's current area which has been
|
||
|
// written to.
|
||
|
func (b *Buffer) Size() geo.XY {
|
||
|
return b.max.Add(geo.XY{1, 1})
|
||
|
}
|