gim: modify terminal to use a buffer, which will allow for much more complex behavior later

This commit is contained in:
Brian Picciano 2018-06-03 08:26:42 +00:00
parent 018e84575f
commit 97be10b03e
10 changed files with 584 additions and 174 deletions

View File

@ -9,24 +9,6 @@ import (
"github.com/mediocregopher/ginger/gim/terminal"
)
const (
boxBorderHoriz = iota
boxBorderVert
boxBorderTL
boxBorderTR
boxBorderBL
boxBorderBR
)
var boxDefault = []string{
"─",
"│",
"┌",
"┐",
"└",
"┘",
}
type box struct {
topLeft geo.XY
flowDir geo.XY
@ -58,6 +40,7 @@ func (b box) bodyLines() []string {
return lines
}
// TODO this is utterly broken, the terminal.Buffer should be used for this
func (b box) bodySize() geo.XY {
var size geo.XY
for _, line := range b.bodyLines() {
@ -102,47 +85,14 @@ func (b box) bodyRect() geo.Rect {
return geo.Rect{Size: b.bodySize()}.Centered(center, rounder)
}
func (b box) draw(term *terminal.Terminal) {
chars := boxDefault
func (b box) draw(buf *terminal.Buffer) {
bodyBuf := terminal.NewBuffer()
bodyBuf.WriteString(b.body)
bodyBufRect := geo.Rect{Size: bodyBuf.Size()}
rect := b.rect()
pos := rect.TopLeft
w, h := rect.Size[0], rect.Size[1]
buf.DrawRect(rect, terminal.SingleLine)
// draw top line
term.MoveCursorTo(pos)
term.Printf(chars[boxBorderTL])
for i := 0; i < w-2; i++ {
term.Printf(chars[boxBorderHoriz])
}
term.Printf(chars[boxBorderTR])
pos[1]++
// draw vertical lines
for i := 0; i < h-2; i++ {
term.MoveCursorTo(pos)
term.Printf(chars[boxBorderVert])
if b.transparent {
term.MoveCursorTo(pos.Add(geo.XY{w, 0}))
} else {
term.Printf(strings.Repeat(" ", w-2))
}
term.Printf(chars[boxBorderVert])
pos[1]++
}
// draw bottom line
term.MoveCursorTo(pos)
term.Printf(chars[boxBorderBL])
for i := 0; i < w-2; i++ {
term.Printf(chars[boxBorderHoriz])
}
term.Printf(chars[boxBorderBR])
// write out inner lines
pos = b.bodyRect().TopLeft
for _, line := range b.bodyLines() {
term.MoveCursorTo(pos)
term.Printf(line)
pos[1]++
}
center := rect.Center(rounder)
buf.DrawBuffer(bodyBufRect.Centered(center, rounder).TopLeft, bodyBuf)
}

View File

@ -5,38 +5,20 @@ import (
"github.com/mediocregopher/ginger/gim/terminal"
)
var lineSegments = func() map[[2]geo.XY]string {
m := map[[2]geo.XY]string{
{geo.Left, geo.Right}: "─",
{geo.Down, geo.Up}: "│",
{geo.Right, geo.Down}: "┌",
{geo.Left, geo.Down}: "┐",
{geo.Right, geo.Up}: "└",
{geo.Left, geo.Up}: "┘",
}
// the inverse segments use the same characters
for seg, str := range m {
seg[0], seg[1] = seg[1], seg[0]
m[seg] = str
}
return m
}()
var edgeSegments = map[geo.XY]string{
geo.Up: "┴",
geo.Down: "┬",
geo.Left: "┤",
geo.Right: "├",
var edgeSegments = map[geo.XY]rune{
geo.Up: '┴',
geo.Down: '┬',
geo.Left: '┤',
geo.Right: '├',
}
// actual unicode arrows were fucking up my terminal, and they didn't even
// connect properly with the line segments anyway
var arrows = map[geo.XY]string{
geo.Up: "^",
geo.Down: "v",
geo.Left: "<",
geo.Right: ">",
var arrows = map[geo.XY]rune{
geo.Up: '^',
geo.Down: 'v',
geo.Left: '<',
geo.Right: '>',
}
type line struct {
@ -54,7 +36,7 @@ func secondaryDir(flowDir, start, end geo.XY) geo.XY {
return end.Sub(start).Mul(perpDir.Abs()).Unit()
}
func (l line) draw(term *terminal.Terminal, flowDir, secFlowDir geo.XY) {
func (l line) draw(buf *terminal.Buffer, flowDir, secFlowDir geo.XY) {
from, to := *(l.from), *(l.to)
start := from.rect().Edge(flowDir, secFlowDir)[0].Add(secFlowDir.Scale(l.fromI*2 + 1))
@ -86,28 +68,24 @@ func (l line) draw(term *terminal.Terminal, flowDir, secFlowDir geo.XY) {
// draw each point
for i, pt := range pts {
var str string
var r rune
switch {
case i == 0:
str = edgeSegments[flowDir]
r = edgeSegments[flowDir]
case i == len(pts)-1:
str = arrows[flowDir]
r = arrows[flowDir]
default:
prev, next := pts[i-1], pts[i+1]
seg := [2]geo.XY{
prev.Sub(pt),
next.Sub(pt),
r = terminal.SingleLine.Segment(prev.Sub(pt), next.Sub(pt))
}
str = lineSegments[seg]
}
term.MoveCursorTo(pt)
term.Printf(str)
buf.SetPos(pt)
buf.WriteRune(r)
}
// draw the body
if l.body != "" {
bodyPos := mid.Add(geo.Left.Scale(len(l.body) / 2))
term.MoveCursorTo(bodyPos)
term.Printf(l.body)
buf.SetPos(bodyPos)
buf.WriteString(l.body)
}
}

View File

@ -80,8 +80,7 @@ func mkGraph() (*gg.Graph, gg.Value) {
func main() {
rand.Seed(time.Now().UnixNano())
term := terminal.New()
//term.Reset()
//term.HideCursor()
wSize := term.WindowSize()
g, start := mkGraph()
v := view{
@ -89,13 +88,11 @@ func main() {
primFlowDir: geo.Right,
secFlowDir: geo.Down,
start: start,
center: geo.Zero.Midpoint(term.WindowSize(), rounder),
center: geo.Zero.Midpoint(wSize, rounder),
}
//for range time.Tick(frameperiod) {
term.Reset()
term.Clear()
v.draw(term)
term.Flush()
//}
time.Sleep(1 * time.Hour)
term.SetPos(wSize.Add(geo.XY{0, -1}))
term.Draw()
}

210
gim/terminal/buffer.go Normal file
View File

@ -0,0 +1,210 @@
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})
}

View File

@ -0,0 +1,59 @@
package main
import (
"log"
"time"
"github.com/mediocregopher/ginger/gim/geo"
"github.com/mediocregopher/ginger/gim/terminal"
)
func main() {
b := terminal.NewBuffer()
b.WriteString("this is fun")
b.SetFGColor(terminal.Blue)
b.SetBGColor(terminal.Green)
b.SetPos(geo.XY{18, 0})
b.WriteString("blue and green")
b.ResetStyle()
b.SetFGColor(terminal.Red)
b.SetPos(geo.XY{3, 3})
b.WriteString("red!!!")
b.ResetStyle()
b.SetFGColor(terminal.Blue)
b.SetPos(geo.XY{20, 0})
b.WriteString("boo")
bcp := b.Copy()
b.DrawBuffer(geo.XY{2, 2}, bcp)
b.DrawBuffer(geo.XY{-1, 1}, bcp)
brect := terminal.NewBuffer()
brect.DrawRect(geo.Rect{Size: b.Size().Add(geo.XY{2, 2})}, terminal.SingleLine)
log.Printf("b.Size:%v", b.Size())
brect.DrawBuffer(geo.XY{1, 1}, b)
t := terminal.New()
p := geo.XY{0, 0}
dirH, dirV := geo.Right, geo.Down
wsize := t.WindowSize()
for range time.Tick(time.Second / 15) {
t.Clear()
t.WriteBuffer(p, brect)
t.Draw()
brectSize := brect.Size()
p = p.Add(dirH).Add(dirV)
if p[0] < 0 || p[0]+brectSize[0] > wsize[0] {
dirH = dirH.Scale(-1)
p = p.Add(dirH.Scale(2))
}
if p[1] < 0 || p[1]+brectSize[1] > wsize[1] {
dirV = dirV.Scale(-1)
p = p.Add(dirV.Scale(2))
}
}
}

117
gim/terminal/mat.go Normal file
View File

@ -0,0 +1,117 @@
package terminal
import (
"container/list"
)
type matEl struct {
x int
v interface{}
}
type matRow struct {
y int
l *list.List
}
// a 2-d sparse matrix
type mat struct {
rows *list.List
currY int
currRowEl *list.Element
currEl *list.Element
}
func newMat() *mat {
return &mat{
rows: list.New(),
}
}
func (m *mat) getRow(y int) *list.List {
m.currY = y // this will end up being true no matter what
if m.currRowEl == nil { // first call
l := list.New()
m.currRowEl = m.rows.PushFront(matRow{y: y, l: l})
return l
} else if m.currRowEl.Value.(matRow).y > y {
m.currRowEl = m.rows.Front()
}
for {
currRow := m.currRowEl.Value.(matRow)
switch {
case currRow.y == y:
return currRow.l
case currRow.y < y:
if m.currRowEl = m.currRowEl.Next(); m.currRowEl == nil {
l := list.New()
m.currRowEl = m.rows.PushBack(matRow{y: y, l: l})
return l
}
default: // currRow.y > y
l := list.New()
m.currRowEl = m.rows.InsertBefore(matRow{y: y, l: l}, m.currRowEl)
return l
}
}
}
func (m *mat) getEl(x, y int) *matEl {
var rowL *list.List
if m.currRowEl == nil || m.currY != y {
rowL = m.getRow(y)
m.currEl = rowL.Front()
} else {
rowL = m.currRowEl.Value.(matRow).l
}
if m.currEl == nil || m.currEl.Value.(*matEl).x > x {
if m.currEl = rowL.Front(); m.currEl == nil {
// row is empty
mel := &matEl{x: x}
m.currEl = rowL.PushFront(mel)
return mel
}
}
for {
currEl := m.currEl.Value.(*matEl)
switch {
case currEl.x == x:
return currEl
case currEl.x < x:
if m.currEl = m.currEl.Next(); m.currEl == nil {
mel := &matEl{x: x}
m.currEl = rowL.PushBack(mel)
return mel
}
default: // currEl.x > x
mel := &matEl{x: x}
m.currEl = rowL.InsertBefore(mel, m.currEl)
return mel
}
}
}
func (m *mat) get(x, y int) interface{} {
return m.getEl(x, y).v
}
func (m *mat) set(x, y int, v interface{}) {
m.getEl(x, y).v = v
}
func (m *mat) iter(f func(x, y int, v interface{}) bool) {
for rowEl := m.rows.Front(); rowEl != nil; rowEl = rowEl.Next() {
row := rowEl.Value.(matRow)
for el := row.l.Front(); el != nil; el = el.Next() {
mel := el.Value.(*matEl)
if !f(mel.x, row.y, mel.v) {
return
}
}
}
}

59
gim/terminal/mat_test.go Normal file
View File

@ -0,0 +1,59 @@
package terminal
import (
"fmt"
"math/rand"
"strings"
. "testing"
"time"
)
func TestMat(t *T) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
type xy struct {
x, y int
}
type action struct {
xy
set int
}
run := func(aa []action) {
aaStr := func(i int) string {
s := fmt.Sprintf("%#v", aa[:i+1])
return strings.Replace(s, "terminal.", "", -1)
}
m := newMat()
mm := map[xy]int{}
for i, a := range aa {
if a.set > 0 {
mm[a.xy] = a.set
m.set(a.xy.x, a.xy.y, a.set)
continue
}
expI, expOk := mm[a.xy]
gotI, gotOk := m.get(a.xy.x, a.xy.y).(int)
if expOk != gotOk {
t.Fatalf("get failed: expOk:%v gotOk:%v actions:%#v", expOk, gotOk, aaStr(i))
} else if expI != gotI {
t.Fatalf("get failed: expI:%v gotI:%v actions:%#v", expI, gotI, aaStr(i))
}
}
}
for i := 0; i < 10000; i++ {
var actions []action
for j := r.Intn(1000); j > 0; j-- {
a := action{xy: xy{x: r.Intn(5), y: r.Intn(5)}}
if r.Intn(3) == 0 {
a.set = r.Intn(10000) + 1
}
actions = append(actions, a)
}
run(actions)
}
}

86
gim/terminal/shape.go Normal file
View File

@ -0,0 +1,86 @@
package terminal
import (
"fmt"
"strings"
"github.com/mediocregopher/ginger/gim/geo"
)
// SingleLine is a set of single-pixel-width lines.
var SingleLine = LineStyle{
Horiz: '─',
Vert: '│',
TopLeft: '┌',
TopRight: '┐',
BottomLeft: '└',
BottomRight: '┘',
}
// LineStyle defines a set of characters to use together when drawing lines and
// corners.
type LineStyle struct {
Horiz, Vert rune
// Corner characters, identified as corners of a rectangle
TopLeft, TopRight, BottomLeft, BottomRight rune
}
// Segment takes two different directions (i.e. geo.Up/Down/Left/Right) and
// returns the line character which points in both of those directions.
//
// For example, SingleLine.Segment(geo.Up, geo.Left) returns '┘'.
func (ls LineStyle) Segment(a, b geo.XY) rune {
inner := func(a, b geo.XY) rune {
type c struct{ a, b geo.XY }
switch (c{a, b}) {
case c{geo.Up, geo.Down}:
return ls.Vert
case c{geo.Left, geo.Right}:
return ls.Horiz
case c{geo.Down, geo.Right}:
return ls.TopLeft
case c{geo.Down, geo.Left}:
return ls.TopRight
case c{geo.Up, geo.Right}:
return ls.BottomLeft
case c{geo.Up, geo.Left}:
return ls.BottomRight
default:
return 0
}
}
if r := inner(a, b); r != 0 {
return r
} else if r = inner(b, a); r != 0 {
return r
}
panic(fmt.Sprintf("invalid LineStyle.Segment directions: %v, %v", a, b))
}
// DrawRect draws the given Rect to the Buffer with the given LineStyle. The
// Rect's TopLeft field is used for its position.
//
// If Rect's Size is not at least 2x2 this does nothing.
func (b *Buffer) DrawRect(r geo.Rect, ls LineStyle) {
if r.Size[0] < 2 || r.Size[1] < 2 {
return
}
horiz := strings.Repeat(string(ls.Horiz), r.Size[0]-2)
b.SetPos(r.TopLeft)
b.WriteRune(ls.TopLeft)
b.WriteString(horiz)
b.WriteRune(ls.TopRight)
for i := 0; i < r.Size[1]-2; i++ {
b.SetPos(r.TopLeft.Add(geo.XY{0, i + 1}))
b.WriteRune(ls.Vert)
b.SetPos(r.TopLeft.Add(geo.XY{r.Size[0] - 1, i + 1}))
b.WriteRune(ls.Vert)
}
b.SetPos(r.TopLeft.Add(geo.XY{0, r.Size[1] - 1}))
b.WriteRune(ls.BottomLeft)
b.WriteString(horiz)
b.WriteRune(ls.BottomRight)
}

View File

@ -8,7 +8,6 @@ import (
"io"
"os"
"syscall"
"unicode/utf8"
"unsafe"
"github.com/mediocregopher/ginger/gim/geo"
@ -34,7 +33,6 @@ import (
//
type Terminal struct {
buf *bytes.Buffer
pos geo.XY
// When initialized this will be set to os.Stdout, but can be set to
// anything
@ -70,17 +68,10 @@ func (t *Terminal) WindowSize() geo.XY {
return geo.XY{int(sz.cols), int(sz.rows)}
}
// MoveCursorTo moves the cursor to the given position
func (t *Terminal) MoveCursorTo(to geo.XY) {
// SetPos sets the terminal's actual cursor position to the given coordinates.
func (t *Terminal) SetPos(to geo.XY) {
// actual terminal uses 1,1 as top-left, because 1-indexing is a great idea
fmt.Fprintf(t.buf, "\033[%d;%dH", to[1]+1, to[0]+1)
t.pos = to
}
// MoveCursor moves the cursor relative to its current position by the given
// vector
func (t *Terminal) MoveCursor(by geo.XY) {
t.MoveCursorTo(t.pos.Add(by))
}
// HideCursor causes the cursor to not actually be shown
@ -93,64 +84,25 @@ func (t *Terminal) ShowCursor() {
fmt.Fprintf(t.buf, "\033[?25h")
}
// Reset completely clears all drawn characters on the screen and returns the
// cursor to the origin
func (t *Terminal) Reset() {
// Clear completely clears all drawn characters on the screen and returns the
// cursor to the origin. This implicitly calls Draw.
func (t *Terminal) Clear() {
t.buf.Reset()
fmt.Fprintf(t.buf, "\033[2J")
t.Draw()
}
// Printf prints the given formatted string to the terminal, updating the
// internal cursor position accordingly
func (t *Terminal) Printf(format string, args ...interface{}) {
str := fmt.Sprintf(format, args...)
t.buf.WriteString(str)
t.pos[0] += utf8.RuneCountInString(str)
// WriteBuffer writes the contents to the Buffer to the Terminal's buffer,
// starting at the given coordinate.
func (t *Terminal) WriteBuffer(at geo.XY, b *Buffer) {
t.SetPos(at)
t.buf.WriteString(b.String())
}
// Flush writes all buffered changes to the screen
func (t *Terminal) Flush() {
// Draw writes all buffered changes to the screen
func (t *Terminal) Draw() {
if _, err := io.Copy(t.Out, t.buf); err != nil {
panic(err)
}
}
// TODO deal with these
const (
// Reset all custom styles
ansiReset = "\033[0m"
// Reset to default color
ansiResetColor = "\033[32m"
// Return curor to start of line and clean it
ansiResetLine = "\r\033[K"
)
// List of possible colors
const (
black = iota
red
green
yellow
blue
magenta
cyan
white
)
func getFgColor(code int) string {
return fmt.Sprintf("\033[3%dm", code)
}
func getBgColor(code int) string {
return fmt.Sprintf("\033[4%dm", code)
}
func fgColor(str string, color int) string {
return fmt.Sprintf("%s%s%s", getFgColor(color), str, ansiReset)
}
func bgColor(str string, color int) string {
return fmt.Sprintf("%s%s%s", getBgColor(color), str, ansiReset)
t.buf.Reset()
}

View File

@ -104,7 +104,7 @@ type view struct {
g *gg.Graph
primFlowDir, secFlowDir geo.XY
start gg.Value
center geo.XY
center geo.XY // TODO this shouldnt be needed
}
func (view *view) draw(term *terminal.Terminal) {
@ -190,10 +190,12 @@ func (view *view) draw(term *terminal.Terminal) {
centerBoxes(boxes, view.center)
// actually draw the boxes and lines
buf := terminal.NewBuffer()
for _, b := range boxes {
b.draw(term)
b.draw(buf)
}
for _, line := range lines {
line.draw(term, view.primFlowDir, view.secFlowDir)
line.draw(buf, view.primFlowDir, view.secFlowDir)
}
term.WriteBuffer(geo.Zero, buf)
}