diff --git a/matrix/automatrix.go b/matrix/automatrix.go new file mode 100644 index 0000000..7410aa9 --- /dev/null +++ b/matrix/automatrix.go @@ -0,0 +1,69 @@ +package matrix + +// AutoMatrix -- automatically growing matrix +type AutoMatrix struct { + lines [][]rune +} + +// NewAutoMatrix creates a new auto-matrix +func NewAutoMatrix() *AutoMatrix { + m := &AutoMatrix{ + lines: make([][]rune, 0), + } + return m +} + +// ExtractFrom extracts from (x1, y1) until the end +func (matrix *AutoMatrix) ExtractFrom(x1, y1 int) []rune { + result := make([]rune, 0) + y := y1 + for y < len(matrix.lines) { + if matrix.lines[y] != nil { + xMin := 0 + if y == y1 { + xMin = x1 + } + result = append(result, matrix.lines[y][xMin:]...) + } + y++ + } + return result +} + +// Extract from (x1, y1) until (x2, y2) +func (matrix *AutoMatrix) Extract(x1, y1, x2, y2 int) []rune { + result := make([]rune, 0) + y := y1 + for y <= y2 && y < len(matrix.lines) { + xMin := 0 + if y == y1 { + xMin = x1 + } + if matrix.lines[y] != nil { + xMax := x2 + if y != y2 { + xMax = len(matrix.lines[y]) - 1 + } + result = append(result, matrix.lines[y][xMin:xMax]...) + } + y++ + } + return result +} + +func (matrix *AutoMatrix) setAtLine(value rune, x int, line []rune) []rune { + if x >= len(line) { + line = append(line, make([]rune, x-len(line)+1)...) + } + line[x] = value + return line +} + +// SetAt sets a value at (x, y) growing the matrix as necessary +func (matrix *AutoMatrix) SetAt(value rune, x int, y int) { + + if y >= len(matrix.lines) { + matrix.lines = append(matrix.lines, make([][]rune, y-len(matrix.lines)+1)...) + } + matrix.lines[y] = matrix.setAtLine(value, x, matrix.lines[y]) +} diff --git a/terminal/csi.go b/terminal/csi.go index 7ef6781..8f16aea 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -49,11 +49,17 @@ var csiSequences = []csiMapping{ {id: '@', handler: csiInsertBlankCharactersHandler, description: "Insert Ps (Blank) Character(s) (default = 1) (ICH)"}, } -func csiHandler(pty chan rune, terminal *Terminal) error { - var final rune +type runeRange struct { + min rune + max rune +} + +var csiTerminators = runeRange{0x40, 0x7e} + +func loadCSI(pty chan rune) (final rune, param string, intermediate string) { var b rune - param := "" - intermediate := "" + param = "" + intermediate = "" CSI: for { b = <-pty @@ -63,16 +69,27 @@ CSI: case b >= 0x20 && b <= 0x2F: //intermediate? useful? intermediate += string(b) - case b >= 0x40 && b <= 0x7e: + case b >= csiTerminators.min && b <= csiTerminators.max: final = b break CSI } } - params := strings.Split(param, ";") - if param == "" { + return final, param, intermediate +} + +func splitParams(paramString string) []string { + params := strings.Split(paramString, ";") + if paramString == "" { params = []string{} } + return params +} + +func csiHandler(pty chan rune, terminal *Terminal) error { + final, param, intermediate := loadCSI(pty) + + params := splitParams(param) for _, sequence := range csiSequences { if sequence.id == final { @@ -221,8 +238,8 @@ func csiCursorCharacterAbsoluteHandler(params []string, intermediate string, ter return nil } -func csiCursorPositionHandler(params []string, intermediate string, terminal *Terminal) error { - x, y := 1, 1 +func parseCursorPosition(params []string) (x, y int) { + x, y = 1, 1 if len(params) == 2 { var err error if params[0] != "" { @@ -238,6 +255,11 @@ func csiCursorPositionHandler(params []string, intermediate string, terminal *Te } } } + return x, y +} + +func csiCursorPositionHandler(params []string, intermediate string, terminal *Terminal) error { + x, y := parseCursorPosition(params) terminal.ActiveBuffer().SetPosition(uint16(x-1), uint16(y-1)) return nil diff --git a/terminal/sixel.go b/terminal/sixel.go index 1faefbd..65c00a7 100644 --- a/terminal/sixel.go +++ b/terminal/sixel.go @@ -7,32 +7,130 @@ import ( "math" "strings" + "github.com/liamg/aminal/matrix" "github.com/liamg/aminal/sixel" ) -func sixelHandler(pty chan rune, terminal *Terminal) error { - - data := []rune{} +type boolFormRuneFunc func(rune) bool +func swallowByFunction(pty chan rune, isTerminator boolFormRuneFunc) { for { b := <-pty - if b == 0x1b { // terminated by ESC bell or ESC \ + if isTerminator(b) { + break + } + } +} + +func filter(src []rune) []rune { + result := make([]rune, 0, len(src)) + for _, v := range src { + if v >= 33 { + result = append(result, v) + } + } + return result +} + +func sixelHandler(pty chan rune, terminal *Terminal) error { + debug := "" + + // data := []rune{} + + // track for Windows formatting workaround + scrollOffset := uint16(terminal.GetScrollOffset()) + x := terminal.ActiveBuffer().CursorColumn() + 2 // reserve two bytes for Sixel prefix (ESC P) + y := terminal.ActiveBuffer().CursorLine() + scrollingLine := terminal.ActiveBuffer().ViewHeight() - 1 + xStart := x + yStartWithOffset := y + scrollOffset + matrix := matrix.NewAutoMatrix() // a simplified version of Buffer + for { + b := <-pty + if b == 0x1b { t := <-pty + if t == '[' { // Windows injected a CSI sequence + final, param, _ := loadCSI(pty) + + if final == 'H' { + // position cursor + params := splitParams(param) + { + xT, yT := parseCursorPosition(params) // 1 based + x = uint16(xT - 1) // 0 based + y = uint16(yT - 1) // 0 based + } + } + debug += "[CSI " + param + string(final) + "]" + continue + } + if t == ']' { // Windows injected an OSC sequence + // TODO: pass through as if it came via normal stream + swallowByFunction(pty, terminal.IsOSCTerminator) + debug += "[OSC]" + continue + } + // if re-drawing a region beforethe start of sixel sequencce, + // ignore all possible ESC pairs (including ESC P) + if y+scrollOffset < yStartWithOffset || (y+scrollOffset == yStartWithOffset && x < xStart) { + x += 2 + continue + } if t != 0x07 && t != 0x5c { return fmt.Errorf("Incorrect terminator in sixel sequence: 0x%02X [%c]", t, t) } - break + break // terminated by ESC bell or ESC \ } - if b >= 33 { - data = append(data, b) + + if b == 0x0d { + // skip + } else if b == 0x0a { + terminal.logger.Debugf("Sixel line: %s", debug) + debug = "" + if y == scrollingLine { + scrollOffset++ + } else { + y++ + } + x = 0 + } else if y+scrollOffset < yStartWithOffset || (y+scrollOffset == yStartWithOffset && x < xStart) { + x++ + } else if b < 32 { + x++ // always? + } else { + debug += string(b) + matrix.SetAt(b, int(x), int(y+scrollOffset-yStartWithOffset)) + x++ } + /* + if b >= 33 { + data = append(data, b) + } + */ } - six, err := sixel.ParseString(string(data)) + if debug != "" { + terminal.logger.Debugf("Sixel last line: %s", debug) + } + + newData := matrix.ExtractFrom(int(xStart), 0) // , int(x), int(y+scrollOffset)) + + terminal.logger.Debugf("Sixel data: %s", string(newData)) + + filteredData := filter(newData) + + six, err := sixel.ParseString(string(filteredData)) + if err != nil { return fmt.Errorf("Failed to parse sixel data: %s", err) } + drawSixel(six, terminal) + + return nil +} + +func drawSixel(six *sixel.Sixel, terminal *Terminal) { originalImage := six.RGBA() w := originalImage.Bounds().Size().X @@ -70,6 +168,4 @@ func sixelHandler(pty chan rune, terminal *Terminal) error { cell.SetImage(rgba) } } - - return nil }