diff --git a/terminal/ansi.go b/terminal/ansi.go new file mode 100644 index 0000000..cc8c3b2 --- /dev/null +++ b/terminal/ansi.go @@ -0,0 +1,99 @@ +package terminal + +var ansiSequenceMap = map[rune]escapeSequenceHandler{ + '[': csiHandler, +} + +func ansiHandler(buffer chan rune, terminal *Terminal) error { + // if the byte is an escape character, read the next byte to determine which one + b := <-buffer + + handler, ok := ansiSequenceMap[b] + if ok { + return handler(buffer, terminal) + } + + switch b { + case 0x5d: // OSC: Operating System Command + b = <-buffer + switch b { + case rune('0'): + b = <-buffer + if b == rune(';') { + title := []rune{} + for { + b = <-buffer + if b == 0x07 || b == 0x5c { // 0x07 -> BELL, 0x5c -> ST (\) + break + } + title = append(title, b) + } + terminal.title = string(title) + } else { + terminal.logger.Errorf("Invalid OSC 0 control sequence: 0x%02X", b) + } + default: + terminal.logger.Errorf("Unknown OSC control sequence: 0x%02X", b) + } + case 'c': + terminal.logger.Errorf("RIS not yet supported") + case '(': + b = <-buffer + switch b { + case 'A': //uk @todo handle these? + //terminal.charSet = C0 + case 'B': //us + //terminal.charSet = C0 + } + case ')': + b = <-buffer + switch b { + case 'A': //uk @todo handle these? + //terminal.charSet = C1 + case 'B': //us + //terminal.charSet = C1 + } + case '*': + b = <-buffer + switch b { + case 'A': //uk @todo handle these? + //terminal.charSet = C2 + case 'B': //us + //terminal.charSet = C2 + } + case '+': + b = <-buffer + switch b { + case 'A': //uk @todo handle these? + //terminal.charSet = C3 + case 'B': //us + //terminal.charSet = C3 + } + case '>': + // numeric char selection @todo + case '=': + //alternate char selection @todo + case '?': + pm := "" + for { + b = <-buffer + switch b { + case 'h': + switch pm { + default: + terminal.logger.Errorf("Unknown private code ESC?%sh", pm) + } + case 'l': + switch pm { + default: + terminal.logger.Errorf("Unknown private code ESC?%sl", pm) + } + default: + pm += string(b) + } + } + default: + terminal.logger.Errorf("Unknown control sequence: 0x%02X [%s]", b, string(b)) + } + return nil +} diff --git a/terminal/csi.go b/terminal/csi.go new file mode 100644 index 0000000..526edfb --- /dev/null +++ b/terminal/csi.go @@ -0,0 +1,284 @@ +package terminal + +import ( + "fmt" + "strconv" + "strings" +) + +var csiSequenceMap = map[rune]csiSequenceHandler{ + 'm': sgrSequenceHandler, +} + +type csiSequenceHandler func(params []string, intermediate string, terminal *Terminal) error + +// CSI: Control Sequence Introducer [ +func csiHandler(buffer chan rune, terminal *Terminal) error { + var final rune + var b rune + param := "" + intermediate := "" +CSI: + for { + b = <-buffer + 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 { + return handler(params, intermediate, terminal) + } + + switch final { + case 'A': + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + if terminal.position.Line-distance >= 0 { + terminal.position.Line -= distance + } else { + terminal.position.Line = 0 + } + case 'B': + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + _, h := terminal.GetSize() + if terminal.position.Line+distance >= h { + terminal.position.Line = h - 1 + } else { + terminal.position.Line += distance + } + case 'C': + + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + terminal.position.Col += distance + w, _ := terminal.GetSize() + if terminal.position.Col >= w { + terminal.position.Col = w - 1 + } + + case 'D': + + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + terminal.position.Col -= distance + if terminal.position.Col < 0 { + terminal.position.Col = 0 + } + + case 'E': + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + + terminal.position.Line += distance + terminal.position.Col = 0 + + case 'F': + + distance := 1 + if len(params) > 0 { + var err error + distance, err = strconv.Atoi(params[0]) + if err != nil { + distance = 1 + } + } + if terminal.position.Line-distance >= 0 { + terminal.position.Line -= distance + } else { + terminal.position.Line = 0 + } + terminal.position.Col = 0 + + 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.position.Col = distance - 1 // 1 based to 0 based + + 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.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 + } + } + + // @todo delete n cells at position + _ = n + + default: + switch param + intermediate + string(final) { + case "?25h": + terminal.showCursor() + case "?25l": + terminal.hideCursor() + case "?12h": + // todo enable cursor blink + case "?12l": + // todo disable cursor blink + default: + return fmt.Errorf("Unknown CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final)) + } + + } + //terminal.logger.Debugf("Received CSI control sequence: 0x%02X (ESC[%s%s%s)", final, param, intermediate, string(final)) + return nil +} diff --git a/terminal/sgr.go b/terminal/sgr.go new file mode 100644 index 0000000..0b08405 --- /dev/null +++ b/terminal/sgr.go @@ -0,0 +1,111 @@ +package terminal + +import "fmt" + +func sgrSequenceHandler(params []string, intermediate string, terminal *Terminal) error { + + for i := range params { + param := params[i] + switch param { + case "00", "0", "": + terminal.cellAttr = terminal.defaultCellAttr + case "1", "01": + terminal.cellAttr.Bold = true + case "2", "02": + terminal.cellAttr.Dim = true + case "4", "04": + terminal.cellAttr.Underline = true + case "5", "05": + terminal.cellAttr.Blink = true + case "7", "07": + terminal.cellAttr.Reverse = true + case "8", "08": + terminal.cellAttr.Hidden = true + case "21": + terminal.cellAttr.Bold = false + case "22": + terminal.cellAttr.Dim = false + case "24": + terminal.cellAttr.Underline = false + case "25": + terminal.cellAttr.Blink = false + case "27": + terminal.cellAttr.Reverse = false + case "28": + terminal.cellAttr.Hidden = false + case "39": + terminal.cellAttr.FgColour = terminal.colourScheme.DefaultFg + case "30": + terminal.cellAttr.FgColour = terminal.colourScheme.BlackFg + case "31": + terminal.cellAttr.FgColour = terminal.colourScheme.RedFg + case "32": + terminal.cellAttr.FgColour = terminal.colourScheme.GreenFg + case "33": + terminal.cellAttr.FgColour = terminal.colourScheme.YellowFg + case "34": + terminal.cellAttr.FgColour = terminal.colourScheme.BlueFg + case "35": + terminal.cellAttr.FgColour = terminal.colourScheme.MagentaFg + case "36": + terminal.cellAttr.FgColour = terminal.colourScheme.CyanFg + case "37": + terminal.cellAttr.FgColour = terminal.colourScheme.WhiteFg + case "90": + terminal.cellAttr.FgColour = terminal.colourScheme.DarkGreyFg + case "91": + terminal.cellAttr.FgColour = terminal.colourScheme.LightRedFg + case "92": + terminal.cellAttr.FgColour = terminal.colourScheme.LightGreenFg + case "93": + terminal.cellAttr.FgColour = terminal.colourScheme.LightYellowFg + case "94": + terminal.cellAttr.FgColour = terminal.colourScheme.LightBlueFg + case "95": + terminal.cellAttr.FgColour = terminal.colourScheme.LightMagentaFg + case "96": + terminal.cellAttr.FgColour = terminal.colourScheme.LightCyanFg + case "97": + terminal.cellAttr.FgColour = terminal.colourScheme.WhiteFg + case "49": + terminal.cellAttr.BgColour = terminal.colourScheme.DefaultBg + case "40": + terminal.cellAttr.BgColour = terminal.colourScheme.BlackBg + case "41": + terminal.cellAttr.BgColour = terminal.colourScheme.RedBg + case "42": + terminal.cellAttr.BgColour = terminal.colourScheme.GreenBg + case "43": + terminal.cellAttr.BgColour = terminal.colourScheme.YellowBg + case "44": + terminal.cellAttr.BgColour = terminal.colourScheme.BlueBg + case "45": + terminal.cellAttr.BgColour = terminal.colourScheme.MagentaBg + case "46": + terminal.cellAttr.BgColour = terminal.colourScheme.CyanBg + case "47": + terminal.cellAttr.BgColour = terminal.colourScheme.WhiteBg + case "100": + terminal.cellAttr.BgColour = terminal.colourScheme.DarkGreyBg + case "101": + terminal.cellAttr.BgColour = terminal.colourScheme.LightRedBg + case "102": + terminal.cellAttr.BgColour = terminal.colourScheme.LightGreenBg + case "103": + terminal.cellAttr.BgColour = terminal.colourScheme.LightYellowBg + case "104": + terminal.cellAttr.BgColour = terminal.colourScheme.LightBlueBg + case "105": + terminal.cellAttr.BgColour = terminal.colourScheme.LightMagentaBg + case "106": + terminal.cellAttr.BgColour = terminal.colourScheme.LightCyanBg + case "107": + terminal.cellAttr.BgColour = terminal.colourScheme.WhiteBg + default: + return fmt.Errorf("Unknown SGR control sequence: (ESC[%s%sm)", param, intermediate) + } + + //terminal.logger.Debugf("SGR control sequence: (ESC[%s%sm)", param, intermediate) + } + return nil +}