Skip to content

Latest commit

 

History

125 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DX Command Terminal

Disclosure

DxCommandTerminal builds on the original Command Terminal project. The initial development of this fork was entirely human-authored, involving months of code study, refactoring, testing, and refinement. Recent development is LLM-assisted, including generated code. I, wallstop, heavily review user-facing code changes and will continue to do so, but those changes should no longer be understood as entirely human-authored. This approach makes continued development practical within the time I can commit and allows faster iteration.

This project is MIT licensed and will remain so. There are no plans for paid features, subscriptions, or mechanisms for charging users. This disclosure is provided so you can decide whether the project fits your preferences.

Notice

This is a fork of Command Terminal for Unity, mainly to address usability gaps and add maintenance

Compatibility

Platform Compatible
Unity 2021 Likely, but untested
Unity 2022
Unity 2023
Unity 6
URP
HDRP
.NET Standard 2.0 API level
.NET Framework 4.x API level

The package ships no precompiled assemblies and uses only APIs available to both API compatibility levels; the full test suite passes with the project set to either .NET Standard 2.0 or .NET Framework in Player Settings (verified on Unity 6000.4.6f1).

Installation

From Releases

Check out the latest Releases to grab the Unity Package and import to your project.

To Install as Unity Package

  1. Open Unity Package Manager
  2. (Optional) Enable Pre-release packages to get the latest, cutting-edge builds
  3. Open the Advanced Package Settings
  4. Add an entry for a new "Scoped Registry"
    • Name: NPM
    • URL: https://registry.npmjs.org
    • Scope(s): com.wallstop-studios.dxcommandterminal
  5. Resolve the latest com.wallstop-studios.dxcommandterminal

From Source

Grab a copy of this repo (either git clone or download a zip of the source) and copy the contents to your project's Assets folder.

Improvements Over Baseline

  • Enhanced Auto-Complete + Hint system + styling
  • Fixed Input handling bugs related to WebGL
  • Fully integrated with Unity's new Input System
  • Fully configurable and bindable controls for every action
  • Add ability to ignore commands that have been annotated with RegisterCommandAttribute. In this way, your terminals can ignore any built-in commands, for cleanliness. A custom editor has been added to provide users with the ability to identify what commands are available to ignore, and selectively ignore them.
  • Add ability to ignore certain (or all) log levels, such that unwanted logs do not clutter terminal output
  • Add ability to optionally have Unity log messages routed to the terminal, default on, but can be turned off
  • Add ability to optionally ignore all default commands, such that you can write your own commands that conflict with the default provided ones, such as HELP, TIME, etc
  • CommandArg value parsing has been overhauled. Instead of specific properties like Float and Int that always return a value, regardless of if parsing succeeded or not, a singular bool TryGet<T>(out T parsed) method is supplied. This method provides robust parsing for all common types and has the ability to take in user-specified custom parsers for any fancy logic. See Custom Parsing for details.
  • CommandArgs can now have arguments with spaces passed to them, similar to how real terminals work. Any string values surrounded by single or double quotes will be interpreted as a single input (including spaces within those quotes). Single quotes must match single quotes and double quotes must match double quotes. If there is an unmatched (hanging) quote, the entire rest of the input will be interpreted as the command argument value.
  • Dynamic console resizing when Screen Size change is detected, implemented in a very performant fashion
  • Added Assembly Definitions, so that this project can be used cleanly in projects that utilize Assembly Definitions
  • Fixed a bug where commands annotated with RegisterCommandAttribute in other assemblies failed to be recognized. User assemblies have higher precedence.
  • Fixed a bug where only the latest error message was preserved - errors are now queued
  • Fixed a bug where attempting to access static Terminal properties would throw if the Terminal had been enabled yet.
  • Fixed a bug where moving through command history did not update the cursor position
  • Fixed a bug where enabling and disabling the Terminal would break AutoComplete
  • Fixed a bug where you could interact with the terminal when it was in closed state
  • Fixed a bug where commands run programmatically were not added to history
  • Fixed a bug where terminal line height was not being calculated correctly
  • Fixed a bug where input caret width was not being calculated correctly. Previously, long caret strings would be rendered improperly
  • Fixed a bug related to input swallowing
  • Fixed a bug where transitioning from full to small terminal would instead, close the terminal
  • Added ability to skip same commands when navigating through history
  • Unified behavior around navigating up and down through command history. Previously, navigating up as far as possible would "stick" to the up-most command, while navigating down as far as possible would result in a "blank" command. Now, walking past either end of up/down results in a blank command.
  • History is now filterable by execution success (command existence) as well as error status of the command
  • Minor performance benefits if there are terminals in multiple scenes
  • Minor performance benefits (O(n) -> O(1)) when the terminal buffer becomes full
  • Minor performance benefits around multiple-indexing into dictionary issues
  • Minor performance benefits around using string interpolation and intelligent checking of parameters to only force string.Format when relevant in logging paths
  • Better invalid command identification and error messages
  • All string comparisons are now OrdinalIgnoreCase instead of relying on CurrentCulture
  • Terminal has been made to be Editor-Change-Aware. If the Editor is in Play mode, changes to the current terminal properties will take effect immediately. This functionality has a minor performance hit and can be disabled via un-checking Track Changes In Editor under the System header.
  • Extra input validation has been added on all public methods, such that user code is sanitized where appropriate, or rejected if invalid.
  • The concept of "FrontCommands" has been exterminated

