working on wave OSC escapes, modes for the terminal (#46)

This commit is contained in:
Mike Sawka 2024-06-13 14:41:28 -07:00 committed by GitHub
parent 336dd0c0e3
commit 0f992c535d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 327 additions and 31 deletions

View File

@ -64,10 +64,11 @@ const atoms = {
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void }; type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
const orefSubjects = new Map<string, SubjectWithRef<any>>(); // key is "eventType" or "eventType|oref"
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
function getORefSubject(oref: string): SubjectWithRef<any> { function getSubjectInternal(subjectKey: string): SubjectWithRef<WSEventType> {
let subject = orefSubjects.get(oref); let subject = eventSubjects.get(subjectKey);
if (subject == null) { if (subject == null) {
subject = new rxjs.Subject<any>() as any; subject = new rxjs.Subject<any>() as any;
subject.refCount = 0; subject.refCount = 0;
@ -75,15 +76,23 @@ function getORefSubject(oref: string): SubjectWithRef<any> {
subject.refCount--; subject.refCount--;
if (subject.refCount === 0) { if (subject.refCount === 0) {
subject.complete(); subject.complete();
orefSubjects.delete(oref); eventSubjects.delete(subjectKey);
} }
}; };
orefSubjects.set(oref, subject); eventSubjects.set(subjectKey, subject);
} }
subject.refCount++; subject.refCount++;
return subject; return subject;
} }
function getEventSubject(eventType: string): SubjectWithRef<WSEventType> {
return getSubjectInternal(eventType);
}
function getEventORefSubject(eventType: string, oref: string): SubjectWithRef<WSEventType> {
return getSubjectInternal(eventType + "|" + oref);
}
const blockCache = new Map<string, Map<string, any>>(); const blockCache = new Map<string, Map<string, any>>();
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T { function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
@ -129,16 +138,20 @@ function getBackendWSHostPort(): string {
let globalWS: WSControl = null; let globalWS: WSControl = null;
function handleWSEventMessage(msg: WSEventType) { function handleWSEventMessage(msg: WSEventType) {
if (msg.oref == null) { if (msg.eventtype == null) {
console.log("unsupported event", msg); console.log("unsupported event", msg);
return; return;
} }
// we send to two subjects just eventType and eventType|oref
// we don't use getORefSubject here because we don't want to create a new subject // we don't use getORefSubject here because we don't want to create a new subject
const subject = orefSubjects.get(msg.oref); const eventSubject = eventSubjects.get(msg.eventtype);
if (subject == null) { if (eventSubject != null) {
return; eventSubject.next(msg);
}
const eventOrefSubject = eventSubjects.get(msg.eventtype + "|" + msg.oref);
if (eventOrefSubject != null) {
eventOrefSubject.next(msg);
} }
subject.next(msg.data);
} }
function handleWSMessage(msg: any) { function handleWSMessage(msg: any) {
@ -161,11 +174,20 @@ function sendWSCommand(command: WSCommandType) {
globalWS.pushMessage(command); globalWS.pushMessage(command);
} }
// more code that could be moved into an init
// here we want to set up a "waveobj:update" handler
const waveobjUpdateSubject = getEventSubject("waveobj:update");
waveobjUpdateSubject.subscribe((msg: WSEventType) => {
const update: WaveObjUpdate = msg.data;
WOS.updateWaveObject(update);
});
export { export {
WOS, WOS,
atoms, atoms,
getBackendHostPort, getBackendHostPort,
getORefSubject, getEventORefSubject,
getEventSubject,
globalStore, globalStore,
globalWS, globalWS,
initWS, initWS,

View File

@ -18,6 +18,9 @@ const MaxFileSize = 1024 * 1024 * 10; // 10MB
function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom<string, [string], void> }) { function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom<string, [string], void> }) {
const [cwd, setCwd] = jotai.useAtom(cwdAtom); const [cwd, setCwd] = jotai.useAtom(cwdAtom);
if (cwd == null || cwd == "") {
return null;
}
let splitNav = [cwd]; let splitNav = [cwd];
let remaining = cwd; let remaining = cwd;
@ -170,7 +173,9 @@ function PreviewView({ blockId }: { blockId: string }) {
) { ) {
specializedView = <StreamingPreview fileInfo={fileInfo} />; specializedView = <StreamingPreview fileInfo={fileInfo} />;
} else if (fileInfo == null) { } else if (fileInfo == null) {
specializedView = <CenteredDiv>File Not Found</CenteredDiv>; specializedView = (
<CenteredDiv>File Not Found{util.isBlank(fileName) ? null : JSON.stringify(fileName)}</CenteredDiv>
);
} else if (fileInfo.size > MaxFileSize) { } else if (fileInfo.size > MaxFileSize) {
specializedView = <CenteredDiv>File Too Large to Preview</CenteredDiv>; specializedView = <CenteredDiv>File Too Large to Preview</CenteredDiv>;
} else if (mimeType === "text/markdown") { } else if (mimeType === "text/markdown") {

View File

@ -1,12 +1,13 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { WOS, getBackendHostPort, getORefSubject, sendWSCommand } from "@/store/global"; import { WOS, getBackendHostPort, getEventORefSubject, sendWSCommand } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import { base64ToArray } from "@/util/util"; import { base64ToArray } from "@/util/util";
import { FitAddon } from "@xterm/addon-fit"; import { FitAddon } from "@xterm/addon-fit";
import type { ITheme } from "@xterm/xterm"; import type { ITheme } from "@xterm/xterm";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import clsx from "clsx";
import * as React from "react"; import * as React from "react";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
@ -67,6 +68,8 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
const connectElemRef = React.useRef<HTMLDivElement>(null); const connectElemRef = React.useRef<HTMLDivElement>(null);
const termRef = React.useRef<Terminal>(null); const termRef = React.useRef<Terminal>(null);
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] }); const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
React.useEffect(() => { React.useEffect(() => {
console.log("terminal created"); console.log("terminal created");
const newTerm = new Terminal({ const newTerm = new Terminal({
@ -82,10 +85,6 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
newTerm.loadAddon(newFitAddon); newTerm.loadAddon(newFitAddon);
newTerm.open(connectElemRef.current); newTerm.open(connectElemRef.current);
newFitAddon.fit(); newFitAddon.fit();
// services.BlockService.SendCommand(blockId, {
// command: "controller:input",
// termsize: { rows: newTerm.rows, cols: newTerm.cols },
// });
sendWSCommand({ sendWSCommand({
wscommand: "setblocktermsize", wscommand: "setblocktermsize",
blockid: blockId, blockid: blockId,
@ -98,9 +97,10 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
}); });
// block subject // block subject
const blockSubject = getORefSubject(WOS.makeORef("block", blockId)); const blockSubject = getEventORefSubject("block:ptydata", WOS.makeORef("block", blockId));
blockSubject.subscribe((data) => { blockSubject.subscribe((msg: WSEventType) => {
// base64 decode // base64 decode
const data = msg.data;
const decodedData = base64ToArray(data.ptydata); const decodedData = base64ToArray(data.ptydata);
if (initialLoadRef.current.loaded) { if (initialLoadRef.current.loaded) {
newTerm.write(decodedData); newTerm.write(decodedData);
@ -150,9 +150,39 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
}; };
}, []); }, []);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code === "Escape" && event.metaKey) {
// reset term:mode
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
services.BlockService.SendCommand(blockId, metaCmd);
return false;
}
return true;
};
let termMode = blockData?.meta?.["term:mode"] ?? "term";
if (termMode != "term" && termMode != "html") {
termMode = "term";
}
return ( return (
<div className="view-term"> <div className={clsx("view-term", "term-mode-" + termMode)}>
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div> <div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
<div
key="htmlElem"
className="term-htmlelem"
onClick={() => {
if (htmlElemFocusRef.current != null) {
htmlElemFocusRef.current.focus();
}
}}
>
<div key="htmlElemFocus" className="term-htmlelem-focus">
<input type="text" ref={htmlElemFocusRef} onKeyDown={handleKeyDown} />
</div>
<div key="htmlElemContent" className="term-htmlelem-content">
HTML MODE
</div>
</div>
</div> </div>
); );
}; };

View File

@ -7,6 +7,12 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
border-left: 4px solid transparent;
padding-left: 4px;
&:focus-within {
border-left: 4px solid var(--accent-color);
}
.term-header { .term-header {
display: flex; display: flex;
@ -25,6 +31,53 @@
overflow: hidden; overflow: hidden;
line-height: 1; line-height: 1;
} }
.term-htmlelem {
display: flex;
flex-direction: column;
width: 100%;
flex-grow: 1;
min-height: 0;
overflow: hidden;
.term-htmlelem-focus {
height: 0;
width: 0;
input {
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
}
}
.term-htmlelem-content {
display: flex;
flex-direction: row;
width: 100%;
flex-grow: 1;
min-height: 0;
overflow: hidden;
}
}
&.term-mode-term {
.term-connectelem {
display: flex;
}
.term-htmlelem {
display: none;
}
}
&.term-mode-html {
.term-connectelem {
display: none;
}
.term-htmlelem {
display: flex;
}
}
} }
.view-codeedit { .view-codeedit {

View File

@ -3,7 +3,17 @@
import base64 from "base64-js"; import base64 from "base64-js";
function isBlank(str: string): boolean {
return str == null || str == "";
}
function base64ToString(b64: string): string { function base64ToString(b64: string): string {
if (b64 == null) {
return null;
}
if (b64 == "") {
return "";
}
const stringBytes = base64.toByteArray(b64); const stringBytes = base64.toByteArray(b64);
return new TextDecoder().decode(stringBytes); return new TextDecoder().decode(stringBytes);
} }
@ -22,4 +32,4 @@ function base64ToArray(b64: string): Uint8Array {
return rtnArr; return rtnArr;
} }
export { base64ToArray, base64ToString, stringToBase64 }; export { base64ToArray, base64ToString, isBlank, stringToBase64 };

View File

@ -25,6 +25,7 @@ var CommandToTypeMap = map[string]reflect.Type{
BlockCommand_Input: reflect.TypeOf(BlockInputCommand{}), BlockCommand_Input: reflect.TypeOf(BlockInputCommand{}),
BlockCommand_SetView: reflect.TypeOf(BlockSetViewCommand{}), BlockCommand_SetView: reflect.TypeOf(BlockSetViewCommand{}),
BlockCommand_SetMeta: reflect.TypeOf(BlockSetMetaCommand{}), BlockCommand_SetMeta: reflect.TypeOf(BlockSetMetaCommand{}),
BlockCommand_Message: reflect.TypeOf(BlockMessageCommand{}),
} }
func CommandTypeUnionMeta() tsgenmeta.TypeUnionMeta { func CommandTypeUnionMeta() tsgenmeta.TypeUnionMeta {
@ -35,6 +36,7 @@ func CommandTypeUnionMeta() tsgenmeta.TypeUnionMeta {
reflect.TypeOf(BlockInputCommand{}), reflect.TypeOf(BlockInputCommand{}),
reflect.TypeOf(BlockSetViewCommand{}), reflect.TypeOf(BlockSetViewCommand{}),
reflect.TypeOf(BlockSetMetaCommand{}), reflect.TypeOf(BlockSetMetaCommand{}),
reflect.TypeOf(BlockMessageCommand{}),
}, },
} }
} }
@ -96,3 +98,12 @@ type BlockSetMetaCommand struct {
func (smc *BlockSetMetaCommand) GetCommand() string { func (smc *BlockSetMetaCommand) GetCommand() string {
return BlockCommand_SetMeta return BlockCommand_SetMeta
} }
type BlockMessageCommand struct {
Command string `json:"command" tstype:"\"message\""`
Message string `json:"message"`
}
func (bmc *BlockMessageCommand) GetCommand() string {
return BlockCommand_Message
}

View File

@ -11,6 +11,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"strings"
"sync" "sync"
"time" "time"
@ -39,6 +40,7 @@ type BlockController struct {
InputCh chan BlockCommand InputCh chan BlockCommand
Status string Status string
PtyBuffer *PtyBuffer
ShellProc *shellexec.ShellProc ShellProc *shellexec.ShellProc
ShellInputCh chan *BlockInputCommand ShellInputCh chan *BlockInputCommand
} }
@ -90,7 +92,7 @@ func (bc *BlockController) Close() {
const DefaultTermMaxFileSize = 256 * 1024 const DefaultTermMaxFileSize = 256 * 1024
func (bc *BlockController) handleShellProcData(data []byte, seqNum int) error { func (bc *BlockController) handleShellProcData(data []byte) error {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
err := filestore.WFS.AppendData(ctx, bc.BlockId, "main", data) err := filestore.WFS.AppendData(ctx, bc.BlockId, "main", data)
@ -104,7 +106,6 @@ func (bc *BlockController) handleShellProcData(data []byte, seqNum int) error {
"blockid": bc.BlockId, "blockid": bc.BlockId,
"blockfile": "main", "blockfile": "main",
"ptydata": base64.StdEncoding.EncodeToString(data), "ptydata": base64.StdEncoding.EncodeToString(data),
"seqnum": seqNum,
}, },
}) })
return nil return nil
@ -159,15 +160,13 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts) error {
bc.ShellProc = nil bc.ShellProc = nil
bc.ShellInputCh = nil bc.ShellInputCh = nil
}() }()
seqNum := 0
buf := make([]byte, 4096) buf := make([]byte, 4096)
for { for {
nr, err := bc.ShellProc.Pty.Read(buf) nr, err := bc.ShellProc.Pty.Read(buf)
seqNum++
if nr > 0 { if nr > 0 {
handleDataErr := bc.handleShellProcData(buf[:nr], seqNum) bc.PtyBuffer.AppendData(buf[:nr])
if handleDataErr != nil { if bc.PtyBuffer.Err != nil {
log.Printf("error handling shell data: %v\n", handleDataErr) log.Printf("error processing pty data: %v\n", bc.PtyBuffer.Err)
break break
} }
} }
@ -269,6 +268,15 @@ func StartBlockController(ctx context.Context, blockId string) error {
Status: "init", Status: "init",
InputCh: make(chan BlockCommand), InputCh: make(chan BlockCommand),
} }
ptyBuffer := MakePtyBuffer(bc.handleShellProcData, func(cmd BlockCommand) error {
if strings.HasPrefix(cmd.GetCommand(), "controller:") {
bc.InputCh <- cmd
} else {
ProcessStaticCommand(blockId, cmd)
}
return nil
})
bc.PtyBuffer = ptyBuffer
blockControllerMap[blockId] = bc blockControllerMap[blockId] = bc
go bc.Run(blockData) go bc.Run(blockData)
return nil return nil
@ -304,6 +312,21 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) error {
if err != nil { if err != nil {
return fmt.Errorf("error updating block: %w", err) return fmt.Errorf("error updating block: %w", err)
} }
// send a waveobj:update event
updatedBlock, err := wstore.DBGet[*wstore.Block](ctx, blockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
eventbus.SendEvent(eventbus.WSEventType{
EventType: "waveobj:update",
ORef: waveobj.MakeORef(wstore.OType_Block, blockId).String(),
Data: wstore.WaveObjUpdate{
UpdateType: wstore.UpdateType_Update,
OType: wstore.OType_Block,
OID: blockId,
Obj: updatedBlock,
},
})
return nil return nil
case *BlockSetMetaCommand: case *BlockSetMetaCommand:
log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta) log.Printf("SETMETA: %s | %v\n", blockId, cmd.Meta)
@ -314,6 +337,9 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) error {
if block == nil { if block == nil {
return nil return nil
} }
if block.Meta == nil {
block.Meta = make(map[string]any)
}
for k, v := range cmd.Meta { for k, v := range cmd.Meta {
if v == nil { if v == nil {
delete(block.Meta, k) delete(block.Meta, k)
@ -325,6 +351,24 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) error {
if err != nil { if err != nil {
return fmt.Errorf("error updating block: %w", err) return fmt.Errorf("error updating block: %w", err)
} }
// send a waveobj:update event
updatedBlock, err := wstore.DBGet[*wstore.Block](ctx, blockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
eventbus.SendEvent(eventbus.WSEventType{
EventType: "waveobj:update",
ORef: waveobj.MakeORef(wstore.OType_Block, blockId).String(),
Data: wstore.WaveObjUpdate{
UpdateType: wstore.UpdateType_Update,
OType: wstore.OType_Block,
OID: blockId,
Obj: updatedBlock,
},
})
return nil
case *BlockMessageCommand:
log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message)
return nil return nil
default: default:
return fmt.Errorf("unknown command type %T", cmdGen) return fmt.Errorf("unknown command type %T", cmdGen)

View File

@ -0,0 +1,119 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package blockcontroller
import (
"encoding/json"
"fmt"
"github.com/wavetermdev/thenextwave/pkg/wshutil"
)
const (
Mode_Normal = "normal"
Mode_Esc = "esc"
Mode_WaveEsc = "waveesc"
)
type PtyBuffer struct {
Mode string
EscSeqBuf []byte
DataOutputFn func([]byte) error
CommandOutputFn func(BlockCommand) error
Err error
}
func MakePtyBuffer(dataOutputFn func([]byte) error, commandOutputFn func(BlockCommand) error) *PtyBuffer {
return &PtyBuffer{
Mode: Mode_Normal,
DataOutputFn: dataOutputFn,
CommandOutputFn: commandOutputFn,
}
}
func (b *PtyBuffer) setErr(err error) {
if b.Err == nil {
b.Err = err
}
}
func (b *PtyBuffer) processWaveEscSeq(escSeq []byte) {
jmsg := make(map[string]any)
err := json.Unmarshal(escSeq, &jmsg)
if err != nil {
b.setErr(fmt.Errorf("error unmarshalling Wave OSC sequence data: %w", err))
return
}
cmd, err := ParseCmdMap(jmsg)
if err != nil {
b.setErr(fmt.Errorf("error parsing Wave OSC command: %w", err))
return
}
err = b.CommandOutputFn(cmd)
if err != nil {
b.setErr(fmt.Errorf("error processing Wave OSC command: %w", err))
return
}
}
func (b *PtyBuffer) AppendData(data []byte) {
outputBuf := make([]byte, 0, len(data))
for _, ch := range data {
if b.Mode == Mode_WaveEsc {
if ch == wshutil.ESC {
// terminates the escape sequence (and the rest was invalid)
b.Mode = Mode_Normal
outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch)
b.EscSeqBuf = nil
} else if ch == wshutil.BEL || ch == wshutil.ST {
// terminates the escpae sequence (is a valid Wave OSC command)
b.Mode = Mode_Normal
waveEscSeq := b.EscSeqBuf[len(wshutil.WaveOSCPrefix):]
b.EscSeqBuf = nil
b.processWaveEscSeq(waveEscSeq)
} else {
b.EscSeqBuf = append(b.EscSeqBuf, ch)
}
continue
}
if b.Mode == Mode_Esc {
if ch == wshutil.ESC || ch == wshutil.BEL || ch == wshutil.ST {
// these all terminate the escape sequence (invalid, not a Wave OSC)
b.Mode = Mode_Normal
outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch)
} else {
if ch == wshutil.WaveOSCPrefixBytes[len(b.EscSeqBuf)] {
// we're still building what could be a Wave OSC sequence
b.EscSeqBuf = append(b.EscSeqBuf, ch)
} else {
// this is not a Wave OSC sequence, just an escape sequence
b.Mode = Mode_Normal
outputBuf = append(outputBuf, b.EscSeqBuf...)
outputBuf = append(outputBuf, ch)
continue
}
// check to see if we have a full Wave OSC prefix
if len(b.EscSeqBuf) == len(wshutil.WaveOSCPrefixBytes) {
b.Mode = Mode_WaveEsc
}
}
continue
}
// Mode_Normal
if ch == wshutil.ESC {
b.Mode = Mode_Esc
b.EscSeqBuf = []byte{ch}
continue
}
outputBuf = append(outputBuf, ch)
}
if len(outputBuf) > 0 {
err := b.DataOutputFn(outputBuf)
if err != nil {
b.setErr(fmt.Errorf("error processing data output: %w", err))
}
}
}

View File

@ -15,8 +15,10 @@ const WaveOSC = "23198"
const WaveOSCPrefix = "\x1b]" + WaveOSC + ";" const WaveOSCPrefix = "\x1b]" + WaveOSC + ";"
const HexChars = "0123456789ABCDEF" const HexChars = "0123456789ABCDEF"
const BEL = 0x07 const BEL = 0x07
const ST = 0x9c
const ESC = 0x1b
var waveOSCPrefixBytes = []byte(WaveOSCPrefix) var WaveOSCPrefixBytes = []byte(WaveOSCPrefix)
// OSC escape types // OSC escape types
// OSC 23198 ; (JSON | base64-JSON) ST // OSC 23198 ; (JSON | base64-JSON) ST
@ -50,14 +52,14 @@ func EncodeWaveOSCMessage(cmd Command) ([]byte, error) {
// If no control characters, directly construct the output // If no control characters, directly construct the output
// \x1b] (2) + WaveOSC + ; (1) + message + \x07 (1) // \x1b] (2) + WaveOSC + ; (1) + message + \x07 (1)
output := make([]byte, len(WaveOSCPrefix)+len(barr)+1) output := make([]byte, len(WaveOSCPrefix)+len(barr)+1)
copy(output, waveOSCPrefixBytes) copy(output, WaveOSCPrefixBytes)
copy(output[len(WaveOSCPrefix):], barr) copy(output[len(WaveOSCPrefix):], barr)
output[len(output)-1] = BEL output[len(output)-1] = BEL
return output, nil return output, nil
} }
var buf bytes.Buffer var buf bytes.Buffer
buf.Write(waveOSCPrefixBytes) buf.Write(WaveOSCPrefixBytes)
escSeq := [6]byte{'\\', 'u', '0', '0', '0', '0'} escSeq := [6]byte{'\\', 'u', '0', '0', '0', '0'}
for _, b := range barr { for _, b := range barr {
if b < 0x20 || b == 0x7f { if b < 0x20 || b == 0x7f {