You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
4.8 KiB
227 lines
4.8 KiB
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)) |
|
}
|
|
|