drawing edges in gim, and split out some parts into their own packages
This commit is contained in:
parent
b4bf8f6c5a
commit
11328fa76c
157
gim/box.go
Normal file
157
gim/box.go
Normal file
@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mediocregopher/ginger/gim/geo"
|
||||
"github.com/mediocregopher/ginger/gim/terminal"
|
||||
)
|
||||
|
||||
const (
|
||||
boxBorderHoriz = iota
|
||||
boxBorderVert
|
||||
boxBorderTL
|
||||
boxBorderTR
|
||||
boxBorderBL
|
||||
boxBorderBR
|
||||
)
|
||||
|
||||
var boxDefault = []string{
|
||||
"─",
|
||||
"│",
|
||||
"┌",
|
||||
"┐",
|
||||
"└",
|
||||
"┘",
|
||||
}
|
||||
|
||||
type box struct {
|
||||
pos geo.XY
|
||||
size geo.XY // if unset, auto-determined
|
||||
body string
|
||||
|
||||
transparent bool
|
||||
}
|
||||
|
||||
func (b box) lines() []string {
|
||||
lines := strings.Split(b.body, "\n")
|
||||
// if the last line is empty don't include it, it means there was a trailing
|
||||
// newline (or the whole string is empty)
|
||||
if lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (b box) innerSize() geo.XY {
|
||||
if b.size != (geo.XY{}) {
|
||||
return b.size
|
||||
}
|
||||
var size geo.XY
|
||||
for _, line := range b.lines() {
|
||||
size[1]++
|
||||
if l := len(line); l > size[0] {
|
||||
size[0] = l
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (b box) rectSize() geo.XY {
|
||||
return b.innerSize().Add(geo.XY{2, 2})
|
||||
}
|
||||
|
||||
// edge returns the coordinate of the edge indicated by the given direction (Up,
|
||||
// Down, Left, or Right). The coordinate will be for the axis applicable to the
|
||||
// direction, so for Left/Right it will be the x coordinate and for Up/Down the
|
||||
// y.
|
||||
func (b box) rectEdge(dir geo.XY) int {
|
||||
size := b.rectSize()
|
||||
switch dir {
|
||||
case geo.Up:
|
||||
return b.pos[1]
|
||||
case geo.Down:
|
||||
return b.pos[1] + size[1]
|
||||
case geo.Left:
|
||||
return b.pos[0]
|
||||
case geo.Right:
|
||||
return b.pos[0] + size[0]
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported direction: %#v", dir))
|
||||
}
|
||||
}
|
||||
|
||||
func (b box) rectCorner(xDir, yDir geo.XY) geo.XY {
|
||||
switch {
|
||||
case xDir == geo.Left && yDir == geo.Up:
|
||||
return b.pos
|
||||
case xDir == geo.Right && yDir == geo.Up:
|
||||
size := b.rectSize()
|
||||
return b.pos.Add(size.Mul(geo.Right)).Add(geo.XY{-1, 0})
|
||||
case xDir == geo.Left && yDir == geo.Down:
|
||||
size := b.rectSize()
|
||||
return b.pos.Add(size.Mul(geo.Down)).Add(geo.XY{0, -1})
|
||||
case xDir == geo.Right && yDir == geo.Down:
|
||||
size := b.rectSize()
|
||||
return b.pos.Add(size).Add(geo.XY{-1, -1})
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported rectCorner args: %v, %v", xDir, yDir))
|
||||
}
|
||||
}
|
||||
|
||||
func (b box) draw(term *terminal.Terminal) {
|
||||
chars := boxDefault
|
||||
pos := b.pos
|
||||
size := b.innerSize()
|
||||
w, h := size[0], size[1]
|
||||
|
||||
// draw top line
|
||||
term.MoveCursorTo(pos)
|
||||
term.Printf(chars[boxBorderTL])
|
||||
for i := 0; i < w; i++ {
|
||||
term.Printf(chars[boxBorderHoriz])
|
||||
}
|
||||
term.Printf(chars[boxBorderTR])
|
||||
|
||||
drawLine := func(line string) {
|
||||
pos[1]++
|
||||
term.MoveCursorTo(pos)
|
||||
term.Printf(chars[boxBorderVert])
|
||||
if len(line) > w {
|
||||
line = line[:w]
|
||||
}
|
||||
term.Printf(line)
|
||||
if b.transparent {
|
||||
term.MoveCursor(geo.XY{w + 1, 0})
|
||||
} else {
|
||||
term.Printf(strings.Repeat(" ", w-len(line)))
|
||||
}
|
||||
term.Printf(chars[boxBorderVert])
|
||||
}
|
||||
|
||||
// truncate lines if necessary
|
||||
lines := b.lines()
|
||||
if len(lines) > h {
|
||||
lines = lines[:h]
|
||||
}
|
||||
|
||||
// draw body
|
||||
for _, line := range lines {
|
||||
drawLine(line)
|
||||
}
|
||||
|
||||
// draw empty lines
|
||||
for i := 0; i < h-len(lines); i++ {
|
||||
drawLine("")
|
||||
}
|
||||
|
||||
// draw bottom line
|
||||
pos[1]++
|
||||
term.MoveCursorTo(pos)
|
||||
term.Printf(chars[boxBorderBL])
|
||||
for i := 0; i < w; i++ {
|
||||
term.Printf(chars[boxBorderHoriz])
|
||||
}
|
||||
term.Printf(chars[boxBorderBR])
|
||||
}
|
28
gim/box_test.go
Normal file
28
gim/box_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
. "testing"
|
||||
|
||||
"github.com/mediocregopher/ginger/gim/geo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBox(t *T) {
|
||||
b := box{
|
||||
pos: geo.XY{1, 2},
|
||||
size: geo.XY{10, 11},
|
||||
}
|
||||
|
||||
assert.Equal(t, geo.XY{10, 11}, b.innerSize())
|
||||
assert.Equal(t, geo.XY{12, 13}, b.rectSize())
|
||||
|
||||
assert.Equal(t, 2, b.rectEdge(geo.Up))
|
||||
assert.Equal(t, 15, b.rectEdge(geo.Down))
|
||||
assert.Equal(t, 1, b.rectEdge(geo.Left))
|
||||
assert.Equal(t, 13, b.rectEdge(geo.Right))
|
||||
|
||||
assert.Equal(t, geo.XY{1, 2}, b.rectCorner(geo.Left, geo.Up))
|
||||
assert.Equal(t, geo.XY{1, 14}, b.rectCorner(geo.Left, geo.Down))
|
||||
assert.Equal(t, geo.XY{12, 2}, b.rectCorner(geo.Right, geo.Up))
|
||||
assert.Equal(t, geo.XY{12, 14}, b.rectCorner(geo.Right, geo.Down))
|
||||
}
|
87
gim/geo/geo.go
Normal file
87
gim/geo/geo.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Package geo implements basic geometric concepts used by gim
|
||||
package geo
|
||||
|
||||
import "math"
|
||||
|
||||
// XY describes a 2-dimensional position or vector. The origin of the
|
||||
// 2-dimensional space is a 0,0, with the x-axis going to the left and the
|
||||
// y-axis going down.
|
||||
type XY [2]int
|
||||
|
||||
// Zero is the zero point, or a zero vector, depending on what you're doing
|
||||
var Zero = XY{0, 0}
|
||||
|
||||
// Unit vectors
|
||||
var (
|
||||
Up = XY{0, -1}
|
||||
Down = XY{0, 1}
|
||||
Left = XY{-1, 0}
|
||||
Right = XY{1, 0}
|
||||
)
|
||||
|
||||
// Add returns the result of adding the two XYs' fields individually
|
||||
func (xy XY) Add(xy2 XY) XY {
|
||||
xy[0] += xy2[0]
|
||||
xy[1] += xy2[1]
|
||||
return xy
|
||||
}
|
||||
|
||||
// Mul returns the result of multiplying the two XYs' fields individually
|
||||
func (xy XY) Mul(xy2 XY) XY {
|
||||
xy[0] *= xy2[0]
|
||||
xy[1] *= xy2[1]
|
||||
return xy
|
||||
}
|
||||
|
||||
// Scale returns the result of multiplying both of the XY's fields by the scalar
|
||||
func (xy XY) Scale(scalar int) XY {
|
||||
return xy.Mul(XY{scalar, scalar})
|
||||
}
|
||||
|
||||
// Inv inverses the XY, a shortcut for xy.Scale(-1)
|
||||
func (xy XY) Inv() XY {
|
||||
return xy.Scale(-1)
|
||||
}
|
||||
|
||||
// Sub subtracts xy2 from xy and returns the result. A shortcut for
|
||||
// xy.Add(xy2.Inv())
|
||||
func (xy XY) Sub(xy2 XY) XY {
|
||||
return xy.Add(xy2.Inv())
|
||||
}
|
||||
|
||||
func round(f float64, r int) int {
|
||||
switch {
|
||||
case r < 0:
|
||||
f = math.Floor(f)
|
||||
case r == 0:
|
||||
if f < 0 {
|
||||
f = math.Ceil(f - 0.5)
|
||||
}
|
||||
f = math.Floor(f + 0.5)
|
||||
case r > 0:
|
||||
f = math.Ceil(f)
|
||||
}
|
||||
return int(f)
|
||||
}
|
||||
|
||||
func (xy XY) toF64() [2]float64 {
|
||||
return [2]float64{
|
||||
float64(xy[0]),
|
||||
float64(xy[1]),
|
||||
}
|
||||
}
|
||||
|
||||
// Midpoint returns the midpoint between the two XYs. The rounder indicates what
|
||||
// to do about non-whole values when they're come across:
|
||||
// - rounder < 0 : floor
|
||||
// - rounder = 0 : round
|
||||
// - rounder > 0 : ceil
|
||||
func (xy XY) Midpoint(xy2 XY, rounder int) XY {
|
||||
xyf, xy2f := xy.toF64(), xy2.toF64()
|
||||
xf := xyf[0] + ((xy2f[0] - xyf[0]) / 2)
|
||||
yf := xyf[1] + ((xy2f[1] - xyf[1]) / 2)
|
||||
return XY{
|
||||
round(xf, rounder),
|
||||
round(yf, rounder),
|
||||
}
|
||||
}
|
178
gim/line.go
Normal file
178
gim/line.go
Normal file
@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mediocregopher/ginger/gim/geo"
|
||||
"github.com/mediocregopher/ginger/gim/terminal"
|
||||
)
|
||||
|
||||
// boxEdgeAdj returns the midpoint of a box's edge, using the given direction
|
||||
// (single-dimension unit-vector) to know which edge to look at.
|
||||
func boxEdgeAdj(box box, dir geo.XY) geo.XY {
|
||||
var a, b geo.XY
|
||||
switch dir {
|
||||
case geo.Up:
|
||||
a, b = box.rectCorner(geo.Left, geo.Up), box.rectCorner(geo.Right, geo.Up)
|
||||
case geo.Down:
|
||||
a, b = box.rectCorner(geo.Left, geo.Down), box.rectCorner(geo.Right, geo.Down)
|
||||
case geo.Left:
|
||||
a, b = box.rectCorner(geo.Left, geo.Up), box.rectCorner(geo.Left, geo.Down)
|
||||
case geo.Right:
|
||||
a, b = box.rectCorner(geo.Right, geo.Up), box.rectCorner(geo.Right, geo.Down)
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported direction: %#v", dir))
|
||||
}
|
||||
|
||||
mid := a.Midpoint(b, 0)
|
||||
return mid
|
||||
}
|
||||
|
||||
var dirs = []geo.XY{
|
||||
geo.Up,
|
||||
geo.Down,
|
||||
geo.Left,
|
||||
geo.Right,
|
||||
}
|
||||
|
||||
// boxesRelDir returns the "best" direction between from and to. Returns
|
||||
// geo.Zero if they overlap. It also returns the secondary direction. E.g. Down
|
||||
// and Left. The secondary direction will never be zero if primary is given,
|
||||
// even if the two boxes are in-line
|
||||
func boxesRelDir(from, to box) (geo.XY, geo.XY) {
|
||||
rels := make([]int, len(dirs))
|
||||
for i, dir := range dirs {
|
||||
rels[i] = to.rectEdge(dir.Inv()) - from.rectEdge(dir)
|
||||
if dir == geo.Up || dir == geo.Left {
|
||||
rels[i] *= -1
|
||||
}
|
||||
}
|
||||
|
||||
// find primary
|
||||
var primary geo.XY
|
||||
var primaryMax int
|
||||
for i, rel := range rels {
|
||||
if rel < 0 {
|
||||
continue
|
||||
} else if rel > primaryMax || i == 0 {
|
||||
primary = dirs[i]
|
||||
primaryMax = rel
|
||||
}
|
||||
}
|
||||
|
||||
// if all rels were negative the boxes are overlapping, return zeros
|
||||
if primary == geo.Zero {
|
||||
return geo.Zero, geo.Zero
|
||||
}
|
||||
|
||||
// now find secondary, which must be perpendicular to primary
|
||||
var secondary geo.XY
|
||||
var secondaryMax int
|
||||
var secondarySet bool
|
||||
for i, rel := range rels {
|
||||
if dirs[i] == primary {
|
||||
continue
|
||||
} else if dirs[i][0] == 0 && primary[0] == 0 {
|
||||
continue
|
||||
} else if dirs[i][1] == 0 && primary[1] == 0 {
|
||||
continue
|
||||
} else if !secondarySet || rel > secondaryMax {
|
||||
secondary = dirs[i]
|
||||
secondaryMax = rel
|
||||
secondarySet = true
|
||||
}
|
||||
}
|
||||
|
||||
return primary, secondary
|
||||
}
|
||||
|
||||
// liner will draw a line from one box to another
|
||||
type liner func(*terminal.Terminal, box, box)
|
||||
|
||||
var lineSegments = func() map[[2]geo.XY]string {
|
||||
m := map[[2]geo.XY]string{
|
||||
{{-1, 0}, {1, 0}}: "─",
|
||||
{{0, 1}, {0, -1}}: "│",
|
||||
{{1, 0}, {0, 1}}: "┌",
|
||||
{{-1, 0}, {0, 1}}: "┐",
|
||||
{{1, 0}, {0, -1}}: "└",
|
||||
{{-1, 0}, {0, -1}}: "┘",
|
||||
}
|
||||
|
||||
// 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
|
||||
// connect properly with the line segments anyway
|
||||
var arrows = map[geo.XY]string{
|
||||
geo.Up: "^",
|
||||
geo.Down: "v",
|
||||
geo.Left: "<",
|
||||
geo.Right: ">",
|
||||
}
|
||||
|
||||
func basicLine(term *terminal.Terminal, from, to box) {
|
||||
dir, dirSec := boxesRelDir(from, to)
|
||||
|
||||
// if the boxes overlap then don't draw anything
|
||||
if dir == geo.Zero {
|
||||
return
|
||||
}
|
||||
|
||||
dirInv := dir.Inv()
|
||||
start := boxEdgeAdj(from, dir)
|
||||
end := boxEdgeAdj(to, dirInv)
|
||||
mid := start.Midpoint(end, 0)
|
||||
|
||||
along := func(xy, dir geo.XY) int {
|
||||
if dir[0] != 0 {
|
||||
return xy[0]
|
||||
}
|
||||
return xy[1]
|
||||
}
|
||||
|
||||
var pts []geo.XY
|
||||
midPrim := along(mid, dir)
|
||||
endSec := along(end, dirSec)
|
||||
for curr := start; curr != end; {
|
||||
pts = append(pts, curr)
|
||||
if prim := along(curr, dir); prim == midPrim {
|
||||
if sec := along(curr, dirSec); sec != endSec {
|
||||
curr = curr.Add(dirSec)
|
||||
continue
|
||||
}
|
||||
}
|
||||
curr = curr.Add(dir)
|
||||
}
|
||||
|
||||
for i, pt := range pts {
|
||||
var str string
|
||||
switch {
|
||||
case i == 0:
|
||||
str = edgeSegments[dir]
|
||||
case i == len(pts)-1:
|
||||
str = arrows[dir]
|
||||
default:
|
||||
prev, next := pts[i-1], pts[i+1]
|
||||
seg := [2]geo.XY{
|
||||
prev.Sub(pt),
|
||||
next.Sub(pt),
|
||||
}
|
||||
str = lineSegments[seg]
|
||||
}
|
||||
term.MoveCursorTo(pt)
|
||||
term.Printf(str)
|
||||
}
|
||||
}
|
247
gim/main.go
247
gim/main.go
@ -4,217 +4,34 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/mediocregopher/ginger/gim/geo"
|
||||
"github.com/mediocregopher/ginger/gim/terminal"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type xy [2]int
|
||||
|
||||
func (p xy) x() int {
|
||||
return p[0]
|
||||
}
|
||||
|
||||
func (p xy) y() int {
|
||||
return p[1]
|
||||
}
|
||||
|
||||
func (p xy) add(p2 xy) xy {
|
||||
p[0] += p2[0]
|
||||
p[1] += p2[1]
|
||||
return p
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type terminal struct {
|
||||
cursorPos xy
|
||||
}
|
||||
|
||||
func (t *terminal) moveAbs(to xy) {
|
||||
t.cursorPos = to
|
||||
goterm.MoveCursor(to.x()+1, to.y()+1)
|
||||
}
|
||||
|
||||
func (t *terminal) size() xy {
|
||||
return xy{goterm.Width(), goterm.Height()}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const (
|
||||
boxBorderHoriz = iota
|
||||
boxBorderVert
|
||||
boxBorderTL
|
||||
boxBorderTR
|
||||
boxBorderBL
|
||||
boxBorderBR
|
||||
)
|
||||
|
||||
var boxDefault = []string{
|
||||
"─",
|
||||
"│",
|
||||
"┌",
|
||||
"┐",
|
||||
"└",
|
||||
"┘",
|
||||
}
|
||||
|
||||
type box struct {
|
||||
pos xy
|
||||
size xy // if unset, auto-determined
|
||||
body string
|
||||
|
||||
transparent bool
|
||||
}
|
||||
|
||||
func (b box) lines() []string {
|
||||
lines := strings.Split(b.body, "\n")
|
||||
// if the last line is empty don't include it, it means there was a trailing
|
||||
// newline (or the whole string is empty)
|
||||
if lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (b box) getSize() xy {
|
||||
if b.size != (xy{}) {
|
||||
return b.size
|
||||
}
|
||||
var size xy
|
||||
for _, line := range b.lines() {
|
||||
size[1]++
|
||||
if l := len(line); l > size[0] {
|
||||
size[0] = l
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (b box) draw(term *terminal) {
|
||||
chars := boxDefault
|
||||
pos := b.pos
|
||||
size := b.getSize()
|
||||
w, h := size.x(), size.y()
|
||||
|
||||
// draw top line
|
||||
term.moveAbs(pos)
|
||||
goterm.Print(chars[boxBorderTL])
|
||||
for i := 0; i < w; i++ {
|
||||
goterm.Print(chars[boxBorderHoriz])
|
||||
}
|
||||
goterm.Print(chars[boxBorderTR])
|
||||
|
||||
drawLine := func(line string) {
|
||||
pos[1]++
|
||||
term.moveAbs(pos)
|
||||
goterm.Print(chars[boxBorderVert])
|
||||
if len(line) > w {
|
||||
line = line[:w]
|
||||
}
|
||||
goterm.Print(line)
|
||||
if b.transparent {
|
||||
term.moveAbs(pos.add(xy{w + 1, 0}))
|
||||
} else {
|
||||
goterm.Print(strings.Repeat(" ", w-len(line)))
|
||||
}
|
||||
goterm.Print(chars[boxBorderVert])
|
||||
}
|
||||
|
||||
// truncate lines if necessary
|
||||
lines := b.lines()
|
||||
if len(lines) > h {
|
||||
lines = lines[:h]
|
||||
}
|
||||
|
||||
// draw body
|
||||
for _, line := range lines {
|
||||
drawLine(line)
|
||||
}
|
||||
|
||||
// draw empty lines
|
||||
for i := 0; i < h-len(lines); i++ {
|
||||
drawLine("")
|
||||
}
|
||||
|
||||
// draw bottom line
|
||||
pos[1]++
|
||||
term.moveAbs(pos)
|
||||
goterm.Print(chars[boxBorderBL])
|
||||
for i := 0; i < w; i++ {
|
||||
goterm.Print(chars[boxBorderHoriz])
|
||||
}
|
||||
goterm.Print(chars[boxBorderBR])
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const (
|
||||
framerate = 30
|
||||
framerate = 10
|
||||
frameperiod = time.Second / time.Duration(framerate)
|
||||
)
|
||||
|
||||
func debugf(str string, args ...interface{}) {
|
||||
if !strings.HasSuffix(str, "\n") {
|
||||
str += "\n"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, str, args...)
|
||||
}
|
||||
|
||||
// TODO
|
||||
// * Use actual gg graphs and not fake "boxes"
|
||||
// - This will involve wrapping the vertices in some way, to preserve position
|
||||
// * Once gg graphs are used we can use that birds-eye-view to make better
|
||||
// decisions about edge placement
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
{ // exit signal handling, cause ctrl-c doesn't work with goterm otherwise
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
<-c
|
||||
goterm.Clear()
|
||||
goterm.Flush()
|
||||
os.Stdout.Sync()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
term := new(terminal)
|
||||
term := terminal.New()
|
||||
|
||||
type movingBox struct {
|
||||
box
|
||||
@ -223,10 +40,11 @@ func main() {
|
||||
}
|
||||
|
||||
randBox := func() movingBox {
|
||||
tsize := term.size()
|
||||
tsize := term.WindowSize()
|
||||
return movingBox{
|
||||
box: box{
|
||||
pos: xy{rand.Intn(tsize[0]), rand.Intn(tsize[1])},
|
||||
pos: geo.XY{rand.Intn(tsize[0]), rand.Intn(tsize[1])},
|
||||
size: geo.XY{30, 2},
|
||||
},
|
||||
xRight: rand.Intn(1) == 0,
|
||||
yDown: rand.Intn(1) == 0,
|
||||
@ -242,23 +60,25 @@ func main() {
|
||||
}
|
||||
|
||||
for range time.Tick(frameperiod) {
|
||||
goterm.Clear()
|
||||
|
||||
termSize := term.size()
|
||||
now := time.Now()
|
||||
// update phase
|
||||
termSize := term.WindowSize()
|
||||
for i := range boxes {
|
||||
b := &boxes[i]
|
||||
b.body = fmt.Sprintf("%d\n%s", now.Unix(), now.String())
|
||||
b.body = fmt.Sprintf("%d) %v", i, b.rectCorner(geo.Left, geo.Up))
|
||||
b.body += fmt.Sprintf(" | %v\n", b.rectCorner(geo.Right, geo.Up))
|
||||
b.body += fmt.Sprintf(" %v", b.rectCorner(geo.Left, geo.Down))
|
||||
b.body += fmt.Sprintf(" | %v", b.rectCorner(geo.Right, geo.Down))
|
||||
|
||||
size := b.getSize()
|
||||
size := b.rectSize()
|
||||
if b.pos[0] <= 0 {
|
||||
b.xRight = true
|
||||
} else if b.pos[0]+size[0]+2 > termSize[0] {
|
||||
} else if b.pos[0]+size[0] >= termSize[0] {
|
||||
b.xRight = false
|
||||
}
|
||||
if b.pos[1] <= 0 {
|
||||
b.yDown = true
|
||||
} else if b.pos[1]+size[1]+2 > termSize[1] {
|
||||
} else if b.pos[1]+size[1] >= termSize[1] {
|
||||
b.yDown = false
|
||||
}
|
||||
|
||||
@ -272,9 +92,20 @@ func main() {
|
||||
} else {
|
||||
b.pos[1]--
|
||||
}
|
||||
|
||||
b.draw(term)
|
||||
}
|
||||
goterm.Flush()
|
||||
|
||||
// draw phase
|
||||
term.Reset()
|
||||
for i := range boxes {
|
||||
boxes[i].draw(term)
|
||||
}
|
||||
term.Flush()
|
||||
for i := range boxes {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
basicLine(term, boxes[i-1].box, boxes[i].box)
|
||||
}
|
||||
term.Flush()
|
||||
}
|
||||
}
|
||||
|
146
gim/terminal/terminal.go
Normal file
146
gim/terminal/terminal.go
Normal file
@ -0,0 +1,146 @@
|
||||
// Package terminal implements functionality related to interacting with a
|
||||
// terminal. Using this package takes the place of using stdout directly
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
|
||||
"github.com/mediocregopher/ginger/gim/geo"
|
||||
)
|
||||
|
||||
// Terminal provides an interface to a terminal which allows for "drawing"
|
||||
// rather than just writing. Note that all operations on a Terminal aren't
|
||||
// actually drawn to the screen until Flush is called.
|
||||
//
|
||||
// The coordinate system described by Terminal looks like this:
|
||||
//
|
||||
// 0,0 ------------------> x
|
||||
// |
|
||||
// |
|
||||
// |
|
||||
// |
|
||||
// |
|
||||
// |
|
||||
// |
|
||||
// |
|
||||
// v
|
||||
// y
|
||||
//
|
||||
type Terminal struct {
|
||||
buf *bytes.Buffer
|
||||
pos geo.XY
|
||||
|
||||
// When initialized this will be set to os.Stdout, but can be set to
|
||||
// anything
|
||||
Out io.Writer
|
||||
}
|
||||
|
||||
// New initializes and returns a usable Terminal
|
||||
func New() *Terminal {
|
||||
return &Terminal{
|
||||
buf: new(bytes.Buffer),
|
||||
Out: os.Stdout,
|
||||
}
|
||||
}
|
||||
|
||||
// WindowSize returns the size of the terminal window (width/height)
|
||||
// TODO this doesn't support winblows
|
||||
func (t *Terminal) WindowSize() geo.XY {
|
||||
var sz struct {
|
||||
rows uint16
|
||||
cols uint16
|
||||
xpixels uint16
|
||||
ypixels uint16
|
||||
}
|
||||
_, _, err := syscall.Syscall(
|
||||
syscall.SYS_IOCTL,
|
||||
uintptr(syscall.Stdin),
|
||||
uintptr(syscall.TIOCGWINSZ),
|
||||
uintptr(unsafe.Pointer(&sz)),
|
||||
)
|
||||
if err != 0 {
|
||||
panic(err.Error())
|
||||
}
|
||||
return geo.XY{int(sz.cols), int(sz.rows)}
|
||||
}
|
||||
|
||||
// MoveCursorTo moves the cursor to the given position
|
||||
func (t *Terminal) MoveCursorTo(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))
|
||||
}
|
||||
|
||||
// Reset completely clears all drawn characters on the screen and returns the
|
||||
// cursor to the origin
|
||||
func (t *Terminal) Reset() {
|
||||
fmt.Fprintf(t.buf, "\033[2J")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Flush writes all buffered changes to the screen
|
||||
func (t *Terminal) Flush() {
|
||||
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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user