Skip to content

Commit 116150c

Browse files
feat: port APC parser restructuring from upstream xterm.js
Replace single APC_STRING state with APC_ENTRY, APC_INTERMEDIATE, and APC_PASSTHROUGH states matching the DCS pattern. Remove ApcState internal state machine — the main parser now drives APC transitions and passes the computed identifier to ApcParser.Start(ident). Change RegisterApcHandler to accept FunctionIdentifier instead of a plain int, aligning APC handler registration with CSI/ESC/DCS handlers. Add APC fallback handler wiring in InputHandler. Fixes #23 Co-authored-by: Ona <no-reply@ona.com>
1 parent 15b8673 commit 116150c

10 files changed

Lines changed: 123 additions & 137 deletions

constants.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,10 @@ const (
126126
ParserStateDCSIgnore ParserState = 11
127127
ParserStateDCSIntermediate ParserState = 12
128128
ParserStateDCSPassthrough ParserState = 13
129-
ParserStateAPCString ParserState = 14
130-
ParserStateLength ParserState = 15 // number of states
129+
ParserStateAPCEntry ParserState = 14
130+
ParserStateAPCIntermediate ParserState = 15
131+
ParserStateAPCPassthrough ParserState = 16
132+
ParserStateLength ParserState = 17 // number of states
131133
)
132134

133135
// ParserAction enumerates the internal actions of the escape sequence parser.
@@ -164,15 +166,5 @@ const (
164166
OscStateAbort OscState = 3
165167
)
166168

167-
// ApcState enumerates the internal states of the APC parser.
168-
type ApcState uint8
169-
170-
const (
171-
ApcStateStart ApcState = 0
172-
ApcStateID ApcState = 1
173-
ApcStatePayload ApcState = 2
174-
ApcStateAbort ApcState = 3
175-
)
176-
177169
// ParserPayloadLimit is the maximum payload size for OSC and DCS sequences.
178170
const ParserPayloadLimit = 10000000