Code changes

  • All code is formatted via Csharpier
  • All variables are now consistently named
  • Access modifiers have been explicitly applied to every field
  • All warnings have been gotten rid of
  • Collections / properties are now exposed as immutable by default. Mutable fields / properties are only exposed as necessary
  • Conversion to capitalization/lowercase is now only done where absolutely required
  • Annotated all logging methods with Jetbrain's StringFormatMethod attribute to aid in intellisense and help identify formatting issues
  • All classes / structs have been made sealed / readonly where possible to promote immutability
  • Validation around command ignoring and log level ignoring has been added to Terminal, to prevent invalid data

The Future

More improvements coming soon, stick around :)

Planned improvements:

  • More and better documentation
  • Wikification
  • Command Groups
  • Ensure working in Mobile builds
  • Smarter auto complete
  • Ensure HTML color coding works
  • More parsing (Bounds, Matrix4x4, Plane, Ray, BoundsInt, Pose, LayerMask, Color32)
  • More documentation

A simple and highly performant in-game drop down Console.

gif

Command Terminal is based on an implementation by Jonathan Blow done in the Jai programming language.

Usage

Copy the contents from CommandTerminal to your Assets folder. Attach a Terminal Component to a game object. The console window can be toggled with a hotkey (default is backtick), and another hotkey can be used to toggle the full size window (default is shift+backtick).

Enter help in the console to view all available commands, use the up and down arrow keys to traverse the command history, and the tab key to autocomplete commands.

Quick-Launch Bar

Add a CommandPaletteUI component next to a UIDocument to get a launcher-style command bar with no terminal animation. Press Ctrl+Space (configurable via toggleHotkey) to open a compact search bar; results appear as you type, ranked exact-first, then prefix, then fuzzy subsequence, and run through the same shell as the terminal. Up/Down selects a result and auto-loads its name into the input (the match list stays put until you edit the text again), Tab commits the selected name, Enter runs it (a failed command keeps the bar open with visible feedback), and Escape closes while restoring the previous focus. Once the input names a command and reaches an argument, the bar shows that command's completion candidates (static choices and dynamic providers, staged across arguments): Tab applies the selected candidate to the active token — quoting values that contain spaces when the token is not already quoted — and clicking a row applies it without running. Commands that print output (list-fonts, list-themes, help, ...) clear the executed command from the bar, show their results inside the palette, and stay open, so the output is readable; Enter does not re-run until you type a new query. Opening the terminal closes the bar and vice versa, so a hotkey press can never execute twice, and the Opened/Closed events let gameplay code release its own input maps. Styling rides the terminal's stylesheet chain and every shipped TerminalThemePack theme, so the bar, its results, and its slim scrollbar follow whatever theme the terminal is using. As with the terminal itself, full visuals require the package's BaseStyles.uss on the panel settings; without it the bar falls back to engine defaults.

Registering Commands

There are 2 options to register commands to be used in the Command Terminal.

1. Using the RegisterCommand attribute:

The command method must be static (public or non-public).

[RegisterCommand(Help = "Adds 2 numbers", MinArgCount = 2, MaxArgCount = 2)]
static void CommandAdd(CommandArg[] args) {
    int a = args[0].Int;
    int b = args[1].Int;

    if (Terminal.IssuedError) return; // Error will be handled by Terminal

    int result = a + b;
    Terminal.Log("{0} + {1} = {2}", a, b, result);
}

MinArgCount and MaxArgCount allows the Command Interpreter to issue an error if arguments have been passed incorrectly, this way you can index the CommandArg array, knowing the array will have the correct size.

In this case the command name (add) will be inferred from the method name, you can override this by setting Name in RegisterCommand.

