-
Notifications
You must be signed in to change notification settings - Fork 357
COMMAND: Custom help commands for your own scripts #2227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
51e7fdc
530590d
ed9c2c5
be981c1
e0b971f
ca4dad2
907876f
e729125
ed659dd
23272a3
417c99c
0ca7d4d
ea3f6f0
5eb04ba
56f14b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,14 @@ | ||
| # Autocomplete | ||
| # Autocomplete and Help | ||
|
|
||
| The BitBurner terminal offers tab-completion, where pressing tab after typing a command offers suggestions for arguments to pass. You can customize this behavior for your scripts. | ||
| Beyond the scope of executing your [scripts](scripts.md) in BitBurner, you have extra functionality that may be **exported** out of your files. | ||
|
|
||
| This relies on an exported function named "autocomplete" that is placed _outside_ of main, in the base scope of the script. | ||
| You have the capability of implementing _autocomplete_ for your scripts terminal interaction, and custom _help_ instructions that are shown when you use the `help` command. | ||
|
|
||
| These rely on exported functions named `autocomplete()` and `help()`, that must be placed _outside_ of main, in the base scope of the script. | ||
|
|
||
| ## Autocomplete | ||
|
|
||
| The BitBurner terminal offers tab-completion, where pressing `tab` after typing a command offers suggestions for arguments to pass. You can customize this behavior for your scripts. | ||
|
|
||
| This function must return an array, the contents of which make up the autocomplete options. | ||
|
|
||
|
|
@@ -27,7 +33,7 @@ export function main(ns) { | |
|
|
||
| Running this script from the terminal like `run script.js` or `./script.js` and pressing tab, would offer "argument0", "argument1" and "argument2" as autocomplete options. | ||
|
|
||
| ## AutocompleteData | ||
| ### AutocompleteData | ||
|
|
||
| To make this feature more useful, an [AutocompleteData](https://github.com/bitburner-official/bitburner-src/blob/stable/markdown/bitburner.autocompletedata.md) object is provided to the autocomplete function that holds information commonly passed as arguments to scripts, such as server names and filenames. | ||
|
|
||
|
|
@@ -90,8 +96,103 @@ export function autocomplete(data, args) { | |
|
|
||
| In that example typing `run script.js` and pressing tab would initially suggest every server for autocomplete. Then if "n00dles" is added to the arguments and tab is pressed again, "n00dles" would no longer be suggested in subsequent autocomplete calls. | ||
|
|
||
| # Notes | ||
| # Help | ||
|
|
||
| The `help` terminal command offers detailed information about existing terminal commands. It can also display custom help messages defined inside a script file. | ||
|
|
||
| - The autocomplete function in the file is called each time the tab key is pressed following `run file.js` or `./file.js` in the terminal. | ||
| - The autocomplete function is separate from `main`, and does not receive an `ns` context as a parameter. This means no `ns` game commands will work in autocomplete functions. | ||
| - If a multi-element array is returned then multiple options are displayed. If a single-element array is returned then that element is auto-filled to the terminal. This is handy for the "--tail" run argument, for example. | ||
| This function's return type can be either a simple string, or if you prefer a little bit more customization, a [ReactNode](../programming/react.md). | ||
|
|
||
| For example: | ||
|
|
||
| ```javascript | ||
| /** | ||
| * @param {AutocompleteData} data - context about the game, may be useful to list argument documentation | ||
| * @returns {string|ReactNode} - Outputted to the Terminal | ||
| */ | ||
| export function help(data) { | ||
| return ["This is a simple script.", " ", "This script will output foo."].join("\n"); | ||
| } | ||
|
|
||
| /** @param {NS} ns */ | ||
| export function main(ns) { | ||
| ns.tprint("foo"); | ||
| } | ||
| ``` | ||
|
|
||
| Running `help` on the terminal as `help script.js` would display the provided strings line-by-line, as shown below: | ||
|
|
||
| ```plaintext | ||
| Usage for script.js: | ||
| This is a simple script. | ||
|
|
||
| This script will output foo. | ||
| ``` | ||
|
|
||
| This function supports ANSI escape codes, in case you'd like a bit of customization in your text. | ||
|
|
||
| ```javascript | ||
| /** | ||
| * @param {AutocompleteData} data - context about the game, may be useful to list argument documentation | ||
| * @returns {string|ReactNode} - Outputted to the Terminal | ||
| */ | ||
| export function help() { | ||
| return `${"\x1b[2m"}This is fancy bold text!`; | ||
| } | ||
|
|
||
| /** @param {NS} ns */ | ||
| export function main(ns) { | ||
| // ... | ||
| } | ||
| ``` | ||
| Which would show "**This is fancy bold text!**" in the Terminal. | ||
|
|
||
| ## Advanced Use | ||
|
|
||
| For more advanced uses of this function, you might consider using _TypeScript_ files. | ||
|
|
||
| ### Context clues through AutocompleteData | ||
|
|
||
| AutocompleteData may be passed into the help function to give custom messages based on context. This may be helpful when you have a script that takes in game-specific arguments, which synergize with the autocomplete function: | ||
|
|
||
| A few notable differences with this and autocomplete is that the `.command` property contains the file path and name, the `.flags` property is empty. | ||
|
|
||
| _Example: contracts.ts_ | ||
|
|
||
| ```ts | ||
| export function help(data: AutocompleteData): string { | ||
| return [ | ||
| "Finds and solves contracts in a server.", | ||
| "Possible arguments: ", | ||
| ` ${data.servers.join(", ")}` | ||
| ].join("\n"); | ||
| } | ||
|
|
||
| // ... | ||
| ``` | ||
|
|
||
| ```plaintext | ||
| Usage for contracts.ts: | ||
| Finds and solves contracts in a server. | ||
| Possible arguments: | ||
| n00dles, foodnstuff, sigma-cosmetics, joesguns, hong-fang-tea, harakiri-sushi, iron-gym | ||
| ``` | ||
|
|
||
|
|
||
| The function can also return a React element. For this, it's highly recommended to write a `*.tsx` script file. | ||
| The example above, written in `.tsx`: | ||
|
|
||
| ```tsx | ||
| export function help(data: AutocompleteData): ReactNode { | ||
| return <li><ul> | ||
| <li> ./{data.command} {"<servers...>"} </li> | ||
| <li> Servers must be one of: {data.servers.join(", ")} </li> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
| </ul></li>; | ||
| } | ||
| ``` | ||
|
|
||
| Outputs: | ||
| ```plaintext | ||
| Usage for contracts.tsx: | ||
| - ./contracts.tsx <servers...> | ||
| - Servers must be one of: n00dles, foodnstuff, sigma-cosmetics, joesguns, hong-fang-tea, harakiri-sushi, iron-gym | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,20 +1,104 @@ | ||||||
| import { Terminal } from "../../Terminal"; | ||||||
| import { Player } from "@player"; | ||||||
| import type { AutocompleteData } from "@nsdefs"; | ||||||
| import { TerminalHelpText, HelpTexts } from "../HelpText"; | ||||||
| import { Terminal } from "../../Terminal"; | ||||||
| import { compile } from "../../NetscriptJSEvaluator"; | ||||||
| import { wrapUserNode } from "../../Netscript/NetscriptHelpers"; | ||||||
| import { enums } from "../../NetscriptFunctions"; | ||||||
| import { GetAllServers } from "../../Server/AllServers"; | ||||||
| import { hasScriptExtension, resolveScriptFilePath, ScriptFilePath } from "../../Paths/ScriptFilePath"; | ||||||
| import { Flags } from "../../NetscriptFunctions/Flags"; | ||||||
| import { hasTextExtension } from "../../Paths/TextFilePath"; | ||||||
|
|
||||||
| export function help(args: (string | number | boolean)[]): void { | ||||||
| if (args.length !== 0 && args.length !== 1) { | ||||||
| Terminal.error("Incorrect usage of help command. Usage: help"); | ||||||
| return; | ||||||
| } | ||||||
| if (args.length === 0) { | ||||||
| TerminalHelpText.forEach((line) => Terminal.print(line)); | ||||||
| const flushedOut = TerminalHelpText.join("\n"); | ||||||
| Terminal.print(flushedOut); | ||||||
| } else { | ||||||
| const cmd = args[0] + ""; | ||||||
| const txt = HelpTexts[cmd]; | ||||||
|
|
||||||
| if (txt == null) { | ||||||
| Terminal.error("No help topics match '" + cmd + "'"); | ||||||
| // Here is where flow lands if we have a player-implemented command | ||||||
|
|
||||||
| // Input sanitization | ||||||
| const cmdCopy = String(cmd).replace(/^[/.]+/, "") as ScriptFilePath; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this? Does help not work for scripts with relative directories? This doesn't feel like the right way to be checking for a script. I think Terminal.getFilepath is what you ought to be using here. |
||||||
| const filePath = resolveScriptFilePath(cmdCopy); | ||||||
|
|
||||||
| const localServer = Player.getCurrentServer(); | ||||||
|
|
||||||
| if (filePath == null) { | ||||||
| if (hasScriptExtension(cmdCopy)) { | ||||||
| Terminal.error(`Could not find script '${cmdCopy}'\nMake sure this file exists in this server.`); | ||||||
| } else if (hasTextExtension(cmdCopy)) { | ||||||
| Terminal.error( | ||||||
| `'${cmdCopy}' needs to be either a *.js, *.ts, *.jsx or *.tsx file to have detailed help information.`, | ||||||
| ); | ||||||
| } else { | ||||||
| Terminal.error(`No help entry for '${cmdCopy}'.`); | ||||||
| } | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| const script = localServer.scripts.get(filePath); | ||||||
|
|
||||||
| try { | ||||||
| if (script == null) { | ||||||
| throw new Error("Script pathname has no valid script object"); | ||||||
| } | ||||||
|
|
||||||
| // Deal with help() here | ||||||
|
|
||||||
| // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This warning is legitimate, although ultimately you will still end up doing the equivalent of dangling a promise. But, the issue is that everything within here is asynchronous, whereas the help command ends immediately. So, multiple lines can end up appended to the terminal before the script comes back with the result of the help. I think what you're going to need to do is I think you might be able to get fancy and still use Terminal.print/Terminal.error and thus avoid having to do ugly things for the ANSI support, by then directly mucking with the Terminal object and essentially swapping the placeholder object you created with printRaw for the new line, and removing the placeholder after. Since this all takes place synchronously (inside the resolution of the async |
||||||
| compile(script, localServer.scripts).then((compiledModule) => { | ||||||
| if (compiledModule.help == null) { | ||||||
| Terminal.error("No help text for '" + cmdCopy + "'. Implement it by exporting a help() function."); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| const autocompleteData: AutocompleteData = { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Surely there's a version of this already for autocomplete? They should be shared, unless there's a painful reason why they can't. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While refactoring, as d0sboots said, you should be careful when making any differences (e.g., |
||||||
| hostname: localServer.hostname, | ||||||
| servers: GetAllServers() | ||||||
| .filter((server) => server.backdoorInstalled || localServer.serversOnNetwork.includes(server.hostname)) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If there is a reason that |
||||||
| .map((server) => server.hostname), | ||||||
| scripts: [...localServer.scripts.keys()], | ||||||
| txts: [...localServer.textFiles.keys()], | ||||||
| enums: enums, | ||||||
| // Pass no flags as the help command does not use flags | ||||||
| flags: Flags([""]), | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| filename: script.filename, | ||||||
| processes: Array.from(localServer.runningScriptMap.values(), (m) => | ||||||
| Array.from(m.values(), (r) => ({ | ||||||
| pid: r.pid, | ||||||
| filename: r.filename, | ||||||
| threads: r.threads, | ||||||
| args: r.args.slice(), | ||||||
| temporary: r.temporary, | ||||||
| })), | ||||||
| ).flat(), | ||||||
| // Pass the command as the script filename | ||||||
| command: `${cmdCopy}`, | ||||||
| }; | ||||||
|
|
||||||
| const helpObj = compiledModule.help(autocompleteData); | ||||||
| Terminal.print(`Usage for ${cmdCopy}:`); | ||||||
|
|
||||||
| if (typeof helpObj === "string") { | ||||||
| Terminal.print(helpObj); | ||||||
| } else { | ||||||
| Terminal.printRaw(wrapUserNode(helpObj)); | ||||||
| } | ||||||
| }); | ||||||
| } catch (err) { | ||||||
| Terminal.error("Failed to get information for '" + cmdCopy + "'. Check if the script has any syntax errors."); | ||||||
| } | ||||||
| return; | ||||||
| } | ||||||
| txt.forEach((t) => Terminal.print(t)); | ||||||
| const flushedOut: string = txt.join("\n"); | ||||||
| Terminal.print(flushedOut); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to update this example code and output. Please check my comment in
help.ts. I recommend using something that is notdata.servers. That list is too long after making the suggested change in that comment, so it's not really a good example.