diff --git a/buffer/buffer.go b/buffer/buffer.go index d4deadd..08389e0 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -23,6 +23,8 @@ type Buffer struct { topMargin uint // see DECSTBM docs - this is for scrollable regions bottomMargin uint // see DECSTBM docs - this is for scrollable regions replaceMode bool // overwrite character at cursor or insert new + originMode bool // see DECOM docs - whether cursor is positioned within the margins or not + lineFeedMode bool autoWrap bool dirty bool selectionStart *Position @@ -317,6 +319,15 @@ func (buffer *Buffer) SetAutoWrap(enabled bool) { buffer.autoWrap = enabled } +func (buffer *Buffer) IsAutoWrap() bool { + return buffer.autoWrap +} + +func (buffer *Buffer) SetOriginMode(enabled bool) { + buffer.originMode = enabled + buffer.SetPosition(0, 0) +} + func (buffer *Buffer) SetInsertMode() { buffer.replaceMode = false } @@ -330,6 +341,11 @@ func (buffer *Buffer) SetVerticalMargins(top uint, bottom uint) { buffer.bottomMargin = bottom } +// ResetVerticalMargins resets margins to extreme positions +func (buffer *Buffer) ResetVerticalMargins() { + buffer.SetVerticalMargins(0, uint(buffer.viewHeight-1)) +} + func (buffer *Buffer) GetScrollOffset() uint { return buffer.scrollLinesFromBottom } @@ -419,11 +435,19 @@ func (buffer *Buffer) emitDisplayChange() { // Column returns cursor column func (buffer *Buffer) CursorColumn() uint16 { + // @todo originMode and left margin return buffer.cursorX } // Line returns cursor line func (buffer *Buffer) CursorLine() uint16 { + if buffer.originMode { + result := buffer.cursorY - uint16(buffer.topMargin) + if result < 0 { + result = 0 + } + return result + } return buffer.cursorY } @@ -492,8 +516,8 @@ func (buffer *Buffer) insertLine() { out := make([]Line, newLineCount) copy( - out[ : pos - ( uint64(len(buffer.lines)) + 1 - newLineCount )], - buffer.lines[ uint64(len(buffer.lines)) + 1 - newLineCount : pos] ) + out[:pos-(uint64(len(buffer.lines))+1-newLineCount)], + buffer.lines[uint64(len(buffer.lines))+1-newLineCount:pos]) out[pos] = newLine() copy(out[pos+1:], buffer.lines[pos:]) buffer.lines = out @@ -655,13 +679,13 @@ func (buffer *Buffer) Write(runes ...rune) { if buffer.autoWrap { - buffer.NewLine() + buffer.NewLineEx(true) newLine := buffer.getCurrentLine() if len(newLine.cells) == 0 { - newLine.cells = []Cell{{}} + newLine.cells = append(newLine.cells, buffer.defaultCell) } - cell := &newLine.cells[buffer.CursorColumn()] + cell := &newLine.cells[0] cell.setRune(r) cell.attr = buffer.cursorAttr @@ -694,6 +718,13 @@ func (buffer *Buffer) incrementCursorPosition() { } } +func (buffer *Buffer) inDoWrap() bool { + // xterm uses 'do_wrap' flag for this special terminal state + // we use the cursor position right after the boundary + // let's see how it works out + return buffer.cursorX == buffer.viewWidth // @todo rightMargin +} + func (buffer *Buffer) Backspace() { if buffer.cursorX == 0 { @@ -703,6 +734,9 @@ func (buffer *Buffer) Backspace() { } else { //@todo ring bell or whatever - actually i think the pty will trigger this } + } else if buffer.inDoWrap() { + // the "do_wrap" implementation + buffer.MovePosition(-2, 0) } else { buffer.MovePosition(-1, 0) } @@ -727,15 +761,33 @@ func (buffer *Buffer) CarriageReturn() { func (buffer *Buffer) Tab() { tabSize := 4 + max := tabSize + + // @todo rightMargin + if buffer.cursorX < buffer.viewWidth { + max = int(buffer.viewWidth - buffer.cursorX - 1) + } + shift := tabSize - (int(buffer.cursorX+1) % tabSize) + + if shift > max { + shift = max + } + for i := 0; i < shift; i++ { buffer.Write(' ') } } func (buffer *Buffer) NewLine() { + buffer.NewLineEx(false) +} - buffer.cursorX = 0 +func (buffer *Buffer) NewLineEx(forceCursorToMargin bool) { + + if buffer.IsNewLineMode() || forceCursorToMargin { + buffer.cursorX = 0 + } buffer.Index() for { @@ -747,21 +799,34 @@ func (buffer *Buffer) NewLine() { } } +func (buffer *Buffer) SetNewLineMode() { + buffer.lineFeedMode = false +} + +func (buffer *Buffer) SetLineFeedMode() { + buffer.lineFeedMode = true +} + +func (buffer *Buffer) IsNewLineMode() bool { + return buffer.lineFeedMode == false +} + func (buffer *Buffer) MovePosition(x int16, y int16) { var toX uint16 var toY uint16 - if int16(buffer.cursorX)+x < 0 { + if int16(buffer.CursorColumn())+x < 0 { toX = 0 } else { - toX = uint16(int16(buffer.cursorX) + x) + toX = uint16(int16(buffer.CursorColumn()) + x) } - if int16(buffer.cursorY)+y < 0 { + // should either use CursorLine() and SetPosition() or use absolutes, mind Origin Mode (DECOM) + if int16(buffer.CursorLine())+y < 0 { toY = 0 } else { - toY = uint16(int16(buffer.cursorY) + y) + toY = uint16(int16(buffer.CursorLine()) + y) } buffer.SetPosition(toX, toY) @@ -770,17 +835,26 @@ func (buffer *Buffer) MovePosition(x int16, y int16) { func (buffer *Buffer) SetPosition(col uint16, line uint16) { defer buffer.emitDisplayChange() - if col >= buffer.ViewWidth() { - col = buffer.ViewWidth() - 1 - //logrus.Errorf("Cannot set cursor position: column %d is outside of the current view width (%d columns)", col, buffer.ViewWidth()) + useCol := col + useLine := line + maxLine := buffer.ViewHeight() - 1 + + if buffer.originMode { + useLine += uint16(buffer.topMargin) + maxLine = uint16(buffer.bottomMargin) + // @todo left and right margins } - if line >= buffer.ViewHeight() { - line = buffer.ViewHeight() - 1 - //logrus.Errorf("Cannot set cursor position: line %d is outside of the current view height (%d lines)", line, buffer.ViewHeight()) + if useLine > maxLine { + useLine = maxLine } - buffer.cursorX = col - buffer.cursorY = line + if useCol >= buffer.ViewWidth() { + useCol = buffer.ViewWidth() - 1 + //logrus.Errorf("Cannot set cursor position: column %d is outside of the current view width (%d columns)", col, buffer.ViewWidth()) + } + + buffer.cursorX = useCol + buffer.cursorY = useLine } func (buffer *Buffer) GetVisibleLines() []Line { @@ -928,7 +1002,7 @@ func (buffer *Buffer) EraseDisplayToCursor() { defer buffer.emitDisplayChange() line := buffer.getCurrentLine() - for i := 0; i < int(buffer.cursorX); i++ { + for i := 0; i <= int(buffer.cursorX); i++ { if i >= len(line.cells) { break } @@ -1042,7 +1116,7 @@ func (buffer *Buffer) ResizeView(width uint16, height uint16) { line = buffer.getCurrentLine() buffer.cursorX = uint16((len(line.cells) - cXFromEndOfLine) - 1) - buffer.SetVerticalMargins(0, uint(buffer.viewHeight-1)) + buffer.ResetVerticalMargins() } func (buffer *Buffer) getMaxLines() uint64 { diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go index 523635c..1b48f2e 100644 --- a/buffer/buffer_test.go +++ b/buffer/buffer_test.go @@ -508,7 +508,7 @@ func TestEraseDisplayToCursor(t *testing.T) { lines := b.GetVisibleLines() assert.Equal(t, "", lines[0].String()) assert.Equal(t, "", lines[1].String()) - assert.Equal(t, "\x00\x00\x00ng", lines[2].String()) + assert.Equal(t, "\x00\x00\x00\x00g", lines[2].String()) } @@ -634,4 +634,4 @@ func TestBufferMaxLines(t *testing.T) { assert.Equal(t, 2, len(b.lines)) assert.Equal(t, "funny", b.lines[0].String()) assert.Equal(t, "world", b.lines[1].String()) -} \ No newline at end of file +} diff --git a/gui/gui.go b/gui/gui.go index e6fca8a..204ad6b 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -2,6 +2,7 @@ package gui import ( "fmt" + "math" "image" "image/png" "os" @@ -28,8 +29,9 @@ type GUI struct { logger *zap.SugaredLogger config *config.Config terminal *terminal.Terminal - width int //window width in pixels - height int //window height in pixels + width int //window width in pixels + height int //window height in pixels + resizeCache *ResizeCache // resize cache formed by resizeToTerminal() dpiScale float32 fontMap *FontMap fontScale float32 @@ -59,6 +61,13 @@ func Max(x, y int) int { return y } +type ResizeCache struct { + Width int + Height int + Cols uint + Rows uint +} + func (g *GUI) GetMonitor() *glfw.Monitor { if g.window == nil { @@ -78,8 +87,8 @@ func (g *GUI) GetMonitor() *glfw.Monitor { for _, monitor := range monitors { mode := monitor.GetVideoMode() mx, my := monitor.GetPos() - overlap := Max(0, Min(x + w, mx + mode.Width) - Max(x, mx)) * - Max(0, Min(y + h, my + mode.Height) - Max(y, my)) + overlap := Max(0, Min(x+w, mx+mode.Width)-Max(x, mx)) * + Max(0, Min(y+h, my+mode.Height)-Max(y, my)) if bestMatch < overlap { bestMatch = overlap currentMonitor = monitor @@ -153,6 +162,35 @@ func (gui *GUI) scale() float32 { return float32(ww) / float32(pw) } +// can only be called on OS thread +func (gui *GUI) resizeToTerminal(newCols uint, newRows uint) { + + if gui.window.GetAttrib(glfw.Iconified) != 0 { + return + } + + gui.resizeLock.Lock() + defer gui.resizeLock.Unlock() + + cols, rows := gui.renderer.GetTermSize() + if cols == newCols && rows == newRows { + return + } + + gui.logger.Debugf("Initiating GUI resize to columns=%d rows=%d", newCols, newRows) + + gui.logger.Debugf("Calculating size...") + width, height := gui.renderer.GetRectangleSize(newCols, newRows) + + roundedWidth := int(math.Ceil(float64(width))) + roundedHeight := int(math.Ceil(float64(height))) + + gui.resizeCache = &ResizeCache{roundedWidth, roundedHeight, newCols, newRows} + + gui.logger.Debugf("Resizing window to %dx%d", roundedWidth, roundedHeight) + gui.window.SetSize(roundedWidth, roundedHeight) // will trigger resize() +} + // can only be called on OS thread func (gui *GUI) resize(w *glfw.Window, width int, height int) { @@ -174,14 +212,19 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) { gui.logger.Debugf("Setting renderer area...") gui.renderer.SetArea(0, 0, gui.Width(), gui.Height()) - gui.logger.Debugf("Calculating size in cols/rows...") - cols, rows := gui.renderer.GetTermSize() - - gui.logger.Debugf("Resizing internal terminal...") - if err := gui.terminal.SetSize(cols, rows); err != nil { - gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err) + if gui.resizeCache != nil && gui.resizeCache.Width == width && gui.resizeCache.Height == height { + gui.logger.Debugf("No need to resize internal terminal!") + } else { + gui.logger.Debugf("Calculating size in cols/rows...") + cols, rows := gui.renderer.GetTermSize() + gui.logger.Debugf("Resizing internal terminal...") + if err := gui.terminal.SetSize(cols, rows); err != nil { + gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err) + } } + gui.resizeCache = nil + gui.logger.Debugf("Setting viewport size...") gl.Viewport(0, 0, int32(gui.Width()), int32(gui.Height())) @@ -234,6 +277,7 @@ func (gui *GUI) Render() error { } titleChan := make(chan bool, 1) + resizeChan := make(chan bool, 1) gui.renderer = NewOpenGLRenderer(gui.config, gui.fontMap, 0, 0, gui.Width(), gui.Height(), gui.colourAttr, program) @@ -283,6 +327,7 @@ func (gui *GUI) Render() error { ) gui.terminal.AttachTitleChangeHandler(titleChan) + gui.terminal.AttachResizeHandler(resizeChan) ticker := time.NewTicker(time.Second) defer ticker.Stop() @@ -316,6 +361,9 @@ func (gui *GUI) Render() error { select { case <-titleChan: gui.window.SetTitle(gui.terminal.GetTitle()) + case <-resizeChan: + cols, rows := gui.terminal.GetSize() + gui.resizeToTerminal(uint(cols), uint(rows)) default: // this is more efficient than glfw.PollEvents() glfw.WaitEventsTimeout(0.02) // up to 50fps on no input, otherwise higher @@ -497,7 +545,7 @@ func (gui *GUI) createWindow() (*glfw.Window, error) { return nil, fmt.Errorf("failed to create window, please update your graphics drivers and try again") } - window.SetSizeLimits(int(300 * gui.dpiScale), int(150 * gui.dpiScale), 10000, 10000) + window.SetSizeLimits(int(300*gui.dpiScale), int(150*gui.dpiScale), 10000, 10000) window.MakeContextCurrent() window.Show() window.Focus() diff --git a/gui/input.go b/gui/input.go index 9b2515d..ab97d68 100644 --- a/gui/input.go +++ b/gui/input.go @@ -212,9 +212,7 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti 0x09, }) case glfw.KeyEnter: - gui.terminal.Write([]byte{ - 0x0d, - }) + gui.terminal.WriteReturn() case glfw.KeyKPEnter: if gui.terminal.IsApplicationCursorKeysModeEnabled() { gui.terminal.Write([]byte{ @@ -223,9 +221,7 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti 'M', }) } else { - gui.terminal.Write([]byte{ - 0x0d, - }) + gui.terminal.WriteReturn() } case glfw.KeyBackspace: if modsPressed(mods, glfw.ModAlt) { diff --git a/gui/renderer.go b/gui/renderer.go index 5a87e26..0f9ab5e 100644 --- a/gui/renderer.go +++ b/gui/renderer.go @@ -176,6 +176,13 @@ func (r *OpenGLRenderer) SetArea(areaX int, areaY int, areaWidth int, areaHeight r.termRows = uint(math.Floor(float64(float32(r.areaHeight) / r.cellHeight))) } +func (r *OpenGLRenderer) GetRectangleSize(col uint, row uint) (float32, float32) { + x := float32(float32(col) * r.cellWidth) + y := float32(float32(row) * r.cellHeight) + + return x, y +} + func (r *OpenGLRenderer) getRectangle(col uint, row uint) *rectangle { x := float32(float32(col) * r.cellWidth) y := float32(float32(row) * r.cellHeight) + r.cellHeight diff --git a/main_test.go b/main_test.go index 1eef9ea..152906f 100644 --- a/main_test.go +++ b/main_test.go @@ -65,11 +65,33 @@ func TestCursorMovement(t *testing.T) { if term.ActiveBuffer().Compare("vttest/test-cursor-movement-1") == false { t.Error(fmt.Sprint("ActiveBuffer doesn't match vttest template")) } + g.Screenshot ("test-cursor-movement-1.png") + compareImages("vttest/test-cursor-movement-1.png", "test-cursor-movement-1.png") enter(term) sleep() g.Screenshot ("test-cursor-movement-2.png") compareImages("vttest/test-cursor-movement-2.png", "test-cursor-movement-2.png") + + enter(term) + sleep() + g.Screenshot ("test-cursor-movement-3.png") + compareImages("vttest/test-cursor-movement-3.png", "test-cursor-movement-3.png") + + enter(term) + sleep() + g.Screenshot ("test-cursor-movement-4.png") + compareImages("vttest/test-cursor-movement-4.png", "test-cursor-movement-4.png") + + enter(term) + sleep() + g.Screenshot ("test-cursor-movement-5.png") + compareImages("vttest/test-cursor-movement-5.png", "test-cursor-movement-5.png") + + enter(term) + sleep() + g.Screenshot ("test-cursor-movement-6.png") + compareImages("vttest/test-cursor-movement-6.png", "test-cursor-movement-6.png") os.Exit(0) } diff --git a/terminal/ansi.go b/terminal/ansi.go index c05af3f..e56ddb1 100644 --- a/terminal/ansi.go +++ b/terminal/ansi.go @@ -11,9 +11,11 @@ var ansiSequenceMap = map[rune]escapeSequenceHandler{ '7': saveCursorHandler, '8': restoreCursorHandler, 'D': indexHandler, + 'E': nextLineHandler, // NEL 'M': reverseIndexHandler, 'P': sixelHandler, - 'c': risHandler, //RIS + 'c': risHandler, //RIS + '#': screenStateHandler, '(': swallowHandler(1), // character set bullshit ')': swallowHandler(1), // character set bullshit '*': swallowHandler(1), // character set bullshit @@ -68,3 +70,8 @@ func ansiHandler(pty chan rune, terminal *Terminal) error { return fmt.Errorf("Unknown ANSI control sequence byte: 0x%02X [%v]", b, string(b)) } + +func nextLineHandler(pty chan rune, terminal *Terminal) error { + terminal.ActiveBuffer().NewLineEx(true) + return nil +} diff --git a/terminal/csi.go b/terminal/csi.go index 7ed515d..ea39a6f 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -6,7 +6,7 @@ import ( "strings" ) -type csiSequenceHandler func(params []string, intermediate string, terminal *Terminal) error +type csiSequenceHandler func(params []string, terminal *Terminal) error type csiMapping struct { id rune @@ -56,19 +56,18 @@ type runeRange struct { var csiTerminators = runeRange{0x40, 0x7e} -func loadCSI(pty chan rune) (final rune, param string, intermediate string) { +func loadCSI(pty chan rune) (final rune, param string, intermediate []rune) { var b rune param = "" - intermediate = "" + intermediate = []rune{} CSI: for { b = <-pty switch true { case b >= 0x30 && b <= 0x3F: param = param + string(b) - case b >= 0x20 && b <= 0x2F: - //intermediate? useful? - intermediate += string(b) + case b > 0 && b <= 0x2F: + intermediate = append(intermediate, b) case b >= csiTerminators.min && b <= csiTerminators.max: final = b break CSI @@ -89,6 +88,11 @@ func splitParams(paramString string) []string { func csiHandler(pty chan rune, terminal *Terminal) error { final, param, intermediate := loadCSI(pty) + // process intermediate control codes before the CSI + for _, b := range intermediate { + terminal.processRune(b) + } + params := splitParams(param) for _, sequence := range csiSequences { @@ -97,16 +101,16 @@ func csiHandler(pty chan rune, terminal *Terminal) error { continue } x, y := terminal.ActiveBuffer().CursorColumn(), terminal.ActiveBuffer().CursorLine() - err := sequence.handler(params, intermediate, terminal) - terminal.logger.Debugf("CSI 0x%02X (ESC[%s%s%s) %s - %d,%d -> %d,%d", final, param, intermediate, string(final), sequence.description, x, y, terminal.ActiveBuffer().CursorColumn(), terminal.ActiveBuffer().CursorLine()) + err := sequence.handler(params, terminal) + terminal.logger.Debugf("CSI 0x%02X (ESC[%s%s) %s - %d,%d -> %d,%d", final, param, string(final), sequence.description, x, y, terminal.ActiveBuffer().CursorColumn(), terminal.ActiveBuffer().CursorLine()) return err } } - return fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final)) + return fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s)", final, param, string(final)) } -func csiSendDeviceAttributesHandler(params []string, intermediate string, terminal *Terminal) error { +func csiSendDeviceAttributesHandler(params []string, terminal *Terminal) error { // we are VT100 // for DA1 we'll respond 1;2 @@ -132,7 +136,7 @@ func csiSendDeviceAttributesHandler(params []string, intermediate string, termin return nil } -func csiDeviceStatusReportHandler(params []string, intermediate string, terminal *Terminal) error { +func csiDeviceStatusReportHandler(params []string, terminal *Terminal) error { if len(params) == 0 { return fmt.Errorf("Missing Device Status Report identifier") @@ -154,7 +158,7 @@ func csiDeviceStatusReportHandler(params []string, intermediate string, terminal return nil } -func csiCursorUpHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorUpHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 0 { var err error @@ -167,7 +171,7 @@ func csiCursorUpHandler(params []string, intermediate string, terminal *Terminal return nil } -func csiCursorDownHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorDownHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 0 { var err error @@ -181,7 +185,7 @@ func csiCursorDownHandler(params []string, intermediate string, terminal *Termin return nil } -func csiCursorForwardHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorForwardHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 0 { var err error @@ -195,7 +199,7 @@ func csiCursorForwardHandler(params []string, intermediate string, terminal *Ter return nil } -func csiCursorBackwardHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorBackwardHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 0 { var err error @@ -209,7 +213,7 @@ func csiCursorBackwardHandler(params []string, intermediate string, terminal *Te return nil } -func csiCursorNextLineHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorNextLineHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 0 { @@ -225,7 +229,7 @@ func csiCursorNextLineHandler(params []string, intermediate string, terminal *Te return nil } -func csiCursorPrecedingLineHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorPrecedingLineHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 0 { @@ -240,7 +244,7 @@ func csiCursorPrecedingLineHandler(params []string, intermediate string, termina return nil } -func csiCursorCharacterAbsoluteHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorCharacterAbsoluteHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 0 { var err error @@ -274,14 +278,14 @@ func parseCursorPosition(params []string) (x, y int) { return x, y } -func csiCursorPositionHandler(params []string, intermediate string, terminal *Terminal) error { +func csiCursorPositionHandler(params []string, terminal *Terminal) error { x, y := parseCursorPosition(params) terminal.ActiveBuffer().SetPosition(uint16(x-1), uint16(y-1)) return nil } -func csiScrollUpHandler(params []string, intermediate string, terminal *Terminal) error { +func csiScrollUpHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 1 { return fmt.Errorf("Not supported") @@ -298,7 +302,7 @@ func csiScrollUpHandler(params []string, intermediate string, terminal *Terminal return nil } -func csiInsertBlankCharactersHandler(params []string, intermediate string, terminal *Terminal) error { +func csiInsertBlankCharactersHandler(params []string, terminal *Terminal) error { count := 1 if len(params) > 1 { return fmt.Errorf("Not supported") @@ -316,7 +320,7 @@ func csiInsertBlankCharactersHandler(params []string, intermediate string, termi return nil } -func csiInsertLinesHandler(params []string, intermediate string, terminal *Terminal) error { +func csiInsertLinesHandler(params []string, terminal *Terminal) error { count := 1 if len(params) > 1 { return fmt.Errorf("Not supported") @@ -334,7 +338,7 @@ func csiInsertLinesHandler(params []string, intermediate string, terminal *Termi return nil } -func csiDeleteLinesHandler(params []string, intermediate string, terminal *Terminal) error { +func csiDeleteLinesHandler(params []string, terminal *Terminal) error { count := 1 if len(params) > 1 { return fmt.Errorf("Not supported") @@ -352,7 +356,7 @@ func csiDeleteLinesHandler(params []string, intermediate string, terminal *Termi return nil } -func csiScrollDownHandler(params []string, intermediate string, terminal *Terminal) error { +func csiScrollDownHandler(params []string, terminal *Terminal) error { distance := 1 if len(params) > 1 { return fmt.Errorf("Not supported") @@ -370,7 +374,7 @@ func csiScrollDownHandler(params []string, intermediate string, terminal *Termin } // DECSTBM -func csiSetMarginsHandler(params []string, intermediate string, terminal *Terminal) error { +func csiSetMarginsHandler(params []string, terminal *Terminal) error { top := 1 bottom := int(terminal.ActiveBuffer().ViewHeight()) @@ -402,7 +406,7 @@ func csiSetMarginsHandler(params []string, intermediate string, terminal *Termin return nil } -func csiEraseCharactersHandler(params []string, intermediate string, terminal *Terminal) error { +func csiEraseCharactersHandler(params []string, terminal *Terminal) error { count := 1 if len(params) > 0 { var err error @@ -417,19 +421,19 @@ func csiEraseCharactersHandler(params []string, intermediate string, terminal *T return nil } -func csiResetModeHandler(params []string, intermediate string, terminal *Terminal) error { +func csiResetModeHandler(params []string, terminal *Terminal) error { return csiSetMode(strings.Join(params, ""), false, terminal) } -func csiSetModeHandler(params []string, intermediate string, terminal *Terminal) error { +func csiSetModeHandler(params []string, terminal *Terminal) error { return csiSetMode(strings.Join(params, ""), true, terminal) } -func csiWindowManipulation(params []string, intermediate string, terminal *Terminal) error { +func csiWindowManipulation(params []string, terminal *Terminal) error { return fmt.Errorf("Window manipulation is not yet supported") } -func csiLinePositionAbsolute(params []string, intermediate string, terminal *Terminal) error { +func csiLinePositionAbsolute(params []string, terminal *Terminal) error { row := 1 if len(params) > 0 { var err error @@ -444,7 +448,7 @@ func csiLinePositionAbsolute(params []string, intermediate string, terminal *Ter return nil } -func csiDeleteHandler(params []string, intermediate string, terminal *Terminal) error { +func csiDeleteHandler(params []string, terminal *Terminal) error { n := 1 if len(params) >= 1 { var err error @@ -459,7 +463,7 @@ func csiDeleteHandler(params []string, intermediate string, terminal *Terminal) } // CSI Ps J -func csiEraseInDisplayHandler(params []string, intermediate string, terminal *Terminal) error { +func csiEraseInDisplayHandler(params []string, terminal *Terminal) error { n := "0" if len(params) > 0 { n = params[0] @@ -481,7 +485,7 @@ func csiEraseInDisplayHandler(params []string, intermediate string, terminal *Te } // CSI Ps K -func csiEraseInLineHandler(params []string, intermediate string, terminal *Terminal) error { +func csiEraseInLineHandler(params []string, terminal *Terminal) error { n := "0" if len(params) > 0 { diff --git a/terminal/modes.go b/terminal/modes.go index 1a95468..ae69664 100644 --- a/terminal/modes.go +++ b/terminal/modes.go @@ -29,8 +29,39 @@ func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error { } else { terminal.ActiveBuffer().SetReplaceMode() } + case "20": + if enabled { + terminal.ActiveBuffer().SetNewLineMode() + } else { + terminal.ActiveBuffer().SetLineFeedMode() + } case "?1": terminal.modes.ApplicationCursorKeys = enabled + case "?3": + _, lines := terminal.GetSize() + if enabled { + // DECCOLM - COLumn mode, 132 characters per line + terminal.SetSize(132, uint(lines)) + } else { + // DECCOLM - 80 characters per line (erases screen) + terminal.SetSize(80, uint(lines)) + } + terminal.Clear() + /* + case "?4": + // DECSCLM + // @todo smooth scrolling / jump scrolling + case "?5": + // DECSCNM + if enabled { + // @todo SCreeN Mode, black on white background + } else { + // @todo Normal screen (white on black background) + } + */ + case "?6": + // DECOM + terminal.ActiveBuffer().SetOriginMode(enabled) case "?7": // auto-wrap mode //DECAWM @@ -78,7 +109,13 @@ func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error { case "?2004": terminal.SetBracketedPasteMode(enabled) default: - return fmt.Errorf("Unsupported CSI %sl code", modeStr) + code := "" + if enabled { + code = "h" + } else { + code = "l" + } + return fmt.Errorf("Unsupported CSI %s%s code", modeStr, code) } return nil diff --git a/terminal/output.go b/terminal/output.go index 9dd19a3..3dfd3df 100644 --- a/terminal/output.go +++ b/terminal/output.go @@ -8,66 +8,81 @@ import ( type TerminalCharSet int +// single rune handler +type runeHandler func(terminal *Terminal) error + type escapeSequenceHandler func(pty chan rune, terminal *Terminal) error -var escapeSequenceMap = map[rune]escapeSequenceHandler{ - 0x05: enqSequenceHandler, - 0x07: bellSequenceHandler, - 0x08: backspaceSequenceHandler, - 0x09: tabSequenceHandler, - 0x0a: newLineSequenceHandler, - 0x0b: newLineSequenceHandler, - 0x0c: newLineSequenceHandler, - 0x0d: carriageReturnSequenceHandler, - 0x0e: shiftOutSequenceHandler, - 0x0f: shiftInSequenceHandler, - 0x1b: ansiHandler, +var runeMap = map[rune]runeHandler{ + 0x05: enqHandler, + 0x07: bellHandler, + 0x08: backspaceHandler, + 0x09: tabHandler, + 0x0a: newLineHandler, + 0x0b: newLineHandler, + 0x0c: newLineHandler, + 0x0d: carriageReturnHandler, + 0x0e: shiftOutHandler, + 0x0f: shiftInHandler, } -func newLineSequenceHandler(pty chan rune, terminal *Terminal) error { +func newLineHandler(terminal *Terminal) error { terminal.ActiveBuffer().NewLine() terminal.isDirty = true return nil } -func tabSequenceHandler(pty chan rune, terminal *Terminal) error { +func tabHandler(terminal *Terminal) error { terminal.ActiveBuffer().Tab() terminal.isDirty = true return nil } -func carriageReturnSequenceHandler(pty chan rune, terminal *Terminal) error { +func carriageReturnHandler(terminal *Terminal) error { terminal.ActiveBuffer().CarriageReturn() terminal.isDirty = true return nil } -func backspaceSequenceHandler(pty chan rune, terminal *Terminal) error { +func backspaceHandler(terminal *Terminal) error { terminal.ActiveBuffer().Backspace() terminal.isDirty = true return nil } -func bellSequenceHandler(pty chan rune, terminal *Terminal) error { +func bellHandler(terminal *Terminal) error { // @todo ring bell - flash red or some shit? return nil } -func enqSequenceHandler(pty chan rune, terminal *Terminal) error { +func enqHandler(terminal *Terminal) error { terminal.logger.Errorf("Received ENQ!") return nil } -func shiftOutSequenceHandler(pty chan rune, terminal *Terminal) error { +func shiftOutHandler(terminal *Terminal) error { terminal.logger.Errorf("Received shift out") return nil } -func shiftInSequenceHandler(pty chan rune, terminal *Terminal) error { +func shiftInHandler(terminal *Terminal) error { terminal.logger.Errorf("Received shift in") return nil } +func (terminal *Terminal) processRune(b rune) { + if handler, ok := runeMap[b]; ok { + if err := handler(terminal); err != nil { + terminal.logger.Errorf("Error handling control code: %s", err) + } + terminal.isDirty = true + return + } + //terminal.logger.Debugf("Received character 0x%X: %q", b, string(b)) + terminal.ActiveBuffer().Write(b) + terminal.isDirty = true +} + func (terminal *Terminal) processInput(pty chan rune) { // https://en.wikipedia.org/wiki/ANSI_escape_code @@ -82,19 +97,15 @@ func (terminal *Terminal) processInput(pty chan rune) { b = <-pty - if b < 0x20 { - if handler, ok := escapeSequenceMap[b]; ok { - //terminal.logger.Debugf("Handling escape sequence: 0x%x", b) - if err := handler(pty, terminal); err != nil { - terminal.logger.Errorf("Error handling escape sequence: %s", err) - } - terminal.isDirty = true - continue + if b == 0x1b { + //terminal.logger.Debugf("Handling escape sequence: 0x%x", b) + if err := ansiHandler(pty, terminal); err != nil { + terminal.logger.Errorf("Error handling escape sequence: %s", err) } + terminal.isDirty = true + continue } - //terminal.logger.Debugf("Received character 0x%X: %q", b, string(b)) - terminal.ActiveBuffer().Write(b) - terminal.isDirty = true + terminal.processRune(b) } } diff --git a/terminal/scr_state.go b/terminal/scr_state.go new file mode 100644 index 0000000..752c20d --- /dev/null +++ b/terminal/scr_state.go @@ -0,0 +1,30 @@ +package terminal + +import "fmt" + +func screenStateHandler(pty chan rune, terminal *Terminal) error { + b := <-pty + switch b { + case '8': // DECALN -- Screen Alignment Pattern + // hide cursor? + buffer := terminal.ActiveBuffer() + buffer.ResetVerticalMargins() + buffer.ScrollToEnd() + + // Fill the whole screen with E's + count := buffer.ViewHeight() * buffer.ViewWidth() + for count > 0 { + buffer.Write('E') + count-- + if count > 0 && !buffer.IsAutoWrap() && count%buffer.ViewWidth() == 0 { + buffer.Index() + buffer.CarriageReturn() + } + } + // restore cursor + buffer.SetPosition(0, 0) + default: + return fmt.Errorf("Screen State code not supported: 0x%02X [%v]", b, string(b)) + } + return nil +} diff --git a/terminal/sgr.go b/terminal/sgr.go index b136679..87c40db 100644 --- a/terminal/sgr.go +++ b/terminal/sgr.go @@ -9,7 +9,7 @@ import ( "github.com/liamg/aminal/config" ) -func sgrSequenceHandler(params []string, intermediate string, terminal *Terminal) error { +func sgrSequenceHandler(params []string, terminal *Terminal) error { if len(params) == 0 { params = []string{"0"} @@ -137,7 +137,7 @@ func sgrSequenceHandler(params []string, intermediate string, terminal *Terminal terminal.ActiveBuffer().CursorAttr().BgColour = c return nil default: - return fmt.Errorf("Unknown SGR control sequence: (ESC[%s%sm)", params[i:], intermediate) + return fmt.Errorf("Unknown SGR control sequence: (ESC[%sm)", params[i:]) } } diff --git a/terminal/terminal.go b/terminal/terminal.go index 6b9e31a..68b0c44 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -3,12 +3,13 @@ package terminal import ( "bufio" "fmt" + "io" + "sync" + "github.com/liamg/aminal/buffer" "github.com/liamg/aminal/config" "github.com/liamg/aminal/platform" "go.uber.org/zap" - "io" - "sync" ) const ( @@ -39,6 +40,7 @@ type Terminal struct { size Winsize config *config.Config titleHandlers []chan bool + resizeHandlers []chan bool modes Modes mouseMode MouseMode bracketedPasteMode bool @@ -194,6 +196,10 @@ func (terminal *Terminal) AttachTitleChangeHandler(handler chan bool) { terminal.titleHandlers = append(terminal.titleHandlers, handler) } +func (terminal *Terminal) AttachResizeHandler(handler chan bool) { + terminal.resizeHandlers = append(terminal.resizeHandlers, handler) +} + func (terminal *Terminal) Modes() Modes { return terminal.modes } @@ -206,6 +212,14 @@ func (terminal *Terminal) emitTitleChange() { } } +func (terminal *Terminal) emitResize() { + for _, h := range terminal.resizeHandlers { + go func(c chan bool) { + c <- true + }(h) + } +} + func (terminal *Terminal) GetLogicalCursorX() uint16 { if terminal.ActiveBuffer().CursorColumn() >= terminal.ActiveBuffer().Width() { return 0 @@ -237,6 +251,14 @@ func (terminal *Terminal) Write(data []byte) error { return err } +func (terminal *Terminal) WriteReturn() error { + if terminal.ActiveBuffer().IsNewLineMode() { + return terminal.Write([]byte{0x0d, 0x0a}) + } else { + return terminal.Write([]byte{0x0d}) + } +} + func (terminal *Terminal) Paste(data []byte) error { if terminal.bracketedPasteMode { @@ -281,14 +303,20 @@ func (terminal *Terminal) SetSize(newCols uint, newLines uint) error { terminal.lock.Lock() defer terminal.lock.Unlock() - terminal.size.Width = uint16(newCols) - terminal.size.Height = uint16(newLines) + if terminal.size.Width == uint16(newCols) && terminal.size.Height == uint16(newLines) { + return nil + } err := terminal.pty.Resize(int(newCols), int(newLines)) if err != nil { return fmt.Errorf("Failed to set terminal size vai ioctl: Error no %d", err) } + terminal.size.Width = uint16(newCols) + terminal.size.Height = uint16(newLines) + terminal.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height) + + terminal.emitResize() return nil } diff --git a/vttest/test-cursor-movement-1.png b/vttest/test-cursor-movement-1.png new file mode 100644 index 0000000..0adc66b Binary files /dev/null and b/vttest/test-cursor-movement-1.png differ diff --git a/vttest/test-cursor-movement-2.png b/vttest/test-cursor-movement-2.png index abfe168..4d286e2 100644 Binary files a/vttest/test-cursor-movement-2.png and b/vttest/test-cursor-movement-2.png differ diff --git a/vttest/test-cursor-movement-3.png b/vttest/test-cursor-movement-3.png new file mode 100644 index 0000000..bd2a54a Binary files /dev/null and b/vttest/test-cursor-movement-3.png differ diff --git a/vttest/test-cursor-movement-4.png b/vttest/test-cursor-movement-4.png new file mode 100644 index 0000000..586df52 Binary files /dev/null and b/vttest/test-cursor-movement-4.png differ diff --git a/vttest/test-cursor-movement-5.png b/vttest/test-cursor-movement-5.png new file mode 100644 index 0000000..f034011 Binary files /dev/null and b/vttest/test-cursor-movement-5.png differ diff --git a/vttest/test-cursor-movement-6.png b/vttest/test-cursor-movement-6.png new file mode 100644 index 0000000..d9305a0 Binary files /dev/null and b/vttest/test-cursor-movement-6.png differ