mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
cmd blocks (#74)
This commit is contained in:
@@ -32,7 +32,7 @@ function processTitleString(titleString: string): React.ReactNode[] {
|
||||
if (titleString == null) {
|
||||
return null;
|
||||
}
|
||||
const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9-]+))?>/g;
|
||||
const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g;
|
||||
let lastIdx = 0;
|
||||
let match;
|
||||
let partsStack = [[]];
|
||||
@@ -46,10 +46,11 @@ function processTitleString(titleString: string): React.ReactNode[] {
|
||||
if (tagParam == null) {
|
||||
continue;
|
||||
}
|
||||
if (!tagParam.match(/^[a-z0-9-]+$/)) {
|
||||
const iconClass = util.makeIconClass(tagParam, false);
|
||||
if (iconClass == null) {
|
||||
continue;
|
||||
}
|
||||
lastPart.push(<i key={match.index} className={`fa fa-solid fa-${tagParam}`} />);
|
||||
lastPart.push(<i key={match.index} className={iconClass} />);
|
||||
continue;
|
||||
}
|
||||
if (tagName == "c" || tagName == "color") {
|
||||
@@ -105,10 +106,9 @@ function getBlockHeaderText(blockIcon: string, blockData: Block): React.ReactNod
|
||||
if (!util.isBlank(iconColor)) {
|
||||
iconStyle = { color: iconColor };
|
||||
}
|
||||
if (blockIcon.match(/^[a-z0-9-]+$/)) {
|
||||
blockIconElem = (
|
||||
<i key="icon" style={iconStyle} className={`block-frame-icon fa fa-solid fa-${blockIcon}`} />
|
||||
);
|
||||
const iconClass = util.makeIconClass(blockIcon, false);
|
||||
if (iconClass != null) {
|
||||
blockIconElem = <i key="icon" style={iconStyle} className={clsx(`block-frame-icon`, iconClass)} />;
|
||||
}
|
||||
}
|
||||
if (!util.isBlank(blockData?.meta?.title)) {
|
||||
@@ -123,7 +123,11 @@ function getBlockHeaderText(blockIcon: string, blockData: Block): React.ReactNod
|
||||
return [blockIconElem, blockData.meta.title];
|
||||
}
|
||||
}
|
||||
return [blockIconElem, `${blockData?.view} [${blockData.oid.substring(0, 8)}]`];
|
||||
let viewString = blockData?.view;
|
||||
if (blockData.controller == "cmd") {
|
||||
viewString = "cmd";
|
||||
}
|
||||
return [blockIconElem, `${viewString} [${blockData.oid.substring(0, 8)}]`];
|
||||
}
|
||||
|
||||
interface FramelessBlockHeaderProps {
|
||||
|
||||
@@ -216,7 +216,6 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// we send to two subjects just eventType and eventType|oref
|
||||
// we don't use getORefSubject here because we don't want to create a new subject
|
||||
const eventSubject = eventSubjects.get(msg.eventtype);
|
||||
|
||||
@@ -7,6 +7,9 @@ import * as WOS from "./wos";
|
||||
|
||||
// blockservice.BlockService (block)
|
||||
class BlockServiceType {
|
||||
GetControllerStatus(arg2: string): Promise<BlockControllerRuntimeStatus> {
|
||||
return WOS.callBackendService("block", "GetControllerStatus", Array.from(arguments))
|
||||
}
|
||||
SaveTerminalState(arg2: string, arg3: string, arg4: string, arg5: number): Promise<void> {
|
||||
return WOS.callBackendService("block", "SaveTerminalState", Array.from(arguments))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WOS, atoms, globalStore, sendWSCommand, useBlockAtom, useSettingsAtom } from "@/store/global";
|
||||
import {
|
||||
WOS,
|
||||
atoms,
|
||||
getEventORefSubject,
|
||||
globalStore,
|
||||
sendWSCommand,
|
||||
useBlockAtom,
|
||||
useSettingsAtom,
|
||||
} from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as keyutil from "@/util/keyutil";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
@@ -142,7 +151,10 @@ function setBlockFocus(blockId: string) {
|
||||
const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||
const termRef = React.useRef<TermWrap>(null);
|
||||
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
||||
const shellProcStatusRef = React.useRef<string>(null);
|
||||
const blockIconOverrideAtom = useBlockAtom<string>(blockId, "blockicon:override", () => {
|
||||
return jotai.atom<string>(null);
|
||||
}) as jotai.PrimitiveAtom<string>;
|
||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
||||
@@ -157,14 +169,38 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
React.useEffect(() => {
|
||||
const termWrap = new TermWrap(blockId, connectElemRef.current, {
|
||||
theme: getThemeFromCSSVars(connectElemRef.current),
|
||||
fontSize: termSettings?.fontsize ?? 12,
|
||||
fontFamily: termSettings?.fontfamily ?? "Hack",
|
||||
drawBoldTextInBrightColors: false,
|
||||
fontWeight: "normal",
|
||||
fontWeightBold: "bold",
|
||||
});
|
||||
function handleTerminalKeydown(event: KeyboardEvent) {
|
||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": "html" } };
|
||||
services.BlockService.SendCommand(this.blockId, metaCmd);
|
||||
return false;
|
||||
}
|
||||
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
|
||||
// restart
|
||||
const restartCmd: BlockRestartCommand = { command: "controller:restart", blockid: blockId };
|
||||
services.BlockService.SendCommand(blockId, restartCmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const termWrap = new TermWrap(
|
||||
blockId,
|
||||
connectElemRef.current,
|
||||
{
|
||||
theme: getThemeFromCSSVars(connectElemRef.current),
|
||||
fontSize: termSettings?.fontsize ?? 12,
|
||||
fontFamily: termSettings?.fontfamily ?? "Hack",
|
||||
drawBoldTextInBrightColors: false,
|
||||
fontWeight: "normal",
|
||||
fontWeightBold: "bold",
|
||||
},
|
||||
{
|
||||
keydownHandler: handleTerminalKeydown,
|
||||
}
|
||||
);
|
||||
(window as any).term = termWrap;
|
||||
termRef.current = termWrap;
|
||||
termWrap.addFocusListener(() => {
|
||||
@@ -181,7 +217,8 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
}, []);
|
||||
|
||||
const handleHtmlKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.code === "Escape" && event.metaKey) {
|
||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
// reset term:mode
|
||||
const metaCmd: BlockSetMetaCommand = { command: "setmeta", meta: { "term:mode": null } };
|
||||
services.BlockService.SendCommand(blockId, metaCmd);
|
||||
@@ -211,6 +248,32 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
function updateShellProcStatus(status: string) {
|
||||
if (status == null) {
|
||||
return;
|
||||
}
|
||||
shellProcStatusRef.current = status;
|
||||
if (status == "running") {
|
||||
termRef.current?.setIsRunning(true);
|
||||
globalStore.set(blockIconOverrideAtom, "square-terminal");
|
||||
} else {
|
||||
termRef.current?.setIsRunning(false);
|
||||
globalStore.set(blockIconOverrideAtom, "regular@square-terminal");
|
||||
}
|
||||
}
|
||||
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
||||
initialRTStatus.then((rts) => {
|
||||
updateShellProcStatus(rts?.shellprocstatus);
|
||||
});
|
||||
const bcSubject = getEventORefSubject("blockcontroller:status", WOS.makeORef("block", blockId));
|
||||
bcSubject.subscribe((data: WSEventType) => {
|
||||
let bcRTS: BlockControllerRuntimeStatus = data.data;
|
||||
updateShellProcStatus(bcRTS?.shellprocstatus);
|
||||
});
|
||||
return undefined;
|
||||
});
|
||||
|
||||
let stickerConfig = {
|
||||
charWidth: 8,
|
||||
charHeight: 16,
|
||||
|
||||
@@ -22,11 +22,14 @@ export class TermWrap {
|
||||
loaded: boolean;
|
||||
heldData: Uint8Array[];
|
||||
handleResize_debounced: () => void;
|
||||
isRunning: boolean;
|
||||
keydownHandler: (e: KeyboardEvent) => void;
|
||||
|
||||
constructor(
|
||||
blockId: string,
|
||||
connectElem: HTMLDivElement,
|
||||
options?: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions
|
||||
options: TermTypes.ITerminalOptions & TermTypes.ITerminalInitOnlyOptions,
|
||||
waveOptions: { keydownHandler?: (e: KeyboardEvent) => void }
|
||||
) {
|
||||
this.blockId = blockId;
|
||||
this.ptyOffset = 0;
|
||||
@@ -43,10 +46,12 @@ export class TermWrap {
|
||||
this.handleResize_debounced = debounce(50, this.handleResize.bind(this));
|
||||
this.terminal.open(this.connectElem);
|
||||
this.handleResize();
|
||||
this.isRunning = true;
|
||||
this.keydownHandler = waveOptions.keydownHandler;
|
||||
}
|
||||
|
||||
async initTerminal() {
|
||||
this.connectElem.addEventListener("keydown", this.keydownListener.bind(this), true);
|
||||
this.connectElem.addEventListener("keydown", this.keydownHandler, true);
|
||||
this.terminal.onData(this.handleTermData.bind(this));
|
||||
this.mainFileSubject = getFileSubject(this.blockId, "main");
|
||||
this.mainFileSubject.subscribe(this.handleNewFileSubjectData.bind(this));
|
||||
@@ -58,6 +63,10 @@ export class TermWrap {
|
||||
this.runProcessIdleTimeout();
|
||||
}
|
||||
|
||||
setIsRunning(isRunning: boolean) {
|
||||
this.isRunning = isRunning;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.terminal.dispose();
|
||||
this.mainFileSubject.release();
|
||||
@@ -69,7 +78,11 @@ export class TermWrap {
|
||||
const wsCmd: BlockInputWSCommand = { wscommand: "blockinput", blockid: this.blockId, inputdata64: b64data };
|
||||
sendWSCommand(wsCmd);
|
||||
} else {
|
||||
const inputCmd: BlockInputCommand = { command: "controller:input", inputdata64: b64data };
|
||||
const inputCmd: BlockInputCommand = {
|
||||
command: "controller:input",
|
||||
blockid: this.blockId,
|
||||
inputdata64: b64data,
|
||||
};
|
||||
services.BlockService.SendCommand(this.blockId, inputCmd);
|
||||
}
|
||||
}
|
||||
@@ -78,27 +91,21 @@ export class TermWrap {
|
||||
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") {
|
||||
if (msg.fileop == "truncate") {
|
||||
this.terminal.clear();
|
||||
this.heldData = [];
|
||||
} else if (msg.fileop == "append") {
|
||||
const decodedData = base64ToArray(msg.data64);
|
||||
if (this.loaded) {
|
||||
this.doTerminalWrite(decodedData, null);
|
||||
} else {
|
||||
this.heldData.push(decodedData);
|
||||
}
|
||||
} else {
|
||||
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> {
|
||||
|
||||
Vendored
+33
-18
@@ -31,7 +31,14 @@ declare global {
|
||||
|
||||
type BlockCommand = {
|
||||
command: string;
|
||||
} & ( BlockAppendFileCommand | BlockAppendIJsonCommand | BlockInputCommand | CreateBlockCommand | BlockGetMetaCommand | BlockMessageCommand | ResolveIdsCommand | BlockSetMetaCommand | BlockSetViewCommand );
|
||||
} & ( BlockAppendFileCommand | BlockAppendIJsonCommand | BlockInputCommand | BlockRestartCommand | CreateBlockCommand | BlockGetMetaCommand | BlockMessageCommand | ResolveIdsCommand | BlockSetMetaCommand | BlockSetViewCommand );
|
||||
|
||||
// blockcontroller.BlockControllerRuntimeStatus
|
||||
type BlockControllerRuntimeStatus = {
|
||||
blockid: string;
|
||||
status: string;
|
||||
shellprocstatus?: string;
|
||||
};
|
||||
|
||||
// wstore.BlockDef
|
||||
type BlockDef = {
|
||||
@@ -69,6 +76,12 @@ declare global {
|
||||
message: string;
|
||||
};
|
||||
|
||||
// wshutil.BlockRestartCommand
|
||||
type BlockRestartCommand = {
|
||||
command: "controller:restart";
|
||||
blockid: string;
|
||||
};
|
||||
|
||||
// wshutil.BlockSetMetaCommand
|
||||
type BlockSetMetaCommand = {
|
||||
command: "setmeta";
|
||||
@@ -97,15 +110,17 @@ declare global {
|
||||
rtopts?: RuntimeOpts;
|
||||
};
|
||||
|
||||
type DateTimeConfigType = {
|
||||
locale: string;
|
||||
format: DateTimeFormatConfigType;
|
||||
}
|
||||
// wconfig.DateTimeConfigType
|
||||
type DateTimeConfigType = {
|
||||
locale: string;
|
||||
format: DateTimeFormatConfigType;
|
||||
};
|
||||
|
||||
type DateTimeFormatConfigType = {
|
||||
dateStyle: "full" | "long" | "medium" | "short";
|
||||
timeStyle: "full" | "long" | "medium" | "short";
|
||||
}
|
||||
// wconfig.DateTimeFormatConfigType
|
||||
type DateTimeFormatConfigType = {
|
||||
dateStyle: number;
|
||||
timeStyle: number;
|
||||
};
|
||||
|
||||
// wstore.FileDef
|
||||
type FileDef = {
|
||||
@@ -122,7 +137,7 @@ declare global {
|
||||
notfound?: boolean;
|
||||
size: number;
|
||||
mode: number;
|
||||
modestr: string;
|
||||
modestr: string;
|
||||
modtime: number;
|
||||
isdir?: boolean;
|
||||
mimetype?: string;
|
||||
@@ -157,10 +172,10 @@ declare global {
|
||||
ReturnDesc: string;
|
||||
};
|
||||
|
||||
//wconfig.MimeTypeConfigType
|
||||
type MimeTypeConfigType = {
|
||||
icon: string;
|
||||
}
|
||||
// wconfig.MimeTypeConfigType
|
||||
type MimeTypeConfigType = {
|
||||
icon: string;
|
||||
};
|
||||
|
||||
// waveobj.ORef
|
||||
type ORef = {
|
||||
@@ -195,10 +210,10 @@ declare global {
|
||||
|
||||
// wconfig.SettingsConfigType
|
||||
type SettingsConfigType = {
|
||||
datetime: DateTimeConfigType;
|
||||
mimetypes: {[key:string]: MimeTypeConfigType}
|
||||
widgets: WidgetsConfigType[];
|
||||
mimetypes: {[key: string]: MimeTypeConfigType};
|
||||
datetime: DateTimeConfigType;
|
||||
term: TerminalConfigType;
|
||||
widgets: WidgetsConfigType[];
|
||||
};
|
||||
|
||||
// wstore.StickerClickOptsType
|
||||
@@ -352,4 +367,4 @@ declare global {
|
||||
|
||||
}
|
||||
|
||||
export {}
|
||||
export {}
|
||||
|
||||
+19
-1
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0s
|
||||
|
||||
import base64 from "base64-js";
|
||||
import clsx from "clsx";
|
||||
|
||||
function isBlank(str: string): boolean {
|
||||
return str == null || str == "";
|
||||
@@ -71,4 +72,21 @@ function jsonDeepEqual(v1: any, v2: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export { base64ToArray, base64ToString, isBlank, jsonDeepEqual, stringToBase64 };
|
||||
function makeIconClass(icon: string, fw: boolean): string {
|
||||
if (icon == null) {
|
||||
return null;
|
||||
}
|
||||
if (icon.match(/^(solid@)?[a-z0-9-]+$/)) {
|
||||
// strip off "solid@" prefix if it exists
|
||||
icon = icon.replace(/^solid@/, "");
|
||||
return clsx(`fa fa-sharp fa-solid fa-${icon}`, fw ? "fa-fw" : null);
|
||||
}
|
||||
if (icon.match(/^regular@[a-z0-9-]+$/)) {
|
||||
// strip off the "regular@" prefix if it exists
|
||||
icon = icon.replace(/^regular@/, "");
|
||||
return clsx(`fa fa-sharp fa-regular fa-${icon}`, fw ? "fa-fw" : null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { base64ToArray, base64ToString, isBlank, jsonDeepEqual, makeIconClass, stringToBase64 };
|
||||
|
||||
Reference in New Issue
Block a user