228 lines
4.8 KiB
Go
228 lines
4.8 KiB
Go
|
package terminal
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"syscall"
|
||
|
"unsafe"
|
||
|
|
||
|
"github.com/mattn/go-isatty"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
cursorFunctions = map[rune]func(c *Cursor) func(int){
|
||
|
'A': func(c *Cursor) func(int) { return c.Up },
|
||
|
'B': func(c *Cursor) func(int) { return c.Down },
|
||
|
'C': func(c *Cursor) func(int) { return c.Forward },
|
||
|
'D': func(c *Cursor) func(int) { return c.Back },
|
||
|
'E': func(c *Cursor) func(int) { return c.NextLine },
|
||
|
'F': func(c *Cursor) func(int) { return c.PreviousLine },
|
||
|
'G': func(c *Cursor) func(int) { return c.HorizontalAbsolute },
|
||
|
}
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
foregroundBlue = 0x1
|
||
|
foregroundGreen = 0x2
|
||
|
foregroundRed = 0x4
|
||
|
foregroundIntensity = 0x8
|
||
|
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
|
||
|
backgroundBlue = 0x10
|
||
|
backgroundGreen = 0x20
|
||
|
backgroundRed = 0x40
|
||
|
backgroundIntensity = 0x80
|
||
|
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
|
||
|
)
|
||
|
|
||
|
type Writer struct {
|
||
|
out FileWriter
|
||
|
handle syscall.Handle
|
||
|
orgAttr word
|
||
|
}
|
||
|
|
||
|
func NewAnsiStdout(out FileWriter) io.Writer {
|
||
|
var csbi consoleScreenBufferInfo
|
||
|
if !isatty.IsTerminal(out.Fd()) {
|
||
|
return out
|
||
|
}
|
||
|
handle := syscall.Handle(out.Fd())
|
||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||
|
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
|
||
|
}
|
||
|
|
||
|
func NewAnsiStderr(out FileWriter) io.Writer {
|
||
|
var csbi consoleScreenBufferInfo
|
||
|
if !isatty.IsTerminal(out.Fd()) {
|
||
|
return out
|
||
|
}
|
||
|
handle := syscall.Handle(out.Fd())
|
||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||
|
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
|
||
|
}
|
||
|
|
||
|
func (w *Writer) Write(data []byte) (n int, err error) {
|
||
|
r := bytes.NewReader(data)
|
||
|
|
||
|
for {
|
||
|
ch, size, err := r.ReadRune()
|
||
|
if err != nil {
|
||
|
break
|
||
|
}
|
||
|
n += size
|
||
|
|
||
|
switch ch {
|
||
|
case '\x1b':
|
||
|
size, err = w.handleEscape(r)
|
||
|
n += size
|
||
|
if err != nil {
|
||
|
break
|
||
|
}
|
||
|
default:
|
||
|
fmt.Fprint(w.out, string(ch))
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
|
||
|
buf := make([]byte, 0, 10)
|
||
|
buf = append(buf, "\x1b"...)
|
||
|
|
||
|
// Check '[' continues after \x1b
|
||
|
ch, size, err := r.ReadRune()
|
||
|
if err != nil {
|
||
|
fmt.Fprint(w.out, string(buf))
|
||
|
return
|
||
|
}
|
||
|
n += size
|
||
|
if ch != '[' {
|
||
|
fmt.Fprint(w.out, string(buf))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Parse escape code
|
||
|
var code rune
|
||
|
argBuf := make([]byte, 0, 10)
|
||
|
for {
|
||
|
ch, size, err = r.ReadRune()
|
||
|
if err != nil {
|
||
|
fmt.Fprint(w.out, string(buf))
|
||
|
return
|
||
|
}
|
||
|
n += size
|
||
|
if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') {
|
||
|
code = ch
|
||
|
break
|
||
|
}
|
||
|
argBuf = append(argBuf, string(ch)...)
|
||
|
}
|
||
|
|
||
|
w.applyEscapeCode(buf, string(argBuf), code)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
|
||
|
c := &Cursor{Out: w.out}
|
||
|
|
||
|
switch arg + string(code) {
|
||
|
case "?25h":
|
||
|
c.Show()
|
||
|
return
|
||
|
case "?25l":
|
||
|
c.Hide()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if f, ok := cursorFunctions[code]; ok {
|
||
|
if n, err := strconv.Atoi(arg); err == nil {
|
||
|
f(c)(n)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch code {
|
||
|
case 'm':
|
||
|
w.applySelectGraphicRendition(arg)
|
||
|
default:
|
||
|
buf = append(buf, string(code)...)
|
||
|
fmt.Fprint(w.out, string(buf))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Original implementation: https://github.com/mattn/go-colorable
|
||
|
func (w *Writer) applySelectGraphicRendition(arg string) {
|
||
|
if arg == "" {
|
||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var csbi consoleScreenBufferInfo
|
||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||
|
attr := csbi.attributes
|
||
|
|
||
|
for _, param := range strings.Split(arg, ";") {
|
||
|
n, err := strconv.Atoi(param)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
switch {
|
||
|
case n == 0 || n == 100:
|
||
|
attr = w.orgAttr
|
||
|
case 1 <= n && n <= 5:
|
||
|
attr |= foregroundIntensity
|
||
|
case 30 <= n && n <= 37:
|
||
|
attr = (attr & backgroundMask)
|
||
|
if (n-30)&1 != 0 {
|
||
|
attr |= foregroundRed
|
||
|
}
|
||
|
if (n-30)&2 != 0 {
|
||
|
attr |= foregroundGreen
|
||
|
}
|
||
|
if (n-30)&4 != 0 {
|
||
|
attr |= foregroundBlue
|
||
|
}
|
||
|
case 40 <= n && n <= 47:
|
||
|
attr = (attr & foregroundMask)
|
||
|
if (n-40)&1 != 0 {
|
||
|
attr |= backgroundRed
|
||
|
}
|
||
|
if (n-40)&2 != 0 {
|
||
|
attr |= backgroundGreen
|
||
|
}
|
||
|
if (n-40)&4 != 0 {
|
||
|
attr |= backgroundBlue
|
||
|
}
|
||
|
case 90 <= n && n <= 97:
|
||
|
attr = (attr & backgroundMask)
|
||
|
attr |= foregroundIntensity
|
||
|
if (n-90)&1 != 0 {
|
||
|
attr |= foregroundRed
|
||
|
}
|
||
|
if (n-90)&2 != 0 {
|
||
|
attr |= foregroundGreen
|
||
|
}
|
||
|
if (n-90)&4 != 0 {
|
||
|
attr |= foregroundBlue
|
||
|
}
|
||
|
case 100 <= n && n <= 107:
|
||
|
attr = (attr & foregroundMask)
|
||
|
attr |= backgroundIntensity
|
||
|
if (n-100)&1 != 0 {
|
||
|
attr |= backgroundRed
|
||
|
}
|
||
|
if (n-100)&2 != 0 {
|
||
|
attr |= backgroundGreen
|
||
|
}
|
||
|
if (n-100)&4 != 0 {
|
||
|
attr |= backgroundBlue
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
|
||
|
}
|