ginger/gim/terminal/terminal.go

147 lines
3.0 KiB
Go

// 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)
}