Merge pull request #14838 from Microsoft/10662-ansirewrite
Windows: CLI Improvement (TP3)
This commit is contained in:
commit
809a231314
10 changed files with 499 additions and 1939 deletions
|
@ -3,11 +3,14 @@
|
||||||
package term
|
package term
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
|
||||||
|
"github.com/Azure/go-ansiterm/winterm"
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/docker/pkg/term/winconsole"
|
"github.com/docker/docker/pkg/term/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
// State holds the console mode for the terminal.
|
// State holds the console mode for the terminal.
|
||||||
|
@ -31,53 +34,97 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
|
||||||
return os.Stdin, os.Stdout, os.Stderr
|
return os.Stdin, os.Stdout, os.Stderr
|
||||||
case os.Getenv("MSYSTEM") != "":
|
case os.Getenv("MSYSTEM") != "":
|
||||||
// MSYS (mingw) does not emulate ANSI well.
|
// MSYS (mingw) does not emulate ANSI well.
|
||||||
return winconsole.WinConsoleStreams()
|
return windows.ConsoleStreams()
|
||||||
default:
|
default:
|
||||||
return winconsole.WinConsoleStreams()
|
return windows.ConsoleStreams()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal.
|
// GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal.
|
||||||
func GetFdInfo(in interface{}) (uintptr, bool) {
|
func GetFdInfo(in interface{}) (uintptr, bool) {
|
||||||
return winconsole.GetHandleInfo(in)
|
return windows.GetHandleInfo(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWinsize returns the window size based on the specified file descriptor.
|
// GetWinsize returns the window size based on the specified file descriptor.
|
||||||
func GetWinsize(fd uintptr) (*Winsize, error) {
|
func GetWinsize(fd uintptr) (*Winsize, error) {
|
||||||
info, err := winconsole.GetConsoleScreenBufferInfo(fd)
|
|
||||||
|
info, err := winterm.GetConsoleScreenBufferInfo(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(azlinux): Set the pixel width / height of the console (currently unused by any caller)
|
winsize := &Winsize{
|
||||||
return &Winsize{
|
|
||||||
Width: uint16(info.Window.Right - info.Window.Left + 1),
|
Width: uint16(info.Window.Right - info.Window.Left + 1),
|
||||||
Height: uint16(info.Window.Bottom - info.Window.Top + 1),
|
Height: uint16(info.Window.Bottom - info.Window.Top + 1),
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0}, nil
|
y: 0}
|
||||||
|
|
||||||
|
// Note: GetWinsize is called frequently -- uncomment only for excessive details
|
||||||
|
// logrus.Debugf("[windows] GetWinsize: Console(%v)", info.String())
|
||||||
|
// logrus.Debugf("[windows] GetWinsize: Width(%v), Height(%v), x(%v), y(%v)", winsize.Width, winsize.Height, winsize.x, winsize.y)
|
||||||
|
return winsize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWinsize tries to set the specified window size for the specified file descriptor.
|
// SetWinsize tries to set the specified window size for the specified file descriptor.
|
||||||
func SetWinsize(fd uintptr, ws *Winsize) error {
|
func SetWinsize(fd uintptr, ws *Winsize) error {
|
||||||
// TODO(azlinux): Implement SetWinsize
|
|
||||||
logrus.Debugf("[windows] SetWinsize: WARNING -- Unsupported method invoked")
|
// Ensure the requested dimensions are no larger than the maximum window size
|
||||||
return nil
|
info, err := winterm.GetConsoleScreenBufferInfo(fd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws.Width == 0 || ws.Height == 0 || ws.Width > uint16(info.MaximumWindowSize.X) || ws.Height > uint16(info.MaximumWindowSize.Y) {
|
||||||
|
return fmt.Errorf("Illegal window size: (%v,%v) -- Maximum allow: (%v,%v)",
|
||||||
|
ws.Width, ws.Height, info.MaximumWindowSize.X, info.MaximumWindowSize.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow the sizes to that used by Windows
|
||||||
|
var width winterm.SHORT = winterm.SHORT(ws.Width)
|
||||||
|
var height winterm.SHORT = winterm.SHORT(ws.Height)
|
||||||
|
|
||||||
|
// Set the dimensions while ensuring they remain within the bounds of the backing console buffer
|
||||||
|
// -- Shrinking will always succeed. Growing may push the edges past the buffer boundary. When that occurs,
|
||||||
|
// shift the upper left just enough to keep the new window within the buffer.
|
||||||
|
rect := info.Window
|
||||||
|
if width < rect.Right-rect.Left+1 {
|
||||||
|
rect.Right = rect.Left + width - 1
|
||||||
|
} else if width > rect.Right-rect.Left+1 {
|
||||||
|
rect.Right = rect.Left + width - 1
|
||||||
|
if rect.Right >= info.Size.X {
|
||||||
|
rect.Left = info.Size.X - width
|
||||||
|
rect.Right = info.Size.X - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if height < rect.Bottom-rect.Top+1 {
|
||||||
|
rect.Bottom = rect.Top + height - 1
|
||||||
|
} else if height > rect.Bottom-rect.Top+1 {
|
||||||
|
rect.Bottom = rect.Top + height - 1
|
||||||
|
if rect.Bottom >= info.Size.Y {
|
||||||
|
rect.Top = info.Size.Y - height
|
||||||
|
rect.Bottom = info.Size.Y - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logrus.Debugf("[windows] SetWinsize: Requested((%v,%v)) Actual(%v)", ws.Width, ws.Height, rect)
|
||||||
|
|
||||||
|
return winterm.SetConsoleWindowInfo(fd, true, rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
func IsTerminal(fd uintptr) bool {
|
func IsTerminal(fd uintptr) bool {
|
||||||
return winconsole.IsConsole(fd)
|
return windows.IsConsole(fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreTerminal restores the terminal connected to the given file descriptor
|
// RestoreTerminal restores the terminal connected to the given file descriptor
|
||||||
// to a previous state.
|
// to a previous state.
|
||||||
func RestoreTerminal(fd uintptr, state *State) error {
|
func RestoreTerminal(fd uintptr, state *State) error {
|
||||||
return winconsole.SetConsoleMode(fd, state.mode)
|
return winterm.SetConsoleMode(fd, state.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveState saves the state of the terminal connected to the given file descriptor.
|
// SaveState saves the state of the terminal connected to the given file descriptor.
|
||||||
func SaveState(fd uintptr) (*State, error) {
|
func SaveState(fd uintptr) (*State, error) {
|
||||||
mode, e := winconsole.GetConsoleMode(fd)
|
mode, e := winterm.GetConsoleMode(fd)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
|
@ -85,13 +132,20 @@ func SaveState(fd uintptr) (*State, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableEcho disables echo for the terminal connected to the given file descriptor.
|
// DisableEcho disables echo for the terminal connected to the given file descriptor.
|
||||||
// -- See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
|
// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
|
||||||
func DisableEcho(fd uintptr, state *State) error {
|
func DisableEcho(fd uintptr, state *State) error {
|
||||||
mode := state.mode
|
mode := state.mode
|
||||||
mode &^= winconsole.ENABLE_ECHO_INPUT
|
mode &^= winterm.ENABLE_ECHO_INPUT
|
||||||
mode |= winconsole.ENABLE_PROCESSED_INPUT | winconsole.ENABLE_LINE_INPUT
|
mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT
|
||||||
// TODO(azlinux): Core code registers a goroutine to catch os.Interrupt and reset the terminal state.
|
|
||||||
return winconsole.SetConsoleMode(fd, mode)
|
err := winterm.SetConsoleMode(fd, mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register an interrupt handler to catch and restore prior state
|
||||||
|
restoreAtInterrupt(fd, state)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRawTerminal puts the terminal connected to the given file descriptor into raw
|
// SetRawTerminal puts the terminal connected to the given file descriptor into raw
|
||||||
|
@ -101,13 +155,14 @@ func SetRawTerminal(fd uintptr) (*State, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// TODO(azlinux): Core code registers a goroutine to catch os.Interrupt and reset the terminal state.
|
|
||||||
|
// Register an interrupt handler to catch and restore prior state
|
||||||
|
restoreAtInterrupt(fd, state)
|
||||||
return state, err
|
return state, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeRaw puts the terminal connected to the given file descriptor into raw
|
// MakeRaw puts the terminal (Windows Console) connected to the given file descriptor into raw
|
||||||
// mode and returns the previous state of the terminal so that it can be
|
// mode and returns the previous state of the terminal so that it can be restored.
|
||||||
// restored.
|
|
||||||
func MakeRaw(fd uintptr) (*State, error) {
|
func MakeRaw(fd uintptr) (*State, error) {
|
||||||
state, err := SaveState(fd)
|
state, err := SaveState(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -120,20 +175,31 @@ func MakeRaw(fd uintptr) (*State, error) {
|
||||||
mode := state.mode
|
mode := state.mode
|
||||||
|
|
||||||
// Disable these modes
|
// Disable these modes
|
||||||
mode &^= winconsole.ENABLE_ECHO_INPUT
|
mode &^= winterm.ENABLE_ECHO_INPUT
|
||||||
mode &^= winconsole.ENABLE_LINE_INPUT
|
mode &^= winterm.ENABLE_LINE_INPUT
|
||||||
mode &^= winconsole.ENABLE_MOUSE_INPUT
|
mode &^= winterm.ENABLE_MOUSE_INPUT
|
||||||
mode &^= winconsole.ENABLE_WINDOW_INPUT
|
mode &^= winterm.ENABLE_WINDOW_INPUT
|
||||||
mode &^= winconsole.ENABLE_PROCESSED_INPUT
|
mode &^= winterm.ENABLE_PROCESSED_INPUT
|
||||||
|
|
||||||
// Enable these modes
|
// Enable these modes
|
||||||
mode |= winconsole.ENABLE_EXTENDED_FLAGS
|
mode |= winterm.ENABLE_EXTENDED_FLAGS
|
||||||
mode |= winconsole.ENABLE_INSERT_MODE
|
mode |= winterm.ENABLE_INSERT_MODE
|
||||||
mode |= winconsole.ENABLE_QUICK_EDIT_MODE
|
mode |= winterm.ENABLE_QUICK_EDIT_MODE
|
||||||
|
|
||||||
err = winconsole.SetConsoleMode(fd, mode)
|
err = winterm.SetConsoleMode(fd, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restoreAtInterrupt(fd uintptr, state *State) {
|
||||||
|
sigchan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigchan, os.Interrupt)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = <-sigchan
|
||||||
|
RestoreTerminal(fd, state)
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,232 +0,0 @@
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package winconsole
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func helpsTestParseInt16OrDefault(t *testing.T, expectedValue int16, shouldFail bool, input string, defaultValue int16, format string, args ...string) {
|
|
||||||
value, err := parseInt16OrDefault(input, defaultValue)
|
|
||||||
if nil != err && !shouldFail {
|
|
||||||
t.Errorf("Unexpected error returned %v", err)
|
|
||||||
t.Errorf(format, args)
|
|
||||||
}
|
|
||||||
if nil == err && shouldFail {
|
|
||||||
t.Errorf("Should have failed as expected\n\tReturned value = %d", value)
|
|
||||||
t.Errorf(format, args)
|
|
||||||
}
|
|
||||||
if expectedValue != value {
|
|
||||||
t.Errorf("The value returned does not match expected\n\tExpected:%v\n\t:Actual%v", expectedValue, value)
|
|
||||||
t.Errorf(format, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseInt16OrDefault(t *testing.T) {
|
|
||||||
// empty string
|
|
||||||
helpsTestParseInt16OrDefault(t, 0, false, "", 0, "Empty string returns default")
|
|
||||||
helpsTestParseInt16OrDefault(t, 2, false, "", 2, "Empty string returns default")
|
|
||||||
|
|
||||||
// normal case
|
|
||||||
helpsTestParseInt16OrDefault(t, 0, false, "0", 0, "0 handled correctly")
|
|
||||||
helpsTestParseInt16OrDefault(t, 111, false, "111", 2, "Normal")
|
|
||||||
helpsTestParseInt16OrDefault(t, 111, false, "+111", 2, "+N")
|
|
||||||
helpsTestParseInt16OrDefault(t, -111, false, "-111", 2, "-N")
|
|
||||||
helpsTestParseInt16OrDefault(t, 0, false, "+0", 11, "+0")
|
|
||||||
helpsTestParseInt16OrDefault(t, 0, false, "-0", 12, "-0")
|
|
||||||
|
|
||||||
// ill formed strings
|
|
||||||
helpsTestParseInt16OrDefault(t, 0, true, "abc", 0, "Invalid string")
|
|
||||||
helpsTestParseInt16OrDefault(t, 42, true, "+= 23", 42, "Invalid string")
|
|
||||||
helpsTestParseInt16OrDefault(t, 42, true, "123.45", 42, "float like")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func helpsTestGetNumberOfChars(t *testing.T, expected uint32, fromCoord COORD, toCoord COORD, screenSize COORD, format string, args ...interface{}) {
|
|
||||||
actual := getNumberOfChars(fromCoord, toCoord, screenSize)
|
|
||||||
mesg := fmt.Sprintf(format, args)
|
|
||||||
assertTrue(t, expected == actual, fmt.Sprintf("%s Expected=%d, Actual=%d, Parameters = { fromCoord=%+v, toCoord=%+v, screenSize=%+v", mesg, expected, actual, fromCoord, toCoord, screenSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetNumberOfChars(t *testing.T) {
|
|
||||||
// Note: The columns and lines are 0 based
|
|
||||||
// Also that interval is "inclusive" means will have both start and end chars
|
|
||||||
// This test only tests the number opf characters being written
|
|
||||||
|
|
||||||
// all four corners
|
|
||||||
maxWindow := COORD{X: 80, Y: 50}
|
|
||||||
leftTop := COORD{X: 0, Y: 0}
|
|
||||||
rightTop := COORD{X: 79, Y: 0}
|
|
||||||
leftBottom := COORD{X: 0, Y: 49}
|
|
||||||
rightBottom := COORD{X: 79, Y: 49}
|
|
||||||
|
|
||||||
// same position
|
|
||||||
helpsTestGetNumberOfChars(t, 1, COORD{X: 1, Y: 14}, COORD{X: 1, Y: 14}, COORD{X: 80, Y: 50}, "Same position random line")
|
|
||||||
|
|
||||||
// four corners
|
|
||||||
helpsTestGetNumberOfChars(t, 1, leftTop, leftTop, maxWindow, "Same position- leftTop")
|
|
||||||
helpsTestGetNumberOfChars(t, 1, rightTop, rightTop, maxWindow, "Same position- rightTop")
|
|
||||||
helpsTestGetNumberOfChars(t, 1, leftBottom, leftBottom, maxWindow, "Same position- leftBottom")
|
|
||||||
helpsTestGetNumberOfChars(t, 1, rightBottom, rightBottom, maxWindow, "Same position- rightBottom")
|
|
||||||
|
|
||||||
// from this char to next char on same line
|
|
||||||
helpsTestGetNumberOfChars(t, 2, COORD{X: 0, Y: 0}, COORD{X: 1, Y: 0}, maxWindow, "Next position on same line")
|
|
||||||
helpsTestGetNumberOfChars(t, 2, COORD{X: 1, Y: 14}, COORD{X: 2, Y: 14}, maxWindow, "Next position on same line")
|
|
||||||
|
|
||||||
// from this char to next 10 chars on same line
|
|
||||||
helpsTestGetNumberOfChars(t, 11, COORD{X: 0, Y: 0}, COORD{X: 10, Y: 0}, maxWindow, "Next position on same line")
|
|
||||||
helpsTestGetNumberOfChars(t, 11, COORD{X: 1, Y: 14}, COORD{X: 11, Y: 14}, maxWindow, "Next position on same line")
|
|
||||||
|
|
||||||
helpsTestGetNumberOfChars(t, 5, COORD{X: 3, Y: 11}, COORD{X: 7, Y: 11}, maxWindow, "To and from on same line")
|
|
||||||
|
|
||||||
helpsTestGetNumberOfChars(t, 8, COORD{X: 0, Y: 34}, COORD{X: 7, Y: 34}, maxWindow, "Start of line to middle")
|
|
||||||
helpsTestGetNumberOfChars(t, 4, COORD{X: 76, Y: 34}, COORD{X: 79, Y: 34}, maxWindow, "Middle to end of line")
|
|
||||||
|
|
||||||
// multiple lines - 1
|
|
||||||
helpsTestGetNumberOfChars(t, 81, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 1}, maxWindow, "one line below same X")
|
|
||||||
helpsTestGetNumberOfChars(t, 81, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 11}, maxWindow, "one line below same X")
|
|
||||||
|
|
||||||
// multiple lines - 2
|
|
||||||
helpsTestGetNumberOfChars(t, 161, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 2}, maxWindow, "one line below same X")
|
|
||||||
helpsTestGetNumberOfChars(t, 161, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 12}, maxWindow, "one line below same X")
|
|
||||||
|
|
||||||
// multiple lines - 3
|
|
||||||
helpsTestGetNumberOfChars(t, 241, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 3}, maxWindow, "one line below same X")
|
|
||||||
helpsTestGetNumberOfChars(t, 241, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 13}, maxWindow, "one line below same X")
|
|
||||||
|
|
||||||
// full line
|
|
||||||
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 0}, COORD{X: 79, Y: 0}, maxWindow, "Full line - first")
|
|
||||||
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 23}, COORD{X: 79, Y: 23}, maxWindow, "Full line - random")
|
|
||||||
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 49}, COORD{X: 79, Y: 49}, maxWindow, "Full line - last")
|
|
||||||
|
|
||||||
// full screen
|
|
||||||
helpsTestGetNumberOfChars(t, 80*50, leftTop, rightBottom, maxWindow, "full screen")
|
|
||||||
|
|
||||||
helpsTestGetNumberOfChars(t, 80*50-1, COORD{X: 1, Y: 0}, rightBottom, maxWindow, "dropping first char to, end of screen")
|
|
||||||
helpsTestGetNumberOfChars(t, 80*50-2, COORD{X: 2, Y: 0}, rightBottom, maxWindow, "dropping first two char to, end of screen")
|
|
||||||
|
|
||||||
helpsTestGetNumberOfChars(t, 80*50-1, leftTop, COORD{X: 78, Y: 49}, maxWindow, "from start of screen, till last char-1")
|
|
||||||
helpsTestGetNumberOfChars(t, 80*50-2, leftTop, COORD{X: 77, Y: 49}, maxWindow, "from start of screen, till last char-2")
|
|
||||||
|
|
||||||
helpsTestGetNumberOfChars(t, 80*50-5, COORD{X: 4, Y: 0}, COORD{X: 78, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-1")
|
|
||||||
helpsTestGetNumberOfChars(t, 80*50-6, COORD{X: 4, Y: 0}, COORD{X: 77, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-2")
|
|
||||||
}
|
|
||||||
|
|
||||||
var allForeground = []int16{
|
|
||||||
ANSI_FOREGROUND_BLACK,
|
|
||||||
ANSI_FOREGROUND_RED,
|
|
||||||
ANSI_FOREGROUND_GREEN,
|
|
||||||
ANSI_FOREGROUND_YELLOW,
|
|
||||||
ANSI_FOREGROUND_BLUE,
|
|
||||||
ANSI_FOREGROUND_MAGENTA,
|
|
||||||
ANSI_FOREGROUND_CYAN,
|
|
||||||
ANSI_FOREGROUND_WHITE,
|
|
||||||
ANSI_FOREGROUND_DEFAULT,
|
|
||||||
}
|
|
||||||
var allBackground = []int16{
|
|
||||||
ANSI_BACKGROUND_BLACK,
|
|
||||||
ANSI_BACKGROUND_RED,
|
|
||||||
ANSI_BACKGROUND_GREEN,
|
|
||||||
ANSI_BACKGROUND_YELLOW,
|
|
||||||
ANSI_BACKGROUND_BLUE,
|
|
||||||
ANSI_BACKGROUND_MAGENTA,
|
|
||||||
ANSI_BACKGROUND_CYAN,
|
|
||||||
ANSI_BACKGROUND_WHITE,
|
|
||||||
ANSI_BACKGROUND_DEFAULT,
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskForeground(flag WORD) WORD {
|
|
||||||
return flag & FOREGROUND_MASK_UNSET
|
|
||||||
}
|
|
||||||
|
|
||||||
func onlyForeground(flag WORD) WORD {
|
|
||||||
return flag & FOREGROUND_MASK_SET
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskBackground(flag WORD) WORD {
|
|
||||||
return flag & BACKGROUND_MASK_UNSET
|
|
||||||
}
|
|
||||||
|
|
||||||
func onlyBackground(flag WORD) WORD {
|
|
||||||
return flag & BACKGROUND_MASK_SET
|
|
||||||
}
|
|
||||||
|
|
||||||
func helpsTestGetWindowsTextAttributeForAnsiValue(t *testing.T, oldValue WORD /*, expected WORD*/, ansi int16, onlyMask WORD, restMask WORD) WORD {
|
|
||||||
actual, err := getWindowsTextAttributeForAnsiValue(oldValue, FOREGROUND_MASK_SET, ansi)
|
|
||||||
assertTrue(t, nil == err, "Should be no error")
|
|
||||||
// assert that other bits are not affected
|
|
||||||
if 0 != oldValue {
|
|
||||||
assertTrue(t, (actual&restMask) == (oldValue&restMask), "The operation should not have affected other bits actual=%X oldValue=%X ansi=%d", actual, oldValue, ansi)
|
|
||||||
}
|
|
||||||
return actual
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBackgroundForAnsiValue(t *testing.T) {
|
|
||||||
// Check that nothing else changes
|
|
||||||
// background changes
|
|
||||||
for _, state1 := range allBackground {
|
|
||||||
for _, state2 := range allBackground {
|
|
||||||
flag := WORD(0)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// cummulative bcakground changes
|
|
||||||
for _, state1 := range allBackground {
|
|
||||||
flag := WORD(0)
|
|
||||||
for _, state2 := range allBackground {
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// change background after foreground
|
|
||||||
for _, state1 := range allForeground {
|
|
||||||
for _, state2 := range allBackground {
|
|
||||||
flag := WORD(0)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// change background after change cumulative
|
|
||||||
for _, state1 := range allForeground {
|
|
||||||
flag := WORD(0)
|
|
||||||
for _, state2 := range allBackground {
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForegroundForAnsiValue(t *testing.T) {
|
|
||||||
// Check that nothing else changes
|
|
||||||
for _, state1 := range allForeground {
|
|
||||||
for _, state2 := range allForeground {
|
|
||||||
flag := WORD(0)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, state1 := range allForeground {
|
|
||||||
flag := WORD(0)
|
|
||||||
for _, state2 := range allForeground {
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, state1 := range allBackground {
|
|
||||||
for _, state2 := range allForeground {
|
|
||||||
flag := WORD(0)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, state1 := range allBackground {
|
|
||||||
flag := WORD(0)
|
|
||||||
for _, state2 := range allForeground {
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
|
||||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,234 +0,0 @@
|
||||||
package winconsole
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
|
||||||
const (
|
|
||||||
ANSI_ESCAPE_PRIMARY = 0x1B
|
|
||||||
ANSI_ESCAPE_SECONDARY = 0x5B
|
|
||||||
ANSI_COMMAND_FIRST = 0x40
|
|
||||||
ANSI_COMMAND_LAST = 0x7E
|
|
||||||
ANSI_PARAMETER_SEP = ";"
|
|
||||||
ANSI_CMD_G0 = '('
|
|
||||||
ANSI_CMD_G1 = ')'
|
|
||||||
ANSI_CMD_G2 = '*'
|
|
||||||
ANSI_CMD_G3 = '+'
|
|
||||||
ANSI_CMD_DECPNM = '>'
|
|
||||||
ANSI_CMD_DECPAM = '='
|
|
||||||
ANSI_CMD_OSC = ']'
|
|
||||||
ANSI_CMD_STR_TERM = '\\'
|
|
||||||
ANSI_BEL = 0x07
|
|
||||||
KEY_EVENT = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interface that implements terminal handling
|
|
||||||
type terminalEmulator interface {
|
|
||||||
HandleOutputCommand(fd uintptr, command []byte) (n int, err error)
|
|
||||||
HandleInputSequence(fd uintptr, command []byte) (n int, err error)
|
|
||||||
WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error)
|
|
||||||
ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type terminalWriter struct {
|
|
||||||
wrappedWriter io.Writer
|
|
||||||
emulator terminalEmulator
|
|
||||||
command []byte
|
|
||||||
inSequence bool
|
|
||||||
fd uintptr
|
|
||||||
}
|
|
||||||
|
|
||||||
type terminalReader struct {
|
|
||||||
wrappedReader io.ReadCloser
|
|
||||||
emulator terminalEmulator
|
|
||||||
command []byte
|
|
||||||
inSequence bool
|
|
||||||
fd uintptr
|
|
||||||
}
|
|
||||||
|
|
||||||
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
|
||||||
func isAnsiCommandChar(b byte) bool {
|
|
||||||
switch {
|
|
||||||
case ANSI_COMMAND_FIRST <= b && b <= ANSI_COMMAND_LAST && b != ANSI_ESCAPE_SECONDARY:
|
|
||||||
return true
|
|
||||||
case b == ANSI_CMD_G1 || b == ANSI_CMD_OSC || b == ANSI_CMD_DECPAM || b == ANSI_CMD_DECPNM:
|
|
||||||
// non-CSI escape sequence terminator
|
|
||||||
return true
|
|
||||||
case b == ANSI_CMD_STR_TERM || b == ANSI_BEL:
|
|
||||||
// String escape sequence terminator
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCharacterSelectionCmdChar(b byte) bool {
|
|
||||||
return (b == ANSI_CMD_G0 || b == ANSI_CMD_G1 || b == ANSI_CMD_G2 || b == ANSI_CMD_G3)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isXtermOscSequence(command []byte, current byte) bool {
|
|
||||||
return (len(command) >= 2 && command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_CMD_OSC && current != ANSI_BEL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes len(p) bytes from p to the underlying data stream.
|
|
||||||
// http://golang.org/pkg/io/#Writer
|
|
||||||
func (tw *terminalWriter) Write(p []byte) (n int, err error) {
|
|
||||||
if len(p) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
if tw.emulator == nil {
|
|
||||||
return tw.wrappedWriter.Write(p)
|
|
||||||
}
|
|
||||||
// Emulate terminal by extracting commands and executing them
|
|
||||||
totalWritten := 0
|
|
||||||
start := 0 // indicates start of the next chunk
|
|
||||||
end := len(p)
|
|
||||||
for current := 0; current < end; current++ {
|
|
||||||
if tw.inSequence {
|
|
||||||
// inside escape sequence
|
|
||||||
tw.command = append(tw.command, p[current])
|
|
||||||
if isAnsiCommandChar(p[current]) {
|
|
||||||
if !isXtermOscSequence(tw.command, p[current]) {
|
|
||||||
// found the last command character.
|
|
||||||
// Now we have a complete command.
|
|
||||||
nchar, err := tw.emulator.HandleOutputCommand(tw.fd, tw.command)
|
|
||||||
totalWritten += nchar
|
|
||||||
if err != nil {
|
|
||||||
return totalWritten, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear the command
|
|
||||||
// don't include current character again
|
|
||||||
tw.command = tw.command[:0]
|
|
||||||
start = current + 1
|
|
||||||
tw.inSequence = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if p[current] == ANSI_ESCAPE_PRIMARY {
|
|
||||||
// entering escape sequnce
|
|
||||||
tw.inSequence = true
|
|
||||||
// indicates end of "normal sequence", write whatever you have so far
|
|
||||||
if len(p[start:current]) > 0 {
|
|
||||||
nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:current])
|
|
||||||
totalWritten += nw
|
|
||||||
if err != nil {
|
|
||||||
return totalWritten, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// include the current character as part of the next sequence
|
|
||||||
tw.command = append(tw.command, p[current])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// note that so far, start of the escape sequence triggers writing out of bytes to console.
|
|
||||||
// For the part _after_ the end of last escape sequence, it is not written out yet. So write it out
|
|
||||||
if !tw.inSequence {
|
|
||||||
// assumption is that we can't be inside sequence and therefore command should be empty
|
|
||||||
if len(p[start:]) > 0 {
|
|
||||||
nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:])
|
|
||||||
totalWritten += nw
|
|
||||||
if err != nil {
|
|
||||||
return totalWritten, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return totalWritten, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads up to len(p) bytes into p.
|
|
||||||
// http://golang.org/pkg/io/#Reader
|
|
||||||
func (tr *terminalReader) Read(p []byte) (n int, err error) {
|
|
||||||
//Implementations of Read are discouraged from returning a zero byte count
|
|
||||||
// with a nil error, except when len(p) == 0.
|
|
||||||
if len(p) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
if nil == tr.emulator {
|
|
||||||
return tr.readFromWrappedReader(p)
|
|
||||||
}
|
|
||||||
return tr.emulator.ReadChars(tr.fd, tr.wrappedReader, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the underlying stream
|
|
||||||
func (tr *terminalReader) Close() (err error) {
|
|
||||||
return tr.wrappedReader.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr *terminalReader) readFromWrappedReader(p []byte) (n int, err error) {
|
|
||||||
return tr.wrappedReader.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ansiCommand struct {
|
|
||||||
CommandBytes []byte
|
|
||||||
Command string
|
|
||||||
Parameters []string
|
|
||||||
IsSpecial bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAnsiCommand(command []byte) *ansiCommand {
|
|
||||||
if isCharacterSelectionCmdChar(command[1]) {
|
|
||||||
// Is Character Set Selection commands
|
|
||||||
return &ansiCommand{
|
|
||||||
CommandBytes: command,
|
|
||||||
Command: string(command),
|
|
||||||
IsSpecial: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// last char is command character
|
|
||||||
lastCharIndex := len(command) - 1
|
|
||||||
|
|
||||||
retValue := &ansiCommand{
|
|
||||||
CommandBytes: command,
|
|
||||||
Command: string(command[lastCharIndex]),
|
|
||||||
IsSpecial: false,
|
|
||||||
}
|
|
||||||
// more than a single escape
|
|
||||||
if lastCharIndex != 0 {
|
|
||||||
start := 1
|
|
||||||
// skip if double char escape sequence
|
|
||||||
if command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_ESCAPE_SECONDARY {
|
|
||||||
start++
|
|
||||||
}
|
|
||||||
// convert this to GetNextParam method
|
|
||||||
retValue.Parameters = strings.Split(string(command[start:lastCharIndex]), ANSI_PARAMETER_SEP)
|
|
||||||
}
|
|
||||||
return retValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ansiCommand) getParam(index int) string {
|
|
||||||
if len(c.Parameters) > index {
|
|
||||||
return c.Parameters[index]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *ansiCommand) String() string {
|
|
||||||
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
|
|
||||||
bytesToHex(ac.CommandBytes),
|
|
||||||
ac.Command,
|
|
||||||
strings.Join(ac.Parameters, "\",\""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesToHex(b []byte) string {
|
|
||||||
hex := make([]string, len(b))
|
|
||||||
for i, ch := range b {
|
|
||||||
hex[i] = fmt.Sprintf("%X", ch)
|
|
||||||
}
|
|
||||||
return strings.Join(hex, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInt16OrDefault(s string, defaultValue int16) (n int16, err error) {
|
|
||||||
if s == "" {
|
|
||||||
return defaultValue, nil
|
|
||||||
}
|
|
||||||
parsedValue, err := strconv.ParseInt(s, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue, err
|
|
||||||
}
|
|
||||||
return int16(parsedValue), nil
|
|
||||||
}
|
|
|
@ -1,388 +0,0 @@
|
||||||
package winconsole
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
WRITE_OPERATION = iota
|
|
||||||
COMMAND_OPERATION = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
var languages = []string{
|
|
||||||
"Български",
|
|
||||||
"Català",
|
|
||||||
"Čeština",
|
|
||||||
"Ελληνικά",
|
|
||||||
"Español",
|
|
||||||
"Esperanto",
|
|
||||||
"Euskara",
|
|
||||||
"Français",
|
|
||||||
"Galego",
|
|
||||||
"한국어",
|
|
||||||
"ქართული",
|
|
||||||
"Latviešu",
|
|
||||||
"Lietuvių",
|
|
||||||
"Magyar",
|
|
||||||
"Nederlands",
|
|
||||||
"日本語",
|
|
||||||
"Norsk bokmål",
|
|
||||||
"Norsk nynorsk",
|
|
||||||
"Polski",
|
|
||||||
"Português",
|
|
||||||
"Română",
|
|
||||||
"Русский",
|
|
||||||
"Slovenčina",
|
|
||||||
"Slovenščina",
|
|
||||||
"Српски",
|
|
||||||
"српскохрватски",
|
|
||||||
"Suomi",
|
|
||||||
"Svenska",
|
|
||||||
"ไทย",
|
|
||||||
"Tiếng Việt",
|
|
||||||
"Türkçe",
|
|
||||||
"Українська",
|
|
||||||
"中文",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock terminal handler object
|
|
||||||
type mockTerminal struct {
|
|
||||||
OutputCommandSequence []terminalOperation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for recording the callback data
|
|
||||||
type terminalOperation struct {
|
|
||||||
Operation int
|
|
||||||
Data []byte
|
|
||||||
Str string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mt *mockTerminal) record(operation int, data []byte) {
|
|
||||||
op := terminalOperation{
|
|
||||||
Operation: operation,
|
|
||||||
Data: make([]byte, len(data)),
|
|
||||||
}
|
|
||||||
copy(op.Data, data)
|
|
||||||
op.Str = string(op.Data)
|
|
||||||
mt.OutputCommandSequence = append(mt.OutputCommandSequence, op)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mt *mockTerminal) HandleOutputCommand(fd uintptr, command []byte) (n int, err error) {
|
|
||||||
mt.record(COMMAND_OPERATION, command)
|
|
||||||
return len(command), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mt *mockTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mt *mockTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) {
|
|
||||||
mt.record(WRITE_OPERATION, p)
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mt *mockTerminal) ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) {
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertTrue(t *testing.T, cond bool, format string, args ...interface{}) {
|
|
||||||
if !cond {
|
|
||||||
t.Errorf(format, args...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reflect.DeepEqual does not provide detailed information as to what excatly failed.
|
|
||||||
func assertBytesEqual(t *testing.T, expected, actual []byte, format string, args ...interface{}) {
|
|
||||||
match := true
|
|
||||||
mismatchIndex := 0
|
|
||||||
if len(expected) == len(actual) {
|
|
||||||
for i := 0; i < len(expected); i++ {
|
|
||||||
if expected[i] != actual[i] {
|
|
||||||
match = false
|
|
||||||
mismatchIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match = false
|
|
||||||
t.Errorf("Lengths don't match Expected=%d Actual=%d", len(expected), len(actual))
|
|
||||||
}
|
|
||||||
if !match {
|
|
||||||
t.Errorf("Mismatch at index %d ", mismatchIndex)
|
|
||||||
t.Errorf("\tActual String = %s", string(actual))
|
|
||||||
t.Errorf("\tExpected String = %s", string(expected))
|
|
||||||
t.Errorf("\tActual = %v", actual)
|
|
||||||
t.Errorf("\tExpected = %v", expected)
|
|
||||||
t.Errorf(format, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just to make sure :)
|
|
||||||
func TestAssertEqualBytes(t *testing.T) {
|
|
||||||
data := []byte{9, 9, 1, 1, 1, 9, 9}
|
|
||||||
assertBytesEqual(t, data, data, "Self")
|
|
||||||
assertBytesEqual(t, data[1:4], data[1:4], "Self")
|
|
||||||
assertBytesEqual(t, []byte{1, 1}, []byte{1, 1}, "Simple match")
|
|
||||||
assertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 2, 3}, "content mismatch")
|
|
||||||
assertBytesEqual(t, []byte{1, 1, 1}, data[2:5], "slice match")
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
func TestAssertEqualBytesNegative(t *testing.T) {
|
|
||||||
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
|
|
||||||
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
|
|
||||||
AssertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 1, 1}, "content mismatch")
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Checks that the calls received
|
|
||||||
func assertHandlerOutput(t *testing.T, mock *mockTerminal, plainText string, commands ...string) {
|
|
||||||
text := make([]byte, 0, 3*len(plainText))
|
|
||||||
cmdIndex := 0
|
|
||||||
for opIndex := 0; opIndex < len(mock.OutputCommandSequence); opIndex++ {
|
|
||||||
op := mock.OutputCommandSequence[opIndex]
|
|
||||||
if op.Operation == WRITE_OPERATION {
|
|
||||||
t.Logf("\nThe data is[%d] == %s", opIndex, string(op.Data))
|
|
||||||
text = append(text[:], op.Data...)
|
|
||||||
} else {
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[opIndex].Operation == COMMAND_OPERATION, "Operation should be command : %s", fmt.Sprintf("%+v", mock))
|
|
||||||
assertBytesEqual(t, StringToBytes(commands[cmdIndex]), mock.OutputCommandSequence[opIndex].Data, "Command data should match")
|
|
||||||
cmdIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertBytesEqual(t, StringToBytes(plainText), text, "Command data should match %#v", mock)
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringToBytes(str string) []byte {
|
|
||||||
bytes := make([]byte, len(str))
|
|
||||||
copy(bytes[:], str)
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseAnsiCommand(t *testing.T) {
|
|
||||||
// Note: if the parameter does not exist then the empty value is returned
|
|
||||||
|
|
||||||
c := parseAnsiCommand(StringToBytes("\x1Bm"))
|
|
||||||
assertTrue(t, c.Command == "m", "Command should be m")
|
|
||||||
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
|
||||||
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
|
||||||
|
|
||||||
// Escape sequence - ESC[
|
|
||||||
c = parseAnsiCommand(StringToBytes("\x1B[m"))
|
|
||||||
assertTrue(t, c.Command == "m", "Command should be m")
|
|
||||||
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
|
||||||
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
|
||||||
|
|
||||||
// Escape sequence With empty parameters- ESC[
|
|
||||||
c = parseAnsiCommand(StringToBytes("\x1B[;m"))
|
|
||||||
assertTrue(t, c.Command == "m", "Command should be m")
|
|
||||||
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
|
||||||
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
|
||||||
assertTrue(t, "" == c.getParam(2), "should return empty string")
|
|
||||||
|
|
||||||
// Escape sequence With empty muliple parameters- ESC[
|
|
||||||
c = parseAnsiCommand(StringToBytes("\x1B[;;m"))
|
|
||||||
assertTrue(t, c.Command == "m", "Command should be m")
|
|
||||||
assertTrue(t, "" == c.getParam(0), "")
|
|
||||||
assertTrue(t, "" == c.getParam(1), "")
|
|
||||||
assertTrue(t, "" == c.getParam(2), "")
|
|
||||||
|
|
||||||
// Escape sequence With muliple parameters- ESC[
|
|
||||||
c = parseAnsiCommand(StringToBytes("\x1B[1;2;3m"))
|
|
||||||
assertTrue(t, c.Command == "m", "Command should be m")
|
|
||||||
assertTrue(t, "1" == c.getParam(0), "")
|
|
||||||
assertTrue(t, "2" == c.getParam(1), "")
|
|
||||||
assertTrue(t, "3" == c.getParam(2), "")
|
|
||||||
|
|
||||||
// Escape sequence With muliple parameters- some missing
|
|
||||||
c = parseAnsiCommand(StringToBytes("\x1B[1;;3;;;6m"))
|
|
||||||
assertTrue(t, c.Command == "m", "Command should be m")
|
|
||||||
assertTrue(t, "1" == c.getParam(0), "")
|
|
||||||
assertTrue(t, "" == c.getParam(1), "")
|
|
||||||
assertTrue(t, "3" == c.getParam(2), "")
|
|
||||||
assertTrue(t, "" == c.getParam(3), "")
|
|
||||||
assertTrue(t, "" == c.getParam(4), "")
|
|
||||||
assertTrue(t, "6" == c.getParam(5), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBufferedMockTerm() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser, mock *mockTerminal) {
|
|
||||||
var input bytes.Buffer
|
|
||||||
var output bytes.Buffer
|
|
||||||
var err bytes.Buffer
|
|
||||||
|
|
||||||
mock = &mockTerminal{
|
|
||||||
OutputCommandSequence: make([]terminalOperation, 0, 256),
|
|
||||||
}
|
|
||||||
|
|
||||||
stdOut = &terminalWriter{
|
|
||||||
wrappedWriter: &output,
|
|
||||||
emulator: mock,
|
|
||||||
command: make([]byte, 0, 256),
|
|
||||||
}
|
|
||||||
stdErr = &terminalWriter{
|
|
||||||
wrappedWriter: &err,
|
|
||||||
emulator: mock,
|
|
||||||
command: make([]byte, 0, 256),
|
|
||||||
}
|
|
||||||
stdIn = &terminalReader{
|
|
||||||
wrappedReader: ioutil.NopCloser(&input),
|
|
||||||
emulator: mock,
|
|
||||||
command: make([]byte, 0, 256),
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputSimple(t *testing.T) {
|
|
||||||
stdOut, _, _, mock := newBufferedMockTerm()
|
|
||||||
|
|
||||||
stdOut.Write(StringToBytes("Hello world"))
|
|
||||||
stdOut.Write(StringToBytes("\x1BmHello again"))
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("\x1Bm"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputSplitCommand(t *testing.T) {
|
|
||||||
stdOut, _, _, mock := newBufferedMockTerm()
|
|
||||||
|
|
||||||
stdOut.Write(StringToBytes("Hello world\x1B[1;2;3"))
|
|
||||||
stdOut.Write(StringToBytes("mHello again"))
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputMultipleCommands(t *testing.T) {
|
|
||||||
stdOut, _, _, mock := newBufferedMockTerm()
|
|
||||||
|
|
||||||
stdOut.Write(StringToBytes("Hello world"))
|
|
||||||
stdOut.Write(StringToBytes("\x1B[1;2;3m"))
|
|
||||||
stdOut.Write(StringToBytes("\x1B[J"))
|
|
||||||
stdOut.Write(StringToBytes("Hello again"))
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("\x1B[J"), mock.OutputCommandSequence[2].Data, "Command data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[3].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[3].Data, "Write data should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Splits the given data in two chunks , makes two writes and checks the split data is parsed correctly
|
|
||||||
// checks output write/command is passed to handler correctly
|
|
||||||
func helpsTestOutputSplitChunksAtIndex(t *testing.T, i int, data []byte) {
|
|
||||||
t.Logf("\ni=%d", i)
|
|
||||||
stdOut, _, _, mock := newBufferedMockTerm()
|
|
||||||
|
|
||||||
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
|
|
||||||
t.Logf("\nWriting chunk[1] == %s", string(data[i:]))
|
|
||||||
stdOut.Write(data[:i])
|
|
||||||
stdOut.Write(data[i:])
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, data[i:], mock.OutputCommandSequence[1].Data, "Write data should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Splits the given data in three chunks , makes three writes and checks the split data is parsed correctly
|
|
||||||
// checks output write/command is passed to handler correctly
|
|
||||||
func helpsTestOutputSplitThreeChunksAtIndex(t *testing.T, data []byte, i int, j int) {
|
|
||||||
stdOut, _, _, mock := newBufferedMockTerm()
|
|
||||||
|
|
||||||
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
|
|
||||||
t.Logf("\nWriting chunk[1] == %s", string(data[i:j]))
|
|
||||||
t.Logf("\nWriting chunk[2] == %s", string(data[j:]))
|
|
||||||
stdOut.Write(data[:i])
|
|
||||||
stdOut.Write(data[i:j])
|
|
||||||
stdOut.Write(data[j:])
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, data[i:j], mock.OutputCommandSequence[1].Data, "Write data should match")
|
|
||||||
|
|
||||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
|
||||||
assertBytesEqual(t, data[j:], mock.OutputCommandSequence[2].Data, "Write data should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Splits the output into two parts and tests all such possible pairs
|
|
||||||
func helpsTestOutputSplitChunks(t *testing.T, data []byte) {
|
|
||||||
for i := 1; i < len(data)-1; i++ {
|
|
||||||
helpsTestOutputSplitChunksAtIndex(t, i, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Splits the output in three parts and tests all such possible triples
|
|
||||||
func helpsTestOutputSplitThreeChunks(t *testing.T, data []byte) {
|
|
||||||
for i := 1; i < len(data)-2; i++ {
|
|
||||||
for j := i + 1; j < len(data)-1; j++ {
|
|
||||||
helpsTestOutputSplitThreeChunksAtIndex(t, data, i, j)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func helpsTestOutputSplitCommandsAtIndex(t *testing.T, data []byte, i int, plainText string, commands ...string) {
|
|
||||||
t.Logf("\ni=%d", i)
|
|
||||||
stdOut, _, _, mock := newBufferedMockTerm()
|
|
||||||
|
|
||||||
stdOut.Write(data[:i])
|
|
||||||
stdOut.Write(data[i:])
|
|
||||||
assertHandlerOutput(t, mock, plainText, commands...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func helpsTestOutputSplitCommands(t *testing.T, data []byte, plainText string, commands ...string) {
|
|
||||||
for i := 1; i < len(data)-1; i++ {
|
|
||||||
helpsTestOutputSplitCommandsAtIndex(t, data, i, plainText, commands...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func injectCommandAt(data string, i int, command string) string {
|
|
||||||
retValue := make([]byte, len(data)+len(command)+4)
|
|
||||||
retValue = append(retValue, data[:i]...)
|
|
||||||
retValue = append(retValue, data[i:]...)
|
|
||||||
return string(retValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputSplitChunks(t *testing.T) {
|
|
||||||
data := StringToBytes("qwertyuiopasdfghjklzxcvbnm")
|
|
||||||
helpsTestOutputSplitChunks(t, data)
|
|
||||||
helpsTestOutputSplitChunks(t, StringToBytes("BBBBB"))
|
|
||||||
helpsTestOutputSplitThreeChunks(t, StringToBytes("ABCDE"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputSplitChunksIncludingCommands(t *testing.T) {
|
|
||||||
helpsTestOutputSplitCommands(t, StringToBytes("Hello world.\x1B[mHello again."), "Hello world.Hello again.", "\x1B[m")
|
|
||||||
helpsTestOutputSplitCommandsAtIndex(t, StringToBytes("Hello world.\x1B[mHello again."), 2, "Hello world.Hello again.", "\x1B[m")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSplitChunkUnicode(t *testing.T) {
|
|
||||||
for _, l := range languages {
|
|
||||||
data := StringToBytes(l)
|
|
||||||
helpsTestOutputSplitChunks(t, data)
|
|
||||||
helpsTestOutputSplitThreeChunks(t, data)
|
|
||||||
}
|
|
||||||
}
|
|
256
term/windows/ansi_reader.go
Normal file
256
term/windows/ansi_reader.go
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
. "github.com/Azure/go-ansiterm"
|
||||||
|
. "github.com/Azure/go-ansiterm/winterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ansiReader wraps a standard input file (e.g., os.Stdin) providing ANSI sequence translation.
|
||||||
|
type ansiReader struct {
|
||||||
|
file *os.File
|
||||||
|
fd uintptr
|
||||||
|
buffer []byte
|
||||||
|
cbBuffer int
|
||||||
|
command []byte
|
||||||
|
// TODO(azlinux): Remove this and hard-code the string -- it is not going to change
|
||||||
|
escapeSequence []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnsiReader(nFile int) *ansiReader {
|
||||||
|
file, fd := GetStdFile(nFile)
|
||||||
|
return &ansiReader{
|
||||||
|
file: file,
|
||||||
|
fd: fd,
|
||||||
|
command: make([]byte, 0, ANSI_MAX_CMD_LENGTH),
|
||||||
|
escapeSequence: []byte(KEY_ESC_CSI),
|
||||||
|
buffer: make([]byte, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the wrapped file.
|
||||||
|
func (ar *ansiReader) Close() (err error) {
|
||||||
|
return ar.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fd returns the file descriptor of the wrapped file.
|
||||||
|
func (ar *ansiReader) Fd() uintptr {
|
||||||
|
return ar.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads up to len(p) bytes of translated input events into p.
|
||||||
|
func (ar *ansiReader) Read(p []byte) (int, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previously read bytes exist, read as much as we can and return
|
||||||
|
if len(ar.buffer) > 0 {
|
||||||
|
logger.Debugf("Reading previously cached bytes")
|
||||||
|
|
||||||
|
originalLength := len(ar.buffer)
|
||||||
|
copiedLength := copy(p, ar.buffer)
|
||||||
|
|
||||||
|
if copiedLength == originalLength {
|
||||||
|
ar.buffer = make([]byte, 0, len(p))
|
||||||
|
} else {
|
||||||
|
ar.buffer = ar.buffer[copiedLength:]
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Read from cache p[%d]: % x", copiedLength, p)
|
||||||
|
return copiedLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and translate key events
|
||||||
|
events, err := readInputEvents(ar.fd, len(p))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if len(events) == 0 {
|
||||||
|
logger.Debug("No input events detected")
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes := translateKeyEvents(events, ar.escapeSequence)
|
||||||
|
|
||||||
|
// Save excess bytes and right-size keyBytes
|
||||||
|
if len(keyBytes) > len(p) {
|
||||||
|
logger.Debugf("Received %d keyBytes, only room for %d bytes", len(keyBytes), len(p))
|
||||||
|
ar.buffer = keyBytes[len(p):]
|
||||||
|
keyBytes = keyBytes[:len(p)]
|
||||||
|
} else if len(keyBytes) == 0 {
|
||||||
|
logger.Debug("No key bytes returned from the translater")
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
copiedLength := copy(p, keyBytes)
|
||||||
|
if copiedLength != len(keyBytes) {
|
||||||
|
return 0, errors.New("Unexpected copy length encountered.")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Read p[%d]: % x", copiedLength, p)
|
||||||
|
logger.Debugf("Read keyBytes[%d]: % x", copiedLength, keyBytes)
|
||||||
|
return copiedLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readInputEvents polls until at least one event is available.
|
||||||
|
func readInputEvents(fd uintptr, maxBytes int) ([]INPUT_RECORD, error) {
|
||||||
|
// Determine the maximum number of records to retrieve
|
||||||
|
// -- Cast around the type system to obtain the size of a single INPUT_RECORD.
|
||||||
|
// unsafe.Sizeof requires an expression vs. a type-reference; the casting
|
||||||
|
// tricks the type system into believing it has such an expression.
|
||||||
|
recordSize := int(unsafe.Sizeof(*((*INPUT_RECORD)(unsafe.Pointer(&maxBytes)))))
|
||||||
|
countRecords := maxBytes / recordSize
|
||||||
|
if countRecords > MAX_INPUT_EVENTS {
|
||||||
|
countRecords = MAX_INPUT_EVENTS
|
||||||
|
}
|
||||||
|
logger.Debugf("[windows] readInputEvents: Reading %v records (buffer size %v, record size %v)", countRecords, maxBytes, recordSize)
|
||||||
|
|
||||||
|
// Wait for and read input events
|
||||||
|
events := make([]INPUT_RECORD, countRecords)
|
||||||
|
nEvents := uint32(0)
|
||||||
|
eventsExist, err := WaitForSingleObject(fd, WAIT_INFINITE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventsExist {
|
||||||
|
err = ReadConsoleInput(fd, events, &nEvents)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a slice restricted to the number of returned records
|
||||||
|
logger.Debugf("[windows] readInputEvents: Read %v events", nEvents)
|
||||||
|
return events[:nEvents], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyEvent Translation Helpers
|
||||||
|
|
||||||
|
var arrowKeyMapPrefix = map[WORD]string{
|
||||||
|
VK_UP: "%s%sA",
|
||||||
|
VK_DOWN: "%s%sB",
|
||||||
|
VK_RIGHT: "%s%sC",
|
||||||
|
VK_LEFT: "%s%sD",
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyMapPrefix = map[WORD]string{
|
||||||
|
VK_UP: "\x1B[%sA",
|
||||||
|
VK_DOWN: "\x1B[%sB",
|
||||||
|
VK_RIGHT: "\x1B[%sC",
|
||||||
|
VK_LEFT: "\x1B[%sD",
|
||||||
|
VK_HOME: "\x1B[1%s~", // showkey shows ^[[1
|
||||||
|
VK_END: "\x1B[4%s~", // showkey shows ^[[4
|
||||||
|
VK_INSERT: "\x1B[2%s~",
|
||||||
|
VK_DELETE: "\x1B[3%s~",
|
||||||
|
VK_PRIOR: "\x1B[5%s~",
|
||||||
|
VK_NEXT: "\x1B[6%s~",
|
||||||
|
VK_F1: "",
|
||||||
|
VK_F2: "",
|
||||||
|
VK_F3: "\x1B[13%s~",
|
||||||
|
VK_F4: "\x1B[14%s~",
|
||||||
|
VK_F5: "\x1B[15%s~",
|
||||||
|
VK_F6: "\x1B[17%s~",
|
||||||
|
VK_F7: "\x1B[18%s~",
|
||||||
|
VK_F8: "\x1B[19%s~",
|
||||||
|
VK_F9: "\x1B[20%s~",
|
||||||
|
VK_F10: "\x1B[21%s~",
|
||||||
|
VK_F11: "\x1B[23%s~",
|
||||||
|
VK_F12: "\x1B[24%s~",
|
||||||
|
}
|
||||||
|
|
||||||
|
// translateKeyEvents converts the input events into the appropriate ANSI string.
|
||||||
|
func translateKeyEvents(events []INPUT_RECORD, escapeSequence []byte) []byte {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for _, event := range events {
|
||||||
|
if event.EventType == KEY_EVENT && event.KeyEvent.KeyDown != 0 {
|
||||||
|
buffer.WriteString(keyToString(&event.KeyEvent, escapeSequence))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyToString maps the given input event record to the corresponding string.
|
||||||
|
func keyToString(keyEvent *KEY_EVENT_RECORD, escapeSequence []byte) string {
|
||||||
|
if keyEvent.UnicodeChar == 0 {
|
||||||
|
return formatVirtualKey(keyEvent.VirtualKeyCode, keyEvent.ControlKeyState, escapeSequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, alt, control := getControlKeys(keyEvent.ControlKeyState)
|
||||||
|
if control {
|
||||||
|
// TODO(azlinux): Implement following control sequences
|
||||||
|
// <Ctrl>-D Signals the end of input from the keyboard; also exits current shell.
|
||||||
|
// <Ctrl>-H Deletes the first character to the left of the cursor. Also called the ERASE key.
|
||||||
|
// <Ctrl>-Q Restarts printing after it has been stopped with <Ctrl>-s.
|
||||||
|
// <Ctrl>-S Suspends printing on the screen (does not stop the program).
|
||||||
|
// <Ctrl>-U Deletes all characters on the current line. Also called the KILL key.
|
||||||
|
// <Ctrl>-E Quits current command and creates a core
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// <Alt>+Key generates ESC N Key
|
||||||
|
if !control && alt {
|
||||||
|
return KEY_ESC_N + strings.ToLower(string(keyEvent.UnicodeChar))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(keyEvent.UnicodeChar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatVirtualKey converts a virtual key (e.g., up arrow) into the appropriate ANSI string.
|
||||||
|
func formatVirtualKey(key WORD, controlState DWORD, escapeSequence []byte) string {
|
||||||
|
shift, alt, control := getControlKeys(controlState)
|
||||||
|
modifier := getControlKeysModifier(shift, alt, control, false)
|
||||||
|
|
||||||
|
if format, ok := arrowKeyMapPrefix[key]; ok {
|
||||||
|
return fmt.Sprintf(format, escapeSequence, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
if format, ok := keyMapPrefix[key]; ok {
|
||||||
|
return fmt.Sprintf(format, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getControlKeys extracts the shift, alt, and ctrl key states.
|
||||||
|
func getControlKeys(controlState DWORD) (shift, alt, control bool) {
|
||||||
|
shift = 0 != (controlState & SHIFT_PRESSED)
|
||||||
|
alt = 0 != (controlState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED))
|
||||||
|
control = 0 != (controlState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED))
|
||||||
|
return shift, alt, control
|
||||||
|
}
|
||||||
|
|
||||||
|
// getControlKeysModifier returns the ANSI modifier for the given combination of control keys.
|
||||||
|
func getControlKeysModifier(shift, alt, control, meta bool) string {
|
||||||
|
if shift && alt && control {
|
||||||
|
return KEY_CONTROL_PARAM_8
|
||||||
|
}
|
||||||
|
if alt && control {
|
||||||
|
return KEY_CONTROL_PARAM_7
|
||||||
|
}
|
||||||
|
if shift && control {
|
||||||
|
return KEY_CONTROL_PARAM_6
|
||||||
|
}
|
||||||
|
if control {
|
||||||
|
return KEY_CONTROL_PARAM_5
|
||||||
|
}
|
||||||
|
if shift && alt {
|
||||||
|
return KEY_CONTROL_PARAM_4
|
||||||
|
}
|
||||||
|
if alt {
|
||||||
|
return KEY_CONTROL_PARAM_3
|
||||||
|
}
|
||||||
|
if shift {
|
||||||
|
return KEY_CONTROL_PARAM_2
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
76
term/windows/ansi_writer.go
Normal file
76
term/windows/ansi_writer.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
. "github.com/Azure/go-ansiterm"
|
||||||
|
. "github.com/Azure/go-ansiterm/winterm"
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger *logrus.Logger
|
||||||
|
|
||||||
|
// ansiWriter wraps a standard output file (e.g., os.Stdout) providing ANSI sequence translation.
|
||||||
|
type ansiWriter struct {
|
||||||
|
file *os.File
|
||||||
|
fd uintptr
|
||||||
|
infoReset *CONSOLE_SCREEN_BUFFER_INFO
|
||||||
|
command []byte
|
||||||
|
escapeSequence []byte
|
||||||
|
inAnsiSequence bool
|
||||||
|
parser *AnsiParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnsiWriter(nFile int) *ansiWriter {
|
||||||
|
logFile := ioutil.Discard
|
||||||
|
|
||||||
|
if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" {
|
||||||
|
logFile, _ = os.Create("ansiReaderWriter.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = &logrus.Logger{
|
||||||
|
Out: logFile,
|
||||||
|
Formatter: new(logrus.TextFormatter),
|
||||||
|
Level: logrus.DebugLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
file, fd := GetStdFile(nFile)
|
||||||
|
info, err := GetConsoleScreenBufferInfo(fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := CreateParser("Ground", CreateWinEventHandler(fd, file))
|
||||||
|
logger.Infof("newAnsiWriter: parser %p", parser)
|
||||||
|
|
||||||
|
aw := &ansiWriter{
|
||||||
|
file: file,
|
||||||
|
fd: fd,
|
||||||
|
infoReset: info,
|
||||||
|
command: make([]byte, 0, ANSI_MAX_CMD_LENGTH),
|
||||||
|
escapeSequence: []byte(KEY_ESC_CSI),
|
||||||
|
parser: parser,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("newAnsiWriter: aw.parser %p", aw.parser)
|
||||||
|
logger.Infof("newAnsiWriter: %v", aw)
|
||||||
|
return aw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (aw *ansiWriter) Fd() uintptr {
|
||||||
|
return aw.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes len(p) bytes from p to the underlying data stream.
|
||||||
|
func (aw *ansiWriter) Write(p []byte) (total int, err error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Write: % x", p)
|
||||||
|
logger.Infof("Write: %s", string(p))
|
||||||
|
return aw.parser.Parse(p)
|
||||||
|
}
|
61
term/windows/console.go
Normal file
61
term/windows/console.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
. "github.com/Azure/go-ansiterm/winterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConsoleStreams, for each standard stream referencing a console, returns a wrapped version
|
||||||
|
// that handles ANSI character sequences.
|
||||||
|
func ConsoleStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
|
||||||
|
if IsConsole(os.Stdin.Fd()) {
|
||||||
|
stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE)
|
||||||
|
} else {
|
||||||
|
stdIn = os.Stdin
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsConsole(os.Stdout.Fd()) {
|
||||||
|
stdOut = newAnsiWriter(syscall.STD_OUTPUT_HANDLE)
|
||||||
|
} else {
|
||||||
|
stdOut = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsConsole(os.Stderr.Fd()) {
|
||||||
|
stdErr = newAnsiWriter(syscall.STD_ERROR_HANDLE)
|
||||||
|
} else {
|
||||||
|
stdErr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdIn, stdOut, stdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHandleInfo returns file descriptor and bool indicating whether the file is a console.
|
||||||
|
func GetHandleInfo(in interface{}) (uintptr, bool) {
|
||||||
|
switch t := in.(type) {
|
||||||
|
case *ansiReader:
|
||||||
|
return t.Fd(), true
|
||||||
|
case *ansiWriter:
|
||||||
|
return t.Fd(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
var inFd uintptr
|
||||||
|
var isTerminal bool
|
||||||
|
|
||||||
|
if file, ok := in.(*os.File); ok {
|
||||||
|
inFd = file.Fd()
|
||||||
|
isTerminal = IsConsole(inFd)
|
||||||
|
}
|
||||||
|
return inFd, isTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConsole returns true if the given file descriptor is a Windows Console.
|
||||||
|
// The code assumes that GetConsoleMode will return an error for file descriptors that are not a console.
|
||||||
|
func IsConsole(fd uintptr) bool {
|
||||||
|
_, e := GetConsoleMode(fd)
|
||||||
|
return e == nil
|
||||||
|
}
|
5
term/windows/windows.go
Normal file
5
term/windows/windows.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// These files implement ANSI-aware input and output streams for use by the Docker Windows client.
|
||||||
|
// When asked for the set of standard streams (e.g., stdin, stdout, stderr), the code will create
|
||||||
|
// and return pseudo-streams that convert ANSI sequences to / from Windows Console API calls.
|
||||||
|
|
||||||
|
package windows
|
3
term/windows/windows_test.go
Normal file
3
term/windows/windows_test.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// This file is necessary to pass the Docker tests.
|
||||||
|
|
||||||
|
package windows
|
Loading…
Reference in a new issue