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

Commit

Permalink
Add terminal.Cursor error handling on Windows (#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
mislav authored Mar 23, 2022
1 parent 6cbb195 commit 459523e
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 71 deletions.
7 changes: 4 additions & 3 deletions terminal/cursor.go
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
}

0 comments on commit 459523e

Please sign in to comment.