[RegisterCommand(Name = "MyAdd", Help = "Adds 2 numbers", MinArgCount = 2, MaxArgCount = 2)]

2. Manually adding Commands:

RegisterCommand only works for static methods. If you want to use a non-static method, you may add the command manually.

Terminal.Shell.AddCommand("add", CommandAdd, 2, 2, "Adds 2 numbers");

3. Using the typed command builder:

For instance-owned commands (per-player abilities, scene tools, debug overlays), the fluent CommandBuilder derives argument bounds, the usage hint, validation, and Tab completion from one declaration, and returns a disposable handle that removes exactly its own registration on dispose:

// Register on enable, dispose on disable.
CommandRegistrationHandle handle;
void OnEnable()
{
    Terminal.Shell.AddCommand(
        CommandBuilder
            .Create("heal", "Heals a target")
            .Arg<int>("amount", spec => spec.Required().Range(1, 100))
            .Arg<string>("target", spec => spec.Default("self").Choices("self", "ally"))
            .Handler((context, arguments) =>
            {
                int amount = arguments.Get<int>(0);
                string target = arguments.Get<string>("target");
                // ...
            }),
        out handle
    );
}

void OnDisable() => handle?.Dispose();

Arguments parse through the same CommandArg parsers as [RegisterCommand] methods (including your registered custom parsers). Bad user input - unknown values, out-of-range numbers, too many arguments - is rejected with a descriptive error before the handler runs. Static and dynamic choices feed Tab completion stage by stage: with pickaxe as a preceding argument, the second argument's provider sees it and completes the next stage.

Subcommands route one registered command through its first argument, each with its own arguments, validation, and completion. inventory below registers once; inventory add pickaxe 3 and inventory remove pickaxe route to their own handlers:

Terminal.Shell.AddCommand(
    CommandBuilder
        .Create("inventory", "Manage the inventory")
        .Handler((context, arguments) => { /* runs for a bare `inventory` */ })
        .Subcommand(
            "add",
            add => add
                .Arg<string>("item", spec => spec.Required())
                .Arg<int>("count", spec => spec.Default(1).Range(1, 99))
                .Handler((context, arguments) => { /* ... */ })
        )
        .Subcommand(
            "remove",
            remove => remove
                .Arg<string>("item", spec => spec.Required())
                .Handler((context, arguments) => { /* ... */ })
        ),
    out handle
);

Tab completion offers subcommand names at the first argument and the routed subcommand's choices beyond it. Bare invocations run the parent handler when one is set, otherwise a descriptive error lists the available subcommands. The parent's execution contexts and history policy govern every subcommand; declaring parent arguments on a command that has subcommands, or setting Contexts/AddToHistory on a subcommand, throws at definition time. Like any command with a completion provider, a routed command keeps provider-owned completion: history-based suggestions do not apply to its arguments.

.Remaining<T>(name) declares the command's unbounded trailing argument: every token after the declared arguments parses and validates with the same rules as any typed argument and collects, in order, into one array the handler reads with arguments.Get<T[]>(name). An invocation without trailing tokens reads as an empty array; mark the argument .Required() to demand at least one token instead. The usage hint marks it (announce <message:string...>), and its choices complete every trailing stage:

Terminal.Shell.AddCommand(
    CommandBuilder
        .Create("say", "Prints a message")
        .Remaining<string>("message")
        .Handler((context, arguments) =>
        {
            string[] message = arguments.Get<string[]>("message");
            // ...
        }),
    out handle
);

The remaining argument must be the final declaration: arguments declared after it, a second remaining argument, or a default on it throw at definition time.

Definition-time mistakes throw CommandConfigurationException, which carries structured data for programmatic handling: Failure classifies the problem (empty name, duplicate name, missing handler, argument ordering, remaining-argument misuse, unparseable argument type, unsupported argument feature, invalid choices or range or default, subcommand conflicts, null configure result), and CommandName, ArgumentName, SubcommandName, and ArgumentType identify what failed - no message parsing required. Reading a parsed argument with the wrong type inside a handler throws CommandArgumentTypeMismatchException, carrying ArgumentName, StoredType, and RequestedType; remaining arguments store an array of their element type, so reading the element type itself reports StoredType as that array type.

Note: if your TerminalUI uses Reset State On Init, the terminal rebuilds its shell during its own startup. Register commands from Start (or after the terminal is ready) rather than from another component's OnEnable, or the registration can be discarded by that reset.


Custom Parsing

