mostly convert terminal to react

This commit is contained in:
Olivier Gagnon
2021-09-16 02:52:45 -04:00
parent aba97d2baa
commit 05718e00ea
18 changed files with 475 additions and 511 deletions

View File

@ -5,15 +5,17 @@
terminal which has its own page) */
.generic-menupage-container {
padding-left: 2px;
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */
flex-grow: 1;
height: 100vh;
bottom: 0px;
}
#generic-react-container {
padding: 10px;
overflow-y: scroll;
height: 100vh;
bottom: 0px;
}
#generic-react-container::-webkit-scrollbar {

View File

@ -1,5 +1,5 @@
import { IMap } from "./types";
import { post } from "./ui/postToTerminal";
import { Terminal } from "./Terminal";
export let Aliases: IMap<string> = {};
export let GlobalAliases: IMap<string> = {};
@ -24,12 +24,12 @@ export function loadGlobalAliases(saveString: string): void {
export function printAliases(): void {
for (const name in Aliases) {
if (Aliases.hasOwnProperty(name)) {
post("alias " + name + "=" + Aliases[name]);
Terminal.print("alias " + name + "=" + Aliases[name]);
}
}
for (const name in GlobalAliases) {
if (GlobalAliases.hasOwnProperty(name)) {
post("global alias " + name + "=" + GlobalAliases[name]);
Terminal.print("global alias " + name + "=" + GlobalAliases[name]);
}
}
}

View File

@ -2,8 +2,9 @@ import * as React from "react";
import { DarkWebItems } from "./DarkWebItems";
import { Player } from "../Player";
import { Terminal } from "../Terminal";
import { SpecialServerIps } from "../Server/SpecialServerIps";
import { post, postElement } from "../ui/postToTerminal";
import { numeralWrapper } from "../ui/numeralFormat";
import { Money } from "../ui/React/Money";
import { isValidIPAddress } from "../../utils/helpers/isValidIPAddress";
@ -16,7 +17,7 @@ export function checkIfConnectedToDarkweb(): void {
return;
}
if (darkwebIp == Player.getCurrentServer().ip) {
post(
Terminal.print(
"You are now connected to the dark web. From the dark web you can purchase illegal items. " +
"Use the 'buy -l' command to display a list of all the items you can buy. Use 'buy [item-name] " +
"to purchase an item.",
@ -35,9 +36,9 @@ export function executeDarkwebTerminalCommand(commandArray: string[]): void {
switch (commandArray[0]) {
case "buy": {
if (commandArray.length != 2) {
post("Incorrect number of arguments. Usage: ");
post("buy -l");
post("buy [item name]");
Terminal.error("Incorrect number of arguments. Usage: ");
Terminal.print("buy -l");
Terminal.print("buy [item name]");
return;
}
const arg = commandArray[1];
@ -49,7 +50,7 @@ export function executeDarkwebTerminalCommand(commandArray: string[]): void {
break;
}
default:
post("Command not found");
Terminal.error("Command not found");
break;
}
}
@ -57,11 +58,7 @@ export function executeDarkwebTerminalCommand(commandArray: string[]): void {
export function listAllDarkwebItems(): void {
for (const key in DarkWebItems) {
const item = DarkWebItems[key];
postElement(
<>
{item.program} - <Money money={item.price} player={Player} /> - {item.description}
</>,
);
Terminal.print(`${item.program} - ${numeralWrapper.formatMoney(item.price)} - ${item.description}`);
}
}
@ -79,24 +76,26 @@ export function buyDarkwebItem(itemName: string): void {
// return if invalid
if (item === null) {
post("Unrecognized item: " + itemName);
Terminal.print("Unrecognized item: " + itemName);
return;
}
// return if the player already has it.
if (Player.hasProgram(item.program)) {
post("You already have the " + item.program + " program");
Terminal.print("You already have the " + item.program + " program");
return;
}
// return if the player doesn't have enough money
if (Player.money.lt(item.price)) {
post("Not enough money to purchase " + item.program);
Terminal.print("Not enough money to purchase " + item.program);
return;
}
// buy and push
Player.loseMoney(item.price);
Player.getHomeComputer().programs.push(item.program);
post("You have purchased the " + item.program + " program. The new program can be found on your home computer.");
Terminal.print(
"You have purchased the " + item.program + " program. The new program can be found on your home computer.",
);
}

View File

@ -1590,7 +1590,7 @@ HackingMission.prototype.finishMission = function (win) {
Mission won! You earned {Reputation(gain)} reputation with {this.faction.name}
</>,
);
Player.gainIntelligenceExp(this.difficulty * CONSTANTS.IntelligenceHackingMissionBaseExpGain);
Player.gainIntelligenceExp(Math.pow(this.difficulty * CONSTANTS.IntelligenceHackingMissionBaseExpGain, 0.5));
this.faction.playerReputation += gain;
} else {
dialogBoxCreate("Mission lost/forfeited! You did not gain any faction reputation.");

View File

@ -145,7 +145,6 @@ import { findSleevePurchasableAugs } from "./PersonObjects/Sleeve/SleeveHelpers"
import { Exploit } from "./Exploits/Exploit.ts";
import { numeralWrapper } from "./ui/numeralFormat";
import { post } from "./ui/postToTerminal";
import { setTimeoutRef } from "./utils/SetTimeoutRef";
import { is2DArray } from "./utils/helpers/is2DArray";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
@ -1059,10 +1058,10 @@ function NetscriptFunctions(workerScript) {
if (arguments.length === 0) {
throw makeRuntimeErrorMsg("tprint", "Takes at least 1 argument.");
}
post(`${workerScript.scriptRef.filename}: ${argsToString(arguments)}`);
Terminal.print(`${workerScript.scriptRef.filename}: ${argsToString(arguments)}`);
},
tprintf: function (format, ...args) {
post(vsprintf(format, args));
Terminal.print(vsprintf(format, args));
},
clearLog: function () {
workerScript.scriptRef.clearLog();
@ -3134,8 +3133,7 @@ function NetscriptFunctions(workerScript) {
Player.getCurrentServer().isConnectedTo = false;
Player.currentServer = Player.getHomeComputer().ip;
Player.getCurrentServer().isConnectedTo = true;
Terminal.currDir = "/";
Terminal.resetTerminalInput(true);
Terminal.setcwd("/");
return true;
}
@ -3146,8 +3144,7 @@ function NetscriptFunctions(workerScript) {
Player.getCurrentServer().isConnectedTo = false;
Player.currentServer = target.ip;
Player.getCurrentServer().isConnectedTo = true;
Terminal.currDir = "/";
Terminal.resetTerminalInput(true);
Terminal.setcwd("/");
return true;
}
}

View File

@ -23,7 +23,7 @@ import { prestigeHomeComputer } from "./Server/ServerHelpers";
import { SourceFileFlags, updateSourceFileFlags } from "./SourceFile/SourceFileFlags";
import { SpecialServerIps, prestigeSpecialServerIps, SpecialServerNames } from "./Server/SpecialServerIps";
import { deleteStockMarket, initStockMarket, initSymbolToStockMap } from "./StockMarket/StockMarket";
import { Terminal, postVersion } from "./Terminal";
import { Terminal } from "./Terminal";
import { Page, routing } from "./ui/navigationTracking";
@ -65,13 +65,8 @@ function prestigeAugmentation() {
Player.factions = Player.factions.concat(maintainMembership);
// Now actually go to the Terminal Screen (and reset it)
var mainMenu = document.getElementById("mainmenu-container");
mainMenu.style.visibility = "visible";
Terminal.resetTerminalInput();
Terminal.clear();
Engine.loadTerminalContent();
$("#terminal tr:not(:last)").remove();
postVersion();
// Delete all Worker Scripts objects
prestigeWorkerScripts();

View File

@ -6,7 +6,7 @@ import { Script } from "./Script";
import { FconfSettings } from "../Fconf/FconfSettings";
import { Settings } from "../Settings/Settings";
import { IMap } from "../types";
import { post } from "../ui/postToTerminal";
import { Terminal } from "../Terminal";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver";
import { getTimestamp } from "../../utils/helpers/getTimestamp";
@ -85,7 +85,7 @@ export class RunningScript {
displayLog(): void {
for (let i = 0; i < this.logs.length; ++i) {
post(this.logs[i]);
Terminal.print(this.logs[i]);
}
}

2
src/Terminal.d.ts vendored
View File

@ -1 +1 @@
export declare const Terminal: any;
export declare const Terminal: ITerminal;

View File

@ -9,235 +9,25 @@ import { setTimeoutRef } from "./utils/SetTimeoutRef";
import { Page, routing } from "./ui/navigationTracking";
import { KEY } from "../utils/helpers/keyCodes";
import { getTimestamp } from "../utils/helpers/getTimestamp";
import { post } from "./ui/postToTerminal";
import { Terminal as TTerminal } from "./Terminal/Terminal";
const NewTerminal = new TTerminal();
import autosize from "autosize";
function postVersion() {
post("Bitburner v" + CONSTANTS.Version);
}
function getTerminalInput() {
return document.getElementById("terminal-input-text-box").value;
}
const Terminal = new TTerminal();
// Defines key commands in terminal
$(document).keydown(function (event) {
// Terminal
if (routing.isOn(Page.Terminal)) {
var terminalInput = document.getElementById("terminal-input-text-box");
if (terminalInput != null && !event.ctrlKey && !event.shiftKey && !NewTerminal.contractOpen) {
terminalInput.focus();
if (event.keyCode === KEY.C && event.ctrlKey) {
if (Engine._actionInProgress) {
// Cancel action
// post("Cancelling...");
Engine._actionInProgress = false;
Terminal.finishAction(true);
} else if (FconfSettings.ENABLE_BASH_HOTKEYS) {
// Dont prevent default so it still copies
Terminal.clear(); // Clear Terminal
}
if (event.keyCode === KEY.ENTER) {
event.preventDefault(); // Prevent newline from being entered in Script Editor
const command = getTerminalInput();
const dir = NewTerminal.currDir;
post(
"<span class='prompt'>[" +
(FconfSettings.ENABLE_TIMESTAMPS ? getTimestamp() + " " : "") +
Player.getCurrentServer().hostname +
` ~${dir}]&gt;</span> ${command}`,
);
if (command.length > 0) {
Terminal.resetTerminalInput(); // Clear input first
NewTerminal.executeCommands(Engine, Player, command);
}
}
if (event.keyCode === KEY.C && event.ctrlKey) {
if (Engine._actionInProgress) {
// Cancel action
post("Cancelling...");
Engine._actionInProgress = false;
NewTerminal.finishAction(true);
} else if (FconfSettings.ENABLE_BASH_HOTKEYS) {
// Dont prevent default so it still copies
Terminal.resetTerminalInput(); // Clear Terminal
}
}
if (event.keyCode === KEY.L && event.ctrlKey) {
event.preventDefault();
NewTerminal.executeCommands(Engine, Player, "clear"); // Clear screen
}
// Ctrl p same as up arrow
// Ctrl n same as down arrow
if (
event.keyCode === KEY.UPARROW ||
(FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.P && event.ctrlKey)
) {
if (FconfSettings.ENABLE_BASH_HOTKEYS) {
event.preventDefault();
}
// Cycle through past commands
if (terminalInput == null) {
return;
}
var i = NewTerminal.commandHistoryIndex;
var len = NewTerminal.commandHistory.length;
if (len == 0) {
return;
}
if (i < 0 || i > len) {
NewTerminal.commandHistoryIndex = len;
}
if (i != 0) {
--NewTerminal.commandHistoryIndex;
}
var prevCommand = NewTerminal.commandHistory[NewTerminal.commandHistoryIndex];
terminalInput.value = prevCommand;
setTimeoutRef(function () {
terminalInput.selectionStart = terminalInput.selectionEnd = 10000;
}, 10);
}
if (
event.keyCode === KEY.DOWNARROW ||
(FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.M && event.ctrlKey)
) {
if (FconfSettings.ENABLE_BASH_HOTKEYS) {
event.preventDefault();
}
// Cycle through past commands
if (terminalInput == null) {
return;
}
var i = NewTerminal.commandHistoryIndex;
var len = NewTerminal.commandHistory.length;
if (len == 0) {
return;
}
if (i < 0 || i > len) {
NewTerminal.commandHistoryIndex = len;
}
// Latest command, put nothing
if (i == len || i == len - 1) {
NewTerminal.commandHistoryIndex = len;
terminalInput.value = "";
} else {
++NewTerminal.commandHistoryIndex;
var prevCommand = NewTerminal.commandHistory[NewTerminal.commandHistoryIndex];
terminalInput.value = prevCommand;
}
}
if (event.keyCode === KEY.TAB) {
event.preventDefault();
// Autocomplete
if (terminalInput == null) {
return;
}
let input = terminalInput.value;
if (input == "") {
return;
}
const semiColonIndex = input.lastIndexOf(";");
if (semiColonIndex !== -1) {
input = input.slice(semiColonIndex + 1);
}
input = input.trim();
input = input.replace(/\s\s+/g, " ");
const commandArray = input.split(" ");
let index = commandArray.length - 2;
if (index < -1) {
index = 0;
}
const allPos = determineAllPossibilitiesForTabCompletion(Player, input, index, NewTerminal.currDir);
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(" ");
}
tabCompletion(command, arg, allPos);
terminalInput.focus();
}
// Extra Bash Emulation Hotkeys, must be enabled through .fconf
if (FconfSettings.ENABLE_BASH_HOTKEYS) {
if (event.keyCode === KEY.A && event.ctrlKey) {
event.preventDefault();
Terminal.moveTextCursor("home");
}
if (event.keyCode === KEY.E && event.ctrlKey) {
event.preventDefault();
Terminal.moveTextCursor("end");
}
if (event.keyCode === KEY.B && event.ctrlKey) {
event.preventDefault();
Terminal.moveTextCursor("prevchar");
}
if (event.keyCode === KEY.B && event.altKey) {
event.preventDefault();
Terminal.moveTextCursor("prevword");
}
if (event.keyCode === KEY.F && event.ctrlKey) {
event.preventDefault();
Terminal.moveTextCursor("nextchar");
}
if (event.keyCode === KEY.F && event.altKey) {
event.preventDefault();
Terminal.moveTextCursor("nextword");
}
if ((event.keyCode === KEY.H || event.keyCode === KEY.D) && event.ctrlKey) {
Terminal.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
}
}
});
// Keep terminal in focus
let terminalCtrlPressed = false,
shiftKeyPressed = false;
$(document).ready(function () {
if (routing.isOn(Page.Terminal)) {
$(".terminal-input").focus();
}
});
@ -247,7 +37,7 @@ $(document).keydown(function (e) {
terminalCtrlPressed = true;
} else if (e.shiftKey) {
shiftKeyPressed = true;
} else if (terminalCtrlPressed || shiftKeyPressed || NewTerminal.contractOpen) {
} else if (terminalCtrlPressed || shiftKeyPressed || Terminal.contractOpen) {
// Don't focus
} else {
var inputTextBox = document.getElementById("terminal-input-text-box");
@ -261,146 +51,4 @@ $(document).keydown(function (e) {
}
});
$(document).keyup(function (e) {
if (routing.isOn(Page.Terminal)) {
if (e.which == KEY.CTRL) {
terminalCtrlPressed = false;
}
if (e.shiftKey) {
shiftKeyPressed = false;
}
}
});
let Terminal = {
resetTerminalInput: function (keepInput = false) {
let input = "";
if (keepInput) {
input = getTerminalInput();
}
const dir = NewTerminal.currDir;
if (FconfSettings.WRAP_INPUT) {
document.getElementById("terminal-input-td").innerHTML =
`<div id='terminal-input-header' class='prompt'>[${Player.getCurrentServer().hostname} ~${dir}]$ </div>` +
`<textarea type="text" id="terminal-input-text-box" class="terminal-input" tabindex="1" value=\"${input}\" autocomplete="off" />`;
// Auto re-size the line element as it wraps
autosize(document.getElementById("terminal-input-text-box"));
} else {
document.getElementById("terminal-input-td").innerHTML =
`<div id='terminal-input-header' class='prompt'>[${Player.getCurrentServer().hostname} ~${dir}]$ </div>` +
`<input type="text" id="terminal-input-text-box" class="terminal-input" tabindex="1" value=\"${input}\" autocomplete="off" />`;
}
const hdr = document.getElementById("terminal-input-header");
hdr.style.display = "inline";
const terminalInput = document.getElementById("terminal-input-text-box");
if (typeof terminalInput.selectionStart == "number") {
terminalInput.selectionStart = terminalInput.selectionEnd = terminalInput.value.length;
} else if (typeof terminalInput.createTextRange != "undefined") {
terminalInput.focus();
var range = el.createTextRange();
range.collapse(false);
range.select();
}
},
modifyInput: function (mod) {
try {
var terminalInput = document.getElementById("terminal-input-text-box");
if (terminalInput == null) {
return;
}
terminalInput.focus();
var inputLength = terminalInput.value.length;
var start = terminalInput.selectionStart;
var inputText = terminalInput.value;
switch (mod.toLowerCase()) {
case "backspace":
if (start > 0 && start <= inputLength + 1) {
terminalInput.value = 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) === " ") {
terminalInput.value = inputText.substr(0, delStart) + inputText.substr(start);
return;
}
}
break;
case "deletewordafter": // Delete rest of word after the cursor
for (var delStart = start + 1; delStart <= text.length + 1; ++delStart) {
if (inputText.charAt(delStart) === " ") {
terminalInput.value = inputText.substr(0, start) + inputText.substr(delStart);
return;
}
}
break;
case "clearafter": // Deletes everything after cursor
break;
case "clearbefore:": // Deleetes everything before cursor
break;
}
} catch (e) {
console.error("Exception in Terminal.modifyInput: " + e);
}
},
moveTextCursor: function (loc) {
try {
var terminalInput = document.getElementById("terminal-input-text-box");
if (terminalInput == null) {
return;
}
terminalInput.focus();
var inputLength = terminalInput.value.length;
var start = terminalInput.selectionStart;
switch (loc.toLowerCase()) {
case "home":
terminalInput.setSelectionRange(0, 0);
break;
case "end":
terminalInput.setSelectionRange(inputLength, inputLength);
break;
case "prevchar":
if (start > 0) {
terminalInput.setSelectionRange(start - 1, start - 1);
}
break;
case "prevword":
for (var i = start - 2; i >= 0; --i) {
if (terminalInput.value.charAt(i) === " ") {
terminalInput.setSelectionRange(i + 1, i + 1);
return;
}
}
terminalInput.setSelectionRange(0, 0);
break;
case "nextchar":
terminalInput.setSelectionRange(start + 1, start + 1);
break;
case "nextword":
for (var i = start + 1; i <= inputLength; ++i) {
if (terminalInput.value.charAt(i) === " ") {
terminalInput.setSelectionRange(i, i);
return;
}
}
terminalInput.setSelectionRange(inputLength, inputLength);
break;
default:
console.warn("Invalid loc argument in Terminal.moveTextCursor()");
break;
}
} catch (e) {
console.error("Exception in Terminal.moveTextCursor: " + e);
}
},
};
export { postVersion, Terminal };
export { Terminal };

View File

@ -3,6 +3,11 @@ import { Script } from "../Script/Script";
import { IPlayer } from "../PersonObjects/IPlayer";
import { IEngine } from "../IEngine";
export interface IOutput {
text: string;
color: "inherit" | "initial" | "primary" | "secondary" | "error" | "textPrimary" | "textSecondary" | undefined;
}
export interface ITerminal {
// Flags to determine whether the player is currently running a hack or an analyze
hackFlag: boolean;
@ -14,6 +19,8 @@ export interface ITerminal {
commandHistory: string[];
commandHistoryIndex: number;
outputHistory: IOutput[];
// True if a Coding Contract prompt is opened
contractOpen: boolean;
@ -24,6 +31,7 @@ export interface ITerminal {
print(s: string, config?: any): void;
error(s: string): void;
clear(): void;
startAnalyze(): void;
startBackdoor(player: IPlayer): void;
startHack(player: IPlayer): void;
@ -36,7 +44,6 @@ export interface ITerminal {
getScript(player: IPlayer, filename: string): Script | null;
getTextFile(player: IPlayer, filename: string): TextFile | null;
getLitFile(player: IPlayer, filename: string): string | null;
resetTerminalInput(): void;
cwd(): string;
setcwd(dir: string): void;
runContract(player: IPlayer, name: string): void;
@ -44,4 +51,6 @@ export interface ITerminal {
connectToServer(player: IPlayer, server: string): void;
executeCommand(engine: IEngine, player: IPlayer, command: string): void;
executeCommands(engine: IEngine, player: IPlayer, commands: string): void;
// If there was any changes, will return true, once.
pollChanges(): boolean;
}

View File

@ -1,5 +1,5 @@
import { postContent, hackProgressBarPost, hackProgressPost } from "../ui/postToTerminal";
import { ITerminal } from "./ITerminal";
import { ITerminal, IOutput } from "./ITerminal";
import { IEngine } from "../IEngine";
import { IPlayer } from "../PersonObjects/IPlayer";
import { HacknetServer } from "../Hacknet/HacknetServer";
@ -71,6 +71,7 @@ import { wget } from "./commands/wget";
import { clear } from "./commands/clear";
export class Terminal implements ITerminal {
hasChanges = false;
// Flags to determine whether the player is currently running a hack or an analyze
hackFlag = false;
backdoorFlag = false;
@ -81,6 +82,8 @@ export class Terminal implements ITerminal {
commandHistory: string[] = [];
commandHistoryIndex = 0;
outputHistory: IOutput[] = [{ text: `Bitburner v${CONSTANTS.Version}`, color: "primary" }];
// True if a Coding Contract prompt is opened
contractOpen = false;
@ -88,12 +91,22 @@ export class Terminal implements ITerminal {
// Excludes the trailing forward slash
currDir = "/";
pollChanges(): boolean {
if (this.hasChanges) {
this.hasChanges = false;
return true;
}
return false;
}
print(s: string, config?: any): void {
postContent(s, config);
this.outputHistory.push({ text: s, color: "primary" });
this.hasChanges = true;
}
error(s: string): void {
postContent(`ERROR: ${s}`, { color: "#ff2929" });
this.outputHistory.push({ text: s, color: "error" });
this.hasChanges = true;
}
startHack(player: IPlayer): void {
@ -251,7 +264,6 @@ export class Terminal implements ITerminal {
// Rename the progress bar so that the next hacks dont trigger it. Re-enable terminal
$("#hack-progress-bar").attr("id", "old-hack-progress-bar");
$("#hack-progress").attr("id", "old-hack-progress");
this.resetTerminalInput();
$("input[class=terminal-input]").prop("disabled", false);
}
@ -320,16 +332,13 @@ export class Terminal implements ITerminal {
return null;
}
resetTerminalInput(): void {
OldTerminal.resetTerminalInput();
}
cwd(): string {
return this.currDir;
}
setcwd(dir: string): void {
this.currDir = dir;
this.hasChanges = true;
}
async runContract(player: IPlayer, contractName: string): Promise<void> {
@ -467,7 +476,6 @@ export class Terminal implements ITerminal {
if (player.getCurrentServer().hostname == "darkweb") {
checkIfConnectedToDarkweb(); // Posts a 'help' message if connecting to dark web
}
this.resetTerminalInput();
}
executeCommands(engine: IEngine, player: IPlayer, commands: string): void {
@ -490,6 +498,11 @@ export class Terminal implements ITerminal {
}
}
clear(): void {
this.outputHistory = [{ text: `Bitburner v${CONSTANTS.Version}`, color: "primary" }];
this.hasChanges = true;
}
executeCommand(engine: IEngine, player: IPlayer, command: string): void {
if (this.hackFlag || this.backdoorFlag || this.analyzeFlag) {
this.error(`Cannot execute command (${command}) while an action is in progress`);
@ -666,8 +679,8 @@ export class Terminal implements ITerminal {
cat: cat,
cd: cd,
check: check,
cls: clear,
clear: clear,
cls: () => this.clear(),
clear: () => this.clear(),
connect: connect,
download: download,
expr: expr,

View File

@ -41,8 +41,5 @@ export function cd(
}
terminal.setcwd(evaledDir);
// Reset input to update current directory on UI
terminal.resetTerminalInput();
}
}

View File

@ -19,5 +19,4 @@ export function home(
player.getCurrentServer().isConnectedTo = true;
terminal.print("Connected to home");
terminal.setcwd("/");
terminal.resetTerminalInput();
}

View File

@ -1,5 +1,3 @@
import { post } from "../ui/postToTerminal";
import { containsAllStrings, longestCommonStart } from "../../utils/StringHelperFunctions";
/**
@ -9,7 +7,12 @@ import { containsAllStrings, longestCommonStart } from "../../utils/StringHelper
* @param arg {string} Last argument that is being completed
* @param allPossibilities {string[]} All values that `arg` can complete to
*/
export function tabCompletion(command: string, arg: string, allPossibilities: string[]): void {
export function tabCompletion(
command: string,
arg: string,
allPossibilities: string[],
oldValue: string,
): string[] | string | undefined {
if (!(allPossibilities.constructor === Array)) {
return;
}
@ -33,14 +36,6 @@ export function tabCompletion(command: string, arg: string, allPossibilities: st
}
}
const textBoxElem = document.getElementById("terminal-input-text-box");
if (textBoxElem == null) {
console.warn(`Couldn't find terminal input DOM element (id=terminal-input-text-box) when trying to autocomplete`);
return;
}
const textBox = textBoxElem as HTMLInputElement;
const oldValue = textBox.value;
const semiColonIndex = oldValue.lastIndexOf(";");
let val = "";
@ -56,13 +51,11 @@ export function tabCompletion(command: string, arg: string, allPossibilities: st
if (semiColonIndex === -1) {
// No semicolon, so replace the whole command
textBox.value = val;
return val;
} else {
// Replace only after the last semicolon
textBox.value = textBox.value.slice(0, semiColonIndex + 1) + " " + val;
return oldValue.slice(0, semiColonIndex + 1) + " " + val;
}
textBox.focus();
} else {
const longestStartSubstr = longestCommonStart(allPossibilities);
/**
@ -70,41 +63,30 @@ export function tabCompletion(command: string, arg: string, allPossibilities: st
* as whatevers already in terminal, just list all possible options. Otherwise,
* change the input in the terminal to the longest common starting substr
*/
let allOptionsStr = "";
for (let i = 0; i < allPossibilities.length; ++i) {
allOptionsStr += allPossibilities[i];
allOptionsStr += " ";
}
if (arg === "") {
if (longestStartSubstr === command) {
post("> " + command);
post(allOptionsStr);
return allPossibilities;
} else {
if (semiColonIndex === -1) {
// No semicolon, so replace the whole command
textBox.value = longestStartSubstr;
return longestStartSubstr;
} else {
// Replace only after the last semicolon
textBox.value = `${textBox.value.slice(0, semiColonIndex + 1)} ${longestStartSubstr}`;
return `${oldValue.slice(0, semiColonIndex + 1)} ${longestStartSubstr}`;
}
textBox.focus();
}
} else {
if (longestStartSubstr === arg) {
// List all possible options
post("> " + command + " " + arg);
post(allOptionsStr);
return allPossibilities;
} else {
if (semiColonIndex == -1) {
// No semicolon, so replace the whole command
textBox.value = `${command} ${longestStartSubstr}`;
return `${command} ${longestStartSubstr}`;
} else {
// Replace only after the last semicolon
textBox.value = `${textBox.value.slice(0, semiColonIndex + 1)} ${command} ${longestStartSubstr}`;
return `${oldValue.slice(0, semiColonIndex + 1)} ${command} ${longestStartSubstr}`;
}
textBox.focus();
}
}
}

View File

@ -0,0 +1,371 @@
import React, { useState, useEffect, useRef } from "react";
import Typography from "@material-ui/core/Typography";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
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 } 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: 0,
width: "100%",
},
input: {
backgroundColor: "#000",
},
nopadding: {
padding: 0,
},
preformatted: {
whiteSpace: "pre",
margin: 0,
},
}),
);
interface IProps {
terminal: ITerminal;
engine: IEngine;
player: IPlayer;
}
export function TerminalRoot({ terminal, engine, player }: IProps): React.ReactElement {
const terminalInput = useRef<HTMLInputElement>(null);
const setRerender = useState(false)[1];
function rerender(): void {
setRerender((old) => !old);
}
useEffect(() => {
const id = setInterval(() => {
if (terminal.pollChanges()) rerender();
}, 100);
return () => clearInterval(id);
}, []);
const [value, setValue] = useState("");
const [possibilities, setPossibilities] = useState<string[]>([]);
const classes = useStyles();
function handleValueChange(event: React.ChangeEvent<HTMLInputElement>): 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();
}
document.addEventListener("keydown", keyDown);
return () => document.removeEventListener("keydown", keyDown);
});
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
// Run command.
if (event.keyCode === KEY.ENTER && value !== "") {
event.preventDefault();
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 (
<Box position="fixed" bottom="0" width="100%">
<List classes={{ root: classes.nopadding }}>
{terminal.outputHistory.map((output, i) => (
<ListItem key={i} classes={{ root: classes.nopadding }}>
<Typography classes={{ root: classes.preformatted }} color={output.color} paragraph={false}>
{output.text}
</Typography>
</ListItem>
))}
</List>
{possibilities.length > 0 && (
<>
<Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}>
Possible autocomplete candidate:
</Typography>
<Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}>
{possibilities.join(" ")}
</Typography>
</>
)}
<TextField
autoFocus
classes={{ root: classes.textfield }}
value={value}
onChange={handleValueChange}
inputRef={terminalInput}
InputProps={{
className: classes.input,
startAdornment: (
<>
<Typography color="primary">
[{player.getCurrentServer().hostname}&nbsp;~{terminal.cwd()}]&gt;&nbsp;
</Typography>
</>
),
spellCheck: false,
onKeyDown: onKeyDown,
}}
></TextField>
</Box>
);
}

View File

@ -54,7 +54,8 @@ import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags";
import { initSpecialServerIps } from "./Server/SpecialServerIps";
import { initSymbolToStockMap, processStockPrices, displayStockMarketContent } from "./StockMarket/StockMarket";
import { MilestonesRoot } from "./Milestones/ui/MilestonesRoot";
import { Terminal, postVersion } from "./Terminal";
import { TerminalRoot } from "./Terminal/ui/TerminalRoot";
import { Terminal } from "./Terminal";
import { TutorialRoot } from "./Tutorial/ui/TutorialRoot";
import { Sleeve } from "./PersonObjects/Sleeve/Sleeve";
@ -90,7 +91,6 @@ const Engine = {
// Generic page that most react loads into.
content: null,
// Main menu content
terminalContent: null,
infiltrationContent: null,
workInProgressContent: null,
redPillContent: null,
@ -107,10 +107,15 @@ const Engine = {
loadTerminalContent: function () {
Engine.hideAllContent();
Engine.Display.terminalContent.style.display = "block";
document.getElementById("terminal-input-td").scrollIntoView(false);
routing.navigateTo(Page.Terminal);
MainMenuLinks.Terminal.classList.add("active");
Engine.Display.content.style.display = "block";
routing.navigateTo(Page.CharacterInfo);
ReactDOM.render(
<Theme>
<TerminalRoot terminal={Terminal} engine={this} player={Player} />
</Theme>,
Engine.Display.content,
);
MainMenuLinks.Stats.classList.add("active");
},
loadCharacterContent: function () {
@ -382,8 +387,6 @@ const Engine = {
// Helper function that hides all content
hideAllContent: function () {
Engine.Display.terminalContent.style.display = "none";
Engine.Display.content.style.display = "none";
Engine.Display.content.scrollTop = 0;
ReactDOM.unmountComponentAtNode(Engine.Display.content);
@ -844,16 +847,12 @@ const Engine = {
}
ReactDOM.render(<SidebarRoot engine={this} player={Player} />, document.getElementById("sidebar"));
// Initialize labels on game settings
Terminal.resetTerminalInput();
Engine.loadTerminalContent();
},
setDisplayElements: function () {
Engine.Display.content = document.getElementById("generic-react-container");
Engine.Display.content.style.display = "none";
// Content elements
Engine.Display.terminalContent = document.getElementById("terminal-container");
routing.navigateTo(Page.Terminal);
Engine.Display.missionContent = document.getElementById("mission-container");
Engine.Display.missionContent.style.display = "none";
@ -878,9 +877,6 @@ const Engine = {
// Initialization
init: function () {
// Message at the top of terminal
postVersion();
// Player was working cancel button
if (Player.isWorking) {
var cancelButton = document.getElementById("work-in-progress-cancel-button");

View File

@ -42,25 +42,6 @@
<div id="mainmenu-container" style="display: flex; flex-direction: row">
<!-- Main menu -->
<div id="sidebar" style=""></div>
<!-- Terminal page -->
<div id="terminal-container" style="flex-grow: 1">
<table id="terminal">
<tr id="terminal-input">
<td id="terminal-input-td" tabindex="2">
$
<input
type="text"
id="terminal-input-text-box"
class="terminal-input"
tabindex="1"
onfocus="this.value = this.value;"
autocomplete="off"
/>
</td>
</tr>
</table>
</div>
<div class="generic-menupage-container">
<div id="generic-react-container"></div>
</div>

View File

@ -1,18 +1,6 @@
import { renderToStaticMarkup } from "react-dom/server";
import { getElementById } from "../../utils/uiHelpers/getElementById";
/**
* Adds some output to the terminal.
* @param input Text or HTML to output to the terminal
*/
export function post(input: string): void {
postContent(input);
}
export function postError(input: string): void {
postContent(`ERROR: ${input}`, { color: "#ff2929" });
}
/**
* Adds some output to the terminal with an identifier of "hack-progress-bar"
* @param input Text or HTML to output to the terminal
@ -34,10 +22,6 @@ interface IPostContentConfig {
color?: string; // Additional class for terminal-line. Does NOT replace
}
export function postElement(element: JSX.Element): void {
postContent(renderToStaticMarkup(element));
}
export function postContent(input: string, config: IPostContentConfig = {}): void {
// tslint:disable-next-line:max-line-length
const style = `color: ${
@ -49,13 +33,4 @@ export function postContent(input: string, config: IPostContentConfig = {}): voi
} style="${style}">${input}</td></tr>`;
const inputElement: HTMLElement = getElementById("terminal-input");
inputElement.insertAdjacentHTML("beforebegin", content);
scrollTerminalToBottom();
}
function scrollTerminalToBottom(): void {
const container: HTMLElement = getElementById("terminal-container");
container.scrollTop = container.scrollHeight;
const td = document.getElementById("terminal-input-td");
if (td === null) throw new Error("terminal-input-td should not be null");
td.scrollIntoView(false);
}