From ce22032e9b0d23924e2677c0c7e48f88d38ba1ee Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Fri, 23 Jan 2015 17:33:49 -0800 Subject: [PATCH 1/6] 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 --- term/console_windows.go | 1074 +++++++++++++++++++++++++++++++++- term/console_windows_test.go | 232 ++++++++ term/term.go | 15 + term/term_emulator.go | 216 +++++++ term/term_emulator_test.go | 388 ++++++++++++ term/term_windows.go | 25 +- 6 files changed, 1917 insertions(+), 33 deletions(-) create mode 100644 term/console_windows_test.go create mode 100644 term/term_emulator.go create mode 100644 term/term_emulator_test.go diff --git a/term/console_windows.go b/term/console_windows.go index 6335b2b..5306920 100644 --- a/term/console_windows.go +++ b/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/term/console_windows_test.go b/term/console_windows_test.go new file mode 100644 index 0000000..01c2572 --- /dev/null +++ b/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/term/term.go b/term/term.go index 8d807d8..d21c73f 100644 --- a/term/term.go +++ b/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/term/term_emulator.go b/term/term_emulator.go new file mode 100644 index 0000000..1713f42 --- /dev/null +++ b/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/term/term_emulator_test.go b/term/term_emulator_test.go new file mode 100644 index 0000000..7a9e1ab --- /dev/null +++ b/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/term/term_windows.go b/term/term_windows.go index d372e86..ea4ba53 100644 --- a/term/term_windows.go +++ b/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 From c23a02e41afd0fd218a0b0c8161866d7314fb0f1 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Thu, 5 Mar 2015 16:41:48 -0800 Subject: [PATCH 2/6] Move windows console specific implementation in sub package Signed-off-by: Sachin Joshi --- term/term_windows.go | 33 +++++++++++++------ term/{ => winconsole}/console_windows.go | 15 ++++++--- term/{ => winconsole}/console_windows_test.go | 2 +- term/{ => winconsole}/term_emulator.go | 2 +- term/{ => winconsole}/term_emulator_test.go | 2 +- 5 files changed, 36 insertions(+), 18 deletions(-) rename term/{ => winconsole}/console_windows.go (99%) rename term/{ => winconsole}/console_windows_test.go (99%) rename term/{ => winconsole}/term_emulator.go (99%) rename term/{ => winconsole}/term_emulator_test.go (99%) diff --git a/term/term_windows.go b/term/term_windows.go index ea4ba53..711f744 100644 --- a/term/term_windows.go +++ b/term/term_windows.go @@ -1,7 +1,11 @@ // +build windows - package term +import ( + "github.com/docker/docker/pkg/term/winconsole" + "io" +) + // State holds the console mode for the terminal. type State struct { mode uint32 @@ -18,8 +22,8 @@ type Winsize struct { // GetWinsize gets the window size of the given terminal func GetWinsize(fd uintptr) (*Winsize, error) { ws := &Winsize{} - var info *CONSOLE_SCREEN_BUFFER_INFO - info, err := GetConsoleScreenBufferInfo(fd) + var info *winconsole.CONSOLE_SCREEN_BUFFER_INFO + info, err := winconsole.GetConsoleScreenBufferInfo(fd) if err != nil { return nil, err } @@ -41,19 +45,19 @@ func SetWinsize(fd uintptr, ws *Winsize) error { // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd uintptr) bool { - _, e := GetConsoleMode(fd) + _, e := winconsole.GetConsoleMode(fd) return e == nil } // 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) + return winconsole.SetConsoleMode(fd, state.mode) } // SaveState saves the state of the given console func SaveState(fd uintptr) (*State, error) { - mode, e := GetConsoleMode(fd) + mode, e := winconsole.GetConsoleMode(fd) if e != nil { return nil, e } @@ -63,9 +67,9 @@ func SaveState(fd uintptr) (*State, error) { // 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) - state.mode |= (ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT) - return SetConsoleMode(fd, state.mode) + state.mode &^= (winconsole.ENABLE_ECHO_INPUT) + state.mode |= (winconsole.ENABLE_PROCESSED_INPUT | winconsole.ENABLE_LINE_INPUT) + return winconsole.SetConsoleMode(fd, state.mode) } // SetRawTerminal puts the terminal connected to the given file descriptor into raw @@ -96,9 +100,18 @@ func MakeRaw(fd uintptr) (*State, error) { // 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) + err = winconsole.SetConsoleMode(fd, state.mode) if err != nil { return nil, err } return state, nil } + +// GetHandleInfo returns file descriptor and bool indicating whether the file is a terminal +func GetHandleInfo(in interface{}) (uintptr, bool) { + return winconsole.GetHandleInfo(in) +} + +func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { + return winconsole.StdStreams() +} diff --git a/term/console_windows.go b/term/winconsole/console_windows.go similarity index 99% rename from term/console_windows.go rename to term/winconsole/console_windows.go index 5306920..8121aee 100644 --- a/term/console_windows.go +++ b/term/winconsole/console_windows.go @@ -1,6 +1,6 @@ // +build windows -package term +package winconsole import ( "bytes" @@ -415,7 +415,7 @@ func getNumberOfChars(fromCoord COORD, toCoord COORD, screenSize COORD) uint32 { return 0 } -func clearDisplayRect(fileDesc uintptr, fillChar byte, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { +func clearDisplayRect(fileDesc uintptr, fillChar rune, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { var writeRegion SMALL_RECT writeRegion.Top = fromCoord.Y writeRegion.Left = fromCoord.X @@ -429,7 +429,7 @@ func clearDisplayRect(fileDesc uintptr, fillChar byte, attributes WORD, fromCoor if size > 0 { buffer := make([]CHAR_INFO, size) for i := 0; i < len(buffer); i++ { - buffer[i].UnicodeChar = WCHAR(string(fillChar)[0]) + buffer[i].UnicodeChar = WCHAR(fillChar) buffer[i].Attributes = attributes } @@ -445,7 +445,7 @@ func clearDisplayRect(fileDesc uintptr, fillChar byte, attributes WORD, fromCoor return true, uint32(size), nil } -func clearDisplayRange(fileDesc uintptr, fillChar byte, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { +func clearDisplayRange(fileDesc uintptr, fillChar rune, 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 { @@ -531,7 +531,6 @@ func getNumberOfConsoleInputEvents(fileDesc uintptr) (uint16, error) { 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 @@ -1103,3 +1102,9 @@ func marshal(c COORD) uint32 { // works only on intel-endian machines return uint32(uint32(uint16(c.Y))<<16 | uint32(uint16(c.X))) } + +// IsTerminal returns true if the given file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + _, e := GetConsoleMode(fd) + return e == nil +} diff --git a/term/console_windows_test.go b/term/winconsole/console_windows_test.go similarity index 99% rename from term/console_windows_test.go rename to term/winconsole/console_windows_test.go index 01c2572..ee9d968 100644 --- a/term/console_windows_test.go +++ b/term/winconsole/console_windows_test.go @@ -1,6 +1,6 @@ // +build windows -package term +package winconsole import ( "fmt" diff --git a/term/term_emulator.go b/term/winconsole/term_emulator.go similarity index 99% rename from term/term_emulator.go rename to term/winconsole/term_emulator.go index 1713f42..13bcde8 100644 --- a/term/term_emulator.go +++ b/term/winconsole/term_emulator.go @@ -1,4 +1,4 @@ -package term +package winconsole import ( "io" diff --git a/term/term_emulator_test.go b/term/winconsole/term_emulator_test.go similarity index 99% rename from term/term_emulator_test.go rename to term/winconsole/term_emulator_test.go index 7a9e1ab..7017d42 100644 --- a/term/term_emulator_test.go +++ b/term/winconsole/term_emulator_test.go @@ -1,4 +1,4 @@ -package term +package winconsole import ( "bytes" From 27c2a55648f891f53cf33fb488a784ed4cd1d491 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Fri, 6 Mar 2015 17:04:35 -0800 Subject: [PATCH 3/6] Use syscall consts, check for errors, Also rename func for non-windows specific names. Signed-off-by: Sachin Joshi --- term/term.go | 2 +- term/term_windows.go | 4 +-- term/winconsole/console_windows.go | 38 ++++++++++++++++----------- term/winconsole/term_emulator.go | 18 +++++++------ term/winconsole/term_emulator_test.go | 8 +++--- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/term/term.go b/term/term.go index d21c73f..b7a5d18 100644 --- a/term/term.go +++ b/term/term.go @@ -30,7 +30,7 @@ func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { return os.Stdout, os.Stderr, os.Stdin } -func GetHandleInfo(in interface{}) (uintptr, bool) { +func GetFdInfo(in interface{}) (uintptr, bool) { var inFd uintptr var isTerminalIn bool if file, ok := in.(*os.File); ok { diff --git a/term/term_windows.go b/term/term_windows.go index 711f744..7135728 100644 --- a/term/term_windows.go +++ b/term/term_windows.go @@ -107,8 +107,8 @@ func MakeRaw(fd uintptr) (*State, error) { return state, nil } -// GetHandleInfo returns file descriptor and bool indicating whether the file is a terminal -func GetHandleInfo(in interface{}) (uintptr, bool) { +// GetFdInfo returns file descriptor and bool indicating whether the file is a terminal +func GetFdInfo(in interface{}) (uintptr, bool) { return winconsole.GetHandleInfo(in) } diff --git a/term/winconsole/console_windows.go b/term/winconsole/console_windows.go index 8121aee..2d0421f 100644 --- a/term/winconsole/console_windows.go +++ b/term/winconsole/console_windows.go @@ -83,11 +83,6 @@ const ( 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 @@ -212,7 +207,10 @@ func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { } // Save current screen buffer info - handle, _ := syscall.GetStdHandle(STD_OUTPUT_HANDLE) + handle, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) + if nil != err { + panic("This should never happen as it is predefined handle.") + } screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle)) if err == nil { handler.screenBufferInfo = screenBufferInfo @@ -225,26 +223,36 @@ func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { wrappedWriter: os.Stdout, emulator: handler, command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + fd: uintptr(handle), } } else { stdOut = os.Stdout } if IsTerminal(os.Stderr.Fd()) { + handle, err := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) + if nil != err { + panic("This should never happen as it is predefined handle.") + } stdErr = &terminalWriter{ wrappedWriter: os.Stderr, emulator: handler, command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + fd: uintptr(handle), } } else { stdErr = os.Stderr - } if IsTerminal(os.Stdin.Fd()) { + handle, err := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE) + if nil != err { + panic("This should never happen as it is predefined handle.") + } stdIn = &terminalReader{ wrappedReader: os.Stdin, emulator: handler, command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + fd: uintptr(handle), } } else { stdIn = os.Stdin @@ -626,7 +634,7 @@ func getWindowsTextAttributeForAnsiValue(originalFlag WORD, defaultValue WORD, a } // HandleOutputCommand interpretes the Ansi commands and then makes appropriate Win32 calls -func (term *WindowsTerminal) HandleOutputCommand(command []byte) (n int, err error) { +func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n int, err error) { // console settings changes need to happen in atomic way term.outMutex.Lock() defer term.outMutex.Unlock() @@ -636,7 +644,7 @@ func (term *WindowsTerminal) HandleOutputCommand(command []byte) (n int, err err parsedCommand := parseAnsiCommand(command) // use appropriate handle - handle, _ := syscall.GetStdHandle(STD_OUTPUT_HANDLE) + handle := syscall.Handle(fd) switch parsedCommand.Command { case "m": @@ -891,7 +899,7 @@ func (term *WindowsTerminal) HandleOutputCommand(command []byte) (n int, err err } // WriteChars writes the bytes to given writer. -func (term *WindowsTerminal) WriteChars(w io.Writer, p []byte) (n int, err error) { +func (term *WindowsTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) { return w.Write(p) } @@ -1027,8 +1035,8 @@ func mapKeystokeToTerminalString(keyEvent *KEY_EVENT_RECORD, escapeSequence []by // 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) +func getAvailableInputEvents(fd uintptr) (inputEvents []INPUT_RECORD, err error) { + handle := syscall.Handle(fd) if nil != err { return nil, err } @@ -1064,7 +1072,7 @@ func getTranslatedKeyCodes(inputEvents []INPUT_RECORD, escapeSequence []byte) st } // ReadChars reads the characters from the given reader -func (term *WindowsTerminal) ReadChars(w io.Reader, p []byte) (n int, err error) { +func (term *WindowsTerminal) ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) { n = 0 for n < len(p) { select { @@ -1076,7 +1084,7 @@ func (term *WindowsTerminal) ReadChars(w io.Reader, p []byte) (n int, err error) if n > 0 { return n, nil } - inputEvents, _ := getAvailableInputEvents() + inputEvents, _ := getAvailableInputEvents(fd) if inputEvents != nil { if len(inputEvents) == 0 && nil != err { return n, err @@ -1094,7 +1102,7 @@ func (term *WindowsTerminal) ReadChars(w io.Reader, p []byte) (n int, err error) } // HandleInputSequence interprets the input sequence command -func (term *WindowsTerminal) HandleInputSequence(command []byte) (n int, err error) { +func (term *WindowsTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) { return 0, nil } diff --git a/term/winconsole/term_emulator.go b/term/winconsole/term_emulator.go index 13bcde8..2678aab 100644 --- a/term/winconsole/term_emulator.go +++ b/term/winconsole/term_emulator.go @@ -27,10 +27,10 @@ const ( // 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) + HandleOutputCommand(fd uintptr, command []byte) (n int, err error) + HandleInputSequence(fd uintptr, command []byte) (n int, err error) + WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) + ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) } type terminalWriter struct { @@ -38,6 +38,7 @@ type terminalWriter struct { emulator terminalEmulator command []byte inSequence bool + fd uintptr } type terminalReader struct { @@ -45,6 +46,7 @@ type terminalReader struct { emulator terminalEmulator command []byte inSequence bool + fd uintptr } // http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html @@ -91,7 +93,7 @@ func (tw *terminalWriter) Write(p []byte) (n int, err error) { if !isXtermOscSequence(tw.command, p[current]) { // found the last command character. // Now we have a complete command. - nchar, err := tw.emulator.HandleOutputCommand(tw.command) + nchar, err := tw.emulator.HandleOutputCommand(tw.fd, tw.command) totalWritten += nchar if err != nil { return totalWritten, err @@ -110,7 +112,7 @@ func (tw *terminalWriter) Write(p []byte) (n int, err error) { 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]) + nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:current]) totalWritten += nw if err != nil { return totalWritten, err @@ -126,7 +128,7 @@ func (tw *terminalWriter) Write(p []byte) (n int, err error) { 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:]) + nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:]) totalWritten += nw if err != nil { return totalWritten, err @@ -148,7 +150,7 @@ func (tr *terminalReader) Read(p []byte) (n int, err error) { if nil == tr.emulator { return tr.readFromWrappedReader(p) } - return tr.emulator.ReadChars(tr.wrappedReader, p) + return tr.emulator.ReadChars(tr.fd, tr.wrappedReader, p) } // Close the underlying stream diff --git a/term/winconsole/term_emulator_test.go b/term/winconsole/term_emulator_test.go index 7017d42..65de5a7 100644 --- a/term/winconsole/term_emulator_test.go +++ b/term/winconsole/term_emulator_test.go @@ -71,21 +71,21 @@ func (mt *mockTerminal) record(operation int, data []byte) { mt.OutputCommandSequence = append(mt.OutputCommandSequence, op) } -func (mt *mockTerminal) HandleOutputCommand(command []byte) (n int, err error) { +func (mt *mockTerminal) HandleOutputCommand(fd uintptr, command []byte) (n int, err error) { mt.record(COMMAND_OPERATION, command) return len(command), nil } -func (mt *mockTerminal) HandleInputSequence(command []byte) (n int, err error) { +func (mt *mockTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) { return 0, nil } -func (mt *mockTerminal) WriteChars(w io.Writer, p []byte) (n int, err error) { +func (mt *mockTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) { mt.record(WRITE_OPERATION, p) return len(p), nil } -func (mt *mockTerminal) ReadChars(w io.Reader, p []byte) (n int, err error) { +func (mt *mockTerminal) ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) { return len(p), nil } From 7fc3c0ba26088fae71f7a57a9195ee9bae71e2a0 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Wed, 18 Mar 2015 13:10:57 -0700 Subject: [PATCH 4/6] Fix panic with vi in busybox Following bugs are fixed: 1.Handle out of bound cursor movements: vi in busybox sets cursor to (999,999) expecting it to be set to right, bottom correctly. 2.Correctly determine redirected non-terminal file. Signed-off-by: Sachin Joshi --- term/winconsole/console_windows.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/term/winconsole/console_windows.go b/term/winconsole/console_windows.go index 2d0421f..859ee1a 100644 --- a/term/winconsole/console_windows.go +++ b/term/winconsole/console_windows.go @@ -265,6 +265,10 @@ func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { 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) + } if tr, ok := in.(*terminalReader); ok { if file, ok := tr.wrappedReader.(*os.File); ok { inFd = file.Fd() @@ -678,14 +682,24 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // [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). + screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle)) + if err != nil { + return len(command), err + } line, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) if err != nil { return len(command), err } + if line > int16(screenBufferInfo.Window.Bottom) { + line = int16(screenBufferInfo.Window.Bottom) + } column, err := parseInt16OrDefault(parsedCommand.getParam(1), 1) if err != nil { return len(command), err } + if column > int16(screenBufferInfo.Window.Right) { + column = int16(screenBufferInfo.Window.Right) + } // The numbers are not 0 based, but 1 based r, err = setConsoleCursorPosition(uintptr(handle), false, int16(column-1), int16(line-1)) if !r { From 9cc42dea7c4a1482bb1bcaa67b2026d4cfaa2068 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Thu, 19 Mar 2015 18:07:56 -0700 Subject: [PATCH 5/6] Reduce memory allocation and remove channels Signed-off-by: Sachin Joshi --- term/winconsole/console_windows.go | 70 +++++++++++++----------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/term/winconsole/console_windows.go b/term/winconsole/console_windows.go index 859ee1a..f10b1e0 100644 --- a/term/winconsole/console_windows.go +++ b/term/winconsole/console_windows.go @@ -83,6 +83,7 @@ const ( ANSI_MAX_CMD_LENGTH = 256 + MAX_INPUT_EVENTS = 128 MAX_INPUT_BUFFER = 1024 DEFAULT_WIDTH = 80 DEFAULT_HEIGHT = 24 @@ -195,15 +196,18 @@ type ( type WindowsTerminal struct { outMutex sync.Mutex inMutex sync.Mutex - inputBuffer chan byte + inputBuffer []byte + inputSize int + inputEvents []INPUT_RECORD 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), + inputBuffer: make([]byte, MAX_INPUT_BUFFER), inputEscapeSequence: []byte(KEY_ESC_CSI), + inputEvents: make([]INPUT_RECORD, MAX_INPUT_EVENTS), } // Save current screen buffer info @@ -218,6 +222,8 @@ func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { // Set the window size SetWindowSize(uintptr(handle), DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_HEIGHT) + buffer = make([]CHAR_INFO, screenBufferInfo.MaximumWindowSize.X*screenBufferInfo.MaximumWindowSize.Y) + if IsTerminal(os.Stdout.Fd()) { stdOut = &terminalWriter{ wrappedWriter: os.Stdout, @@ -427,6 +433,8 @@ func getNumberOfChars(fromCoord COORD, toCoord COORD, screenSize COORD) uint32 { return 0 } +var buffer []CHAR_INFO + func clearDisplayRect(fileDesc uintptr, fillChar rune, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { var writeRegion SMALL_RECT writeRegion.Top = fromCoord.Y @@ -439,14 +447,13 @@ func clearDisplayRect(fileDesc uintptr, fillChar rune, attributes WORD, fromCoor height := toCoord.Y - fromCoord.Y + 1 size := width * height if size > 0 { - buffer := make([]CHAR_INFO, size) - for i := 0; i < len(buffer); i++ { + for i := 0; i < int(size); i++ { buffer[i].UnicodeChar = WCHAR(fillChar) buffer[i].Attributes = attributes } // Write to buffer - r, err := writeConsoleOutput(fileDesc, buffer, windowSize, COORD{X: 0, Y: 0}, &writeRegion) + r, err := writeConsoleOutput(fileDesc, buffer[:size], windowSize, COORD{X: 0, Y: 0}, &writeRegion) if !r { if err != nil { return false, 0, err @@ -914,6 +921,9 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // WriteChars writes the bytes to given writer. func (term *WindowsTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) { + if len(p) == 0 { + return 0, nil + } return w.Write(p) } @@ -1049,24 +1059,19 @@ func mapKeystokeToTerminalString(keyEvent *KEY_EVENT_RECORD, escapeSequence []by // getAvailableInputEvents polls the console for availble events // The function does not return until at least one input record has been read. -func getAvailableInputEvents(fd uintptr) (inputEvents []INPUT_RECORD, err error) { +func getAvailableInputEvents(fd uintptr, inputEvents []INPUT_RECORD) (n int, err error) { handle := syscall.Handle(fd) if nil != err { - return nil, err + return 0, err } for { // Read number of console events available - tempBuffer := make([]INPUT_RECORD, MAX_INPUT_BUFFER) - nr, err := readConsoleInputKey(uintptr(handle), tempBuffer) + nr, err := readConsoleInputKey(uintptr(handle), inputEvents) if nr == 0 { - return nil, err + return 0, err } if 0 < nr { - retValue := make([]INPUT_RECORD, nr) - for i := 0; i < nr; i++ { - retValue[i] = tempBuffer[i] - } - return retValue, nil + return nr, nil } } } @@ -1086,32 +1091,19 @@ func getTranslatedKeyCodes(inputEvents []INPUT_RECORD, escapeSequence []byte) st } // ReadChars reads the characters from the given reader -func (term *WindowsTerminal) ReadChars(fd uintptr, 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(fd) - 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 - } - } - } +func (term *WindowsTerminal) ReadChars(fd uintptr, r io.Reader, p []byte) (n int, err error) { + for term.inputSize == 0 { + nr, err := getAvailableInputEvents(fd, term.inputEvents) + if nr == 0 && nil != err { + return n, err + } + if nr > 0 { + keyCodes := getTranslatedKeyCodes(term.inputEvents[:nr], term.inputEscapeSequence) + term.inputSize = copy(term.inputBuffer, keyCodes) } } + n = copy(p, term.inputBuffer[:term.inputSize]) + term.inputSize -= n return n, nil } From 4cbedb8d54eeeff614ccf875b2f5145d9f4e248a Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Sun, 22 Mar 2015 12:55:21 -0400 Subject: [PATCH 6/6] winconsole: cleanup Signed-off-by: Tibor Vass --- term/term.go | 4 +- term/term_windows.go | 5 +- term/winconsole/console_windows.go | 592 +++++++++++++---------------- term/winconsole/term_emulator.go | 2 +- 4 files changed, 261 insertions(+), 342 deletions(-) diff --git a/term/term.go b/term/term.go index b7a5d18..b945a3d 100644 --- a/term/term.go +++ b/term/term.go @@ -26,8 +26,8 @@ type Winsize struct { y uint16 } -func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { - return os.Stdout, os.Stderr, os.Stdin +func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { + return os.Stdin, os.Stdout, os.Stderr } func GetFdInfo(in interface{}) (uintptr, bool) { diff --git a/term/term_windows.go b/term/term_windows.go index 7135728..e6d9466 100644 --- a/term/term_windows.go +++ b/term/term_windows.go @@ -2,8 +2,9 @@ package term import ( - "github.com/docker/docker/pkg/term/winconsole" "io" + + "github.com/docker/docker/pkg/term/winconsole" ) // State holds the console mode for the terminal. @@ -112,6 +113,6 @@ func GetFdInfo(in interface{}) (uintptr, bool) { return winconsole.GetHandleInfo(in) } -func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { +func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { return winconsole.StdStreams() } diff --git a/term/winconsole/console_windows.go b/term/winconsole/console_windows.go index f10b1e0..19977b1 100644 --- a/term/winconsole/console_windows.go +++ b/term/winconsole/console_windows.go @@ -203,68 +203,70 @@ type WindowsTerminal struct { inputEscapeSequence []byte } -func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) { +func getStdHandle(stdhandle int) uintptr { + handle, err := syscall.GetStdHandle(stdhandle) + if err != nil { + panic(fmt.Errorf("could not get standard io handle %d", stdhandle)) + } + return uintptr(handle) +} + +func StdStreams() (stdIn io.ReadCloser, stdOut io.Writer, stdErr io.Writer) { handler := &WindowsTerminal{ inputBuffer: make([]byte, MAX_INPUT_BUFFER), inputEscapeSequence: []byte(KEY_ESC_CSI), inputEvents: make([]INPUT_RECORD, MAX_INPUT_EVENTS), } - // Save current screen buffer info - handle, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) - if nil != err { - panic("This should never happen as it is predefined 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) - buffer = make([]CHAR_INFO, screenBufferInfo.MaximumWindowSize.X*screenBufferInfo.MaximumWindowSize.Y) - - if IsTerminal(os.Stdout.Fd()) { - stdOut = &terminalWriter{ - wrappedWriter: os.Stdout, - emulator: handler, - command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), - fd: uintptr(handle), - } - } else { - stdOut = os.Stdout - - } - if IsTerminal(os.Stderr.Fd()) { - handle, err := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) - if nil != err { - panic("This should never happen as it is predefined handle.") - } - stdErr = &terminalWriter{ - wrappedWriter: os.Stderr, - emulator: handler, - command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), - fd: uintptr(handle), - } - } else { - stdErr = os.Stderr - } if IsTerminal(os.Stdin.Fd()) { - handle, err := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE) - if nil != err { - panic("This should never happen as it is predefined handle.") - } stdIn = &terminalReader{ wrappedReader: os.Stdin, emulator: handler, command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), - fd: uintptr(handle), + fd: getStdHandle(syscall.STD_INPUT_HANDLE), } } else { stdIn = os.Stdin } - return + if IsTerminal(os.Stdout.Fd()) { + stdoutHandle := getStdHandle(syscall.STD_OUTPUT_HANDLE) + + // Save current screen buffer info + screenBufferInfo, err := GetConsoleScreenBufferInfo(stdoutHandle) + if err != nil { + // If GetConsoleScreenBufferInfo returns a nil error, it usually means that stdout is not a TTY. + // However, this is in the branch where stdout is a TTY, hence the panic. + panic("could not get console screen buffer info") + } + handler.screenBufferInfo = screenBufferInfo + + // Set the window size + SetWindowSize(stdoutHandle, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_HEIGHT) + buffer = make([]CHAR_INFO, screenBufferInfo.MaximumWindowSize.X*screenBufferInfo.MaximumWindowSize.Y) + + stdOut = &terminalWriter{ + wrappedWriter: os.Stdout, + emulator: handler, + command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + fd: stdoutHandle, + } + } else { + stdOut = os.Stdout + } + + if IsTerminal(os.Stderr.Fd()) { + stdErr = &terminalWriter{ + wrappedWriter: os.Stderr, + emulator: handler, + command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + fd: getStdHandle(syscall.STD_ERROR_HANDLE), + } + } else { + stdErr = os.Stderr + } + + return stdIn, stdOut, stdErr } // GetHandleInfo returns file descriptor and bool indicating whether the file is a terminal @@ -284,81 +286,64 @@ func GetHandleInfo(in interface{}) (uintptr, bool) { 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 +func getError(r1, r2 uintptr, lastErr error) error { + // If the function fails, the return value is zero. + if r1 == 0 { + if lastErr != nil { + return lastErr } return syscall.EINVAL } return nil } +// 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(handle uintptr) (uint32, error) { + var mode uint32 + err := syscall.GetConsoleMode(syscall.Handle(handle), &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(handle uintptr, mode uint32) error { + return getError(setConsoleModeProc.Call(handle, uintptr(mode), 0)) +} + // 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) { +func SetCursorVisible(handle 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 + if err := getError(getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(&cursorInfo)), 0)); err != nil { + return false, err } 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 + if err := getError(setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(&cursorInfo)), 0)); err != nil { + return false, err } return true, nil } // SetWindowSize sets the size of the console window. -func SetWindowSize(fileDesc uintptr, width, height, max SHORT) (bool, error) { +func SetWindowSize(handle 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 + if err := getError(setConsoleWindowInfoProc.Call(handle, uintptr(1), uintptr(unsafe.Pointer(&window)))); err != nil { + return false, err } - r, _, err = setConsoleScreenBufferSizeProc.Call(uintptr(fileDesc), uintptr(marshal(coord))) - if r == 0 { - if err != nil { - return false, err - } - return false, syscall.EINVAL + if err := getError(setConsoleScreenBufferSizeProc.Call(handle, marshal(coord))); err != nil { + return false, err } - 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) { +func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { var info CONSOLE_SCREEN_BUFFER_INFO - r, _, err := getConsoleScreenBufferInfoProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&info)), 0) - if r == 0 { - if err != nil { - return nil, err - } - return nil, syscall.EINVAL + if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil { + return nil, err } return &info, nil } @@ -366,38 +351,22 @@ func GetConsoleScreenBufferInfo(fileDesc uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, // 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 setConsoleTextAttribute(handle uintptr, attribute WORD) error { + return getError(setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)) } -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 +func writeConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) (bool, error) { + if err := getError(writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), marshal(bufferSize), marshal(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))); err != nil { + return false, err } 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) { +func fillConsoleOutputCharacter(handle 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 + if err := getError(fillConsoleOutputCharacterProc.Call(handle, uintptr(fillChar), uintptr(length), marshal(writeCord), uintptr(unsafe.Pointer(&out)))); err != nil { + return false, err } return true, nil } @@ -435,7 +404,7 @@ func getNumberOfChars(fromCoord COORD, toCoord COORD, screenSize COORD) uint32 { var buffer []CHAR_INFO -func clearDisplayRect(fileDesc uintptr, fillChar rune, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { +func clearDisplayRect(handle uintptr, fillChar rune, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (uint32, error) { var writeRegion SMALL_RECT writeRegion.Top = fromCoord.Y writeRegion.Left = fromCoord.X @@ -453,117 +422,87 @@ func clearDisplayRect(fileDesc uintptr, fillChar rune, attributes WORD, fromCoor } // Write to buffer - r, err := writeConsoleOutput(fileDesc, buffer[:size], windowSize, COORD{X: 0, Y: 0}, &writeRegion) + r, err := writeConsoleOutput(handle, buffer[:size], windowSize, COORD{X: 0, Y: 0}, &writeRegion) if !r { if err != nil { - return false, 0, err + return 0, err } - return false, 0, syscall.EINVAL + return 0, syscall.EINVAL } } - return true, uint32(size), nil + return uint32(size), nil } -func clearDisplayRange(fileDesc uintptr, fillChar rune, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (bool, uint32, error) { +func clearDisplayRange(handle uintptr, fillChar rune, attributes WORD, fromCoord COORD, toCoord COORD, windowSize COORD) (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 + return clearDisplayRect(handle, fillChar, attributes, fromCoord, toCoord, windowSize) } // 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 + n, err := clearDisplayRect(handle, fillChar, attributes, fromCoord, COORD{X: windowSize.X - 1, Y: fromCoord.Y}, windowSize) + if err != nil { + return nw, err } 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 + n, err = clearDisplayRect(handle, fillChar, attributes, COORD{X: 0, Y: fromCoord.Y + 1}, COORD{X: windowSize.X - 1, Y: toCoord.Y - 1}, windowSize) + if err != nil { + return nw, err } 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 + n, err = clearDisplayRect(handle, fillChar, attributes, COORD{X: 0, Y: toCoord.Y}, toCoord, windowSize) + if err != nil { + return nw, err } nw += n } - return true, nw, nil + return 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 +func setConsoleCursorPosition(handle uintptr, isRelative bool, column int16, line int16) error { + screenBufferInfo, err := GetConsoleScreenBufferInfo(handle) + if err != nil { + return err } - return false, err + 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) + } + return getError(setConsoleCursorPositionProc.Call(handle, marshal(position), 0)) } // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683207(v=vs.85).aspx -func getNumberOfConsoleInputEvents(fileDesc uintptr) (uint16, error) { +func getNumberOfConsoleInputEvents(handle 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 { - return uint16(n), nil + if err := getError(getNumberOfConsoleInputEventsProc.Call(handle, uintptr(unsafe.Pointer(&n)))); err != nil { + return 0, err } - return 0, err + return uint16(n), nil } //http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx -func readConsoleInputKey(fileDesc uintptr, inputBuffer []INPUT_RECORD) (int, error) { +func readConsoleInputKey(handle 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 + if err := getError(readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&inputBuffer[0])), uintptr(len(inputBuffer)), uintptr(unsafe.Pointer(&nr)))); err != nil { + return 0, err } - return int(0), err + return int(nr), nil } func getWindowsTextAttributeForAnsiValue(originalFlag WORD, defaultValue WORD, ansiValue int16) (WORD, error) { @@ -638,25 +577,21 @@ func getWindowsTextAttributeForAnsiValue(originalFlag WORD, defaultValue WORD, a 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(fd uintptr, command []byte) (n int, err error) { +func (term *WindowsTerminal) HandleOutputCommand(handle uintptr, command []byte) (n int, err error) { + // always consider all the bytes in command, processed + n = len(command) + + parsedCommand := parseAnsiCommand(command) + // 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.Handle(fd) - switch parsedCommand.Command { case "m": // [Value;...;Valuem @@ -664,9 +599,9 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // 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)) + screenBufferInfo, err := GetConsoleScreenBufferInfo(handle) if err != nil { - return len(command), err + return n, err } flag := screenBufferInfo.Attributes for _, e := range parsedCommand.Parameters { @@ -675,42 +610,40 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n flag = term.screenBufferInfo.Attributes // reset } else { flag, err = getWindowsTextAttributeForAnsiValue(flag, term.screenBufferInfo.Attributes, int16(value)) - if nil != err { - return len(command), err + if err != nil { + return n, err } } } - r, err = setConsoleTextAttribute(uintptr(handle), flag) - if !r { - return len(command), err + if err := setConsoleTextAttribute(handle, flag); err != nil { + return n, 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). - screenBufferInfo, err := GetConsoleScreenBufferInfo(uintptr(handle)) + screenBufferInfo, err := GetConsoleScreenBufferInfo(handle) if err != nil { - return len(command), err + return n, err } line, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) if err != nil { - return len(command), err + return n, err } if line > int16(screenBufferInfo.Window.Bottom) { line = int16(screenBufferInfo.Window.Bottom) } column, err := parseInt16OrDefault(parsedCommand.getParam(1), 1) if err != nil { - return len(command), err + return n, err } if column > int16(screenBufferInfo.Window.Right) { column = int16(screenBufferInfo.Window.Right) } // 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 + if err := setConsoleCursorPosition(handle, false, column-1, line-1); err != nil { + return n, err } case "A": @@ -721,9 +654,8 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n if err != nil { return len(command), err } - r, err = setConsoleCursorPosition(uintptr(handle), true, 0, -1*value) - if !r { - return len(command), err + if err := setConsoleCursorPosition(handle, true, 0, -value); err != nil { + return n, err } case "B": // [valueB @@ -731,11 +663,10 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // 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 + return n, err } - r, err = setConsoleCursorPosition(uintptr(handle), true, 0, value) - if !r { - return len(command), err + if err := setConsoleCursorPosition(handle, true, 0, value); err != nil { + return n, err } case "C": // [valueC @@ -743,11 +674,10 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // 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 + return n, err } - r, err = setConsoleCursorPosition(uintptr(handle), true, int16(value), 0) - if !r { - return len(command), err + if err := setConsoleCursorPosition(handle, true, value, 0); err != nil { + return n, err } case "D": // [valueD @@ -755,11 +685,10 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // 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 + return n, err } - r, err = setConsoleCursorPosition(uintptr(handle), true, int16(-1*value), 0) - if !r { - return len(command), err + if err := setConsoleCursorPosition(handle, true, -value, 0); err != nil { + return n, err } case "J": // [J Erases from the cursor to the end of the screen, including the cursor position. @@ -768,50 +697,49 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // 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 + return n, 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: + screenBufferInfo, err := GetConsoleScreenBufferInfo(handle) + if err != nil { + return n, err + } + 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 - } + // 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 + } + if _, err := clearDisplayRange(uintptr(handle), ' ', term.screenBufferInfo.Attributes, start, end, screenBufferInfo.MaximumWindowSize); err != nil { + return n, err + } + // remember the the cursor position is 1 based + if err := setConsoleCursorPosition(handle, false, int16(cursor.X), int16(cursor.Y)); err != nil { + return n, err } case "K": // [K @@ -824,45 +752,44 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n 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 + if err != nil { + return n, err + } + 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 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 + } + if _, err := clearDisplayRange(uintptr(handle), ' ', term.screenBufferInfo.Attributes, start, end, screenBufferInfo.MaximumWindowSize); err != nil { + return n, err + } + // remember the the cursor position is 1 based + if err := setConsoleCursorPosition(uintptr(handle), false, int16(cursor.X), int16(cursor.Y)); err != nil { + return n, err } case "l": @@ -875,7 +802,6 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n 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": @@ -889,34 +815,32 @@ func (term *WindowsTerminal) HandleOutputCommand(fd uintptr, command []byte) (n // 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 + /* + 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. + 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. + 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 + return n, nil } // WriteChars writes the bytes to given writer. @@ -1059,19 +983,13 @@ func mapKeystokeToTerminalString(keyEvent *KEY_EVENT_RECORD, escapeSequence []by // getAvailableInputEvents polls the console for availble events // The function does not return until at least one input record has been read. -func getAvailableInputEvents(fd uintptr, inputEvents []INPUT_RECORD) (n int, err error) { - handle := syscall.Handle(fd) - if nil != err { - return 0, err - } +func getAvailableInputEvents(handle uintptr, inputEvents []INPUT_RECORD) (n int, err error) { + // TODO(azlinux): Why is there a for loop? Seems to me, that `n` cannot be negative. - tibor for { // Read number of console events available - nr, err := readConsoleInputKey(uintptr(handle), inputEvents) - if nr == 0 { - return 0, err - } - if 0 < nr { - return nr, nil + n, err = readConsoleInputKey(handle, inputEvents) + if err != nil || n >= 0 { + return n, err } } } @@ -1112,9 +1030,9 @@ func (term *WindowsTerminal) HandleInputSequence(fd uintptr, command []byte) (n return 0, nil } -func marshal(c COORD) uint32 { +func marshal(c COORD) uintptr { // works only on intel-endian machines - return uint32(uint32(uint16(c.Y))<<16 | uint32(uint16(c.X))) + return uintptr(uint32(uint32(uint16(c.Y))<<16 | uint32(uint16(c.X)))) } // IsTerminal returns true if the given file descriptor is a terminal. diff --git a/term/winconsole/term_emulator.go b/term/winconsole/term_emulator.go index 2678aab..8c9f342 100644 --- a/term/winconsole/term_emulator.go +++ b/term/winconsole/term_emulator.go @@ -211,7 +211,7 @@ func parseInt16OrDefault(s string, defaultValue int16) (n int16, err error) { return defaultValue, nil } parsedValue, err := strconv.ParseInt(s, 10, 16) - if nil != err { + if err != nil { return defaultValue, err } return int16(parsedValue), nil