mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
ConPTY compresses runs of default-colored spaces into ECH + CUF (\x1b[NX\x1b[NC) instead of emitting them literally. ECH is still a no-op for us — our buffer is built sequentially and has nothing to erase — but CUF has to materialize as N visible space cells so the gap actually appears, otherwise content the child wrote with leading indentation slides left against the preceding cell. The view's cursorForward branch reuses the same machinery as tab expansion: substitute the trigger byte for a space and let the repeatCount path emit the cells under the parser-tracked SGR. The existing notifyCellsWritten plumbing then advances screenCol over the gap, keeping subsequent CUP targets aligned. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
600 lines
18 KiB
Go
600 lines
18 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"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
)
|
|
|
|
type escapeInterpreter struct {
|
|
state escapeState
|
|
curch string
|
|
csiParam []string
|
|
curFgColor, curBgColor Attribute
|
|
mode OutputMode
|
|
instruction instruction
|
|
hyperlink strings.Builder
|
|
|
|
// ConPTY emits cursor-positioning escapes (CUP) to skip over blank
|
|
// rows rather than emitting LFs for them. To convert those into row
|
|
// advances the view can act on, we track where in the pseudo-terminal
|
|
// screen the cursor currently is. 1-based to match the escape
|
|
// sequences.
|
|
//
|
|
// We also have to track the column, but only well enough to count
|
|
// soft-wraps when written content runs past the right edge: ConPTY's
|
|
// CUPs are addressed against its post-wrap screen, so a logical line
|
|
// long enough to wrap in ConPTY's screen counts for two rows from the
|
|
// next CUP's perspective. Column accuracy past wrap-counting isn't
|
|
// modelled — we don't track the col argument of CUPs, and most
|
|
// pager-style emitters use col 1 anyway.
|
|
screenRow, screenCol int
|
|
}
|
|
|
|
type (
|
|
escapeState int
|
|
fontEffect int
|
|
)
|
|
|
|
type instruction interface{ isInstruction() }
|
|
|
|
type eraseInLineFromCursor struct{}
|
|
|
|
func (self eraseInLineFromCursor) isInstruction() {}
|
|
|
|
// cursorDown asks the view to advance N rows. Emitted when CUP / CUD /
|
|
// CNL / VPA targets a row past the current one; backward moves are
|
|
// ignored because the view's buffer is line-based and can't undo.
|
|
type cursorDown struct{ n int }
|
|
|
|
func (self cursorDown) isInstruction() {}
|
|
|
|
// cursorForward asks the view to materialize N space cells. Emitted
|
|
// when CUF advances the cursor right — ConPTY uses CUF (often paired
|
|
// with ECH) to encode runs of default-colored spaces compactly, so we
|
|
// have to render the gap, not just bump a counter.
|
|
type cursorForward struct{ n int }
|
|
|
|
func (self cursorForward) isInstruction() {}
|
|
|
|
type noInstruction struct{}
|
|
|
|
func (self noInstruction) isInstruction() {}
|
|
|
|
const (
|
|
stateNone escapeState = iota
|
|
stateEscape
|
|
stateCharacterSetDesignation
|
|
stateCSI
|
|
stateParams
|
|
stateCSIDiscard
|
|
stateOSC
|
|
stateOSCWaitForParams
|
|
stateOSCParams
|
|
stateOSCHyperlink
|
|
stateOSCEndEscape
|
|
stateOSCSkipUnknown
|
|
|
|
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")
|
|
)
|
|
|
|
// characters in case of error will output the non-parsed characters as a string.
|
|
func (ei *escapeInterpreter) characters() []string {
|
|
switch ei.state {
|
|
case stateNone:
|
|
return []string{"\x1b"}
|
|
case stateEscape:
|
|
return []string{"\x1b", ei.curch}
|
|
case stateCSI:
|
|
return []string{"\x1b", "[", ei.curch}
|
|
case stateParams:
|
|
ret := []string{"\x1b", "["}
|
|
for _, s := range ei.csiParam {
|
|
ret = append(ret, s)
|
|
ret = append(ret, ";")
|
|
}
|
|
return append(ret, ei.curch)
|
|
default:
|
|
}
|
|
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{},
|
|
screenRow: 1,
|
|
screenCol: 1,
|
|
}
|
|
return ei
|
|
}
|
|
|
|
// reset sets the escapeInterpreter in initial state. Note: this only resets
|
|
// escape-parsing state. Screen cursor state survives so that mid-stream
|
|
// malformed escapes don't desync the row tracking from the view.
|
|
func (ei *escapeInterpreter) reset() {
|
|
ei.state = stateNone
|
|
ei.curFgColor = ColorDefault
|
|
ei.curBgColor = ColorDefault
|
|
ei.csiParam = nil
|
|
}
|
|
|
|
// resetScreenCursor returns the screen-cursor tracking to the top of the
|
|
// pseudo-terminal screen. Called when the view is rewound before a fresh pty
|
|
// render, and on cursor-home (which ConPTY emits at the start of each screen)
|
|
// for views that aren't rewound in lockstep — see the CUP handling in parseOne.
|
|
func (ei *escapeInterpreter) resetScreenCursor() {
|
|
ei.screenRow = 1
|
|
ei.screenCol = 1
|
|
}
|
|
|
|
// notifyRowAdvance must be called by the view whenever it advances to the
|
|
// next row in response to an LF / CRLF outside of an escape sequence
|
|
// (i.e. the row transitions the parser doesn't see directly). Keeps the
|
|
// parser's notion of the current screen row in sync with the view.
|
|
func (ei *escapeInterpreter) notifyRowAdvance() {
|
|
ei.screenRow++
|
|
ei.screenCol = 1
|
|
}
|
|
|
|
// notifyColumnReset must be called when the view processes a bare CR
|
|
// (column reset without row advance). Keeps screenCol in sync so wrap
|
|
// counting starts over from col 1.
|
|
func (ei *escapeInterpreter) notifyColumnReset() {
|
|
ei.screenCol = 1
|
|
}
|
|
|
|
// notifyCellsWritten must be called after the view writes visible cells
|
|
// to its buffer. Advances the parser's idea of the cursor by `width`
|
|
// columns; if that crosses the right edge of a `screenColMax`-wide pty
|
|
// screen, the corresponding number of soft-wraps are added to screenRow
|
|
// so subsequent CUPs land on the right line.
|
|
func (ei *escapeInterpreter) notifyCellsWritten(width, screenColMax int) {
|
|
if screenColMax <= 0 {
|
|
return
|
|
}
|
|
// One column at a time: matches ConPTY's "pending wrap" semantics
|
|
// where the cursor stays at col max+1 after writing the rightmost
|
|
// cell and only wraps on the next cell. Loops over individual
|
|
// columns rather than doing the math in one shot so wide cells on a
|
|
// row boundary still wrap cleanly.
|
|
for range width {
|
|
if ei.screenCol > screenColMax {
|
|
ei.screenRow++
|
|
ei.screenCol = 1
|
|
}
|
|
ei.screenCol++
|
|
}
|
|
}
|
|
|
|
// emitCursorAdvance schedules a cursorDown instruction for the next time
|
|
// the view checks ei.instruction, advancing the parser's screen row by
|
|
// the same amount. n <= 0 is a no-op (backward / same-row CUPs are
|
|
// ignored — the view's buffer is line-based and can't undo).
|
|
func (ei *escapeInterpreter) emitCursorAdvance(n int) {
|
|
if n <= 0 {
|
|
return
|
|
}
|
|
ei.instruction = cursorDown{n: n}
|
|
ei.screenRow += n
|
|
ei.screenCol = 1
|
|
}
|
|
|
|
// firstParamOrDefault returns the first CSI parameter parsed as an int,
|
|
// or dflt if it's absent / empty / unparseable.
|
|
func (ei *escapeInterpreter) firstParamOrDefault(dflt int) int {
|
|
if len(ei.csiParam) == 0 || ei.csiParam[0] == "" {
|
|
return dflt
|
|
}
|
|
n, err := strconv.Atoi(ei.csiParam[0])
|
|
if err != nil {
|
|
return dflt
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (ei *escapeInterpreter) instructionRead() {
|
|
ei.instruction = noInstruction{}
|
|
}
|
|
|
|
// parseOne parses a character (grapheme cluster). If isEscape is true, it means that the character
|
|
// 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 []byte) (isEscape bool, err error) {
|
|
// Sanity checks: if a sequence has grown absurdly long, stop
|
|
// accumulating state and just swallow bytes until its final byte —
|
|
// much better than leaking the accumulated garbage into the view.
|
|
if len(ei.csiParam) > 20 || (len(ei.csiParam) > 0 && len(ei.csiParam[len(ei.csiParam)-1]) > 255) {
|
|
ei.state = stateCSIDiscard
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
}
|
|
|
|
ei.curch = string(ch)
|
|
|
|
switch ei.state {
|
|
case stateNone:
|
|
if characterEquals(ch, 0x1b) {
|
|
ei.state = stateEscape
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
case stateEscape:
|
|
switch {
|
|
case characterEquals(ch, '['):
|
|
ei.state = stateCSI
|
|
return true, nil
|
|
case characterEquals(ch, ']'):
|
|
ei.state = stateOSC
|
|
return true, nil
|
|
case characterEquals(ch, '('),
|
|
characterEquals(ch, ')'),
|
|
characterEquals(ch, '*'),
|
|
characterEquals(ch, '+'):
|
|
ei.state = stateCharacterSetDesignation
|
|
return true, nil
|
|
case len(ch) == 1 && ch[0] >= 0x30 && ch[0] <= 0x7E:
|
|
// Single-byte ESC sequence (e.g. ESC c = RIS). We don't
|
|
// interpret these, but we must consume them so they don't
|
|
// leak into the view as literal text.
|
|
ei.state = stateNone
|
|
return true, nil
|
|
default:
|
|
return false, errNotCSI
|
|
}
|
|
case stateCharacterSetDesignation:
|
|
// Not supported, so just skip it
|
|
ei.state = stateNone
|
|
return true, nil
|
|
case stateCSI:
|
|
switch {
|
|
case len(ch) == 1 && ch[0] >= '0' && ch[0] <= '9':
|
|
ei.csiParam = append(ei.csiParam, "")
|
|
case characterEquals(ch, 'm'):
|
|
ei.csiParam = append(ei.csiParam, "0")
|
|
case characterEquals(ch, 'K'),
|
|
characterEquals(ch, 'H'), characterEquals(ch, 'f'), characterEquals(ch, 'd'),
|
|
characterEquals(ch, 'B'), characterEquals(ch, 'E'),
|
|
characterEquals(ch, 'C'):
|
|
// fall through — let stateParams handle these with default
|
|
// params (CUP/VPA default to row 1, CUD/CNL/CUF default to
|
|
// advance by 1).
|
|
case characterEquals(ch, ';'):
|
|
// Empty first param ([;Xm ≡ [0;Xm). Seed a slot for the
|
|
// empty param; stateParams will append the next one when it
|
|
// re-reads this ';' via the fallthrough.
|
|
ei.csiParam = append(ei.csiParam, "")
|
|
case len(ch) == 1 && ch[0] >= 0x3C && ch[0] <= 0x3F:
|
|
// Private-mode prefix byte (<, =, >, ?). We don't interpret
|
|
// DEC private-mode sequences, but must consume them so they
|
|
// don't leak into the view as literal text. Seed an empty
|
|
// param so the subsequent digits land on a valid slot.
|
|
ei.csiParam = append(ei.csiParam, "")
|
|
ei.state = stateParams
|
|
return true, nil
|
|
case len(ch) == 1 && ch[0] >= 0x20 && ch[0] <= 0x2F:
|
|
// CSI intermediate byte. A sequence with intermediates is
|
|
// one we don't implement; consume the rest until the final
|
|
// byte.
|
|
ei.state = stateCSIDiscard
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
case len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E:
|
|
// Valid CSI final byte we don't implement — swallow.
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
default:
|
|
return false, errCSIParseError
|
|
}
|
|
ei.state = stateParams
|
|
fallthrough
|
|
case stateParams:
|
|
switch {
|
|
case len(ch) == 1 && ch[0] >= '0' && ch[0] <= '9':
|
|
ei.csiParam[len(ei.csiParam)-1] += string(ch)
|
|
return true, nil
|
|
case characterEquals(ch, ';'):
|
|
ei.csiParam = append(ei.csiParam, "")
|
|
return true, nil
|
|
case characterEquals(ch, 'm'):
|
|
// outputCSI applies params left-to-right and mutates as it
|
|
// goes, so on failure some leading params may already have
|
|
// taken effect (e.g. `[1;;m` would leave AttrBold set before
|
|
// hitting the empty param). Snapshot the colors beforehand
|
|
// and restore them on error so a malformed SGR is truly a
|
|
// no-op rather than a partial apply.
|
|
savedFg, savedBg := ei.curFgColor, ei.curBgColor
|
|
if err := ei.outputCSI(); err != nil {
|
|
ei.curFgColor, ei.curBgColor = savedFg, savedBg
|
|
}
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
case characterEquals(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
|
|
case characterEquals(ch, 'H'), characterEquals(ch, 'f'),
|
|
characterEquals(ch, 'd'):
|
|
// CUP / HVP (absolute (row, col), col ignored) or VPA (absolute row).
|
|
targetRow := ei.firstParamOrDefault(1)
|
|
if targetRow <= 1 {
|
|
// Cursor home. ConPTY emits this (after [2J) at the start of
|
|
// every screen, so it marks where ConPTY's coordinate origin
|
|
// now sits. Re-anchor our row tracking to the current write
|
|
// position rather than treating it as a backward move: a view
|
|
// that isn't rewound in lockstep with ConPTY's screen (the
|
|
// command log) would otherwise carry stale drift, making every
|
|
// later absolute CUP compute a negative, dropped advance and
|
|
// collapsing the blank rows ConPTY positioned with.
|
|
ei.resetScreenCursor()
|
|
} else {
|
|
// Skip forward to the target row; ignore backward moves.
|
|
ei.emitCursorAdvance(targetRow - ei.screenRow)
|
|
}
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
case characterEquals(ch, 'B'), characterEquals(ch, 'E'):
|
|
// CUD / CNL — relative row advance by N. CNL also resets
|
|
// the column, which we don't track, so the two are
|
|
// equivalent for our purposes.
|
|
ei.emitCursorAdvance(ei.firstParamOrDefault(1))
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
case characterEquals(ch, 'C'):
|
|
// CUF — cursor forward N. Emit space cells so the gap
|
|
// renders. (screenCol is updated by the view via
|
|
// notifyCellsWritten as those spaces are emitted.)
|
|
ei.instruction = cursorForward{n: ei.firstParamOrDefault(1)}
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
case len(ch) == 1 && ch[0] >= 0x20 && ch[0] <= 0x2F:
|
|
// CSI intermediate byte after params. The final byte will
|
|
// have a semantic we don't implement (e.g. `[0 q` =
|
|
// DECSCUSR); consume everything until it arrives.
|
|
ei.state = stateCSIDiscard
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
case len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E:
|
|
// Valid CSI final byte we don't implement — swallow the
|
|
// whole sequence rather than printing it as text.
|
|
ei.state = stateNone
|
|
ei.csiParam = nil
|
|
return true, nil
|
|
default:
|
|
return false, errCSIParseError
|
|
}
|
|
case stateCSIDiscard:
|
|
// Consume the rest of a CSI sequence whose semantic we don't
|
|
// interpret (one with intermediate bytes, or one the sanity
|
|
// checks at the top of parseOne bailed out of). Any byte in the
|
|
// final-byte range ends it.
|
|
if len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E {
|
|
ei.state = stateNone
|
|
}
|
|
return true, nil
|
|
case stateOSC:
|
|
if characterEquals(ch, '8') {
|
|
ei.state = stateOSCWaitForParams
|
|
ei.hyperlink.Reset()
|
|
return true, nil
|
|
}
|
|
|
|
ei.state = stateOSCSkipUnknown
|
|
return true, nil
|
|
case stateOSCWaitForParams:
|
|
if !characterEquals(ch, ';') {
|
|
// Malformed OSC 8 (expected ';' after '8'). Rather than
|
|
// erroring — which would reset state mid-OSC and cause the
|
|
// rest of the sequence to leak as literal text — treat the
|
|
// whole OSC as one we don't understand and skip to its
|
|
// terminator.
|
|
ei.state = stateOSCSkipUnknown
|
|
return true, nil
|
|
}
|
|
|
|
ei.state = stateOSCParams
|
|
return true, nil
|
|
case stateOSCParams:
|
|
if characterEquals(ch, ';') {
|
|
ei.state = stateOSCHyperlink
|
|
}
|
|
return true, nil
|
|
case stateOSCHyperlink:
|
|
switch {
|
|
case characterEquals(ch, 0x07):
|
|
ei.state = stateNone
|
|
case characterEquals(ch, 0x1b):
|
|
ei.state = stateOSCEndEscape
|
|
default:
|
|
ei.hyperlink.Write(ch)
|
|
}
|
|
return true, nil
|
|
case stateOSCEndEscape:
|
|
ei.state = stateNone
|
|
return true, nil
|
|
case stateOSCSkipUnknown:
|
|
switch {
|
|
case characterEquals(ch, 0x07):
|
|
ei.state = stateNone
|
|
case characterEquals(ch, 0x1b):
|
|
ei.state = stateOSCEndEscape
|
|
}
|
|
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 {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
|
|
switch param[1] {
|
|
case "2":
|
|
// 24-bit color
|
|
if ei.mode < OutputTrue {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
if len(param) < 5 {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
var red, green, blue int
|
|
red, err = strconv.Atoi(param[2])
|
|
if err != nil {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
green, err = strconv.Atoi(param[3])
|
|
if err != nil {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
blue, err = strconv.Atoi(param[4])
|
|
if err != nil {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
return NewRGBColor(int32(red), int32(green), int32(blue)), 5, nil
|
|
case "5":
|
|
// 8-bit color
|
|
if ei.mode < Output256 {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
if len(param) < 3 {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
var hex int
|
|
hex, err = strconv.Atoi(param[2])
|
|
if err != nil {
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
return Get256Color(int32(hex)), 3, nil
|
|
default:
|
|
return 0, 0, errCSIParseError
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|