diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc359d8..4791cc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,9 @@ Thanks for considering contributing to Aminal. ## How do terminals even work? - [ELI5 - How does a terminal emulator work?](https://www.reddit.com/r/linuxquestions/comments/3ciful/eli5_how_does_a_terminal_emulator_work/) -- [Xterm Control Sequences](https://www.xfree86.org/4.8.0/ctlseqs.html) +- [Xterm Control Sequences](https://www.xfree86.org/4.8.0/ctlseqs.html) - [VT100 Programmer Information](https://vt100.net/docs/vt100-ug/chapter3.html) +- [VT100 Manual](http://www.bitsavers.org/pdf/dec/terminal/vt100/EK-VT100-UG-001_VT100_User_Guide_Aug78.pdf) ## What can I work on? diff --git a/buffer/buffer.go b/buffer/buffer.go index 29cf4ef..7bad518 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -56,34 +56,18 @@ func (buffer *Buffer) GetScrollOffset() uint { return buffer.scrollLinesFromBottom } +func (buffer *Buffer) HasScrollableRegion() bool { + return buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1 +} + +func (buffer *Buffer) InScrollableRegion() bool { + return buffer.HasScrollableRegion() && uint(buffer.cursorY) >= buffer.topMargin && uint(buffer.cursorY) <= buffer.bottomMargin +} + func (buffer *Buffer) ScrollDown(lines uint16) { defer buffer.emitDisplayChange() - // scrollable region is enabled - if buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1 { - - for c := 0; c < int(lines); c++ { - - for i := buffer.topMargin; i < buffer.bottomMargin; i++ { - above := buffer.getViewLine(uint16(i)) - below := buffer.getViewLine(uint16(i + 1)) - above.cells = below.cells - } - final := buffer.getViewLine(uint16(buffer.bottomMargin)) - - lineIndex := buffer.convertViewLineToRawLine(uint16(buffer.bottomMargin + 1)) - - if lineIndex < uint64(len(buffer.lines)) { - *final = buffer.lines[lineIndex] - } else { - *final = newLine() - } - } - - return - } - if buffer.Height() < int(buffer.ViewHeight()) { return } @@ -98,30 +82,6 @@ func (buffer *Buffer) ScrollUp(lines uint16) { defer buffer.emitDisplayChange() - // scrollable region is enabled - if buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1 { - - for c := 0; c < int(lines); c++ { - - for i := buffer.bottomMargin; i > buffer.topMargin+1; i-- { - below := buffer.getViewLine(uint16(i)) - above := buffer.getViewLine(uint16(i - 1)) - below.cells = above.cells - } - final := buffer.getViewLine(uint16(buffer.topMargin)) - - lineIndex := buffer.convertViewLineToRawLine(uint16(buffer.topMargin - 1)) - - if lineIndex >= 0 && lineIndex < uint64(len(buffer.lines)) { - *final = buffer.lines[lineIndex] - } else { - *final = newLine() - } - } - - return - } - if buffer.Height() < int(buffer.ViewHeight()) { return } @@ -201,6 +161,14 @@ func (buffer *Buffer) CursorLine() uint16 { return buffer.cursorY } +func (buffer *Buffer) TopMargin() uint { + return buffer.topMargin +} + +func (buffer *Buffer) BottomMargin() uint { + return buffer.bottomMargin +} + // translates the cursor line to the raw buffer line func (buffer *Buffer) RawLine() uint64 { return buffer.convertViewLineToRawLine(buffer.cursorY) @@ -231,6 +199,55 @@ func (buffer *Buffer) ViewHeight() uint16 { return buffer.viewHeight } +func (buffer *Buffer) Index() { + + // This sequence causes the active position to move downward one line without changing the column position. + // If the active position is at the bottom margin, a scroll up is performed." + + if buffer.InScrollableRegion() { + + if uint(buffer.cursorY) < buffer.bottomMargin { + buffer.cursorY++ + return + } + + for i := buffer.topMargin; i < uint(buffer.cursorY); i++ { + buffer.lines[i] = buffer.lines[i+1] + } + buffer.lines[buffer.cursorY] = newLine() + + return + } + + if buffer.cursorY >= buffer.ViewHeight()-1 { + buffer.lines = append(buffer.lines, newLine()) + } else { + buffer.cursorY++ + } +} + +func (buffer *Buffer) ReverseIndex() { + if buffer.InScrollableRegion() { + + if uint(buffer.cursorY) > buffer.topMargin { + buffer.cursorY-- + return + } + + for i := buffer.bottomMargin; i > uint(buffer.cursorY); i-- { + buffer.lines[i] = buffer.lines[i-1] + } + buffer.lines[buffer.cursorY] = newLine() + + return + } + + if buffer.cursorY > 0 { + + buffer.cursorY-- + } +} + // Write will write a rune to the terminal at the position of the cursor, and increment the cursor position func (buffer *Buffer) Write(runes ...rune) { @@ -248,16 +265,21 @@ func (buffer *Buffer) Write(runes ...rune) { } line := buffer.getCurrentLine() + if buffer.replaceMode { + for int(buffer.CursorColumn()) >= len(line.cells) { + line.cells = append(line.cells, NewBackgroundCell(buffer.cursorAttr.BgColour)) + } + line.cells[buffer.cursorX].attr = buffer.cursorAttr + line.cells[buffer.cursorX].setRune(r) + buffer.incrementCursorPosition() + continue + } + if buffer.CursorColumn() >= buffer.Width() { // if we're after the line, move to next if buffer.autoWrap { - buffer.cursorX = 0 - if buffer.cursorY >= buffer.ViewHeight()-1 { - buffer.lines = append(buffer.lines, newLine()) - } else { - buffer.cursorY++ - } + buffer.NewLine() newLine := buffer.getCurrentLine() newLine.setWrapped(true) @@ -331,24 +353,7 @@ func (buffer *Buffer) NewLine() { buffer.cursorX = 0 - if (buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1) && uint(buffer.cursorY) == buffer.bottomMargin { - - // scrollable region is enabled - for i := buffer.topMargin; i < buffer.bottomMargin; i++ { - above := buffer.getViewLine(uint16(i)) - below := buffer.getViewLine(uint16(i + 1)) - above.cells = below.cells - } - final := buffer.getViewLine(uint16(buffer.bottomMargin)) - *final = newLine() - return - } - - if buffer.cursorY >= buffer.ViewHeight()-1 { - buffer.lines = append(buffer.lines, newLine()) - } else { - buffer.cursorY++ - } + buffer.Index() } func (buffer *Buffer) MovePosition(x int16, y int16) { @@ -373,6 +378,7 @@ 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()) @@ -381,12 +387,14 @@ func (buffer *Buffer) SetPosition(col uint16, line uint16) { line = buffer.ViewHeight() - 1 //logrus.Errorf("Cannot set cursor position: line %d is outside of the current view height (%d lines)", line, buffer.ViewHeight()) } + buffer.cursorX = col buffer.cursorY = line } func (buffer *Buffer) GetVisibleLines() []Line { lines := []Line{} + for i := buffer.Height() - int(buffer.ViewHeight()); i < buffer.Height(); i++ { y := i - int(buffer.scrollLinesFromBottom) if y >= 0 && y < len(buffer.lines) { diff --git a/main.go b/main.go index 0dca8aa..8531b07 100644 --- a/main.go +++ b/main.go @@ -86,7 +86,7 @@ func main() { // parse this conf := getConfig() - os.Setenv("TERM", "xterm-256color") + os.Setenv("TERM", "xterm-256color") // contraversial! easier than installing terminfo everywhere, but obviously going to be slightly different to xterm functionality, so we'll see... logger, err := getLogger(conf) if err != nil { diff --git a/terminal/ansi.go b/terminal/ansi.go index 089f314..65b78da 100644 --- a/terminal/ansi.go +++ b/terminal/ansi.go @@ -31,18 +31,12 @@ func swallowHandler(n int) func(pty chan rune, terminal *Terminal) error { } func indexHandler(pty chan rune, terminal *Terminal) error { - // @todo is thus right? - // "This sequence causes the active position to move downward one line without changing the column position. If the active position is at the bottom margin, a scroll up is performed." - if terminal.ActiveBuffer().CursorLine() == terminal.ActiveBuffer().ViewHeight()-1 { - terminal.ActiveBuffer().NewLine() - return nil - } - terminal.ActiveBuffer().MovePosition(0, 1) + terminal.ActiveBuffer().Index() return nil } func reverseIndexHandler(pty chan rune, terminal *Terminal) error { - terminal.ActiveBuffer().MovePosition(0, -1) + terminal.ActiveBuffer().ReverseIndex() return nil } @@ -62,6 +56,7 @@ func ansiHandler(pty chan rune, terminal *Terminal) error { handler, ok := ansiSequenceMap[b] if ok { + //terminal.logger.Debugf("Handling ansi sequence %c", b) return handler(pty, terminal) } diff --git a/terminal/csi.go b/terminal/csi.go index 96fa116..b4b6412 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -6,20 +6,206 @@ import ( "strings" ) -var csiSequenceMap = map[rune]csiSequenceHandler{ - 'd': csiLinePositionAbsolute, - 'h': csiSetModeHandler, - 'l': csiResetModeHandler, - 'm': sgrSequenceHandler, - 'r': csiSetMarginsHandler, - 't': csiWindowManipulation, - 'J': csiEraseInDisplayHandler, - 'K': csiEraseInLineHandler, - 'L': csiInsertLinesHandler, - 'P': csiDeleteHandler, - 'S': csiScrollUpHandler, - 'T': csiScrollDownHandler, - 'X': csiEraseCharactersHandler, +type csiSequenceHandler func(params []string, intermediate string, terminal *Terminal) error + +type csiMapping struct { + id rune + handler csiSequenceHandler + description string + expectedParams *expectedParams +} + +type expectedParams struct { + min uint8 + max uint8 +} + +var csiSequences = []csiMapping{ + csiMapping{id: 'd', handler: csiLinePositionAbsolute, expectedParams: &expectedParams{min: 0, max: 1}, description: "Line Position Absolute [row] (default = [1,column]) (VPA)"}, + csiMapping{id: 'h', handler: csiSetModeHandler, expectedParams: &expectedParams{min: 1, max: 1}, description: "Set Mode (SM)"}, + csiMapping{id: 'l', handler: csiResetModeHandler, expectedParams: &expectedParams{min: 1, max: 1}, description: "Reset Mode (RM)"}, + csiMapping{id: 'm', handler: sgrSequenceHandler, description: "Character Attributes (SGR)"}, + csiMapping{id: 'r', handler: csiSetMarginsHandler, expectedParams: &expectedParams{min: 2, max: 2}, description: "Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100"}, + csiMapping{id: 't', handler: csiWindowManipulation, description: "Window manipulation"}, + csiMapping{id: 'J', handler: csiEraseInDisplayHandler, description: "Erase in Display (ED), VT100"}, + csiMapping{id: 'K', handler: csiEraseInLineHandler, description: "Erase in Line (EL), VT100"}, + csiMapping{id: 'L', handler: csiInsertLinesHandler, description: "Insert Ps Line(s) (default = 1) (IL)"}, + csiMapping{id: 'P', handler: csiDeleteHandler, description: " Delete Ps Character(s) (default = 1) (DCH)"}, + csiMapping{id: 'S', handler: csiScrollUpHandler, description: "Scroll up Ps lines (default = 1) (SU), VT420, ECMA-48"}, + csiMapping{id: 'T', handler: csiScrollDownHandler, description: "Scroll down Ps lines (default = 1) (SD), VT420"}, + csiMapping{id: 'X', handler: csiEraseCharactersHandler, description: "Erase Ps Character(s) (default = 1) (ECH"}, + csiMapping{id: 'A', handler: csiCursorUpHandler, description: "Cursor Up Ps Times (default = 1) (CUU)"}, + csiMapping{id: 'B', handler: csiCursorDownHandler, description: "Cursor Down Ps Times (default = 1) (CUD)"}, + csiMapping{id: 'C', handler: csiCursorForwardHandler, description: "Cursor Forward Ps Times (default = 1) (CUF)"}, + csiMapping{id: 'D', handler: csiCursorBackwardHandler, description: "Cursor Backward Ps Times (default = 1) (CUB)"}, + csiMapping{id: 'E', handler: csiCursorNextLineHandler, description: "Cursor Next Line Ps Times (default = 1) (CNL)"}, + csiMapping{id: 'F', handler: csiCursorPrecedingLineHandler, description: "Cursor Preceding Line Ps Times (default = 1) (CPL)"}, + csiMapping{id: 'G', handler: csiCursorCharacterAbsoluteHandler, description: "Cursor Character Absolute [column] (default = [row,1]) (CHA)"}, + csiMapping{id: 'H', handler: csiCursorPositionHandler, description: "Cursor Position [row;column] (default = [1,1]) (CUP)"}, + csiMapping{id: 'f', handler: csiCursorPositionHandler, description: "Horizontal and Vertical Position [row;column] (default = [1,1]) (HVP)"}, +} + +func csiHandler(pty chan rune, terminal *Terminal) error { + var final rune + var b rune + param := "" + intermediate := "" +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 >= 0x40 && b <= 0x7e: + final = b + break CSI + } + } + + params := strings.Split(param, ";") + if param == "" { + params = []string{} + } + + for _, sequence := range csiSequences { + if sequence.id == final { + if sequence.expectedParams != nil && (uint8(len(params)) < sequence.expectedParams.min || uint8(len(params)) > sequence.expectedParams.max) { + continue + } + terminal.logger.Debugf("CSI 0x%02X (ESC[%s%s%s) %s", final, param, intermediate, string(final), sequence.description) + err := sequence.handler(params, intermediate, terminal) + terminal.logger.Debugf("After CSI, state: Col %d, Line %d, Top: %d, Bottom %d", terminal.ActiveBuffer().CursorColumn(), terminal.ActiveBuffer().CursorLine(), terminal.ActiveBuffer().TopMargin(), terminal.ActiveBuffer().BottomMargin()) + return err + } + } + + return fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final)) + +} + +func csiCursorUpHandler(params []string, intermediate string, terminal *Terminal) error { + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + terminal.ActiveBuffer().MovePosition(0, -int16(distance)) + return nil +} + +func csiCursorDownHandler(params []string, intermediate string, terminal *Terminal) error { + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + terminal.ActiveBuffer().MovePosition(0, int16(distance)) + return nil +} + +func csiCursorForwardHandler(params []string, intermediate string, terminal *Terminal) error { + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + terminal.ActiveBuffer().MovePosition(int16(distance), 0) + return nil +} + +func csiCursorBackwardHandler(params []string, intermediate string, terminal *Terminal) error { + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + terminal.ActiveBuffer().MovePosition(-int16(distance), 0) + return nil +} + +func csiCursorNextLineHandler(params []string, intermediate string, terminal *Terminal) error { + + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + terminal.ActiveBuffer().MovePosition(0, int16(distance)) + terminal.ActiveBuffer().SetPosition(0, terminal.ActiveBuffer().CursorLine()) + return nil +} + +func csiCursorPrecedingLineHandler(params []string, intermediate string, terminal *Terminal) error { + + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + terminal.ActiveBuffer().MovePosition(0, -int16(distance)) + terminal.ActiveBuffer().SetPosition(0, terminal.ActiveBuffer().CursorLine()) + return nil +} + +func csiCursorCharacterAbsoluteHandler(params []string, intermediate string, terminal *Terminal) error { + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil || params[0] == "" { + distance = 1 + } + } + + terminal.ActiveBuffer().SetPosition(uint16(distance-1), terminal.ActiveBuffer().CursorLine()) + return nil +} + +func csiCursorPositionHandler(params []string, intermediate string, terminal *Terminal) error { + x, y := 1, 1 + if len(params) == 2 { + var err error + if params[0] != "" { + y, err = strconv.Atoi(string(params[0])) + if err != nil { + y = 1 + } + } + if params[1] != "" { + x, err = strconv.Atoi(string(params[1])) + if err != nil { + x = 1 + } + } + } + + terminal.ActiveBuffer().SetPosition(uint16(x-1), uint16(y-1)) + return nil } func csiScrollUpHandler(params []string, intermediate string, terminal *Terminal) error { @@ -52,7 +238,10 @@ func csiInsertLinesHandler(params []string, intermediate string, terminal *Termi } } terminal.logger.Debugf("Inserting %d lines", count) - return fmt.Errorf("Not supported") + for i := 0; i < count; i++ { + terminal.ActiveBuffer().Index() + } + return nil } func csiScrollDownHandler(params []string, intermediate string, terminal *Terminal) error { @@ -76,6 +265,11 @@ func csiScrollDownHandler(params []string, intermediate string, terminal *Termin func csiSetMarginsHandler(params []string, intermediate string, terminal *Terminal) error { top := 1 bottom := int(terminal.ActiveBuffer().ViewHeight()) + + if len(params) > 2 { + return fmt.Errorf("Not set margins") + } + if len(params) > 0 { var err error top, err = strconv.Atoi(params[0]) @@ -86,10 +280,7 @@ func csiSetMarginsHandler(params []string, intermediate string, terminal *Termin if len(params) > 1 { var err error bottom, err = strconv.Atoi(params[1]) - if err != nil { - bottom = 1 - } - if bottom > int(terminal.ActiveBuffer().ViewHeight()) { + if err != nil || bottom > int(terminal.ActiveBuffer().ViewHeight()) { bottom = int(terminal.ActiveBuffer().ViewHeight()) } } @@ -127,8 +318,7 @@ func csiSetModeHandler(params []string, intermediate string, terminal *Terminal) } func csiWindowManipulation(params []string, intermediate string, terminal *Terminal) error { - // @todo this - return nil + return fmt.Errorf("Window manipulation is not yet supported") } func csiLinePositionAbsolute(params []string, intermediate string, terminal *Terminal) error { @@ -146,153 +336,6 @@ func csiLinePositionAbsolute(params []string, intermediate string, terminal *Ter return nil } -type csiSequenceHandler func(params []string, intermediate string, terminal *Terminal) error - -// CSI: Control Sequence Introducer [ -func csiHandler(pty chan rune, terminal *Terminal) error { - var final rune - var b rune - var err error - param := "" - intermediate := "" -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 >= 0x40 && b <= 0x7e: - final = b - break CSI - } - } - - params := strings.Split(param, ";") - - handler, ok := csiSequenceMap[final] - if ok { - err = handler(params, intermediate, terminal) - } else { - - switch final { - case 'A': - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil { - distance = 1 - } - } - terminal.ActiveBuffer().MovePosition(0, -int16(distance)) - case 'B': - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil { - distance = 1 - } - } - - terminal.ActiveBuffer().MovePosition(0, int16(distance)) - case 'C': - - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil { - distance = 1 - } - } - - terminal.ActiveBuffer().MovePosition(int16(distance), 0) - - case 'D': - - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil { - distance = 1 - } - } - - terminal.ActiveBuffer().MovePosition(-int16(distance), 0) - - case 'E': - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil { - distance = 1 - } - } - - terminal.ActiveBuffer().MovePosition(0, int16(distance)) - terminal.ActiveBuffer().SetPosition(0, terminal.ActiveBuffer().CursorLine()) - - case 'F': - - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil { - distance = 1 - } - } - terminal.ActiveBuffer().MovePosition(0, -int16(distance)) - terminal.ActiveBuffer().SetPosition(0, terminal.ActiveBuffer().CursorLine()) - - case 'G': - - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || params[0] == "" { - distance = 1 - } - } - - terminal.ActiveBuffer().SetPosition(uint16(distance-1), terminal.ActiveBuffer().CursorLine()) - - case 'H', 'f': - - x, y := 1, 1 - if len(params) == 2 { - var err error - if params[0] != "" { - y, err = strconv.Atoi(string(params[0])) - if err != nil { - y = 1 - } - } - if params[1] != "" { - x, err = strconv.Atoi(string(params[1])) - if err != nil { - x = 1 - } - } - } - - terminal.ActiveBuffer().SetPosition(uint16(x-1), uint16(y-1)) - - default: - err = fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final)) - } - } - fmt.Printf("CSI 0x%02X (ESC[%s%s%s)\n", final, param, intermediate, string(final)) - return err -} - func csiDeleteHandler(params []string, intermediate string, terminal *Terminal) error { n := 1 if len(params) >= 1 { diff --git a/terminal/output.go b/terminal/output.go index 7f38eac..e605362 100644 --- a/terminal/output.go +++ b/terminal/output.go @@ -91,6 +91,7 @@ func (terminal *Terminal) processInput(ctx context.Context, pty chan rune) { handler, ok := escapeSequenceMap[b] if 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) } diff --git a/terminal/sgr.go b/terminal/sgr.go index 7e08c66..6a43760 100644 --- a/terminal/sgr.go +++ b/terminal/sgr.go @@ -11,12 +11,12 @@ import ( func sgrSequenceHandler(params []string, intermediate string, terminal *Terminal) error { if len(params) == 0 { - return nil + params = []string{"0"} } for i := range params { switch params[i] { - case "00", "0", "": + case "00", "0": attr := terminal.ActiveBuffer().CursorAttr() *attr = buffer.CellAttributes{ FgColour: terminal.config.ColourScheme.Foreground,