From c6125dd44741870507400b87bd8bdf43e2c1ee45 Mon Sep 17 00:00:00 2001 From: John Starks Date: Wed, 5 Aug 2015 17:20:48 -0700 Subject: [PATCH] VT100 line wrapping and control character support This change adds support for VT100-style line wrapping: when the cursor reaches the end of a line, it does not immediately move to the next line. Instead, it stays at the end of the line until the next printable character arrives. With this change, full-screen applications work much better because they can successfully write to the lower-right corner of the screen. This change also fixes support for various C0 control characters. Signed-off-by: John Starks --- constants.go | 4 + winterm/win_event_handler.go | 332 ++++++++++++++++++++++++++++------- 2 files changed, 277 insertions(+), 59 deletions(-) diff --git a/constants.go b/constants.go index 906b1bd..ebfce8a 100644 --- a/constants.go +++ b/constants.go @@ -77,7 +77,11 @@ const ( DEFAULT_HEIGHT = 24 ANSI_BEL = 0x07 + ANSI_BACKSPACE = 0x08 + ANSI_TAB = 0x09 ANSI_LINE_FEED = 0x0A + ANSI_VERTICAL_TAB = 0x0B + ANSI_FORM_FEED = 0x0C ANSI_CARRIAGE_RETURN = 0x0D ANSI_ESCAPE_PRIMARY = 0x1B ANSI_ESCAPE_SECONDARY = 0x5B diff --git a/winterm/win_event_handler.go b/winterm/win_event_handler.go index aa904a7..0c149df 100644 --- a/winterm/win_event_handler.go +++ b/winterm/win_event_handler.go @@ -15,13 +15,18 @@ import ( var logger *logrus.Logger type WindowsAnsiEventHandler struct { - fd uintptr - file *os.File - infoReset *CONSOLE_SCREEN_BUFFER_INFO - sr scrollRegion - buffer bytes.Buffer - attributes WORD - inverted bool + fd uintptr + file *os.File + infoReset *CONSOLE_SCREEN_BUFFER_INFO + sr scrollRegion + buffer bytes.Buffer + attributes WORD + inverted bool + wrapNext bool + drewMarginByte bool + marginByte byte + curInfo *CONSOLE_SCREEN_BUFFER_INFO + curPos COORD } func CreateWinEventHandler(fd uintptr, file *os.File) AnsiEventHandler { @@ -55,38 +60,207 @@ type scrollRegion struct { bottom SHORT } -func (h *WindowsAnsiEventHandler) Print(b byte) error { - return h.buffer.WriteByte(b) +// simulateLF simulates a LF or CR+LF by scrolling if necessary to handle the +// current cursor position and scroll region settings, in which case it returns +// true. If no special handling is necessary, then it does nothing and returns +// false. +// +// In the false case, the caller should ensure that a carriage return +// and line feed are inserted or that the text is otherwise wrapped. +func (h *WindowsAnsiEventHandler) simulateLF(includeCR bool) (bool, error) { + if h.wrapNext { + if err := h.Flush(); err != nil { + return false, err + } + h.clearWrap() + } + pos, info, err := h.getCurrentInfo() + if err != nil { + return false, err + } + sr := h.effectiveSr(info.Window) + if pos.Y == sr.bottom { + // Scrolling is necessary. Let Windows automatically scroll if the scrolling region + // is the full window. + if sr.top == info.Window.Top && sr.bottom == info.Window.Bottom { + if includeCR { + pos.X = 0 + h.updatePos(pos) + } + return false, nil + } else { + // A custom scroll region is active. Scroll the window manually to simulate + // the LF. + if err := h.Flush(); err != nil { + return false, err + } + logger.Info("Simulating LF inside scroll region") + if err := h.scrollUp(1); err != nil { + return false, err + } + if includeCR { + pos.X = 0 + if err := SetConsoleCursorPosition(h.fd, pos); err != nil { + return false, err + } + } + return true, nil + } + } else if pos.Y < info.Window.Bottom { + // Let Windows handle the LF. + pos.Y++ + if includeCR { + pos.X = 0 + } + h.updatePos(pos) + return false, nil + } else { + // The cursor is at the bottom of the screen but outside the scroll + // region. Skip the LF. + logger.Info("Simulating LF outside scroll region") + if includeCR { + if err := h.Flush(); err != nil { + return false, err + } + pos.X = 0 + if err := SetConsoleCursorPosition(h.fd, pos); err != nil { + return false, err + } + } + return true, nil + } } -func (h *WindowsAnsiEventHandler) Execute(b byte) error { - if ANSI_LINE_FEED == b { - info, err := GetConsoleScreenBufferInfo(h.fd) +// executeLF executes a LF without a CR. +func (h *WindowsAnsiEventHandler) executeLF() error { + handled, err := h.simulateLF(false) + if err != nil { + return err + } + if !handled { + // Windows LF will reset the cursor column position. Write the LF + // and restore the cursor position. + pos, _, err := h.getCurrentInfo() if err != nil { return err } + h.buffer.WriteByte(ANSI_LINE_FEED) + if pos.X != 0 { + if err := h.Flush(); err != nil { + return err + } + logger.Info("Resetting cursor position for LF without CR") + if err := SetConsoleCursorPosition(h.fd, pos); err != nil { + return err + } + } + } + return nil +} - if info.CursorPosition.Y == h.effectiveSr(info.Window).bottom { +func (h *WindowsAnsiEventHandler) Print(b byte) error { + if h.wrapNext { + h.buffer.WriteByte(h.marginByte) + h.clearWrap() + if _, err := h.simulateLF(true); err != nil { + return err + } + } + pos, info, err := h.getCurrentInfo() + if err != nil { + return err + } + if pos.X == info.Size.X-1 { + h.wrapNext = true + h.marginByte = b + } else { + pos.X++ + h.updatePos(pos) + h.buffer.WriteByte(b) + } + return nil +} + +func (h *WindowsAnsiEventHandler) Execute(b byte) error { + switch b { + case ANSI_TAB: + logger.Info("Execute(TAB)") + // Move to the next tab stop, but preserve auto-wrap if already set. + if !h.wrapNext { + pos, info, err := h.getCurrentInfo() + if err != nil { + return err + } + pos.X = (pos.X + 8) - pos.X%8 + if pos.X >= info.Size.X { + pos.X = info.Size.X - 1 + } if err := h.Flush(); err != nil { return err } + if err := SetConsoleCursorPosition(h.fd, pos); err != nil { + return err + } + } + return nil - logger.Infof("Scrolling due to LF at bottom of scroll region") + case ANSI_BEL: + h.buffer.WriteByte(ANSI_BEL) + return nil - // Scroll up one row if we attempt to line feed at the bottom - // of the scroll region - if err := h.scrollUp(1); err != nil { + case ANSI_BACKSPACE: + if h.wrapNext { + if err := h.Flush(); err != nil { return err } - return h.moveCursorColumn(1) + h.clearWrap() } - } + pos, _, err := h.getCurrentInfo() + if err != nil { + return err + } + if pos.X > 0 { + pos.X-- + h.updatePos(pos) + h.buffer.WriteByte(ANSI_BACKSPACE) + } + return nil - if ANSI_BEL <= b && b <= ANSI_CARRIAGE_RETURN { - return h.buffer.WriteByte(b) - } + case ANSI_VERTICAL_TAB, ANSI_FORM_FEED: + // Treat as true LF. + return h.executeLF() - return nil + case ANSI_LINE_FEED: + // Simulate a CR and LF for now since there is no way in go-ansiterm + // to tell if the LF should include CR (and more things break when it's + // missing than when it's incorrectly added). + handled, err := h.simulateLF(true) + if handled || err != nil { + return err + } + return h.buffer.WriteByte(ANSI_LINE_FEED) + + case ANSI_CARRIAGE_RETURN: + if h.wrapNext { + if err := h.Flush(); err != nil { + return err + } + h.clearWrap() + } + pos, _, err := h.getCurrentInfo() + if err != nil { + return err + } + if pos.X != 0 { + pos.X = 0 + h.updatePos(pos) + h.buffer.WriteByte(ANSI_CARRIAGE_RETURN) + } + return nil + + default: + return nil + } } func (h *WindowsAnsiEventHandler) CUU(param int) error { @@ -94,6 +268,7 @@ func (h *WindowsAnsiEventHandler) CUU(param int) error { return err } logger.Infof("CUU: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.moveCursorVertical(-param) } @@ -102,6 +277,7 @@ func (h *WindowsAnsiEventHandler) CUD(param int) error { return err } logger.Infof("CUD: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.moveCursorVertical(param) } @@ -110,6 +286,7 @@ func (h *WindowsAnsiEventHandler) CUF(param int) error { return err } logger.Infof("CUF: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.moveCursorHorizontal(param) } @@ -118,6 +295,7 @@ func (h *WindowsAnsiEventHandler) CUB(param int) error { return err } logger.Infof("CUB: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.moveCursorHorizontal(-param) } @@ -126,6 +304,7 @@ func (h *WindowsAnsiEventHandler) CNL(param int) error { return err } logger.Infof("CNL: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.moveCursorLine(param) } @@ -134,6 +313,7 @@ func (h *WindowsAnsiEventHandler) CPL(param int) error { return err } logger.Infof("CPL: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.moveCursorLine(-param) } @@ -142,6 +322,7 @@ func (h *WindowsAnsiEventHandler) CHA(param int) error { return err } logger.Infof("CHA: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.moveCursorColumn(param) } @@ -150,12 +331,13 @@ func (h *WindowsAnsiEventHandler) VPA(param int) error { return err } logger.Infof("VPA: [[%d]]", param) + h.clearWrap() info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } pos := info.CursorPosition - pos.Y = ensureInRange(SHORT(param-1), info.Window.Top, info.Window.Bottom) + pos.Y = AddInRange(info.Window.Top, SHORT(param-1), info.Window.Top, info.Window.Bottom) return SetConsoleCursorPosition(h.fd, pos) } @@ -163,8 +345,8 @@ func (h *WindowsAnsiEventHandler) CUP(row int, col int) error { if err := h.Flush(); err != nil { return err } - rowStr, colStr := strconv.Itoa(row), strconv.Itoa(col) - logger.Infof("CUP: [%v]", []string{rowStr, colStr}) + logger.Infof("CUP: [[%d %d]]", row, col) + h.clearWrap() info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err @@ -182,8 +364,8 @@ func (h *WindowsAnsiEventHandler) HVP(row int, col int) error { if err := h.Flush(); err != nil { return err } - rowS, colS := strconv.Itoa(row), strconv.Itoa(row) - logger.Infof("HVP: [%v]", []string{rowS, colS}) + logger.Infof("HVP: [[%d %d]]", row, col) + h.clearWrap() return h.CUP(row, col) } @@ -192,7 +374,7 @@ func (h *WindowsAnsiEventHandler) DECTCEM(visible bool) error { return err } logger.Infof("DECTCEM: [%v]", []string{strconv.FormatBool(visible)}) - + h.clearWrap() return nil } @@ -201,6 +383,7 @@ func (h *WindowsAnsiEventHandler) ED(param int) error { return err } logger.Infof("ED: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() // [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. @@ -259,6 +442,7 @@ func (h *WindowsAnsiEventHandler) EL(param int) error { return err } logger.Infof("EL: [%v]", strconv.Itoa(param)) + h.clearWrap() // [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. @@ -299,6 +483,7 @@ func (h *WindowsAnsiEventHandler) IL(param int) error { return err } logger.Infof("IL: [%v]", strconv.Itoa(param)) + h.clearWrap() if err := h.scrollDown(param); err != nil { return err } @@ -311,6 +496,7 @@ func (h *WindowsAnsiEventHandler) DL(param int) error { return err } logger.Infof("DL: [%v]", strconv.Itoa(param)) + h.clearWrap() return h.scrollUp(param) } @@ -320,7 +506,6 @@ func (h *WindowsAnsiEventHandler) SGR(params []int) error { } strings := []string{} for _, v := range params { - logger.Infof("SGR: [%v]", strings) strings = append(strings, strconv.Itoa(v)) } @@ -359,6 +544,7 @@ func (h *WindowsAnsiEventHandler) SU(param int) error { return err } logger.Infof("SU: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.scrollPageUp() } @@ -367,33 +553,14 @@ func (h *WindowsAnsiEventHandler) SD(param int) error { return err } logger.Infof("SD: [%v]", []string{strconv.Itoa(param)}) + h.clearWrap() return h.scrollPageDown() } func (h *WindowsAnsiEventHandler) DA(params []string) error { logger.Infof("DA: [%v]", params) - - // See the site below for details of the device attributes command - // http://vt100.net/docs/vt220-rm/chapter4.html - - // First character of first parameter string is '>' - if params[0][0] == '>' { - // Secondary device attribute request: - // Respond with: - // "I am a VT220 version 1.0, no options. - // CSI > 1 ; 1 0 ; 0 c CR LF - h.buffer.Write([]byte{CSI_ENTRY, 0x3E, 0x31, 0x3B, 0x31, 0x30, 0x3B, 0x30, 0x63, 0x0D, 0x0A}) - - } else { - // Primary device attribute request: - // Respond with: - // "I am a service class 2 terminal (62) with 132 columns (1), - // printer port (2), selective erase (6), DRCS (7), UDK (8), - // and I support 7-bit national replacement character sets (9)." - // CSI ? 6 2 ; 1 ; 2 ; 6 ; 7 ; 8 ; 9 c CR LF - h.buffer.Write([]byte{CSI_ENTRY, 0x3F, 0x36, 0x32, 0x3B, 0x31, 0x3B, 0x32, 0x3B, 0x36, 0x3B, 0x37, 0x3B, 0x38, 0x3B, 0x39, 0x63, 0x0D, 0x0A}) - } - + // DA cannot be implemented because it must send data on the VT100 input stream, + // which is not available to go-ansiterm. return nil } @@ -408,6 +575,7 @@ func (h *WindowsAnsiEventHandler) DECSTBM(top int, bottom int) error { h.sr.bottom = SHORT(bottom - 1) // This command also moves the cursor to the origin. + h.clearWrap() return h.CUP(1, 1) } @@ -416,29 +584,75 @@ func (h *WindowsAnsiEventHandler) RI() error { return err } logger.Info("RI: []") + h.clearWrap() info, err := GetConsoleScreenBufferInfo(h.fd) if err != nil { return err } - if info.Window.Top == info.CursorPosition.Y { - if err := h.scrollPageDown(); err != nil { - return err - } - - return h.EL(2) + sr := h.effectiveSr(info.Window) + if info.CursorPosition.Y == sr.top { + return h.scrollDown(1) } else { - return h.CUU(1) + return h.moveCursorVertical(-1) } } func (h *WindowsAnsiEventHandler) Flush() error { + h.curInfo = nil if h.buffer.Len() > 0 { logger.Infof("Flush: [%s]", h.buffer.Bytes()) if _, err := h.buffer.WriteTo(h.file); err != nil { return err } } + + if h.wrapNext && !h.drewMarginByte { + logger.Infof("Flush: drawing margin byte '%c'", h.marginByte) + + info, err := GetConsoleScreenBufferInfo(h.fd) + if err != nil { + return err + } + + charInfo := []CHAR_INFO{{UnicodeChar: WCHAR(h.marginByte), Attributes: info.Attributes}} + size := COORD{1, 1} + position := COORD{0, 0} + region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y} + if err := WriteConsoleOutput(h.fd, charInfo, size, position, ®ion); err != nil { + return err + } + h.drewMarginByte = true + } return nil } + +// cacheConsoleInfo ensures that the current console screen information has been queried +// since the last call to Flush(). It must be called before accessing h.curInfo or h.curPos. +func (h *WindowsAnsiEventHandler) getCurrentInfo() (COORD, *CONSOLE_SCREEN_BUFFER_INFO, error) { + if h.curInfo == nil { + info, err := GetConsoleScreenBufferInfo(h.fd) + if err != nil { + return COORD{}, nil, err + } + h.curInfo = info + h.curPos = info.CursorPosition + } + return h.curPos, h.curInfo, nil +} + +func (h *WindowsAnsiEventHandler) updatePos(pos COORD) { + if h.curInfo == nil { + panic("failed to call getCurrentInfo before calling updatePos") + } + h.curPos = pos +} + +// clearWrap clears the state where the cursor is in the margin +// waiting for the next character before wrapping the line. This must +// be done before most operations that act on the cursor. +func (h *WindowsAnsiEventHandler) clearWrap() { + h.wrapNext = false + h.drewMarginByte = false +}