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