mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-03-19 21:28:28 +02:00
365 lines
8.2 KiB
Go
365 lines
8.2 KiB
Go
// Copyright 2014 The gocui Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package gocui
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"github.com/go-errors/errors"
|
|
)
|
|
|
|
type escapeInterpreter struct {
|
|
state escapeState
|
|
curch rune
|
|
csiParam []string
|
|
curFgColor, curBgColor Attribute
|
|
mode OutputMode
|
|
instruction instruction
|
|
}
|
|
|
|
type (
|
|
escapeState int
|
|
fontEffect int
|
|
)
|
|
|
|
type instruction interface{ isInstruction() }
|
|
|
|
type eraseInLineFromCursor struct{}
|
|
|
|
func (self eraseInLineFromCursor) isInstruction() {}
|
|
|
|
type noInstruction struct{}
|
|
|
|
func (self noInstruction) isInstruction() {}
|
|
|
|
const (
|
|
stateNone escapeState = iota
|
|
stateEscape
|
|
stateCSI
|
|
stateParams
|
|
stateOSC
|
|
stateOSCEscape
|
|
|
|
bold fontEffect = 1
|
|
faint fontEffect = 2
|
|
italic fontEffect = 3
|
|
underline fontEffect = 4
|
|
blink fontEffect = 5
|
|
reverse fontEffect = 7
|
|
strike fontEffect = 9
|
|
|
|
setForegroundColor int = 38
|
|
defaultForegroundColor int = 39
|
|
setBackgroundColor int = 48
|
|
defaultBackgroundColor int = 49
|
|
)
|
|
|
|
var (
|
|
errNotCSI = errors.New("Not a CSI escape sequence")
|
|
errCSIParseError = errors.New("CSI escape sequence parsing error")
|
|
errCSITooLong = errors.New("CSI escape sequence is too long")
|
|
)
|
|
|
|
// runes in case of error will output the non-parsed runes as a string.
|
|
func (ei *escapeInterpreter) runes() []rune {
|
|
switch ei.state {
|
|
case stateNone:
|
|
return []rune{0x1b}
|
|
case stateEscape:
|
|
return []rune{0x1b, ei.curch}
|
|
case stateCSI:
|
|
return []rune{0x1b, '[', ei.curch}
|
|
case stateParams:
|
|
ret := []rune{0x1b, '['}
|
|
for _, s := range ei.csiParam {
|
|
ret = append(ret, []rune(s)...)
|
|
ret = append(ret, ';')
|
|
}
|
|
return append(ret, ei.curch)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// newEscapeInterpreter returns an escapeInterpreter that will be able to parse
|
|
// terminal escape sequences.
|
|
func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
|
|
ei := &escapeInterpreter{
|
|
state: stateNone,
|
|
curFgColor: ColorDefault,
|
|
curBgColor: ColorDefault,
|
|
mode: mode,
|
|
instruction: noInstruction{},
|
|
}
|
|
return ei
|
|
}
|
|
|
|
// reset sets the escapeInterpreter in initial state.
|
|
func (ei *escapeInterpreter) reset() {
|
|
ei.state = stateNone
|
|
ei.curFgColor = ColorDefault
|
|
ei.curBgColor = ColorDefault
|
|
ei.csiParam = nil
|
|
}
|
|
|
|
func (ei *escapeInterpreter) instructionRead() {
|
|
ei.instruction = noInstruction{}
|
|
}
|
|
|
|
// parseOne parses a rune. If isEscape is true, it means that the rune is part
|
|
// of an escape sequence, and as such should not be printed verbatim. Otherwise,
|
|
// it's not an escape sequence.
|
|
func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
|
|
// Sanity checks
|
|
if len(ei.csiParam) > 20 {
|
|
return false, errCSITooLong
|
|
}
|
|
if len(ei.csiParam) > 0 && len(ei.csiParam[len(ei.csiParam)-1]) > 255 {
|
|
return false, errCSITooLong
|
|
}
|
|
|
|
ei.curch = ch
|
|
|
|
switch ei.state {
|
|
case stateNone:
|
|
if ch == 0x1b {
|
|
ei.state = stateEscape
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
case stateEscape:
|
|
switch ch {
|
|
case '[':
|
|
ei.state = stateCSI
|
|
return true, nil
|
|
case ']':
|
|
ei.state = stateOSC
|
|
return true, nil
|
|
default:
|
|
return false, errNotCSI
|
|
}
|
|
case stateCSI:
|
|
switch {
|
|
case ch >= '0' && ch <= '9':
|
|
ei.csiParam = append(ei.csiParam, "")
|
|
case ch == 'm':
|
|
ei.csiParam = append(ei.csiParam, "0")
|
|
case ch == 'K':
|
|
// fall through
|
|
default:
|
|
return false, errCSIParseError
|
|
}
|
|
ei.state = stateParams
|
|
fallthrough
|
|
case stateParams:
|
|
switch {
|
|
case ch >= '0' && ch <= '9':
|
|
ei.csiParam[len(ei.csiParam)-1] += string(ch)
|
|
return true, nil
|
|
case ch == ';':
|
|
ei.csiParam = append(ei.csiParam, "")
|
|
return true, nil
|
|
case ch == 'm':
|
|
if err := ei.outputCSI(); err != nil {
|
|
return false, errCSIParseError
|
|
}
|
|
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
case ch == 'K':
|
|
p := 0
|
|
if len(ei.csiParam) != 0 && ei.csiParam[0] != "" {
|
|
p, err = strconv.Atoi(ei.csiParam[0])
|
|
if err != nil {
|
|
return false, errCSIParseError
|
|
}
|
|
}
|
|
|
|
if p == 0 {
|
|
ei.instruction = eraseInLineFromCursor{}
|
|
} else {
|
|
// non-zero values of P not supported
|
|
ei.instruction = noInstruction{}
|
|
}
|
|
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
default:
|
|
return false, errCSIParseError
|
|
}
|
|
case stateOSC:
|
|
switch ch {
|
|
case 0x1b:
|
|
ei.state = stateOSCEscape
|
|
return true, nil
|
|
}
|
|
return true, nil
|
|
case stateOSCEscape:
|
|
ei.state = stateNone
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (ei *escapeInterpreter) outputCSI() error {
|
|
n := len(ei.csiParam)
|
|
for i := 0; i < n; {
|
|
p, err := strconv.Atoi(ei.csiParam[i])
|
|
if err != nil {
|
|
return errCSIParseError
|
|
}
|
|
|
|
skip := 1
|
|
switch {
|
|
case p == 0: // reset style and color
|
|
ei.curFgColor = ColorDefault
|
|
ei.curBgColor = ColorDefault
|
|
case p >= 1 && p <= 9: // set style
|
|
ei.curFgColor |= getFontEffect(p)
|
|
case p >= 21 && p <= 29: // reset style
|
|
ei.curFgColor &= ^getFontEffect(p - 20)
|
|
case p >= 30 && p <= 37: // set foreground color
|
|
ei.curFgColor &= AttrStyleBits
|
|
ei.curFgColor |= Get256Color(int32(p) - 30)
|
|
case p == setForegroundColor: // set foreground color (256-color or true color)
|
|
var color Attribute
|
|
var err error
|
|
color, skip, err = ei.csiColor(ei.csiParam[i:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ei.curFgColor &= AttrStyleBits
|
|
ei.curFgColor |= color
|
|
case p == defaultForegroundColor: // reset foreground color
|
|
ei.curFgColor &= AttrStyleBits
|
|
ei.curFgColor |= ColorDefault
|
|
case p >= 40 && p <= 47: // set background color
|
|
ei.curBgColor &= AttrStyleBits
|
|
ei.curBgColor |= Get256Color(int32(p) - 40)
|
|
case p == setBackgroundColor: // set background color (256-color or true color)
|
|
var color Attribute
|
|
var err error
|
|
color, skip, err = ei.csiColor(ei.csiParam[i:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ei.curBgColor &= AttrStyleBits
|
|
ei.curBgColor |= color
|
|
case p == defaultBackgroundColor: // reset background color
|
|
ei.curBgColor &= AttrStyleBits
|
|
ei.curBgColor |= ColorDefault
|
|
case p >= 90 && p <= 97: // set bright foreground color
|
|
ei.curFgColor &= AttrStyleBits
|
|
ei.curFgColor |= Get256Color(int32(p) - 90 + 8)
|
|
case p >= 100 && p <= 107: // set bright background color
|
|
ei.curBgColor &= AttrStyleBits
|
|
ei.curBgColor |= Get256Color(int32(p) - 100 + 8)
|
|
default:
|
|
}
|
|
i += skip
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ei *escapeInterpreter) csiColor(param []string) (color Attribute, skip int, err error) {
|
|
if len(param) < 2 {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
|
|
switch param[1] {
|
|
case "2":
|
|
// 24-bit color
|
|
if ei.mode < OutputTrue {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
if len(param) < 5 {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
var red, green, blue int
|
|
red, err = strconv.Atoi(param[2])
|
|
if err != nil {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
green, err = strconv.Atoi(param[3])
|
|
if err != nil {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
blue, err = strconv.Atoi(param[4])
|
|
if err != nil {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
return NewRGBColor(int32(red), int32(green), int32(blue)), 5, nil
|
|
case "5":
|
|
// 8-bit color
|
|
if ei.mode < Output256 {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
if len(param) < 3 {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
var hex int
|
|
hex, err = strconv.Atoi(param[2])
|
|
if err != nil {
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
return Get256Color(int32(hex)), 3, nil
|
|
default:
|
|
err = errCSIParseError
|
|
return
|
|
}
|
|
}
|
|
|
|
// splitFgBg splits foreground and background color according to ANSI sequence.
|
|
//
|
|
// num (number of segments in ansi) is used to determine if it's 256 mode or rgb mode (3 - 256-color, 5 - rgb-color)
|
|
func splitFgBg(params []string, num int) [][]string {
|
|
var out [][]string
|
|
var current []string
|
|
for _, p := range params {
|
|
if len(current) == num && (p == "48" || p == "38") {
|
|
out = append(out, current)
|
|
current = []string{}
|
|
}
|
|
current = append(current, p)
|
|
}
|
|
|
|
if len(current) > 0 {
|
|
out = append(out, current)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func getFontEffect(f int) Attribute {
|
|
switch fontEffect(f) {
|
|
case bold:
|
|
return AttrBold
|
|
case faint:
|
|
return AttrDim
|
|
case italic:
|
|
return AttrItalic
|
|
case underline:
|
|
return AttrUnderline
|
|
case blink:
|
|
return AttrBlink
|
|
case reverse:
|
|
return AttrReverse
|
|
case strike:
|
|
return AttrStrikeThrough
|
|
}
|
|
return AttrNone
|
|
}
|