Skip to content
This repository has been archived by the owner on Apr 19, 2024. It is now read-only.

Add terminal.Cursor error handling on Windows #414

Merged
merged 1 commit into from Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions terminal/cursor.go
@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows

package terminal
Expand Down Expand Up @@ -78,8 +79,8 @@ func (c *Cursor) Hide() error {
return err
}

// Move moves the cursor to a specific x,y location.
func (c *Cursor) Move(x int, y int) error {
// move moves the cursor to a specific x,y location.
func (c *Cursor) move(x int, y int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
return err
}
Expand Down Expand Up @@ -194,7 +195,7 @@ func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
defer c.Restore()

// move the cursor to the very bottom of the terminal
c.Move(999, 999)
c.move(999, 999)

// ask for the current location
bottom, err := c.Location(buf)
Expand Down
98 changes: 62 additions & 36 deletions terminal/cursor_windows.go
Expand Up @@ -16,31 +16,37 @@ type Cursor struct {
Out FileWriter
}

func (c *Cursor) Up(n int) {
c.cursorMove(0, n)
func (c *Cursor) Up(n int) error {
return c.cursorMove(0, n)
}

func (c *Cursor) Down(n int) {
c.cursorMove(0, -1*n)
func (c *Cursor) Down(n int) error {
return c.cursorMove(0, -1*n)
}

func (c *Cursor) Forward(n int) {
c.cursorMove(n, 0)
func (c *Cursor) Forward(n int) error {
return c.cursorMove(n, 0)
}

func (c *Cursor) Back(n int) {
c.cursorMove(-1*n, 0)
func (c *Cursor) Back(n int) error {
return c.cursorMove(-1*n, 0)
}

// save the cursor location
func (c *Cursor) Save() {
cursorLoc, _ = c.Location(nil)
func (c *Cursor) Save() error {
loc, err := c.Location(nil)
if err != nil {
return err
}
cursorLoc = *loc
return nil
}

func (c *Cursor) Restore() {
func (c *Cursor) Restore() error {
handle := syscall.Handle(c.Out.Fd())
// restore it to the original position
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
return normalizeError(err)
}

func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
Expand All @@ -51,40 +57,49 @@ func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == 0
}

func (c *Cursor) cursorMove(x int, y int) {
func (c *Cursor) cursorMove(x int, y int) error {
handle := syscall.Handle(c.Out.Fd())

var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}

var cursor Coord
cursor.X = csbi.cursorPosition.X + Short(x)
cursor.Y = csbi.cursorPosition.Y + Short(y)

procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}

func (c *Cursor) NextLine(n int) {
c.Up(n)
c.HorizontalAbsolute(0)
func (c *Cursor) NextLine(n int) error {
if err := c.Up(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}

func (c *Cursor) PreviousLine(n int) {
c.Down(n)
c.HorizontalAbsolute(0)
func (c *Cursor) PreviousLine(n int) error {
if err := c.Down(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}

// for comparability purposes between windows
// in windows we don't have to print out a new line
func (c *Cursor) MoveNextLine(cur Coord, terminalSize *Coord) {
c.NextLine(1)
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
return c.NextLine(1)
}

func (c *Cursor) HorizontalAbsolute(x int) {
func (c *Cursor) HorizontalAbsolute(x int) error {
handle := syscall.Handle(c.Out.Fd())

var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}

var cursor Coord
cursor.X = Short(x)
Expand All @@ -94,43 +109,54 @@ func (c *Cursor) HorizontalAbsolute(x int) {
cursor.X = csbi.size.X
}

procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}

func (c *Cursor) Show() {
func (c *Cursor) Show() error {
handle := syscall.Handle(c.Out.Fd())

var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 1

procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}

func (c *Cursor) Hide() {
func (c *Cursor) Hide() error {
handle := syscall.Handle(c.Out.Fd())

var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 0

procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}

func (c *Cursor) Location(buf *bytes.Buffer) (Coord, error) {
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())

var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}

return csbi.cursorPosition, nil
return &csbi.cursorPosition, nil
}

func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())

var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}
// windows' coordinate system begins at (0, 0)
csbi.size.X--
csbi.size.Y--
Expand Down
10 changes: 7 additions & 3 deletions terminal/display_windows.go
Expand Up @@ -5,11 +5,13 @@ import (
"unsafe"
)

func EraseLine(out FileWriter, mode EraseLineMode) {
func EraseLine(out FileWriter, mode EraseLineMode) error {
handle := syscall.Handle(out.Fd())

var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}

var w uint32
var x Short
Expand All @@ -23,5 +25,7 @@ func EraseLine(out FileWriter, mode EraseLineMode) {
cursor.X = 0
x = csbi.size.X
}
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))

_, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
return normalizeError(err)
}
77 changes: 48 additions & 29 deletions terminal/output_windows.go
Expand Up @@ -12,18 +12,6 @@ import (
"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
Expand Down Expand Up @@ -98,9 +86,14 @@ func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
buf := make([]byte, 0, 10)
buf = append(buf, "\x1b"...)

var ch rune
var size int
// Check '[' continues after \x1b
ch, size, err := r.ReadRune()
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
Expand All @@ -116,6 +109,9 @@ func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
for {
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
Expand All @@ -127,47 +123,62 @@ func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
argBuf = append(argBuf, string(ch)...)
}

w.applyEscapeCode(buf, string(argBuf), code)
err = w.applyEscapeCode(buf, string(argBuf), code)
return
}

func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) error {
c := &Cursor{Out: w.out}

switch arg + string(code) {
case "?25h":
c.Show()
return
return c.Show()
case "?25l":
c.Hide()
return
return c.Hide()
}

if f, ok := cursorFunctions[code]; ok {
if code >= 'A' && code <= 'G' {
if n, err := strconv.Atoi(arg); err == nil {
f(c)(n)
return
switch code {
case 'A':
return c.Up(n)
case 'B':
return c.Down(n)
case 'C':
return c.Forward(n)
case 'D':
return c.Back(n)
case 'E':
return c.NextLine(n)
case 'F':
return c.PreviousLine(n)
case 'G':
return c.HorizontalAbsolute(n)
}
}
}

switch code {
case 'm':
w.applySelectGraphicRendition(arg)
return w.applySelectGraphicRendition(arg)
default:
buf = append(buf, string(code)...)
fmt.Fprint(w.out, string(buf))
_, err := fmt.Fprint(w.out, string(buf))
return err
}
}

// Original implementation: https://github.com/mattn/go-colorable
func (w *Writer) applySelectGraphicRendition(arg string) {
func (w *Writer) applySelectGraphicRendition(arg string) error {
if arg == "" {
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return normalizeError(err)
}

var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
attr := csbi.attributes

for _, param := range strings.Split(arg, ";") {
Expand Down Expand Up @@ -230,5 +241,13 @@ func (w *Writer) applySelectGraphicRendition(arg string) {
}
}

procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
return normalizeError(err)
}

func normalizeError(err error) error {
if syserr, ok := err.(syscall.Errno); ok && syserr == 0 {
return nil
}
return err
}