waveterm/frontend/app/view/term/term.tsx
Mike Sawka 72f36a9639
wsh run (#1376)
implements `wsh run` command.  lots of fixes (and new options) for command blocks.  cleans up the UX/UI for command blocks.  lots of bug fixes for blockcontrollers.  other minor bug fixes.

also makes editor:* vars into settings override atoms.
2024-12-04 14:16:50 -08:00

817 lines
30 KiB
TypeScript

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Block, SubBlock } from "@/app/block/block";
import { BlockNodeModel } from "@/app/block/blocktypes";
import { getAllGlobalKeyBindings } from "@/app/store/keymodel";
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { TermWshClient } from "@/app/view/term/term-wsh";
import { VDomModel } from "@/app/view/vdom/vdom-model";
import {
atoms,
getBlockComponentModel,
getBlockMetaKeyAtom,
getConnStatusAtom,
getOverrideConfigAtom,
getSettingsKeyAtom,
globalStore,
useBlockAtom,
useSettingsPrefixAtom,
WOS,
} from "@/store/global";
import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
import clsx from "clsx";
import debug from "debug";
import * as jotai from "jotai";
import * as React from "react";
import { TermStickers } from "./termsticker";
import { TermThemeUpdater } from "./termtheme";
import { computeTheme, DefaultTermTheme } from "./termutil";
import { TermWrap } from "./termwrap";
import "./xterm.css";
const dlog = debug("wave:term");
type InitialLoadDataType = {
loaded: boolean;
heldData: Uint8Array[];
};
class TermViewModel {
viewType: string;
nodeModel: BlockNodeModel;
connected: boolean;
termRef: React.RefObject<TermWrap>;
blockAtom: jotai.Atom<Block>;
termMode: jotai.Atom<string>;
blockId: string;
viewIcon: jotai.Atom<string>;
viewName: jotai.Atom<string>;
viewText: jotai.Atom<HeaderElem[]>;
blockBg: jotai.Atom<MetaType>;
manageConnection: jotai.Atom<boolean>;
connStatus: jotai.Atom<ConnStatus>;
termWshClient: TermWshClient;
vdomBlockId: jotai.Atom<string>;
vdomToolbarBlockId: jotai.Atom<string>;
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
fontSizeAtom: jotai.Atom<number>;
termThemeNameAtom: jotai.Atom<string>;
noPadding: jotai.PrimitiveAtom<boolean>;
endIconButtons: jotai.Atom<IconButtonDecl[]>;
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
shellProcStatus: jotai.Atom<string>;
shellProcStatusUnsubFn: () => void;
isCmdController: jotai.Atom<boolean>;
isRestarting: jotai.PrimitiveAtom<boolean>;
constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "term";
this.blockId = blockId;
this.termWshClient = new TermWshClient(blockId, this);
DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.termWshClient);
this.nodeModel = nodeModel;
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.vdomBlockId = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:vdomblockid"];
});
this.vdomToolbarBlockId = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:vdomtoolbarblockid"];
});
this.vdomToolbarTarget = jotai.atom<VDomTargetToolbar>(null) as jotai.PrimitiveAtom<VDomTargetToolbar>;
this.termMode = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:mode"] ?? "term";
});
this.isRestarting = jotai.atom(false);
this.viewIcon = jotai.atom((get) => {
const termMode = get(this.termMode);
if (termMode == "vdom") {
return "bolt";
}
const isCmd = get(this.isCmdController);
if (isCmd) {
}
return "terminal";
});
this.viewName = jotai.atom((get) => {
const blockData = get(this.blockAtom);
const termMode = get(this.termMode);
if (termMode == "vdom") {
return "Wave App";
}
if (blockData?.meta?.controller == "cmd") {
return "";
}
return "Terminal";
});
this.viewText = jotai.atom((get) => {
const termMode = get(this.termMode);
if (termMode == "vdom") {
return [
{
elemtype: "iconbutton",
icon: "square-terminal",
title: "Switch back to Terminal",
click: () => {
this.setTermMode("term");
},
},
];
}
const vdomBlockId = get(this.vdomBlockId);
const rtn = [];
if (vdomBlockId) {
rtn.push({
elemtype: "iconbutton",
icon: "bolt",
title: "Switch to Wave App",
click: () => {
this.setTermMode("vdom");
},
});
}
const isCmd = get(this.isCmdController);
if (isCmd) {
const blockMeta = get(this.blockAtom)?.meta;
let cmdText = blockMeta?.["cmd"];
let cmdArgs = blockMeta?.["cmd:args"];
if (cmdArgs != null && Array.isArray(cmdArgs) && cmdArgs.length > 0) {
cmdText += " " + cmdArgs.join(" ");
}
rtn.push({
elemtype: "text",
text: cmdText,
noGrow: true,
});
const isRestarting = get(this.isRestarting);
if (isRestarting) {
rtn.push({
elemtype: "iconbutton",
icon: "refresh",
iconColor: "var(--success-color)",
iconSpin: true,
title: "Restarting Command",
noAction: true,
});
} else {
const fullShellProcStatus = get(this.shellProcFullStatus);
if (fullShellProcStatus?.shellprocstatus == "done") {
if (fullShellProcStatus?.shellprocexitcode == 0) {
rtn.push({
elemtype: "iconbutton",
icon: "check",
iconColor: "var(--success-color)",
title: "Command Exited Successfully",
noAction: true,
});
} else {
rtn.push({
elemtype: "iconbutton",
icon: "xmark-large",
iconColor: "var(--error-color)",
title: "Exit Code: " + fullShellProcStatus?.shellprocexitcode,
noAction: true,
});
}
}
}
}
return rtn;
});
this.manageConnection = jotai.atom((get) => {
const termMode = get(this.termMode);
if (termMode == "vdom") {
return false;
}
const isCmd = get(this.isCmdController);
if (isCmd) {
return false;
}
return true;
});
this.termThemeNameAtom = useBlockAtom(blockId, "termthemeatom", () => {
return jotai.atom<string>((get) => {
return get(getOverrideConfigAtom(this.blockId, "term:theme")) ?? DefaultTermTheme;
});
});
this.blockBg = jotai.atom((get) => {
const fullConfig = get(atoms.fullConfigAtom);
const themeName = get(this.termThemeNameAtom);
const [_, bgcolor] = computeTheme(fullConfig, themeName);
if (bgcolor != null) {
return { bg: bgcolor };
}
return null;
});
this.connStatus = jotai.atom((get) => {
const blockData = get(this.blockAtom);
const connName = blockData?.meta?.connection;
const connAtom = getConnStatusAtom(connName);
return get(connAtom);
});
this.fontSizeAtom = useBlockAtom(blockId, "fontsizeatom", () => {
return jotai.atom<number>((get) => {
const blockData = get(this.blockAtom);
const fsSettingsAtom = getSettingsKeyAtom("term:fontsize");
const settingsFontSize = get(fsSettingsAtom);
const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? settingsFontSize ?? 12;
if (typeof rtnFontSize != "number" || isNaN(rtnFontSize) || rtnFontSize < 4 || rtnFontSize > 64) {
return 12;
}
return rtnFontSize;
});
});
this.noPadding = jotai.atom(true);
this.endIconButtons = jotai.atom((get) => {
const blockData = get(this.blockAtom);
const shellProcStatus = get(this.shellProcStatus);
const connStatus = get(this.connStatus);
const isCmd = get(this.isCmdController);
if (blockData?.meta?.["controller"] != "cmd" && shellProcStatus != "done") {
return [];
}
if (connStatus?.status != "connected") {
return [];
}
let iconName: string = null;
let title: string = null;
const noun = isCmd ? "Command" : "Shell";
if (shellProcStatus == "init") {
iconName = "play";
title = "Click to Start " + noun;
} else if (shellProcStatus == "running") {
iconName = "refresh";
title = noun + " Running. Click to Restart";
} else if (shellProcStatus == "done") {
iconName = "refresh";
title = noun + " Exited. Click to Restart";
}
if (iconName == null) {
return [];
}
const buttonDecl: IconButtonDecl = {
elemtype: "iconbutton",
icon: iconName,
click: this.forceRestartController.bind(this),
title: title,
};
const rtn = [buttonDecl];
return rtn;
});
this.isCmdController = jotai.atom((get) => {
const controllerMetaAtom = getBlockMetaKeyAtom(this.blockId, "controller");
return get(controllerMetaAtom) == "cmd";
});
this.shellProcFullStatus = jotai.atom(null) as jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
const initialShellProcStatus = services.BlockService.GetControllerStatus(blockId);
initialShellProcStatus.then((rts) => {
this.updateShellProcStatus(rts);
});
this.shellProcStatusUnsubFn = waveEventSubscribe({
eventType: "controllerstatus",
scope: WOS.makeORef("block", blockId),
handler: (event) => {
let bcRTS: BlockControllerRuntimeStatus = event.data;
this.updateShellProcStatus(bcRTS);
},
});
this.shellProcStatus = jotai.atom((get) => {
const fullStatus = get(this.shellProcFullStatus);
return fullStatus?.shellprocstatus ?? "init";
});
}
setTermMode(mode: "term" | "vdom") {
if (mode == "term") {
mode = null;
}
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:mode": mode },
});
}
triggerRestartAtom() {
globalStore.set(this.isRestarting, true);
setTimeout(() => {
globalStore.set(this.isRestarting, false);
}, 300);
}
updateShellProcStatus(fullStatus: BlockControllerRuntimeStatus) {
globalStore.set(this.shellProcFullStatus, fullStatus);
const status = fullStatus?.shellprocstatus ?? "init";
if (status == "running") {
this.termRef.current?.setIsRunning?.(true);
} else {
this.termRef.current?.setIsRunning?.(false);
}
}
getVDomModel(): VDomModel {
const vdomBlockId = globalStore.get(this.vdomBlockId);
if (!vdomBlockId) {
return null;
}
const bcm = getBlockComponentModel(vdomBlockId);
if (!bcm) {
return null;
}
return bcm.viewModel as VDomModel;
}
getVDomToolbarModel(): VDomModel {
const vdomToolbarBlockId = globalStore.get(this.vdomToolbarBlockId);
if (!vdomToolbarBlockId) {
return null;
}
const bcm = getBlockComponentModel(vdomToolbarBlockId);
if (!bcm) {
return null;
}
return bcm.viewModel as VDomModel;
}
dispose() {
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
if (this.shellProcStatusUnsubFn) {
this.shellProcStatusUnsubFn();
}
}
giveFocus(): boolean {
let termMode = globalStore.get(this.termMode);
if (termMode == "term") {
if (this.termRef?.current?.terminal) {
this.termRef.current.terminal.focus();
return true;
}
}
return false;
}
keyDownHandler(waveEvent: WaveKeyboardEvent): boolean {
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${this.blockId}`);
const blockData = globalStore.get(blockAtom);
const newTermMode = blockData?.meta?.["term:mode"] == "vdom" ? null : "vdom";
const vdomBlockId = globalStore.get(this.vdomBlockId);
if (newTermMode == "vdom" && !vdomBlockId) {
return;
}
this.setTermMode(newTermMode);
return true;
}
const blockData = globalStore.get(this.blockAtom);
if (blockData.meta?.["term:mode"] == "vdom") {
const vdomModel = this.getVDomModel();
return vdomModel?.keyDownHandler(waveEvent);
}
return false;
}
handleTerminalKeydown(event: KeyboardEvent): boolean {
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
if (waveEvent.type != "keydown") {
return true;
}
if (this.keyDownHandler(waveEvent)) {
event.preventDefault();
event.stopPropagation();
return false;
}
// deal with terminal specific keybindings
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
const p = navigator.clipboard.readText();
p.then((text) => {
this.termRef.current?.terminal.paste(text);
});
event.preventDefault();
event.stopPropagation();
return false;
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
const sel = this.termRef.current?.terminal.getSelection();
navigator.clipboard.writeText(sel);
event.preventDefault();
event.stopPropagation();
return false;
}
const shellProcStatus = globalStore.get(this.shellProcStatus);
if ((shellProcStatus == "done" || shellProcStatus == "init") && keyutil.checkKeyPressed(waveEvent, "Enter")) {
this.forceRestartController();
return false;
}
const globalKeys = getAllGlobalKeyBindings();
for (const key of globalKeys) {
if (keyutil.checkKeyPressed(waveEvent, key)) {
return false;
}
}
return true;
}
setTerminalTheme(themeName: string) {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:theme": themeName },
});
}
forceRestartController() {
if (globalStore.get(this.isRestarting)) {
return;
}
this.triggerRestartAtom();
const termsize = {
rows: this.termRef.current?.terminal?.rows,
cols: this.termRef.current?.terminal?.cols,
};
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
forcerestart: true,
rtopts: { termsize: termsize },
});
prtn.catch((e) => console.log("error controller resync (force restart)", e));
}
getSettingsMenuItems(): ContextMenuItem[] {
const fullConfig = globalStore.get(atoms.fullConfigAtom);
const termThemes = fullConfig?.termthemes ?? {};
const termThemeKeys = Object.keys(termThemes);
const curThemeName = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:theme"));
const defaultFontSize = globalStore.get(getSettingsKeyAtom("term:fontsize")) ?? 12;
const blockData = globalStore.get(this.blockAtom);
const overrideFontSize = blockData?.meta?.["term:fontsize"];
termThemeKeys.sort((a, b) => {
return (termThemes[a]["display:order"] ?? 0) - (termThemes[b]["display:order"] ?? 0);
});
const fullMenu: ContextMenuItem[] = [];
const submenu: ContextMenuItem[] = termThemeKeys.map((themeName) => {
return {
label: termThemes[themeName]["display:name"] ?? themeName,
type: "checkbox",
checked: curThemeName == themeName,
click: () => this.setTerminalTheme(themeName),
};
});
submenu.unshift({
label: "Default",
type: "checkbox",
checked: curThemeName == null,
click: () => this.setTerminalTheme(null),
});
const fontSizeSubMenu: ContextMenuItem[] = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].map(
(fontSize: number) => {
return {
label: fontSize.toString() + "px",
type: "checkbox",
checked: overrideFontSize == fontSize,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:fontsize": fontSize },
});
},
};
}
);
fontSizeSubMenu.unshift({
label: "Default (" + defaultFontSize + "px)",
type: "checkbox",
checked: overrideFontSize == null,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "term:fontsize": null },
});
},
});
fullMenu.push({
label: "Themes",
submenu: submenu,
});
fullMenu.push({
label: "Font Size",
submenu: fontSizeSubMenu,
});
fullMenu.push({ type: "separator" });
fullMenu.push({
label: "Force Restart Controller",
click: this.forceRestartController.bind(this),
});
const isClearOnStart = blockData?.meta?.["cmd:clearonstart"];
fullMenu.push({
label: "Clear Output On Restart",
submenu: [
{
label: "On",
type: "checkbox",
checked: isClearOnStart,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "cmd:clearonstart": true },
});
},
},
{
label: "Off",
type: "checkbox",
checked: !isClearOnStart,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "cmd:clearonstart": false },
});
},
},
],
});
const runOnStart = blockData?.meta?.["cmd:runonstart"];
fullMenu.push({
label: "Run On Startup",
submenu: [
{
label: "On",
type: "checkbox",
checked: runOnStart,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "cmd:runonstart": true },
});
},
},
{
label: "Off",
type: "checkbox",
checked: !runOnStart,
click: () => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
meta: { "cmd:runonstart": false },
});
},
},
],
});
if (blockData?.meta?.["term:vdomtoolbarblockid"]) {
fullMenu.push({ type: "separator" });
fullMenu.push({
label: "Close Toolbar",
click: () => {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: blockData.meta["term:vdomtoolbarblockid"] });
},
});
}
return fullMenu;
}
}
function makeTerminalModel(blockId: string, nodeModel: BlockNodeModel): TermViewModel {
return new TermViewModel(blockId, nodeModel);
}
interface TerminalViewProps {
blockId: string;
model: TermViewModel;
}
const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) => {
const connStatus = jotai.useAtomValue(model.connStatus);
const [lastConnStatus, setLastConnStatus] = React.useState<ConnStatus>(connStatus);
React.useEffect(() => {
if (!model.termRef.current?.hasResized) {
return;
}
const isConnected = connStatus?.status == "connected";
const wasConnected = lastConnStatus?.status == "connected";
const curConnName = connStatus?.connection;
const lastConnName = lastConnStatus?.connection;
if (isConnected == wasConnected && curConnName == lastConnName) {
return;
}
model.termRef.current?.resyncController("resync handler");
setLastConnStatus(connStatus);
}, [connStatus]);
return null;
});
const TermVDomToolbarNode = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
React.useEffect(() => {
const unsub = waveEventSubscribe({
eventType: "blockclose",
scope: WOS.makeORef("block", vdomBlockId),
handler: (event) => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", blockId),
meta: {
"term:mode": null,
"term:vdomtoolbarblockid": null,
},
});
},
});
return () => {
unsub();
};
}, []);
let vdomNodeModel = {
blockId: vdomBlockId,
isFocused: jotai.atom(false),
focusNode: () => {},
onClose: () => {
if (vdomBlockId != null) {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
}
},
};
const toolbarTarget = jotai.useAtomValue(model.vdomToolbarTarget);
const heightStr = toolbarTarget?.height ?? "1.5em";
return (
<div key="vdomToolbar" className="term-toolbar" style={{ height: heightStr }}>
<SubBlock key="vdom" nodeModel={vdomNodeModel} />
</div>
);
};
const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
React.useEffect(() => {
const unsub = waveEventSubscribe({
eventType: "blockclose",
scope: WOS.makeORef("block", vdomBlockId),
handler: (event) => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", blockId),
meta: {
"term:mode": null,
"term:vdomblockid": null,
},
});
},
});
return () => {
unsub();
};
}, []);
const isFocusedAtom = jotai.atom((get) => {
return get(model.nodeModel.isFocused) && get(model.termMode) == "vdom";
});
let vdomNodeModel = {
blockId: vdomBlockId,
isFocused: isFocusedAtom,
focusNode: () => {
model.nodeModel.focusNode();
},
onClose: () => {
if (vdomBlockId != null) {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
}
},
};
return (
<div key="htmlElem" className="term-htmlelem">
<SubBlock key="vdom" nodeModel={vdomNodeModel} />
</div>
);
};
const TermVDomNode = ({ blockId, model }: TerminalViewProps) => {
const vdomBlockId = jotai.useAtomValue(model.vdomBlockId);
if (vdomBlockId == null) {
return null;
}
return <TermVDomNodeSingleId key={vdomBlockId} vdomBlockId={vdomBlockId} blockId={blockId} model={model} />;
};
const TermToolbarVDomNode = ({ blockId, model }: TerminalViewProps) => {
const vdomToolbarBlockId = jotai.useAtomValue(model.vdomToolbarBlockId);
if (vdomToolbarBlockId == null) {
return null;
}
return (
<TermVDomToolbarNode
key={vdomToolbarBlockId}
vdomBlockId={vdomToolbarBlockId}
blockId={blockId}
model={model}
/>
);
};
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
const viewRef = React.useRef<HTMLDivElement>(null);
const connectElemRef = React.useRef<HTMLDivElement>(null);
const termRef = React.useRef<TermWrap>(null);
model.termRef = termRef;
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const termSettingsAtom = useSettingsPrefixAtom("term");
const termSettings = jotai.useAtomValue(termSettingsAtom);
let termMode = blockData?.meta?.["term:mode"] ?? "term";
if (termMode != "term" && termMode != "vdom") {
termMode = "term";
}
const termModeRef = React.useRef(termMode);
const termFontSize = jotai.useAtomValue(model.fontSizeAtom);
React.useEffect(() => {
const fullConfig = globalStore.get(atoms.fullConfigAtom);
const termThemeName = globalStore.get(model.termThemeNameAtom);
const [termTheme, _] = computeTheme(fullConfig, termThemeName);
let termScrollback = 1000;
if (termSettings?.["term:scrollback"]) {
termScrollback = Math.floor(termSettings["term:scrollback"]);
}
if (blockData?.meta?.["term:scrollback"]) {
termScrollback = Math.floor(blockData.meta["term:scrollback"]);
}
if (termScrollback < 0) {
termScrollback = 0;
}
if (termScrollback > 10000) {
termScrollback = 10000;
}
const wasFocused = termRef.current != null && globalStore.get(model.nodeModel.isFocused);
const termWrap = new TermWrap(
blockId,
connectElemRef.current,
{
theme: termTheme,
fontSize: termFontSize,
fontFamily: termSettings?.["term:fontfamily"] ?? "Hack",
drawBoldTextInBrightColors: false,
fontWeight: "normal",
fontWeightBold: "bold",
allowTransparency: true,
scrollback: termScrollback,
},
{
keydownHandler: model.handleTerminalKeydown.bind(model),
useWebGl: !termSettings?.["term:disablewebgl"],
}
);
const shellProcStatus = globalStore.get(model.shellProcStatus);
if (shellProcStatus == "running") {
termWrap.setIsRunning(true);
} else if (shellProcStatus == "done") {
termWrap.setIsRunning(false);
}
(window as any).term = termWrap;
termRef.current = termWrap;
const rszObs = new ResizeObserver(() => {
termWrap.handleResize_debounced();
});
rszObs.observe(connectElemRef.current);
termWrap.initTerminal();
if (wasFocused) {
setTimeout(() => {
model.giveFocus();
}, 10);
}
return () => {
termWrap.dispose();
rszObs.disconnect();
};
}, [blockId, termSettings, termFontSize]);
React.useEffect(() => {
if (termModeRef.current == "vdom" && termMode == "term") {
// focus the terminal
model.giveFocus();
}
termModeRef.current = termMode;
}, [termMode]);
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)} ref={viewRef}>
<TermResyncHandler blockId={blockId} model={model} />
<TermThemeUpdater blockId={blockId} model={model} termRef={termRef} />
<TermStickers config={stickerConfig} />
<TermToolbarVDomNode key="vdom-toolbar" blockId={blockId} model={model} />
<TermVDomNode key="vdom" blockId={blockId} model={model} />
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
</div>
);
};
export { makeTerminalModel, TerminalView, TermViewModel };