From 79be2cf8cb5692721ecfc33032c5fcf258fc9de9 Mon Sep 17 00:00:00 2001 From: Liam Galvin Date: Mon, 6 Aug 2018 17:53:27 +0100 Subject: [PATCH] bleurgh --- buffer/buffer.go | 79 ++++++++++++++++ buffer/cell.go | 6 +- buffer/line.go | 24 +++++ gui/gui.go | 2 +- terminal/csi.go | 156 +++++++++++++------------------- terminal/delete.go | 38 -------- terminal/escapes.go | 29 ++---- terminal/terminal.go | 208 ++----------------------------------------- 8 files changed, 181 insertions(+), 361 deletions(-) diff --git a/buffer/buffer.go b/buffer/buffer.go index 23fecb1..be97534 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -1,6 +1,8 @@ package buffer import ( + "fmt" + "github.com/sirupsen/logrus" ) @@ -24,6 +26,18 @@ func NewBuffer(viewCols uint16, viewLines uint16) *Buffer { return b } +func (buffer *Buffer) attachDisplayChangeHandler(handler func()) { + +} + +func (buffer *Buffer) attachLineChangeHandler(handler func(line uint16)) { + +} + +func (buffer *Buffer) attachCellChangeHandler(handler func(col uint16, line uint16)) { + +} + // Column returns cursor column func (buffer *Buffer) CursorColumn() uint16 { return buffer.cursorX @@ -175,6 +189,71 @@ func (buffer *Buffer) Clear() { buffer.SetPosition(0, 0) } +func (buffer *Buffer) getCurrentLine() (*Line, error) { + + if int(buffer.RawLine()) < len(buffer.lines) { + return &buffer.lines[buffer.RawLine()], nil + } + + return nil, fmt.Errorf("Line %d does not exist", buffer.cursorY) +} + +func (buffer *Buffer) EraseLine() { + line, err := buffer.getCurrentLine() + if err != nil { + return + } + line.cells = []Cell{} +} + +func (buffer *Buffer) EraseLineToCursor() { + line, err := buffer.getCurrentLine() + if err != nil { + return + } + for i := 0; i <= int(buffer.cursorX); i++ { + if i < len(line.cells) { + line.cells[i].erase() + } + } +} + +func (buffer *Buffer) EraseLineAfterCursor() { + line, err := buffer.getCurrentLine() + if err != nil { + return + } + for i := int(buffer.cursorX + 1); i < len(line.cells); i++ { + line.cells[i].erase() + } +} + +func (buffer *Buffer) EraseDisplayAfterCursor() { + line, err := buffer.getCurrentLine() + if err != nil { + return + } + line.cells = line.cells[:buffer.cursorX] + for i := int(buffer.RawLine() + 1); i < buffer.Height(); i++ { + if i < len(buffer.lines) { + buffer.lines[i].cells = []Cell{} + } + } +} + +func (buffer *Buffer) EraseDisplayToCursor() { + line, err := buffer.getCurrentLine() + if err != nil { + return + } + line.cells = line.cells[buffer.cursorX+1:] + for i := 0; i < int(buffer.RawLine()); i++ { + if i < len(buffer.lines) { + buffer.lines[i].cells = []Cell{} + } + } +} + func (buffer *Buffer) ResizeView(width uint16, height uint16) { buffer.viewWidth = width buffer.viewHeight = height diff --git a/buffer/cell.go b/buffer/cell.go index 6ae66ba..8bd8156 100644 --- a/buffer/cell.go +++ b/buffer/cell.go @@ -21,7 +21,11 @@ func newCell() Cell { return Cell{} } +func (cell *Cell) erase() { + cell.setRune(0) +} + func (cell *Cell) setRune(r rune) { cell.r = r - cell.hasContent = true + cell.hasContent = r > 0 } diff --git a/buffer/line.go b/buffer/line.go index 08c1914..2284645 100644 --- a/buffer/line.go +++ b/buffer/line.go @@ -23,3 +23,27 @@ func (line *Line) String() string { } return string(runes) } + +// @todo test these (ported from legacy) ------------------ +func (line *Line) CutCellsAfter(n int) []Cell { + cut := line.cells[n:] + line.cells = line.cells[:n] + return cut +} + +func (line *Line) CutCellsFromBeginning(n int) []Cell { + if n > len(line.cells) { + n = len(line.cells) + } + cut := line.cells[:n] + line.cells = line.cells[n:] + return cut +} + +func (line *Line) CutCellsFromEnd(n int) []Cell { + cut := line.cells[len(line.cells)-n:] + line.cells = line.cells[:len(line.cells)-n] + return cut +} + +// ------------------------------------------------------- diff --git a/gui/gui.go b/gui/gui.go index 54e1610..6026f74 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -285,7 +285,7 @@ func (gui *GUI) Render() error { } gui.font.SetColor(1, 0.2, 0.2, 0.5) - gui.font.Printf(100, 300, 1.5, "%#v %s", gui.terminal.GetPosition(), gui.terminal.GetLineString()) + gui.font.Printf(100, 300, 1.5, "%#v", gui.terminal.GetPosition()) } diff --git a/terminal/csi.go b/terminal/csi.go index 01c3951..61b63f4 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -8,6 +8,9 @@ import ( var csiSequenceMap = map[rune]csiSequenceHandler{ 'm': sgrSequenceHandler, + 'P': csiDeleteHandler, + 'J': csiEraseInDisplayHandler, + 'K': csiEraseInLineHandler, } type csiSequenceHandler func(params []string, intermediate string, terminal *Terminal) error @@ -168,101 +171,6 @@ CSI: terminal.position.Col = x - 1 terminal.position.Line = y - 1 - case 'J': - - n := "0" - if len(params) > 0 { - n = params[0] - } - - switch n { - - case "0", "": - line := terminal.getBufferedLine(terminal.position.Line) - if line != nil { - line.Cells = line.Cells[:terminal.position.Col] - } - _, h := terminal.GetSize() - for i := terminal.position.Line + 1; i < h; i++ { - line := terminal.getBufferedLine(i) - if line != nil { - line.Cells = []Cell{} - } - } - case "1": - line := terminal.getBufferedLine(terminal.position.Line) - if line != nil { - for i := 0; i <= terminal.position.Col; i++ { - if i < len(line.Cells) { - line.Cells[i].r = 0 - } - } - } - for i := 0; i < terminal.position.Line; i++ { - line := terminal.getBufferedLine(i) - if line != nil { - line.Cells = []Cell{} - } - } - - case "2": - _, h := terminal.GetSize() - for i := 0; i < h; i++ { - line := terminal.getBufferedLine(i) - if line != nil { - line.Cells = []Cell{} - } - } - case "3": - terminal.lines = []Line{} - - default: - return fmt.Errorf("Unknown CSI ED sequence: %s", n) - } - - case 'K': // K - EOL - Erase to end of line - n := "0" - if len(params) > 0 { - n = params[0] - } - - switch n { - case "0", "": - line := terminal.getBufferedLine(terminal.position.Line) - if line != nil { - line.Cells = line.Cells[:terminal.position.Col] - } - case "1": - line := terminal.getBufferedLine(terminal.position.Line) - if line != nil { - for i := 0; i <= terminal.position.Col; i++ { - if i < len(line.Cells) { - line.Cells[i].r = 0 - } - } - } - case "2": - line := terminal.getBufferedLine(terminal.position.Line) - if line != nil { - line.Cells = []Cell{} - } - default: - return fmt.Errorf("Unsupported EL: %s", n) - } - - case 'P': // delete - - n := 1 - if len(params) >= 1 { - var err error - n, err = strconv.Atoi(params[0]) - if err != nil { - n = 1 - } - } - - _ = terminal.delete(n) - default: switch param + intermediate + string(final) { case "?25h": @@ -281,3 +189,61 @@ CSI: //terminal.logger.Debugf("Received CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final)) return nil } + +func csiDeleteHandler(params []string, intermediate string, terminal *Terminal) error { + n := 1 + if len(params) >= 1 { + var err error + n, err = strconv.Atoi(params[0]) + if err != nil { + n = 1 + } + } + _ = n + return nil +} + +// CSI Ps J +func csiEraseInDisplayHandler(params []string, intermediate string, terminal *Terminal) error { + n := "0" + if len(params) > 0 { + n = params[0] + } + + switch n { + + case "0", "": + terminal.buffer.EraseDisplayAfterCursor() + case "1": + terminal.buffer.EraseDisplayToCursor() + case "2": + terminal.Clear() + case "3": + terminal.Clear() + + default: + return fmt.Errorf("Unsupported ED: CSI %s J", n) + } + + return nil +} + +// CSI Ps K +func csiEraseInLineHandler(params []string, intermediate string, terminal *Terminal) error { + n := "0" + if len(params) > 0 { + n = params[0] + } + + switch n { + case "0", "": //erase adter cursor + terminal.buffer.EraseLineAfterCursor() + case "1": // erase to cursor inclusive + terminal.buffer.EraseLineToCursor() + case "2": // erase entire + terminal.buffer.EraseLine() + default: + return fmt.Errorf("Unsupported EL: CSI %s K", n) + } + return nil +} diff --git a/terminal/delete.go b/terminal/delete.go index d46da82..10009a5 100644 --- a/terminal/delete.go +++ b/terminal/delete.go @@ -1,39 +1 @@ package terminal - -import ( - "fmt" -) - -func (terminal *Terminal) delete(n int) error { - if len(terminal.lines) <= terminal.position.Line { - return fmt.Errorf("Cannot delete character at current position - line does not exist") - } - line := &terminal.lines[terminal.position.Line] - - if terminal.position.Col >= len(line.Cells) { - return fmt.Errorf("Line not long enough to delete anything") - } - - for terminal.position.Col+n > len(line.Cells) { - n-- - } - after := line.Cells[terminal.position.Col+n:] - before := line.Cells[:terminal.position.Col] - - line.Cells = append(before, after...) - - // @todo rewrap lines here - // so if line overflows and then we delete characters from beginnign of the line - - return nil -} - -// @todo remove this debug func -func (terminal *Terminal) GetLineString() string { - if len(terminal.lines) <= terminal.position.Line { - return "" - } - line := &terminal.lines[terminal.position.Line] - - return line.String() -} diff --git a/terminal/escapes.go b/terminal/escapes.go index c5c4a19..a62976d 100644 --- a/terminal/escapes.go +++ b/terminal/escapes.go @@ -19,8 +19,6 @@ func (terminal *Terminal) processInput(ctx context.Context, buffer chan rune) { // https://en.wikipedia.org/wiki/ANSI_escape_code - lineOverflow := false - for { select { @@ -41,41 +39,24 @@ func (terminal *Terminal) processInput(ctx context.Context, buffer chan rune) { } if b != 0x0d { - lineOverflow = false + //lineOverflow = false } switch b { case 0x0a: - - _, h := terminal.GetSize() - if terminal.position.Line+1 >= h { - terminal.lines = append(terminal.lines, NewLine()) - } else { - terminal.position.Line++ - } - + terminal.buffer.NewLine() case 0x0d: - if terminal.position.Col == 0 && terminal.position.Line > 0 && lineOverflow { - terminal.position.Line-- - terminal.logger.Debugf("Swallowing forced new line for CR") - lineOverflow = false - } - terminal.position.Col = 0 - + terminal.buffer.SetPosition(0, terminal.buffer.CursorLine()) case 0x08: // backspace - terminal.position.Col-- - if terminal.position.Col < 0 { - terminal.position.Col = 0 - } + terminal.buffer.MovePosition(-1, 0) case 0x07: // @todo ring bell default: // render character at current location // fmt.Printf("%s\n", string([]byte{b})) if b >= 0x20 { - terminal.writeRune(b) - lineOverflow = terminal.position.Col == 0 + terminal.buffer.Write(b) } else { terminal.logger.Error("Non-readable rune received: 0x%X", b) } diff --git a/terminal/terminal.go b/terminal/terminal.go index 1eeabbc..045f0da 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -10,11 +10,12 @@ import ( "syscall" "unsafe" + "gitlab.com/liamg/raft/buffer" "go.uber.org/zap" ) type Terminal struct { - lines []Line // lines, where 0 is earliest, n is latest + buffer *buffer.Buffer position Position // line and col lock sync.Mutex pty *os.File @@ -33,51 +34,6 @@ type Line struct { wrapped bool } -func NewLine() Line { - return Line{ - Cells: []Cell{}, - } -} - -func (line *Line) String() string { - s := "" - for _, c := range line.Cells { - s += string(c.r) - } - return s -} - -func (line *Line) CutCellsAfter(n int) []Cell { - cut := line.Cells[n:] - line.Cells = line.Cells[:n] - return cut -} - -func (line *Line) CutCellsFromBeginning(n int) []Cell { - if n > len(line.Cells) { - n = len(line.Cells) - } - cut := line.Cells[:n] - line.Cells = line.Cells[n:] - return cut -} - -func (line *Line) CutCellsFromEnd(n int) []Cell { - cut := line.Cells[len(line.Cells)-n:] - line.Cells = line.Cells[:len(line.Cells)-n] - return cut -} - -func (line *Line) GetRenderedLength() int { - l := 0 - for x, c := range line.Cells { - if c.r > 0 { - l = x - } - } - return l -} - type Winsize struct { Height uint16 Width uint16 @@ -98,9 +54,7 @@ func New(pty *os.File, logger *zap.SugaredLogger, colourScheme ColourScheme) *Te } return &Terminal{ - lines: []Line{ - NewLine(), - }, + buffer: buffer.NewBuffer(0, 0), pty: pty, logger: logger, onUpdate: []func(){}, @@ -166,20 +120,6 @@ func (terminal *Terminal) Write(data []byte) error { return err } -// we have thousands of lines of output. if the terminal is X lines high, we just want to lookat the most recent X lines to render (unless scroll etc) -func (terminal *Terminal) getBufferedLine(line int) *Line { - - if len(terminal.lines) >= int(terminal.size.Height) { - line = len(terminal.lines) - int(terminal.size.Height) + line - } - - if line < 0 || line >= len(terminal.lines) { - return nil - } - - return &terminal.lines[line] -} - // Read needs to be run on a goroutine, as it continually reads output to set on the terminal func (terminal *Terminal) Read() error { @@ -206,85 +146,8 @@ func (terminal *Terminal) Read() error { return nil } -func (terminal *Terminal) writeRune(r rune) { - w, h := terminal.GetSize() - wrap := false - if terminal.position.Col >= w { - terminal.position.Col = 0 - terminal.position.Line++ - wrap = true - for terminal.position.Line >= h { - terminal.position.Line-- - line := NewLine() - line.wrapped = true - terminal.lines = append(terminal.lines, line) - } - } else { - for terminal.position.Line >= h { - terminal.position.Line-- - //terminal.lines = append(terminal.lines, NewLine()) - } - } - terminal.setRuneAtPos(terminal.position, r, wrap) - terminal.incrementPosition() - -} - func (terminal *Terminal) Clear() { - // @todo actually should just add a bunch of newlines? - for i := 0; i < int(terminal.size.Height); i++ { - terminal.lines = append(terminal.lines, NewLine()) - } - terminal.SetPosition(0, 0) -} - -func (terminal *Terminal) GetCellAtPos(pos Position) (*Cell, error) { - - if int(terminal.size.Height) <= pos.Line { - terminal.logger.Errorf("Line %d does not exist", pos.Line) - return nil, fmt.Errorf("Line %d does not exist", pos.Line) - } - - if int(terminal.size.Width) <= pos.Col { - terminal.logger.Errorf("Col %d does not exist", pos.Col) - return nil, fmt.Errorf("Col %d does not exist", pos.Col) - } - - line := terminal.getBufferedLine(pos.Line) - if line == nil { - return nil, fmt.Errorf("Line missing") - } - for pos.Col >= len(line.Cells) { - line.Cells = append(line.Cells, terminal.NewCell()) - } - return &line.Cells[pos.Col], nil -} - -func (terminal *Terminal) setRuneAtPos(pos Position, r rune, wrap bool) error { - - if int(terminal.size.Width) <= pos.Col { - terminal.logger.Errorf("Col %d does not exist", pos.Col) - return fmt.Errorf("Col %d does not exist", pos.Col) - } - - for terminal.position.Line >= len(terminal.lines) { - nl := NewLine() - nl.wrapped = wrap - terminal.lines = append(terminal.lines, nl) - } - - line := terminal.getBufferedLine(pos.Line) - if line == nil { - return fmt.Errorf("Impossible?") - } - - for pos.Col >= len(line.Cells) { - line.Cells = append(line.Cells, terminal.NewCell()) - } - - line.Cells[pos.Col].attr = terminal.cellAttr - line.Cells[pos.Col].r = r - return nil + terminal.buffer.Clear() } func (terminal *Terminal) GetSize() (int, int) { @@ -295,70 +158,11 @@ func (terminal *Terminal) SetSize(newCols int, newLines int) error { terminal.lock.Lock() defer terminal.lock.Unlock() - oldCols := int(terminal.size.Width) - oldLines := int(terminal.size.Height) - - if oldLines > 0 && oldCols > 0 { // only bother resizing content if there is some - if newCols < oldCols { // if the width decreased, we need to do some line trimming - - for l := range terminal.lines { - if terminal.lines[l].GetRenderedLength() > newCols { - cells := terminal.lines[l].CutCellsAfter(newCols) - line := Line{ - Cells: cells, - wrapped: true, - } - terminal.lines = append(terminal.lines[:l+1], append([]Line{line}, terminal.lines[l+1:]...)...) - if terminal.getPosition().Line > l { - terminal.position.Line++ - } else if terminal.getPosition().Line == l { - if terminal.getPosition().Col >= newCols { - //terminal.position.Line++ - } - } - } - } - - } else if newCols > oldCols { // if width increased, we need to potentially unwrap some lines - for l := 0; l < len(terminal.lines); l++ { - if terminal.lines[l].GetRenderedLength() < newCols { // there is space here to unwrap a line if needed - if l+1 < len(terminal.lines) { - if terminal.lines[l+1].wrapped { - wrapSize := newCols - terminal.lines[l].GetRenderedLength() - cells := terminal.lines[l+1].CutCellsFromBeginning(wrapSize) - terminal.lines[l].Cells = append(terminal.lines[l].Cells, cells...) - if terminal.lines[l+1].GetRenderedLength() == 0 { - // remove line - terminal.lines = append(terminal.lines[:l+1], terminal.lines[l+2:]...) - if terminal.getPosition().Line >= l+1 { - //terminal.position.Line-- - } - } - } - } - } - } - - } - - if terminal.position.Line >= newLines { - terminal.position.Line = newLines - 1 - } else { - linesFromEnd := oldLines - terminal.position.Line - terminal.position.Line = newLines - linesFromEnd - if terminal.position.Line >= len(terminal.lines) { - terminal.position.Line = len(terminal.lines) - 1 - } - } - if terminal.position.Line < 0 { - terminal.position.Line = 0 - } - - } - terminal.size.Width = uint16(newCols) terminal.size.Height = uint16(newLines) + terminal.buffer.ResizeView(terminal.size.Width, terminal.size.Height) + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(terminal.pty.Fd()), uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(&terminal.size))) if err != 0 {