turbo-editors/turbo-corepublic Fork 0
d662cebdb65b319885da903daf7eff9ab1bfbb78
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on d662cebdb65b319885da903daf7eff9ab1bfbb78 · k33g · 21h ago
parser.go · 186 lines · 5.0 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package terminal

import "unicode/utf8"

// maxParams caps how many numbers one control sequence may carry. Real
// sequences use a handful; the limit is there so that a stream of digits
// cannot make the parser allocate without bound.
const maxParams = 32

// maxOSC caps the length of an operating-system command — a window title, say.
// Anything longer is a program that will never send its terminator.
const maxOSC = 4096

// state is where the parser is in a control sequence.
type state int

const (
	// ground is ordinary text, the state the parser spends its life in.
	ground state = iota
	// afterEscape has just seen ESC and is waiting to learn what kind of
	// sequence this is.
	afterEscape
	// inCSI is collecting the parameters of a control sequence, ESC [ … .
	inCSI
	// inOSC is collecting an operating-system command, ESC ] … , which runs
	// until a BEL or a string terminator.
	inOSC
	// inOSCEscape has seen ESC inside an OSC and is checking whether it is the
	// backslash that ends it.
	inOSCEscape
	// ignoring is skipping a sequence this emulator does not implement, up to
	// and including its final byte.
	ignoring
)

// Parser turns a byte stream from a terminal program into changes to a Screen.
//
// It implements io.Writer, so the output of a pseudo-terminal can be copied
// straight into it. Bytes may arrive split anywhere — in the middle of a
// control sequence or of a UTF-8 character — and the parser carries its state
// across calls.
//
//	screen := terminal.NewScreen(80, 24)
//	parser := terminal.NewParser(screen)
//	parser.Write([]byte("\x1b[1;32mgreen\x1b[0m"))
//	fmt.Println(screen.LineText(0)) // green
type Parser struct {
	screen *Screen

	state  state
	params []int
	// private holds the '?' of a DEC private sequence, or zero.
	private byte
	// intermediate holds a byte such as the '>' of a secondary device
	// attributes request, or zero.
	intermediate byte

	osc []byte

	// partial holds the bytes of a UTF-8 character that arrived split across
	// two writes.
	partial []byte

	// title is the last window title the program asked for.
	title string
	// bell is set when the program rang the terminal bell.
	bell bool
}

// NewParser returns a parser writing onto a screen.
func NewParser(screen *Screen) *Parser {
	return &Parser{screen: screen, params: make([]int, 0, maxParams)}
}

// Screen returns the screen the parser writes onto.
func (p *Parser) Screen() *Screen { return p.screen }

// Title returns the last window title the program asked for, or the empty
// string when it has asked for none.
func (p *Parser) Title() string { return p.title }

// TakeBell reports whether the bell has rung since it was last asked, and
// forgets it. A caller that never asks simply never rings.
func (p *Parser) TakeBell() bool {
	rang := p.bell
	p.bell = false
	return rang
}

// Write feeds bytes to the parser. It never fails and always consumes
// everything, because there is nothing a terminal can do with a byte it does
// not understand except carry on.
func (p *Parser) Write(data []byte) (int, error) {
	for _, b := range data {
		p.step(b)
	}
	return len(data), nil
}

// step advances the parser by one byte.
func (p *Parser) step(b byte) {
	switch p.state {
	case ground:
		p.stepGround(b)
	case afterEscape:
		p.stepAfterEscape(b)
	case inCSI:
		p.stepCSI(b)
	case inOSC:
		p.stepOSC(b)
	case inOSCEscape:
		p.stepOSCEscape(b)
	case ignoring:
		p.stepIgnoring(b)
	}
}

// escape and the other control bytes the parser acts on directly.
const (
	bell           = 0x07
	backspace      = 0x08
	tab            = 0x09
	lineFeed       = 0x0a
	verticalTab    = 0x0b
	formFeed       = 0x0c
	carriageReturn = 0x0d
	escape         = 0x1b
	del            = 0x7f
)

// stepGround handles ordinary text and the C0 control characters.
func (p *Parser) stepGround(b byte) {
	switch b {
	case escape:
		p.beginEscape()
	case bell:
		p.bell = true
	case backspace:
		p.screen.Backspace()
	case tab:
		p.screen.Tab()
	case lineFeed, verticalTab, formFeed:
		p.screen.LineFeed()
	case carriageReturn:
		p.screen.CarriageReturn()
	case del:
		// Delete is padding on a real terminal, and prints nothing.
	default:
		p.text(b)
	}
}

// beginEscape starts a new escape sequence, forgetting whatever an unfinished
// one had collected.
func (p *Parser) beginEscape() {
	p.state = afterEscape
	p.params = p.params[:0]
	p.private, p.intermediate = 0, 0
	p.partial = p.partial[:0]
}

// text handles a byte of ordinary text, assembling UTF-8 characters that
// arrive split across writes.
func (p *Parser) text(b byte) {
	if b < utf8.RuneSelf {
		p.screen.WriteRune(rune(b))
		return
	}

	p.partial = append(p.partial, b)
	r, size := utf8.DecodeRune(p.partial)

	if r == utf8.RuneError && size <= 1 {
		// Either more bytes are still to come, or this is not UTF-8 at all.
		// Waiting is right for the first and harmless for the second, until
		// the buffer grows past any legal character.
		if len(p.partial) >= utf8.UTFMax {
			p.screen.WriteRune(utf8.RuneError)
			p.partial = p.partial[:0]
		}
		return
	}

	p.screen.WriteRune(r)
	p.partial = p.partial[:0]
}