Windows: CLI Improvement
The Ansi parser and their associated actions have been decoupled. Now parsing results in call backs to an interface which performs the appropriate actions depending on the environment. This improvement provides a functional Vi experience and the vttest no longer panics. This PR replaces docker/docker #13224 with the latest console updates. Signed-off-by: John Howard <jhoward@microsoft.com>
This commit is contained in:
parent
e6e9fbe4f7
commit
9a30d8cf9f
10 changed files with 499 additions and 1939 deletions
|
@ -3,11 +3,14 @@
|
|||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/Azure/go-ansiterm/winterm"
|
||||
"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.
|
||||
|
@ -30,53 +33,97 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
|
|||
return os.Stdin, os.Stdout, os.Stderr
|
||||
case os.Getenv("MSYSTEM") != "":
|
||||
// MSYS (mingw) does not emulate ANSI well.
|
||||
return winconsole.WinConsoleStreams()
|
||||
return windows.ConsoleStreams()
|
||||
default:
|
||||
return winconsole.WinConsoleStreams()
|
||||
return windows.ConsoleStreams()
|
||||
}
|
||||
}
|
||||
|
||||
// GetFdInfo returns file descriptor and bool indicating whether the file is a terminal.
|
||||
func GetFdInfo(in interface{}) (uintptr, bool) {
|
||||
return winconsole.GetHandleInfo(in)
|
||||
return windows.GetHandleInfo(in)
|
||||
}
|
||||
|
||||
// GetWinsize retrieves the window size of the terminal connected to the passed file descriptor.
|
||||
func GetWinsize(fd uintptr) (*Winsize, error) {
|
||||
info, err := winconsole.GetConsoleScreenBufferInfo(fd)
|
||||
|
||||
info, err := winterm.GetConsoleScreenBufferInfo(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(azlinux): Set the pixel width / height of the console (currently unused by any caller)
|
||||
return &Winsize{
|
||||
winsize := &Winsize{
|
||||
Width: uint16(info.Window.Right - info.Window.Left + 1),
|
||||
Height: uint16(info.Window.Bottom - info.Window.Top + 1),
|
||||
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 sets the size of the given terminal connected to the passed file descriptor.
|
||||
func SetWinsize(fd uintptr, ws *Winsize) error {
|
||||
// TODO(azlinux): Implement SetWinsize
|
||||
logrus.Debugf("[windows] SetWinsize: WARNING -- Unsupported method invoked")
|
||||
return nil
|
||||
|
||||
// Ensure the requested dimensions are no larger than the maximum window size
|
||||
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.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
return winconsole.IsConsole(fd)
|
||||
return windows.IsConsole(fd)
|
||||
}
|
||||
|
||||
// RestoreTerminal restores the terminal connected to the given file descriptor to a
|
||||
// previous state.
|
||||
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.
|
||||
func SaveState(fd uintptr) (*State, error) {
|
||||
mode, e := winconsole.GetConsoleMode(fd)
|
||||
mode, e := winterm.GetConsoleMode(fd)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
@ -84,13 +131,20 @@ func SaveState(fd uintptr) (*State, error) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
mode := state.mode
|
||||
mode &^= winconsole.ENABLE_ECHO_INPUT
|
||||
mode |= winconsole.ENABLE_PROCESSED_INPUT | winconsole.ENABLE_LINE_INPUT
|
||||
// TODO(azlinux): Core code registers a goroutine to catch os.Interrupt and reset the terminal state.
|
||||
return winconsole.SetConsoleMode(fd, mode)
|
||||
mode &^= winterm.ENABLE_ECHO_INPUT
|
||||
mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT
|
||||
|
||||
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
|
||||
|
@ -101,13 +155,14 @@ func SetRawTerminal(fd uintptr) (*State, error) {
|
|||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// MakeRaw puts the terminal connected to the given file descriptor into raw
|
||||
// mode and returns the previous state of the terminal so that it can be
|
||||
// restored.
|
||||
// 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 restored.
|
||||
func MakeRaw(fd uintptr) (*State, error) {
|
||||
state, err := SaveState(fd)
|
||||
if err != nil {
|
||||
|
@ -120,20 +175,31 @@ func MakeRaw(fd uintptr) (*State, error) {
|
|||
mode := state.mode
|
||||
|
||||
// Disable these modes
|
||||
mode &^= winconsole.ENABLE_ECHO_INPUT
|
||||
mode &^= winconsole.ENABLE_LINE_INPUT
|
||||
mode &^= winconsole.ENABLE_MOUSE_INPUT
|
||||
mode &^= winconsole.ENABLE_WINDOW_INPUT
|
||||
mode &^= winconsole.ENABLE_PROCESSED_INPUT
|
||||
mode &^= winterm.ENABLE_ECHO_INPUT
|
||||
mode &^= winterm.ENABLE_LINE_INPUT
|
||||
mode &^= winterm.ENABLE_MOUSE_INPUT
|
||||
mode &^= winterm.ENABLE_WINDOW_INPUT
|
||||
mode &^= winterm.ENABLE_PROCESSED_INPUT
|
||||
|
||||
// Enable these modes
|
||||
mode |= winconsole.ENABLE_EXTENDED_FLAGS
|
||||
mode |= winconsole.ENABLE_INSERT_MODE
|
||||
mode |= winconsole.ENABLE_QUICK_EDIT_MODE
|
||||
mode |= winterm.ENABLE_EXTENDED_FLAGS
|
||||
mode |= winterm.ENABLE_INSERT_MODE
|
||||
mode |= winterm.ENABLE_QUICK_EDIT_MODE
|
||||
|
||||
err = winconsole.SetConsoleMode(fd, mode)
|
||||
err = winterm.SetConsoleMode(fd, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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