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 }