mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
stickers and terminal serialization (#57)
This commit is contained in:
parent
b6c85e38f6
commit
4ded6d94b6
@ -1,8 +1,13 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index";
|
||||
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||||
import { layoutTreeStateReducer } from "@/faraday/lib/layoutState";
|
||||
|
||||
import * as jotai from "jotai";
|
||||
import * as rxjs from "rxjs";
|
||||
import * as services from "./services";
|
||||
import * as WOS from "./wos";
|
||||
import { WSControl } from "./ws";
|
||||
|
||||
@ -62,8 +67,6 @@ const atoms = {
|
||||
workspace: workspaceAtom,
|
||||
};
|
||||
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
|
||||
// key is "eventType" or "eventType|oref"
|
||||
const eventSubjects = new Map<string, SubjectWithRef<WSEventType>>();
|
||||
const fileSubjects = new Map<string, SubjectWithRef<WSFileEventData>>();
|
||||
@ -215,9 +218,58 @@ function getApi(): ElectronApi {
|
||||
return (window as any).api;
|
||||
}
|
||||
|
||||
async function createBlock(blockDef: BlockDef) {
|
||||
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
||||
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
|
||||
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
|
||||
type: LayoutTreeActionType.InsertNode,
|
||||
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, { blockId }),
|
||||
};
|
||||
const activeTabId = globalStore.get(atoms.uiContext).activetabid;
|
||||
const layoutStateAtom = getLayoutStateAtomForTab(
|
||||
activeTabId,
|
||||
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", activeTabId))
|
||||
);
|
||||
const curState = globalStore.get(layoutStateAtom);
|
||||
globalStore.set(layoutStateAtom, layoutTreeStateReducer(curState, insertNodeAction));
|
||||
}
|
||||
|
||||
// when file is not found, returns {data: null, fileInfo: null}
|
||||
async function fetchWaveFile(
|
||||
zoneId: string,
|
||||
fileName: string,
|
||||
offset?: number
|
||||
): Promise<{ data: Uint8Array; fileInfo: WaveFile }> {
|
||||
let usp = new URLSearchParams();
|
||||
usp.set("zoneid", zoneId);
|
||||
usp.set("name", fileName);
|
||||
if (offset != null) {
|
||||
usp.set("offset", offset.toString());
|
||||
}
|
||||
const resp = await fetch(getBackendHostPort() + "/wave/file?" + usp.toString());
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 404) {
|
||||
return { data: null, fileInfo: null };
|
||||
}
|
||||
throw new Error("error getting wave file: " + resp.statusText);
|
||||
}
|
||||
if (resp.status == 204) {
|
||||
return { data: null, fileInfo: null };
|
||||
}
|
||||
let fileInfo64 = resp.headers.get("X-ZoneFileInfo");
|
||||
if (fileInfo64 == null) {
|
||||
throw new Error(`missing zone file info for ${zoneId}:${fileName}`);
|
||||
}
|
||||
let fileInfo = JSON.parse(atob(fileInfo64));
|
||||
const data = await resp.arrayBuffer();
|
||||
return { data: new Uint8Array(data), fileInfo };
|
||||
}
|
||||
|
||||
export {
|
||||
WOS,
|
||||
atoms,
|
||||
createBlock,
|
||||
fetchWaveFile,
|
||||
getApi,
|
||||
getBackendHostPort,
|
||||
getEventORefSubject,
|
||||
|
@ -7,6 +7,10 @@ import * as WOS from "./wos";
|
||||
|
||||
// blockservice.BlockService (block)
|
||||
class BlockServiceType {
|
||||
SaveTerminalState(arg2: string, arg3: string, arg4: string, arg5: number): Promise<void> {
|
||||
return WOS.callBackendService("block", "SaveTerminalState", Array.from(arguments))
|
||||
}
|
||||
|
||||
// send command to block
|
||||
SendCommand(blockid: string, cmd: BlockCommand): Promise<void> {
|
||||
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
|
||||
|
@ -27,4 +27,5 @@
|
||||
|
||||
/* z-index values */
|
||||
--zindex-header-hover: 100;
|
||||
--zindex-termstickers: 20;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
overflow: hidden;
|
||||
border-left: 4px solid transparent;
|
||||
padding-left: 4px;
|
||||
position: relative;
|
||||
|
||||
&:focus-within {
|
||||
border-left: 4px solid var(--accent-color);
|
||||
@ -84,4 +85,30 @@
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.term-stickers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: var(--zindex-termstickers);
|
||||
pointer-events: none;
|
||||
|
||||
.term-sticker-image {
|
||||
img {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.term-sticker-svg {
|
||||
svg {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,8 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
WOS,
|
||||
atoms,
|
||||
getBackendHostPort,
|
||||
getFileSubject,
|
||||
globalStore,
|
||||
sendWSCommand,
|
||||
useBlockAtom,
|
||||
} from "@/store/global";
|
||||
import { WOS, atoms, 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";
|
||||
@ -20,9 +11,10 @@ import { produce } from "immer";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import { IJsonView } from "./ijson";
|
||||
import { TermStickers } from "./termsticker";
|
||||
import { TermWrap } from "./termwrap";
|
||||
|
||||
import "public/xterm.css";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import "./term.less";
|
||||
|
||||
function getThemeFromCSSVars(el: Element): ITheme {
|
||||
@ -149,7 +141,7 @@ function setBlockFocus(blockId: string) {
|
||||
|
||||
const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
const termRef = React.useRef<Terminal>(null);
|
||||
const termRef = React.useRef<TermWrap>(null);
|
||||
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
@ -161,8 +153,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
});
|
||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
React.useEffect(() => {
|
||||
console.log("terminal created");
|
||||
const newTerm = new Terminal({
|
||||
const termWrap = new TermWrap(blockId, connectElemRef.current, {
|
||||
theme: getThemeFromCSSVars(connectElemRef.current),
|
||||
fontSize: 12,
|
||||
fontFamily: "Hack",
|
||||
@ -170,93 +161,22 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
fontWeight: "normal",
|
||||
fontWeightBold: "bold",
|
||||
});
|
||||
termRef.current = newTerm;
|
||||
const newFitAddon = new FitAddon();
|
||||
newTerm.loadAddon(newFitAddon);
|
||||
newTerm.open(connectElemRef.current);
|
||||
newFitAddon.fit();
|
||||
sendWSCommand({
|
||||
wscommand: "setblocktermsize",
|
||||
blockid: blockId,
|
||||
termsize: { rows: newTerm.rows, cols: newTerm.cols },
|
||||
});
|
||||
connectElemRef.current.addEventListener(
|
||||
"keydown",
|
||||
(ev) => {
|
||||
if (ev.code == "Escape" && ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
|
||||
services.BlockService.SendCommand(blockId, metaCmd);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
newTerm.onData((data) => {
|
||||
const b64data = btoa(data);
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
services.BlockService.SendCommand(blockId, inputCmd);
|
||||
});
|
||||
newTerm.textarea.addEventListener("focus", () => {
|
||||
(window as any).term = termWrap;
|
||||
termRef.current = termWrap;
|
||||
termWrap.addFocusListener(() => {
|
||||
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 {
|
||||
initialLoadRef.current.heldData.push(decodedData);
|
||||
}
|
||||
});
|
||||
// load data from filestore
|
||||
const startTs = Date.now();
|
||||
let loadedBytes = 0;
|
||||
const localTerm = termRef.current; // avoids devmode double effect running issue (terminal gets created twice)
|
||||
const usp = new URLSearchParams();
|
||||
usp.set("zoneid", blockId);
|
||||
usp.set("name", "main");
|
||||
fetch(getBackendHostPort() + "/wave/file?" + usp.toString())
|
||||
.then((resp) => {
|
||||
if (resp.ok) {
|
||||
return resp.arrayBuffer();
|
||||
}
|
||||
console.log("error loading file", resp.status, resp.statusText);
|
||||
})
|
||||
.then((data: ArrayBuffer) => {
|
||||
const uint8View = new Uint8Array(data);
|
||||
localTerm.write(uint8View);
|
||||
loadedBytes = uint8View.byteLength;
|
||||
})
|
||||
.finally(() => {
|
||||
initialLoadRef.current.heldData.forEach((data) => {
|
||||
localTerm.write(data);
|
||||
});
|
||||
initialLoadRef.current.loaded = true;
|
||||
initialLoadRef.current.heldData = [];
|
||||
console.log(`terminal loaded file ${loadedBytes} bytes, ${Date.now() - startTs}ms`);
|
||||
});
|
||||
|
||||
const resize_debounced = debounce(50, () => {
|
||||
handleResize(newFitAddon, blockId, newTerm);
|
||||
});
|
||||
const rszObs = new ResizeObserver(() => {
|
||||
resize_debounced();
|
||||
termWrap.handleResize_debounced();
|
||||
});
|
||||
rszObs.observe(connectElemRef.current);
|
||||
|
||||
termWrap.initTerminal();
|
||||
return () => {
|
||||
newTerm.dispose();
|
||||
mainFileSubject.release();
|
||||
termWrap.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleHtmlKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.code === "Escape" && event.metaKey) {
|
||||
// reset term:mode
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
|
||||
@ -280,15 +200,24 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFocused && termMode == "term") {
|
||||
termRef.current?.focus();
|
||||
termRef.current?.terminal.focus();
|
||||
}
|
||||
if (isFocused && termMode == "html") {
|
||||
htmlElemFocusRef.current?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
let stickerConfig = {
|
||||
charWidth: 8,
|
||||
charHeight: 16,
|
||||
rows: termRef.current?.terminal.rows ?? 24,
|
||||
cols: termRef.current?.terminal.cols ?? 80,
|
||||
blockId: blockId,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}>
|
||||
<TermStickers config={stickerConfig} />
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
<div
|
||||
key="htmlElem"
|
||||
@ -305,7 +234,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
type="text"
|
||||
value={""}
|
||||
ref={htmlElemFocusRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={handleHtmlKeyDown}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
|
204
frontend/app/view/term/termsticker.tsx
Normal file
204
frontend/app/view/term/termsticker.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { createBlock, getBackendHostPort } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import clsx from "clsx";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import GaugeChart from "react-gauge-chart";
|
||||
import "./term.less";
|
||||
|
||||
type StickerType = {
|
||||
position: "absolute";
|
||||
top?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
color?: string;
|
||||
opacity?: number;
|
||||
pointerevents?: boolean;
|
||||
fontsize?: number;
|
||||
transform?: string;
|
||||
|
||||
stickertype: "icon" | "image" | "gauge";
|
||||
icon?: string;
|
||||
imgsrc?: string;
|
||||
clickcmd?: string;
|
||||
clickblockdef?: BlockDef;
|
||||
};
|
||||
|
||||
type StickerTermConfig = {
|
||||
charWidth: number;
|
||||
charHeight: number;
|
||||
rows: number;
|
||||
cols: number;
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
function convertWidthDimToPx(dim: number, config: StickerTermConfig) {
|
||||
if (dim == null) {
|
||||
return null;
|
||||
}
|
||||
return dim * config.charWidth;
|
||||
}
|
||||
|
||||
function convertHeightDimToPx(dim: number, config: StickerTermConfig) {
|
||||
if (dim == null) {
|
||||
return null;
|
||||
}
|
||||
return dim * config.charHeight;
|
||||
}
|
||||
|
||||
var valueAtom = jotai.atom(Math.random() * 100);
|
||||
|
||||
function GaugeSticker() {
|
||||
let [value, setValue] = jotai.useAtom(valueAtom);
|
||||
React.useEffect(() => {
|
||||
let interval = setInterval(() => {
|
||||
var amt = Math.random() * 10 - 5;
|
||||
setValue((value) => Math.max(0, Math.min(100, value + amt)));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
return <GaugeChart id="gauge-chart1" nrOfLevels={20} percent={value / 100} />;
|
||||
}
|
||||
|
||||
function TermSticker({ sticker, config }: { sticker: StickerType; config: StickerTermConfig }) {
|
||||
let style: React.CSSProperties = {
|
||||
position: sticker.position,
|
||||
top: convertHeightDimToPx(sticker.top, config),
|
||||
left: convertWidthDimToPx(sticker.left, config),
|
||||
right: convertWidthDimToPx(sticker.right, config),
|
||||
bottom: convertHeightDimToPx(sticker.bottom, config),
|
||||
width: convertWidthDimToPx(sticker.width, config),
|
||||
height: convertHeightDimToPx(sticker.height, config),
|
||||
color: sticker.color,
|
||||
fontSize: sticker.fontsize,
|
||||
transform: sticker.transform,
|
||||
opacity: sticker.opacity,
|
||||
fill: sticker.color,
|
||||
stroke: sticker.color,
|
||||
};
|
||||
if (sticker.pointerevents) {
|
||||
style.pointerEvents = "auto";
|
||||
}
|
||||
if (style.width != null) {
|
||||
style.overflowX = "hidden";
|
||||
}
|
||||
if (style.height != null) {
|
||||
style.overflowY = "hidden";
|
||||
}
|
||||
let clickHandler = null;
|
||||
if (sticker.pointerevents && (sticker.clickcmd || sticker.clickblockdef)) {
|
||||
style.cursor = "pointer";
|
||||
clickHandler = () => {
|
||||
console.log("clickHandler", sticker.clickcmd, sticker.clickblockdef);
|
||||
if (sticker.clickcmd) {
|
||||
const b64data = btoa(sticker.clickcmd);
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
services.BlockService.SendCommand(config.blockId, inputCmd);
|
||||
}
|
||||
if (sticker.clickblockdef) {
|
||||
createBlock(sticker.clickblockdef);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (sticker.stickertype == "icon") {
|
||||
return (
|
||||
<div className="term-sticker" style={style} onClick={clickHandler}>
|
||||
<i className={clsx("fa", "fa-" + sticker.icon)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (sticker.stickertype == "image") {
|
||||
if (sticker.imgsrc == null) {
|
||||
return null;
|
||||
}
|
||||
const streamingUrl = getBackendHostPort() + "/wave/stream-file?path=" + encodeURIComponent(sticker.imgsrc);
|
||||
return (
|
||||
<div className="term-sticker term-sticker-image" style={style} onClick={clickHandler}>
|
||||
<img src={streamingUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (sticker.stickertype == "gauge") {
|
||||
return (
|
||||
<div className="term-sticker term-sticker-gauge" style={style}>
|
||||
<GaugeSticker />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function TermStickers({ config }: { config: StickerTermConfig }) {
|
||||
let stickers: StickerType[] = [];
|
||||
if (config.blockId.startsWith("d1eaddcb")) {
|
||||
stickers.push({
|
||||
position: "absolute",
|
||||
top: 5,
|
||||
right: 7,
|
||||
stickertype: "icon",
|
||||
icon: "paw",
|
||||
color: "#40cc40aa",
|
||||
fontsize: 30,
|
||||
transform: "rotate(-18deg)",
|
||||
pointerevents: true,
|
||||
clickcmd: "ls\n",
|
||||
});
|
||||
stickers.push({
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
stickertype: "icon",
|
||||
icon: "paw",
|
||||
color: "#4040ccaa",
|
||||
fontsize: 30,
|
||||
transform: "rotate(-20deg)",
|
||||
pointerevents: true,
|
||||
clickcmd: "git status\n",
|
||||
});
|
||||
stickers.push({
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
right: 5,
|
||||
stickertype: "icon",
|
||||
icon: "paw",
|
||||
color: "#cc4040aa",
|
||||
fontsize: 30,
|
||||
transform: "rotate(-15deg)",
|
||||
pointerevents: true,
|
||||
clickcmd: "cd ~/work/wails/thenextwave\n",
|
||||
});
|
||||
stickers.push({
|
||||
position: "absolute",
|
||||
top: 18,
|
||||
right: 8,
|
||||
stickertype: "image",
|
||||
width: 12,
|
||||
height: 6,
|
||||
imgsrc: "~/Downloads/natureicon.png",
|
||||
opacity: 0.8,
|
||||
pointerevents: true,
|
||||
clickblockdef: { view: "preview", meta: { file: "~/" } },
|
||||
});
|
||||
stickers.push({
|
||||
position: "absolute",
|
||||
top: 2,
|
||||
right: 25,
|
||||
width: 20,
|
||||
stickertype: "gauge",
|
||||
opacity: 0.7,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div className="term-stickers">
|
||||
{stickers.map((sticker, i) => (
|
||||
<TermSticker key={i} sticker={sticker} config={config} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
172
frontend/app/view/term/termwrap.ts
Normal file
172
frontend/app/view/term/termwrap.ts
Normal file
@ -0,0 +1,172 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fetchWaveFile, getFileSubject, sendWSCommand } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import { base64ToArray } from "@/util/util";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { SerializeAddon } from "@xterm/addon-serialize";
|
||||
import * as TermTypes from "@xterm/xterm";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { debounce } from "throttle-debounce";
|
||||
|
||||
export class TermWrap {
|
||||
blockId: string;
|
||||
ptyOffset: number;
|
||||
dataBytesProcessed: number;
|
||||
terminal: Terminal;
|
||||
connectElem: HTMLDivElement;
|
||||
fitAddon: FitAddon;
|
||||
serializeAddon: SerializeAddon;
|
||||
mainFileSubject: SubjectWithRef<Uint8Array>;
|
||||
loaded: boolean;
|
||||
heldData: Uint8Array[];
|
||||
handleResize_debounced: () => void;
|
||||
|
||||
constructor(
|
||||
blockId: string,
|
||||
connectElem: HTMLDivElement,
|
||||
options?: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions
|
||||
) {
|
||||
this.blockId = blockId;
|
||||
this.ptyOffset = 0;
|
||||
this.dataBytesProcessed = 0;
|
||||
this.terminal = new Terminal(options);
|
||||
this.fitAddon = new FitAddon();
|
||||
this.serializeAddon = new SerializeAddon();
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
this.terminal.loadAddon(this.serializeAddon);
|
||||
this.connectElem = connectElem;
|
||||
this.mainFileSubject = null;
|
||||
this.loaded = false;
|
||||
this.heldData = [];
|
||||
this.handleResize_debounced = debounce(50, this.handleResize.bind(this));
|
||||
this.terminal.open(this.connectElem);
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
async initTerminal() {
|
||||
this.connectElem.addEventListener("keydown", this.keydownListener.bind(this), true);
|
||||
this.terminal.onData(this.handleTermData.bind(this));
|
||||
this.mainFileSubject = getFileSubject(this.blockId, "main");
|
||||
this.mainFileSubject.subscribe(this.handleNewFileSubjectData.bind(this));
|
||||
try {
|
||||
await this.loadInitialTerminalData();
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
this.runProcessIdleTimeout();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.terminal.dispose();
|
||||
this.mainFileSubject.release();
|
||||
}
|
||||
|
||||
handleTermData(data: string) {
|
||||
const b64data = btoa(data);
|
||||
if (b64data.length < 512) {
|
||||
const wsCmd: BlockInputWSCommand = { wscommand: "blockinput", blockid: this.blockId, inputdata64: b64data };
|
||||
sendWSCommand(wsCmd);
|
||||
} else {
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
services.BlockService.SendCommand(this.blockId, inputCmd);
|
||||
}
|
||||
}
|
||||
|
||||
addFocusListener(focusFn: () => void) {
|
||||
this.terminal.textarea.addEventListener("focus", focusFn);
|
||||
}
|
||||
|
||||
keydownListener(ev: KeyboardEvent) {
|
||||
if (ev.code == "Escape" && ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
|
||||
services.BlockService.SendCommand(this.blockId, metaCmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
handleNewFileSubjectData(msg: WSFileEventData) {
|
||||
if (msg.fileop != "append") {
|
||||
console.log("bad fileop for terminal", msg);
|
||||
return;
|
||||
}
|
||||
const decodedData = base64ToArray(msg.data64);
|
||||
if (this.loaded) {
|
||||
this.doTerminalWrite(decodedData, null);
|
||||
} else {
|
||||
this.heldData.push(decodedData);
|
||||
}
|
||||
}
|
||||
|
||||
doTerminalWrite(data: string | Uint8Array, setPtyOffset?: number): Promise<void> {
|
||||
let resolve: () => void = null;
|
||||
let prtn = new Promise<void>((presolve, _) => {
|
||||
resolve = presolve;
|
||||
});
|
||||
this.terminal.write(data, () => {
|
||||
if (setPtyOffset != null) {
|
||||
this.ptyOffset = setPtyOffset;
|
||||
} else {
|
||||
this.ptyOffset += data.length;
|
||||
this.dataBytesProcessed += data.length;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
return prtn;
|
||||
}
|
||||
|
||||
async loadInitialTerminalData(): Promise<void> {
|
||||
let startTs = Date.now();
|
||||
const { data: cacheData, fileInfo: cacheFile } = await fetchWaveFile(this.blockId, "cache:term:full");
|
||||
let ptyOffset = 0;
|
||||
if (cacheFile != null) {
|
||||
ptyOffset = cacheFile.meta["ptyoffset"] ?? 0;
|
||||
if (cacheData.byteLength > 0) {
|
||||
this.doTerminalWrite(cacheData, ptyOffset);
|
||||
}
|
||||
}
|
||||
const { data: mainData, fileInfo: mainFile } = await fetchWaveFile(this.blockId, "main", ptyOffset);
|
||||
console.log(
|
||||
`terminal loaded cachefile:${cacheData?.byteLength ?? 0} main:${mainData?.byteLength ?? 0} bytes, ${Date.now() - startTs}ms`
|
||||
);
|
||||
if (mainFile != null) {
|
||||
await this.doTerminalWrite(mainData, null);
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const oldRows = this.terminal.rows;
|
||||
const oldCols = this.terminal.cols;
|
||||
this.fitAddon.fit();
|
||||
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
|
||||
const wsCommand: SetBlockTermSizeWSCommand = {
|
||||
wscommand: "setblocktermsize",
|
||||
blockid: this.blockId,
|
||||
termsize: { rows: this.terminal.rows, cols: this.terminal.cols },
|
||||
};
|
||||
sendWSCommand(wsCommand);
|
||||
}
|
||||
}
|
||||
|
||||
processAndCacheData() {
|
||||
if (this.dataBytesProcessed < 10 * 1024) {
|
||||
return;
|
||||
}
|
||||
const serializedOutput = this.serializeAddon.serialize();
|
||||
console.log("idle timeout term", this.dataBytesProcessed, serializedOutput.length);
|
||||
services.BlockService.SaveTerminalState(this.blockId, serializedOutput, "full", this.ptyOffset);
|
||||
this.dataBytesProcessed = 0;
|
||||
}
|
||||
|
||||
runProcessIdleTimeout() {
|
||||
setTimeout(() => {
|
||||
window.requestIdleCallback(() => {
|
||||
this.processAndCacheData();
|
||||
this.runProcessIdleTimeout();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
}
|
@ -3,41 +3,13 @@
|
||||
|
||||
import { TabBar } from "@/app/tab/tabbar";
|
||||
import { TabContent } from "@/app/tab/tabcontent";
|
||||
import { atoms } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { atoms, createBlock } from "@/store/global";
|
||||
import * as jotai from "jotai";
|
||||
import { CenteredDiv } from "../element/quickelems";
|
||||
|
||||
import { LayoutTreeActionType, LayoutTreeInsertNodeAction, newLayoutNode } from "@/faraday/index";
|
||||
import { getLayoutStateAtomForTab, useLayoutTreeStateReducerAtom } from "@/faraday/lib/layoutAtom";
|
||||
import { useMemo } from "react";
|
||||
import "./workspace.less";
|
||||
|
||||
function Widgets() {
|
||||
const windowData = jotai.useAtomValue(atoms.waveWindow);
|
||||
const activeTabAtom = useMemo(() => {
|
||||
return getLayoutStateAtomForTab(
|
||||
windowData.activetabid,
|
||||
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", windowData.activetabid))
|
||||
);
|
||||
}, [windowData.activetabid]);
|
||||
const [, dispatchLayoutStateAction] = useLayoutTreeStateReducerAtom(activeTabAtom);
|
||||
|
||||
function addBlockToTab(blockId: string) {
|
||||
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
|
||||
type: LayoutTreeActionType.InsertNode,
|
||||
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, { blockId }),
|
||||
};
|
||||
dispatchLayoutStateAction(insertNodeAction);
|
||||
}
|
||||
|
||||
async function createBlock(blockDef: BlockDef) {
|
||||
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
||||
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
|
||||
addBlockToTab(blockId);
|
||||
}
|
||||
|
||||
async function clickTerminal() {
|
||||
const termBlockDef = {
|
||||
controller: "shell",
|
||||
|
2
frontend/types/custom.d.ts
vendored
2
frontend/types/custom.d.ts
vendored
@ -10,6 +10,8 @@ declare global {
|
||||
isDev: () => boolean;
|
||||
isDevServer: () => boolean;
|
||||
};
|
||||
|
||||
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
|
||||
}
|
||||
|
||||
export {};
|
||||
|
80
frontend/types/gotypes.d.ts
vendored
80
frontend/types/gotypes.d.ts
vendored
@ -4,12 +4,14 @@
|
||||
// generated by cmd/generate/main-generate.go
|
||||
|
||||
declare global {
|
||||
|
||||
// wstore.Block
|
||||
type Block = WaveObj & {
|
||||
blockdef: BlockDef;
|
||||
controller: string;
|
||||
view: string;
|
||||
runtimeopts?: RuntimeOpts;
|
||||
stickers?: StickerType[];
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
@ -29,28 +31,20 @@ declare global {
|
||||
|
||||
type BlockCommand = {
|
||||
command: string;
|
||||
} & (
|
||||
| BlockSetMetaCommand
|
||||
| BlockGetMetaCommand
|
||||
| BlockMessageCommand
|
||||
| BlockAppendFileCommand
|
||||
| BlockAppendIJsonCommand
|
||||
| BlockInputCommand
|
||||
| BlockSetViewCommand
|
||||
);
|
||||
} & ( BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand );
|
||||
|
||||
// wstore.BlockDef
|
||||
type BlockDef = {
|
||||
controller?: string;
|
||||
view?: string;
|
||||
files?: { [key: string]: FileDef };
|
||||
files?: {[key: string]: FileDef};
|
||||
meta?: MetaType;
|
||||
};
|
||||
|
||||
// wshutil.BlockGetMetaCommand
|
||||
type BlockGetMetaCommand = {
|
||||
command: "getmeta";
|
||||
oid: string;
|
||||
oref: string;
|
||||
};
|
||||
|
||||
// wshutil.BlockInputCommand
|
||||
@ -61,6 +55,13 @@ declare global {
|
||||
termsize?: TermSize;
|
||||
};
|
||||
|
||||
// webcmd.BlockInputWSCommand
|
||||
type BlockInputWSCommand = {
|
||||
wscommand: "blockinput";
|
||||
blockid: string;
|
||||
inputdata64: string;
|
||||
};
|
||||
|
||||
// wshutil.BlockMessageCommand
|
||||
type BlockMessageCommand = {
|
||||
command: "message";
|
||||
@ -70,7 +71,7 @@ declare global {
|
||||
// wshutil.BlockSetMetaCommand
|
||||
type BlockSetMetaCommand = {
|
||||
command: "setmeta";
|
||||
oid?: string;
|
||||
oref?: string;
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
@ -106,6 +107,14 @@ declare global {
|
||||
mimetype?: string;
|
||||
};
|
||||
|
||||
// filestore.FileOptsType
|
||||
type FileOptsType = {
|
||||
maxsize?: number;
|
||||
circular?: boolean;
|
||||
ijson?: boolean;
|
||||
ijsonbudget?: number;
|
||||
};
|
||||
|
||||
// fileservice.FullFile
|
||||
type FullFile = {
|
||||
info: FileInfo;
|
||||
@ -118,7 +127,7 @@ declare global {
|
||||
meta?: MetaType;
|
||||
};
|
||||
|
||||
type MetaType = { [key: string]: any };
|
||||
type MetaType = {[key: string]: any}
|
||||
|
||||
// tsgenmeta.MethodMeta
|
||||
type MethodMeta = {
|
||||
@ -139,6 +148,12 @@ declare global {
|
||||
y: number;
|
||||
};
|
||||
|
||||
// wshutil.ResolveIdsCommand
|
||||
type ResolveIdsCommand = {
|
||||
command: "resolveids";
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
// wstore.RuntimeOpts
|
||||
type RuntimeOpts = {
|
||||
termsize?: TermSize;
|
||||
@ -152,6 +167,27 @@ declare global {
|
||||
termsize: TermSize;
|
||||
};
|
||||
|
||||
// wstore.StickerClickOptsType
|
||||
type StickerClickOptsType = {
|
||||
sendinput?: string;
|
||||
createblock?: BlockDef;
|
||||
};
|
||||
|
||||
// wstore.StickerDisplayOptsType
|
||||
type StickerDisplayOptsType = {
|
||||
icon: string;
|
||||
imgsrc: string;
|
||||
svgblob?: string;
|
||||
};
|
||||
|
||||
// wstore.StickerType
|
||||
type StickerType = {
|
||||
stickertype: string;
|
||||
style: MetaType;
|
||||
clickopts?: StickerClickOptsType;
|
||||
display: StickerDisplayOptsType;
|
||||
};
|
||||
|
||||
// wstore.Tab
|
||||
type Tab = WaveObj & {
|
||||
name: string;
|
||||
@ -174,7 +210,7 @@ declare global {
|
||||
|
||||
type WSCommandType = {
|
||||
wscommand: string;
|
||||
} & SetBlockTermSizeWSCommand;
|
||||
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand );
|
||||
|
||||
// eventbus.WSEventType
|
||||
type WSEventType = {
|
||||
@ -191,6 +227,17 @@ declare global {
|
||||
data64: string;
|
||||
};
|
||||
|
||||
// filestore.WaveFile
|
||||
type WaveFile = {
|
||||
zoneid: string;
|
||||
name: string;
|
||||
opts: FileOptsType;
|
||||
createdts: number;
|
||||
size: number;
|
||||
modts: number;
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
// waveobj.WaveObj
|
||||
type WaveObj = {
|
||||
otype: string;
|
||||
@ -233,7 +280,7 @@ declare global {
|
||||
workspaceid: string;
|
||||
activetabid: string;
|
||||
activeblockid?: string;
|
||||
activeblockmap: { [key: string]: string };
|
||||
activeblockmap: {[key: string]: string};
|
||||
pos: Point;
|
||||
winsize: WinSize;
|
||||
lastfocusts: number;
|
||||
@ -246,6 +293,7 @@ declare global {
|
||||
tabids: string[];
|
||||
meta: MetaType;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export {};
|
||||
export {}
|
||||
|
@ -55,6 +55,7 @@
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -69,6 +70,7 @@
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^5.2.7",
|
||||
"react-gauge-chart": "^0.5.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
|
@ -402,7 +402,6 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
|
||||
}
|
||||
inputUnion.InputData = inputBuf[:nw]
|
||||
}
|
||||
log.Printf("INPUT: %s | %q\n", bc.BlockId, string(inputUnion.InputData))
|
||||
bc.ShellInputCh <- inputUnion
|
||||
default:
|
||||
log.Printf("unknown command type %T\n", cmd)
|
||||
|
@ -185,6 +185,9 @@ func (s *FileStore) Stat(ctx context.Context, zoneId string, name string) (*Wave
|
||||
return withLockRtn(s, zoneId, name, func(entry *CacheEntry) (*WaveFile, error) {
|
||||
file, err := entry.loadFileForRead(ctx)
|
||||
if err != nil {
|
||||
if err == fs.ErrNotExist {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("error getting file: %v", err)
|
||||
}
|
||||
return file.DeepCopy(), nil
|
||||
|
@ -4,13 +4,16 @@
|
||||
package blockservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
|
||||
type BlockService struct{}
|
||||
@ -38,3 +41,24 @@ func (bs *BlockService) SendCommand(blockId string, cmd wshutil.BlockCommand) er
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *BlockService) SaveTerminalState(ctx context.Context, blockId string, state string, stateType string, ptyOffset int64) error {
|
||||
_, err := wstore.DBMustGet[*wstore.Block](ctx, blockId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stateType != "full" && stateType != "preview" {
|
||||
return fmt.Errorf("invalid state type: %q", stateType)
|
||||
}
|
||||
// ignore MakeFile error (already exists is ok)
|
||||
filestore.WFS.MakeFile(ctx, blockId, "cache:term:"+stateType, nil, filestore.FileOptsType{})
|
||||
err = filestore.WFS.WriteFile(ctx, blockId, "cache:term:"+stateType, []byte(state))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot save terminal state: %w", err)
|
||||
}
|
||||
err = filestore.WFS.WriteMeta(ctx, blockId, "cache:term:"+stateType, filestore.FileMeta{"ptyoffset": ptyOffset}, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot save terminal state meta: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/service"
|
||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
@ -364,6 +365,7 @@ func GenerateWaveObjTypes(tsTypesMap map[reflect.Type]string) {
|
||||
GenerateTSType(reflect.TypeOf(wstore.UIContext{}), tsTypesMap)
|
||||
GenerateTSType(reflect.TypeOf(eventbus.WSEventType{}), tsTypesMap)
|
||||
GenerateTSType(reflect.TypeOf(eventbus.WSFileEventData{}), tsTypesMap)
|
||||
GenerateTSType(reflect.TypeOf(filestore.WaveFile{}), tsTypesMap)
|
||||
for _, rtype := range wstore.AllWaveObjTypes() {
|
||||
GenerateTSType(rtype, tsTypesMap)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -102,6 +103,15 @@ func marshalReturnValue(data any, err error) []byte {
|
||||
func handleWaveFile(w http.ResponseWriter, r *http.Request) {
|
||||
zoneId := r.URL.Query().Get("zoneid")
|
||||
name := r.URL.Query().Get("name")
|
||||
offsetStr := r.URL.Query().Get("offset")
|
||||
var offset int64 = 0
|
||||
if offsetStr != "" {
|
||||
var err error
|
||||
offset, err = strconv.ParseInt(offsetStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid offset: %v", err), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
if _, err := uuid.Parse(zoneId); err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid zoneid: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
@ -113,7 +123,7 @@ func handleWaveFile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
file, err := filestore.WFS.Stat(r.Context(), zoneId, name)
|
||||
if err == fs.ErrNotExist {
|
||||
http.NotFound(w, r)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
@ -125,15 +135,19 @@ func handleWaveFile(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf("error serializing file info: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
// can make more efficient by checking modtime + If-Modified-Since headers to allow caching
|
||||
dataStartIdx := file.DataStartIdx()
|
||||
if offset >= dataStartIdx {
|
||||
dataStartIdx = offset
|
||||
}
|
||||
w.Header().Set(ContentTypeHeaderKey, ContentTypeBinary)
|
||||
w.Header().Set(ContentLengthHeaderKey, fmt.Sprintf("%d", file.Size))
|
||||
w.Header().Set(ContentLengthHeaderKey, fmt.Sprintf("%d", file.Size-dataStartIdx))
|
||||
w.Header().Set(WaveZoneFileInfoHeaderKey, base64.StdEncoding.EncodeToString(jsonFileBArr))
|
||||
w.Header().Set(LastModifiedHeaderKey, time.UnixMilli(file.ModTs).UTC().Format(http.TimeFormat))
|
||||
if file.Size == 0 {
|
||||
if dataStartIdx >= file.Size {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
for offset := file.DataStartIdx(); offset < file.Size; offset += filestore.DefaultPartDataSize {
|
||||
for offset := dataStartIdx; offset < file.Size; offset += filestore.DefaultPartDataSize {
|
||||
_, data, err := filestore.WFS.ReadAt(r.Context(), zoneId, name, offset, filestore.DefaultPartDataSize)
|
||||
if err != nil {
|
||||
if offset == 0 {
|
||||
@ -177,6 +191,7 @@ func WebFnWrap(opts WebFnOpts, fn WebFnType) WebFnType {
|
||||
if !opts.AllowCaching {
|
||||
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
|
||||
}
|
||||
w.Header().Set("Access-Control-Expose-Headers", "X-ZoneFileInfo")
|
||||
// reqAuthKey := r.Header.Get("X-AuthKey")
|
||||
// if reqAuthKey == "" {
|
||||
// w.WriteHeader(http.StatusInternalServerError)
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
|
||||
const (
|
||||
WSCommand_SetBlockTermSize = "setblocktermsize"
|
||||
WSCommand_BlockInput = "blockinput"
|
||||
)
|
||||
|
||||
type WSCommandType interface {
|
||||
@ -26,6 +27,7 @@ func WSCommandTypeUnionMeta() tsgenmeta.TypeUnionMeta {
|
||||
TypeFieldName: "wscommand",
|
||||
Types: []reflect.Type{
|
||||
reflect.TypeOf(SetBlockTermSizeWSCommand{}),
|
||||
reflect.TypeOf(BlockInputWSCommand{}),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -40,6 +42,16 @@ func (cmd *SetBlockTermSizeWSCommand) GetWSCommand() string {
|
||||
return cmd.WSCommand
|
||||
}
|
||||
|
||||
type BlockInputWSCommand struct {
|
||||
WSCommand string `json:"wscommand" tstype:"\"blockinput\""`
|
||||
BlockId string `json:"blockid"`
|
||||
InputData64 string `json:"inputdata64"`
|
||||
}
|
||||
|
||||
func (cmd *BlockInputWSCommand) GetWSCommand() string {
|
||||
return cmd.WSCommand
|
||||
}
|
||||
|
||||
func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) {
|
||||
cmdType, ok := cmdMap["wscommand"].(string)
|
||||
if !ok {
|
||||
@ -53,6 +65,13 @@ func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) {
|
||||
return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err)
|
||||
}
|
||||
return &cmd, nil
|
||||
case WSCommand_BlockInput:
|
||||
var cmd BlockInputWSCommand
|
||||
err := utilfn.DoMapStucture(&cmd, cmdMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding BlockInputWSCommand: %w", err)
|
||||
}
|
||||
return &cmd, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown wscommand type %q", cmdType)
|
||||
}
|
||||
|
@ -100,6 +100,12 @@ func processWSCommand(jmsg map[string]any, outputCh chan any) {
|
||||
TermSize: &cmd.TermSize,
|
||||
}
|
||||
blockservice.BlockServiceInstance.SendCommand(cmd.BlockId, blockCmd)
|
||||
case *webcmd.BlockInputWSCommand:
|
||||
blockCmd := &wshutil.BlockInputCommand{
|
||||
Command: wshutil.BlockCommand_Input,
|
||||
InputData64: cmd.InputData64,
|
||||
}
|
||||
blockservice.BlockServiceInstance.SendCommand(cmd.BlockId, blockCmd)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,6 +201,24 @@ type BlockDef struct {
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type StickerClickOptsType struct {
|
||||
SendInput string `json:"sendinput,omitempty"`
|
||||
CreateBlock *BlockDef `json:"createblock,omitempty"`
|
||||
}
|
||||
|
||||
type StickerDisplayOptsType struct {
|
||||
Icon string `json:"icon"`
|
||||
ImgSrc string `json:"imgsrc"`
|
||||
SvgBlob string `json:"svgblob,omitempty"`
|
||||
}
|
||||
|
||||
type StickerType struct {
|
||||
StickerType string `json:"stickertype"`
|
||||
Style map[string]any `json:"style"`
|
||||
ClickOpts *StickerClickOptsType `json:"clickopts,omitempty"`
|
||||
Display *StickerDisplayOptsType `json:"display"`
|
||||
}
|
||||
|
||||
type RuntimeOpts struct {
|
||||
TermSize shellexec.TermSize `json:"termsize,omitempty"`
|
||||
WinSize WinSize `json:"winsize,omitempty"`
|
||||
@ -223,6 +241,7 @@ type Block struct {
|
||||
Controller string `json:"controller"`
|
||||
View string `json:"view"`
|
||||
RuntimeOpts *RuntimeOpts `json:"runtimeopts,omitempty"`
|
||||
Stickers []*StickerType `json:"stickers,omitempty"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
}
|
||||
|
||||
|
25
yarn.lock
25
yarn.lock
@ -4706,6 +4706,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@xterm/addon-serialize@npm:^0.13.0":
|
||||
version: 0.13.0
|
||||
resolution: "@xterm/addon-serialize@npm:0.13.0"
|
||||
peerDependencies:
|
||||
"@xterm/xterm": ^5.0.0
|
||||
checksum: 10c0/090f502867250cdceca9abdbe37aebe0c01eb5d708a09c51d3e0e9bb5d4d3b0d4d67af1c4c8bde9b78f108a5e4150420180de69e6228aac76596fdbf6c4c59dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@xterm/xterm@npm:^5.5.0":
|
||||
version: 5.5.0
|
||||
resolution: "@xterm/xterm@npm:5.5.0"
|
||||
@ -6051,7 +6060,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3@npm:^7.9.0":
|
||||
"d3@npm:^7.6.1, d3@npm:^7.9.0":
|
||||
version: 7.9.0
|
||||
resolution: "d3@npm:7.9.0"
|
||||
dependencies:
|
||||
@ -11026,6 +11035,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-gauge-chart@npm:^0.5.1":
|
||||
version: 0.5.1
|
||||
resolution: "react-gauge-chart@npm:0.5.1"
|
||||
dependencies:
|
||||
d3: "npm:^7.6.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.2 || ^17.0 || ^18.x
|
||||
react-dom: ^16.8.2 || ^17.0 || ^18.x
|
||||
checksum: 10c0/16d0142130ed56c8c5cd710d596499b44ccf654ea8973a4ed973ff8dca6b01e0d03b9885c34276cf678cd0f6bae1eee836d921a3bcbdbbf4a21eadd9f1686b90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:18.1.0":
|
||||
version: 18.1.0
|
||||
resolution: "react-is@npm:18.1.0"
|
||||
@ -12268,6 +12289,7 @@ __metadata:
|
||||
"@vitejs/plugin-react": "npm:^4.3.0"
|
||||
"@vitest/coverage-istanbul": "npm:^1.6.0"
|
||||
"@xterm/addon-fit": "npm:^0.10.0"
|
||||
"@xterm/addon-serialize": "npm:^0.13.0"
|
||||
"@xterm/xterm": "npm:^5.5.0"
|
||||
base64-js: "npm:^1.5.1"
|
||||
clsx: "npm:^2.1.1"
|
||||
@ -12289,6 +12311,7 @@ __metadata:
|
||||
react-dnd-html5-backend: "npm:^16.0.1"
|
||||
react-dom: "npm:^18.3.1"
|
||||
react-frame-component: "npm:^5.2.7"
|
||||
react-gauge-chart: "npm:^0.5.1"
|
||||
react-markdown: "npm:^9.0.1"
|
||||
remark-gfm: "npm:^4.0.0"
|
||||
rxjs: "npm:^7.8.1"
|
||||
|
Loading…
Reference in New Issue
Block a user