cmd blocks (#74)

This commit is contained in:
Mike Sawka
2024-06-24 14:34:31 -07:00
committed by GitHub
parent edb8eb25b8
commit 77b5acfc5a
16 changed files with 523 additions and 124 deletions
+12 -8
View File
@@ -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 {
-1
View File
@@ -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);
+3
View File
@@ -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))
}
+74 -11
View File
@@ -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,
+27 -20
View File
@@ -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> {
+33 -18
View File
@@ -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
View File
@@ -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 };