constants_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ func TestParserEnums(t *testing.T) {
105105
}
106106
tests := []TestCase{
107107
{"ground/ignore", Expectation{State: 0, Action: 0}},
108-
{"apc_string/apc_end", Expectation{State: 14, Action: 17}},
109-
{"state_length/print", Expectation{State: 15, Action: 2}},
108+
{"apc_passthrough/apc_end", Expectation{State: 16, Action: 17}},
109+
{"state_length/print", Expectation{State: 17, Action: 2}},
110110
}
111-
states := []ParserState{ParserStateGround, ParserStateAPCString, ParserStateLength}
111+
states := []ParserState{ParserStateGround, ParserStateAPCPassthrough, ParserStateLength}
112112
actions := []ParserAction{ParserActionIgnore, ParserActionAPCEnd, ParserActionPrint}
113113
for i, tc := range tests {
114114
t.Run(tc.Name, func(t *testing.T) {

inputhandler.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ func NewInputHandler(
248248
p.RegisterCsiHandler(FunctionIdentifier{Prefix: '?', Final: 'h'}, h.setModePrivate)
249249
p.RegisterCsiHandler(FunctionIdentifier{Prefix: '?', Final: 'l'}, h.resetModePrivate)
250250

251+
// Fallback handlers (upstream uses these for debug logging)
252+
p.SetApcHandlerFallback(func(ident int, action string, payload ...interface{}) {
253+
// no-op: upstream logs "Unknown APC code" here
254+
})
255+
251256
// DCS handlers
252257
p.RegisterDcsHandler(FunctionIdentifier{Intermediates: "$", Final: 'q'}, NewDcsStringHandler(h.requestStatusString))
253258

parser.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,15 @@ func (p *EscapeSequenceParser) SetOscHandlerFallback(handler OscFallbackHandler)
265265
}
266266

267267
// RegisterApcHandler registers an APC handler.
268-
func (p *EscapeSequenceParser) RegisterApcHandler(ident int, handler ApcHandler) Disposable {
269-
return p.apcParser.RegisterHandler(ident, handler)
268+
func (p *EscapeSequenceParser) RegisterApcHandler(id FunctionIdentifier, handler ApcHandler) Disposable {
269+
id.Prefix = 0 // APC does not support prefix byte
270+
return p.apcParser.RegisterHandler(p.identifier(id), handler)
270271
}
271272

272273
// ClearApcHandler removes all APC handlers for the identifier.
273-
func (p *EscapeSequenceParser) ClearApcHandler(ident int) {
274-
p.apcParser.ClearHandler(ident)
274+
func (p *EscapeSequenceParser) ClearApcHandler(id FunctionIdentifier) {
275+
id.Prefix = 0 // APC does not support prefix byte
276+
p.apcParser.ClearHandler(p.identifier(id))
275277
}
276278

277279
// SetApcHandlerFallback sets the APC fallback handler.
@@ -492,16 +494,17 @@ func (p *EscapeSequenceParser) Parse(data []uint32, length int) {
492494
p.precedingJoinState = 0
493495

494496
case ParserActionAPCStart:
495-
p.apcParser.Start()
497+
p.apcParser.Start(p.collect<<8 | int(code))
496498

497499
case ParserActionAPCPut:
498-
// Inner loop: exit on 0x18, 0x1a, 0x1b, 0x9c
500+
// Inner loop: allow 0x08-0x0d, 0x20-0x7e, non-ASCII printable
499501
j := i + 1
500502
for ; j < length; j++ {
501503
code = data[j]
502-
if code == 0x18 || code == 0x1a || code == 0x1b || code == 0x9c || (code > 0x7f && code < nonASCIIPrintable) {
503-
break
504+
if (code >= 0x20 && code < 0x7f) || (code >= 0x08 && code < 0x0e) || code >= nonASCIIPrintable {
505+
continue
504506
}
507+
break
505508
}
506509
p.apcParser.Put(data, i, j)
507510
i = j - 1

parser_apc.go

Lines changed: 27 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ package xterm
44
//
55
// ApcParser handles Application Program Command sequences. Unlike OSC which
66
// uses numeric identifiers, APC uses the first character as the identifier
7-
// (e.g., 'G' for Kitty graphics protocol).
7+
// (e.g., 'G' for Kitty graphics protocol). The main parser drives APC state
8+
// transitions (APC_ENTRY → APC_INTERMEDIATE → APC_PASSTHROUGH) and calls
9+
// Start(ident) with the computed identifier directly.
810

911
// ApcHandler is the interface for handlers that process APC sequences.
1012
// Intentionally separate from OscHandler to mirror xterm.js type structure.
@@ -19,24 +21,21 @@ type ApcFallbackHandler func(ident int, action string, payload ...interface{})
1921

2022
// ApcParser parses APC sequences and dispatches to registered handlers.
2123
type ApcParser struct {
22-
state ApcState
2324
active []ApcHandler
24-
id int
25+
ident int
2526
handlers map[int][]ApcHandler
2627
handlerFb ApcFallbackHandler
2728
}
2829

2930
// NewApcParser creates a new ApcParser.
3031
func NewApcParser() *ApcParser {
3132
return &ApcParser{
32-
state: ApcStateStart,
33-
id: -1,
3433
handlers: make(map[int][]ApcHandler),
3534
handlerFb: func(int, string, ...interface{}) {},
3635
}
3736
}
3837

39-
// RegisterHandler registers a handler for the given APC identifier (character code).
38+
// RegisterHandler registers a handler for the given APC identifier.
4039
// Returns a Disposable that removes the handler when disposed.
4140
func (p *ApcParser) RegisterHandler(ident int, handler ApcHandler) Disposable {
4241
p.handlers[ident] = append(p.handlers[ident], handler)
@@ -70,93 +69,60 @@ func (p *ApcParser) Dispose() {
7069

7170
// Reset forces cleanup of active handlers and resets parser state.
7271
func (p *ApcParser) Reset() {
73-
if p.state == ApcStatePayload {
72+
if len(p.active) > 0 {
7473
for j := len(p.active) - 1; j >= 0; j-- {
7574
p.active[j].End(false)
7675
}
7776
}
7877
p.active = nil
79-
p.id = -1
80-
p.state = ApcStateStart
78+
p.ident = 0
8179
}
8280

83-
func (p *ApcParser) start() {
84-
if handlers, ok := p.handlers[p.id]; ok && len(handlers) > 0 {
81+
// Start begins a new APC sequence with the given identifier.
82+
// The identifier is computed by the main parser from collect bytes and the
83+
// final character (collect<<8 | code), matching the DCS pattern.
84+
func (p *ApcParser) Start(ident int) {
85+
p.Reset()
86+
p.ident = ident
87+
if handlers, ok := p.handlers[ident]; ok && len(handlers) > 0 {
8588
p.active = handlers
8689
for j := len(p.active) - 1; j >= 0; j-- {
8790
p.active[j].Start()
8891
}
8992
} else {
9093
p.active = nil
91-
p.handlerFb(p.id, "START")
94+
p.handlerFb(p.ident, "START")
9295
}
9396
}
9497

95-
func (p *ApcParser) put(data []uint32, start, end int) {
98+
// Put feeds payload data to the active APC handlers.
99+
func (p *ApcParser) Put(data []uint32, start, end int) {
96100
if len(p.active) == 0 {
97-
p.handlerFb(p.id, "PUT", utf32ToString(data, start, end))
101+
p.handlerFb(p.ident, "PUT", utf32ToString(data, start, end))
98102
} else {
99103
for j := len(p.active) - 1; j >= 0; j-- {
100104
p.active[j].Put(data, start, end)
101105
}
102106
}
103107
}
104108

105-
// Start begins a new APC sequence, resetting any leftover state.
106-
func (p *ApcParser) Start() {
107-
p.Reset()
108-
p.state = ApcStateID
109-
}
110-
111-
// Put feeds data to the current APC command. The first character is used as
112-
// the identifier, and subsequent data is passed as payload.
113-
func (p *ApcParser) Put(data []uint32, start, end int) {
114-
if p.state == ApcStateAbort {
115-
return
116-
}
117-
if p.state == ApcStateID {
118-
if start < end {
119-
p.id = int(data[start])
120-
start++
121-
p.state = ApcStatePayload
122-
p.start()
123-
}
124-
}
125-
if p.state == ApcStatePayload && end-start > 0 {
126-
p.put(data, start, end)
127-
}
128-
}
129-
130109
// End signals the end of an APC sequence. success indicates whether the
131110
// sequence terminated normally or was aborted.
132111
func (p *ApcParser) End(success bool) {
133-
if p.state == ApcStateStart {
134-
return
135-
}
136-
if p.state != ApcStateAbort {
137-
// Early end in ID state means empty APC — invalid, just reset.
138-
if p.state == ApcStateID {
139-
p.active = nil
140-
p.id = -1
141-
p.state = ApcStateStart
142-
return
143-
}
144-
if len(p.active) == 0 {
145-
p.handlerFb(p.id, "END", success)
146-
} else {
147-
for j := len(p.active) - 1; j >= 0; j-- {
148-
if p.active[j].End(success) {
149-
for k := j - 1; k >= 0; k-- {
150-
p.active[k].End(false)
151-
}
152-
break
112+
if len(p.active) == 0 {
113+
p.handlerFb(p.ident, "END", success)
114+
} else {
115+
for j := len(p.active) - 1; j >= 0; j-- {
116+
if p.active[j].End(success) {
117+
for k := j - 1; k >= 0; k-- {
118+
p.active[k].End(false)
153119
}
120+
break
154121
}
155122
}
156123
}
157124
p.active = nil
158-
p.id = -1
159-
p.state = ApcStateStart
125+
p.ident = 0
160126
}
161127

162128
// ApcStringHandler is a convenience wrapper that collects APC payload as a

0 commit comments

Comments
 (0)