gim: modify terminal to use a buffer, which will allow for much more complex behavior later
This commit is contained in:
parent
018e84575f
commit
97be10b03e
68
gim/box.go
68
gim/box.go
@ -9,24 +9,6 @@ import (
|
|||||||
"github.com/mediocregopher/ginger/gim/terminal"
|
"github.com/mediocregopher/ginger/gim/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
boxBorderHoriz = iota
|
|
||||||
boxBorderVert
|
|
||||||
boxBorderTL
|
|
||||||
boxBorderTR
|
|
||||||
boxBorderBL
|
|
||||||
boxBorderBR
|
|
||||||
)
|
|
||||||
|
|
||||||
var boxDefault = []string{
|
|
||||||
"─",
|
|
||||||
"│",
|
|
||||||
"┌",
|
|
||||||
"┐",
|
|
||||||
"└",
|
|
||||||
"┘",
|
|
||||||
}
|
|
||||||
|
|
||||||
type box struct {
|
type box struct {
|
||||||
topLeft geo.XY
|
topLeft geo.XY
|
||||||
flowDir geo.XY
|
flowDir geo.XY
|
||||||
@ -58,6 +40,7 @@ func (b box) bodyLines() []string {
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO this is utterly broken, the terminal.Buffer should be used for this
|
||||||
func (b box) bodySize() geo.XY {
|
func (b box) bodySize() geo.XY {
|
||||||
var size geo.XY
|
var size geo.XY
|
||||||
for _, line := range b.bodyLines() {
|
for _, line := range b.bodyLines() {
|
||||||
@ -102,47 +85,14 @@ func (b box) bodyRect() geo.Rect {
|
|||||||
return geo.Rect{Size: b.bodySize()}.Centered(center, rounder)
|
return geo.Rect{Size: b.bodySize()}.Centered(center, rounder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b box) draw(term *terminal.Terminal) {
|
func (b box) draw(buf *terminal.Buffer) {
|
||||||
chars := boxDefault
|
bodyBuf := terminal.NewBuffer()
|
||||||
|
bodyBuf.WriteString(b.body)
|
||||||
|
bodyBufRect := geo.Rect{Size: bodyBuf.Size()}
|
||||||
|
|
||||||
rect := b.rect()
|
rect := b.rect()
|
||||||
pos := rect.TopLeft
|
buf.DrawRect(rect, terminal.SingleLine)
|
||||||
w, h := rect.Size[0], rect.Size[1]
|
|
||||||
|
|
||||||
// draw top line
|
center := rect.Center(rounder)
|
||||||
term.MoveCursorTo(pos)
|
buf.DrawBuffer(bodyBufRect.Centered(center, rounder).TopLeft, bodyBuf)
|
||||||
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]++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
60
gim/line.go
60
gim/line.go
@ -5,38 +5,20 @@ import (
|
|||||||
"github.com/mediocregopher/ginger/gim/terminal"
|
"github.com/mediocregopher/ginger/gim/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var lineSegments = func() map[[2]geo.XY]string {
|
var edgeSegments = map[geo.XY]rune{
|
||||||
m := map[[2]geo.XY]string{
|
geo.Up: '┴',
|
||||||
{geo.Left, geo.Right}: "─",
|
geo.Down: '┬',
|
||||||
{geo.Down, geo.Up}: "│",
|
geo.Left: '┤',
|
||||||
{geo.Right, geo.Down}: "┌",
|
geo.Right: '├',
|
||||||
{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: "├",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// actual unicode arrows were fucking up my terminal, and they didn't even
|
// actual unicode arrows were fucking up my terminal, and they didn't even
|
||||||
// connect properly with the line segments anyway
|
// connect properly with the line segments anyway
|
||||||
var arrows = map[geo.XY]string{
|
var arrows = map[geo.XY]rune{
|
||||||
geo.Up: "^",
|
geo.Up: '^',
|
||||||
geo.Down: "v",
|
geo.Down: 'v',
|
||||||
geo.Left: "<",
|
geo.Left: '<',
|
||||||
geo.Right: ">",
|
geo.Right: '>',
|
||||||
}
|
}
|
||||||
|
|
||||||
type line struct {
|
type line struct {
|
||||||
@ -54,7 +36,7 @@ func secondaryDir(flowDir, start, end geo.XY) geo.XY {
|
|||||||
return end.Sub(start).Mul(perpDir.Abs()).Unit()
|
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)
|
from, to := *(l.from), *(l.to)
|
||||||
|
|
||||||
start := from.rect().Edge(flowDir, secFlowDir)[0].Add(secFlowDir.Scale(l.fromI*2 + 1))
|
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
|
// draw each point
|
||||||
for i, pt := range pts {
|
for i, pt := range pts {
|
||||||
var str string
|
var r rune
|
||||||
switch {
|
switch {
|
||||||
case i == 0:
|
case i == 0:
|
||||||
str = edgeSegments[flowDir]
|
r = edgeSegments[flowDir]
|
||||||
case i == len(pts)-1:
|
case i == len(pts)-1:
|
||||||
str = arrows[flowDir]
|
r = arrows[flowDir]
|
||||||
default:
|
default:
|
||||||
prev, next := pts[i-1], pts[i+1]
|
prev, next := pts[i-1], pts[i+1]
|
||||||
seg := [2]geo.XY{
|
r = terminal.SingleLine.Segment(prev.Sub(pt), next.Sub(pt))
|
||||||
prev.Sub(pt),
|
|
||||||
next.Sub(pt),
|
|
||||||
}
|
|
||||||
str = lineSegments[seg]
|
|
||||||
}
|
}
|
||||||
term.MoveCursorTo(pt)
|
buf.SetPos(pt)
|
||||||
term.Printf(str)
|
buf.WriteRune(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw the body
|
// draw the body
|
||||||
if l.body != "" {
|
if l.body != "" {
|
||||||
bodyPos := mid.Add(geo.Left.Scale(len(l.body) / 2))
|
bodyPos := mid.Add(geo.Left.Scale(len(l.body) / 2))
|
||||||
term.MoveCursorTo(bodyPos)
|
buf.SetPos(bodyPos)
|
||||||
term.Printf(l.body)
|
buf.WriteString(l.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
gim/main.go
13
gim/main.go
@ -80,8 +80,7 @@ func mkGraph() (*gg.Graph, gg.Value) {
|
|||||||
func main() {
|
func main() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
term := terminal.New()
|
term := terminal.New()
|
||||||
//term.Reset()
|
wSize := term.WindowSize()
|
||||||
//term.HideCursor()
|
|
||||||
|
|
||||||
g, start := mkGraph()
|
g, start := mkGraph()
|
||||||
v := view{
|
v := view{
|
||||||
@ -89,13 +88,11 @@ func main() {
|
|||||||
primFlowDir: geo.Right,
|
primFlowDir: geo.Right,
|
||||||
secFlowDir: geo.Down,
|
secFlowDir: geo.Down,
|
||||||
start: start,
|
start: start,
|
||||||
center: geo.Zero.Midpoint(term.WindowSize(), rounder),
|
center: geo.Zero.Midpoint(wSize, rounder),
|
||||||
}
|
}
|
||||||
|
|
||||||
//for range time.Tick(frameperiod) {
|
term.Clear()
|
||||||
term.Reset()
|
|
||||||
v.draw(term)
|
v.draw(term)
|
||||||
term.Flush()
|
term.SetPos(wSize.Add(geo.XY{0, -1}))
|
||||||
//}
|
term.Draw()
|
||||||
time.Sleep(1 * time.Hour)
|
|
||||||
}
|
}
|
||||||
|
210
gim/terminal/buffer.go
Normal file
210
gim/terminal/buffer.go
Normal 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})
|
||||||
|
}
|
59
gim/terminal/example/main.go
Normal file
59
gim/terminal/example/main.go
Normal 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
117
gim/terminal/mat.go
Normal 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
59
gim/terminal/mat_test.go
Normal 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
86
gim/terminal/shape.go
Normal 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)
|
||||||
|
}
|
@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unicode/utf8"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/mediocregopher/ginger/gim/geo"
|
"github.com/mediocregopher/ginger/gim/geo"
|
||||||
@ -34,7 +33,6 @@ import (
|
|||||||
//
|
//
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
buf *bytes.Buffer
|
buf *bytes.Buffer
|
||||||
pos geo.XY
|
|
||||||
|
|
||||||
// When initialized this will be set to os.Stdout, but can be set to
|
// When initialized this will be set to os.Stdout, but can be set to
|
||||||
// anything
|
// anything
|
||||||
@ -70,17 +68,10 @@ func (t *Terminal) WindowSize() geo.XY {
|
|||||||
return geo.XY{int(sz.cols), int(sz.rows)}
|
return geo.XY{int(sz.cols), int(sz.rows)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveCursorTo moves the cursor to the given position
|
// SetPos sets the terminal's actual cursor position to the given coordinates.
|
||||||
func (t *Terminal) MoveCursorTo(to geo.XY) {
|
func (t *Terminal) SetPos(to geo.XY) {
|
||||||
// actual terminal uses 1,1 as top-left, because 1-indexing is a great idea
|
// 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)
|
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
|
// HideCursor causes the cursor to not actually be shown
|
||||||
@ -93,64 +84,25 @@ func (t *Terminal) ShowCursor() {
|
|||||||
fmt.Fprintf(t.buf, "\033[?25h")
|
fmt.Fprintf(t.buf, "\033[?25h")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset completely clears all drawn characters on the screen and returns the
|
// Clear completely clears all drawn characters on the screen and returns the
|
||||||
// cursor to the origin
|
// cursor to the origin. This implicitly calls Draw.
|
||||||
func (t *Terminal) Reset() {
|
func (t *Terminal) Clear() {
|
||||||
|
t.buf.Reset()
|
||||||
fmt.Fprintf(t.buf, "\033[2J")
|
fmt.Fprintf(t.buf, "\033[2J")
|
||||||
|
t.Draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printf prints the given formatted string to the terminal, updating the
|
// WriteBuffer writes the contents to the Buffer to the Terminal's buffer,
|
||||||
// internal cursor position accordingly
|
// starting at the given coordinate.
|
||||||
func (t *Terminal) Printf(format string, args ...interface{}) {
|
func (t *Terminal) WriteBuffer(at geo.XY, b *Buffer) {
|
||||||
str := fmt.Sprintf(format, args...)
|
t.SetPos(at)
|
||||||
t.buf.WriteString(str)
|
t.buf.WriteString(b.String())
|
||||||
t.pos[0] += utf8.RuneCountInString(str)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush writes all buffered changes to the screen
|
// Draw writes all buffered changes to the screen
|
||||||
func (t *Terminal) Flush() {
|
func (t *Terminal) Draw() {
|
||||||
if _, err := io.Copy(t.Out, t.buf); err != nil {
|
if _, err := io.Copy(t.Out, t.buf); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
t.buf.Reset()
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ type view struct {
|
|||||||
g *gg.Graph
|
g *gg.Graph
|
||||||
primFlowDir, secFlowDir geo.XY
|
primFlowDir, secFlowDir geo.XY
|
||||||
start gg.Value
|
start gg.Value
|
||||||
center geo.XY
|
center geo.XY // TODO this shouldnt be needed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (view *view) draw(term *terminal.Terminal) {
|
func (view *view) draw(term *terminal.Terminal) {
|
||||||
@ -190,10 +190,12 @@ func (view *view) draw(term *terminal.Terminal) {
|
|||||||
centerBoxes(boxes, view.center)
|
centerBoxes(boxes, view.center)
|
||||||
|
|
||||||
// actually draw the boxes and lines
|
// actually draw the boxes and lines
|
||||||
|
buf := terminal.NewBuffer()
|
||||||
for _, b := range boxes {
|
for _, b := range boxes {
|
||||||
b.draw(term)
|
b.draw(buf)
|
||||||
}
|
}
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
line.draw(term, view.primFlowDir, view.secFlowDir)
|
line.draw(buf, view.primFlowDir, view.secFlowDir)
|
||||||
}
|
}
|
||||||
|
term.WriteBuffer(geo.Zero, buf)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user