bitburner-src/src/Terminal/ui/TerminalInput.tsx

421 lines
13 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect, useRef } from "react";
2021-09-17 01:23:03 +02:00
import Typography from "@mui/material/Typography";
import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
2022-04-18 13:55:59 +02:00
import Paper from "@mui/material/Paper";
2022-04-18 13:41:09 +02:00
import Popper from "@mui/material/Popper";
2022-04-18 13:55:59 +02:00
import TextField from "@mui/material/TextField";
2021-09-17 09:08:15 +02:00
import { KEY, KEYCODE } from "../../utils/helpers/keyCodes";
2022-09-06 15:07:12 +02:00
import { Terminal } from "../../Terminal";
import { Player } from "../../Player";
import { determineAllPossibilitiesForTabCompletion } from "../determineAllPossibilitiesForTabCompletion";
import { tabCompletion } from "../tabCompletion";
2021-09-22 07:36:17 +02:00
import { Settings } from "../../Settings/Settings";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
textfield: {
2021-09-18 01:43:08 +02:00
margin: theme.spacing(0),
},
input: {
2021-12-17 02:18:38 +01:00
backgroundColor: theme.colors.backgroundprimary,
},
nopadding: {
2021-09-18 01:43:08 +02:00
padding: theme.spacing(0),
},
preformatted: {
whiteSpace: "pre-wrap",
2021-09-18 01:43:08 +02:00
margin: theme.spacing(0),
},
list: {
2021-09-18 01:43:08 +02:00
padding: theme.spacing(0),
height: "100%",
},
}),
);
2021-09-18 08:34:59 +02:00
// Save command in case we de-load this screen.
let command = "";
2022-09-06 15:07:12 +02:00
export function TerminalInput(): React.ReactElement {
const terminalInput = useRef<HTMLInputElement>(null);
2021-09-18 08:34:59 +02:00
const [value, setValue] = useState(command);
2022-03-21 04:11:26 +01:00
const [postUpdateValue, setPostUpdateValue] = useState<{ postUpdate: () => void } | null>();
const [possibilities, setPossibilities] = useState<string[]>([]);
const classes = useStyles();
// If we have no data in the current terminal history, let's initialize it from the player save
2022-09-06 15:07:12 +02:00
if (Terminal.commandHistory.length === 0 && Player.terminalCommandHistory.length > 0) {
Terminal.commandHistory = Player.terminalCommandHistory;
Terminal.commandHistoryIndex = Terminal.commandHistory.length;
}
2021-12-16 23:03:49 +01:00
// Need to run after state updates, for example if we need to move cursor
// *after* we modify input
useEffect(() => {
if (postUpdateValue?.postUpdate) {
postUpdateValue.postUpdate();
setPostUpdateValue(null);
}
2022-03-21 04:11:26 +01:00
}, [postUpdateValue]);
function saveValue(newValue: string, postUpdate?: () => void): void {
command = newValue;
setValue(newValue);
if (postUpdate) {
2022-03-21 04:11:26 +01:00
setPostUpdateValue({ postUpdate });
}
2021-09-18 08:34:59 +02:00
}
function handleValueChange(event: React.ChangeEvent<HTMLInputElement>): void {
2021-09-18 08:34:59 +02:00
saveValue(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) {
2021-09-18 08:34:59 +02:00
saveValue(inputText.substr(0, start - 1) + inputText.substr(start));
}
break;
case "deletewordbefore": // Delete rest of word before the cursor
for (let delStart = start - 1; delStart > -2; --delStart) {
2022-03-24 16:09:24 +01:00
if ((inputText.charAt(delStart) === KEY.SPACE || delStart === -1) && delStart !== start - 1) {
saveValue(inputText.substr(0, delStart + 1) + inputText.substr(start), () => {
// Move cursor to correct location
// foo bar |baz bum --> foo |baz bum
const ref = terminalInput.current;
2022-03-21 04:11:26 +01:00
ref?.setSelectionRange(delStart + 1, delStart + 1);
});
return;
}
}
break;
case "deletewordafter": // Delete rest of word after the cursor, including trailing space
for (let delStart = start + 1; delStart <= value.length + 1; ++delStart) {
2022-03-24 16:09:24 +01:00
if (inputText.charAt(delStart) === KEY.SPACE || delStart === value.length + 1) {
saveValue(inputText.substr(0, start) + inputText.substr(delStart + 1), () => {
// Move cursor to correct location
// foo bar |baz bum --> foo bar |bum
const ref = terminalInput.current;
2022-03-21 04:11:26 +01:00
ref?.setSelectionRange(start, start);
});
return;
}
}
break;
case "clearafter": // Deletes everything after cursor
saveValue(inputText.substr(0, start));
break;
case "clearbefore": // Deletes everything before cursor
saveValue(inputText.substr(start), () => moveTextCursor("home"));
break;
case "clearall": // Deletes everything in the input
saveValue("");
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) {
2022-03-24 16:09:24 +01:00
if (ref.value.charAt(i) === KEY.SPACE) {
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) {
2022-03-24 16:09:24 +01:00
if (ref.value.charAt(i) === KEY.SPACE) {
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 {
2022-09-06 15:07:12 +02:00
if (Terminal.contractOpen) return;
if (Terminal.action !== null && event.key === KEY.C && event.ctrlKey) {
Terminal.finishAction(true);
2021-09-22 06:57:37 +02:00
return;
}
2021-09-22 06:57:37 +02:00
const ref = terminalInput.current;
if (event.ctrlKey || event.metaKey) return;
if (event.key === KEY.C && (event.ctrlKey || event.metaKey)) return; // trying to copy
2021-09-22 06:57:37 +02:00
if (ref) ref.focus();
}
document.addEventListener("keydown", keyDown);
return () => document.removeEventListener("keydown", keyDown);
});
2021-10-15 04:36:28 +02:00
async function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): Promise<void> {
const ref = terminalInput.current;
// Run command.
if (event.key === KEY.ENTER && value !== "") {
event.preventDefault();
2022-09-06 15:07:12 +02:00
Terminal.print(`[${Player.getCurrentServer().hostname} ~${Terminal.cwd()}]> ${value}`);
Terminal.executeCommands(value);
2021-09-18 08:34:59 +02:00
saveValue("");
return;
}
// Autocomplete
2022-03-21 04:11:26 +01:00
if (event.key === 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;
}
2022-09-18 03:09:15 +02:00
const allPos = await determineAllPossibilitiesForTabCompletion(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(" ");
}
2021-10-15 04:36:28 +02:00
let newValue = tabCompletion(command, arg, allPos, value);
if (typeof newValue === "string" && newValue !== "") {
2021-10-16 00:33:27 +02:00
if (!newValue.endsWith(" ") && !newValue.endsWith("/") && allPos.length === 1) newValue += " ";
2021-09-18 08:34:59 +02:00
saveValue(newValue);
}
if (Array.isArray(newValue)) {
setPossibilities(newValue);
}
}
// Clear screen.
if (event.key === KEY.L && event.ctrlKey) {
event.preventDefault();
2022-09-06 15:07:12 +02:00
Terminal.clear();
}
// Select previous command.
2022-03-24 16:09:24 +01:00
if (event.key === KEY.UP_ARROW || (Settings.EnableBashHotkeys && event.key === KEY.P && event.ctrlKey)) {
2021-09-22 07:36:17 +02:00
if (Settings.EnableBashHotkeys) {
event.preventDefault();
}
2022-09-06 15:07:12 +02:00
const i = Terminal.commandHistoryIndex;
const len = Terminal.commandHistory.length;
if (len == 0) {
return;
}
if (i < 0 || i > len) {
2022-09-06 15:07:12 +02:00
Terminal.commandHistoryIndex = len;
}
if (i != 0) {
2022-09-06 15:07:12 +02:00
--Terminal.commandHistoryIndex;
}
2022-09-06 15:07:12 +02:00
const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
2021-09-18 08:34:59 +02:00
saveValue(prevCommand);
if (ref) {
setTimeout(function () {
ref.selectionStart = ref.selectionEnd = 10000;
}, 10);
}
}
// Select next command
2022-03-24 16:09:24 +01:00
if (event.key === KEY.DOWN_ARROW || (Settings.EnableBashHotkeys && event.key === KEY.M && event.ctrlKey)) {
2021-09-22 07:36:17 +02:00
if (Settings.EnableBashHotkeys) {
event.preventDefault();
}
2022-09-06 15:07:12 +02:00
const i = Terminal.commandHistoryIndex;
const len = Terminal.commandHistory.length;
if (len == 0) {
return;
}
if (i < 0 || i > len) {
2022-09-06 15:07:12 +02:00
Terminal.commandHistoryIndex = len;
}
// Latest command, put nothing
if (i == len || i == len - 1) {
2022-09-06 15:07:12 +02:00
Terminal.commandHistoryIndex = len;
2021-09-18 08:34:59 +02:00
saveValue("");
} else {
2022-09-06 15:07:12 +02:00
++Terminal.commandHistoryIndex;
const prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
2021-09-18 08:34:59 +02:00
saveValue(prevCommand);
}
}
2021-11-27 00:05:14 +01:00
// Extra Bash Emulation Hotkeys, must be enabled through options
2021-09-22 07:36:17 +02:00
if (Settings.EnableBashHotkeys) {
if (event.code === KEYCODE.C && event.ctrlKey && ref && ref.selectionStart === ref.selectionEnd) {
event.preventDefault();
2022-09-06 15:07:12 +02:00
Terminal.print(`[${Player.getCurrentServer().hostname} ~${Terminal.cwd()}]> ${value}`);
modifyInput("clearall");
}
if (event.code === KEYCODE.A && event.ctrlKey) {
event.preventDefault();
moveTextCursor("home");
}
if (event.code === KEYCODE.E && event.ctrlKey) {
event.preventDefault();
moveTextCursor("end");
}
if (event.code === KEYCODE.B && event.ctrlKey) {
event.preventDefault();
moveTextCursor("prevchar");
}
if (event.code === KEYCODE.B && event.altKey) {
event.preventDefault();
moveTextCursor("prevword");
}
if (event.code === KEYCODE.F && event.ctrlKey) {
event.preventDefault();
moveTextCursor("nextchar");
}
if (event.code === KEYCODE.F && event.altKey) {
event.preventDefault();
moveTextCursor("nextword");
}
if ((event.code === KEYCODE.H || event.code === KEYCODE.D) && event.ctrlKey) {
modifyInput("backspace");
event.preventDefault();
}
if (event.code === KEYCODE.W && event.ctrlKey) {
event.preventDefault();
modifyInput("deletewordbefore");
}
if (event.code === KEYCODE.D && event.altKey) {
event.preventDefault();
modifyInput("deletewordafter");
}
if (event.code === KEYCODE.U && event.ctrlKey) {
event.preventDefault();
modifyInput("clearbefore");
}
if (event.code === KEYCODE.K && event.ctrlKey) {
event.preventDefault();
modifyInput("clearafter");
}
}
}
return (
<>
2022-04-18 13:41:09 +02:00
<TextField
fullWidth
2022-09-06 15:07:12 +02:00
color={Terminal.action === null ? "primary" : "secondary"}
2022-04-18 13:41:09 +02:00
autoFocus
2022-09-06 15:07:12 +02:00
disabled={Terminal.action !== null}
2022-04-18 13:41:09 +02:00
autoComplete="off"
value={value}
classes={{ root: classes.textfield }}
onChange={handleValueChange}
inputRef={terminalInput}
InputProps={{
// for players to hook in
id: "terminal-input",
className: classes.input,
startAdornment: (
2022-09-06 15:07:12 +02:00
<Typography color={Terminal.action === null ? "primary" : "secondary"} flexShrink={0}>
[{Player.getCurrentServer().hostname}&nbsp;~{Terminal.cwd()}]&gt;&nbsp;
2022-04-18 13:41:09 +02:00
</Typography>
),
spellCheck: false,
2022-04-18 13:55:59 +02:00
onBlur: () => setPossibilities([]),
2022-04-18 13:41:09 +02:00
onKeyDown: onKeyDown,
}}
></TextField>
<Popper open={possibilities.length > 0} anchorEl={terminalInput.current} placement={"top-start"}>
2022-04-18 13:55:59 +02:00
<Paper sx={{ m: 1, p: 2 }}>
<Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}>
Possible autocomplete candidates:
</Typography>
<Typography classes={{ root: classes.preformatted }} color={"primary"} paragraph={false}>
{possibilities.join(" ")}
</Typography>
</Paper>
2022-04-18 13:41:09 +02:00
</Popper>
</>
);
}