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 <jhoward@microsoft.com>
This commit is contained in:
John Howard 2015-04-16 14:01:37 -07:00
parent e6e9fbe4f7
commit 9a30d8cf9f
10 changed files with 499 additions and 1939 deletions

View file

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