Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
de48830
implement the remote side of the termlisten protocol
sawka Apr 27, 2026
1e14a37
updates for wshcmdreader to support multiple osc sequences
sawka Apr 27, 2026
868470f
md formatting
sawka Apr 27, 2026
3d442dc
termlistensrv + working http integration test
sawka Apr 27, 2026
507a988
implement termproxy support in tsunami command
sawka Apr 27, 2026
bb8ba32
default raw tsunami binaries to termproxy, all wave paths disable it …
sawka Apr 27, 2026
50ee4e3
commit generated file update
sawka Apr 27, 2026
6fb3ffc
integrate server side via ptybuffer
sawka Apr 27, 2026
003d073
checkpoint, got tsunami sub-block and client<->server signaling working
sawka Apr 27, 2026
6d7790c
fix some bugs (more remaining)
sawka Apr 27, 2026
b335043
checkpoint, fixing bugs
sawka Apr 28, 2026
4db2737
fix bugs, term mode switcher, preload changes, etc.
sawka Apr 28, 2026
7945bd1
fix manifest for pre-build binaries
sawka Apr 28, 2026
802f928
meta sync + header
sawka May 1, 2026
f61eb14
better global keybindngs for builder window
sawka May 1, 2026
c7480fb
small change to publish dialog
sawka May 1, 2026
e96a78b
update to gpt-5.5 for builder
sawka May 1, 2026
a947688
fix header icons for tsunami sub-blocks
sawka May 1, 2026
5aea889
fix tsunami sub-block menu items
sawka May 1, 2026
17e636d
show app name in header in tsunami blocks
sawka May 1, 2026
db626d0
simplify, use tsunamidirect
sawka May 2, 2026
5925b66
update copyright years
sawka May 2, 2026
cd36896
more simplifications to tsunami now that we have tsunamidirect
sawka May 2, 2026
14bd2d0
move allowtermlisten to a global config setting
sawka May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
updates for wshcmdreader to support multiple osc sequences
  • Loading branch information
sawka committed Apr 27, 2026
commit 1e14a37d3e5cb6428b3525e268cc1b39f6058272
144 changes: 82 additions & 62 deletions pkg/wshutil/wshcmdreader.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2025, Command Line Inc.
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package wshutil
Expand All @@ -13,42 +13,47 @@ import (
)

const (
Mode_Normal = "normal"
Mode_Esc = "esc"
Mode_WaveEsc = "waveesc"
ModeNormal = "normal"
ModeEscStart = "escstart"
ModeOSCNum = "oscnum"
ModeOSCPayload = "oscpayload"
)

const MaxBufferedDataSize = 256 * 1024
const MaxOSCNumLen = 5

type PtyBuffer struct {
CVar *sync.Cond
DataBuf *bytes.Buffer
EscMode string
EscSeqBuf []byte
OSCPrefix string
InputReader io.Reader
MessageCh chan baseds.RpcInputChType
AtEOF bool
Err error
CVar *sync.Cond
DataBuf *bytes.Buffer
EscMode string
EscSeqBuf []byte // raw bytes buffered for passthrough if no handler matches
PayloadBuf []byte // OSC payload accumulation (ModeOSCPayload)
Handlers map[string]func([]byte)
CurrentHandler func([]byte)
InputReader io.Reader
AtEOF bool
Err error
}

