147 lines
3.0 KiB
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)
|
||
|
}
|