Merge pull request #14838 from Microsoft/10662-ansirewrite

Windows: CLI Improvement (TP3)
This commit is contained in:
Jessie Frazelle 2015-07-27 17:30:14 -07:00
commit 809a231314
10 changed files with 499 additions and 1939 deletions

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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
View 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 ""
}

View 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
View 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
View 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

View file

@ -0,0 +1,3 @@
// This file is necessary to pass the Docker tests.
package windows