UI: LogBox overhaul (#508)

This commit is contained in:
David Walker 2023-05-26 05:07:37 -07:00 committed by GitHub
parent 113af6e711
commit 4503da6226
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 431 additions and 114 deletions

@ -84,6 +84,7 @@
| [Player](./bitburner.player.md) | |
| [ProcessInfo](./bitburner.processinfo.md) | A single process on a server. |
| [Product](./bitburner.product.md) | Product in a warehouse |
| [ReactElement](./bitburner.reactelement.md) | A stand-in for the real React.ReactElement, which API-extractor doesn't know about. Don't try to create one of these by hand; use React.createElement(). |
| [RecentScript](./bitburner.recentscript.md) | |
| [ReputationFormulas](./bitburner.reputationformulas.md) | Reputation formulas |
| [ResetInfo](./bitburner.resetinfo.md) | Various info about resets |
@ -99,6 +100,7 @@
| [Stanek](./bitburner.stanek.md) | Stanek's Gift API. |
| [StockOrder](./bitburner.stockorder.md) | <p>Return value of [getOrders](./bitburner.tix.getorders.md)</p><p>Keys are stock symbols, properties are arrays of [StockOrderObject](./bitburner.stockorderobject.md)</p> |
| [StockOrderObject](./bitburner.stockorderobject.md) | Value in map of [StockOrder](./bitburner.stockorder.md) |
| [TailProperties](./bitburner.tailproperties.md) | |
| [TIX](./bitburner.tix.md) | Stock market API |
| [UserInterface](./bitburner.userinterface.md) | User Interface API. |
| [UserInterfaceTheme](./bitburner.userinterfacetheme.md) | Interface Theme |

@ -21,7 +21,7 @@ exec(
| Parameter | Type | Description |
| --- | --- | --- |
| script | string | Filename of script to execute. |
| script | string | Filename of script to execute. This file must already exist on the target server. |
| hostname | string | Hostname of the <code>target server</code> on which to execute the script. |
| threadOrOptions | number \| [RunOptions](./bitburner.runoptions.md) | _(Optional)_ Either an integer number of threads for new script, or a [RunOptions](./bitburner.runoptions.md) object. Threads defaults to 1. |
| args | (string \| number \| boolean)\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument threadOrOptions must be filled in with a value. |
@ -36,7 +36,7 @@ Returns the PID of a successfully started script, and 0 otherwise.
RAM cost: 1.3 GB
Run a script as a separate process on a specified server. This is similar to the function [run](./bitburner.ns.run.md) except that it can be used to run a script on any server, instead of just the current server.
Run a script as a separate process on a specified server. This is similar to the function [run](./bitburner.ns.run.md) except that it can be used to run a script that already exists on any server, instead of just the current server.
If the script was successfully started, then this function returns the PID of that script. Otherwise, it returns 0.

@ -152,6 +152,7 @@ export async function main(ns) {
| [scriptKill(script, host)](./bitburner.ns.scriptkill.md) | Kill all scripts with a filename. |
| [scriptRunning(script, host)](./bitburner.ns.scriptrunning.md) | Check if any script with a filename is running. |
| [serverExists(host)](./bitburner.ns.serverexists.md) | Returns a boolean denoting whether or not the specified server exists. |
| [setTitle(title, pid)](./bitburner.ns.settitle.md) | Set the title of the tail window of a script. |
| [share()](./bitburner.ns.share.md) | Share the server's ram with your factions. |
| [sleep(millis)](./bitburner.ns.sleep.md) | Suspends the script for n milliseconds. |
| [spawn(script, threadOrOptions, args)](./bitburner.ns.spawn.md) | Terminate current script and start another in 10 seconds. |

@ -0,0 +1,39 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [NS](./bitburner.ns.md) &gt; [setTitle](./bitburner.ns.settitle.md)
## NS.setTitle() method
Set the title of the tail window of a script.
**Signature:**
```typescript
setTitle(title: string | ReactElement, pid?: number): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| title | string \| [ReactElement](./bitburner.reactelement.md) | |
| pid | number | _(Optional)_ Optional. PID of the script having its tail closed. If omitted, the current script is used. |
**Returns:**
void
## Remarks
RAM cost: 0 GB
This sets the title to the given string, and also forces an update of the tail window's contents.
The title is saved across restarts, but only if it is a simple string.
If the pid is unspecified, it will modify the current scripts logs.
Otherwise, the pid argument can be used to change the logs from another script.
It is possible to pass a React Element instead of a string. Get these by calling React.createElement() with appropriate parameters. You should either know or be willing to learn about the React UI library if you go down this route, and there will likely be rough edges.

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md) &gt; [key](./bitburner.reactelement.key.md)
## ReactElement.key property
**Signature:**
```typescript
key: string | number | null;
```

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md)
## ReactElement interface
A stand-in for the real React.ReactElement, which API-extractor doesn't know about. Don't try to create one of these by hand; use React.createElement().
**Signature:**
```typescript
interface ReactElement
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [key](./bitburner.reactelement.key.md) | | string \| number \| null | |
| [props](./bitburner.reactelement.props.md) | | any | |
| [type](./bitburner.reactelement.type.md) | | string \| ((props: any) =&gt; [ReactElement](./bitburner.reactelement.md) \| null) \| (new (props: any) =&gt; object) | |

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md) &gt; [props](./bitburner.reactelement.props.md)
## ReactElement.props property
**Signature:**
```typescript
props: any;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md) &gt; [type](./bitburner.reactelement.type.md)
## ReactElement.type property
**Signature:**
```typescript
type: string | ((props: any) => ReactElement | null) | (new (props: any) => object);
```

@ -27,6 +27,8 @@ interface RunningScript
| [pid](./bitburner.runningscript.pid.md) | | number | Process ID. Must be an integer |
| [ramUsage](./bitburner.runningscript.ramusage.md) | | number | How much RAM this script uses for ONE thread |
| [server](./bitburner.runningscript.server.md) | | string | Hostname of the server on which this script runs |
| [tailProperties](./bitburner.runningscript.tailproperties.md) | | [TailProperties](./bitburner.tailproperties.md) \| null | Properties of the tail window, or null if it is not shown |
| [temporary](./bitburner.runningscript.temporary.md) | | boolean | Whether this RunningScript is excluded from saves |
| [threads](./bitburner.runningscript.threads.md) | | number | Number of threads that this script runs with |
| [title](./bitburner.runningscript.title.md) | | string \| [ReactElement](./bitburner.reactelement.md) | The title, as shown in the script's log box. Defaults to the name + args, but can be changed by the user. If it is set to a React element (only by the user), that will not be persisted, and will be restored to default on load. |

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [RunningScript](./bitburner.runningscript.md) &gt; [tailProperties](./bitburner.runningscript.tailproperties.md)
## RunningScript.tailProperties property
Properties of the tail window, or null if it is not shown
**Signature:**
```typescript
tailProperties: TailProperties | null;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [RunningScript](./bitburner.runningscript.md) &gt; [title](./bitburner.runningscript.title.md)
## RunningScript.title property
The title, as shown in the script's log box. Defaults to the name + args, but can be changed by the user. If it is set to a React element (only by the user), that will not be persisted, and will be restored to default on load.
**Signature:**
```typescript
title: string | ReactElement;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [height](./bitburner.tailproperties.height.md)
## TailProperties.height property
Height of the log window content area
**Signature:**
```typescript
height: number;
```

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md)
## TailProperties interface
**Signature:**
```typescript
interface TailProperties
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [height](./bitburner.tailproperties.height.md) | | number | Height of the log window content area |
| [width](./bitburner.tailproperties.width.md) | | number | Width of the log window content area |
| [x](./bitburner.tailproperties.x.md) | | number | X-coordinate of the log window |
| [y](./bitburner.tailproperties.y.md) | | number | Y-coordinate of the log window |

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [width](./bitburner.tailproperties.width.md)
## TailProperties.width property
Width of the log window content area
**Signature:**
```typescript
width: number;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [x](./bitburner.tailproperties.x.md)
## TailProperties.x property
X-coordinate of the log window
**Signature:**
```typescript
x: number;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [y](./bitburner.tailproperties.y.md)
## TailProperties.y property
Y-coordinate of the log window
**Signature:**
```typescript
y: number;
```

@ -236,6 +236,7 @@ v2.3.1 dev
GENERAL / MISC:
* Tail window overhaul, ability to set tail title with ns.setTitle, other tail bugfixes and improvements. (@d0sboots)
* Nerf noodle bar
`,
};

@ -1,3 +1,4 @@
import React from "react";
import { NetscriptContext } from "./APIWrapper";
import { WorkerScript } from "./WorkerScript";
import { killWorkerScript } from "./killWorkerScript";
@ -38,6 +39,7 @@ import { isPositiveInteger, PositiveInteger, Unknownify } from "../types";
import { Engine } from "../engine";
import { resolveFilePath, FilePath } from "../Paths/FilePath";
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
import { CustomBoundary } from "../ui/Components/CustomBoundary";
export const helpers = {
string,
@ -736,6 +738,7 @@ function getCannotFindRunningScriptErrorMessage(ident: ScriptIdentifier): string
* @returns A sanitized, NS-facing copy of the RunningScript
*/
function createPublicRunningScript(runningScript: RunningScript): IRunningScript {
const logProps = runningScript.tailProps;
return {
args: runningScript.args.slice(),
filename: runningScript.filename,
@ -749,6 +752,16 @@ function createPublicRunningScript(runningScript: RunningScript): IRunningScript
pid: runningScript.pid,
ramUsage: runningScript.ramUsage,
server: runningScript.server,
tailProperties:
!logProps || !logProps.isVisible()
? null
: {
x: logProps.x,
y: logProps.y,
width: logProps.width,
height: logProps.height,
},
title: runningScript.title,
threads: runningScript.threads,
temporary: runningScript.temporary,
};
@ -800,3 +813,14 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | ScriptDeath |
}
dialogBoxCreate(initialText + e);
}
// Incrementing value for custom element keys
let customElementKey = 0;
/**
* Wrap a user-provided React Element (or maybe invalid junk) in an Error-catching component,
* so the game won't crash and the user gets sensible messages.
*/
export function wrapUserNode(value: unknown) {
return <CustomBoundary key={`PlayerContent${customElementKey++}`} children={value as React.ReactNode} />;
}

@ -540,6 +540,7 @@ export const RamCosts: RamCostTree<NSFull> = {
moveTail: 0,
resizeTail: 0,
closeTail: 0,
setTitle: 0,
clearPort: 0,
openDevMenu: 0,
alert: 0,

@ -37,7 +37,7 @@ import { runScriptFromScript } from "./NetscriptWorker";
import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScript } from "./Netscript/WorkerScript";
import { helpers, assertObjectType } from "./Netscript/NetscriptHelpers";
import { helpers, assertObjectType, wrapUserNode } from "./Netscript/NetscriptHelpers";
import {
formatExp,
formatNumberNoSuffix,
@ -49,7 +49,7 @@ import {
formatNumber,
} from "./ui/formatNumber";
import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions";
import { LogBoxEvents, LogBoxCloserEvents, LogBoxPositionEvents, LogBoxSizeEvents } from "./ui/React/LogBoxManager";
import { LogBoxEvents, LogBoxCloserEvents } from "./ui/React/LogBoxManager";
import { arrayToString } from "./utils/helpers/ArrayHelpers";
import { NetscriptGang } from "./NetscriptFunctions/Gang";
import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
@ -557,7 +557,12 @@ export const ns: InternalAPI<NSFull> = {
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
const pid = helpers.number(ctx, "pid", _pid);
LogBoxPositionEvents.emit({ pid, data: { x, y } });
const runningScriptObj = helpers.getRunningScript(ctx, pid);
if (runningScriptObj == null) {
helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(pid));
return;
}
runningScriptObj.tailProps?.setPosition(x, y);
},
resizeTail:
(ctx) =>
@ -565,7 +570,12 @@ export const ns: InternalAPI<NSFull> = {
const w = helpers.number(ctx, "w", _w);
const h = helpers.number(ctx, "h", _h);
const pid = helpers.number(ctx, "pid", _pid);
LogBoxSizeEvents.emit({ pid, data: { w, h } });
const runningScriptObj = helpers.getRunningScript(ctx, pid);
if (runningScriptObj == null) {
helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(pid));
return;
}
runningScriptObj.tailProps?.setSize(w, h);
},
closeTail:
(ctx) =>
@ -574,6 +584,18 @@ export const ns: InternalAPI<NSFull> = {
//Emit an event to tell the game to close the tail window if it exists
LogBoxCloserEvents.emit(pid);
},
setTitle:
(ctx) =>
(title, _pid = ctx.workerScript.scriptRef.pid) => {
const pid = helpers.number(ctx, "pid", _pid);
const runningScriptObj = helpers.getRunningScript(ctx, pid);
if (runningScriptObj == null) {
helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(pid));
return;
}
runningScriptObj.title = typeof title === "string" ? title : wrapUserNode(title);
runningScriptObj.tailProps?.rerender();
},
nuke: (ctx) => (_hostname) => {
const hostname = helpers.string(ctx, "hostname", _hostname);

@ -1,16 +1,11 @@
import React from "react";
import { Player } from "../Player";
import { Exploit } from "../Exploits/Exploit";
import * as bcrypt from "bcryptjs";
import { Apr1Events as devMenu } from "../ui/Apr1";
import { InternalAPI } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import { helpers, wrapUserNode } from "../Netscript/NetscriptHelpers";
import { Terminal } from "../Terminal";
import { RamCostConstants } from "../Netscript/RamCostGenerator";
import { CustomBoundary } from "../ui/Components/CustomBoundary";
// Incrementing value for custom element keys
let customElementKey = 0;
export interface INetscriptExtra {
heart: {
@ -72,14 +67,10 @@ export function NetscriptExtra(): InternalAPI<INetscriptExtra> {
return true;
},
tprintRaw: () => (value) => {
Terminal.printRaw(
<CustomBoundary key={`PlayerContent${customElementKey++}`} children={value as React.ReactNode} />,
);
Terminal.printRaw(wrapUserNode(value));
},
printRaw: (ctx) => (value) => {
ctx.workerScript.print(
<CustomBoundary key={`PlayerContent${customElementKey++}`} children={value as React.ReactNode} />,
);
ctx.workerScript.print(wrapUserNode(value));
},
};
}

@ -17,6 +17,8 @@ import { getKeyList } from "../utils/helpers/getKeyList";
import { ScriptFilePath } from "../Paths/ScriptFilePath";
import { ScriptKey, scriptKey } from "../utils/helpers/scriptKey";
import type { LogBoxProperties } from "../ui/React/LogBoxManager";
export class RunningScript {
// Script arguments
args: ScriptArg[] = [];
@ -65,6 +67,14 @@ export class RunningScript {
// Cached key for ByArgs lookups. Will be overwritten by a correct ScriptKey in fromJSON or constructor
scriptKey = "" as ScriptKey;
// Access to properties of the tail window. Can be used to get/set size, position, etc.
tailProps = null as LogBoxProperties | null;
// The title, as shown in the script's log box. Defaults to the name + args,
// but can be changed by the user. If it is set to a React element (only by the user),
// that will not be persisted, and will be restored to default on load.
title = "" as string | React.ReactElement;
// Number of threads that this script is running with
threads = 1 as PositiveInteger;
@ -83,6 +93,7 @@ export class RunningScript {
this.server = script.server;
this.ramUsage = ramUsage;
this.dependencies = script.dependencies;
this.title = `${this.filename} ${args.join(" ")}`;
}
log(txt: React.ReactNode): void {
@ -140,7 +151,12 @@ export class RunningScript {
// Serialize the current object to a JSON save state
toJSON(): IReviverValue {
return Generic_toJSON("RunningScript", this, includedProperties);
// Omit the title if it's a ReactNode, it will be filled in with the default on load.
return Generic_toJSON(
"RunningScript",
this,
typeof this.title === "string" ? includedProperties : includedPropsNoTitle,
);
}
// Initializes a RunningScript Object from a JSON save state
@ -150,6 +166,9 @@ export class RunningScript {
return runningScript;
}
}
const includedProperties = getKeyList(RunningScript, { removedKeys: ["logs", "dependencies", "logUpd", "pid"] });
const includedProperties = getKeyList(RunningScript, {
removedKeys: ["logs", "dependencies", "logUpd", "pid", "tailProps"],
});
const includedPropsNoTitle = includedProperties.filter((x) => x !== "title");
constructorsForReviver.RunningScript = RunningScript;

@ -169,6 +169,29 @@ interface Multipliers {
bladeburner_success_chance: number;
}
/** @public */
interface TailProperties {
/** X-coordinate of the log window */
x: number;
/** Y-coordinate of the log window */
y: number;
/** Width of the log window content area */
width: number;
/** Height of the log window content area */
height: number;
}
/**
* @public
* A stand-in for the real React.ReactElement, which API-extractor doesn't know about.
* Don't try to create one of these by hand; use React.createElement().
*/
interface ReactElement {
type: string | ((props: any) => ReactElement | null) | (new (props: any) => object);
props: any;
key: string | number | null;
}
/** @public */
interface RunningScript {
/** Arguments the script was called with */
@ -198,6 +221,15 @@ interface RunningScript {
ramUsage: number;
/** Hostname of the server on which this script runs */
server: string;
/** Properties of the tail window, or null if it is not shown */
tailProperties: TailProperties | null;
/**
* The title, as shown in the script's log box. Defaults to the name + args,
* but can be changed by the user. If it is set to a React element (only by
* the user), that will not be persisted, and will be restored to default on
* load.
*/
title: string | ReactElement;
/** Number of threads that this script runs with */
threads: number;
/** Whether this RunningScript is excluded from saves */
@ -5103,6 +5135,29 @@ export interface NS {
*/
closeTail(pid?: number): void;
/**
* Set the title of the tail window of a script.
* @remarks
* RAM cost: 0 GB
*
* This sets the title to the given string, and also forces an update of the
* tail window's contents.
*
* The title is saved across restarts, but only if it is a simple string.
*
* If the pid is unspecified, it will modify the current scripts logs.
*
* Otherwise, the pid argument can be used to change the logs from another script.
*
* It is possible to pass a React Element instead of a string. Get these by calling
* React.createElement() with appropriate parameters. You should either know
* or be willing to learn about the React UI library if you go down this
* route, and there will likely be rough edges.
*
* @param pid - Optional. PID of the script having its tail closed. If omitted, the current script is used.
*/
setTitle(title: string | ReactElement, pid?: number): void;
/**
* Get the list of servers connected to a server.
* @remarks

@ -4,13 +4,18 @@ import { RunningScript } from "../../Script/RunningScript";
import { killWorkerScriptByPid } 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 IconButton from "@mui/material/IconButton";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import CloseIcon from "@mui/icons-material/Close";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import StopCircleIcon from "@mui/icons-material/StopCircle";
import PlayCircleIcon from "@mui/icons-material/PlayCircle";
import { workerScripts } from "../../Netscript/WorkerScripts";
import { startWorkerScript } from "../../NetscriptWorker";
import { GetServer } from "../../Server/AllServers";
@ -18,7 +23,6 @@ import { findRunningScriptByPid } 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";
@ -28,27 +32,47 @@ export const LogBoxEvents = new EventEmitter<[RunningScript]>();
export const LogBoxCloserEvents = new EventEmitter<[number]>();
export const LogBoxClearEvents = new EventEmitter<[]>();
interface LogBoxUIEvent<T> {
pid: number;
data: T;
// Dynamic properties (size, position) bound to a specific rendered instance of a LogBox
export class LogBoxProperties {
x = window.innerWidth * 0.4;
y = window.innerHeight * 0.3;
width = 500;
height = 500;
rerender: () => void;
rootRef: React.RefObject<Draggable>;
constructor(rerender: () => void, rootRef: React.RefObject<Draggable>) {
this.rerender = rerender;
this.rootRef = rootRef;
}
interface LogBoxPositionData {
x: number;
y: number;
updateDOM(): void {
if (!this.rootRef.current) return;
const state = this.rootRef.current.state as { x: number; y: number };
state.x = this.x;
state.y = this.y;
}
export const LogBoxPositionEvents = new EventEmitter<[LogBoxUIEvent<LogBoxPositionData>]>();
interface LogBoxResizeData {
w: number;
h: number;
setPosition(x: number, y: number): void {
this.x = x;
this.y = y;
this.updateDOM();
}
export const LogBoxSizeEvents = new EventEmitter<[LogBoxUIEvent<LogBoxResizeData>]>();
setSize(width: number, height: number): void {
this.width = width;
this.height = height;
this.rerender();
}
isVisible(): boolean {
return this.rootRef.current !== null;
}
}
interface Log {
id: string;
id: number; // The PID of the script *when the window was first opened*
script: RunningScript;
}
@ -59,10 +83,9 @@ export function LogBoxManager(): React.ReactElement {
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;
if (logs.some((l) => l.script.pid === script.pid)) return;
logs.push({
id: id,
id: script.pid,
script: script,
});
rerender();
@ -87,21 +110,21 @@ export function LogBoxManager(): React.ReactElement {
);
//Close tail windows by their id
function close(id: string): void {
function close(id: number): void {
logs = logs.filter((l) => l.id !== id);
rerender();
}
//Close tail windows by their pid
//Close tail windows by their pid.
function closePid(pid: number): void {
logs = logs.filter((log) => log.script.pid != pid);
logs = logs.filter((log) => log.script.pid !== pid);
rerender();
}
return (
<>
{logs.map((log) => (
<LogWindow key={log.id} script={log.script} id={log.id} onClose={() => close(log.id)} />
<LogWindow key={log.id} script={log.script} onClose={() => close(log.id)} />
))}
</>
);
@ -109,7 +132,6 @@ export function LogBoxManager(): React.ReactElement {
interface IProps {
script: RunningScript;
id: string;
onClose: () => void;
}
@ -124,7 +146,11 @@ const useStyles = makeStyles(() =>
wordWrap: "break-word",
},
titleButton: {
padding: "1px 0",
borderWidth: "0 0 0 1px",
borderColor: Settings.theme.welllight,
borderStyle: "solid",
borderRadius: "0",
padding: "0",
height: "100%",
},
}),
@ -135,12 +161,13 @@ export const logBoxBaseZIndex = 1500;
function LogWindow(props: IProps): React.ReactElement {
const draggableRef = useRef<HTMLDivElement>(null);
const rootRef = useRef<Draggable>(null);
const [script, setScript] = useState(props.script);
const script = props.script;
const classes = useStyles();
const container = useRef<HTMLDivElement>(null);
const textArea = useRef<HTMLDivElement>(null);
const rerender = useRerender(1000);
const [size, setSize] = useState<[number, number]>([500, 500]);
const propsRef = useRef(new LogBoxProperties(rerender, rootRef));
script.tailProps = propsRef.current;
const [minimized, setMinimized] = useState(false);
const textAreaKeyDown = (e: React.KeyboardEvent) => {
@ -157,46 +184,17 @@ function LogWindow(props: IProps): React.ReactElement {
};
const onResize = (e: React.SyntheticEvent, { size }: ResizeCallbackData) => {
setSize([size.width, size.height]);
propsRef.current.setSize(size.width, size.height);
};
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(() => {
propsRef.current.updateDOM();
updateLayer();
}, []);
function kill(): void {
killWorkerScriptByPid(script.pid);
rerender();
}
function run(): void {
@ -214,10 +212,17 @@ function LogWindow(props: IProps): React.ReactElement {
if (!ramUsage) {
return dialogBoxCreate(`Could not calculate ram usage for ${script.filename} on ${server.hostname}.`);
}
// Reset some things, because we're reusing the RunningScript instance
script.ramUsage = ramUsage;
script.dataMap = {};
script.onlineExpGained = 0;
script.onlineMoneyMade = 0;
script.onlineRunningTime = 0.01;
startWorkerScript(script, server);
rerender();
} else {
setScript(s);
console.warn(`Tried to rerun pid ${script.pid} that was already running!`);
}
}
@ -229,13 +234,17 @@ function LogWindow(props: IProps): React.ReactElement {
rerender();
}
function title(full = false): string {
const maxLength = 30;
const t = `${script.filename} ${script.args.join(" ")}`;
if (full || t.length <= maxLength) {
return t;
}
return t.slice(0, maxLength - 3) + "...";
function title(): React.ReactElement {
const title_str = script.title === "string" ? script.title : `${script.filename} ${script.args.join(" ")}`;
return (
<Typography
variant="h6"
sx={{ marginRight: "auto", textOverflow: "ellipsis", whiteSpace: "nowrap", overflow: "hidden" }}
title={title_str}
>
{script.title}
</Typography>
);
}
function minimize(): void {
@ -271,7 +280,7 @@ function LogWindow(props: IProps): React.ReactElement {
if (!node) return;
if (!isOnScreen(node)) {
setPosition({ x: 0, y: 0 });
propsRef.current.setPosition(0, 0);
}
}, 100);
@ -290,7 +299,7 @@ function LogWindow(props: IProps): React.ReactElement {
};
// Max [width, height]
const minConstraints: [number, number] = [250, 33];
const minConstraints: [number, number] = [150, 33];
return (
<Draggable handle=".drag" onDrag={boundToBody} ref={rootRef} onMouseDown={updateLayer}>
@ -316,8 +325,8 @@ function LogWindow(props: IProps): React.ReactElement {
ref={container}
>
<ResizableBox
width={size[0]}
height={size[1]}
width={propsRef.current.width}
height={propsRef.current.height}
onResize={onResize}
minConstraints={minConstraints}
handle={
@ -336,30 +345,24 @@ function LogWindow(props: IProps): React.ReactElement {
>
<>
<Paper className="drag" sx={{ display: "flex", alignItems: "center", cursor: "grab" }} ref={draggableRef}>
<Typography
variant="h6"
sx={{ marginRight: "auto", textOverflow: "ellipsis", whiteSpace: "nowrap", overflow: "hidden" }}
title={title(true)}
>
{title(true)}
</Typography>
{title()}
<span style={{ minWidth: "fit-content", height: `${minConstraints[1]}px` }}>
{!workerScripts.has(script.pid) ? (
<Button className={classes.titleButton} onClick={run} onTouchEnd={run}>
Run
</Button>
<IconButton className={classes.titleButton} onClick={run} onTouchEnd={run}>
<PlayCircleIcon />
</IconButton>
) : (
<Button className={classes.titleButton} onClick={kill} onTouchEnd={kill}>
Kill
</Button>
<IconButton className={classes.titleButton} onClick={kill} onTouchEnd={kill}>
<StopCircleIcon color="error" />
</IconButton>
)}
<Button className={classes.titleButton} onClick={minimize} onTouchEnd={minimize}>
{minimized ? "\u{1F5D6}" : "\u{1F5D5}"}
</Button>
<Button className={classes.titleButton} onClick={props.onClose} onTouchEnd={props.onClose}>
Close
</Button>
<IconButton className={classes.titleButton} onClick={minimize} onTouchEnd={minimize}>
{minimized ? <ExpandMoreIcon /> : <ExpandLessIcon />}
</IconButton>
<IconButton className={classes.titleButton} onClick={props.onClose} onTouchEnd={props.onClose}>
<CloseIcon />
</IconButton>
</span>
</Paper>

@ -77,6 +77,7 @@ function loadStandardServers() {
"ramUsage": 1.6,
"server": "home",
"scriptKey": "script.js*[]",
"title": "Awesome Script",
"dependencies": [
{
"filename": "script.js",

@ -74,6 +74,7 @@ exports[`load/saveAllServers 1`] = `
"ramUsage": 1.6,
"server": "home",
"scriptKey": "script.js*[]",
"title": "Awesome Script",
"threads": 1,
"temporary": false
}