From 9a30d8cf9ffdc86e6e973b2f7252d37766b780a5 Mon Sep 17 00:00:00 2001 From: John Howard Date: Thu, 16 Apr 2015 14:01:37 -0700 Subject: [PATCH] Windows: CLI Improvement The Ansi parser and their associated actions have been decoupled. Now parsing results in call backs to an interface which performs the appropriate actions depending on the environment. This improvement provides a functional Vi experience and the vttest no longer panics. This PR replaces docker/docker #13224 with the latest console updates. Signed-off-by: John Howard --- term/term_windows.go | 130 ++- term/winconsole/console_windows.go | 1053 ----------------------- term/winconsole/console_windows_test.go | 232 ----- term/winconsole/term_emulator.go | 234 ----- term/winconsole/term_emulator_test.go | 388 --------- term/windows/ansi_reader.go | 256 ++++++ term/windows/ansi_writer.go | 76 ++ term/windows/console.go | 61 ++ term/windows/windows.go | 5 + term/windows/windows_test.go | 3 + 10 files changed, 499 insertions(+), 1939 deletions(-) delete mode 100644 term/winconsole/console_windows.go delete mode 100644 term/winconsole/console_windows_test.go delete mode 100644 term/winconsole/term_emulator.go delete mode 100644 term/winconsole/term_emulator_test.go create mode 100644 term/windows/ansi_reader.go create mode 100644 term/windows/ansi_writer.go create mode 100644 term/windows/console.go create mode 100644 term/windows/windows.go create mode 100644 term/windows/windows_test.go diff --git a/term/term_windows.go b/term/term_windows.go index f7fa1b3..2e97abf 100644 --- a/term/term_windows.go +++ b/term/term_windows.go @@ -3,11 +3,14 @@ package term import ( + "fmt" "io" "os" + "os/signal" + "github.com/Azure/go-ansiterm/winterm" "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/term/winconsole" + "github.com/docker/docker/pkg/term/windows" ) // State holds the console mode for the terminal. @@ -30,53 +33,97 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { return os.Stdin, os.Stdout, os.Stderr case os.Getenv("MSYSTEM") != "": // MSYS (mingw) does not emulate ANSI well. - return winconsole.WinConsoleStreams() + return windows.ConsoleStreams() default: - return winconsole.WinConsoleStreams() + return windows.ConsoleStreams() } } // GetFdInfo returns file descriptor and bool indicating whether the file is a terminal. func GetFdInfo(in interface{}) (uintptr, bool) { - return winconsole.GetHandleInfo(in) + return windows.GetHandleInfo(in) } // GetWinsize retrieves the window size of the terminal connected to the passed file descriptor. func GetWinsize(fd uintptr) (*Winsize, error) { - info, err := winconsole.GetConsoleScreenBufferInfo(fd) + + info, err := winterm.GetConsoleScreenBufferInfo(fd) if err != nil { return nil, err } - // TODO(azlinux): Set the pixel width / height of the console (currently unused by any caller) - return &Winsize{ + winsize := &Winsize{ Width: uint16(info.Window.Right - info.Window.Left + 1), Height: uint16(info.Window.Bottom - info.Window.Top + 1), x: 0, - y: 0}, nil + y: 0} + + // Note: GetWinsize is called frequently -- uncomment only for excessive details + // logrus.Debugf("[windows] GetWinsize: Console(%v)", info.String()) + // logrus.Debugf("[windows] GetWinsize: Width(%v), Height(%v), x(%v), y(%v)", winsize.Width, winsize.Height, winsize.x, winsize.y) + return winsize, nil } // SetWinsize sets the size of the given terminal connected to the passed file descriptor. func SetWinsize(fd uintptr, ws *Winsize) error { - // TODO(azlinux): Implement SetWinsize - logrus.Debugf("[windows] SetWinsize: WARNING -- Unsupported method invoked") - return nil + + // Ensure the requested dimensions are no larger than the maximum window size + info, err := winterm.GetConsoleScreenBufferInfo(fd) + if err != nil { + return err + } + + if ws.Width == 0 || ws.Height == 0 || ws.Width > uint16(info.MaximumWindowSize.X) || ws.Height > uint16(info.MaximumWindowSize.Y) { + return fmt.Errorf("Illegal window size: (%v,%v) -- Maximum allow: (%v,%v)", + ws.Width, ws.Height, info.MaximumWindowSize.X, info.MaximumWindowSize.Y) + } + + // Narrow the sizes to that used by Windows + var width winterm.SHORT = winterm.SHORT(ws.Width) + var height winterm.SHORT = winterm.SHORT(ws.Height) + + // Set the dimensions while ensuring they remain within the bounds of the backing console buffer + // -- Shrinking will always succeed. Growing may push the edges past the buffer boundary. When that occurs, + // shift the upper left just enough to keep the new window within the buffer. + rect := info.Window + if width < rect.Right-rect.Left+1 { + rect.Right = rect.Left + width - 1 + } else if width > rect.Right-rect.Left+1 { + rect.Right = rect.Left + width - 1 + if rect.Right >= info.Size.X { + rect.Left = info.Size.X - width + rect.Right = info.Size.X - 1 + } + } + + if height < rect.Bottom-rect.Top+1 { + rect.Bottom = rect.Top + height - 1 + } else if height > rect.Bottom-rect.Top+1 { + rect.Bottom = rect.Top + height - 1 + if rect.Bottom >= info.Size.Y { + rect.Top = info.Size.Y - height + rect.Bottom = info.Size.Y - 1 + } + } + logrus.Debugf("[windows] SetWinsize: Requested((%v,%v)) Actual(%v)", ws.Width, ws.Height, rect) + + return winterm.SetConsoleWindowInfo(fd, true, rect) } // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd uintptr) bool { - return winconsole.IsConsole(fd) + return windows.IsConsole(fd) } // RestoreTerminal restores the terminal connected to the given file descriptor to a // previous state. func RestoreTerminal(fd uintptr, state *State) error { - return winconsole.SetConsoleMode(fd, state.mode) + return winterm.SetConsoleMode(fd, state.mode) } // SaveState saves the state of the terminal connected to the given file descriptor. func SaveState(fd uintptr) (*State, error) { - mode, e := winconsole.GetConsoleMode(fd) + mode, e := winterm.GetConsoleMode(fd) if e != nil { return nil, e } @@ -84,13 +131,20 @@ func SaveState(fd uintptr) (*State, error) { } // DisableEcho disables echo for the terminal connected to the given file descriptor. -// -- See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx +// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx func DisableEcho(fd uintptr, state *State) error { mode := state.mode - mode &^= winconsole.ENABLE_ECHO_INPUT - mode |= winconsole.ENABLE_PROCESSED_INPUT | winconsole.ENABLE_LINE_INPUT - // TODO(azlinux): Core code registers a goroutine to catch os.Interrupt and reset the terminal state. - return winconsole.SetConsoleMode(fd, mode) + mode &^= winterm.ENABLE_ECHO_INPUT + mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT + + err := winterm.SetConsoleMode(fd, mode) + if err != nil { + return err + } + + // Register an interrupt handler to catch and restore prior state + restoreAtInterrupt(fd, state) + return nil } // SetRawTerminal puts the terminal connected to the given file descriptor into raw @@ -101,13 +155,14 @@ func SetRawTerminal(fd uintptr) (*State, error) { if err != nil { return nil, err } - // TODO(azlinux): Core code registers a goroutine to catch os.Interrupt and reset the terminal state. + + // Register an interrupt handler to catch and restore prior state + restoreAtInterrupt(fd, state) return state, err } -// MakeRaw puts the terminal connected to the given file descriptor into raw -// mode and returns the previous state of the terminal so that it can be -// restored. +// MakeRaw puts the terminal (Windows Console) connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be restored. func MakeRaw(fd uintptr) (*State, error) { state, err := SaveState(fd) if err != nil { @@ -120,20 +175,31 @@ func MakeRaw(fd uintptr) (*State, error) { mode := state.mode // Disable these modes - mode &^= winconsole.ENABLE_ECHO_INPUT - mode &^= winconsole.ENABLE_LINE_INPUT - mode &^= winconsole.ENABLE_MOUSE_INPUT - mode &^= winconsole.ENABLE_WINDOW_INPUT - mode &^= winconsole.ENABLE_PROCESSED_INPUT + mode &^= winterm.ENABLE_ECHO_INPUT + mode &^= winterm.ENABLE_LINE_INPUT + mode &^= winterm.ENABLE_MOUSE_INPUT + mode &^= winterm.ENABLE_WINDOW_INPUT + mode &^= winterm.ENABLE_PROCESSED_INPUT // Enable these modes - mode |= winconsole.ENABLE_EXTENDED_FLAGS - mode |= winconsole.ENABLE_INSERT_MODE - mode |= winconsole.ENABLE_QUICK_EDIT_MODE + mode |= winterm.ENABLE_EXTENDED_FLAGS + mode |= winterm.ENABLE_INSERT_MODE + mode |= winterm.ENABLE_QUICK_EDIT_MODE - err = winconsole.SetConsoleMode(fd, mode) + err = winterm.SetConsoleMode(fd, mode) if err != nil { return nil, err } return state, nil } + +func restoreAtInterrupt(fd uintptr, state *State) { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, os.Interrupt) + + go func() { + _ = <-sigchan + RestoreTerminal(fd, state) + os.Exit(0) + }() +} diff --git a/term/winconsole/console_windows.go b/term/winconsole/console_windows.go deleted file mode 100644 index ce40a93..0000000 --- a/term/winconsole/console_windows.go +++ /dev/null @@ -1,1053 +0,0 @@ -// +build windows - -package winconsole - -import ( - "bytes" - "fmt" - "io" - "os" - "strconv" - "strings" - "sync" - "syscall" - "unsafe" - - "github.com/Sirupsen/logrus" -) - -const ( - // Consts for Get/SetConsoleMode function - // -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx - ENABLE_PROCESSED_INPUT = 0x0001 - ENABLE_LINE_INPUT = 0x0002 - ENABLE_ECHO_INPUT = 0x0004 - ENABLE_WINDOW_INPUT = 0x0008 - ENABLE_MOUSE_INPUT = 0x0010 - ENABLE_INSERT_MODE = 0x0020 - ENABLE_QUICK_EDIT_MODE = 0x0040 - ENABLE_EXTENDED_FLAGS = 0x0080 - - // 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 - - MAX_INPUT_EVENTS = 128 - 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") - 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") -) - -// types for calling various windows API -// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx -type ( - SHORT int16 - BOOL int32 - WORD uint16 - WCHAR uint16 - DWORD uint32 - - SMALL_RECT struct { - Left SHORT - Top SHORT - Right SHORT - Bottom SHORT - } - - COORD struct { - X SHORT - Y SHORT - } - - CONSOLE_SCREEN_BUFFER_INFO struct { - 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 - } -) - -// TODO(azlinux): Basic type clean-up -// -- Convert all uses of uintptr to syscall.Handle to be consistent with Windows syscall -// -- Convert, as appropriate, types to use defined Windows types (e.g., DWORD instead of uint32) - -// Implements the TerminalEmulator interface -type WindowsTerminal struct { - outMutex sync.Mutex - inMutex sync.Mutex - inputBuffer []byte - inputSize int - inputEvents []INPUT_RECORD - screenBufferInfo *CONSOLE_SCREEN_BUFFER_INFO - inputEscapeSequence []byte -} - -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 WinConsoleStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { - handler := &WindowsTerminal{ - inputBuffer: make([]byte, MAX_INPUT_BUFFER), - inputEscapeSequence: []byte(KEY_ESC_CSI), - inputEvents: make([]INPUT_RECORD, MAX_INPUT_EVENTS), - } - - if IsConsole(os.Stdin.Fd()) { - stdIn = &terminalReader{ - wrappedReader: os.Stdin, - emulator: handler, - command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), - fd: getStdHandle(syscall.STD_INPUT_HANDLE), - } - } else { - stdIn = os.Stdin - } - - if IsConsole(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 - - 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 IsConsole(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 console. -func GetHandleInfo(in interface{}) (uintptr, bool) { - var inFd uintptr - var isTerminalIn bool - - switch t := in.(type) { - case *terminalReader: - in = t.wrappedReader - case *terminalWriter: - in = t.wrappedWriter - } - - if file, ok := in.(*os.File); ok { - inFd = file.Fd() - isTerminalIn = IsConsole(inFd) - } - return inFd, isTerminalIn -} - -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(handle uintptr, isVisible BOOL) (bool, error) { - var cursorInfo *CONSOLE_CURSOR_INFO = &CONSOLE_CURSOR_INFO{} - if err := getError(getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)); err != nil { - return false, err - } - cursorInfo.Visible = isVisible - 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(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} - if err := getError(setConsoleWindowInfoProc.Call(handle, uintptr(1), uintptr(unsafe.Pointer(&window)))); err != nil { - return false, err - } - 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(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { - var info CONSOLE_SCREEN_BUFFER_INFO - if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil { - return nil, err - } - 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(handle uintptr, attribute WORD) error { - return getError(setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)) -} - -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(handle uintptr, fillChar byte, length uint32, writeCord COORD) (bool, error) { - out := int64(0) - if err := getError(fillConsoleOutputCharacterProc.Call(handle, uintptr(fillChar), uintptr(length), marshal(writeCord), uintptr(unsafe.Pointer(&out)))); err != nil { - return false, err - } - 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 -} - -var buffer []CHAR_INFO - -func clearDisplayRect(handle uintptr, attributes WORD, fromCoord COORD, toCoord COORD) (uint32, error) { - var writeRegion SMALL_RECT - writeRegion.Left = fromCoord.X - writeRegion.Top = fromCoord.Y - 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 := uint32(width) * uint32(height) - if size > 0 { - buffer := make([]CHAR_INFO, size) - for i := range buffer { - buffer[i] = CHAR_INFO{WCHAR(' '), attributes} - } - - // Write to buffer - r, err := writeConsoleOutput(handle, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, &writeRegion) - if !r { - if err != nil { - return 0, err - } - return 0, syscall.EINVAL - } - } - return uint32(size), nil -} - -func clearDisplayRange(handle uintptr, attributes WORD, fromCoord COORD, toCoord COORD) (uint32, error) { - nw := uint32(0) - // start and end on same line - if fromCoord.Y == toCoord.Y { - return clearDisplayRect(handle, attributes, fromCoord, toCoord) - } - // 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 - n, err := clearDisplayRect(handle, attributes, fromCoord, COORD{X: toCoord.X, Y: fromCoord.Y}) - if err != nil { - return nw, err - } - nw += n - // lines between - linesBetween := toCoord.Y - fromCoord.Y - 1 - if linesBetween > 0 { - n, err = clearDisplayRect(handle, attributes, COORD{X: 0, Y: fromCoord.Y + 1}, COORD{X: toCoord.X, Y: toCoord.Y - 1}) - if err != nil { - return nw, err - } - nw += n - } - // lines at end - n, err = clearDisplayRect(handle, attributes, COORD{X: 0, Y: toCoord.Y}, toCoord) - if err != nil { - return nw, err - } - nw += n - } - 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(handle uintptr, isRelative bool, column int16, line int16) error { - screenBufferInfo, err := GetConsoleScreenBufferInfo(handle) - if err != nil { - return 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(handle uintptr) (uint16, error) { - var n DWORD - if err := getError(getNumberOfConsoleInputEventsProc.Call(handle, uintptr(unsafe.Pointer(&n)))); err != nil { - return 0, err - } - return uint16(n), nil -} - -//http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx -func readConsoleInputKey(handle uintptr, inputBuffer []INPUT_RECORD) (int, error) { - var nr DWORD - 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(nr), nil -} - -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 - } - return flag, nil -} - -// HandleOutputCommand interpretes the Ansi commands and then makes appropriate Win32 calls -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) - logrus.Debugf("[windows] HandleOutputCommand: %v", parsedCommand) - - // console settings changes need to happen in atomic way - term.outMutex.Lock() - defer term.outMutex.Unlock() - - 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(handle) - if err != nil { - return n, 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 err != nil { - return n, 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(handle) - if err != nil { - return n, err - } - line, err := parseInt16OrDefault(parsedCommand.getParam(0), 1) - if err != nil { - return n, err - } - if line > int16(screenBufferInfo.Window.Bottom) { - line = int16(screenBufferInfo.Window.Bottom) + 1 - } - column, err := parseInt16OrDefault(parsedCommand.getParam(1), 1) - if err != nil { - return n, err - } - if column > int16(screenBufferInfo.Window.Right) { - column = int16(screenBufferInfo.Window.Right) + 1 - } - // The numbers are not 0 based, but 1 based - logrus.Debugf("[windows] HandleOutputCommmand: Moving cursor to (%v,%v)", column-1, line-1) - if err := setConsoleCursorPosition(handle, false, column-1, line-1); err != nil { - return n, 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 - } - if err := setConsoleCursorPosition(handle, true, 0, -value); err != nil { - return n, 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 n, err - } - if err := setConsoleCursorPosition(handle, true, 0, value); err != nil { - return n, 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 n, err - } - if err := setConsoleCursorPosition(handle, true, value, 0); err != nil { - return n, 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 n, 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. - // [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 n, err - } - var start COORD - var cursor COORD - var end COORD - screenBufferInfo, err := GetConsoleScreenBufferInfo(handle) - if err != nil { - return n, err - } - switch value { - case 0: - start = screenBufferInfo.CursorPosition - // end of the buffer - end.X = screenBufferInfo.Size.X - 1 - end.Y = screenBufferInfo.Size.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 buffer - end.X = screenBufferInfo.Size.X - 1 - end.Y = screenBufferInfo.Size.Y - 1 - // cursor - cursor.X = 0 - cursor.Y = 0 - } - if _, err := clearDisplayRange(uintptr(handle), term.screenBufferInfo.Attributes, start, end); 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 - // 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 { - return n, err - } - switch value { - case 0: - // start is where cursor is - start = screenBufferInfo.CursorPosition - // end of line - end.X = screenBufferInfo.Size.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.CursorPosition.Y - 1 - // end of the line - end.X = screenBufferInfo.Size.X - 1 - end.Y = screenBufferInfo.CursorPosition.Y - 1 - // cursor - cursor.X = 0 - cursor.Y = screenBufferInfo.CursorPosition.Y - 1 - } - if _, err := clearDisplayRange(uintptr(handle), term.screenBufferInfo.Attributes, start, end); 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": - 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) - } - } - 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) - } - } - - 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. - - */ - } - return n, nil -} - -// 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) -} - -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(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 - n, err = readConsoleInputKey(handle, inputEvents) - if err != nil || n >= 0 { - return n, err - } - } -} - -// 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(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 -} - -// HandleInputSequence interprets the input sequence command -func (term *WindowsTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) { - return 0, nil -} - -func marshal(c COORD) uintptr { - return uintptr(*((*DWORD)(unsafe.Pointer(&c)))) -} - -// IsConsole returns true if the given file descriptor is a terminal. -// -- The code assumes that GetConsoleMode will return an error for file descriptors that are not a console. -func IsConsole(fd uintptr) bool { - _, e := GetConsoleMode(fd) - return e == nil -} diff --git a/term/winconsole/console_windows_test.go b/term/winconsole/console_windows_test.go deleted file mode 100644 index edb5d6f..0000000 --- a/term/winconsole/console_windows_test.go +++ /dev/null @@ -1,232 +0,0 @@ -// +build windows - -package winconsole - -import ( - "fmt" - "testing" -) - -func helpsTestParseInt16OrDefault(t *testing.T, expectedValue int16, shouldFail bool, input string, defaultValue int16, format string, args ...string) { - value, err := parseInt16OrDefault(input, defaultValue) - if nil != err && !shouldFail { - t.Errorf("Unexpected error returned %v", err) - t.Errorf(format, args) - } - if nil == err && shouldFail { - t.Errorf("Should have failed as expected\n\tReturned value = %d", value) - t.Errorf(format, args) - } - if expectedValue != value { - t.Errorf("The value returned does not match expected\n\tExpected:%v\n\t:Actual%v", expectedValue, value) - t.Errorf(format, args) - } -} - -func TestParseInt16OrDefault(t *testing.T) { - // empty string - helpsTestParseInt16OrDefault(t, 0, false, "", 0, "Empty string returns default") - helpsTestParseInt16OrDefault(t, 2, false, "", 2, "Empty string returns default") - - // normal case - helpsTestParseInt16OrDefault(t, 0, false, "0", 0, "0 handled correctly") - helpsTestParseInt16OrDefault(t, 111, false, "111", 2, "Normal") - helpsTestParseInt16OrDefault(t, 111, false, "+111", 2, "+N") - helpsTestParseInt16OrDefault(t, -111, false, "-111", 2, "-N") - helpsTestParseInt16OrDefault(t, 0, false, "+0", 11, "+0") - helpsTestParseInt16OrDefault(t, 0, false, "-0", 12, "-0") - - // ill formed strings - helpsTestParseInt16OrDefault(t, 0, true, "abc", 0, "Invalid string") - helpsTestParseInt16OrDefault(t, 42, true, "+= 23", 42, "Invalid string") - helpsTestParseInt16OrDefault(t, 42, true, "123.45", 42, "float like") - -} - -func helpsTestGetNumberOfChars(t *testing.T, expected uint32, fromCoord COORD, toCoord COORD, screenSize COORD, format string, args ...interface{}) { - actual := getNumberOfChars(fromCoord, toCoord, screenSize) - mesg := fmt.Sprintf(format, args) - assertTrue(t, expected == actual, fmt.Sprintf("%s Expected=%d, Actual=%d, Parameters = { fromCoord=%+v, toCoord=%+v, screenSize=%+v", mesg, expected, actual, fromCoord, toCoord, screenSize)) -} - -func TestGetNumberOfChars(t *testing.T) { - // Note: The columns and lines are 0 based - // Also that interval is "inclusive" means will have both start and end chars - // This test only tests the number opf characters being written - - // all four corners - maxWindow := COORD{X: 80, Y: 50} - leftTop := COORD{X: 0, Y: 0} - rightTop := COORD{X: 79, Y: 0} - leftBottom := COORD{X: 0, Y: 49} - rightBottom := COORD{X: 79, Y: 49} - - // same position - helpsTestGetNumberOfChars(t, 1, COORD{X: 1, Y: 14}, COORD{X: 1, Y: 14}, COORD{X: 80, Y: 50}, "Same position random line") - - // four corners - helpsTestGetNumberOfChars(t, 1, leftTop, leftTop, maxWindow, "Same position- leftTop") - helpsTestGetNumberOfChars(t, 1, rightTop, rightTop, maxWindow, "Same position- rightTop") - helpsTestGetNumberOfChars(t, 1, leftBottom, leftBottom, maxWindow, "Same position- leftBottom") - helpsTestGetNumberOfChars(t, 1, rightBottom, rightBottom, maxWindow, "Same position- rightBottom") - - // from this char to next char on same line - helpsTestGetNumberOfChars(t, 2, COORD{X: 0, Y: 0}, COORD{X: 1, Y: 0}, maxWindow, "Next position on same line") - helpsTestGetNumberOfChars(t, 2, COORD{X: 1, Y: 14}, COORD{X: 2, Y: 14}, maxWindow, "Next position on same line") - - // from this char to next 10 chars on same line - helpsTestGetNumberOfChars(t, 11, COORD{X: 0, Y: 0}, COORD{X: 10, Y: 0}, maxWindow, "Next position on same line") - helpsTestGetNumberOfChars(t, 11, COORD{X: 1, Y: 14}, COORD{X: 11, Y: 14}, maxWindow, "Next position on same line") - - helpsTestGetNumberOfChars(t, 5, COORD{X: 3, Y: 11}, COORD{X: 7, Y: 11}, maxWindow, "To and from on same line") - - helpsTestGetNumberOfChars(t, 8, COORD{X: 0, Y: 34}, COORD{X: 7, Y: 34}, maxWindow, "Start of line to middle") - helpsTestGetNumberOfChars(t, 4, COORD{X: 76, Y: 34}, COORD{X: 79, Y: 34}, maxWindow, "Middle to end of line") - - // multiple lines - 1 - helpsTestGetNumberOfChars(t, 81, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 1}, maxWindow, "one line below same X") - helpsTestGetNumberOfChars(t, 81, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 11}, maxWindow, "one line below same X") - - // multiple lines - 2 - helpsTestGetNumberOfChars(t, 161, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 2}, maxWindow, "one line below same X") - helpsTestGetNumberOfChars(t, 161, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 12}, maxWindow, "one line below same X") - - // multiple lines - 3 - helpsTestGetNumberOfChars(t, 241, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 3}, maxWindow, "one line below same X") - helpsTestGetNumberOfChars(t, 241, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 13}, maxWindow, "one line below same X") - - // full line - helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 0}, COORD{X: 79, Y: 0}, maxWindow, "Full line - first") - helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 23}, COORD{X: 79, Y: 23}, maxWindow, "Full line - random") - helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 49}, COORD{X: 79, Y: 49}, maxWindow, "Full line - last") - - // full screen - helpsTestGetNumberOfChars(t, 80*50, leftTop, rightBottom, maxWindow, "full screen") - - helpsTestGetNumberOfChars(t, 80*50-1, COORD{X: 1, Y: 0}, rightBottom, maxWindow, "dropping first char to, end of screen") - helpsTestGetNumberOfChars(t, 80*50-2, COORD{X: 2, Y: 0}, rightBottom, maxWindow, "dropping first two char to, end of screen") - - helpsTestGetNumberOfChars(t, 80*50-1, leftTop, COORD{X: 78, Y: 49}, maxWindow, "from start of screen, till last char-1") - helpsTestGetNumberOfChars(t, 80*50-2, leftTop, COORD{X: 77, Y: 49}, maxWindow, "from start of screen, till last char-2") - - helpsTestGetNumberOfChars(t, 80*50-5, COORD{X: 4, Y: 0}, COORD{X: 78, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-1") - helpsTestGetNumberOfChars(t, 80*50-6, COORD{X: 4, Y: 0}, COORD{X: 77, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-2") -} - -var allForeground = []int16{ - ANSI_FOREGROUND_BLACK, - ANSI_FOREGROUND_RED, - ANSI_FOREGROUND_GREEN, - ANSI_FOREGROUND_YELLOW, - ANSI_FOREGROUND_BLUE, - ANSI_FOREGROUND_MAGENTA, - ANSI_FOREGROUND_CYAN, - ANSI_FOREGROUND_WHITE, - ANSI_FOREGROUND_DEFAULT, -} -var allBackground = []int16{ - ANSI_BACKGROUND_BLACK, - ANSI_BACKGROUND_RED, - ANSI_BACKGROUND_GREEN, - ANSI_BACKGROUND_YELLOW, - ANSI_BACKGROUND_BLUE, - ANSI_BACKGROUND_MAGENTA, - ANSI_BACKGROUND_CYAN, - ANSI_BACKGROUND_WHITE, - ANSI_BACKGROUND_DEFAULT, -} - -func maskForeground(flag WORD) WORD { - return flag & FOREGROUND_MASK_UNSET -} - -func onlyForeground(flag WORD) WORD { - return flag & FOREGROUND_MASK_SET -} - -func maskBackground(flag WORD) WORD { - return flag & BACKGROUND_MASK_UNSET -} - -func onlyBackground(flag WORD) WORD { - return flag & BACKGROUND_MASK_SET -} - -func helpsTestGetWindowsTextAttributeForAnsiValue(t *testing.T, oldValue WORD /*, expected WORD*/, ansi int16, onlyMask WORD, restMask WORD) WORD { - actual, err := getWindowsTextAttributeForAnsiValue(oldValue, FOREGROUND_MASK_SET, ansi) - assertTrue(t, nil == err, "Should be no error") - // assert that other bits are not affected - if 0 != oldValue { - assertTrue(t, (actual&restMask) == (oldValue&restMask), "The operation should not have affected other bits actual=%X oldValue=%X ansi=%d", actual, oldValue, ansi) - } - return actual -} - -func TestBackgroundForAnsiValue(t *testing.T) { - // Check that nothing else changes - // background changes - for _, state1 := range allBackground { - for _, state2 := range allBackground { - flag := WORD(0) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - } - } - // cummulative bcakground changes - for _, state1 := range allBackground { - flag := WORD(0) - for _, state2 := range allBackground { - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - } - } - // change background after foreground - for _, state1 := range allForeground { - for _, state2 := range allBackground { - flag := WORD(0) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - } - } - // change background after change cumulative - for _, state1 := range allForeground { - flag := WORD(0) - for _, state2 := range allBackground { - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - } - } -} - -func TestForegroundForAnsiValue(t *testing.T) { - // Check that nothing else changes - for _, state1 := range allForeground { - for _, state2 := range allForeground { - flag := WORD(0) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - } - } - - for _, state1 := range allForeground { - flag := WORD(0) - for _, state2 := range allForeground { - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - } - } - for _, state1 := range allBackground { - for _, state2 := range allForeground { - flag := WORD(0) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - } - } - for _, state1 := range allBackground { - flag := WORD(0) - for _, state2 := range allForeground { - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET) - flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET) - } - } -} diff --git a/term/winconsole/term_emulator.go b/term/winconsole/term_emulator.go deleted file mode 100644 index 2d5edc0..0000000 --- a/term/winconsole/term_emulator.go +++ /dev/null @@ -1,234 +0,0 @@ -package winconsole - -import ( - "fmt" - "io" - "strconv" - "strings" -) - -// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html -const ( - ANSI_ESCAPE_PRIMARY = 0x1B - ANSI_ESCAPE_SECONDARY = 0x5B - ANSI_COMMAND_FIRST = 0x40 - ANSI_COMMAND_LAST = 0x7E - ANSI_PARAMETER_SEP = ";" - ANSI_CMD_G0 = '(' - ANSI_CMD_G1 = ')' - ANSI_CMD_G2 = '*' - ANSI_CMD_G3 = '+' - ANSI_CMD_DECPNM = '>' - ANSI_CMD_DECPAM = '=' - ANSI_CMD_OSC = ']' - ANSI_CMD_STR_TERM = '\\' - ANSI_BEL = 0x07 - KEY_EVENT = 1 -) - -// Interface that implements terminal handling -type terminalEmulator interface { - HandleOutputCommand(fd uintptr, command []byte) (n int, err error) - HandleInputSequence(fd uintptr, command []byte) (n int, err error) - WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) - ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) -} - -type terminalWriter struct { - wrappedWriter io.Writer - emulator terminalEmulator - command []byte - inSequence bool - fd uintptr -} - -type terminalReader struct { - wrappedReader io.ReadCloser - emulator terminalEmulator - command []byte - inSequence bool - fd uintptr -} - -// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html -func isAnsiCommandChar(b byte) bool { - switch { - case ANSI_COMMAND_FIRST <= b && b <= ANSI_COMMAND_LAST && b != ANSI_ESCAPE_SECONDARY: - return true - case b == ANSI_CMD_G1 || b == ANSI_CMD_OSC || b == ANSI_CMD_DECPAM || b == ANSI_CMD_DECPNM: - // non-CSI escape sequence terminator - return true - case b == ANSI_CMD_STR_TERM || b == ANSI_BEL: - // String escape sequence terminator - return true - } - return false -} - -func isCharacterSelectionCmdChar(b byte) bool { - return (b == ANSI_CMD_G0 || b == ANSI_CMD_G1 || b == ANSI_CMD_G2 || b == ANSI_CMD_G3) -} - -func isXtermOscSequence(command []byte, current byte) bool { - return (len(command) >= 2 && command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_CMD_OSC && current != ANSI_BEL) -} - -// Write writes len(p) bytes from p to the underlying data stream. -// http://golang.org/pkg/io/#Writer -func (tw *terminalWriter) Write(p []byte) (n int, err error) { - if len(p) == 0 { - return 0, nil - } - if tw.emulator == nil { - return tw.wrappedWriter.Write(p) - } - // Emulate terminal by extracting commands and executing them - totalWritten := 0 - start := 0 // indicates start of the next chunk - end := len(p) - for current := 0; current < end; current++ { - if tw.inSequence { - // inside escape sequence - tw.command = append(tw.command, p[current]) - if isAnsiCommandChar(p[current]) { - if !isXtermOscSequence(tw.command, p[current]) { - // found the last command character. - // Now we have a complete command. - nchar, err := tw.emulator.HandleOutputCommand(tw.fd, tw.command) - totalWritten += nchar - if err != nil { - return totalWritten, err - } - - // clear the command - // don't include current character again - tw.command = tw.command[:0] - start = current + 1 - tw.inSequence = false - } - } - } else { - if p[current] == ANSI_ESCAPE_PRIMARY { - // entering escape sequnce - tw.inSequence = true - // indicates end of "normal sequence", write whatever you have so far - if len(p[start:current]) > 0 { - nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:current]) - totalWritten += nw - if err != nil { - return totalWritten, err - } - } - // include the current character as part of the next sequence - tw.command = append(tw.command, p[current]) - } - } - } - // note that so far, start of the escape sequence triggers writing out of bytes to console. - // For the part _after_ the end of last escape sequence, it is not written out yet. So write it out - if !tw.inSequence { - // assumption is that we can't be inside sequence and therefore command should be empty - if len(p[start:]) > 0 { - nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:]) - totalWritten += nw - if err != nil { - return totalWritten, err - } - } - } - return totalWritten, nil - -} - -// Read reads up to len(p) bytes into p. -// http://golang.org/pkg/io/#Reader -func (tr *terminalReader) Read(p []byte) (n int, err error) { - //Implementations of Read are discouraged from returning a zero byte count - // with a nil error, except when len(p) == 0. - if len(p) == 0 { - return 0, nil - } - if nil == tr.emulator { - return tr.readFromWrappedReader(p) - } - return tr.emulator.ReadChars(tr.fd, tr.wrappedReader, p) -} - -// Close the underlying stream -func (tr *terminalReader) Close() (err error) { - return tr.wrappedReader.Close() -} - -func (tr *terminalReader) readFromWrappedReader(p []byte) (n int, err error) { - return tr.wrappedReader.Read(p) -} - -type ansiCommand struct { - CommandBytes []byte - Command string - Parameters []string - IsSpecial bool -} - -func parseAnsiCommand(command []byte) *ansiCommand { - if isCharacterSelectionCmdChar(command[1]) { - // Is Character Set Selection commands - return &ansiCommand{ - CommandBytes: command, - Command: string(command), - IsSpecial: true, - } - } - // last char is command character - lastCharIndex := len(command) - 1 - - retValue := &ansiCommand{ - CommandBytes: command, - Command: string(command[lastCharIndex]), - IsSpecial: false, - } - // more than a single escape - if lastCharIndex != 0 { - start := 1 - // skip if double char escape sequence - if command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_ESCAPE_SECONDARY { - start++ - } - // convert this to GetNextParam method - retValue.Parameters = strings.Split(string(command[start:lastCharIndex]), ANSI_PARAMETER_SEP) - } - return retValue -} - -func (c *ansiCommand) getParam(index int) string { - if len(c.Parameters) > index { - return c.Parameters[index] - } - return "" -} - -func (ac *ansiCommand) String() string { - return fmt.Sprintf("0x%v \"%v\" (\"%v\")", - bytesToHex(ac.CommandBytes), - ac.Command, - strings.Join(ac.Parameters, "\",\"")) -} - -func bytesToHex(b []byte) string { - hex := make([]string, len(b)) - for i, ch := range b { - hex[i] = fmt.Sprintf("%X", ch) - } - return strings.Join(hex, "") -} - -func parseInt16OrDefault(s string, defaultValue int16) (n int16, err error) { - if s == "" { - return defaultValue, nil - } - parsedValue, err := strconv.ParseInt(s, 10, 16) - if err != nil { - return defaultValue, err - } - return int16(parsedValue), nil -} diff --git a/term/winconsole/term_emulator_test.go b/term/winconsole/term_emulator_test.go deleted file mode 100644 index 94104ff..0000000 --- a/term/winconsole/term_emulator_test.go +++ /dev/null @@ -1,388 +0,0 @@ -package winconsole - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "testing" -) - -const ( - WRITE_OPERATION = iota - COMMAND_OPERATION = iota -) - -var languages = []string{ - "Български", - "Català", - "Čeština", - "Ελληνικά", - "Español", - "Esperanto", - "Euskara", - "Français", - "Galego", - "한국어", - "ქართული", - "Latviešu", - "Lietuvių", - "Magyar", - "Nederlands", - "日本語", - "Norsk bokmål", - "Norsk nynorsk", - "Polski", - "Português", - "Română", - "Русский", - "Slovenčina", - "Slovenščina", - "Српски", - "српскохрватски", - "Suomi", - "Svenska", - "ไทย", - "Tiếng Việt", - "Türkçe", - "Українська", - "中文", -} - -// Mock terminal handler object -type mockTerminal struct { - OutputCommandSequence []terminalOperation -} - -// Used for recording the callback data -type terminalOperation struct { - Operation int - Data []byte - Str string -} - -func (mt *mockTerminal) record(operation int, data []byte) { - op := terminalOperation{ - Operation: operation, - Data: make([]byte, len(data)), - } - copy(op.Data, data) - op.Str = string(op.Data) - mt.OutputCommandSequence = append(mt.OutputCommandSequence, op) -} - -func (mt *mockTerminal) HandleOutputCommand(fd uintptr, command []byte) (n int, err error) { - mt.record(COMMAND_OPERATION, command) - return len(command), nil -} - -func (mt *mockTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) { - return 0, nil -} - -func (mt *mockTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) { - mt.record(WRITE_OPERATION, p) - return len(p), nil -} - -func (mt *mockTerminal) ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) { - return len(p), nil -} - -func assertTrue(t *testing.T, cond bool, format string, args ...interface{}) { - if !cond { - t.Errorf(format, args...) - } -} - -// reflect.DeepEqual does not provide detailed information as to what excatly failed. -func assertBytesEqual(t *testing.T, expected, actual []byte, format string, args ...interface{}) { - match := true - mismatchIndex := 0 - if len(expected) == len(actual) { - for i := 0; i < len(expected); i++ { - if expected[i] != actual[i] { - match = false - mismatchIndex = i - break - } - } - } else { - match = false - t.Errorf("Lengths don't match Expected=%d Actual=%d", len(expected), len(actual)) - } - if !match { - t.Errorf("Mismatch at index %d ", mismatchIndex) - t.Errorf("\tActual String = %s", string(actual)) - t.Errorf("\tExpected String = %s", string(expected)) - t.Errorf("\tActual = %v", actual) - t.Errorf("\tExpected = %v", expected) - t.Errorf(format, args) - } -} - -// Just to make sure :) -func TestAssertEqualBytes(t *testing.T) { - data := []byte{9, 9, 1, 1, 1, 9, 9} - assertBytesEqual(t, data, data, "Self") - assertBytesEqual(t, data[1:4], data[1:4], "Self") - assertBytesEqual(t, []byte{1, 1}, []byte{1, 1}, "Simple match") - assertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 2, 3}, "content mismatch") - assertBytesEqual(t, []byte{1, 1, 1}, data[2:5], "slice match") -} - -/* -func TestAssertEqualBytesNegative(t *testing.T) { - AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch") - AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch") - AssertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 1, 1}, "content mismatch") -}*/ - -// Checks that the calls received -func assertHandlerOutput(t *testing.T, mock *mockTerminal, plainText string, commands ...string) { - text := make([]byte, 0, 3*len(plainText)) - cmdIndex := 0 - for opIndex := 0; opIndex < len(mock.OutputCommandSequence); opIndex++ { - op := mock.OutputCommandSequence[opIndex] - if op.Operation == WRITE_OPERATION { - t.Logf("\nThe data is[%d] == %s", opIndex, string(op.Data)) - text = append(text[:], op.Data...) - } else { - assertTrue(t, mock.OutputCommandSequence[opIndex].Operation == COMMAND_OPERATION, "Operation should be command : %s", fmt.Sprintf("%+v", mock)) - assertBytesEqual(t, StringToBytes(commands[cmdIndex]), mock.OutputCommandSequence[opIndex].Data, "Command data should match") - cmdIndex++ - } - } - assertBytesEqual(t, StringToBytes(plainText), text, "Command data should match %#v", mock) -} - -func StringToBytes(str string) []byte { - bytes := make([]byte, len(str)) - copy(bytes[:], str) - return bytes -} - -func TestParseAnsiCommand(t *testing.T) { - // Note: if the parameter does not exist then the empty value is returned - - c := parseAnsiCommand(StringToBytes("\x1Bm")) - assertTrue(t, c.Command == "m", "Command should be m") - assertTrue(t, "" == c.getParam(0), "should return empty string") - assertTrue(t, "" == c.getParam(1), "should return empty string") - - // Escape sequence - ESC[ - c = parseAnsiCommand(StringToBytes("\x1B[m")) - assertTrue(t, c.Command == "m", "Command should be m") - assertTrue(t, "" == c.getParam(0), "should return empty string") - assertTrue(t, "" == c.getParam(1), "should return empty string") - - // Escape sequence With empty parameters- ESC[ - c = parseAnsiCommand(StringToBytes("\x1B[;m")) - assertTrue(t, c.Command == "m", "Command should be m") - assertTrue(t, "" == c.getParam(0), "should return empty string") - assertTrue(t, "" == c.getParam(1), "should return empty string") - assertTrue(t, "" == c.getParam(2), "should return empty string") - - // Escape sequence With empty muliple parameters- ESC[ - c = parseAnsiCommand(StringToBytes("\x1B[;;m")) - assertTrue(t, c.Command == "m", "Command should be m") - assertTrue(t, "" == c.getParam(0), "") - assertTrue(t, "" == c.getParam(1), "") - assertTrue(t, "" == c.getParam(2), "") - - // Escape sequence With muliple parameters- ESC[ - c = parseAnsiCommand(StringToBytes("\x1B[1;2;3m")) - assertTrue(t, c.Command == "m", "Command should be m") - assertTrue(t, "1" == c.getParam(0), "") - assertTrue(t, "2" == c.getParam(1), "") - assertTrue(t, "3" == c.getParam(2), "") - - // Escape sequence With muliple parameters- some missing - c = parseAnsiCommand(StringToBytes("\x1B[1;;3;;;6m")) - assertTrue(t, c.Command == "m", "Command should be m") - assertTrue(t, "1" == c.getParam(0), "") - assertTrue(t, "" == c.getParam(1), "") - assertTrue(t, "3" == c.getParam(2), "") - assertTrue(t, "" == c.getParam(3), "") - assertTrue(t, "" == c.getParam(4), "") - assertTrue(t, "6" == c.getParam(5), "") -} - -func newBufferedMockTerm() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser, mock *mockTerminal) { - var input bytes.Buffer - var output bytes.Buffer - var err bytes.Buffer - - mock = &mockTerminal{ - OutputCommandSequence: make([]terminalOperation, 0, 256), - } - - stdOut = &terminalWriter{ - wrappedWriter: &output, - emulator: mock, - command: make([]byte, 0, 256), - } - stdErr = &terminalWriter{ - wrappedWriter: &err, - emulator: mock, - command: make([]byte, 0, 256), - } - stdIn = &terminalReader{ - wrappedReader: ioutil.NopCloser(&input), - emulator: mock, - command: make([]byte, 0, 256), - } - - return -} - -func TestOutputSimple(t *testing.T) { - stdOut, _, _, mock := newBufferedMockTerm() - - stdOut.Write(StringToBytes("Hello world")) - stdOut.Write(StringToBytes("\x1BmHello again")) - - assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match") - - assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock) - assertBytesEqual(t, StringToBytes("\x1Bm"), mock.OutputCommandSequence[1].Data, "Command data should match") - - assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match") -} - -func TestOutputSplitCommand(t *testing.T) { - stdOut, _, _, mock := newBufferedMockTerm() - - stdOut.Write(StringToBytes("Hello world\x1B[1;2;3")) - stdOut.Write(StringToBytes("mHello again")) - - assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match") - - assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock) - assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match") - - assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match") -} - -func TestOutputMultipleCommands(t *testing.T) { - stdOut, _, _, mock := newBufferedMockTerm() - - stdOut.Write(StringToBytes("Hello world")) - stdOut.Write(StringToBytes("\x1B[1;2;3m")) - stdOut.Write(StringToBytes("\x1B[J")) - stdOut.Write(StringToBytes("Hello again")) - - assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match") - - assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock) - assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match") - - assertTrue(t, mock.OutputCommandSequence[2].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock) - assertBytesEqual(t, StringToBytes("\x1B[J"), mock.OutputCommandSequence[2].Data, "Command data should match") - - assertTrue(t, mock.OutputCommandSequence[3].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[3].Data, "Write data should match") -} - -// Splits the given data in two chunks , makes two writes and checks the split data is parsed correctly -// checks output write/command is passed to handler correctly -func helpsTestOutputSplitChunksAtIndex(t *testing.T, i int, data []byte) { - t.Logf("\ni=%d", i) - stdOut, _, _, mock := newBufferedMockTerm() - - t.Logf("\nWriting chunk[0] == %s", string(data[:i])) - t.Logf("\nWriting chunk[1] == %s", string(data[i:])) - stdOut.Write(data[:i]) - stdOut.Write(data[i:]) - - assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match") - - assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, data[i:], mock.OutputCommandSequence[1].Data, "Write data should match") -} - -// Splits the given data in three chunks , makes three writes and checks the split data is parsed correctly -// checks output write/command is passed to handler correctly -func helpsTestOutputSplitThreeChunksAtIndex(t *testing.T, data []byte, i int, j int) { - stdOut, _, _, mock := newBufferedMockTerm() - - t.Logf("\nWriting chunk[0] == %s", string(data[:i])) - t.Logf("\nWriting chunk[1] == %s", string(data[i:j])) - t.Logf("\nWriting chunk[2] == %s", string(data[j:])) - stdOut.Write(data[:i]) - stdOut.Write(data[i:j]) - stdOut.Write(data[j:]) - - assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match") - - assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, data[i:j], mock.OutputCommandSequence[1].Data, "Write data should match") - - assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock) - assertBytesEqual(t, data[j:], mock.OutputCommandSequence[2].Data, "Write data should match") -} - -// Splits the output into two parts and tests all such possible pairs -func helpsTestOutputSplitChunks(t *testing.T, data []byte) { - for i := 1; i < len(data)-1; i++ { - helpsTestOutputSplitChunksAtIndex(t, i, data) - } -} - -// Splits the output in three parts and tests all such possible triples -func helpsTestOutputSplitThreeChunks(t *testing.T, data []byte) { - for i := 1; i < len(data)-2; i++ { - for j := i + 1; j < len(data)-1; j++ { - helpsTestOutputSplitThreeChunksAtIndex(t, data, i, j) - } - } -} - -func helpsTestOutputSplitCommandsAtIndex(t *testing.T, data []byte, i int, plainText string, commands ...string) { - t.Logf("\ni=%d", i) - stdOut, _, _, mock := newBufferedMockTerm() - - stdOut.Write(data[:i]) - stdOut.Write(data[i:]) - assertHandlerOutput(t, mock, plainText, commands...) -} - -func helpsTestOutputSplitCommands(t *testing.T, data []byte, plainText string, commands ...string) { - for i := 1; i < len(data)-1; i++ { - helpsTestOutputSplitCommandsAtIndex(t, data, i, plainText, commands...) - } -} - -func injectCommandAt(data string, i int, command string) string { - retValue := make([]byte, len(data)+len(command)+4) - retValue = append(retValue, data[:i]...) - retValue = append(retValue, data[i:]...) - return string(retValue) -} - -func TestOutputSplitChunks(t *testing.T) { - data := StringToBytes("qwertyuiopasdfghjklzxcvbnm") - helpsTestOutputSplitChunks(t, data) - helpsTestOutputSplitChunks(t, StringToBytes("BBBBB")) - helpsTestOutputSplitThreeChunks(t, StringToBytes("ABCDE")) -} - -func TestOutputSplitChunksIncludingCommands(t *testing.T) { - helpsTestOutputSplitCommands(t, StringToBytes("Hello world.\x1B[mHello again."), "Hello world.Hello again.", "\x1B[m") - helpsTestOutputSplitCommandsAtIndex(t, StringToBytes("Hello world.\x1B[mHello again."), 2, "Hello world.Hello again.", "\x1B[m") -} - -func TestSplitChunkUnicode(t *testing.T) { - for _, l := range languages { - data := StringToBytes(l) - helpsTestOutputSplitChunks(t, data) - helpsTestOutputSplitThreeChunks(t, data) - } -} diff --git a/term/windows/ansi_reader.go b/term/windows/ansi_reader.go new file mode 100644 index 0000000..53becb0 --- /dev/null +++ b/term/windows/ansi_reader.go @@ -0,0 +1,256 @@ +// +build windows + +package windows + +import ( + "bytes" + "errors" + "fmt" + "os" + "strings" + "unsafe" + + . "github.com/Azure/go-ansiterm" + . "github.com/Azure/go-ansiterm/winterm" +) + +// ansiReader wraps a standard input file (e.g., os.Stdin) providing ANSI sequence translation. +type ansiReader struct { + file *os.File + fd uintptr + buffer []byte + cbBuffer int + command []byte + // TODO(azlinux): Remove this and hard-code the string -- it is not going to change + escapeSequence []byte +} + +func newAnsiReader(nFile int) *ansiReader { + file, fd := GetStdFile(nFile) + return &ansiReader{ + file: file, + fd: fd, + command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + escapeSequence: []byte(KEY_ESC_CSI), + buffer: make([]byte, 0), + } +} + +// Close closes the wrapped file. +func (ar *ansiReader) Close() (err error) { + return ar.file.Close() +} + +// Fd returns the file descriptor of the wrapped file. +func (ar *ansiReader) Fd() uintptr { + return ar.fd +} + +// Read reads up to len(p) bytes of translated input events into p. +func (ar *ansiReader) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + // Previously read bytes exist, read as much as we can and return + if len(ar.buffer) > 0 { + logger.Debugf("Reading previously cached bytes") + + originalLength := len(ar.buffer) + copiedLength := copy(p, ar.buffer) + + if copiedLength == originalLength { + ar.buffer = make([]byte, 0, len(p)) + } else { + ar.buffer = ar.buffer[copiedLength:] + } + + logger.Debugf("Read from cache p[%d]: % x", copiedLength, p) + return copiedLength, nil + } + + // Read and translate key events + events, err := readInputEvents(ar.fd, len(p)) + if err != nil { + return 0, err + } else if len(events) == 0 { + logger.Debug("No input events detected") + return 0, nil + } + + keyBytes := translateKeyEvents(events, ar.escapeSequence) + + // Save excess bytes and right-size keyBytes + if len(keyBytes) > len(p) { + logger.Debugf("Received %d keyBytes, only room for %d bytes", len(keyBytes), len(p)) + ar.buffer = keyBytes[len(p):] + keyBytes = keyBytes[:len(p)] + } else if len(keyBytes) == 0 { + logger.Debug("No key bytes returned from the translater") + return 0, nil + } + + copiedLength := copy(p, keyBytes) + if copiedLength != len(keyBytes) { + return 0, errors.New("Unexpected copy length encountered.") + } + + logger.Debugf("Read p[%d]: % x", copiedLength, p) + logger.Debugf("Read keyBytes[%d]: % x", copiedLength, keyBytes) + return copiedLength, nil +} + +// readInputEvents polls until at least one event is available. +func readInputEvents(fd uintptr, maxBytes int) ([]INPUT_RECORD, error) { + // Determine the maximum number of records to retrieve + // -- Cast around the type system to obtain the size of a single INPUT_RECORD. + // unsafe.Sizeof requires an expression vs. a type-reference; the casting + // tricks the type system into believing it has such an expression. + recordSize := int(unsafe.Sizeof(*((*INPUT_RECORD)(unsafe.Pointer(&maxBytes))))) + countRecords := maxBytes / recordSize + if countRecords > MAX_INPUT_EVENTS { + countRecords = MAX_INPUT_EVENTS + } + logger.Debugf("[windows] readInputEvents: Reading %v records (buffer size %v, record size %v)", countRecords, maxBytes, recordSize) + + // Wait for and read input events + events := make([]INPUT_RECORD, countRecords) + nEvents := uint32(0) + eventsExist, err := WaitForSingleObject(fd, WAIT_INFINITE) + if err != nil { + return nil, err + } + + if eventsExist { + err = ReadConsoleInput(fd, events, &nEvents) + if err != nil { + return nil, err + } + } + + // Return a slice restricted to the number of returned records + logger.Debugf("[windows] readInputEvents: Read %v events", nEvents) + return events[:nEvents], nil +} + +// KeyEvent Translation Helpers + +var arrowKeyMapPrefix = map[WORD]string{ + VK_UP: "%s%sA", + VK_DOWN: "%s%sB", + VK_RIGHT: "%s%sC", + VK_LEFT: "%s%sD", +} + +var keyMapPrefix = map[WORD]string{ + VK_UP: "\x1B[%sA", + VK_DOWN: "\x1B[%sB", + VK_RIGHT: "\x1B[%sC", + VK_LEFT: "\x1B[%sD", + VK_HOME: "\x1B[1%s~", // showkey shows ^[[1 + VK_END: "\x1B[4%s~", // showkey shows ^[[4 + VK_INSERT: "\x1B[2%s~", + VK_DELETE: "\x1B[3%s~", + VK_PRIOR: "\x1B[5%s~", + VK_NEXT: "\x1B[6%s~", + VK_F1: "", + VK_F2: "", + VK_F3: "\x1B[13%s~", + VK_F4: "\x1B[14%s~", + VK_F5: "\x1B[15%s~", + VK_F6: "\x1B[17%s~", + VK_F7: "\x1B[18%s~", + VK_F8: "\x1B[19%s~", + VK_F9: "\x1B[20%s~", + VK_F10: "\x1B[21%s~", + VK_F11: "\x1B[23%s~", + VK_F12: "\x1B[24%s~", +} + +// translateKeyEvents converts the input events into the appropriate ANSI string. +func translateKeyEvents(events []INPUT_RECORD, escapeSequence []byte) []byte { + var buffer bytes.Buffer + for _, event := range events { + if event.EventType == KEY_EVENT && event.KeyEvent.KeyDown != 0 { + buffer.WriteString(keyToString(&event.KeyEvent, escapeSequence)) + } + } + + return buffer.Bytes() +} + +// keyToString maps the given input event record to the corresponding string. +func keyToString(keyEvent *KEY_EVENT_RECORD, escapeSequence []byte) string { + if keyEvent.UnicodeChar == 0 { + return formatVirtualKey(keyEvent.VirtualKeyCode, keyEvent.ControlKeyState, escapeSequence) + } + + _, alt, control := getControlKeys(keyEvent.ControlKeyState) + if control { + // TODO(azlinux): Implement following control sequences + // -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) +} + +// formatVirtualKey converts a virtual key (e.g., up arrow) into the appropriate ANSI string. +func formatVirtualKey(key WORD, controlState DWORD, escapeSequence []byte) string { + shift, alt, control := getControlKeys(controlState) + modifier := getControlKeysModifier(shift, alt, control, false) + + if format, ok := arrowKeyMapPrefix[key]; ok { + return fmt.Sprintf(format, escapeSequence, modifier) + } + + if format, ok := keyMapPrefix[key]; ok { + return fmt.Sprintf(format, modifier) + } + + return "" +} + +// getControlKeys extracts the shift, alt, and ctrl key states. +func getControlKeys(controlState DWORD) (shift, alt, control bool) { + shift = 0 != (controlState & SHIFT_PRESSED) + alt = 0 != (controlState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) + control = 0 != (controlState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) + return shift, alt, control +} + +// getControlKeysModifier returns the ANSI modifier for the given combination of control keys. +func getControlKeysModifier(shift, alt, control, meta bool) string { + if shift && alt && control { + return KEY_CONTROL_PARAM_8 + } + if alt && control { + return KEY_CONTROL_PARAM_7 + } + if shift && control { + return KEY_CONTROL_PARAM_6 + } + if control { + return KEY_CONTROL_PARAM_5 + } + if shift && alt { + return KEY_CONTROL_PARAM_4 + } + if alt { + return KEY_CONTROL_PARAM_3 + } + if shift { + return KEY_CONTROL_PARAM_2 + } + return "" +} diff --git a/term/windows/ansi_writer.go b/term/windows/ansi_writer.go new file mode 100644 index 0000000..a22d47f --- /dev/null +++ b/term/windows/ansi_writer.go @@ -0,0 +1,76 @@ +// +build windows + +package windows + +import ( + "io/ioutil" + "os" + + . "github.com/Azure/go-ansiterm" + . "github.com/Azure/go-ansiterm/winterm" + "github.com/Sirupsen/logrus" +) + +var logger *logrus.Logger + +// ansiWriter wraps a standard output file (e.g., os.Stdout) providing ANSI sequence translation. +type ansiWriter struct { + file *os.File + fd uintptr + infoReset *CONSOLE_SCREEN_BUFFER_INFO + command []byte + escapeSequence []byte + inAnsiSequence bool + parser *AnsiParser +} + +func newAnsiWriter(nFile int) *ansiWriter { + logFile := ioutil.Discard + + if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" { + logFile, _ = os.Create("ansiReaderWriter.log") + } + + logger = &logrus.Logger{ + Out: logFile, + Formatter: new(logrus.TextFormatter), + Level: logrus.DebugLevel, + } + + file, fd := GetStdFile(nFile) + info, err := GetConsoleScreenBufferInfo(fd) + if err != nil { + return nil + } + + parser := CreateParser("Ground", CreateWinEventHandler(fd, file)) + logger.Infof("newAnsiWriter: parser %p", parser) + + aw := &ansiWriter{ + file: file, + fd: fd, + infoReset: info, + command: make([]byte, 0, ANSI_MAX_CMD_LENGTH), + escapeSequence: []byte(KEY_ESC_CSI), + parser: parser, + } + + logger.Infof("newAnsiWriter: aw.parser %p", aw.parser) + logger.Infof("newAnsiWriter: %v", aw) + return aw +} + +func (aw *ansiWriter) Fd() uintptr { + return aw.fd +} + +// Write writes len(p) bytes from p to the underlying data stream. +func (aw *ansiWriter) Write(p []byte) (total int, err error) { + if len(p) == 0 { + return 0, nil + } + + logger.Infof("Write: % x", p) + logger.Infof("Write: %s", string(p)) + return aw.parser.Parse(p) +} diff --git a/term/windows/console.go b/term/windows/console.go new file mode 100644 index 0000000..ecd1c59 --- /dev/null +++ b/term/windows/console.go @@ -0,0 +1,61 @@ +// +build windows + +package windows + +import ( + "io" + "os" + "syscall" + + . "github.com/Azure/go-ansiterm/winterm" +) + +// ConsoleStreams, for each standard stream referencing a console, returns a wrapped version +// that handles ANSI character sequences. +func ConsoleStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { + if IsConsole(os.Stdin.Fd()) { + stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE) + } else { + stdIn = os.Stdin + } + + if IsConsole(os.Stdout.Fd()) { + stdOut = newAnsiWriter(syscall.STD_OUTPUT_HANDLE) + } else { + stdOut = os.Stdout + } + + if IsConsole(os.Stderr.Fd()) { + stdErr = newAnsiWriter(syscall.STD_ERROR_HANDLE) + } else { + stdErr = os.Stderr + } + + return stdIn, stdOut, stdErr +} + +// GetHandleInfo returns file descriptor and bool indicating whether the file is a console. +func GetHandleInfo(in interface{}) (uintptr, bool) { + switch t := in.(type) { + case *ansiReader: + return t.Fd(), true + case *ansiWriter: + return t.Fd(), true + } + + var inFd uintptr + var isTerminal bool + + if file, ok := in.(*os.File); ok { + inFd = file.Fd() + isTerminal = IsConsole(inFd) + } + return inFd, isTerminal +} + +// IsConsole returns true if the given file descriptor is a Windows Console. +// The code assumes that GetConsoleMode will return an error for file descriptors that are not a console. +func IsConsole(fd uintptr) bool { + _, e := GetConsoleMode(fd) + return e == nil +} diff --git a/term/windows/windows.go b/term/windows/windows.go new file mode 100644 index 0000000..bf4c7b5 --- /dev/null +++ b/term/windows/windows.go @@ -0,0 +1,5 @@ +// These files implement ANSI-aware input and output streams for use by the Docker Windows client. +// When asked for the set of standard streams (e.g., stdin, stdout, stderr), the code will create +// and return pseudo-streams that convert ANSI sequences to / from Windows Console API calls. + +package windows diff --git a/term/windows/windows_test.go b/term/windows/windows_test.go new file mode 100644 index 0000000..52aeab5 --- /dev/null +++ b/term/windows/windows_test.go @@ -0,0 +1,3 @@ +// This file is necessary to pass the Docker tests. + +package windows