One change from the original Command Parser is the usage / functionality exposed for parsing the parameters to the CommandArgs themselves. The original library exposed four methods for retrieving arguments - String, Float, Int, and bool. If the input was invalid, these parsing methods would simply return the default value and log an error, making it very difficult to programmatically react to bad input.

To remedy this, DxCommandTerminal exposes a single method, TryGet, and a readonly string field contents. contents returns the original input parameters as-is. TryGet attempts to parse the provided input parameter as the given type, taking in an optional parser. You can also register and deregister parsers for any type, which will override the built-in ones, making use of whatever custom logic you want (JSON, for example).

public bool TryGet<T>(out T parsed);

Sample usage:

CommandArg arg = new CommandArg("1");
Assert.IsTrue(arg.TryGet(out int parsedInt)); // 1
Assert.IsTrue(arg.TryGet(out double parsedDouble)); // 1.0d
Assert.IsTrue(arg.TryGet(out float parsedFloat)); // 1.0f
Assert.IsTrue(arg.TryGet(out char parsedChar)); // '1'

Assert.IsFalse(arg.TryGet(out Vector2 invalidVector2)); // Failed to parse
Assert.IsFalse(arg.TryGet(out Color invalidColor)); // Failed to parse

arg = new CommandArg("red");
Assert.IsTrue(arg.TryGet(out Color color)); // Color.red
Assert.IsFalse(arg.TryGet(out int invalidInt)); // Failed to parse

TryGet<T> supports the following types out of the box:

  • string
  • char
  • bool
  • float
  • double
  • decimal
  • int
  • uint
  • long
  • ulong
  • short
  • ushort
  • byte
  • sbyte
  • Guid
  • DateTime
  • DateTimeOffset
  • Enums

Unity Types:

  • Vector2
  • Vector3
  • Vector4
  • Quaternion
  • Vector2Int
  • Vector3Int
  • Rect
  • RectInt
  • Color

In addition to parsing values directly, TryGet will automatically attempt to match public static or public const named fields. For example, all of the following will parse as doubles:

  • "1.2"
  • "MaxValue"
  • "MinValue"

Similarly, for Colors:

  • "RGBA(0.7, 0.5, 0.1, 1.0)"
  • "(0.7, 0.5, 0.1, 1.0)"
  • "(0.7, 0.5, 0.1)"
  • "red"
  • "clear"

For all built-in types, the parsers are guaranteed to work with the type's default ToString() implementation, as well as logical versions of the data structure. See Color sample parsable inputs for an example of this.

If you would like to have built-in parsers for a type that is not listed above, please open an issue!

Advanced Parsing - Custom Parsers

TryGet<T> has an overload that takes a nullable CommandArgParser<T>, which is a function with the following definition:

bool CustomParser<T>(string input, out T value);

If a custom parser is provided, it will be used instead of any of the built in parsing functions. Please note that, as input, if the type is not in the DoNotCleanTypes set, the function will be provided with a cleaned version of the input string.

// Somewhere
public static bool AlwaysParseIntAs32(string input, out int value)
{
    // Ignore input, always parse as 32
    value = 32;
    return true;
}

// Usage
CommandArg arg = new CommandArg("SomeInput");
Assert.IsTrue(arg.TryGet(out int value, AlwaysParseIntAs32)); // 32
Assert.IsFalse(arg.TryGet(out float invalidFloat)); // Failed to parse
Assert.IsFalse(arg.TryGet(out Color invalidColor)); // Failed to parse

If a parsing function is common enough, it can be registered via:

bool CommandArgParser.RegisterParser<T>(CommandArgParser<T> parser, bool force);

This function will return true if the parser was registered successfully. You can provide an optional force flag to override any pre-existing registered parsers for that type.

Parsers registered in this way will be used instead of any built-in parsing functions.

Parsers can be unregistered via:

bool CommandArgParser.UnregisterParser<T>();

or

bool CommandArgParser.UnregisterParser(Type parserType);

or even

int CommandArgParser.UnregisterAllParsers();

All of these unregistration functions will return you information on whether the unregistration functions were successful.

Note: Built in parser functions cannot be unregistered.

Advanced Parsing - Changing Control Sets

By default, command parameter input is stripped of whitespace characters. This, along with several other parsing-specific behaviors, are controlled via public static sets on CommandArg itself. If you would like to change this behavior in your code, you can modify the contents of these sets to be whatever you'd like. Below are a description of the sets and what they control.

Delimiters: For complex, multi-value types like Vector2, Quaternion, Color, etc, the parameter is split using the first match, if any, found in this set. ie, 1,2,3 will get split into the array ["1", "2", "3"] for parsing

