From 22648df8577941655ed3c056f71c2d1a04f0b530 Mon Sep 17 00:00:00 2001 From: Olivier Gagnon Date: Thu, 16 Sep 2021 15:37:01 -0400 Subject: [PATCH] refactor temrinal input for more performace --- src/Settings/Settings.ts | 7 + src/Terminal/Terminal.ts | 8 + src/Terminal/ui/TerminalInput.tsx | 362 ++++++++++++++++++++++++++++++ src/Terminal/ui/TerminalRoot.tsx | 340 +--------------------------- src/ui/React/GameOptionsRoot.tsx | 28 +++ 5 files changed, 412 insertions(+), 333 deletions(-) create mode 100644 src/Terminal/ui/TerminalInput.tsx diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index d464088c5..36a5ab924 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -45,6 +45,11 @@ interface IDefaultSettings { */ MaxPortCapacity: number; + /** + * Limit the number of entries in the terminal. + */ + MaxTerminalCapacity: number; + /** * Whether the player should be asked to confirm purchasing each and every augmentation. */ @@ -104,6 +109,7 @@ const defaultSettings: IDefaultSettings = { Locale: "en", MaxLogCapacity: 50, MaxPortCapacity: 50, + MaxTerminalCapacity: 200, SuppressBuyAugmentationConfirmation: false, SuppressFactionInvites: false, SuppressHospitalizationPopup: false, @@ -125,6 +131,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = { Locale: "en", MaxLogCapacity: defaultSettings.MaxLogCapacity, MaxPortCapacity: defaultSettings.MaxPortCapacity, + MaxTerminalCapacity: defaultSettings.MaxTerminalCapacity, OwnedAugmentationsOrder: OwnedAugmentationsOrderSetting.AcquirementTime, PurchaseAugmentationsOrder: PurchaseAugmentationsOrderSetting.Default, SuppressBuyAugmentationConfirmation: defaultSettings.SuppressBuyAugmentationConfirmation, diff --git a/src/Terminal/Terminal.ts b/src/Terminal/Terminal.ts index 510c6da1e..8eef71a66 100644 --- a/src/Terminal/Terminal.ts +++ b/src/Terminal/Terminal.ts @@ -23,6 +23,7 @@ import { TerminalHelpText } from "./HelpText"; import { GetServerByHostname, getServer, getServerOnNetwork } from "../Server/ServerHelpers"; import { ParseCommand, ParseCommands } from "./Parser"; import { SpecialServerIps, SpecialServerNames } from "../Server/SpecialServerIps"; +import { Settings } from "../Settings/Settings"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { calculateHackingChance, @@ -101,6 +102,13 @@ export class Terminal implements ITerminal { return false; } + append(item: Output | Link): void { + this.outputHistory.push(item); + if (this.outputHistory.length > Settings.MaxTerminalCapacity) { + this.outputHistory.slice(this.outputHistory.length - Settings.MaxTerminalCapacity); + } + } + print(s: string, config?: any): void { this.outputHistory.push(new Output(s, "primary")); this.hasChanges = true; diff --git a/src/Terminal/ui/TerminalInput.tsx b/src/Terminal/ui/TerminalInput.tsx new file mode 100644 index 000000000..e682da5d8 --- /dev/null +++ b/src/Terminal/ui/TerminalInput.tsx @@ -0,0 +1,362 @@ +import React, { useState, useEffect, useRef } from "react"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; +import TextField from "@material-ui/core/TextField"; +import { KEY } from "../../../utils/helpers/keyCodes"; +import { ITerminal } from "../ITerminal"; +import { IEngine } from "../../IEngine"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { determineAllPossibilitiesForTabCompletion } from "../determineAllPossibilitiesForTabCompletion"; +import { tabCompletion } from "../tabCompletion"; +import { FconfSettings } from "../../Fconf/FconfSettings"; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + textfield: { + margin: theme.spacing(0), + width: "100%", + }, + input: { + backgroundColor: "#000", + }, + nopadding: { + padding: theme.spacing(0), + }, + preformatted: { + whiteSpace: "pre-wrap", + margin: theme.spacing(0), + }, + list: { + padding: theme.spacing(0), + height: "100%", + }, + }), +); + +interface IProps { + terminal: ITerminal; + engine: IEngine; + player: IPlayer; +} + +export function TerminalInput({ terminal, engine, player }: IProps): React.ReactElement { + const terminalInput = useRef(null); + + const [value, setValue] = useState(""); + const [possibilities, setPossibilities] = useState([]); + const classes = useStyles(); + + function handleValueChange(event: React.ChangeEvent): void { + setValue(event.target.value); + setPossibilities([]); + } + + function modifyInput(mod: string): void { + const ref = terminalInput.current; + if (!ref) return; + const inputLength = value.length; + const start = ref.selectionStart; + if (start === null) return; + const inputText = ref.value; + + switch (mod.toLowerCase()) { + case "backspace": + if (start > 0 && start <= inputLength + 1) { + setValue(inputText.substr(0, start - 1) + inputText.substr(start)); + } + break; + case "deletewordbefore": // Delete rest of word before the cursor + for (let delStart = start - 1; delStart > 0; --delStart) { + if (inputText.charAt(delStart) === " ") { + setValue(inputText.substr(0, delStart) + inputText.substr(start)); + return; + } + } + break; + case "deletewordafter": // Delete rest of word after the cursor + for (let delStart = start + 1; delStart <= value.length + 1; ++delStart) { + if (inputText.charAt(delStart) === " ") { + setValue(inputText.substr(0, start) + inputText.substr(delStart)); + return; + } + } + break; + case "clearafter": // Deletes everything after cursor + break; + case "clearbefore:": // Deleetes everything before cursor + break; + } + } + + function moveTextCursor(loc: string): void { + const ref = terminalInput.current; + if (!ref) return; + const inputLength = value.length; + const start = ref.selectionStart; + if (start === null) return; + + switch (loc.toLowerCase()) { + case "home": + ref.setSelectionRange(0, 0); + break; + case "end": + ref.setSelectionRange(inputLength, inputLength); + break; + case "prevchar": + if (start > 0) { + ref.setSelectionRange(start - 1, start - 1); + } + break; + case "prevword": + for (let i = start - 2; i >= 0; --i) { + if (ref.value.charAt(i) === " ") { + ref.setSelectionRange(i + 1, i + 1); + return; + } + } + ref.setSelectionRange(0, 0); + break; + case "nextchar": + ref.setSelectionRange(start + 1, start + 1); + break; + case "nextword": + for (let i = start + 1; i <= inputLength; ++i) { + if (ref.value.charAt(i) === " ") { + ref.setSelectionRange(i, i); + return; + } + } + ref.setSelectionRange(inputLength, inputLength); + break; + default: + console.warn("Invalid loc argument in Terminal.moveTextCursor()"); + break; + } + } + + // Catch all key inputs and redirect them to the terminal. + useEffect(() => { + function keyDown(this: Document, event: KeyboardEvent): void { + if (terminal.contractOpen) return; + const ref = terminalInput.current; + if (ref) ref.focus(); + + // Cancel action + if (event.keyCode === KEY.C && event.ctrlKey) { + terminal.finishAction(player, true); + } + } + document.addEventListener("keydown", keyDown); + return () => document.removeEventListener("keydown", keyDown); + }); + + function onKeyDown(event: React.KeyboardEvent): void { + // Run command. + if (event.keyCode === KEY.ENTER && value !== "") { + event.preventDefault(); + terminal.print(`[${player.getCurrentServer().hostname} ~${terminal.cwd()}]> ${value}`); + terminal.executeCommands(engine, player, value); + setValue(""); + return; + } + + // Autocomplete + if (event.keyCode === KEY.TAB && value !== "") { + event.preventDefault(); + + let copy = value; + const semiColonIndex = copy.lastIndexOf(";"); + if (semiColonIndex !== -1) { + copy = copy.slice(semiColonIndex + 1); + } + + copy = copy.trim(); + copy = copy.replace(/\s\s+/g, " "); + + const commandArray = copy.split(" "); + let index = commandArray.length - 2; + if (index < -1) { + index = 0; + } + const allPos = determineAllPossibilitiesForTabCompletion(player, copy, index, terminal.cwd()); + if (allPos.length == 0) { + return; + } + + let arg = ""; + let command = ""; + if (commandArray.length == 0) { + return; + } + if (commandArray.length == 1) { + command = commandArray[0]; + } else if (commandArray.length == 2) { + command = commandArray[0]; + arg = commandArray[1]; + } else if (commandArray.length == 3) { + command = commandArray[0] + " " + commandArray[1]; + arg = commandArray[2]; + } else { + arg = commandArray.pop() + ""; + command = commandArray.join(" "); + } + + const newValue = tabCompletion(command, arg, allPos, value); + if (typeof newValue === "string" && newValue !== "") { + setValue(newValue); + } + if (Array.isArray(newValue)) { + setPossibilities(newValue); + } + } + + // Clear screen. + if (event.keyCode === KEY.L && event.ctrlKey) { + event.preventDefault(); + terminal.clear(); + } + + // Select previous command. + if ( + event.keyCode === KEY.UPARROW || + (FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.P && event.ctrlKey) + ) { + if (FconfSettings.ENABLE_BASH_HOTKEYS) { + event.preventDefault(); + } + const i = terminal.commandHistoryIndex; + const len = terminal.commandHistory.length; + + if (len == 0) { + return; + } + if (i < 0 || i > len) { + terminal.commandHistoryIndex = len; + } + + if (i != 0) { + --terminal.commandHistoryIndex; + } + const prevCommand = terminal.commandHistory[terminal.commandHistoryIndex]; + setValue(prevCommand); + const ref = terminalInput.current; + if (ref) { + setTimeout(function () { + ref.selectionStart = ref.selectionEnd = 10000; + }, 10); + } + } + + // Select next command + if ( + event.keyCode === KEY.DOWNARROW || + (FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.M && event.ctrlKey) + ) { + if (FconfSettings.ENABLE_BASH_HOTKEYS) { + event.preventDefault(); + } + const i = terminal.commandHistoryIndex; + const len = terminal.commandHistory.length; + + if (len == 0) { + return; + } + if (i < 0 || i > len) { + terminal.commandHistoryIndex = len; + } + + // Latest command, put nothing + if (i == len || i == len - 1) { + terminal.commandHistoryIndex = len; + setValue(""); + } else { + ++terminal.commandHistoryIndex; + const prevCommand = terminal.commandHistory[terminal.commandHistoryIndex]; + setValue(prevCommand); + } + } + + // Extra Bash Emulation Hotkeys, must be enabled through .fconf + if (FconfSettings.ENABLE_BASH_HOTKEYS) { + if (event.keyCode === KEY.A && event.ctrlKey) { + event.preventDefault(); + moveTextCursor("home"); + } + + if (event.keyCode === KEY.E && event.ctrlKey) { + event.preventDefault(); + moveTextCursor("end"); + } + + if (event.keyCode === KEY.B && event.ctrlKey) { + event.preventDefault(); + moveTextCursor("prevchar"); + } + + if (event.keyCode === KEY.B && event.altKey) { + event.preventDefault(); + moveTextCursor("prevword"); + } + + if (event.keyCode === KEY.F && event.ctrlKey) { + event.preventDefault(); + moveTextCursor("nextchar"); + } + + if (event.keyCode === KEY.F && event.altKey) { + event.preventDefault(); + moveTextCursor("nextword"); + } + + if ((event.keyCode === KEY.H || event.keyCode === KEY.D) && event.ctrlKey) { + modifyInput("backspace"); + event.preventDefault(); + } + + // TODO AFTER THIS: + // alt + d deletes word after cursor + // ^w deletes word before cursor + // ^k clears line after cursor + // ^u clears line before cursor + } + } + + return ( + <> + {possibilities.length > 0 && ( + <> + + Possible autocomplete candidate: + + + {possibilities.join(" ")} + + + )} + + + [{player.getCurrentServer().hostname} ~{terminal.cwd()}]>  + + + ), + spellCheck: false, + onKeyDown: onKeyDown, + }} + > + + ); +} diff --git a/src/Terminal/ui/TerminalRoot.tsx b/src/Terminal/ui/TerminalRoot.tsx index 925963c6f..bc5c5ea8d 100644 --- a/src/Terminal/ui/TerminalRoot.tsx +++ b/src/Terminal/ui/TerminalRoot.tsx @@ -1,19 +1,14 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect } from "react"; import Typography from "@material-ui/core/Typography"; import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import { Link as MuiLink } from "@material-ui/core"; import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; -import TextField from "@material-ui/core/TextField"; import Box from "@material-ui/core/Box"; -import { KEY } from "../../../utils/helpers/keyCodes"; -import { ITerminal, Output, Link, TTimer } from "../ITerminal"; +import { ITerminal, Output, Link } from "../ITerminal"; import { IEngine } from "../../IEngine"; import { IPlayer } from "../../PersonObjects/IPlayer"; -import { determineAllPossibilitiesForTabCompletion } from "../determineAllPossibilitiesForTabCompletion"; -import { tabCompletion } from "../tabCompletion"; -import { FconfSettings } from "../../Fconf/FconfSettings"; -import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; +import { TerminalInput } from "./TerminalInput"; interface IActionTimerProps { terminal: ITerminal; @@ -29,24 +24,16 @@ function ActionTimer({ terminal }: IActionTimerProps): React.ReactElement { const useStyles = makeStyles((theme: Theme) => createStyles({ - textfield: { - margin: 0, - width: "100%", - }, - input: { - backgroundColor: "#000", - }, nopadding: { - padding: 0, + padding: theme.spacing(0), }, preformatted: { whiteSpace: "pre-wrap", - margin: 0, + margin: theme.spacing(0), }, list: { - padding: 0, + padding: theme.spacing(0), height: "100%", - overflowY: "scroll", }, }), ); @@ -58,7 +45,6 @@ interface IProps { } export function TerminalRoot({ terminal, engine, player }: IProps): React.ReactElement { - const terminalInput = useRef(null); const setRerender = useState(false)[1]; function rerender(): void { setRerender((old) => !old); @@ -71,286 +57,7 @@ export function TerminalRoot({ terminal, engine, player }: IProps): React.ReactE return () => clearInterval(id); }, []); - const [value, setValue] = useState(""); - const [possibilities, setPossibilities] = useState([]); const classes = useStyles(); - - function handleValueChange(event: React.ChangeEvent): void { - setValue(event.target.value); - setPossibilities([]); - } - - function modifyInput(mod: string): void { - const ref = terminalInput.current; - if (!ref) return; - const inputLength = value.length; - const start = ref.selectionStart; - if (start === null) return; - const inputText = ref.value; - - switch (mod.toLowerCase()) { - case "backspace": - if (start > 0 && start <= inputLength + 1) { - setValue(inputText.substr(0, start - 1) + inputText.substr(start)); - } - break; - case "deletewordbefore": // Delete rest of word before the cursor - for (var delStart = start - 1; delStart > 0; --delStart) { - if (inputText.charAt(delStart) === " ") { - setValue(inputText.substr(0, delStart) + inputText.substr(start)); - return; - } - } - break; - case "deletewordafter": // Delete rest of word after the cursor - for (var delStart = start + 1; delStart <= value.length + 1; ++delStart) { - if (inputText.charAt(delStart) === " ") { - setValue(inputText.substr(0, start) + inputText.substr(delStart)); - return; - } - } - break; - case "clearafter": // Deletes everything after cursor - break; - case "clearbefore:": // Deleetes everything before cursor - break; - } - } - - function moveTextCursor(loc: string) { - const ref = terminalInput.current; - if (!ref) return; - const inputLength = value.length; - const start = ref.selectionStart; - if (start === null) return; - - switch (loc.toLowerCase()) { - case "home": - ref.setSelectionRange(0, 0); - break; - case "end": - ref.setSelectionRange(inputLength, inputLength); - break; - case "prevchar": - if (start > 0) { - ref.setSelectionRange(start - 1, start - 1); - } - break; - case "prevword": - for (let i = start - 2; i >= 0; --i) { - if (ref.value.charAt(i) === " ") { - ref.setSelectionRange(i + 1, i + 1); - return; - } - } - ref.setSelectionRange(0, 0); - break; - case "nextchar": - ref.setSelectionRange(start + 1, start + 1); - break; - case "nextword": - for (let i = start + 1; i <= inputLength; ++i) { - if (ref.value.charAt(i) === " ") { - ref.setSelectionRange(i, i); - return; - } - } - ref.setSelectionRange(inputLength, inputLength); - break; - default: - console.warn("Invalid loc argument in Terminal.moveTextCursor()"); - break; - } - } - - // Catch all key inputs and redirect them to the terminal. - useEffect(() => { - function keyDown(this: Document, event: KeyboardEvent): void { - if (terminal.contractOpen) return; - const ref = terminalInput.current; - if (ref) ref.focus(); - - // Cancel action - if (event.keyCode === KEY.C && event.ctrlKey) { - terminal.finishAction(player, true); - } - } - document.addEventListener("keydown", keyDown); - return () => document.removeEventListener("keydown", keyDown); - }); - - function onKeyDown(event: React.KeyboardEvent): void { - // Run command. - if (event.keyCode === KEY.ENTER && value !== "") { - event.preventDefault(); - terminal.print(`[${player.getCurrentServer().hostname} ~${terminal.cwd()}]> ${value}`); - terminal.executeCommands(engine, player, value); - setValue(""); - return; - } - - // Autocomplete - if (event.keyCode === KEY.TAB && value !== "") { - event.preventDefault(); - - let copy = value; - const semiColonIndex = copy.lastIndexOf(";"); - if (semiColonIndex !== -1) { - copy = copy.slice(semiColonIndex + 1); - } - - copy = copy.trim(); - copy = copy.replace(/\s\s+/g, " "); - - const commandArray = copy.split(" "); - let index = commandArray.length - 2; - if (index < -1) { - index = 0; - } - const allPos = determineAllPossibilitiesForTabCompletion(player, copy, index, terminal.cwd()); - if (allPos.length == 0) { - return; - } - - let arg = ""; - let command = ""; - if (commandArray.length == 0) { - return; - } - if (commandArray.length == 1) { - command = commandArray[0]; - } else if (commandArray.length == 2) { - command = commandArray[0]; - arg = commandArray[1]; - } else if (commandArray.length == 3) { - command = commandArray[0] + " " + commandArray[1]; - arg = commandArray[2]; - } else { - arg = commandArray.pop() + ""; - command = commandArray.join(" "); - } - - const newValue = tabCompletion(command, arg, allPos, value); - if (typeof newValue === "string" && newValue !== "") { - setValue(newValue); - } - if (Array.isArray(newValue)) { - setPossibilities(newValue); - } - } - - // Clear screen. - if (event.keyCode === KEY.L && event.ctrlKey) { - event.preventDefault(); - terminal.clear(); - rerender(); - } - - // Select previous command. - if ( - event.keyCode === KEY.UPARROW || - (FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.P && event.ctrlKey) - ) { - if (FconfSettings.ENABLE_BASH_HOTKEYS) { - event.preventDefault(); - } - const i = terminal.commandHistoryIndex; - const len = terminal.commandHistory.length; - - if (len == 0) { - return; - } - if (i < 0 || i > len) { - terminal.commandHistoryIndex = len; - } - - if (i != 0) { - --terminal.commandHistoryIndex; - } - const prevCommand = terminal.commandHistory[terminal.commandHistoryIndex]; - setValue(prevCommand); - const ref = terminalInput.current; - if (ref) { - setTimeout(function () { - ref.selectionStart = ref.selectionEnd = 10000; - }, 10); - } - } - - // Select next command - if ( - event.keyCode === KEY.DOWNARROW || - (FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.M && event.ctrlKey) - ) { - if (FconfSettings.ENABLE_BASH_HOTKEYS) { - event.preventDefault(); - } - const i = terminal.commandHistoryIndex; - const len = terminal.commandHistory.length; - - if (len == 0) { - return; - } - if (i < 0 || i > len) { - terminal.commandHistoryIndex = len; - } - - // Latest command, put nothing - if (i == len || i == len - 1) { - terminal.commandHistoryIndex = len; - setValue(""); - } else { - ++terminal.commandHistoryIndex; - const prevCommand = terminal.commandHistory[terminal.commandHistoryIndex]; - setValue(prevCommand); - } - } - - // Extra Bash Emulation Hotkeys, must be enabled through .fconf - if (FconfSettings.ENABLE_BASH_HOTKEYS) { - if (event.keyCode === KEY.A && event.ctrlKey) { - event.preventDefault(); - moveTextCursor("home"); - } - - if (event.keyCode === KEY.E && event.ctrlKey) { - event.preventDefault(); - moveTextCursor("end"); - } - - if (event.keyCode === KEY.B && event.ctrlKey) { - event.preventDefault(); - moveTextCursor("prevchar"); - } - - if (event.keyCode === KEY.B && event.altKey) { - event.preventDefault(); - moveTextCursor("prevword"); - } - - if (event.keyCode === KEY.F && event.ctrlKey) { - event.preventDefault(); - moveTextCursor("nextchar"); - } - - if (event.keyCode === KEY.F && event.altKey) { - event.preventDefault(); - moveTextCursor("nextword"); - } - - if ((event.keyCode === KEY.H || event.keyCode === KEY.D) && event.ctrlKey) { - modifyInput("backspace"); - event.preventDefault(); - } - - // TODO AFTER THIS: - // alt + d deletes word after cursor - // ^w deletes word before cursor - // ^k clears line after cursor - // ^u clears line before cursor - } - } - return ( @@ -378,41 +85,8 @@ export function TerminalRoot({ terminal, engine, player }: IProps): React.ReactE ); })} - {possibilities.length > 0 && ( - <> - - Possible autocomplete candidate: - - - {possibilities.join(" ")} - - - )} {terminal.action !== null && } - - - [{player.getCurrentServer().hostname} ~{terminal.cwd()}]>  - - - ), - spellCheck: false, - onKeyDown: onKeyDown, - }} - > + ); } diff --git a/src/ui/React/GameOptionsRoot.tsx b/src/ui/React/GameOptionsRoot.tsx index 70c8a7f06..bc7727b7a 100644 --- a/src/ui/React/GameOptionsRoot.tsx +++ b/src/ui/React/GameOptionsRoot.tsx @@ -50,6 +50,8 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { const [execTime, setExecTime] = useState(Settings.CodeInstructionRunTime); const [logSize, setLogSize] = useState(Settings.MaxLogCapacity); const [portSize, setPortSize] = useState(Settings.MaxPortCapacity); + const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity); + const [autosaveInterval, setAutosaveInterval] = useState(Settings.AutosaveInterval); const [suppressMessages, setSuppressMessages] = useState(Settings.SuppressMessages); @@ -86,6 +88,11 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { Settings.MaxPortCapacity = newValue as number; } + function handleTerminalSizeChange(event: any, newValue: number | number[]): void { + setTerminalSize(newValue as number); + Settings.MaxTerminalCapacity = newValue as number; + } + function handleAutosaveIntervalChange(event: any, newValue: number | number[]): void { setAutosaveInterval(newValue as number); Settings.AutosaveInterval = newValue as number; @@ -209,6 +216,27 @@ export function GameOptionsRoot(props: IProps): React.ReactElement { valueLabelDisplay="auto" /> + + + The maximum number of entries that can be written to a the terminal. Setting this too high can cause + the game to use a lot of memory. + + } + > + Terminal capacity + + +