mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
working on ijson and wsh magic (#53)
This commit is contained in:
@@ -49,7 +49,7 @@ const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
} else if (blockData.view === "plot") {
|
||||
blockElem = <PlotView />;
|
||||
} else if (blockData.view === "codeedit") {
|
||||
blockElem = <CodeEdit text={null} />;
|
||||
blockElem = <CodeEdit text={null} filename={null} />;
|
||||
}
|
||||
return (
|
||||
<div className="block" ref={blockRef}>
|
||||
|
||||
@@ -66,6 +66,7 @@ type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => vo
|
||||
|
||||
// key is "eventType" or "eventType|oref"
|
||||
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
|
||||
const fileSubjects = new Map<string, SubjectWithRef<WSFileEventData>>();
|
||||
|
||||
function getSubjectInternal(subjectKey: string): SubjectWithRef<WSEventType> {
|
||||
let subject = eventSubjects.get(subjectKey);
|
||||
@@ -93,6 +94,25 @@ function getEventORefSubject(eventType: string, oref: string): SubjectWithRef<WS
|
||||
return getSubjectInternal(eventType + "|" + oref);
|
||||
}
|
||||
|
||||
function getFileSubject(zoneId: string, fileName: string): SubjectWithRef<WSFileEventData> {
|
||||
const subjectKey = zoneId + "|" + fileName;
|
||||
let subject = fileSubjects.get(subjectKey);
|
||||
if (subject == null) {
|
||||
subject = new rxjs.Subject<any>() as any;
|
||||
subject.refCount = 0;
|
||||
subject.release = () => {
|
||||
subject.refCount--;
|
||||
if (subject.refCount === 0) {
|
||||
subject.complete();
|
||||
fileSubjects.delete(subjectKey);
|
||||
}
|
||||
};
|
||||
fileSubjects.set(subjectKey, subject);
|
||||
}
|
||||
subject.refCount++;
|
||||
return subject;
|
||||
}
|
||||
|
||||
const blockCache = new Map<string, Map<string, any>>();
|
||||
|
||||
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
|
||||
@@ -142,6 +162,15 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
console.log("unsupported event", msg);
|
||||
return;
|
||||
}
|
||||
if (msg.eventtype == "blockfile") {
|
||||
const fileData: WSFileEventData = msg.data;
|
||||
const fileSubject = getFileSubject(fileData.zoneid, fileData.filename);
|
||||
if (fileSubject != null) {
|
||||
fileSubject.next(fileData);
|
||||
}
|
||||
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
|
||||
const eventSubject = eventSubjects.get(msg.eventtype);
|
||||
@@ -193,6 +222,7 @@ export {
|
||||
getBackendHostPort,
|
||||
getEventORefSubject,
|
||||
getEventSubject,
|
||||
getFileSubject,
|
||||
globalStore,
|
||||
globalWS,
|
||||
initWS,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import Frame from "react-frame-component";
|
||||
|
||||
type IJsonNode = {
|
||||
tag: string;
|
||||
props?: Record<string, any>;
|
||||
children?: (IJsonNode | string)[];
|
||||
};
|
||||
|
||||
const TagMap: Record<string, React.ComponentType<{ node: IJsonNode }>> = {};
|
||||
|
||||
function convertNodeToTag(node: IJsonNode | string, idx?: number): JSX.Element | string {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
if (idx == null) {
|
||||
idx = 0;
|
||||
}
|
||||
if (typeof node === "string") {
|
||||
return node;
|
||||
}
|
||||
let key = node.props?.key ?? "child-" + idx;
|
||||
let TagComp = TagMap[node.tag];
|
||||
if (!TagComp) {
|
||||
return <div key={key}>Unknown tag:{node.tag}</div>;
|
||||
}
|
||||
return <TagComp key={key} node={node} />;
|
||||
}
|
||||
|
||||
function IJsonHtmlTag({ node }: { node: IJsonNode }) {
|
||||
let { tag, props, children } = node;
|
||||
let divProps = {};
|
||||
if (props != null) {
|
||||
for (let [key, val] of Object.entries(props)) {
|
||||
if (key.startsWith("on")) {
|
||||
divProps[key] = (e: any) => {
|
||||
console.log("handler", key, val);
|
||||
};
|
||||
} else {
|
||||
divProps[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
let childrenComps: (string | JSX.Element)[] = [];
|
||||
if (children != null) {
|
||||
for (let idx = 0; idx < children.length; idx++) {
|
||||
let comp = convertNodeToTag(children[idx], idx);
|
||||
if (comp != null) {
|
||||
childrenComps.push(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
return React.createElement(tag, divProps, childrenComps);
|
||||
}
|
||||
|
||||
TagMap["div"] = IJsonHtmlTag;
|
||||
TagMap["b"] = IJsonHtmlTag;
|
||||
TagMap["i"] = IJsonHtmlTag;
|
||||
TagMap["p"] = IJsonHtmlTag;
|
||||
TagMap["s"] = IJsonHtmlTag;
|
||||
TagMap["span"] = IJsonHtmlTag;
|
||||
TagMap["a"] = IJsonHtmlTag;
|
||||
TagMap["img"] = IJsonHtmlTag;
|
||||
TagMap["h1"] = IJsonHtmlTag;
|
||||
TagMap["h2"] = IJsonHtmlTag;
|
||||
TagMap["h3"] = IJsonHtmlTag;
|
||||
TagMap["h4"] = IJsonHtmlTag;
|
||||
TagMap["h5"] = IJsonHtmlTag;
|
||||
TagMap["h6"] = IJsonHtmlTag;
|
||||
TagMap["ul"] = IJsonHtmlTag;
|
||||
TagMap["ol"] = IJsonHtmlTag;
|
||||
TagMap["li"] = IJsonHtmlTag;
|
||||
TagMap["input"] = IJsonHtmlTag;
|
||||
TagMap["button"] = IJsonHtmlTag;
|
||||
TagMap["textarea"] = IJsonHtmlTag;
|
||||
TagMap["select"] = IJsonHtmlTag;
|
||||
TagMap["option"] = IJsonHtmlTag;
|
||||
TagMap["form"] = IJsonHtmlTag;
|
||||
|
||||
function IJsonView({ rootNode }: { rootNode: IJsonNode }) {
|
||||
// TODO fix this huge inline style
|
||||
return (
|
||||
<div className="ijson">
|
||||
<Frame>
|
||||
<style>
|
||||
{`
|
||||
*::before, *::after { box-sizing: border-box; }
|
||||
* { margin: 0; }
|
||||
body { line-height: 1.2; -webkit-font-smoothing: antialiased; }
|
||||
img, picture, video, canvas, sgv { display: block; }
|
||||
input, button, textarea, select { font: inherit; }
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
font: normal 15px / normal "Lato", sans-serif;
|
||||
}
|
||||
|
||||
.fixed-font {
|
||||
normal 12px / normal "Hack", monospace;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
{convertNodeToTag(rootNode)}
|
||||
</Frame>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { IJsonView };
|
||||
+129
-12
@@ -1,14 +1,25 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WOS, getBackendHostPort, getEventORefSubject, sendWSCommand } from "@/store/global";
|
||||
import {
|
||||
WOS,
|
||||
atoms,
|
||||
getBackendHostPort,
|
||||
getFileSubject,
|
||||
globalStore,
|
||||
sendWSCommand,
|
||||
useBlockAtom,
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import { base64ToArray } from "@/util/util";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import clsx from "clsx";
|
||||
import { produce } from "immer";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import { IJsonView } from "./ijson";
|
||||
|
||||
import "public/xterm.css";
|
||||
import { debounce } from "throttle-debounce";
|
||||
@@ -59,17 +70,96 @@ function handleResize(fitAddon: FitAddon, blockId: string, term: Terminal) {
|
||||
}
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
Enter: "\r",
|
||||
Backspace: "\x7f",
|
||||
Tab: "\t",
|
||||
Escape: "\x1b",
|
||||
ArrowUp: "\x1b[A",
|
||||
ArrowDown: "\x1b[B",
|
||||
ArrowRight: "\x1b[C",
|
||||
ArrowLeft: "\x1b[D",
|
||||
Insert: "\x1b[2~",
|
||||
Delete: "\x1b[3~",
|
||||
Home: "\x1b[1~",
|
||||
End: "\x1b[4~",
|
||||
PageUp: "\x1b[5~",
|
||||
PageDown: "\x1b[6~",
|
||||
};
|
||||
|
||||
function keyboardEventToASCII(event: React.KeyboardEvent<HTMLInputElement>): string {
|
||||
// check modifiers
|
||||
// if no modifiers are set, just send the key
|
||||
if (!event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
if (event.key == null || event.key == "") {
|
||||
return "";
|
||||
}
|
||||
if (keyMap[event.key] != null) {
|
||||
return keyMap[event.key];
|
||||
}
|
||||
if (event.key.length == 1) {
|
||||
return event.key;
|
||||
} else {
|
||||
console.log("not sending keyboard event", event.key, event);
|
||||
}
|
||||
}
|
||||
// if meta or alt is set, there is no ASCII representation
|
||||
if (event.metaKey || event.altKey) {
|
||||
return "";
|
||||
}
|
||||
// if ctrl is set, if it is a letter, subtract 64 from the uppercase value to get the ASCII value
|
||||
if (event.ctrlKey) {
|
||||
if (
|
||||
(event.key.length === 1 && event.key >= "A" && event.key <= "Z") ||
|
||||
(event.key >= "a" && event.key <= "z")
|
||||
) {
|
||||
const key = event.key.toUpperCase();
|
||||
return String.fromCharCode(key.charCodeAt(0) - 64);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
type InitialLoadDataType = {
|
||||
loaded: boolean;
|
||||
heldData: Uint8Array[];
|
||||
};
|
||||
|
||||
const IJSONConst = {
|
||||
tag: "div",
|
||||
children: [
|
||||
{
|
||||
tag: "h1",
|
||||
children: ["Hello World"],
|
||||
},
|
||||
{
|
||||
tag: "p",
|
||||
children: ["This is a paragraph"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function setBlockFocus(blockId: string) {
|
||||
let winData = globalStore.get(atoms.waveWindow);
|
||||
winData = produce(winData, (draft) => {
|
||||
draft.activeblockid = blockId;
|
||||
});
|
||||
WOS.setObjectValue(winData, globalStore.set, true);
|
||||
}
|
||||
|
||||
const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
const termRef = React.useRef<Terminal>(null);
|
||||
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
||||
return jotai.atom((get) => {
|
||||
const winData = get(atoms.waveWindow);
|
||||
return winData.activeblockid === blockId;
|
||||
});
|
||||
});
|
||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
React.useEffect(() => {
|
||||
console.log("terminal created");
|
||||
const newTerm = new Terminal({
|
||||
@@ -95,13 +185,16 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
services.BlockService.SendCommand(blockId, inputCmd);
|
||||
});
|
||||
|
||||
// block subject
|
||||
const blockSubject = getEventORefSubject("block:ptydata", WOS.makeORef("block", blockId));
|
||||
blockSubject.subscribe((msg: WSEventType) => {
|
||||
// base64 decode
|
||||
const data = msg.data;
|
||||
const decodedData = base64ToArray(data.ptydata);
|
||||
newTerm.textarea.addEventListener("focus", () => {
|
||||
setBlockFocus(blockId);
|
||||
});
|
||||
const mainFileSubject = getFileSubject(blockId, "main");
|
||||
mainFileSubject.subscribe((msg: WSFileEventData) => {
|
||||
if (msg.fileop != "append") {
|
||||
console.log("bad fileop for terminal", msg);
|
||||
return;
|
||||
}
|
||||
const decodedData = base64ToArray(msg.data64);
|
||||
if (initialLoadRef.current.loaded) {
|
||||
newTerm.write(decodedData);
|
||||
} else {
|
||||
@@ -146,7 +239,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
|
||||
return () => {
|
||||
newTerm.dispose();
|
||||
blockSubject.release();
|
||||
mainFileSubject.release();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -157,6 +250,13 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
services.BlockService.SendCommand(blockId, metaCmd);
|
||||
return false;
|
||||
}
|
||||
const asciiVal = keyboardEventToASCII(event);
|
||||
if (asciiVal.length == 0) {
|
||||
return false;
|
||||
}
|
||||
const b64data = btoa(asciiVal);
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
services.BlockService.SendCommand(blockId, inputCmd);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -164,8 +264,18 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
if (termMode != "term" && termMode != "html") {
|
||||
termMode = "term";
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFocused && termMode == "term") {
|
||||
termRef.current?.focus();
|
||||
}
|
||||
if (isFocused && termMode == "html") {
|
||||
htmlElemFocusRef.current?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={clsx("view-term", "term-mode-" + termMode)}>
|
||||
<div className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}>
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
<div
|
||||
key="htmlElem"
|
||||
@@ -174,13 +284,20 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
if (htmlElemFocusRef.current != null) {
|
||||
htmlElemFocusRef.current.focus();
|
||||
}
|
||||
setBlockFocus(blockId);
|
||||
}}
|
||||
>
|
||||
<div key="htmlElemFocus" className="term-htmlelem-focus">
|
||||
<input type="text" ref={htmlElemFocusRef} onKeyDown={handleKeyDown} />
|
||||
<input
|
||||
type="text"
|
||||
value={""}
|
||||
ref={htmlElemFocusRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div key="htmlElemContent" className="term-htmlelem-content">
|
||||
HTML MODE
|
||||
<IJsonView rootNode={IJSONConst} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,12 @@
|
||||
.term-htmlelem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ijson iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+33
-4
@@ -14,9 +14,23 @@ declare global {
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
// wshutil.BlockAppendFileCommand
|
||||
type BlockAppendFileCommand = {
|
||||
command: "blockfile:append";
|
||||
filename: string;
|
||||
data: number[];
|
||||
};
|
||||
|
||||
// wshutil.BlockAppendIJsonCommand
|
||||
type BlockAppendIJsonCommand = {
|
||||
command: "blockfile:appendijson";
|
||||
filename: string;
|
||||
data: MetaType;
|
||||
};
|
||||
|
||||
type BlockCommand = {
|
||||
command: string;
|
||||
} & ( BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand );
|
||||
} & ( BlockAppendIJsonCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockMessageCommand | BlockAppendFileCommand );
|
||||
|
||||
// wstore.BlockDef
|
||||
type BlockDef = {
|
||||
@@ -26,7 +40,7 @@ declare global {
|
||||
meta?: MetaType;
|
||||
};
|
||||
|
||||
// blockcontroller.BlockInputCommand
|
||||
// wshutil.BlockInputCommand
|
||||
type BlockInputCommand = {
|
||||
command: "controller:input";
|
||||
inputdata64?: string;
|
||||
@@ -34,13 +48,19 @@ declare global {
|
||||
termsize?: TermSize;
|
||||
};
|
||||
|
||||
// blockcontroller.BlockSetMetaCommand
|
||||
// wshutil.BlockMessageCommand
|
||||
type BlockMessageCommand = {
|
||||
command: "message";
|
||||
message: string;
|
||||
};
|
||||
|
||||
// wshutil.BlockSetMetaCommand
|
||||
type BlockSetMetaCommand = {
|
||||
command: "setmeta";
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
// blockcontroller.BlockSetViewCommand
|
||||
// wshutil.BlockSetViewCommand
|
||||
type BlockSetViewCommand = {
|
||||
command: "setview";
|
||||
view: string;
|
||||
@@ -149,6 +169,14 @@ declare global {
|
||||
data: any;
|
||||
};
|
||||
|
||||
// eventbus.WSFileEventData
|
||||
type WSFileEventData = {
|
||||
zoneid: string;
|
||||
filename: string;
|
||||
fileop: string;
|
||||
data64: string;
|
||||
};
|
||||
|
||||
// waveobj.WaveObj
|
||||
type WaveObj = {
|
||||
otype: string;
|
||||
@@ -190,6 +218,7 @@ declare global {
|
||||
type WaveWindow = WaveObj & {
|
||||
workspaceid: string;
|
||||
activetabid: string;
|
||||
activeblockid?: string;
|
||||
activeblockmap: {[key: string]: string};
|
||||
pos: Point;
|
||||
winsize: WinSize;
|
||||
|
||||
Reference in New Issue
Block a user