import React, { useState, useEffect, useRef } from "react"; import { EventEmitter } from "../../utils/EventEmitter"; import { RunningScript } from "../../Script/RunningScript"; import { killWorkerScript } from "../../Netscript/killWorkerScript"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Paper from "@mui/material/Paper"; import Draggable, { DraggableEvent } from "react-draggable"; import { ResizableBox, ResizeCallbackData } from "react-resizable"; import makeStyles from "@mui/styles/makeStyles"; import createStyles from "@mui/styles/createStyles"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import { workerScripts } from "../../Netscript/WorkerScripts"; import { startWorkerScript } from "../../NetscriptWorker"; import { GetServer } from "../../Server/AllServers"; import { findRunningScript } from "../../Script/ScriptHelpers"; import { debounce } from "lodash"; import { Settings } from "../../Settings/Settings"; import { ANSIITypography } from "./ANSIITypography"; import { ScriptArg } from "../../Netscript/ScriptArg"; import { useRerender } from "./hooks"; import { dialogBoxCreate } from "./DialogBox"; let layerCounter = 0; export const LogBoxEvents = new EventEmitter<[RunningScript]>(); export const LogBoxCloserEvents = new EventEmitter<[number]>(); export const LogBoxClearEvents = new EventEmitter<[]>(); interface LogBoxUIEvent { pid: number; data: T; } interface LogBoxPositionData { x: number; y: number; } export const LogBoxPositionEvents = new EventEmitter<[LogBoxUIEvent]>(); interface LogBoxResizeData { w: number; h: number; } export const LogBoxSizeEvents = new EventEmitter<[LogBoxUIEvent]>(); interface Log { id: string; script: RunningScript; } let logs: Log[] = []; export function LogBoxManager(): React.ReactElement { const rerender = useRerender(); useEffect( () => LogBoxEvents.subscribe((script: RunningScript) => { const id = script.server + "-" + script.filename + script.args.map((x: ScriptArg): string => `${x}`).join("-"); if (logs.find((l) => l.id === id)) return; logs.push({ id: id, script: script, }); rerender(); }), [], ); //Event used by ns.closeTail to close tail windows useEffect( () => LogBoxCloserEvents.subscribe((pid: number) => { closePid(pid); }), [], ); useEffect(() => LogBoxClearEvents.subscribe(() => { logs = []; rerender(); }), ); //Close tail windows by their id function close(id: string): void { logs = logs.filter((l) => l.id !== id); rerender(); } //Close tail windows by their pid function closePid(pid: number): void { logs = logs.filter((log) => log.script.pid != pid); rerender(); } return ( <> {logs.map((log) => ( close(log.id)} /> ))} ); } interface IProps { script: RunningScript; id: string; onClose: () => void; } const useStyles = makeStyles(() => createStyles({ logs: { overflowY: "scroll", overflowX: "hidden", scrollbarWidth: "auto", flexDirection: "column-reverse", whiteSpace: "pre-wrap", wordWrap: "break-word", }, titleButton: { padding: "1px 0", height: "100%", }, }), ); export const logBoxBaseZIndex = 1500; function LogWindow(props: IProps): React.ReactElement { const draggableRef = useRef(null); const rootRef = useRef(null); const [script, setScript] = useState(props.script); const classes = useStyles(); const container = useRef(null); const textArea = useRef(null); const rerender = useRerender(1000); const [size, setSize] = useState<[number, number]>([500, 500]); const [minimized, setMinimized] = useState(false); const textAreaKeyDown = (e: React.KeyboardEvent) => { if (e.ctrlKey && e.key === "a") { if (!textArea.current) return; //Should never happen const r = new Range(); r.setStartBefore(textArea.current); r.setEndAfter(textArea.current); document.getSelection()?.removeAllRanges(); document.getSelection()?.addRange(r); e.preventDefault(); e.stopPropagation(); } }; const onResize = (e: React.SyntheticEvent, { size }: ResizeCallbackData) => { setSize([size.width, size.height]); }; // useEffect( // () => // WorkerScriptStartStopEventEmitter.subscribe(() => { // setTimeout(() => { // const server = GetServer(script.server); // if (server === null) return; // const existingScript = findRunningScript(script.filename, script.args, server); // if (existingScript) { // existingScript.logs = script.logs.concat(existingScript.logs) // setScript(existingScript) // } // rerender(); // }, 100) // }), // [], // ); const setPosition = ({ x, y }: LogBoxPositionData) => { const node = rootRef?.current; if (!node) return; const state = node.state as { x: number; y: number }; state.x = x; state.y = y; }; // Listen to Logbox positioning events. useEffect( () => LogBoxPositionEvents.subscribe((e) => { if (e.pid !== props.script.pid) return; setPosition(e.data); }), [], ); // Listen to Logbox resizing events. useEffect( () => LogBoxSizeEvents.subscribe((e) => { if (e.pid !== props.script.pid) return; setSize([e.data.w, e.data.h]); }), [], ); // initial position if 40%/30% useEffect(() => setPosition({ x: window.innerWidth * 0.4, y: window.innerHeight * 0.3 }), []); useEffect(() => { updateLayer(); }, []); function kill(): void { killWorkerScript({ runningScript: script, hostname: script.server }); } function run(): void { const server = GetServer(script.server); if (server === null) return; const s = findRunningScript(script.filename, script.args, server); if (s === null) { const baseScript = server.scripts.get(script.filename); if (!baseScript) { return dialogBoxCreate( `Could not launch script. The script ${script.filename} no longer exists on the server ${server.hostname}.`, ); } const ramUsage = baseScript.getRamUsage(server.scripts); if (!ramUsage) { return dialogBoxCreate(`Could not calculate ram usage for ${script.filename} on ${server.hostname}.`); } script.ramUsage = ramUsage; startWorkerScript(script, server); } else { setScript(s); } } function updateLayer(): void { const c = container.current; if (c === null) return; c.style.zIndex = logBoxBaseZIndex + layerCounter + ""; layerCounter++; rerender(); } function title(full = false): string { const maxLength = 30; const t = `${script.server}: ${script.filename} ${script.args.join(" ")}`; if (full || t.length <= maxLength) { return t; } return t.slice(0, maxLength - 3) + "..."; } function minimize(): void { setMinimized(!minimized); } function lineColor(s: string): "error" | "success" | "warn" | "info" | "primary" { if (s.match(/(^\[[^\]]+\] )?ERROR/) || s.match(/(^\[[^\]]+\] )?FAIL/)) { return "error"; } if (s.match(/(^\[[^\]]+\] )?SUCCESS/)) { return "success"; } if (s.match(/(^\[[^\]]+\] )?WARN/)) { return "warn"; } if (s.match(/(^\[[^\]]+\] )?INFO/)) { return "info"; } return "primary"; } // And trigger fakeDrag when the window is resized useEffect(() => { window.addEventListener("resize", onWindowResize); return () => { window.removeEventListener("resize", onWindowResize); }; }, []); const onWindowResize = debounce((): void => { const node = draggableRef?.current; if (!node) return; if (!isOnScreen(node)) { setPosition({ x: 0, y: 0 }); } }, 100); const isOnScreen = (node: HTMLDivElement): boolean => { const bounds = node.getBoundingClientRect(); return !(bounds.right < 0 || bounds.bottom < 0 || bounds.left > innerWidth || bounds.top > outerWidth); }; const boundToBody = (e: DraggableEvent): void | false => { if ( e instanceof MouseEvent && (e.clientX < 0 || e.clientY < 0 || e.clientX > innerWidth || e.clientY > innerHeight) ) return false; }; // Max [width, height] const minConstraints: [number, number] = [250, 33]; return ( } > <> {title(true)} {!workerScripts.has(script.pid) ? ( ) : ( )}
{script.logs.map( (line: React.ReactNode, i: number): React.ReactNode => typeof line !== "string" ? line : , )}
); }