// closes messageCh when input is closed (or error)
func MakePtyBuffer(oscPrefix string, input io.Reader, messageCh chan baseds.RpcInputChType) *PtyBuffer {
if len(oscPrefix) != WaveOSCPrefixLen {
panic(fmt.Sprintf("invalid OSC prefix length: %d", len(oscPrefix)))
}
func MakePtyBuffer(input io.Reader, handlers map[string]func([]byte)) *PtyBuffer {
b := &PtyBuffer{
CVar: sync.NewCond(&sync.Mutex{}),
DataBuf: &bytes.Buffer{},
OSCPrefix: oscPrefix,
EscMode: Mode_Normal,
EscMode: ModeNormal,
Handlers: handlers,
InputReader: input,
MessageCh: messageCh,
}
go b.run()
return b
}

// MakeWaveOSCHandler returns a handler func for OSC WaveOSC that sends payloads to messageCh.
func MakeWaveOSCHandler(messageCh chan baseds.RpcInputChType) func([]byte) {
return func(payload []byte) {
messageCh <- baseds.RpcInputChType{MsgBytes: payload}
}
}

func (b *PtyBuffer) setErr(err error) {
b.CVar.L.Lock()
defer b.CVar.L.Unlock()
Expand All @@ -65,12 +70,7 @@ func (b *PtyBuffer) setEOF() {
b.CVar.Broadcast()
}

func (b *PtyBuffer) processWaveEscSeq(escSeq []byte) {
b.MessageCh <- baseds.RpcInputChType{MsgBytes: escSeq}
}

func (b *PtyBuffer) run() {
defer close(b.MessageCh)
buf := make([]byte, 4096)
for {
n, err := b.InputReader.Read(buf)
Expand All @@ -89,56 +89,76 @@ func (b *PtyBuffer) run() {
func (b *PtyBuffer) processData(data []byte) {
outputBuf := make([]byte, 0, len(data))
for _, ch := range data {
if b.EscMode == Mode_WaveEsc {
switch b.EscMode {
case ModeOSCPayload:
if ch == ESC {
// terminates the escape sequence (and the rest was invalid)
b.EscMode = Mode_Normal
outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch)
b.EscSeqBuf = nil
// invalid terminator — discard in-progress sequence, start new escape
b.CurrentHandler = nil
b.PayloadBuf = nil
b.EscMode = ModeEscStart
b.EscSeqBuf = []byte{ESC}
} else if ch == BEL || ch == ST {
// terminates the escpae sequence (is a valid Wave OSC command)
b.EscMode = Mode_Normal
waveEscSeq := b.EscSeqBuf[WaveOSCPrefixLen:]
b.EscSeqBuf = nil
b.processWaveEscSeq(waveEscSeq)
b.CurrentHandler(b.PayloadBuf)
b.CurrentHandler = nil
b.PayloadBuf = nil
b.EscMode = ModeNormal
} else {
b.EscSeqBuf = append(b.EscSeqBuf, ch)
b.PayloadBuf = append(b.PayloadBuf, ch)
}
continue
}
if b.EscMode == Mode_Esc {
if ch == ESC || ch == BEL || ch == ST {
// these all terminate the escape sequence (invalid, not a Wave OSC)
b.EscMode = Mode_Normal

case ModeOSCNum:
// EscSeqBuf holds \x1b] + any digits accumulated so far
numLen := len(b.EscSeqBuf) - 2 // subtract \x1b and ]
if ch == ';' && numLen > 0 {
oscNum := string(b.EscSeqBuf[2:])
if handler, ok := b.Handlers[oscNum]; ok {
b.CurrentHandler = handler
b.EscSeqBuf = nil
b.PayloadBuf = nil
b.EscMode = ModeOSCPayload
} else {
outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch)
b.EscSeqBuf = nil
b.EscMode = ModeNormal
}
} else if ch >= '0' && ch <= '9' && numLen < MaxOSCNumLen {
b.EscSeqBuf = append(b.EscSeqBuf, ch)
} else if ch == ESC {
outputBuf = append(outputBuf, b.EscSeqBuf...)
b.EscSeqBuf = []byte{ESC}
b.EscMode = ModeEscStart
} else {
// non-digit, no `;` yet, or too many digits — passthrough
outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch)
b.EscSeqBuf = nil
continue
b.EscMode = ModeNormal
}
if ch != b.OSCPrefix[len(b.EscSeqBuf)] {
// this is not a Wave OSC sequence, just an escape sequence
b.EscMode = Mode_Normal

case ModeEscStart:
if ch == ']' {
b.EscSeqBuf = append(b.EscSeqBuf, ch)
b.EscMode = ModeOSCNum
} else if ch == ESC {
outputBuf = append(outputBuf, b.EscSeqBuf...)
b.EscSeqBuf = []byte{ESC}
// stay in ModeEscStart
} else {
outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch)
b.EscSeqBuf = nil
continue
b.EscMode = ModeNormal
}
// we're still building what could be a Wave OSC sequence
b.EscSeqBuf = append(b.EscSeqBuf, ch)
// check to see if we have a full Wave OSC prefix
if len(b.EscSeqBuf) == len(b.OSCPrefix) {
b.EscMode = Mode_WaveEsc

default: // ModeNormal
if ch == ESC {
b.EscMode = ModeEscStart
b.EscSeqBuf = []byte{ESC}
} else {
outputBuf = append(outputBuf, ch)
}
continue
}
// Mode_Normal
if ch == ESC {
b.EscMode = Mode_Esc
b.EscSeqBuf = []byte{ch}
continue
}
outputBuf = append(outputBuf, ch)
}
if len(outputBuf) > 0 {
b.writeData(outputBuf)
Expand Down
Loading