Quotes: By default, parameter values are split using whitespace characters. ie, mycommand 1 2 will call mycommand with the arguments 1 and 2. Another augmentation of DxCommandTerminal is the ability to capture spaces, or any normally-ignored character as input, as long as they're surrounded by any of the characters found in the Quotes set. If there is a chunk of text that starts with a character found in the Quotes set, but has no matching terminating character, the input string from the first quote character to the end is considered as one parameter. For example, my command "1 2" will call mycommand with a single argument of "1 2".

IgnoredValuesForCleanedTypes: For cleaned types, string sequences in this set will be replaced with the empty string.

DoNotCleanTypes: A set of types for which input cleaning is not enabled.

IgnoredValuesForComplexTypes: For complex, multi-value types like Vector2, Quaternion, Color, etc, the parameter input will replace all strings found in this set with the empty string.

Hotkeys

All actions are now fully configurable by either explicit keybindings, for keyboard controls, or via new Input System bindings.

png

Keyboard hotkey bindings are now intelligent as they can be about shift key interaction. There are three ways to create a shift+<binding>:

  1. Prefix the binding with the # symbol. For example, #tab will be interpreted as shift+tab
  2. For keys with shifted versions, such as alpha numeric keys, simply use the shifted version. For example, A will be interpreted as shift+a
  3. When using the new input system, hotkeys can be represented as shift+<binding>. shift+tab will be interpreted as the combination left shift + tab.

The only combination keys that are supported without using custom bindings via the new Input System are shift+<binding>.

New Input System

DxCommandTerminal is now fully integrated with Unity's new Input System, if it is found in the project and enabled.

You can also use PlayerInput or similar to bind InputActions to all available command terminal behavior. The available message are:

  • HandlePrevious: Selects the previously issued command form current command history location.
  • HandleNext: Selects the next issued command from current command history location.
  • Close: If the terminal is open, closes it.
  • ToggleSmall: If the terminal is not opened to its small height, opens it to small height, otherwise closes it.
  • ToggleFull: If the terminal is not opened to its full height, opens it to full height, otherwise closes it.
  • CompleteCommand: Uses auto-complete to attempt to complete the current command using what is typed in the command buffer, cycling forwards through results.
  • ReverseCompleteCommand: Uses auto-complete to attempt to complete the current command using what is typed in the command buffer, but cycling backwards through results.
  • EnterCommand: Takes the current buffer and attempts to execute it as a command + parameters.

Note

If using PlayerInput to bind to the above controls, you will need to uncheck Use Hotkeys under the Hotkeys header in the Terminal script.

When InputActions are not bound, there is an order of precedence for input checking. It is:

  • Close
  • Enter Command
  • Previous
  • Next
  • Toggle Full Terminal
  • Toggle Small Terminal
  • Auto Complete (backward)
  • Auto Complete (forward)

This order is irrelevant when using PlayerInput.

Web GL

The shipped source generator emits [Preserve] onto every generated command catalog - on the catalog type and on its Collect entry method - and the catalog binds accessible command methods through direct delegate creation. The linker keeps the rooted Collect entry and everything reachable from it, so commands registered with RegisterCommandAttribute keep working through managed code stripping (IL2CPP/WebGL players) at every Managed Stripping Level.

At Medium or higher, commands reached only through reflection can still be stripped: handlers in private, non-partial types, and commands in precompiled DLLs without a generated catalog. Keep those available with [Preserve] on the methods, a link.xml entry for the assembly, or manual registration through Terminal.Shell.AddCommand.

png

See Unity docs on Managed Stripping Level for more details.

Hints

AutoComplete has gotten a major upgrade in this fork. Completion is now not only case-insensitive, but it will now also search (unique) commands that have been executed, ignoring any irrelevant input. Pressing the complete key multiple times now selects available options in a persistent fashion. Completion can be walked both forward and backwards. Results are now presented in a new UI that intelligently adapts to screen space and current selection position. When completion is no longer relevant, the UI is disabled. However, you can opt to always show the available commands by toggling the new Display Hints option in the Terminal configuration. There are also several new theming options for hints, with controls over the currently selected hint v unselected hints.

png

Note: Currently, if Make Hints Clickable is checked, there is a minor bug where, occasionally, for ~1 frame after auto-completing or deleting a character from a completed word, the text is selected and then unselected. I think it's something to do with the Layout v Repaint events, but I've tried a lot of solutions and none of them worked. Marking this as "won't fix" for now. Open to PRs!

About

Unity Command Terminal: In-Game Console

Resources

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages