Skip to content

Commit

Permalink
Merge pull request #14838 from Microsoft/10662-ansirewrite
Browse files Browse the repository at this point in the history
Windows: CLI Improvement (TP3)
  • Loading branch information
Jessie Frazelle committed Jul 28, 2015
2 parents f32cab8 + c923774 commit 33358f8
Show file tree
Hide file tree
Showing 38 changed files with 3,069 additions and 1,939 deletions.
1 change: 1 addition & 0 deletions hack/vendor.sh
Expand Up @@ -6,6 +6,7 @@ rm -rf vendor/
source 'hack/.vendor-helpers.sh'

# the following lines are in sorted order, FYI
clone git github.com/Azure/go-ansiterm 0a9ca7117fc3e5629da85238ede560cb5e749783
clone git github.com/Sirupsen/logrus v0.8.2 # logrus is a common dependency among multiple deps
clone git github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a
clone git github.com/go-check/check 64131543e7896d5bcc6bd5a76287eb75ea96c673
Expand Down
130 changes: 98 additions & 32 deletions pkg/term/term_windows.go
Expand Up @@ -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.
Expand All @@ -31,67 +34,118 @@ 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 the file descriptor for an os.File and indicates whether the file represents a terminal.
func GetFdInfo(in interface{}) (uintptr, bool) {
return winconsole.GetHandleInfo(in)
return windows.GetHandleInfo(in)
}

// GetWinsize returns the window size based on the specified 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 tries to set the specified window size for the specified 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
}
return &State{mode}, nil
}

// 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
Expand All @@ -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 {
Expand All @@ -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)
}()
}

0 comments on commit 33358f8

Please sign in to comment.