mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
working on wave OSC escapes, modes for the terminal (#46)
This commit is contained in:
parent
336dd0c0e3
commit
0f992c535d
@ -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,
|
||||||
|
@ -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") {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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 };
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
119
pkg/blockcontroller/ptybuffer.go
Normal file
119
pkg/blockcontroller/ptybuffer.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user