ANSI terminal emulation for windows
It is implemented by intercepting and interpreting the output escape sequence by calling win32 console apis. In addition the input from win32 console is translated to linux keycodes Signed-off-by: Sachin Joshi <sachin_jayant_joshi@hotmail.com>
This commit is contained in:
parent
8ae20d8eba
commit
d8c3090dd9
8 changed files with 1926 additions and 44 deletions
|
@ -137,19 +137,12 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, a
|
|||
if tlsConfig != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
if in != nil {
|
||||
if file, ok := in.(*os.File); ok {
|
||||
inFd = file.Fd()
|
||||
isTerminalIn = term.IsTerminal(inFd)
|
||||
}
|
||||
inFd, isTerminalIn = term.GetHandleInfo(in)
|
||||
}
|
||||
|
||||
if out != nil {
|
||||
if file, ok := out.(*os.File); ok {
|
||||
outFd = file.Fd()
|
||||
isTerminalOut = term.IsTerminal(outFd)
|
||||
}
|
||||
outFd, isTerminalOut = term.GetHandleInfo(out)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/docker/docker/autogen/dockerversion"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/reexec"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
|
@ -47,6 +48,10 @@ func main() {
|
|||
initLogging(log.InfoLevel)
|
||||
}
|
||||
|
||||
// Set terminal emulation based on platform as required.
|
||||
stdout, stderr, stdin := term.StdStreams()
|
||||
log.SetOutput(stderr)
|
||||
|
||||
// -D, --debug, -l/--log-level=debug processing
|
||||
// When/if -D is removed this block can be deleted
|
||||
if *flDebug {
|
||||
|
@ -124,9 +129,9 @@ func main() {
|
|||
}
|
||||
|
||||
if *flTls || *flTlsVerify {
|
||||
cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
|
||||
cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
|
||||
} else {
|
||||
cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], nil)
|
||||
cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], nil)
|
||||
}
|
||||
|
||||
if err := cli.Cmd(flag.Args()...); err != nil {
|
||||
|
|
File diff suppressed because it is too large
Load diff
232
pkg/term/console_windows_test.go
Normal file
232
pkg/term/console_windows_test.go
Normal file
|
@ -0,0 +1,232 @@
|
|||
// +build windows
|
||||
|
||||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func helpsTestParseInt16OrDefault(t *testing.T, expectedValue int16, shouldFail bool, input string, defaultValue int16, format string, args ...string) {
|
||||
value, err := parseInt16OrDefault(input, defaultValue)
|
||||
if nil != err && !shouldFail {
|
||||
t.Errorf("Unexpected error returned %v", err)
|
||||
t.Errorf(format, args)
|
||||
}
|
||||
if nil == err && shouldFail {
|
||||
t.Errorf("Should have failed as expected\n\tReturned value = %d", value)
|
||||
t.Errorf(format, args)
|
||||
}
|
||||
if expectedValue != value {
|
||||
t.Errorf("The value returned does not macth expected\n\tExpected:%v\n\t:Actual%v", expectedValue, value)
|
||||
t.Errorf(format, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInt16OrDefault(t *testing.T) {
|
||||
// empty string
|
||||
helpsTestParseInt16OrDefault(t, 0, false, "", 0, "Empty string returns default")
|
||||
helpsTestParseInt16OrDefault(t, 2, false, "", 2, "Empty string returns default")
|
||||
|
||||
// normal case
|
||||
helpsTestParseInt16OrDefault(t, 0, false, "0", 0, "0 handled correctly")
|
||||
helpsTestParseInt16OrDefault(t, 111, false, "111", 2, "Normal")
|
||||
helpsTestParseInt16OrDefault(t, 111, false, "+111", 2, "+N")
|
||||
helpsTestParseInt16OrDefault(t, -111, false, "-111", 2, "-N")
|
||||
helpsTestParseInt16OrDefault(t, 0, false, "+0", 11, "+0")
|
||||
helpsTestParseInt16OrDefault(t, 0, false, "-0", 12, "-0")
|
||||
|
||||
// ill formed strings
|
||||
helpsTestParseInt16OrDefault(t, 0, true, "abc", 0, "Invalid string")
|
||||
helpsTestParseInt16OrDefault(t, 42, true, "+= 23", 42, "Invalid string")
|
||||
helpsTestParseInt16OrDefault(t, 42, true, "123.45", 42, "float like")
|
||||
|
||||
}
|
||||
|
||||
func helpsTestGetNumberOfChars(t *testing.T, expected uint32, fromCoord COORD, toCoord COORD, screenSize COORD, format string, args ...interface{}) {
|
||||
actual := getNumberOfChars(fromCoord, toCoord, screenSize)
|
||||
mesg := fmt.Sprintf(format, args)
|
||||
assertTrue(t, expected == actual, fmt.Sprintf("%s Expected=%d, Actual=%d, Parameters = { fromCoord=%+v, toCoord=%+v, screenSize=%+v", mesg, expected, actual, fromCoord, toCoord, screenSize))
|
||||
}
|
||||
|
||||
func TestGetNumberOfChars(t *testing.T) {
|
||||
// Note: The columns and lines are 0 based
|
||||
// Also that interval is "inclusive" means will have both start and end chars
|
||||
// This test only tests the number opf characters being written
|
||||
|
||||
// all four corners
|
||||
maxWindow := COORD{X: 80, Y: 50}
|
||||
leftTop := COORD{X: 0, Y: 0}
|
||||
rightTop := COORD{X: 79, Y: 0}
|
||||
leftBottom := COORD{X: 0, Y: 49}
|
||||
rightBottom := COORD{X: 79, Y: 49}
|
||||
|
||||
// same position
|
||||
helpsTestGetNumberOfChars(t, 1, COORD{X: 1, Y: 14}, COORD{X: 1, Y: 14}, COORD{X: 80, Y: 50}, "Same position random line")
|
||||
|
||||
// four corners
|
||||
helpsTestGetNumberOfChars(t, 1, leftTop, leftTop, maxWindow, "Same position- leftTop")
|
||||
helpsTestGetNumberOfChars(t, 1, rightTop, rightTop, maxWindow, "Same position- rightTop")
|
||||
helpsTestGetNumberOfChars(t, 1, leftBottom, leftBottom, maxWindow, "Same position- leftBottom")
|
||||
helpsTestGetNumberOfChars(t, 1, rightBottom, rightBottom, maxWindow, "Same position- rightBottom")
|
||||
|
||||
// from this char to next char on same line
|
||||
helpsTestGetNumberOfChars(t, 2, COORD{X: 0, Y: 0}, COORD{X: 1, Y: 0}, maxWindow, "Next position on same line")
|
||||
helpsTestGetNumberOfChars(t, 2, COORD{X: 1, Y: 14}, COORD{X: 2, Y: 14}, maxWindow, "Next position on same line")
|
||||
|
||||
// from this char to next 10 chars on same line
|
||||
helpsTestGetNumberOfChars(t, 11, COORD{X: 0, Y: 0}, COORD{X: 10, Y: 0}, maxWindow, "Next position on same line")
|
||||
helpsTestGetNumberOfChars(t, 11, COORD{X: 1, Y: 14}, COORD{X: 11, Y: 14}, maxWindow, "Next position on same line")
|
||||
|
||||
helpsTestGetNumberOfChars(t, 5, COORD{X: 3, Y: 11}, COORD{X: 7, Y: 11}, maxWindow, "To and from on same line")
|
||||
|
||||
helpsTestGetNumberOfChars(t, 8, COORD{X: 0, Y: 34}, COORD{X: 7, Y: 34}, maxWindow, "Start of line to middle")
|
||||
helpsTestGetNumberOfChars(t, 4, COORD{X: 76, Y: 34}, COORD{X: 79, Y: 34}, maxWindow, "Middle to end of line")
|
||||
|
||||
// multiple lines - 1
|
||||
helpsTestGetNumberOfChars(t, 81, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 1}, maxWindow, "one line below same X")
|
||||
helpsTestGetNumberOfChars(t, 81, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 11}, maxWindow, "one line below same X")
|
||||
|
||||
// multiple lines - 2
|
||||
helpsTestGetNumberOfChars(t, 161, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 2}, maxWindow, "one line below same X")
|
||||
helpsTestGetNumberOfChars(t, 161, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 12}, maxWindow, "one line below same X")
|
||||
|
||||
// multiple lines - 3
|
||||
helpsTestGetNumberOfChars(t, 241, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 3}, maxWindow, "one line below same X")
|
||||
helpsTestGetNumberOfChars(t, 241, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 13}, maxWindow, "one line below same X")
|
||||
|
||||
// full line
|
||||
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 0}, COORD{X: 79, Y: 0}, maxWindow, "Full line - first")
|
||||
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 23}, COORD{X: 79, Y: 23}, maxWindow, "Full line - random")
|
||||
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 49}, COORD{X: 79, Y: 49}, maxWindow, "Full line - last")
|
||||
|
||||
// full screen
|
||||
helpsTestGetNumberOfChars(t, 80*50, leftTop, rightBottom, maxWindow, "full screen")
|
||||
|
||||
helpsTestGetNumberOfChars(t, 80*50-1, COORD{X: 1, Y: 0}, rightBottom, maxWindow, "dropping first char to, end of screen")
|
||||
helpsTestGetNumberOfChars(t, 80*50-2, COORD{X: 2, Y: 0}, rightBottom, maxWindow, "dropping first two char to, end of screen")
|
||||
|
||||
helpsTestGetNumberOfChars(t, 80*50-1, leftTop, COORD{X: 78, Y: 49}, maxWindow, "from start of screen, till last char-1")
|
||||
helpsTestGetNumberOfChars(t, 80*50-2, leftTop, COORD{X: 77, Y: 49}, maxWindow, "from start of screen, till last char-2")
|
||||
|
||||
helpsTestGetNumberOfChars(t, 80*50-5, COORD{X: 4, Y: 0}, COORD{X: 78, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-1")
|
||||
helpsTestGetNumberOfChars(t, 80*50-6, COORD{X: 4, Y: 0}, COORD{X: 77, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-2")
|
||||
}
|
||||
|
||||
var allForeground = []int16{
|
||||
ANSI_FOREGROUND_BLACK,
|
||||
ANSI_FOREGROUND_RED,
|
||||
ANSI_FOREGROUND_GREEN,
|
||||
ANSI_FOREGROUND_YELLOW,
|
||||
ANSI_FOREGROUND_BLUE,
|
||||
ANSI_FOREGROUND_MAGENTA,
|
||||
ANSI_FOREGROUND_CYAN,
|
||||
ANSI_FOREGROUND_WHITE,
|
||||
ANSI_FOREGROUND_DEFAULT,
|
||||
}
|
||||
var allBackground = []int16{
|
||||
ANSI_BACKGROUND_BLACK,
|
||||
ANSI_BACKGROUND_RED,
|
||||
ANSI_BACKGROUND_GREEN,
|
||||
ANSI_BACKGROUND_YELLOW,
|
||||
ANSI_BACKGROUND_BLUE,
|
||||
ANSI_BACKGROUND_MAGENTA,
|
||||
ANSI_BACKGROUND_CYAN,
|
||||
ANSI_BACKGROUND_WHITE,
|
||||
ANSI_BACKGROUND_DEFAULT,
|
||||
}
|
||||
|
||||
func maskForeground(flag WORD) WORD {
|
||||
return flag & FOREGROUND_MASK_UNSET
|
||||
}
|
||||
|
||||
func onlyForeground(flag WORD) WORD {
|
||||
return flag & FOREGROUND_MASK_SET
|
||||
}
|
||||
|
||||
func maskBackground(flag WORD) WORD {
|
||||
return flag & BACKGROUND_MASK_UNSET
|
||||
}
|
||||
|
||||
func onlyBackground(flag WORD) WORD {
|
||||
return flag & BACKGROUND_MASK_SET
|
||||
}
|
||||
|
||||
func helpsTestGetWindowsTextAttributeForAnsiValue(t *testing.T, oldValue WORD /*, expected WORD*/, ansi int16, onlyMask WORD, restMask WORD) WORD {
|
||||
actual, err := getWindowsTextAttributeForAnsiValue(oldValue, FOREGROUND_MASK_SET, ansi)
|
||||
assertTrue(t, nil == err, "Should be no error")
|
||||
// assert that other bits are not affected
|
||||
if 0 != oldValue {
|
||||
assertTrue(t, (actual&restMask) == (oldValue&restMask), "The operation should not have affected other bits actual=%X oldValue=%X ansi=%d", actual, oldValue, ansi)
|
||||
}
|
||||
return actual
|
||||
}
|
||||
|
||||
func TestBackgroundForAnsiValue(t *testing.T) {
|
||||
// Check that nothing else changes
|
||||
// background changes
|
||||
for _, state1 := range allBackground {
|
||||
for _, state2 := range allBackground {
|
||||
flag := WORD(0)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
// cummulative bcakground changes
|
||||
for _, state1 := range allBackground {
|
||||
flag := WORD(0)
|
||||
for _, state2 := range allBackground {
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
// change background after foreground
|
||||
for _, state1 := range allForeground {
|
||||
for _, state2 := range allBackground {
|
||||
flag := WORD(0)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
// change background after change cumulative
|
||||
for _, state1 := range allForeground {
|
||||
flag := WORD(0)
|
||||
for _, state2 := range allBackground {
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForegroundForAnsiValue(t *testing.T) {
|
||||
// Check that nothing else changes
|
||||
for _, state1 := range allForeground {
|
||||
for _, state2 := range allForeground {
|
||||
flag := WORD(0)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
|
||||
for _, state1 := range allForeground {
|
||||
flag := WORD(0)
|
||||
for _, state2 := range allForeground {
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
for _, state1 := range allBackground {
|
||||
for _, state2 := range allForeground {
|
||||
flag := WORD(0)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
for _, state1 := range allBackground {
|
||||
flag := WORD(0)
|
||||
for _, state2 := range allForeground {
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ package term
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
@ -25,6 +26,20 @@ type Winsize struct {
|
|||
y uint16
|
||||
}
|
||||
|
||||
func StdStreams() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser) {
|
||||
return os.Stdout, os.Stderr, os.Stdin
|
||||
}
|
||||
|
||||
func GetHandleInfo(in interface{}) (uintptr, bool) {
|
||||
var inFd uintptr
|
||||
var isTerminalIn bool
|
||||
if file, ok := in.(*os.File); ok {
|
||||
inFd = file.Fd()
|
||||
isTerminalIn = IsTerminal(inFd)
|
||||
}
|
||||
return inFd, isTerminalIn
|
||||
}
|
||||
|
||||
func GetWinsize(fd uintptr) (*Winsize, error) {
|
||||
ws := &Winsize{}
|
||||
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws)))
|
||||
|
|
216
pkg/term/term_emulator.go
Normal file
216
pkg/term/term_emulator.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package term
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||
const (
|
||||
ANSI_ESCAPE_PRIMARY = 0x1B
|
||||
ANSI_ESCAPE_SECONDARY = 0x5B
|
||||
ANSI_COMMAND_FIRST = 0x40
|
||||
ANSI_COMMAND_LAST = 0x7E
|
||||
ANSI_PARAMETER_SEP = ";"
|
||||
ANSI_CMD_G0 = '('
|
||||
ANSI_CMD_G1 = ')'
|
||||
ANSI_CMD_G2 = '*'
|
||||
ANSI_CMD_G3 = '+'
|
||||
ANSI_CMD_DECPNM = '>'
|
||||
ANSI_CMD_DECPAM = '='
|
||||
ANSI_CMD_OSC = ']'
|
||||
ANSI_CMD_STR_TERM = '\\'
|
||||
ANSI_BEL = 0x07
|
||||
KEY_EVENT = 1
|
||||
)
|
||||
|
||||
// Interface that implements terminal handling
|
||||
type terminalEmulator interface {
|
||||
HandleOutputCommand(command []byte) (n int, err error)
|
||||
HandleInputSequence(command []byte) (n int, err error)
|
||||
WriteChars(w io.Writer, p []byte) (n int, err error)
|
||||
ReadChars(w io.Reader, p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
type terminalWriter struct {
|
||||
wrappedWriter io.Writer
|
||||
emulator terminalEmulator
|
||||
command []byte
|
||||
inSequence bool
|
||||
}
|
||||
|
||||
type terminalReader struct {
|
||||
wrappedReader io.ReadCloser
|
||||
emulator terminalEmulator
|
||||
command []byte
|
||||
inSequence bool
|
||||
}
|
||||
|
||||
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||
func isAnsiCommandChar(b byte) bool {
|
||||
switch {
|
||||
case ANSI_COMMAND_FIRST <= b && b <= ANSI_COMMAND_LAST && b != ANSI_ESCAPE_SECONDARY:
|
||||
return true
|
||||
case b == ANSI_CMD_G1 || b == ANSI_CMD_OSC || b == ANSI_CMD_DECPAM || b == ANSI_CMD_DECPNM:
|
||||
// non-CSI escape sequence terminator
|
||||
return true
|
||||
case b == ANSI_CMD_STR_TERM || b == ANSI_BEL:
|
||||
// String escape sequence terminator
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isCharacterSelectionCmdChar(b byte) bool {
|
||||
return (b == ANSI_CMD_G0 || b == ANSI_CMD_G1 || b == ANSI_CMD_G2 || b == ANSI_CMD_G3)
|
||||
}
|
||||
|
||||
func isXtermOscSequence(command []byte, current byte) bool {
|
||||
return (len(command) >= 2 && command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_CMD_OSC && current != ANSI_BEL)
|
||||
}
|
||||
|
||||
// Write writes len(p) bytes from p to the underlying data stream.
|
||||
// http://golang.org/pkg/io/#Writer
|
||||
func (tw *terminalWriter) Write(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if tw.emulator == nil {
|
||||
return tw.wrappedWriter.Write(p)
|
||||
}
|
||||
// Emulate terminal by extracting commands and executing them
|
||||
totalWritten := 0
|
||||
start := 0 // indicates start of the next chunk
|
||||
end := len(p)
|
||||
for current := 0; current < end; current++ {
|
||||
if tw.inSequence {
|
||||
// inside escape sequence
|
||||
tw.command = append(tw.command, p[current])
|
||||
if isAnsiCommandChar(p[current]) {
|
||||
if !isXtermOscSequence(tw.command, p[current]) {
|
||||
// found the last command character.
|
||||
// Now we have a complete command.
|
||||
nchar, err := tw.emulator.HandleOutputCommand(tw.command)
|
||||
totalWritten += nchar
|
||||
if err != nil {
|
||||
return totalWritten, err
|
||||
}
|
||||
|
||||
// clear the command
|
||||
// don't include current character again
|
||||
tw.command = tw.command[:0]
|
||||
start = current + 1
|
||||
tw.inSequence = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if p[current] == ANSI_ESCAPE_PRIMARY {
|
||||
// entering escape sequnce
|
||||
tw.inSequence = true
|
||||
// indicates end of "normal sequence", write whatever you have so far
|
||||
if len(p[start:current]) > 0 {
|
||||
nw, err := tw.emulator.WriteChars(tw.wrappedWriter, p[start:current])
|
||||
totalWritten += nw
|
||||
if err != nil {
|
||||
return totalWritten, err
|
||||
}
|
||||
}
|
||||
// include the current character as part of the next sequence
|
||||
tw.command = append(tw.command, p[current])
|
||||
}
|
||||
}
|
||||
}
|
||||
// note that so far, start of the escape sequence triggers writing out of bytes to console.
|
||||
// For the part _after_ the end of last escape sequence, it is not written out yet. So write it out
|
||||
if !tw.inSequence {
|
||||
// assumption is that we can't be inside sequence and therefore command should be empty
|
||||
if len(p[start:]) > 0 {
|
||||
nw, err := tw.emulator.WriteChars(tw.wrappedWriter, p[start:])
|
||||
totalWritten += nw
|
||||
if err != nil {
|
||||
return totalWritten, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalWritten, nil
|
||||
|
||||
}
|
||||
|
||||
// Read reads up to len(p) bytes into p.
|
||||
// http://golang.org/pkg/io/#Reader
|
||||
func (tr *terminalReader) Read(p []byte) (n int, err error) {
|
||||
//Implementations of Read are discouraged from returning a zero byte count
|
||||
// with a nil error, except when len(p) == 0.
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if nil == tr.emulator {
|
||||
return tr.readFromWrappedReader(p)
|
||||
}
|
||||
return tr.emulator.ReadChars(tr.wrappedReader, p)
|
||||
}
|
||||
|
||||
// Close the underlying stream
|
||||
func (tr *terminalReader) Close() (err error) {
|
||||
return tr.wrappedReader.Close()
|
||||
}
|
||||
|
||||
func (tr *terminalReader) readFromWrappedReader(p []byte) (n int, err error) {
|
||||
return tr.wrappedReader.Read(p)
|
||||
}
|
||||
|
||||
type ansiCommand struct {
|
||||
CommandBytes []byte
|
||||
Command string
|
||||
Parameters []string
|
||||
IsSpecial bool
|
||||
}
|
||||
|
||||
func parseAnsiCommand(command []byte) *ansiCommand {
|
||||
if isCharacterSelectionCmdChar(command[1]) {
|
||||
// Is Character Set Selection commands
|
||||
return &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command),
|
||||
IsSpecial: true,
|
||||
}
|
||||
}
|
||||
// last char is command character
|
||||
lastCharIndex := len(command) - 1
|
||||
|
||||
retValue := &ansiCommand{
|
||||
CommandBytes: command,
|
||||
Command: string(command[lastCharIndex]),
|
||||
IsSpecial: false,
|
||||
}
|
||||
// more than a single escape
|
||||
if lastCharIndex != 0 {
|
||||
start := 1
|
||||
// skip if double char escape sequence
|
||||
if command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_ESCAPE_SECONDARY {
|
||||
start++
|
||||
}
|
||||
// convert this to GetNextParam method
|
||||
retValue.Parameters = strings.Split(string(command[start:lastCharIndex]), ANSI_PARAMETER_SEP)
|
||||
}
|
||||
return retValue
|
||||
}
|
||||
|
||||
func (c *ansiCommand) getParam(index int) string {
|
||||
if len(c.Parameters) > index {
|
||||
return c.Parameters[index]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseInt16OrDefault(s string, defaultValue int16) (n int16, err error) {
|
||||
if s == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
parsedValue, err := strconv.ParseInt(s, 10, 16)
|
||||
if nil != err {
|
||||
return defaultValue, err
|
||||
}
|
||||
return int16(parsedValue), nil
|
||||
}
|
388
pkg/term/term_emulator_test.go
Normal file
388
pkg/term/term_emulator_test.go
Normal file
|
@ -0,0 +1,388 @@
|
|||
package term
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
WRITE_OPERATION = iota
|
||||
COMMAND_OPERATION = iota
|
||||
)
|
||||
|
||||
var languages = []string{
|
||||
"Български",
|
||||
"Català",
|
||||
"Čeština",
|
||||
"Ελληνικά",
|
||||
"Español",
|
||||
"Esperanto",
|
||||
"Euskara",
|
||||
"Français",
|
||||
"Galego",
|
||||
"한국어",
|
||||
"ქართული",
|
||||
"Latviešu",
|
||||
"Lietuvių",
|
||||
"Magyar",
|
||||
"Nederlands",
|
||||
"日本語",
|
||||
"Norsk bokmål",
|
||||
"Norsk nynorsk",
|
||||
"Polski",
|
||||
"Português",
|
||||
"Română",
|
||||
"Русский",
|
||||
"Slovenčina",
|
||||
"Slovenščina",
|
||||
"Српски",
|
||||
"српскохрватски",
|
||||
"Suomi",
|
||||
"Svenska",
|
||||
"ไทย",
|
||||
"Tiếng Việt",
|
||||
"Türkçe",
|
||||
"Українська",
|
||||
"中文",
|
||||
}
|
||||
|
||||
// Mock terminal handler object
|
||||
type mockTerminal struct {
|
||||
OutputCommandSequence []terminalOperation
|
||||
}
|
||||
|
||||
// Used for recording the callback data
|
||||
type terminalOperation struct {
|
||||
Operation int
|
||||
Data []byte
|
||||
Str string
|
||||
}
|
||||
|
||||
func (mt *mockTerminal) record(operation int, data []byte) {
|
||||
op := terminalOperation{
|
||||
Operation: operation,
|
||||
Data: make([]byte, len(data)),
|
||||
}
|
||||
copy(op.Data, data)
|
||||
op.Str = string(op.Data)
|
||||
mt.OutputCommandSequence = append(mt.OutputCommandSequence, op)
|
||||
}
|
||||
|
||||
func (mt *mockTerminal) HandleOutputCommand(command []byte) (n int, err error) {
|
||||
mt.record(COMMAND_OPERATION, command)
|
||||
return len(command), nil
|
||||
}
|
||||
|
||||
func (mt *mockTerminal) HandleInputSequence(command []byte) (n int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (mt *mockTerminal) WriteChars(w io.Writer, p []byte) (n int, err error) {
|
||||
mt.record(WRITE_OPERATION, p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (mt *mockTerminal) ReadChars(w io.Reader, p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func assertTrue(t *testing.T, cond bool, format string, args ...interface{}) {
|
||||
if !cond {
|
||||
t.Errorf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// reflect.DeepEqual does not provide detailed information as to what excatly failed.
|
||||
func assertBytesEqual(t *testing.T, expected, actual []byte, format string, args ...interface{}) {
|
||||
match := true
|
||||
mismatchIndex := 0
|
||||
if len(expected) == len(actual) {
|
||||
for i := 0; i < len(expected); i++ {
|
||||
if expected[i] != actual[i] {
|
||||
match = false
|
||||
mismatchIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match = false
|
||||
t.Errorf("Lengths don't match Expected=%d Actual=%d", len(expected), len(actual))
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("Mismatch at index %d ", mismatchIndex)
|
||||
t.Errorf("\tActual String = %s", string(actual))
|
||||
t.Errorf("\tExpected String = %s", string(expected))
|
||||
t.Errorf("\tActual = %v", actual)
|
||||
t.Errorf("\tExpected = %v", expected)
|
||||
t.Errorf(format, args)
|
||||
}
|
||||
}
|
||||
|
||||
// Just to make sure :)
|
||||
func TestAssertEqualBytes(t *testing.T) {
|
||||
data := []byte{9, 9, 1, 1, 1, 9, 9}
|
||||
assertBytesEqual(t, data, data, "Self")
|
||||
assertBytesEqual(t, data[1:4], data[1:4], "Self")
|
||||
assertBytesEqual(t, []byte{1, 1}, []byte{1, 1}, "Simple match")
|
||||
assertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 2, 3}, "content mismatch")
|
||||
assertBytesEqual(t, []byte{1, 1, 1}, data[2:5], "slice match")
|
||||
}
|
||||
|
||||
/*
|
||||
func TestAssertEqualBytesNegative(t *testing.T) {
|
||||
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
|
||||
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
|
||||
AssertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 1, 1}, "content mismatch")
|
||||
}*/
|
||||
|
||||
// Checks that the calls recieved
|
||||
func assertHandlerOutput(t *testing.T, mock *mockTerminal, plainText string, commands ...string) {
|
||||
text := make([]byte, 0, 3*len(plainText))
|
||||
cmdIndex := 0
|
||||
for opIndex := 0; opIndex < len(mock.OutputCommandSequence); opIndex++ {
|
||||
op := mock.OutputCommandSequence[opIndex]
|
||||
if op.Operation == WRITE_OPERATION {
|
||||
t.Logf("\nThe data is[%d] == %s", opIndex, string(op.Data))
|
||||
text = append(text[:], op.Data...)
|
||||
} else {
|
||||
assertTrue(t, mock.OutputCommandSequence[opIndex].Operation == COMMAND_OPERATION, "Operation should be command : %s", fmt.Sprintf("%+v", mock))
|
||||
assertBytesEqual(t, StringToBytes(commands[cmdIndex]), mock.OutputCommandSequence[opIndex].Data, "Command data should match")
|
||||
cmdIndex++
|
||||
}
|
||||
}
|
||||
assertBytesEqual(t, StringToBytes(plainText), text, "Command data should match %#v", mock)
|
||||
}
|
||||
|
||||
func StringToBytes(str string) []byte {
|
||||
bytes := make([]byte, len(str))
|
||||
copy(bytes[:], str)
|
||||
return bytes
|
||||
}
|
||||
|
||||
func TestParseAnsiCommand(t *testing.T) {
|
||||
// Note: if the parameter does not exist then the empty value is returned
|
||||
|
||||
c := parseAnsiCommand(StringToBytes("\x1Bm"))
|
||||
assertTrue(t, c.Command == "m", "Command should be m")
|
||||
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
||||
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
||||
|
||||
// Escape sequence - ESC[
|
||||
c = parseAnsiCommand(StringToBytes("\x1B[m"))
|
||||
assertTrue(t, c.Command == "m", "Command should be m")
|
||||
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
||||
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
||||
|
||||
// Escape sequence With empty parameters- ESC[
|
||||
c = parseAnsiCommand(StringToBytes("\x1B[;m"))
|
||||
assertTrue(t, c.Command == "m", "Command should be m")
|
||||
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
||||
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
||||
assertTrue(t, "" == c.getParam(2), "should return empty string")
|
||||
|
||||
// Escape sequence With empty muliple parameters- ESC[
|
||||
c = parseAnsiCommand(StringToBytes("\x1B[;;m"))
|
||||
assertTrue(t, c.Command == "m", "Command should be m")
|
||||
assertTrue(t, "" == c.getParam(0), "")
|
||||
assertTrue(t, "" == c.getParam(1), "")
|
||||
assertTrue(t, "" == c.getParam(2), "")
|
||||
|
||||
// Escape sequence With muliple parameters- ESC[
|
||||
c = parseAnsiCommand(StringToBytes("\x1B[1;2;3m"))
|
||||
assertTrue(t, c.Command == "m", "Command should be m")
|
||||
assertTrue(t, "1" == c.getParam(0), "")
|
||||
assertTrue(t, "2" == c.getParam(1), "")
|
||||
assertTrue(t, "3" == c.getParam(2), "")
|
||||
|
||||
// Escape sequence With muliple parameters- some missing
|
||||
c = parseAnsiCommand(StringToBytes("\x1B[1;;3;;;6m"))
|
||||
assertTrue(t, c.Command == "m", "Command should be m")
|
||||
assertTrue(t, "1" == c.getParam(0), "")
|
||||
assertTrue(t, "" == c.getParam(1), "")
|
||||
assertTrue(t, "3" == c.getParam(2), "")
|
||||
assertTrue(t, "" == c.getParam(3), "")
|
||||
assertTrue(t, "" == c.getParam(4), "")
|
||||
assertTrue(t, "6" == c.getParam(5), "")
|
||||
}
|
||||
|
||||
func newBufferedMockTerm() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser, mock *mockTerminal) {
|
||||
var input bytes.Buffer
|
||||
var output bytes.Buffer
|
||||
var err bytes.Buffer
|
||||
|
||||
mock = &mockTerminal{
|
||||
OutputCommandSequence: make([]terminalOperation, 0, 256),
|
||||
}
|
||||
|
||||
stdOut = &terminalWriter{
|
||||
wrappedWriter: &output,
|
||||
emulator: mock,
|
||||
command: make([]byte, 0, 256),
|
||||
}
|
||||
stdErr = &terminalWriter{
|
||||
wrappedWriter: &err,
|
||||
emulator: mock,
|
||||
command: make([]byte, 0, 256),
|
||||
}
|
||||
stdIn = &terminalReader{
|
||||
wrappedReader: ioutil.NopCloser(&input),
|
||||
emulator: mock,
|
||||
command: make([]byte, 0, 256),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestOutputSimple(t *testing.T) {
|
||||
stdOut, _, _, mock := newBufferedMockTerm()
|
||||
|
||||
stdOut.Write(StringToBytes("Hello world"))
|
||||
stdOut.Write(StringToBytes("\x1BmHello again"))
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||
assertBytesEqual(t, StringToBytes("\x1Bm"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
|
||||
}
|
||||
|
||||
func TestOutputSplitCommand(t *testing.T) {
|
||||
stdOut, _, _, mock := newBufferedMockTerm()
|
||||
|
||||
stdOut.Write(StringToBytes("Hello world\x1B[1;2;3"))
|
||||
stdOut.Write(StringToBytes("mHello again"))
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
|
||||
}
|
||||
|
||||
func TestOutputMultipleCommands(t *testing.T) {
|
||||
stdOut, _, _, mock := newBufferedMockTerm()
|
||||
|
||||
stdOut.Write(StringToBytes("Hello world"))
|
||||
stdOut.Write(StringToBytes("\x1B[1;2;3m"))
|
||||
stdOut.Write(StringToBytes("\x1B[J"))
|
||||
stdOut.Write(StringToBytes("Hello again"))
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||
assertBytesEqual(t, StringToBytes("\x1B[J"), mock.OutputCommandSequence[2].Data, "Command data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[3].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[3].Data, "Write data should match")
|
||||
}
|
||||
|
||||
// Splits the given data in two chunks , makes two writes and checks the split data is parsed correctly
|
||||
// checks output write/command is passed to handler correctly
|
||||
func helpsTestOutputSplitChunksAtIndex(t *testing.T, i int, data []byte) {
|
||||
t.Logf("\ni=%d", i)
|
||||
stdOut, _, _, mock := newBufferedMockTerm()
|
||||
|
||||
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
|
||||
t.Logf("\nWriting chunk[1] == %s", string(data[i:]))
|
||||
stdOut.Write(data[:i])
|
||||
stdOut.Write(data[i:])
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, data[i:], mock.OutputCommandSequence[1].Data, "Write data should match")
|
||||
}
|
||||
|
||||
// Splits the given data in three chunks , makes three writes and checks the split data is parsed correctly
|
||||
// checks output write/command is passed to handler correctly
|
||||
func helpsTestOutputSplitThreeChunksAtIndex(t *testing.T, data []byte, i int, j int) {
|
||||
stdOut, _, _, mock := newBufferedMockTerm()
|
||||
|
||||
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
|
||||
t.Logf("\nWriting chunk[1] == %s", string(data[i:j]))
|
||||
t.Logf("\nWriting chunk[2] == %s", string(data[j:]))
|
||||
stdOut.Write(data[:i])
|
||||
stdOut.Write(data[i:j])
|
||||
stdOut.Write(data[j:])
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, data[i:j], mock.OutputCommandSequence[1].Data, "Write data should match")
|
||||
|
||||
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||
assertBytesEqual(t, data[j:], mock.OutputCommandSequence[2].Data, "Write data should match")
|
||||
}
|
||||
|
||||
// Splits the output into two parts and tests all such possible pairs
|
||||
func helpsTestOutputSplitChunks(t *testing.T, data []byte) {
|
||||
for i := 1; i < len(data)-1; i++ {
|
||||
helpsTestOutputSplitChunksAtIndex(t, i, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Splits the output in three parts and tests all such possible triples
|
||||
func helpsTestOutputSplitThreeChunks(t *testing.T, data []byte) {
|
||||
for i := 1; i < len(data)-2; i++ {
|
||||
for j := i + 1; j < len(data)-1; j++ {
|
||||
helpsTestOutputSplitThreeChunksAtIndex(t, data, i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func helpsTestOutputSplitCommandsAtIndex(t *testing.T, data []byte, i int, plainText string, commands ...string) {
|
||||
t.Logf("\ni=%d", i)
|
||||
stdOut, _, _, mock := newBufferedMockTerm()
|
||||
|
||||
stdOut.Write(data[:i])
|
||||
stdOut.Write(data[i:])
|
||||
assertHandlerOutput(t, mock, plainText, commands...)
|
||||
}
|
||||
|
||||
func helpsTestOutputSplitCommands(t *testing.T, data []byte, plainText string, commands ...string) {
|
||||
for i := 1; i < len(data)-1; i++ {
|
||||
helpsTestOutputSplitCommandsAtIndex(t, data, i, plainText, commands...)
|
||||
}
|
||||
}
|
||||
|
||||
func injectCommandAt(data string, i int, command string) string {
|
||||
retValue := make([]byte, len(data)+len(command)+4)
|
||||
retValue = append(retValue, data[:i]...)
|
||||
retValue = append(retValue, data[i:]...)
|
||||
return string(retValue)
|
||||
}
|
||||
|
||||
func TestOutputSplitChunks(t *testing.T) {
|
||||
data := StringToBytes("qwertyuiopasdfghjklzxcvbnm")
|
||||
helpsTestOutputSplitChunks(t, data)
|
||||
helpsTestOutputSplitChunks(t, StringToBytes("BBBBB"))
|
||||
helpsTestOutputSplitThreeChunks(t, StringToBytes("ABCDE"))
|
||||
}
|
||||
|
||||
func TestOutputSplitChunksIncludingCommands(t *testing.T) {
|
||||
helpsTestOutputSplitCommands(t, StringToBytes("Hello world.\x1B[mHello again."), "Hello world.Hello again.", "\x1B[m")
|
||||
helpsTestOutputSplitCommandsAtIndex(t, StringToBytes("Hello world.\x1B[mHello again."), 2, "Hello world.Hello again.", "\x1B[m")
|
||||
}
|
||||
|
||||
func TestSplitChunkUnicode(t *testing.T) {
|
||||
for _, l := range languages {
|
||||
data := StringToBytes(l)
|
||||
helpsTestOutputSplitChunks(t, data)
|
||||
helpsTestOutputSplitThreeChunks(t, data)
|
||||
}
|
||||
}
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
package term
|
||||
|
||||
// State holds the console mode for the terminal.
|
||||
type State struct {
|
||||
mode uint32
|
||||
}
|
||||
|
||||
// Winsize is used for window size.
|
||||
type Winsize struct {
|
||||
Height uint16
|
||||
Width uint16
|
||||
|
@ -13,6 +15,7 @@ type Winsize struct {
|
|||
y uint16
|
||||
}
|
||||
|
||||
// GetWinsize gets the window size of the given terminal
|
||||
func GetWinsize(fd uintptr) (*Winsize, error) {
|
||||
ws := &Winsize{}
|
||||
var info *CONSOLE_SCREEN_BUFFER_INFO
|
||||
|
@ -20,8 +23,9 @@ func GetWinsize(fd uintptr) (*Winsize, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ws.Height = uint16(info.srWindow.Right - info.srWindow.Left + 1)
|
||||
ws.Width = uint16(info.srWindow.Bottom - info.srWindow.Top + 1)
|
||||
|
||||
ws.Width = uint16(info.Window.Right - info.Window.Left + 1)
|
||||
ws.Height = uint16(info.Window.Bottom - info.Window.Top + 1)
|
||||
|
||||
ws.x = 0 // todo azlinux -- this is the pixel size of the Window, and not currently used by any caller
|
||||
ws.y = 0
|
||||
|
@ -29,6 +33,8 @@ func GetWinsize(fd uintptr) (*Winsize, error) {
|
|||
return ws, nil
|
||||
}
|
||||
|
||||
// SetWinsize sets the terminal connected to the given file descriptor to a
|
||||
// given size.
|
||||
func SetWinsize(fd uintptr, ws *Winsize) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -39,12 +45,13 @@ func IsTerminal(fd uintptr) bool {
|
|||
return e == nil
|
||||
}
|
||||
|
||||
// Restore restores the terminal connected to the given file descriptor to a
|
||||
// RestoreTerminal restores the terminal connected to the given file descriptor to a
|
||||
// previous state.
|
||||
func RestoreTerminal(fd uintptr, state *State) error {
|
||||
return SetConsoleMode(fd, state.mode)
|
||||
}
|
||||
|
||||
// SaveState saves the state of the given console
|
||||
func SaveState(fd uintptr) (*State, error) {
|
||||
mode, e := GetConsoleMode(fd)
|
||||
if e != nil {
|
||||
|
@ -53,6 +60,7 @@ func SaveState(fd uintptr) (*State, error) {
|
|||
return &State{mode}, nil
|
||||
}
|
||||
|
||||
// DisableEcho disbales the echo for given file descriptor and returns previous state
|
||||
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings
|
||||
func DisableEcho(fd uintptr, state *State) error {
|
||||
state.mode &^= (ENABLE_ECHO_INPUT)
|
||||
|
@ -60,6 +68,9 @@ func DisableEcho(fd uintptr, state *State) error {
|
|||
return SetConsoleMode(fd, state.mode)
|
||||
}
|
||||
|
||||
// SetRawTerminal puts the terminal connected to the given file descriptor into raw
|
||||
// mode and returns the previous state of the terminal so that it can be
|
||||
// restored.
|
||||
func SetRawTerminal(fd uintptr) (*State, error) {
|
||||
oldState, err := MakeRaw(fd)
|
||||
if err != nil {
|
||||
|
@ -79,8 +90,12 @@ func MakeRaw(fd uintptr) (*State, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings
|
||||
state.mode &^= (ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT)
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
|
||||
// All three input modes, along with processed output mode, are designed to work together.
|
||||
// It is best to either enable or disable all of these modes as a group.
|
||||
// When all are enabled, the application is said to be in "cooked" mode, which means that most of the processing is handled for the application.
|
||||
// When all are disabled, the application is in "raw" mode, which means that input is unfiltered and any processing is left to the application.
|
||||
state.mode = 0
|
||||
err = SetConsoleMode(fd, state.mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
Loading…
Reference in a new issue