2024-05-14 08:45:41 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-10-26 03:36:09 +02:00
|
|
|
import { Block, SubBlock } from "@/app/block/block";
|
|
|
|
import { BlockNodeModel } from "@/app/block/blocktypes";
|
2025-01-01 19:43:02 +01:00
|
|
|
import { Search, useSearch } from "@/app/element/search";
|
2025-01-02 17:38:07 +01:00
|
|
|
import { appHandleKeyDown } from "@/app/store/keymodel";
|
2024-09-12 03:03:55 +02:00
|
|
|
import { waveEventSubscribe } from "@/app/store/wps";
|
2024-09-16 20:59:39 +02:00
|
|
|
import { RpcApi } from "@/app/store/wshclientapi";
|
2024-10-17 23:50:36 +02:00
|
|
|
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
|
|
|
|
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
|
|
|
|
import { TermWshClient } from "@/app/view/term/term-wsh";
|
2024-10-25 22:45:00 +02:00
|
|
|
import { VDomModel } from "@/app/view/vdom/vdom-model";
|
2024-10-24 07:47:29 +02:00
|
|
|
import {
|
|
|
|
atoms,
|
2025-01-02 19:06:47 +01:00
|
|
|
getAllBlockComponentModels,
|
2024-10-24 07:47:29 +02:00
|
|
|
getBlockComponentModel,
|
2024-11-21 19:44:16 +01:00
|
|
|
getBlockMetaKeyAtom,
|
2024-10-24 07:47:29 +02:00
|
|
|
getConnStatusAtom,
|
2024-11-21 19:44:16 +01:00
|
|
|
getOverrideConfigAtom,
|
2024-10-24 07:47:29 +02:00
|
|
|
getSettingsKeyAtom,
|
2024-12-26 20:09:48 +01:00
|
|
|
getSettingsPrefixAtom,
|
2024-10-24 07:47:29 +02:00
|
|
|
globalStore,
|
2024-10-24 20:01:39 +02:00
|
|
|
useBlockAtom,
|
2024-11-21 19:44:16 +01:00
|
|
|
WOS,
|
2024-10-24 07:47:29 +02:00
|
|
|
} from "@/store/global";
|
2024-06-12 02:42:10 +02:00
|
|
|
import * as services from "@/store/services";
|
2024-06-24 23:34:31 +02:00
|
|
|
import * as keyutil from "@/util/keyutil";
|
2025-01-02 19:06:47 +01:00
|
|
|
import { boundNumber, fireAndForget, stringToBase64, useAtomValueSafe } from "@/util/util";
|
2025-01-01 19:43:02 +01:00
|
|
|
import { ISearchOptions } from "@xterm/addon-search";
|
2024-06-13 23:41:28 +02:00
|
|
|
import clsx from "clsx";
|
2024-10-24 07:47:29 +02:00
|
|
|
import debug from "debug";
|
2024-06-14 08:54:04 +02:00
|
|
|
import * as jotai from "jotai";
|
2024-05-28 21:12:28 +02:00
|
|
|
import * as React from "react";
|
2024-06-18 07:38:48 +02:00
|
|
|
import { TermStickers } from "./termsticker";
|
2024-07-25 05:34:22 +02:00
|
|
|
import { TermThemeUpdater } from "./termtheme";
|
2024-11-21 19:44:16 +01:00
|
|
|
import { computeTheme, DefaultTermTheme } from "./termutil";
|
2024-06-18 07:38:48 +02:00
|
|
|
import { TermWrap } from "./termwrap";
|
2024-09-04 03:11:28 +02:00
|
|
|
import "./xterm.css";
|
2024-05-14 08:45:41 +02:00
|
|
|
|
2024-10-24 07:47:29 +02:00
|
|
|
const dlog = debug("wave:term");
|
|
|
|
|
2024-05-29 09:28:25 +02:00
|
|
|
type InitialLoadDataType = {
|
|
|
|
loaded: boolean;
|
|
|
|
heldData: Uint8Array[];
|
|
|
|
};
|
|
|
|
|
2024-12-05 19:02:07 +01:00
|
|
|
class TermViewModel implements ViewModel {
|
2024-08-23 01:25:53 +02:00
|
|
|
viewType: string;
|
2024-10-26 03:36:09 +02:00
|
|
|
nodeModel: BlockNodeModel;
|
2024-08-24 03:12:40 +02:00
|
|
|
connected: boolean;
|
2024-12-11 21:26:56 +01:00
|
|
|
termRef: React.MutableRefObject<TermWrap> = { current: null };
|
2024-07-23 02:08:18 +02:00
|
|
|
blockAtom: jotai.Atom<Block>;
|
|
|
|
termMode: jotai.Atom<string>;
|
|
|
|
blockId: string;
|
2024-07-26 01:45:07 +02:00
|
|
|
viewIcon: jotai.Atom<string>;
|
2024-07-26 03:05:32 +02:00
|
|
|
viewName: jotai.Atom<string>;
|
2024-10-24 07:47:29 +02:00
|
|
|
viewText: jotai.Atom<HeaderElem[]>;
|
2024-07-31 04:52:50 +02:00
|
|
|
blockBg: jotai.Atom<MetaType>;
|
2024-08-27 01:19:03 +02:00
|
|
|
manageConnection: jotai.Atom<boolean>;
|
2024-12-05 19:02:07 +01:00
|
|
|
filterOutNowsh?: jotai.Atom<boolean>;
|
2024-09-05 09:21:08 +02:00
|
|
|
connStatus: jotai.Atom<ConnStatus>;
|
2024-10-17 23:50:36 +02:00
|
|
|
termWshClient: TermWshClient;
|
2024-10-24 07:47:29 +02:00
|
|
|
vdomBlockId: jotai.Atom<string>;
|
2024-11-11 22:11:09 +01:00
|
|
|
vdomToolbarBlockId: jotai.Atom<string>;
|
|
|
|
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
|
2024-10-24 20:01:39 +02:00
|
|
|
fontSizeAtom: jotai.Atom<number>;
|
|
|
|
termThemeNameAtom: jotai.Atom<string>;
|
2024-12-19 19:41:28 +01:00
|
|
|
termTransparencyAtom: jotai.Atom<number>;
|
2024-11-11 22:11:09 +01:00
|
|
|
noPadding: jotai.PrimitiveAtom<boolean>;
|
2024-11-12 00:30:40 +01:00
|
|
|
endIconButtons: jotai.Atom<IconButtonDecl[]>;
|
2024-12-04 23:16:50 +01:00
|
|
|
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
|
|
|
|
shellProcStatus: jotai.Atom<string>;
|
|
|
|
shellProcStatusUnsubFn: () => void;
|
|
|
|
isCmdController: jotai.Atom<boolean>;
|
|
|
|
isRestarting: jotai.PrimitiveAtom<boolean>;
|
2025-01-01 19:43:02 +01:00
|
|
|
searchAtoms?: SearchAtoms;
|
2024-07-23 02:08:18 +02:00
|
|
|
|
2024-10-26 03:36:09 +02:00
|
|
|
constructor(blockId: string, nodeModel: BlockNodeModel) {
|
2024-08-23 01:25:53 +02:00
|
|
|
this.viewType = "term";
|
2024-07-23 02:08:18 +02:00
|
|
|
this.blockId = blockId;
|
2024-10-17 23:50:36 +02:00
|
|
|
this.termWshClient = new TermWshClient(blockId, this);
|
|
|
|
DefaultRouter.registerRoute(makeFeBlockRouteId(blockId), this.termWshClient);
|
|
|
|
this.nodeModel = nodeModel;
|
2024-07-23 02:08:18 +02:00
|
|
|
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
2024-10-24 07:47:29 +02:00
|
|
|
this.vdomBlockId = jotai.atom((get) => {
|
|
|
|
const blockData = get(this.blockAtom);
|
|
|
|
return blockData?.meta?.["term:vdomblockid"];
|
|
|
|
});
|
2024-11-11 22:11:09 +01:00
|
|
|
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>;
|
2024-07-23 02:08:18 +02:00
|
|
|
this.termMode = jotai.atom((get) => {
|
|
|
|
const blockData = get(this.blockAtom);
|
|
|
|
return blockData?.meta?.["term:mode"] ?? "term";
|
|
|
|
});
|
2024-12-04 23:16:50 +01:00
|
|
|
this.isRestarting = jotai.atom(false);
|
2024-07-26 03:05:32 +02:00
|
|
|
this.viewIcon = jotai.atom((get) => {
|
2024-10-24 07:47:29 +02:00
|
|
|
const termMode = get(this.termMode);
|
|
|
|
if (termMode == "vdom") {
|
|
|
|
return "bolt";
|
|
|
|
}
|
2024-12-04 23:16:50 +01:00
|
|
|
const isCmd = get(this.isCmdController);
|
|
|
|
if (isCmd) {
|
|
|
|
}
|
2024-07-26 03:05:32 +02:00
|
|
|
return "terminal";
|
|
|
|
});
|
|
|
|
this.viewName = jotai.atom((get) => {
|
|
|
|
const blockData = get(this.blockAtom);
|
2024-10-24 07:47:29 +02:00
|
|
|
const termMode = get(this.termMode);
|
|
|
|
if (termMode == "vdom") {
|
|
|
|
return "Wave App";
|
|
|
|
}
|
2024-07-30 21:33:28 +02:00
|
|
|
if (blockData?.meta?.controller == "cmd") {
|
2024-12-04 23:16:50 +01:00
|
|
|
return "";
|
2024-07-26 03:05:32 +02:00
|
|
|
}
|
|
|
|
return "Terminal";
|
|
|
|
});
|
2024-10-24 07:47:29 +02:00
|
|
|
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");
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
2024-12-04 23:16:50 +01:00
|
|
|
}
|
|
|
|
const vdomBlockId = get(this.vdomBlockId);
|
2025-01-02 19:06:47 +01:00
|
|
|
const rtn: HeaderElem[] = [];
|
2024-12-04 23:16:50 +01:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-10-24 07:47:29 +02:00
|
|
|
}
|
|
|
|
}
|
2025-01-02 19:06:47 +01:00
|
|
|
const isMI = get(atoms.isTermMultiInput);
|
|
|
|
if (isMI && this.isBasicTerm(get)) {
|
|
|
|
rtn.push({
|
|
|
|
elemtype: "textbutton",
|
|
|
|
text: "Multi Input ON",
|
|
|
|
className: "yellow",
|
|
|
|
title: "Input will be sent to all connected terminals (click to disable)",
|
|
|
|
onClick: () => {
|
|
|
|
globalStore.set(atoms.isTermMultiInput, false);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2024-12-04 23:16:50 +01:00
|
|
|
return rtn;
|
2024-10-24 07:47:29 +02:00
|
|
|
});
|
|
|
|
this.manageConnection = jotai.atom((get) => {
|
|
|
|
const termMode = get(this.termMode);
|
|
|
|
if (termMode == "vdom") {
|
|
|
|
return false;
|
|
|
|
}
|
2024-12-04 23:16:50 +01:00
|
|
|
const isCmd = get(this.isCmdController);
|
|
|
|
if (isCmd) {
|
|
|
|
return false;
|
|
|
|
}
|
2024-10-24 07:47:29 +02:00
|
|
|
return true;
|
|
|
|
});
|
2024-12-05 19:02:07 +01:00
|
|
|
this.filterOutNowsh = jotai.atom(false);
|
2024-11-21 19:44:16 +01:00
|
|
|
this.termThemeNameAtom = useBlockAtom(blockId, "termthemeatom", () => {
|
|
|
|
return jotai.atom<string>((get) => {
|
|
|
|
return get(getOverrideConfigAtom(this.blockId, "term:theme")) ?? DefaultTermTheme;
|
|
|
|
});
|
|
|
|
});
|
2024-12-19 19:41:28 +01:00
|
|
|
this.termTransparencyAtom = useBlockAtom(blockId, "termtransparencyatom", () => {
|
|
|
|
return jotai.atom<number>((get) => {
|
|
|
|
let value = get(getOverrideConfigAtom(this.blockId, "term:transparency")) ?? 0.5;
|
|
|
|
return boundNumber(value, 0, 1);
|
|
|
|
});
|
|
|
|
});
|
2024-07-31 04:52:50 +02:00
|
|
|
this.blockBg = jotai.atom((get) => {
|
2024-08-28 03:49:49 +02:00
|
|
|
const fullConfig = get(atoms.fullConfigAtom);
|
2024-11-21 19:44:16 +01:00
|
|
|
const themeName = get(this.termThemeNameAtom);
|
2024-12-19 19:41:28 +01:00
|
|
|
const termTransparency = get(this.termTransparencyAtom);
|
|
|
|
const [_, bgcolor] = computeTheme(fullConfig, themeName, termTransparency);
|
2024-11-21 19:44:16 +01:00
|
|
|
if (bgcolor != null) {
|
|
|
|
return { bg: bgcolor };
|
2024-07-31 04:52:50 +02:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
});
|
2024-09-05 09:21:08 +02:00
|
|
|
this.connStatus = jotai.atom((get) => {
|
|
|
|
const blockData = get(this.blockAtom);
|
|
|
|
const connName = blockData?.meta?.connection;
|
|
|
|
const connAtom = getConnStatusAtom(connName);
|
|
|
|
return get(connAtom);
|
|
|
|
});
|
2024-10-24 20:01:39 +02:00
|
|
|
this.fontSizeAtom = useBlockAtom(blockId, "fontsizeatom", () => {
|
|
|
|
return jotai.atom<number>((get) => {
|
|
|
|
const blockData = get(this.blockAtom);
|
|
|
|
const fsSettingsAtom = getSettingsKeyAtom("term:fontsize");
|
|
|
|
const settingsFontSize = get(fsSettingsAtom);
|
2024-12-05 19:02:07 +01:00
|
|
|
const connName = blockData?.meta?.connection;
|
|
|
|
const fullConfig = get(atoms.fullConfigAtom);
|
|
|
|
const connFontSize = fullConfig?.connections?.[connName]?.["term:fontsize"];
|
|
|
|
const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? connFontSize ?? settingsFontSize ?? 12;
|
2024-10-24 20:01:39 +02:00
|
|
|
if (typeof rtnFontSize != "number" || isNaN(rtnFontSize) || rtnFontSize < 4 || rtnFontSize > 64) {
|
|
|
|
return 12;
|
|
|
|
}
|
|
|
|
return rtnFontSize;
|
|
|
|
});
|
|
|
|
});
|
2024-11-11 22:11:09 +01:00
|
|
|
this.noPadding = jotai.atom(true);
|
2024-11-12 00:30:40 +01:00
|
|
|
this.endIconButtons = jotai.atom((get) => {
|
|
|
|
const blockData = get(this.blockAtom);
|
2024-12-04 23:16:50 +01:00
|
|
|
const shellProcStatus = get(this.shellProcStatus);
|
|
|
|
const connStatus = get(this.connStatus);
|
|
|
|
const isCmd = get(this.isCmdController);
|
|
|
|
if (blockData?.meta?.["controller"] != "cmd" && shellProcStatus != "done") {
|
2024-11-12 00:30:40 +01:00
|
|
|
return [];
|
|
|
|
}
|
2024-12-04 23:16:50 +01:00
|
|
|
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";
|
2024-11-12 00:30:40 +01:00
|
|
|
});
|
2024-08-24 03:12:40 +02:00
|
|
|
}
|
|
|
|
|
2025-01-02 19:06:47 +01:00
|
|
|
isBasicTerm(getFn: jotai.Getter): boolean {
|
|
|
|
// needs to match "const isBasicTerm" in TerminalView()
|
|
|
|
const termMode = getFn(this.termMode);
|
|
|
|
if (termMode == "vdom") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const blockData = getFn(this.blockAtom);
|
|
|
|
if (blockData?.meta?.controller == "cmd") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
multiInputHandler(data: string) {
|
|
|
|
let tvms = getAllBasicTermModels();
|
|
|
|
// filter out "this" from the list
|
|
|
|
tvms = tvms.filter((tvm) => tvm != this);
|
|
|
|
if (tvms.length == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (const tvm of tvms) {
|
|
|
|
tvm.sendDataToController(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sendDataToController(data: string) {
|
|
|
|
const b64data = stringToBase64(data);
|
|
|
|
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, inputdata64: b64data });
|
|
|
|
}
|
|
|
|
|
2024-10-24 07:47:29 +02:00
|
|
|
setTermMode(mode: "term" | "vdom") {
|
|
|
|
if (mode == "term") {
|
|
|
|
mode = null;
|
|
|
|
}
|
|
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
|
|
|
oref: WOS.makeORef("block", this.blockId),
|
|
|
|
meta: { "term:mode": mode },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-12-04 23:16:50 +01:00
|
|
|
triggerRestartAtom() {
|
|
|
|
globalStore.set(this.isRestarting, true);
|
|
|
|
setTimeout(() => {
|
|
|
|
globalStore.set(this.isRestarting, false);
|
|
|
|
}, 300);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateShellProcStatus(fullStatus: BlockControllerRuntimeStatus) {
|
2024-12-07 04:39:58 +01:00
|
|
|
if (fullStatus == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const curStatus = globalStore.get(this.shellProcFullStatus);
|
|
|
|
if (curStatus == null || curStatus.version < fullStatus.version) {
|
|
|
|
globalStore.set(this.shellProcFullStatus, fullStatus);
|
2024-12-04 23:16:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-24 07:47:29 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-11-11 22:11:09 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-10-17 23:50:36 +02:00
|
|
|
dispose() {
|
|
|
|
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
|
2024-12-04 23:16:50 +01:00
|
|
|
if (this.shellProcStatusUnsubFn) {
|
|
|
|
this.shellProcStatusUnsubFn();
|
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
}
|
|
|
|
|
2024-07-23 02:08:18 +02:00
|
|
|
giveFocus(): boolean {
|
2025-01-01 19:43:02 +01:00
|
|
|
if (this.searchAtoms && globalStore.get(this.searchAtoms.isOpen)) {
|
|
|
|
console.log("search is open, not giving focus");
|
|
|
|
return true;
|
|
|
|
}
|
2024-07-23 02:08:18 +02:00
|
|
|
let termMode = globalStore.get(this.termMode);
|
|
|
|
if (termMode == "term") {
|
|
|
|
if (this.termRef?.current?.terminal) {
|
|
|
|
this.termRef.current.terminal.focus();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2024-07-31 04:52:50 +02:00
|
|
|
|
2024-10-17 23:50:36 +02:00
|
|
|
keyDownHandler(waveEvent: WaveKeyboardEvent): boolean {
|
|
|
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
|
|
|
const blockAtom = WOS.getWaveObjectAtom<Block>(`block:${this.blockId}`);
|
|
|
|
const blockData = globalStore.get(blockAtom);
|
2024-10-24 07:47:29 +02:00
|
|
|
const newTermMode = blockData?.meta?.["term:mode"] == "vdom" ? null : "vdom";
|
|
|
|
const vdomBlockId = globalStore.get(this.vdomBlockId);
|
|
|
|
if (newTermMode == "vdom" && !vdomBlockId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setTermMode(newTermMode);
|
2024-10-17 23:50:36 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
const blockData = globalStore.get(this.blockAtom);
|
2024-10-24 07:47:29 +02:00
|
|
|
if (blockData.meta?.["term:mode"] == "vdom") {
|
|
|
|
const vdomModel = this.getVDomModel();
|
|
|
|
return vdomModel?.keyDownHandler(waveEvent);
|
2024-10-17 23:50:36 +02:00
|
|
|
}
|
|
|
|
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;
|
2024-12-19 19:41:28 +01:00
|
|
|
} else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
this.termRef.current?.terminal?.clear();
|
|
|
|
return false;
|
2024-10-17 23:50:36 +02:00
|
|
|
}
|
2024-12-04 23:16:50 +01:00
|
|
|
const shellProcStatus = globalStore.get(this.shellProcStatus);
|
|
|
|
if ((shellProcStatus == "done" || shellProcStatus == "init") && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
|
|
|
this.forceRestartController();
|
2024-10-17 23:50:36 +02:00
|
|
|
return false;
|
|
|
|
}
|
2025-01-02 17:38:07 +01:00
|
|
|
const appHandled = appHandleKeyDown(waveEvent);
|
|
|
|
if (appHandled) {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
return false;
|
2024-10-17 23:50:36 +02:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-07-31 04:52:50 +02:00
|
|
|
setTerminalTheme(themeName: string) {
|
2024-10-17 23:34:02 +02:00
|
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
2024-09-16 20:59:39 +02:00
|
|
|
oref: WOS.makeORef("block", this.blockId),
|
|
|
|
meta: { "term:theme": themeName },
|
|
|
|
});
|
2024-07-31 04:52:50 +02:00
|
|
|
}
|
|
|
|
|
2024-11-12 00:30:40 +01:00
|
|
|
forceRestartController() {
|
2024-12-04 23:16:50 +01:00
|
|
|
if (globalStore.get(this.isRestarting)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.triggerRestartAtom();
|
2024-11-12 00:30:40 +01:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2024-07-31 04:52:50 +02:00
|
|
|
getSettingsMenuItems(): ContextMenuItem[] {
|
2024-08-30 06:35:22 +02:00
|
|
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
|
|
|
const termThemes = fullConfig?.termthemes ?? {};
|
|
|
|
const termThemeKeys = Object.keys(termThemes);
|
2024-11-21 19:44:16 +01:00
|
|
|
const curThemeName = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:theme"));
|
2024-10-24 20:01:39 +02:00
|
|
|
const defaultFontSize = globalStore.get(getSettingsKeyAtom("term:fontsize")) ?? 12;
|
2024-12-19 19:41:28 +01:00
|
|
|
const transparencyMeta = globalStore.get(getBlockMetaKeyAtom(this.blockId, "term:transparency"));
|
2024-10-24 20:01:39 +02:00
|
|
|
const blockData = globalStore.get(this.blockAtom);
|
|
|
|
const overrideFontSize = blockData?.meta?.["term:fontsize"];
|
2024-09-05 09:21:08 +02:00
|
|
|
|
2024-08-30 06:35:22 +02:00
|
|
|
termThemeKeys.sort((a, b) => {
|
2024-11-09 00:46:44 +01:00
|
|
|
return (termThemes[a]["display:order"] ?? 0) - (termThemes[b]["display:order"] ?? 0);
|
2024-08-30 06:35:22 +02:00
|
|
|
});
|
2024-09-05 09:21:08 +02:00
|
|
|
const fullMenu: ContextMenuItem[] = [];
|
2024-08-30 06:35:22 +02:00
|
|
|
const submenu: ContextMenuItem[] = termThemeKeys.map((themeName) => {
|
|
|
|
return {
|
|
|
|
label: termThemes[themeName]["display:name"] ?? themeName,
|
2024-10-24 20:01:39 +02:00
|
|
|
type: "checkbox",
|
|
|
|
checked: curThemeName == themeName,
|
2024-08-30 06:35:22 +02:00
|
|
|
click: () => this.setTerminalTheme(themeName),
|
|
|
|
};
|
|
|
|
});
|
2024-11-21 19:44:16 +01:00
|
|
|
submenu.unshift({
|
|
|
|
label: "Default",
|
|
|
|
type: "checkbox",
|
|
|
|
checked: curThemeName == null,
|
|
|
|
click: () => this.setTerminalTheme(null),
|
|
|
|
});
|
2024-12-19 19:41:28 +01:00
|
|
|
const transparencySubMenu: ContextMenuItem[] = [];
|
|
|
|
transparencySubMenu.push({
|
|
|
|
label: "Default",
|
|
|
|
type: "checkbox",
|
|
|
|
checked: transparencyMeta == null,
|
|
|
|
click: () => {
|
|
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
|
|
|
oref: WOS.makeORef("block", this.blockId),
|
|
|
|
meta: { "term:transparency": null },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
transparencySubMenu.push({
|
|
|
|
label: "Transparent Background",
|
|
|
|
type: "checkbox",
|
|
|
|
checked: transparencyMeta == 0.5,
|
|
|
|
click: () => {
|
|
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
|
|
|
oref: WOS.makeORef("block", this.blockId),
|
|
|
|
meta: { "term:transparency": 0.5 },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
transparencySubMenu.push({
|
|
|
|
label: "No Transparency",
|
|
|
|
type: "checkbox",
|
|
|
|
checked: transparencyMeta == 0,
|
|
|
|
click: () => {
|
|
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
|
|
|
oref: WOS.makeORef("block", this.blockId),
|
|
|
|
meta: { "term:transparency": 0 },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-10-24 20:01:39 +02:00
|
|
|
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: () => {
|
2024-10-25 02:02:35 +02:00
|
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
2024-10-24 20:01:39 +02:00
|
|
|
oref: WOS.makeORef("block", this.blockId),
|
|
|
|
meta: { "term:fontsize": fontSize },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
);
|
|
|
|
fontSizeSubMenu.unshift({
|
|
|
|
label: "Default (" + defaultFontSize + "px)",
|
|
|
|
type: "checkbox",
|
|
|
|
checked: overrideFontSize == null,
|
|
|
|
click: () => {
|
2024-10-25 02:02:35 +02:00
|
|
|
RpcApi.SetMetaCommand(TabRpcClient, {
|
2024-10-24 20:01:39 +02:00
|
|
|
oref: WOS.makeORef("block", this.blockId),
|
|
|
|
meta: { "term:fontsize": null },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
2024-09-05 09:21:08 +02:00
|
|
|
fullMenu.push({
|
|
|
|
label: "Themes",
|
|
|
|
submenu: submenu,
|
|
|
|
});
|
2024-10-24 20:01:39 +02:00
|
|
|
fullMenu.push({
|
|
|
|
label: "Font Size",
|
|
|
|
submenu: fontSizeSubMenu,
|
|
|
|
});
|
2024-12-19 19:41:28 +01:00
|
|
|
fullMenu.push({
|
|
|
|
label: "Transparency",
|
|
|
|
submenu: transparencySubMenu,
|
|
|
|
});
|
2024-09-05 09:21:08 +02:00
|
|
|
fullMenu.push({ type: "separator" });
|
|
|
|
fullMenu.push({
|
|
|
|
label: "Force Restart Controller",
|
2024-11-12 00:30:40 +01:00
|
|
|
click: this.forceRestartController.bind(this),
|
2024-09-05 09:21:08 +02:00
|
|
|
});
|
2024-12-04 23:16:50 +01:00
|
|
|
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 },
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
2024-11-11 22:11:09 +01:00
|
|
|
if (blockData?.meta?.["term:vdomtoolbarblockid"]) {
|
|
|
|
fullMenu.push({ type: "separator" });
|
|
|
|
fullMenu.push({
|
|
|
|
label: "Close Toolbar",
|
|
|
|
click: () => {
|
|
|
|
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: blockData.meta["term:vdomtoolbarblockid"] });
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
2024-09-05 09:21:08 +02:00
|
|
|
return fullMenu;
|
2024-07-31 04:52:50 +02:00
|
|
|
}
|
2024-07-23 02:08:18 +02:00
|
|
|
}
|
|
|
|
|
2025-01-02 19:06:47 +01:00
|
|
|
function getAllBasicTermModels(): TermViewModel[] {
|
|
|
|
const allBCMs = getAllBlockComponentModels();
|
|
|
|
const rtn: TermViewModel[] = [];
|
|
|
|
for (const bcm of allBCMs) {
|
|
|
|
if (bcm.viewModel?.viewType != "term") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const termVM = bcm.viewModel as TermViewModel;
|
|
|
|
if (termVM.isBasicTerm(globalStore.get)) {
|
|
|
|
rtn.push(termVM);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return rtn;
|
|
|
|
}
|
|
|
|
|
2024-10-26 03:36:09 +02:00
|
|
|
function makeTerminalModel(blockId: string, nodeModel: BlockNodeModel): TermViewModel {
|
2024-10-17 23:50:36 +02:00
|
|
|
return new TermViewModel(blockId, nodeModel);
|
2024-07-23 02:08:18 +02:00
|
|
|
}
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
interface TerminalViewProps {
|
|
|
|
blockId: string;
|
|
|
|
model: TermViewModel;
|
|
|
|
}
|
|
|
|
|
2024-09-05 09:21:08 +02:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2024-11-11 22:11:09 +01:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-10-24 07:47:29 +02:00
|
|
|
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,
|
2024-10-26 03:36:09 +02:00
|
|
|
focusNode: () => {
|
|
|
|
model.nodeModel.focusNode();
|
|
|
|
},
|
2024-10-24 07:47:29 +02:00
|
|
|
onClose: () => {
|
|
|
|
if (vdomBlockId != null) {
|
|
|
|
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return (
|
|
|
|
<div key="htmlElem" className="term-htmlelem">
|
2024-10-26 03:36:09 +02:00
|
|
|
<SubBlock key="vdom" nodeModel={vdomNodeModel} />
|
2024-10-24 07:47:29 +02:00
|
|
|
</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} />;
|
|
|
|
};
|
|
|
|
|
2024-11-11 22:11:09 +01:00
|
|
|
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}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-07-25 05:34:22 +02:00
|
|
|
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
2024-10-17 23:50:36 +02:00
|
|
|
const viewRef = React.useRef<HTMLDivElement>(null);
|
2024-05-14 08:45:41 +02:00
|
|
|
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
2024-06-13 23:41:28 +02:00
|
|
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
2024-12-26 20:09:48 +01:00
|
|
|
const termSettingsAtom = getSettingsPrefixAtom("term");
|
2024-06-21 22:23:07 +02:00
|
|
|
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
2024-10-17 23:50:36 +02:00
|
|
|
let termMode = blockData?.meta?.["term:mode"] ?? "term";
|
2024-10-24 07:47:29 +02:00
|
|
|
if (termMode != "term" && termMode != "vdom") {
|
2024-10-17 23:50:36 +02:00
|
|
|
termMode = "term";
|
|
|
|
}
|
|
|
|
const termModeRef = React.useRef(termMode);
|
2024-07-25 05:34:22 +02:00
|
|
|
|
2024-10-24 20:01:39 +02:00
|
|
|
const termFontSize = jotai.useAtomValue(model.fontSizeAtom);
|
2024-12-05 19:02:07 +01:00
|
|
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
|
|
|
const connFontFamily = fullConfig.connections?.[blockData?.meta?.connection]?.["term:fontfamily"];
|
2025-01-02 19:06:47 +01:00
|
|
|
const isFocused = jotai.useAtomValue(model.nodeModel.isFocused);
|
|
|
|
const isMI = jotai.useAtomValue(atoms.isTermMultiInput);
|
|
|
|
const isBasicTerm = termMode != "vdom" && blockData?.meta?.controller != "cmd"; // needs to match isBasicTerm
|
2024-10-24 20:01:39 +02:00
|
|
|
|
2025-01-01 19:43:02 +01:00
|
|
|
// search
|
|
|
|
const searchProps = useSearch({
|
|
|
|
anchorRef: viewRef,
|
|
|
|
viewModel: model,
|
|
|
|
caseSensitive: false,
|
|
|
|
wholeWord: false,
|
|
|
|
regex: false,
|
|
|
|
});
|
|
|
|
const searchIsOpen = jotai.useAtomValue<boolean>(searchProps.isOpen);
|
|
|
|
const caseSensitive = useAtomValueSafe<boolean>(searchProps.caseSensitive);
|
|
|
|
const wholeWord = useAtomValueSafe<boolean>(searchProps.wholeWord);
|
|
|
|
const regex = useAtomValueSafe<boolean>(searchProps.regex);
|
|
|
|
const searchVal = jotai.useAtomValue<string>(searchProps.searchValue);
|
|
|
|
const searchDecorations = React.useMemo(
|
|
|
|
() => ({
|
|
|
|
matchOverviewRuler: "#000000",
|
|
|
|
activeMatchColorOverviewRuler: "#000000",
|
|
|
|
activeMatchBorder: "#FF9632",
|
|
|
|
matchBorder: "#FFFF00",
|
|
|
|
}),
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
const searchOpts = React.useMemo<ISearchOptions>(
|
|
|
|
() => ({
|
|
|
|
regex,
|
|
|
|
wholeWord,
|
|
|
|
caseSensitive,
|
|
|
|
decorations: searchDecorations,
|
|
|
|
}),
|
|
|
|
[regex, wholeWord, caseSensitive]
|
|
|
|
);
|
|
|
|
const handleSearchError = React.useCallback((e: Error) => {
|
|
|
|
console.warn("search error:", e);
|
|
|
|
}, []);
|
|
|
|
const executeSearch = React.useCallback(
|
|
|
|
(searchText: string, direction: "next" | "previous") => {
|
|
|
|
if (searchText === "") {
|
|
|
|
model.termRef.current?.searchAddon.clearDecorations();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
model.termRef.current?.searchAddon[direction === "next" ? "findNext" : "findPrevious"](
|
|
|
|
searchText,
|
|
|
|
searchOpts
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
handleSearchError(e);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[searchOpts, handleSearchError]
|
|
|
|
);
|
|
|
|
searchProps.onSearch = React.useCallback(
|
|
|
|
(searchText: string) => executeSearch(searchText, "previous"),
|
|
|
|
[executeSearch]
|
|
|
|
);
|
|
|
|
searchProps.onPrev = React.useCallback(() => executeSearch(searchVal, "previous"), [executeSearch, searchVal]);
|
|
|
|
searchProps.onNext = React.useCallback(() => executeSearch(searchVal, "next"), [executeSearch, searchVal]);
|
|
|
|
// Return input focus to the terminal when the search is closed
|
|
|
|
React.useEffect(() => {
|
|
|
|
if (!searchIsOpen) {
|
|
|
|
model.giveFocus();
|
|
|
|
}
|
|
|
|
}, [searchIsOpen]);
|
|
|
|
// rerun search when the searchOpts change
|
|
|
|
React.useEffect(() => {
|
|
|
|
model.termRef.current?.searchAddon.clearDecorations();
|
|
|
|
searchProps.onSearch(searchVal);
|
|
|
|
}, [searchOpts]);
|
|
|
|
// end search
|
|
|
|
|
2024-05-14 08:45:41 +02:00
|
|
|
React.useEffect(() => {
|
2024-08-28 03:49:49 +02:00
|
|
|
const fullConfig = globalStore.get(atoms.fullConfigAtom);
|
2024-11-21 19:44:16 +01:00
|
|
|
const termThemeName = globalStore.get(model.termThemeNameAtom);
|
2024-12-19 19:41:28 +01:00
|
|
|
const termTransparency = globalStore.get(model.termTransparencyAtom);
|
|
|
|
const [termTheme, _] = computeTheme(fullConfig, termThemeName, termTransparency);
|
2024-10-07 18:51:23 +02:00
|
|
|
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;
|
|
|
|
}
|
2024-12-11 21:26:56 +01:00
|
|
|
const wasFocused = model.termRef.current != null && globalStore.get(model.nodeModel.isFocused);
|
2024-06-24 23:34:31 +02:00
|
|
|
const termWrap = new TermWrap(
|
|
|
|
blockId,
|
|
|
|
connectElemRef.current,
|
|
|
|
{
|
2024-11-21 19:44:16 +01:00
|
|
|
theme: termTheme,
|
2024-10-24 20:01:39 +02:00
|
|
|
fontSize: termFontSize,
|
2024-12-05 19:02:07 +01:00
|
|
|
fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "Hack",
|
2024-06-24 23:34:31 +02:00
|
|
|
drawBoldTextInBrightColors: false,
|
|
|
|
fontWeight: "normal",
|
|
|
|
fontWeightBold: "bold",
|
2024-08-24 07:12:27 +02:00
|
|
|
allowTransparency: true,
|
2024-10-07 18:51:23 +02:00
|
|
|
scrollback: termScrollback,
|
2025-01-01 19:43:02 +01:00
|
|
|
allowProposedApi: true, // Required by @xterm/addon-search to enable search functionality and decorations
|
2024-06-24 23:34:31 +02:00
|
|
|
},
|
|
|
|
{
|
2024-10-17 23:50:36 +02:00
|
|
|
keydownHandler: model.handleTerminalKeydown.bind(model),
|
2024-08-28 03:49:49 +02:00
|
|
|
useWebGl: !termSettings?.["term:disablewebgl"],
|
2025-01-02 19:06:47 +01:00
|
|
|
sendDataHandler: model.sendDataToController.bind(model),
|
2024-06-24 23:34:31 +02:00
|
|
|
}
|
|
|
|
);
|
2024-06-18 07:38:48 +02:00
|
|
|
(window as any).term = termWrap;
|
2024-12-11 21:26:56 +01:00
|
|
|
model.termRef.current = termWrap;
|
2024-06-12 02:42:10 +02:00
|
|
|
const rszObs = new ResizeObserver(() => {
|
2024-06-18 07:38:48 +02:00
|
|
|
termWrap.handleResize_debounced();
|
2024-06-12 02:42:10 +02:00
|
|
|
});
|
|
|
|
rszObs.observe(connectElemRef.current);
|
2025-01-01 19:43:02 +01:00
|
|
|
termWrap.onSearchResultsDidChange = (results) => {
|
|
|
|
globalStore.set(searchProps.resultsIndex, results.resultIndex);
|
|
|
|
globalStore.set(searchProps.resultsCount, results.resultCount);
|
|
|
|
};
|
|
|
|
fireAndForget(termWrap.initTerminal.bind(termWrap));
|
2024-10-24 20:01:39 +02:00
|
|
|
if (wasFocused) {
|
|
|
|
setTimeout(() => {
|
|
|
|
model.giveFocus();
|
|
|
|
}, 10);
|
|
|
|
}
|
2024-06-07 00:08:39 +02:00
|
|
|
return () => {
|
2024-06-18 07:38:48 +02:00
|
|
|
termWrap.dispose();
|
2024-06-29 02:53:35 +02:00
|
|
|
rszObs.disconnect();
|
2024-06-07 00:08:39 +02:00
|
|
|
};
|
2024-12-05 19:02:07 +01:00
|
|
|
}, [blockId, termSettings, termFontSize, connFontFamily]);
|
2024-06-06 23:57:37 +02:00
|
|
|
|
2024-10-17 23:50:36 +02:00
|
|
|
React.useEffect(() => {
|
2024-10-24 07:47:29 +02:00
|
|
|
if (termModeRef.current == "vdom" && termMode == "term") {
|
2024-10-17 23:50:36 +02:00
|
|
|
// focus the terminal
|
|
|
|
model.giveFocus();
|
2024-06-14 08:54:04 +02:00
|
|
|
}
|
2024-10-17 23:50:36 +02:00
|
|
|
termModeRef.current = termMode;
|
|
|
|
}, [termMode]);
|
2024-06-14 08:54:04 +02:00
|
|
|
|
2025-01-02 19:06:47 +01:00
|
|
|
React.useEffect(() => {
|
|
|
|
if (isMI && isBasicTerm && isFocused && model.termRef.current != null) {
|
|
|
|
model.termRef.current.multiInputCallback = (data: string) => {
|
|
|
|
model.multiInputHandler(data);
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
if (model.termRef.current != null) {
|
|
|
|
model.termRef.current.multiInputCallback = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [isMI, isBasicTerm, isFocused]);
|
|
|
|
|
Fix Term Widget not registering pointer events (#1614)
This is a bit janky. The problem is that we were placing the
`xterm-viewport` div, which contains the scroll observer for the xterm
contents, at a higher z-index than the xterm contents, meaning that the
contents couldn't register any pointer events. If we don't put a
z-index, though, the scroll bar can't accept pointer events. To get
around this, I've added two observer divs, which control whether the
contents or the viewport have pointer event priority. The first div, the
`term-scrollbar-show-observer`, sits above where the scrollbar will be
rendered. When the user hovers over it, it will cause the viewport div
to move to a z-index above the contents. It will also enable a second
div, the `term-scrollbar-hide-observer`, which sits above the viewport
and the term contents, but not blocking the scrollbar. When the user
hovers over this div (indicating their mouse has left the scrollbar),
the viewport div is moved back to its original z-index and the hide
observer is set to `display: none`. This gives pointer event priority
back to the contents div.
This resolves an issue where the user could not click links in the
terminal output.
Resolves #1357
2024-12-24 05:35:51 +01:00
|
|
|
const scrollbarHideObserverRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
const onScrollbarShowObserver = React.useCallback(() => {
|
|
|
|
const termViewport = viewRef.current.getElementsByClassName("xterm-viewport")[0] as HTMLDivElement;
|
|
|
|
termViewport.style.zIndex = "var(--zindex-xterm-viewport-overlay)";
|
|
|
|
scrollbarHideObserverRef.current.style.display = "block";
|
|
|
|
}, []);
|
|
|
|
const onScrollbarHideObserver = React.useCallback(() => {
|
|
|
|
const termViewport = viewRef.current.getElementsByClassName("xterm-viewport")[0] as HTMLDivElement;
|
|
|
|
termViewport.style.zIndex = "auto";
|
|
|
|
scrollbarHideObserverRef.current.style.display = "none";
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const stickerConfig = {
|
2024-06-18 07:38:48 +02:00
|
|
|
charWidth: 8,
|
|
|
|
charHeight: 16,
|
2024-12-11 21:26:56 +01:00
|
|
|
rows: model.termRef.current?.terminal.rows ?? 24,
|
|
|
|
cols: model.termRef.current?.terminal.cols ?? 80,
|
2024-06-18 07:38:48 +02:00
|
|
|
blockId: blockId,
|
|
|
|
};
|
2025-01-01 19:43:02 +01:00
|
|
|
|
2024-05-15 01:53:03 +02:00
|
|
|
return (
|
2024-09-03 09:02:03 +02:00
|
|
|
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
|
2024-09-05 09:21:08 +02:00
|
|
|
<TermResyncHandler blockId={blockId} model={model} />
|
2024-12-11 21:26:56 +01:00
|
|
|
<TermThemeUpdater blockId={blockId} model={model} termRef={model.termRef} />
|
2024-06-18 07:38:48 +02:00
|
|
|
<TermStickers config={stickerConfig} />
|
2024-11-11 22:11:09 +01:00
|
|
|
<TermToolbarVDomNode key="vdom-toolbar" blockId={blockId} model={model} />
|
2024-10-24 07:47:29 +02:00
|
|
|
<TermVDomNode key="vdom" blockId={blockId} model={model} />
|
Fix Term Widget not registering pointer events (#1614)
This is a bit janky. The problem is that we were placing the
`xterm-viewport` div, which contains the scroll observer for the xterm
contents, at a higher z-index than the xterm contents, meaning that the
contents couldn't register any pointer events. If we don't put a
z-index, though, the scroll bar can't accept pointer events. To get
around this, I've added two observer divs, which control whether the
contents or the viewport have pointer event priority. The first div, the
`term-scrollbar-show-observer`, sits above where the scrollbar will be
rendered. When the user hovers over it, it will cause the viewport div
to move to a z-index above the contents. It will also enable a second
div, the `term-scrollbar-hide-observer`, which sits above the viewport
and the term contents, but not blocking the scrollbar. When the user
hovers over this div (indicating their mouse has left the scrollbar),
the viewport div is moved back to its original z-index and the hide
observer is set to `display: none`. This gives pointer event priority
back to the contents div.
This resolves an issue where the user could not click links in the
terminal output.
Resolves #1357
2024-12-24 05:35:51 +01:00
|
|
|
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}>
|
|
|
|
<div className="term-scrollbar-show-observer" onPointerOver={onScrollbarShowObserver} />
|
|
|
|
<div
|
|
|
|
ref={scrollbarHideObserverRef}
|
|
|
|
className="term-scrollbar-hide-observer"
|
|
|
|
onPointerOver={onScrollbarHideObserver}
|
|
|
|
/>
|
|
|
|
</div>
|
2025-01-01 19:43:02 +01:00
|
|
|
<Search {...searchProps} />
|
2024-05-15 01:53:03 +02:00
|
|
|
</div>
|
|
|
|
);
|
2024-05-14 08:45:41 +02:00
|
|
|
};
|
|
|
|
|
2024-11-21 19:44:16 +01:00
|
|
|
export { makeTerminalModel, TerminalView, TermViewModel };
|