From d8c3090dd9abe3f0d95f99c9d8c7660922e9a719 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Fri, 23 Jan 2015 17:33:49 -0800 Subject: [PATCH] ANSI terminal emulation for windows It is implemented by intercepting and interpreting the output escape sequence by calling win32 console apis. In addition the input from win32 console is translated to linux keycodes Signed-off-by: Sachin Joshi --- api/client/cli.go | 11 +- docker/docker.go | 9 +- pkg/term/console_windows.go | 1074 +++++++++++++++++++++++++++++- pkg/term/console_windows_test.go | 232 +++++++ pkg/term/term.go | 15 + pkg/term/term_emulator.go | 216 ++++++ pkg/term/term_emulator_test.go | 388 +++++++++++ pkg/term/term_windows.go | 25 +- 8 files changed, 1926 insertions(+), 44 deletions(-) create mode 100644 pkg/term/console_windows_test.go create mode 100644 pkg/term/term_emulator.go create mode 100644 pkg/term/term_emulator_test.go diff --git a/api/client/cli.go b/api/client/cli.go index fcf6c033fb..9ca5b1161c 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -137,19 +137,12 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, a if tlsConfig != nil { scheme = "https" } - if in != nil { - if file, ok := in.(*os.File); ok { - inFd = file.Fd() - isTerminalIn = term.IsTerminal(inFd) - } + inFd, isTerminalIn = term.GetHandleInfo(in) } if out != nil { - if file, ok := out.(*os.File); ok { - outFd = file.Fd() - isTerminalOut = term.IsTerminal(outFd) - } + outFd, isTerminalOut = term.GetHandleInfo(out) } if err == nil { diff --git a/docker/docker.go b/docker/docker.go index 0641830098..d55e84b8ec 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/autogen/dockerversion" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/reexec" + "github.com/docker/docker/pkg/term" "github.com/docker/docker/utils" ) @@ -47,6 +48,10 @@ func main() { initLogging(log.InfoLevel) } + // Set terminal emulation based on platform as required. + stdout, stderr, stdin := term.StdStreams() + log.SetOutput(stderr) + // -D, --debug, -l/--log-level=debug processing // When/if -D is removed this block can be deleted if *flDebug { @@ -124,9 +129,9 @@ func main() { } if *flTls || *flTlsVerify { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig) + cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig) } else { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], nil) + cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], nil) } if err := cli.Cmd(flag.Args()...); err != nil { diff --git a/pkg/term/console_windows.go b/pkg/term/console_windows.go index 6335b2b837..53069201c7 100644 --- a/pkg/term/console_windows.go +++ b/pkg/term/console_windows.go @@ -3,6 +3,13 @@ package term import ( + "bytes" + "fmt" + "io" + "os" + "strconv" + "strings" + "sync" "syscall" "unsafe" ) @@ -20,37 +27,124 @@ const ( // If parameter is a screen buffer handle, additional values ENABLE_PROCESSED_OUTPUT = 0x0001 ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 + + //http://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes + FOREGROUND_BLUE = 1 + FOREGROUND_GREEN = 2 + FOREGROUND_RED = 4 + FOREGROUND_INTENSITY = 8 + FOREGROUND_MASK_SET = 0x000F + FOREGROUND_MASK_UNSET = 0xFFF0 + + BACKGROUND_BLUE = 16 + BACKGROUND_GREEN = 32 + BACKGROUND_RED = 64 + BACKGROUND_INTENSITY = 128 + BACKGROUND_MASK_SET = 0x00F0 + BACKGROUND_MASK_UNSET = 0xFF0F + + COMMON_LVB_REVERSE_VIDEO = 0x4000 + COMMON_LVB_UNDERSCORE = 0x8000 + + // http://man7.org/linux/man-pages/man4/console_codes.4.html + // ECMA-48 Set Graphics Rendition + ANSI_ATTR_RESET = 0 + ANSI_ATTR_BOLD = 1 + ANSI_ATTR_DIM = 2 + ANSI_ATTR_UNDERLINE = 4 + ANSI_ATTR_BLINK = 5 + ANSI_ATTR_REVERSE = 7 + ANSI_ATTR_INVISIBLE = 8 + + ANSI_ATTR_UNDERLINE_OFF = 24 + ANSI_ATTR_BLINK_OFF = 25 + ANSI_ATTR_REVERSE_OFF = 27 + ANSI_ATTR_INVISIBLE_OFF = 8 + + ANSI_FOREGROUND_BLACK = 30 + ANSI_FOREGROUND_RED = 31 + ANSI_FOREGROUND_GREEN = 32 + ANSI_FOREGROUND_YELLOW = 33 + ANSI_FOREGROUND_BLUE = 34 + ANSI_FOREGROUND_MAGENTA = 35 + ANSI_FOREGROUND_CYAN = 36 + ANSI_FOREGROUND_WHITE = 37 + ANSI_FOREGROUND_DEFAULT = 39 + + ANSI_BACKGROUND_BLACK = 40 + ANSI_BACKGROUND_RED = 41 + ANSI_BACKGROUND_GREEN = 42 + ANSI_BACKGROUND_YELLOW = 43 + ANSI_BACKGROUND_BLUE = 44 + ANSI_BACKGROUND_MAGENTA = 45 + ANSI_BACKGROUND_CYAN = 46 + ANSI_BACKGROUND_WHITE = 47 + ANSI_BACKGROUND_DEFAULT = 49 + + ANSI_MAX_CMD_LENGTH = 256 + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683231(v=vs.85).aspx + STD_INPUT_HANDLE = -10 + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + + MAX_INPUT_BUFFER = 1024 + DEFAULT_WIDTH = 80 + DEFAULT_HEIGHT = 24 +) + +// http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx +const ( + VK_PRIOR = 0x21 // PAGE UP key + VK_NEXT = 0x22 // PAGE DOWN key + VK_END = 0x23 // END key + VK_HOME = 0x24 // HOME key + VK_LEFT = 0x25 // LEFT ARROW key + VK_UP = 0x26 // UP ARROW key + VK_RIGHT = 0x27 //RIGHT ARROW key + VK_DOWN = 0x28 //DOWN ARROW key + VK_SELECT = 0x29 //SELECT key + VK_PRINT = 0x2A //PRINT key + VK_EXECUTE = 0x2B //EXECUTE key + VK_SNAPSHOT = 0x2C //PRINT SCREEN key + VK_INSERT = 0x2D //INS key + VK_DELETE = 0x2E //DEL key + VK_HELP = 0x2F //HELP key + VK_F1 = 0x70 //F1 key + VK_F2 = 0x71 //F2 key + VK_F3 = 0x72 //F3 key + VK_F4 = 0x73 //F4 key + VK_F5 = 0x74 //F5 key + VK_F6 = 0x75 //F6 key + VK_F7 = 0x76 //F7 key + VK_F8 = 0x77 //F8 key + VK_F9 = 0x78 //F9 key + VK_F10 = 0x79 //F10 key + VK_F11 = 0x7A //F11 key + VK_F12 = 0x7B //F12 key ) var kernel32DLL = syscall.NewLazyDLL("kernel32.dll") var ( - setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode") - getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") + setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode") + getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") + setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition") + setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute") + fillConsoleOutputCharacterProc = kernel32DLL.NewProc("FillConsoleOutputCharacterW") + writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW") + readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW") + getNumberOfConsoleInputEventsProc = kernel32DLL.NewProc("GetNumberOfConsoleInputEvents") + getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo") + setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo") + setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo") + setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize") ) -func GetConsoleMode(fileDesc uintptr) (uint32, error) { - var mode uint32 - err := syscall.GetConsoleMode(syscall.Handle(fileDesc), &mode) - return mode, err -} - -func SetConsoleMode(fileDesc uintptr, mode uint32) error { - r, _, err := setConsoleModeProc.Call(fileDesc, uintptr(mode), 0) - if r == 0 { - if err != nil { - return err - } - return syscall.EINVAL - } - return nil -} - -// types for calling GetConsoleScreenBufferInfo +// types for calling various windows API // see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx type ( - SHORT int16 - + SHORT int16 SMALL_RECT struct { Left SHORT Top SHORT @@ -63,17 +157,182 @@ type ( Y SHORT } - WORD uint16 + BOOL int32 + WORD uint16 + WCHAR uint16 + DWORD uint32 CONSOLE_SCREEN_BUFFER_INFO struct { - dwSize COORD - dwCursorPosition COORD - wAttributes WORD - srWindow SMALL_RECT - dwMaximumWindowSize COORD + Size COORD + CursorPosition COORD + Attributes WORD + Window SMALL_RECT + MaximumWindowSize COORD + } + + CONSOLE_CURSOR_INFO struct { + Size DWORD + Visible BOOL + } + + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms684166(v=vs.85).aspx + KEY_EVENT_RECORD struct { + KeyDown BOOL + RepeatCount WORD + VirtualKeyCode WORD + VirtualScanCode WORD + UnicodeChar WCHAR + ControlKeyState DWORD + } + + INPUT_RECORD struct { + EventType WORD + KeyEvent KEY_EVENT_RECORD + } + + CHAR_INFO struct { + UnicodeChar WCHAR + Attributes WORD } ) +// Implements the TerminalEmulator interface +type WindowsTerminal struct { + outMutex sync.Mutex + inMutex sync.Mutex + inputBuffer chan byte + screenBufferInfo *CONSOLE_SCREEN_BUFFER_INFO + inputEscapeSequence []byte +} + +func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { + handler := &WindowsTerminal{ + inputBuffer: make(chan byte, MAX_INPUT_BUFFER), + inputEscapeSequence: []byte(KEY_ESC_CSI), + } + + // Save current screen buffer info + handle, _ := syscall.GetStdHandle(STD_OUTPUT_HANDLE) + screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle)) + if err == nil { + handler.screenBufferInfo = screenBufferInfo + } + + // Set the window size + SetWindowSize(uintptr(handle), DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_HEIGHT) + if IsTerminal(os.Stdout.Fd()) { + stdOut = &terminalWriter{ + wrappedWriter: os.Stdout, + emulator: handler, + command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + } + } else { + stdOut = os.Stdout + + } + if IsTerminal(os.Stderr.Fd()) { + stdErr = &terminalWriter{ + wrappedWriter: os.Stderr, + emulator: handler, + command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + } + } else { + stdErr = os.Stderr + + } + if IsTerminal(os.Stdin.Fd()) { + stdIn = &terminalReader{ + wrappedReader: os.Stdin, + emulator: handler, + command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + } + } else { + stdIn = os.Stdin + } + + return +} + +// GetHandleInfo returns file descriptor and bool indicating whether the file is a terminal +func GetHandleInfo(in interface{}) (uintptr, bool) { + var inFd uintptr + var isTerminalIn bool + if tr, ok := in.(*terminalReader); ok { + if file, ok := tr.wrappedReader.(*os.File); ok { + inFd = file.Fd() + isTerminalIn = IsTerminal(inFd) + } + } + return inFd, isTerminalIn +} + +// GetConsoleMode gets the console mode for given file descriptor +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx +func GetConsoleMode(fileDesc uintptr) (uint32, error) { + var mode uint32 + err := syscall.GetConsoleMode(syscall.Handle(fileDesc), &mode) + return mode, err +} + +// SetConsoleMode sets the console mode for given file descriptor +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx +func SetConsoleMode(fileDesc uintptr, mode uint32) error { + r, _, err := setConsoleModeProc.Call(fileDesc, uintptr(mode), 0) + if r == 0 { + if err != nil { + return err + } + return syscall.EINVAL + } + return nil +} + +// SetCursorVisible sets the cursor visbility +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx +func SetCursorVisible(fileDesc uintptr, isVisible BOOL) (bool, error) { + var cursorInfo CONSOLE_CURSOR_INFO + r, _, err := getConsoleCursorInfoProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&cursorInfo)), 0) + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + cursorInfo.Visible = isVisible + r, _, err = setConsoleCursorInfoProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&cursorInfo)), 0) + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + return true, nil +} + +// SetWindowSize sets the size of the console window. +func SetWindowSize(fileDesc uintptr, width, height, max SHORT) (bool, error) { + window := SMALL_RECT{Left: 0, Top: 0, Right: width - 1, Bottom: height - 1} + coord := COORD{X: width - 1, Y: max} + r, _, err := setConsoleWindowInfoProc.Call(uintptr(fileDesc), uintptr(BOOL(1)), uintptr(unsafe.Pointer(&window))) + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + r, _, err = setConsoleScreenBufferSizeProc.Call(uintptr(fileDesc), uintptr(marshal(coord))) + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + + return true, nil +} + +// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer. +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx func GetConsoleScreenBufferInfo(fileDesc uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { var info CONSOLE_SCREEN_BUFFER_INFO r, _, err := getConsoleScreenBufferInfoProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&info)), 0) @@ -85,3 +344,762 @@ func GetConsoleScreenBufferInfo(fileDesc uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, } return &info, nil } + +// setConsoleTextAttribute sets the attributes of characters written to the +// console screen buffer by the WriteFile or WriteConsole function, +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx +func setConsoleTextAttribute(fileDesc uintptr, attribute WORD) (bool, error) { + r, _, err := setConsoleTextAttributeProc.Call(uintptr(fileDesc), uintptr(attribute), 0) + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + return true, nil +} + +func writeConsoleOutput(fileDesc uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) (bool, error) { + r, _, err := writeConsoleOutputProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&buffer[0])), uintptr(marshal(bufferSize)), uintptr(marshal(bufferCoord)), uintptr(unsafe.Pointer(writeRegion))) + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + return true, nil +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682663(v=vs.85).aspx +func fillConsoleOutputCharacter(fileDesc uintptr, fillChar byte, length uint32, writeCord COORD) (bool, error) { + out := int64(0) + r, _, err := fillConsoleOutputCharacterProc.Call(uintptr(fileDesc), uintptr(fillChar), uintptr(length), uintptr(marshal(writeCord)), uintptr(unsafe.Pointer(&out))) + // If the function succeeds, the return value is nonzero. + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + return true, nil +} + +// Gets the number of space characters to write for "clearing" the section of terminal +func getNumberOfChars(fromCoord COORD, toCoord COORD, screenSize COORD) uint32 { + // must be valid cursor position + if fromCoord.X < 0 || fromCoord.Y < 0 || toCoord.X < 0 || toCoord.Y < 0 { + return 0 + } + if fromCoord.X >= screenSize.X || fromCoord.Y >= screenSize.Y || toCoord.X >= screenSize.X || toCoord.Y >= screenSize.Y { + return 0 + } + // can't be backwards + if fromCoord.Y > toCoord.Y { + return 0 + } + // same line + if fromCoord.Y == toCoord.Y { + return uint32(toCoord.X-fromCoord.X) + 1 + } + // spans more than one line + if fromCoord.Y < toCoord.Y { + // from start till end of line for first line + from start of line till end + retValue := uint32(screenSize.X-fromCoord.X) + uint32(toCoord.X) + 1 + // don't count first and last line + linesBetween := toCoord.Y - fromCoord.Y - 1 + if linesBetween > 0 { + retValue = retValue + uint32(linesBetween*screenSize.X) + } + return retValue + } + return 0 +} + +func clearDisplayRect(fileDesc uintptr, fillChar byte, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { + var writeRegion SMALL_RECT + writeRegion.Top = fromCoord.Y + writeRegion.Left = fromCoord.X + writeRegion.Right = toCoord.X + writeRegion.Bottom = toCoord.Y + + // allocate and initialize buffer + width := toCoord.X - fromCoord.X + 1 + height := toCoord.Y - fromCoord.Y + 1 + size := width * height + if size > 0 { + buffer := make([]CHAR_INFO, size) + for i := 0; i < len(buffer); i++ { + buffer[i].UnicodeChar = WCHAR(string(fillChar)[0]) + buffer[i].Attributes = attributes + } + + // Write to buffer + r, err := writeConsoleOutput(fileDesc, buffer, windowSize, COORD{X: 0, Y: 0}, &writeRegion) + if !r { + if err != nil { + return false, 0, err + } + return false, 0, syscall.EINVAL + } + } + return true, uint32(size), nil +} + +func clearDisplayRange(fileDesc uintptr, fillChar byte, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { + nw := uint32(0) + // start and end on same line + if fromCoord.Y == toCoord.Y { + r, charWritten, err := clearDisplayRect(fileDesc, fillChar, attributes, fromCoord, toCoord, windowSize) + if !r { + if err != nil { + return false, charWritten, err + } + return false, charWritten, syscall.EINVAL + } + return true, charWritten, nil + } + // TODO(azlinux): if full screen, optimize + + // spans more than one line + if fromCoord.Y < toCoord.Y { + // from start position till end of line for first line + r, n, err := clearDisplayRect(fileDesc, fillChar, attributes, fromCoord, COORD{X: windowSize.X - 1, Y: fromCoord.Y}, windowSize) + if !r { + if err != nil { + return false, nw, err + } + return false, nw, syscall.EINVAL + } + nw += n + // lines between + linesBetween := toCoord.Y - fromCoord.Y - 1 + if linesBetween > 0 { + r, n, err = clearDisplayRect(fileDesc, fillChar, attributes, COORD{X: 0, Y: fromCoord.Y + 1}, COORD{X: windowSize.X - 1, Y: toCoord.Y - 1}, windowSize) + if !r { + if err != nil { + return false, nw, err + } + return false, nw, syscall.EINVAL + } + nw += n + } + // lines at end + r, n, err = clearDisplayRect(fileDesc, fillChar, attributes, COORD{X: 0, Y: toCoord.Y}, toCoord, windowSize) + if !r { + if err != nil { + return false, nw, err + } + return false, nw, syscall.EINVAL + } + nw += n + } + return true, nw, nil +} + +// setConsoleCursorPosition sets the console cursor position +// Note The X and Y are zero based +// If relative is true then the new position is relative to current one +func setConsoleCursorPosition(fileDesc uintptr, isRelative bool, column int16, line int16) (bool, error) { + screenBufferInfo, err := GetConsoleScreenBufferInfo(fileDesc) + if err == nil { + var position COORD + if isRelative { + position.X = screenBufferInfo.CursorPosition.X + SHORT(column) + position.Y = screenBufferInfo.CursorPosition.Y + SHORT(line) + } else { + position.X = SHORT(column) + position.Y = SHORT(line) + } + + //convert + bits := marshal(position) + r, _, err := setConsoleCursorPositionProc.Call(uintptr(fileDesc), uintptr(bits), 0) + if r == 0 { + if err != nil { + return false, err + } + return false, syscall.EINVAL + } + return true, nil + } + return false, err +} + +// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683207(v=vs.85).aspx +func getNumberOfConsoleInputEvents(fileDesc uintptr) (uint16, error) { + var n WORD + r, _, err := getNumberOfConsoleInputEventsProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&n))) + //If the function succeeds, the return value is nonzero + if r != 0 { + //fmt.Printf("################%d #################\n", n) + return uint16(n), nil + } + return 0, err +} + +//http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx +func readConsoleInputKey(fileDesc uintptr, inputBuffer []INPUT_RECORD) (int, error) { + var nr WORD + r, _, err := readConsoleInputProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&inputBuffer[0])), uintptr(WORD(len(inputBuffer))), uintptr(unsafe.Pointer(&nr))) + //If the function succeeds, the return value is nonzero. + if r != 0 { + return int(nr), nil + } + return int(0), err +} + +func getWindowsTextAttributeForAnsiValue(originalFlag WORD, defaultValue WORD, ansiValue int16) (WORD, error) { + flag := WORD(originalFlag) + if flag == 0 { + flag = defaultValue + } + switch ansiValue { + case ANSI_ATTR_RESET: + flag &^= COMMON_LVB_UNDERSCORE + flag &^= BACKGROUND_INTENSITY + flag = flag | FOREGROUND_INTENSITY + case ANSI_ATTR_INVISIBLE: + // TODO: how do you reset reverse? + case ANSI_ATTR_UNDERLINE: + flag = flag | COMMON_LVB_UNDERSCORE + case ANSI_ATTR_BLINK: + // seems like background intenisty is blink + flag = flag | BACKGROUND_INTENSITY + case ANSI_ATTR_UNDERLINE_OFF: + flag &^= COMMON_LVB_UNDERSCORE + case ANSI_ATTR_BLINK_OFF: + // seems like background intenisty is blink + flag &^= BACKGROUND_INTENSITY + case ANSI_ATTR_BOLD: + flag = flag | FOREGROUND_INTENSITY + case ANSI_ATTR_DIM: + flag &^= FOREGROUND_INTENSITY + case ANSI_ATTR_REVERSE, ANSI_ATTR_REVERSE_OFF: + // swap forground and background bits + foreground := flag & FOREGROUND_MASK_SET + background := flag & BACKGROUND_MASK_SET + flag = (flag & BACKGROUND_MASK_UNSET & FOREGROUND_MASK_UNSET) | (foreground << 4) | (background >> 4) + + // FOREGROUND + case ANSI_FOREGROUND_DEFAULT: + flag = (flag & FOREGROUND_MASK_UNSET) | (defaultValue & FOREGROUND_MASK_SET) + case ANSI_FOREGROUND_BLACK: + flag = flag ^ (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE) + case ANSI_FOREGROUND_RED: + flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED + case ANSI_FOREGROUND_GREEN: + flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_GREEN + case ANSI_FOREGROUND_YELLOW: + flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED | FOREGROUND_GREEN + case ANSI_FOREGROUND_BLUE: + flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_BLUE + case ANSI_FOREGROUND_MAGENTA: + flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED | FOREGROUND_BLUE + case ANSI_FOREGROUND_CYAN: + flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_GREEN | FOREGROUND_BLUE + case ANSI_FOREGROUND_WHITE: + flag = (flag & FOREGROUND_MASK_UNSET) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE + + // Background + case ANSI_BACKGROUND_DEFAULT: + // Black with no intensity + flag = (flag & BACKGROUND_MASK_UNSET) | (defaultValue & BACKGROUND_MASK_SET) + case ANSI_BACKGROUND_BLACK: + flag = (flag & BACKGROUND_MASK_UNSET) + case ANSI_BACKGROUND_RED: + flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED + case ANSI_BACKGROUND_GREEN: + flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_GREEN + case ANSI_BACKGROUND_YELLOW: + flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED | BACKGROUND_GREEN + case ANSI_BACKGROUND_BLUE: + flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_BLUE + case ANSI_BACKGROUND_MAGENTA: + flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED | BACKGROUND_BLUE + case ANSI_BACKGROUND_CYAN: + flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_GREEN | BACKGROUND_BLUE + case ANSI_BACKGROUND_WHITE: + flag = (flag & BACKGROUND_MASK_UNSET) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE + default: + + } + return flag, nil +} + +// HandleOutputCommand interpretes the Ansi commands and then makes appropriate Win32 calls +func (term *WindowsTerminal) HandleOutputCommand(command []byte) (n int, err error) { + // console settings changes need to happen in atomic way + term.outMutex.Lock() + defer term.outMutex.Unlock() + + r := false + // Parse the command + parsedCommand := parseAnsiCommand(command) + + // use appropriate handle + handle, _ := syscall.GetStdHandle(STD_OUTPUT_HANDLE) + + switch parsedCommand.Command { + case "m": + // [Value;...;Valuem + // Set Graphics Mode: + // Calls the graphics functions specified by the following values. + // These specified functions remain active until the next occurrence of this escape sequence. + // Graphics mode changes the colors and attributes of text (such as bold and underline) displayed on the screen. + screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle)) + if err != nil { + return len(command), err + } + flag := screenBufferInfo.Attributes + for _, e := range parsedCommand.Parameters { + value, _ := strconv.ParseInt(e, 10, 16) // base 10, 16 bit + if value == ANSI_ATTR_RESET { + flag = term.screenBufferInfo.Attributes // reset + } else { + flag, err = getWindowsTextAttributeForAnsiValue(flag, term.screenBufferInfo.Attributes, int16(value)) + if nil != err { + return len(command), err + } + } + } + r, err = setConsoleTextAttribute(uintptr(handle), flag) + if !r { + return len(command), err + } + case "H", "f": + // [line;columnH + // [line;columnf + // Moves the cursor to the specified position (coordinates). + // If you do not specify a position, the cursor moves to the home position at the upper-left corner of the screen (line 0, column 0). + line, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) + if err != nil { + return len(command), err + } + column, err := parseInt16OrDefault(parsedCommand.getParam(1), 1) + if err != nil { + return len(command), err + } + // The numbers are not 0 based, but 1 based + r, err = setConsoleCursorPosition(uintptr(handle), false, int16(column-1), int16(line-1)) + if !r { + return len(command), err + } + + case "A": + // [valueA + // Moves the cursor up by the specified number of lines without changing columns. + // If the cursor is already on the top line, ignores this sequence. + value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) + if err != nil { + return len(command), err + } + r, err = setConsoleCursorPosition(uintptr(handle), true, 0, -1*value) + if !r { + return len(command), err + } + case "B": + // [valueB + // Moves the cursor down by the specified number of lines without changing columns. + // If the cursor is already on the bottom line, ignores this sequence. + value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) + if err != nil { + return len(command), err + } + r, err = setConsoleCursorPosition(uintptr(handle), true, 0, value) + if !r { + return len(command), err + } + case "C": + // [valueC + // Moves the cursor forward by the specified number of columns without changing lines. + // If the cursor is already in the rightmost column, ignores this sequence. + value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) + if err != nil { + return len(command), err + } + r, err = setConsoleCursorPosition(uintptr(handle), true, int16(value), 0) + if !r { + return len(command), err + } + case "D": + // [valueD + // Moves the cursor back by the specified number of columns without changing lines. + // If the cursor is already in the leftmost column, ignores this sequence. + value, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) + if err != nil { + return len(command), err + } + r, err = setConsoleCursorPosition(uintptr(handle), true, int16(-1*value), 0) + if !r { + return len(command), err + } + case "J": + // [J Erases from the cursor to the end of the screen, including the cursor position. + // [1J Erases from the beginning of the screen to the cursor, including the cursor position. + // [2J Erases the complete display. The cursor does not move. + // Clears the screen and moves the cursor to the home position (line 0, column 0). + value, err := parseInt16OrDefault(parsedCommand.getParam(0), 0) + if err != nil { + return len(command), err + } + var start COORD + var cursor COORD + var end COORD + screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle)) + if err == nil { + switch value { + case 0: + start = screenBufferInfo.CursorPosition + // end of the screen + end.X = screenBufferInfo.MaximumWindowSize.X - 1 + end.Y = screenBufferInfo.MaximumWindowSize.Y - 1 + // cursor + cursor = screenBufferInfo.CursorPosition + case 1: + + // start of the screen + start.X = 0 + start.Y = 0 + // end of the screen + end = screenBufferInfo.CursorPosition + // cursor + cursor = screenBufferInfo.CursorPosition + case 2: + // start of the screen + start.X = 0 + start.Y = 0 + // end of the screen + end.X = screenBufferInfo.MaximumWindowSize.X - 1 + end.Y = screenBufferInfo.MaximumWindowSize.Y - 1 + // cursor + cursor.X = 0 + cursor.Y = 0 + } + r, _, err = clearDisplayRange(uintptr(handle), ' ', term.screenBufferInfo.Attributes, start, end, screenBufferInfo.MaximumWindowSize) + if !r { + return len(command), err + } + // remember the the cursor position is 1 based + r, err = setConsoleCursorPosition(uintptr(handle), false, int16(cursor.X), int16(cursor.Y)) + if !r { + return len(command), err + } + } + case "K": + // [K + // Clears all characters from the cursor position to the end of the line (including the character at the cursor position). + // [K Erases from the cursor to the end of the line, including the cursor position. + // [1K Erases from the beginning of the line to the cursor, including the cursor position. + // [2K Erases the complete line. + value, err := parseInt16OrDefault(parsedCommand.getParam(0), 0) + var start COORD + var cursor COORD + var end COORD + screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle)) + if err == nil { + switch value { + case 0: + // start is where cursor is + start = screenBufferInfo.CursorPosition + // end of line + end.X = screenBufferInfo.MaximumWindowSize.X - 1 + end.Y = screenBufferInfo.CursorPosition.Y + // cursor remains the same + cursor = screenBufferInfo.CursorPosition + + case 1: + // beginning of line + start.X = 0 + start.Y = screenBufferInfo.CursorPosition.Y + // until cursor + end = screenBufferInfo.CursorPosition + // cursor remains the same + cursor = screenBufferInfo.CursorPosition + case 2: + // start of the line + start.X = 0 + start.Y = screenBufferInfo.MaximumWindowSize.Y - 1 + // end of the line + end.X = screenBufferInfo.MaximumWindowSize.X - 1 + end.Y = screenBufferInfo.MaximumWindowSize.Y - 1 + // cursor + cursor.X = 0 + cursor.Y = screenBufferInfo.MaximumWindowSize.Y - 1 + } + r, _, err = clearDisplayRange(uintptr(handle), ' ', term.screenBufferInfo.Attributes, start, end, screenBufferInfo.MaximumWindowSize) + if !r { + return len(command), err + } + // remember the the cursor position is 1 based + r, err = setConsoleCursorPosition(uintptr(handle), false, int16(cursor.X), int16(cursor.Y)) + if !r { + return len(command), err + } + } + + case "l": + for _, value := range parsedCommand.Parameters { + switch value { + case "?25", "25": + SetCursorVisible(uintptr(handle), BOOL(0)) + case "?1049", "1049": + // TODO (azlinux): Restore terminal + case "?1", "1": + // If the DECCKM function is reset, then the arrow keys send ANSI cursor sequences to the host. + term.inputEscapeSequence = []byte(KEY_ESC_CSI) + default: + } + } + case "h": + for _, value := range parsedCommand.Parameters { + switch value { + case "?25", "25": + SetCursorVisible(uintptr(handle), BOOL(1)) + case "?1049", "1049": + // TODO (azlinux): Save terminal + case "?1", "1": + // If the DECCKM function is set, then the arrow keys send application sequences to the host. + // DECCKM (default off): When set, the cursor keys send an ESC O prefix, rather than ESC [. + term.inputEscapeSequence = []byte(KEY_ESC_O) + default: + } + } + + case "]": + /* + TODO (azlinux): + Linux Console Private CSI Sequences + + The following sequences are neither ECMA-48 nor native VT102. They are + native to the Linux console driver. Colors are in SGR parameters: 0 = + black, 1 = red, 2 = green, 3 = brown, 4 = blue, 5 = magenta, 6 = cyan, + 7 = white. + + ESC [ 1 ; n ] Set color n as the underline color + ESC [ 2 ; n ] Set color n as the dim color + ESC [ 8 ] Make the current color pair the default attributes. + ESC [ 9 ; n ] Set screen blank timeout to n minutes. + ESC [ 10 ; n ] Set bell frequency in Hz. + ESC [ 11 ; n ] Set bell duration in msec. + ESC [ 12 ; n ] Bring specified console to the front. + ESC [ 13 ] Unblank the screen. + ESC [ 14 ; n ] Set the VESA powerdown interval in minutes. + + */ + default: + } + return len(command), nil +} + +// WriteChars writes the bytes to given writer. +func (term *WindowsTerminal) WriteChars(w io.Writer, p []byte) (n int, err error) { + return w.Write(p) +} + +const ( + CAPSLOCK_ON = 0x0080 //The CAPS LOCK light is on. + ENHANCED_KEY = 0x0100 //The key is enhanced. + LEFT_ALT_PRESSED = 0x0002 //The left ALT key is pressed. + LEFT_CTRL_PRESSED = 0x0008 //The left CTRL key is pressed. + NUMLOCK_ON = 0x0020 //The NUM LOCK light is on. + RIGHT_ALT_PRESSED = 0x0001 //The right ALT key is pressed. + RIGHT_CTRL_PRESSED = 0x0004 //The right CTRL key is pressed. + SCROLLLOCK_ON = 0x0040 //The SCROLL LOCK light is on. + SHIFT_PRESSED = 0x0010 // The SHIFT key is pressed. +) + +const ( + KEY_CONTROL_PARAM_2 = ";2" + KEY_CONTROL_PARAM_3 = ";3" + KEY_CONTROL_PARAM_4 = ";4" + KEY_CONTROL_PARAM_5 = ";5" + KEY_CONTROL_PARAM_6 = ";6" + KEY_CONTROL_PARAM_7 = ";7" + KEY_CONTROL_PARAM_8 = ";8" + KEY_ESC_CSI = "\x1B[" + KEY_ESC_N = "\x1BN" + KEY_ESC_O = "\x1BO" +) + +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~", +} + +var arrowKeyMapPrefix = map[WORD]string{ + VK_UP: "%s%sA", + VK_DOWN: "%s%sB", + VK_RIGHT: "%s%sC", + VK_LEFT: "%s%sD", +} + +func getControlStateParameter(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 "" +} + +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 +} + +func charSequenceForKeys(key WORD, controlState DWORD, escapeSequence []byte) string { + i, ok := arrowKeyMapPrefix[key] + if ok { + shift, alt, control := getControlKeys(controlState) + modifier := getControlStateParameter(shift, alt, control, false) + return fmt.Sprintf(i, escapeSequence, modifier) + } + + i, ok = keyMapPrefix[key] + if ok { + shift, alt, control := getControlKeys(controlState) + modifier := getControlStateParameter(shift, alt, control, false) + return fmt.Sprintf(i, modifier) + } + + return "" +} + +// mapKeystokeToTerminalString maps the given input event record to string +func mapKeystokeToTerminalString(keyEvent *KEY_EVENT_RECORD, escapeSequence []byte) string { + _, alt, control := getControlKeys(keyEvent.ControlKeyState) + if keyEvent.UnicodeChar == 0 { + return charSequenceForKeys(keyEvent.VirtualKeyCode, keyEvent.ControlKeyState, escapeSequence) + } + if control { + // TODO(azlinux): Implement following control sequences + // -D Signals the end of input from the keyboard; also exits current shell. + // -H Deletes the first character to the left of the cursor. Also called the ERASE key. + // -Q Restarts printing after it has been stopped with -s. + // -S Suspends printing on the screen (does not stop the program). + // -U Deletes all characters on the current line. Also called the KILL key. + // -E Quits current command and creates a core + + } + // +Key generates ESC N Key + if !control && alt { + return KEY_ESC_N + strings.ToLower(string(keyEvent.UnicodeChar)) + } + return string(keyEvent.UnicodeChar) +} + +// getAvailableInputEvents polls the console for availble events +// The function does not return until at least one input record has been read. +func getAvailableInputEvents() (inputEvents []INPUT_RECORD, err error) { + handle, _ := syscall.GetStdHandle(STD_INPUT_HANDLE) + if nil != err { + return nil, err + } + for { + // Read number of console events available + tempBuffer := make([]INPUT_RECORD, MAX_INPUT_BUFFER) + nr, err := readConsoleInputKey(uintptr(handle), tempBuffer) + if nr == 0 { + return nil, err + } + if 0 < nr { + retValue := make([]INPUT_RECORD, nr) + for i := 0; i < nr; i++ { + retValue[i] = tempBuffer[i] + } + return retValue, nil + } + } +} + +// getTranslatedKeyCodes converts the input events into the string of characters +// The ansi escape sequence are used to map key strokes to the strings +func getTranslatedKeyCodes(inputEvents []INPUT_RECORD, escapeSequence []byte) string { + var buf bytes.Buffer + for i := 0; i < len(inputEvents); i++ { + input := inputEvents[i] + if input.EventType == KEY_EVENT && input.KeyEvent.KeyDown != 0 { + keyString := mapKeystokeToTerminalString(&input.KeyEvent, escapeSequence) + buf.WriteString(keyString) + } + } + return buf.String() +} + +// ReadChars reads the characters from the given reader +func (term *WindowsTerminal) ReadChars(w io.Reader, p []byte) (n int, err error) { + n = 0 + for n < len(p) { + select { + case b := <-term.inputBuffer: + p[n] = b + n++ + default: + // Read at least one byte read + if n > 0 { + return n, nil + } + inputEvents, _ := getAvailableInputEvents() + if inputEvents != nil { + if len(inputEvents) == 0 && nil != err { + return n, err + } + if len(inputEvents) != 0 { + keyCodes := getTranslatedKeyCodes(inputEvents, term.inputEscapeSequence) + for _, b := range []byte(keyCodes) { + term.inputBuffer <- b + } + } + } + } + } + return n, nil +} + +// HandleInputSequence interprets the input sequence command +func (term *WindowsTerminal) HandleInputSequence(command []byte) (n int, err error) { + return 0, nil +} + +func marshal(c COORD) uint32 { + // works only on intel-endian machines + return uint32(uint32(uint16(c.Y))<<16 | uint32(uint16(c.X))) +} diff --git a/pkg/term/console_windows_test.go b/pkg/term/console_windows_test.go new file mode 100644 index 0000000000..01c25726c8 --- /dev/null +++ b/pkg/term/console_windows_test.go @@ -0,0 +1,232 @@ +// +build windows + +package term + +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 macth 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) + } + } +} diff --git a/pkg/term/term.go b/pkg/term/term.go index 8d807d8d44..d21c73fdb6 100644 --- a/pkg/term/term.go +++ b/pkg/term/term.go @@ -4,6 +4,7 @@ package term import ( "errors" + "io" "os" "os/signal" "syscall" @@ -25,6 +26,20 @@ type Winsize struct { y uint16 } +func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { + return os.Stdout, os.Stderr, os.Stdin +} + +func GetHandleInfo(in interface{}) (uintptr, bool) { + var inFd uintptr + var isTerminalIn bool + if file, ok := in.(*os.File); ok { + inFd = file.Fd() + isTerminalIn = IsTerminal(inFd) + } + return inFd, isTerminalIn +} + func GetWinsize(fd uintptr) (*Winsize, error) { ws := &Winsize{} _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws))) diff --git a/pkg/term/term_emulator.go b/pkg/term/term_emulator.go new file mode 100644 index 0000000000..1713f428a9 --- /dev/null +++ b/pkg/term/term_emulator.go @@ -0,0 +1,216 @@ +package term + +import ( + "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(command []byte) (n int, err error) + HandleInputSequence(command []byte) (n int, err error) + WriteChars(w io.Writer, p []byte) (n int, err error) + ReadChars(w io.Reader, p []byte) (n int, err error) +} + +type terminalWriter struct { + wrappedWriter io.Writer + emulator terminalEmulator + command []byte + inSequence bool +} + +type terminalReader struct { + wrappedReader io.ReadCloser + emulator terminalEmulator + command []byte + inSequence bool +} + +// 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.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.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.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.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 parseInt16OrDefault(s string, defaultValue int16) (n int16, err error) { + if s == "" { + return defaultValue, nil + } + parsedValue, err := strconv.ParseInt(s, 10, 16) + if nil != err { + return defaultValue, err + } + return int16(parsedValue), nil +} diff --git a/pkg/term/term_emulator_test.go b/pkg/term/term_emulator_test.go new file mode 100644 index 0000000000..7a9e1abc90 --- /dev/null +++ b/pkg/term/term_emulator_test.go @@ -0,0 +1,388 @@ +package term + +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(command []byte) (n int, err error) { + mt.record(COMMAND_OPERATION, command) + return len(command), nil +} + +func (mt *mockTerminal) HandleInputSequence(command []byte) (n int, err error) { + return 0, nil +} + +func (mt *mockTerminal) WriteChars(w io.Writer, p []byte) (n int, err error) { + mt.record(WRITE_OPERATION, p) + return len(p), nil +} + +func (mt *mockTerminal) ReadChars(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 recieved +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) + } +} diff --git a/pkg/term/term_windows.go b/pkg/term/term_windows.go index d372e86a88..ea4ba5376e 100644 --- a/pkg/term/term_windows.go +++ b/pkg/term/term_windows.go @@ -2,10 +2,12 @@ package term +// State holds the console mode for the terminal. type State struct { mode uint32 } +// Winsize is used for window size. type Winsize struct { Height uint16 Width uint16 @@ -13,6 +15,7 @@ type Winsize struct { y uint16 } +// GetWinsize gets the window size of the given terminal func GetWinsize(fd uintptr) (*Winsize, error) { ws := &Winsize{} var info *CONSOLE_SCREEN_BUFFER_INFO @@ -20,8 +23,9 @@ func GetWinsize(fd uintptr) (*Winsize, error) { if err != nil { return nil, err } - ws.Height = uint16(info.srWindow.Right - info.srWindow.Left + 1) - ws.Width = uint16(info.srWindow.Bottom - info.srWindow.Top + 1) + + ws.Width = uint16(info.Window.Right - info.Window.Left + 1) + ws.Height = uint16(info.Window.Bottom - info.Window.Top + 1) ws.x = 0 // todo azlinux -- this is the pixel size of the Window, and not currently used by any caller ws.y = 0 @@ -29,6 +33,8 @@ func GetWinsize(fd uintptr) (*Winsize, error) { return ws, nil } +// SetWinsize sets the terminal connected to the given file descriptor to a +// given size. func SetWinsize(fd uintptr, ws *Winsize) error { return nil } @@ -39,12 +45,13 @@ func IsTerminal(fd uintptr) bool { return e == nil } -// Restore restores the terminal connected to the given file descriptor to a +// RestoreTerminal restores the terminal connected to the given file descriptor to a // previous state. func RestoreTerminal(fd uintptr, state *State) error { return SetConsoleMode(fd, state.mode) } +// SaveState saves the state of the given console func SaveState(fd uintptr) (*State, error) { mode, e := GetConsoleMode(fd) if e != nil { @@ -53,6 +60,7 @@ func SaveState(fd uintptr) (*State, error) { return &State{mode}, nil } +// DisableEcho disbales the echo for given file descriptor and returns previous state // see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings func DisableEcho(fd uintptr, state *State) error { state.mode &^= (ENABLE_ECHO_INPUT) @@ -60,6 +68,9 @@ func DisableEcho(fd uintptr, state *State) error { return SetConsoleMode(fd, state.mode) } +// SetRawTerminal 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. func SetRawTerminal(fd uintptr) (*State, error) { oldState, err := MakeRaw(fd) if err != nil { @@ -79,8 +90,12 @@ func MakeRaw(fd uintptr) (*State, error) { return nil, err } - // see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings - state.mode &^= (ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT) + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx + // All three input modes, along with processed output mode, are designed to work together. + // It is best to either enable or disable all of these modes as a group. + // When all are enabled, the application is said to be in "cooked" mode, which means that most of the processing is handled for the application. + // When all are disabled, the application is in "raw" mode, which means that input is unfiltered and any processing is left to the application. + state.mode = 0 err = SetConsoleMode(fd, state.mode) if err != nil { return nil, err