From ef08ace07eead05b858bd399cbf87f35d275485e Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 12 Oct 2025 20:51:25 -0700 Subject: [PATCH 01/69] Many test/bug fixes --- AGENTS.md | 44 + AGENTS.md.meta | 7 + Editor/Parsers.meta | 8 + Editor/Parsers/ParserAutoDiscovery.cs | 23 + Editor/Parsers/ParserAutoDiscovery.cs.meta | 11 + Editor/TerminalUI.RuntimeMode.Editor.cs | 83 ++ Editor/TerminalUI.RuntimeMode.Editor.cs.meta | 11 + README.md | 83 ++ Runtime/AssemblyInfo.cs | 3 + Runtime/AssemblyInfo.cs.meta | 3 + .../Attributes/RegisterCommandAttribute.cs | 5 +- .../Backend/BuiltinCommands.cs | 77 +- Runtime/CommandTerminal/Backend/CommandArg.cs | 997 +++++++----------- .../Backend/CommandAutoComplete.cs | 46 +- Runtime/CommandTerminal/Backend/CommandLog.cs | 109 +- .../CommandTerminal/Backend/CommandShell.cs | 32 +- Runtime/CommandTerminal/Backend/Parsers.meta | 8 + .../Backend/Parsers/BoolArgParser.cs | 12 + .../Backend/Parsers/BoolArgParser.cs.meta | 11 + .../Backend/Parsers/CommandArgParserCommon.cs | 407 +++++++ .../Parsers/CommandArgParserCommon.cs.meta | 11 + .../Backend/Parsers/EnumArgParser.cs | 58 + .../Backend/Parsers/EnumArgParser.cs.meta | 11 + .../Backend/Parsers/IArgParser.cs | 29 + .../Backend/Parsers/IArgParser.cs.meta | 11 + .../Backend/Parsers/MiscArgParsers.cs | 95 ++ .../Backend/Parsers/MiscArgParsers.cs.meta | 11 + .../Backend/Parsers/NumericArgParsers.cs | 185 ++++ .../Backend/Parsers/NumericArgParsers.cs.meta | 11 + .../Backend/Parsers/StaticMemberParser.cs | 55 + .../Parsers/StaticMemberParser.cs.meta | 11 + .../Backend/Parsers/UnityArgParsers.cs | 915 ++++++++++++++++ .../Backend/Parsers/UnityArgParsers.cs.meta | 11 + Runtime/CommandTerminal/Backend/Terminal.cs | 3 +- .../Backend/TerminalRuntimeConfig.cs | 66 ++ .../Backend/TerminalRuntimeConfig.cs.meta | 11 + Runtime/CommandTerminal/UI/TerminalUI.cs | 65 +- Runtime/DataStructures/CyclicBuffer.cs | 10 +- .../SerializedPropertyExtensions.cs | 2 +- Runtime/Helper/DirectoryHelper.cs | 20 +- Runtime/Utils.meta | 8 + Runtime/Utils/DxArrayPools.cs | 119 +++ Runtime/Utils/DxArrayPools.cs.meta | 11 + Tests/Runtime/ArrayPoolTests.cs | 125 +++ Tests/Runtime/ArrayPoolTests.cs.meta | 11 + Tests/Runtime/CommandShellEscapingTests.cs | 64 ++ .../Runtime/CommandShellEscapingTests.cs.meta | 11 + Tests/Runtime/CommandShellTests.cs | 5 +- Tests/Runtime/Components/TestPacks.cs | 24 + Tests/Runtime/Components/TestPacks.cs.meta | 11 + Tests/Runtime/Components/TestSceneHelpers.cs | 39 + .../Components/TestSceneHelpers.cs.meta | 11 + Tests/Runtime/CultureParsingTests.cs | 70 ++ Tests/Runtime/CultureParsingTests.cs.meta | 11 + Tests/Runtime/LoggingThreadSafetyTests.cs | 70 ++ .../Runtime/LoggingThreadSafetyTests.cs.meta | 11 + Tests/Runtime/Parsers.meta | 8 + Tests/Runtime/Parsers/BoolArgParserTests.cs | 23 + .../Parsers/BoolArgParserTests.cs.meta | 11 + Tests/Runtime/Parsers/EnumArgParserTests.cs | 39 + .../Parsers/EnumArgParserTests.cs.meta | 11 + Tests/Runtime/Parsers/MiscArgParsersTests.cs | 39 + .../Parsers/MiscArgParsersTests.cs.meta | 11 + .../Runtime/Parsers/NumericArgParsersTests.cs | 38 + .../Parsers/NumericArgParsersTests.cs.meta | 11 + .../Parsers/ObjectParserRegistryTests.cs | 46 + .../Parsers/ObjectParserRegistryTests.cs.meta | 11 + Tests/Runtime/Parsers/ParserDiscoveryTests.cs | 27 + .../Parsers/ParserDiscoveryTests.cs.meta | 11 + Tests/Runtime/Parsers/RuntimeConfigTests.cs | 43 + .../Parsers/RuntimeConfigTests.cs.meta | 11 + .../Parsers/StaticMemberParserTests.cs | 28 + .../Parsers/StaticMemberParserTests.cs.meta | 11 + Tests/Runtime/Parsers/UnityArgParsersTests.cs | 30 + .../Parsers/UnityArgParsersTests.cs.meta | 11 + Tests/Runtime/TerminalTests.cs | 68 +- Tests/Runtime/UnityBoundaryMalformedTests.cs | 62 ++ .../UnityBoundaryMalformedTests.cs.meta | 11 + Tests/Runtime/UnityDelimiterParsingTests.cs | 130 +++ .../UnityDelimiterParsingTests.cs.meta | 11 + Tests/Runtime/UnityExtremeParsingTests.cs | 110 ++ .../Runtime/UnityExtremeParsingTests.cs.meta | 11 + .../UnityLabelPermutationSuccessTests.cs | 59 ++ .../UnityLabelPermutationSuccessTests.cs.meta | 11 + Tests/Runtime/UnityLabeledParsingTests.cs | 50 + .../Runtime/UnityLabeledParsingTests.cs.meta | 11 + Tests/Runtime/UnityMalformedParsingTests.cs | 175 +++ .../UnityMalformedParsingTests.cs.meta | 11 + .../Runtime/UnityQuotedWrapperParsingTests.cs | 30 + .../UnityQuotedWrapperParsingTests.cs.meta | 11 + .../UntypedUnityParsingFailureTests.cs | 62 ++ .../UntypedUnityParsingFailureTests.cs.meta | 11 + Tests/Runtime/UntypedUnityParsingTests.cs | 77 ++ .../Runtime/UntypedUnityParsingTests.cs.meta | 11 + Tests/Runtime/UsageHelpTests.cs | 57 + Tests/Runtime/UsageHelpTests.cs.meta | 11 + Tests/Runtime/VectorParsingTests.cs | 36 + Tests/Runtime/VectorParsingTests.cs.meta | 11 + 98 files changed, 4926 insertions(+), 703 deletions(-) create mode 100644 AGENTS.md create mode 100644 AGENTS.md.meta create mode 100644 Editor/Parsers.meta create mode 100644 Editor/Parsers/ParserAutoDiscovery.cs create mode 100644 Editor/Parsers/ParserAutoDiscovery.cs.meta create mode 100644 Editor/TerminalUI.RuntimeMode.Editor.cs create mode 100644 Editor/TerminalUI.RuntimeMode.Editor.cs.meta create mode 100644 Runtime/AssemblyInfo.cs create mode 100644 Runtime/AssemblyInfo.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs create mode 100644 Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs.meta create mode 100644 Runtime/Utils.meta create mode 100644 Runtime/Utils/DxArrayPools.cs create mode 100644 Runtime/Utils/DxArrayPools.cs.meta create mode 100644 Tests/Runtime/ArrayPoolTests.cs create mode 100644 Tests/Runtime/ArrayPoolTests.cs.meta create mode 100644 Tests/Runtime/CommandShellEscapingTests.cs create mode 100644 Tests/Runtime/CommandShellEscapingTests.cs.meta create mode 100644 Tests/Runtime/Components/TestPacks.cs create mode 100644 Tests/Runtime/Components/TestPacks.cs.meta create mode 100644 Tests/Runtime/Components/TestSceneHelpers.cs create mode 100644 Tests/Runtime/Components/TestSceneHelpers.cs.meta create mode 100644 Tests/Runtime/CultureParsingTests.cs create mode 100644 Tests/Runtime/CultureParsingTests.cs.meta create mode 100644 Tests/Runtime/LoggingThreadSafetyTests.cs create mode 100644 Tests/Runtime/LoggingThreadSafetyTests.cs.meta create mode 100644 Tests/Runtime/Parsers.meta create mode 100644 Tests/Runtime/Parsers/BoolArgParserTests.cs create mode 100644 Tests/Runtime/Parsers/BoolArgParserTests.cs.meta create mode 100644 Tests/Runtime/Parsers/EnumArgParserTests.cs create mode 100644 Tests/Runtime/Parsers/EnumArgParserTests.cs.meta create mode 100644 Tests/Runtime/Parsers/MiscArgParsersTests.cs create mode 100644 Tests/Runtime/Parsers/MiscArgParsersTests.cs.meta create mode 100644 Tests/Runtime/Parsers/NumericArgParsersTests.cs create mode 100644 Tests/Runtime/Parsers/NumericArgParsersTests.cs.meta create mode 100644 Tests/Runtime/Parsers/ObjectParserRegistryTests.cs create mode 100644 Tests/Runtime/Parsers/ObjectParserRegistryTests.cs.meta create mode 100644 Tests/Runtime/Parsers/ParserDiscoveryTests.cs create mode 100644 Tests/Runtime/Parsers/ParserDiscoveryTests.cs.meta create mode 100644 Tests/Runtime/Parsers/RuntimeConfigTests.cs create mode 100644 Tests/Runtime/Parsers/RuntimeConfigTests.cs.meta create mode 100644 Tests/Runtime/Parsers/StaticMemberParserTests.cs create mode 100644 Tests/Runtime/Parsers/StaticMemberParserTests.cs.meta create mode 100644 Tests/Runtime/Parsers/UnityArgParsersTests.cs create mode 100644 Tests/Runtime/Parsers/UnityArgParsersTests.cs.meta create mode 100644 Tests/Runtime/UnityBoundaryMalformedTests.cs create mode 100644 Tests/Runtime/UnityBoundaryMalformedTests.cs.meta create mode 100644 Tests/Runtime/UnityDelimiterParsingTests.cs create mode 100644 Tests/Runtime/UnityDelimiterParsingTests.cs.meta create mode 100644 Tests/Runtime/UnityExtremeParsingTests.cs create mode 100644 Tests/Runtime/UnityExtremeParsingTests.cs.meta create mode 100644 Tests/Runtime/UnityLabelPermutationSuccessTests.cs create mode 100644 Tests/Runtime/UnityLabelPermutationSuccessTests.cs.meta create mode 100644 Tests/Runtime/UnityLabeledParsingTests.cs create mode 100644 Tests/Runtime/UnityLabeledParsingTests.cs.meta create mode 100644 Tests/Runtime/UnityMalformedParsingTests.cs create mode 100644 Tests/Runtime/UnityMalformedParsingTests.cs.meta create mode 100644 Tests/Runtime/UnityQuotedWrapperParsingTests.cs create mode 100644 Tests/Runtime/UnityQuotedWrapperParsingTests.cs.meta create mode 100644 Tests/Runtime/UntypedUnityParsingFailureTests.cs create mode 100644 Tests/Runtime/UntypedUnityParsingFailureTests.cs.meta create mode 100644 Tests/Runtime/UntypedUnityParsingTests.cs create mode 100644 Tests/Runtime/UntypedUnityParsingTests.cs.meta create mode 100644 Tests/Runtime/UsageHelpTests.cs create mode 100644 Tests/Runtime/UsageHelpTests.cs.meta create mode 100644 Tests/Runtime/VectorParsingTests.cs create mode 100644 Tests/Runtime/VectorParsingTests.cs.meta diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4ef91b6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `Runtime/` — core C# code (`WallstopStudios.DxCommandTerminal.asmdef`). +- `Editor/` — editor tooling and drawers (`*.Editor.asmdef`). +- `Tests/Runtime/` — Unity Test Framework (NUnit) tests (`*Tests.cs`, `*.asmdef`). +- `Styles/` — UI Toolkit styles (`.uss`) and theme assets (`.tss`). +- `Fonts/`, `Media/`, `Packs/` — assets used by the terminal. +- `package.json` — Unity package manifest + npm metadata. See `README.md` for usage. + +## Build, Test, and Development Commands +- Install tools: `dotnet tool restore` +- Format C#: `dotnet tool run csharpier .` +- Optional hooks: install pre-commit — `pre-commit install` +- Unity tests (CLI example, Windows): + `"C:\\Program Files\\Unity\\Hub\\Editor\\2021.3.x\\Editor\\Unity.exe" -batchmode -projectPath -runTests -testPlatform playmode -assemblyNames WallstopStudios.DxCommandTerminal.Tests.Runtime -logfile - -quit` + Or use Unity’s Test Runner UI. + +## Coding Style & Naming Conventions +- Follow `.editorconfig`: + - Indentation: spaces (C# 4), JSON/YAML/asmdef 2. + - Line endings: CRLF; encoding: UTF-8 BOM. + - C#: prefer braces; explicit types over `var` unless obvious; `using` inside namespace. + - Naming: Interfaces `IType`, type params `TType`, events/types/methods PascalCase; tests end with `Tests`. +- Use CSharpier for formatting before committing. +- Do not use underscores in function names, especially test function names. +- Do not use regions, anywhere, ever. + +## Testing Guidelines +- Framework: Unity Test Framework (NUnit) under `Tests/Runtime`. +- Conventions: file names `*Tests.cs`, one feature per fixture, deterministic tests. +- Run via Unity CLI (above) or Test Runner. Add/adjust tests when changing parsing, history, UI behavior, or input handling. +- Do not use regions. +- Try to use minimal comments and instead rely on expressive naming conventions and assertions. +- Do not use Description annotations for tests. + +## Commit & Pull Request Guidelines +- Commits: imperative, concise subject (≤72 chars), explain “what/why”. Link issues/PRs (`#123`). +- PRs: include description, motivation, and test coverage. For UI/USS changes, add before/after screenshots. +- Versioning/CI: do not change `package.json` version unless preparing a release; npm publishing is automated via GitHub Actions on version bumps. + +## Security & Configuration Tips +- No secrets in repo; publishing uses `NPM_TOKEN` in GitHub secrets. +- Target Unity `2021.3+`. Keep `asmdef` names and folder layout intact to preserve assembly boundaries. diff --git a/AGENTS.md.meta b/AGENTS.md.meta new file mode 100644 index 0000000..494b049 --- /dev/null +++ b/AGENTS.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 32fc9ec590fb00f42b06b82b7954c868 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Parsers.meta b/Editor/Parsers.meta new file mode 100644 index 0000000..41710cf --- /dev/null +++ b/Editor/Parsers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ea07f328d83288f468eb2a0b261abe34 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Parsers/ParserAutoDiscovery.cs b/Editor/Parsers/ParserAutoDiscovery.cs new file mode 100644 index 0000000..f8c3b63 --- /dev/null +++ b/Editor/Parsers/ParserAutoDiscovery.cs @@ -0,0 +1,23 @@ +namespace WallstopStudios.DxCommandTerminal.Editor +{ +#if UNITY_EDITOR + using UnityEditor; + using WallstopStudios.DxCommandTerminal.Backend; + + [InitializeOnLoad] + internal static class ParserAutoDiscovery + { + static ParserAutoDiscovery() + { + // Editor convenience: allow auto-discovery via config flags + if ( + Backend.TerminalRuntimeConfig.ShouldEnableEditorFeatures() + && Backend.TerminalRuntimeConfig.EditorAutoDiscover + ) + { + CommandArg.DiscoverAndRegisterParsers(replaceExisting: false); + } + } + } +#endif +} diff --git a/Editor/Parsers/ParserAutoDiscovery.cs.meta b/Editor/Parsers/ParserAutoDiscovery.cs.meta new file mode 100644 index 0000000..d3b4921 --- /dev/null +++ b/Editor/Parsers/ParserAutoDiscovery.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 865175dec92d20c4d98ce50059e70caf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/TerminalUI.RuntimeMode.Editor.cs b/Editor/TerminalUI.RuntimeMode.Editor.cs new file mode 100644 index 0000000..172f4e4 --- /dev/null +++ b/Editor/TerminalUI.RuntimeMode.Editor.cs @@ -0,0 +1,83 @@ +namespace WallstopStudios.DxCommandTerminal.Editor +{ +#if UNITY_EDITOR + using Backend; + using UI; + using UnityEditor; + using UnityEngine; + + internal static class TerminalUIRuntimeModeMenu + { + private const string MenuRoot = "Tools/DxCommandTerminal/Runtime Mode/"; + + [MenuItem(MenuRoot + "Editor", false, 0)] + private static void SetEditorMode() + { + SetSelectedRuntimeMode(TerminalRuntimeModeFlags.Editor); + } + + [MenuItem(MenuRoot + "Development", false, 1)] + private static void SetDevelopmentMode() + { + SetSelectedRuntimeMode(TerminalRuntimeModeFlags.Development); + } + + [MenuItem(MenuRoot + "Production", false, 2)] + private static void SetProductionMode() + { + SetSelectedRuntimeMode(TerminalRuntimeModeFlags.Production); + } + + [MenuItem(MenuRoot + "Editor+Development", false, 10)] + private static void SetEditorDevMode() + { + SetSelectedRuntimeMode( + TerminalRuntimeModeFlags.Editor | TerminalRuntimeModeFlags.Development + ); + } + + [MenuItem(MenuRoot + "All", false, 11)] + private static void SetAllMode() + { + SetSelectedRuntimeMode(TerminalRuntimeModeFlags.All); + } + + [MenuItem(MenuRoot + "Toggle Auto-Discover Parsers", false, 50)] + private static void ToggleAutoDiscover() + { + foreach (var obj in Selection.objects) + { + if (obj is GameObject go && go.TryGetComponent(out var ui)) + { + var so = new SerializedObject(ui); + var prop = so.FindProperty("_autoDiscoverParsersInEditor"); + if (prop != null) + { + prop.boolValue = !prop.boolValue; + so.ApplyModifiedProperties(); + EditorUtility.SetDirty(ui); + } + } + } + } + + private static void SetSelectedRuntimeMode(TerminalRuntimeModeFlags mode) + { + foreach (var obj in Selection.objects) + { + if (obj is GameObject go && go.TryGetComponent(out var ui)) + { + var so = new SerializedObject(ui); + var prop = so.FindProperty("_runtimeModes"); + if (prop != null) + { + prop.intValue = (int)mode; + so.ApplyModifiedProperties(); + EditorUtility.SetDirty(ui); + } + } + } + } + } +#endif +} diff --git a/Editor/TerminalUI.RuntimeMode.Editor.cs.meta b/Editor/TerminalUI.RuntimeMode.Editor.cs.meta new file mode 100644 index 0000000..2d59f1b --- /dev/null +++ b/Editor/TerminalUI.RuntimeMode.Editor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d3326e2b0b6d5e468717da0295e2692 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index f909d9c..21d4f48 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,89 @@ All of these unregistration functions will return you information on whether the Note: Built in parser functions cannot be unregistered. +### Object Parsers (IArgParser) +For stronger typing and lower allocations, you can also use object parsers that implement `IArgParser`. + +- Built-ins: All common numeric, date/time, IP, and Unity types ship with sealed parsers and public singletons, e.g. `IntArgParser.Instance`, `Vector3ArgParser.Instance`, `ColorArgParser.Instance`. +- Registration: Register any custom parser once at startup. + +```csharp +using WallstopStudios.DxCommandTerminal.Backend.Parsers; + +// Register built-in or custom parsers +CommandArg.RegisterObjectParser(IntArgParser.Instance); // int +CommandArg.RegisterObjectParser(Vector3ArgParser.Instance); // UnityEngine.Vector3 + +// Or discover all IArgParser implementations across loaded assemblies +int discovered = CommandArg.DiscoverAndRegisterParsers(replaceExisting: false); +``` + +Create your own low-allocation parser by deriving from `ArgParser`: + +```csharp +public sealed class PathArgParser : ArgParser +{ + public static readonly PathArgParser Instance = new PathArgParser(); + protected override bool TryParseTyped(string input, out System.IO.FileInfo value) + { + if (string.IsNullOrWhiteSpace(input)) { value = null; return false; } + value = new System.IO.FileInfo(input.Trim()); + return true; + } +} + +// Register once (e.g., game init) +CommandArg.RegisterObjectParser(PathArgParser.Instance); + +// Usage in a command +public static void LoadFile(CommandArg a) +{ + if (!a.TryGet(out System.IO.FileInfo file)) { Terminal.LogError("Invalid path"); return; } + // ... +} +``` + +Enum parsing uses a hot path with cached name and ordinal lookups (`EnumArgParser`) for fast, case-insensitive matching and integer ordinals. + +Editor convenience +- In the Unity Editor, parsers are auto-discovered on domain reload to ease development (`Editor/Parsers/ParserAutoDiscovery.cs`). This does not affect players. +- Inspect what parsers are currently registered: + +```csharp +var types = CommandArg.GetRegisteredObjectParserTypes(); +foreach (var t in types) UnityEngine.Debug.Log($"Parser for type: {t}"); +``` + +Static members parsing +- Constant/Property name parsing (e.g., `MaxValue`, `IPAddress.Any`) is handled by dedicated static-member parsers and no longer lives inside `CommandArg`. + +## Runtime Modes & Editor Toggle + +DxCommandTerminal exposes a runtime mode enum to control environment-specific behavior (e.g., parser auto-discovery), plus an Editor toggle wired through the Terminal component. + +- Enum: `TerminalRuntimeModeFlags` (flags, explicit numeric values) + - `None` (0) — Obsolete; choose explicit modes. + - `Editor` (1) — Enable editor-only features (when running in Editor). + - `Development` (2) — Enable features only for development builds. + - `Production` (4) — Enable features only for non-development builds. + - `All` (7) — Enable all. + +- Set mode on `TerminalUI` (serialized): + - `Runtime Mode` controls active modes. + - `Editor > Auto-Discover Parsers` toggles automatic parser discovery in Editor when `Editor` mode is active. + +- Editor Menu: + - Tools > DxCommandTerminal > Runtime Mode > [Editor | Development | Production | Editor+Development | All] + - Tools > DxCommandTerminal > Runtime Mode > Toggle Auto-Discover Parsers + - Acts on selected `TerminalUI` components (in the Hierarchy). + +- Programmatic checks (no allocations): + - `TerminalRuntimeConfig.HasFlagNoAlloc(value, flag)` bit-tests without boxing. + - `TerminalRuntimeConfig.ShouldEnableEditorFeatures()` respects `Editor` flag and `UNITY_EDITOR`. + - `TerminalRuntimeConfig.ShouldEnableDevelopmentFeatures()` respects `Development` and `Debug.isDebugBuild`. + - `TerminalRuntimeConfig.ShouldEnableProductionFeatures()` respects `Production` and non-development builds. + - `TerminalRuntimeConfig.TryAutoDiscoverParsers()` conditionally registers all IArgParser implementations based on mode + toggle. + ## 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. diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..f0bc465 --- /dev/null +++ b/Runtime/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests.Runtime")] diff --git a/Runtime/AssemblyInfo.cs.meta b/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..feae1a0 --- /dev/null +++ b/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4cbbf6127c01481d9ba98e9bad59e641 +timeCreated: 1760315470 \ No newline at end of file diff --git a/Runtime/Attributes/RegisterCommandAttribute.cs b/Runtime/Attributes/RegisterCommandAttribute.cs index e97841b..b9ace52 100644 --- a/Runtime/Attributes/RegisterCommandAttribute.cs +++ b/Runtime/Attributes/RegisterCommandAttribute.cs @@ -26,7 +26,10 @@ public RegisterCommandAttribute(string commandName = null) } internal RegisterCommandAttribute(bool isDefault) - : this(string.Empty) { } + : this(string.Empty) + { + Default = isDefault; + } public void NormalizeName(MethodInfo method) { diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index a246852..2ce92a0 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -292,31 +292,86 @@ public static void CommandHelp(CommandArg[] args) { foreach (KeyValuePair command in shell.Commands) { - Terminal.Log($"{command.Key.ToUpperInvariant(), -16}: {command.Value.help}"); + string name = command.Key; + CommandInfo info = command.Value; + string usage = BuildUsage(name, info.minArgCount, info.maxArgCount, info.hint); + string helpLine = + $"{name.ToUpperInvariant(), -16}: {info.help}\n -> {usage}"; + Terminal.Log(helpLine); + UnityEngine.Debug.Log(helpLine); } return; } + else + { + string commandName = args[0].contents ?? string.Empty; - string commandName = args[0].contents ?? string.Empty; + if (!shell.Commands.TryGetValue(commandName, out CommandInfo info)) + { + shell.IssueErrorMessage($"Command {commandName} could not be found."); + return; + } - if (!shell.Commands.TryGetValue(commandName, out CommandInfo info)) + string usageLine = BuildUsage( + commandName, + info.minArgCount, + info.maxArgCount, + info.hint + ); + if (string.IsNullOrWhiteSpace(info.help)) + { + string line = $"{commandName}: {usageLine}"; + Terminal.Log(line); + UnityEngine.Debug.Log(line); + } + else + { + string line = $"{info.help}\n{usageLine}"; + Terminal.Log(line); + UnityEngine.Debug.Log(line); + } + } + } + + private static string BuildUsage(string name, int minArgs, int maxArgs, string hint) + { + if (!string.IsNullOrWhiteSpace(hint)) { - shell.IssueErrorMessage($"Command {commandName} could not be found."); - return; + return $"Usage: {hint}"; + } + + StringBuilder.Clear(); + StringBuilder.Append("Usage: "); + StringBuilder.Append(name); + if (minArgs <= 0 && (maxArgs == 0 || maxArgs < 0)) + { + return StringBuilder.ToString(); } - if (string.IsNullOrWhiteSpace(info.help)) + int max = maxArgs < 0 ? minArgs : Math.Max(minArgs, maxArgs); + for (int i = 1; i <= minArgs; ++i) { - Terminal.Log($"{commandName} does not provide any help documentation."); + StringBuilder.Append(' '); + StringBuilder.Append('<'); + StringBuilder.Append("arg"); + StringBuilder.Append(i); + StringBuilder.Append('>'); } - else if (string.IsNullOrWhiteSpace(info.hint)) + for (int i = minArgs + 1; i <= max; ++i) { - Terminal.Log(info.help); + StringBuilder.Append(' '); + StringBuilder.Append('['); + StringBuilder.Append("arg"); + StringBuilder.Append(i); + StringBuilder.Append(']'); } - else + if (maxArgs < 0) { - Terminal.Log($"{info.help}\nUsage: {info.hint}"); + StringBuilder.Append(' '); + StringBuilder.Append("[args...]"); } + + return StringBuilder.ToString(); } [RegisterCommand( diff --git a/Runtime/CommandTerminal/Backend/CommandArg.cs b/Runtime/CommandTerminal/Backend/CommandArg.cs index eef9d5b..c2e9134 100644 --- a/Runtime/CommandTerminal/Backend/CommandArg.cs +++ b/Runtime/CommandTerminal/Backend/CommandArg.cs @@ -1,629 +1,368 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Numerics; - using System.Reflection; - using UnityEngine; - using Quaternion = UnityEngine.Quaternion; - using Vector2 = UnityEngine.Vector2; - using Vector3 = UnityEngine.Vector3; - using Vector4 = UnityEngine.Vector4; - - public delegate bool CommandArgParser(string input, out T parsed); - - public readonly struct CommandArg - { - private static readonly Lazy TryGetMethod = new(() => - typeof(CommandArg) - .GetMethods(BindingFlags.Instance | BindingFlags.Public) - .Where(method => method.Name == nameof(TryGet)) - .FirstOrDefault(method => method.GetParameters().Length == 1) - ); - private static readonly Dictionary RegisteredParsers = new(); - private static readonly Dictionary< - Type, - Dictionary - > StaticProperties = new(); - private static readonly Dictionary> ConstFields = new(); - private static readonly Dictionary EnumValues = new(); - - // Public to allow custom-mutation, if desired - public static readonly HashSet Delimiters = new() { ',', ';', ':', '_', '/', '\\' }; - public static readonly List Quotes = new() { '"', '\'' }; - public static readonly HashSet IgnoredValuesForCleanedTypes = new() { "\r", "\n" }; - public static readonly HashSet DoNotCleanTypes = new() - { - typeof(string), - typeof(char), - typeof(DateTime), - typeof(DateTimeOffset), - }; - public static readonly HashSet IgnoredValuesForComplexTypes = new() - { - "(", - ")", - "[", - "]", - "'", - "`", - "|", - "{", - "}", - "<", - ">", - }; - - public readonly string contents; - public readonly char? startQuote; - public readonly char? endQuote; - - public string CleanedContents - { - get - { - string cleanedString = contents; - cleanedString = IgnoredValuesForCleanedTypes.Aggregate( - cleanedString, - (current, ignoredValue) => - current.Replace( - ignoredValue, - string.Empty, - StringComparison.OrdinalIgnoreCase - ) - ); - return cleanedString; - } - } - - public bool TryGet(Type type, out object parsed) - { - // TODO: Convert into delegates and cache for performance - MethodInfo genericMethod = TryGetMethod.Value; - if (genericMethod == null) - { - parsed = default; - return false; - } - - MethodInfo constructed = genericMethod.MakeGenericMethod(type); - object[] parameters = { null }; - bool success = (bool)constructed.Invoke(this, parameters); - parsed = parameters[0]; - return success; - } - - public bool TryGet(out T parsed) - { - return TryGet(out parsed, parserOverride: null); - } - - public bool TryGet(out T parsed, CommandArgParser parserOverride) - { - Type type = typeof(T); - string stringValue = DoNotCleanTypes.Contains(type) ? contents : CleanedContents; - - if (parserOverride != null) - { - return parserOverride(stringValue, out parsed); - } - - if (TryGetParser(out CommandArgParser parser)) - { - return parser(stringValue, out parsed); - } - - if (type == typeof(string)) - { - parsed = (T)Convert.ChangeType(stringValue, type); - return true; - } - if (TryGetTypeDefined(stringValue, out parsed)) - { - return true; - } - - // TODO: Slap into a dictionary of built-in type -> parser mapping - if (type == typeof(bool)) - { - return InnerParse(stringValue, bool.TryParse, out parsed); - } - if (type == typeof(float)) - { - return InnerParse(stringValue, float.TryParse, out parsed); - } - if (type == typeof(int)) - { - return InnerParse(stringValue, int.TryParse, out parsed); - } - if (type == typeof(uint)) - { - return InnerParse(stringValue, uint.TryParse, out parsed); - } - if (type == typeof(long)) - { - return InnerParse(stringValue, long.TryParse, out parsed); - } - if (type == typeof(ulong)) - { - return InnerParse(stringValue, ulong.TryParse, out parsed); - } - if (type == typeof(double)) - { - return InnerParse(stringValue, double.TryParse, out parsed); - } - if (type == typeof(short)) - { - return InnerParse(stringValue, short.TryParse, out parsed); - } - if (type == typeof(ushort)) - { - return InnerParse(stringValue, ushort.TryParse, out parsed); - } - if (type == typeof(byte)) - { - return InnerParse(stringValue, byte.TryParse, out parsed); - } - if (type == typeof(sbyte)) - { - return InnerParse(stringValue, sbyte.TryParse, out parsed); - } - if (type == typeof(Guid)) - { - return InnerParse(stringValue, Guid.TryParse, out parsed); - } - if (type == typeof(DateTime)) - { - return InnerParse(stringValue, DateTime.TryParse, out parsed); - } - if (type == typeof(DateTimeOffset)) - { - return InnerParse(stringValue, DateTimeOffset.TryParse, out parsed); - } - if (type == typeof(char)) - { - return InnerParse(stringValue, char.TryParse, out parsed); - } - if (type == typeof(decimal)) - { - return InnerParse(stringValue, decimal.TryParse, out parsed); - } - if (type == typeof(BigInteger)) - { - return InnerParse(stringValue, BigInteger.TryParse, out parsed); - } - if (type == typeof(TimeSpan)) - { - return InnerParse(stringValue, TimeSpan.TryParse, out parsed); - } - if (type == typeof(Version)) - { - return InnerParse(stringValue, Version.TryParse, out parsed); - } - if (type == typeof(IPAddress)) - { - return InnerParse(stringValue, IPAddress.TryParse, out parsed); - } - if (type.IsEnum) - { - if (Enum.IsDefined(type, stringValue)) - { - bool parseOk = Enum.TryParse(type, stringValue, out object parsedObject); - if (parseOk) - { - parsed = (T)Convert.ChangeType(parsedObject, type); - return true; - } - } - - if (int.TryParse(stringValue, out int enumIntValue)) - { - if (!EnumValues.TryGetValue(type, out object enumValues)) - { - enumValues = Enum.GetValues(type).OfType().ToArray(); - EnumValues[type] = enumValues; - } - - T[] values = (T[])enumValues; - if (0 <= enumIntValue && enumIntValue < values.Length) - { - parsed = values[enumIntValue]; - return true; - } - } - } - if (type == typeof(Vector2)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 2 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y): - parsed = (T)Convert.ChangeType(new Vector2(x, y), type); - return true; - case 3 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y) - && float.TryParse(split[2], out float z): - parsed = (T)Convert.ChangeType((Vector2)new Vector3(x, y, z), type); - return true; - } - } - else if (type == typeof(Vector3)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 2 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y): - parsed = (T)Convert.ChangeType(new Vector3(x, y), type); - return true; - case 3 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y) - && float.TryParse(split[2], out float z): - parsed = (T)Convert.ChangeType(new Vector3(x, y, z), type); - return true; - } - } - else if (type == typeof(Vector4)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 2 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y): - parsed = (T)Convert.ChangeType(new Vector4(x, y), type); - return true; - case 3 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y) - && float.TryParse(split[2], out float z): - parsed = (T)Convert.ChangeType(new Vector4(x, y, z), type); - return true; - case 4 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y) - && float.TryParse(split[2], out float z) - && float.TryParse(split[3], out float w): - parsed = (T)Convert.ChangeType(new Vector4(x, y, z, w), type); - return true; - } - } - else if (type == typeof(Vector2Int)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 2 - when int.TryParse(split[0], out int x) && int.TryParse(split[1], out int y): - parsed = (T)Convert.ChangeType(new Vector2Int(x, y), type); - return true; - case 3 - when int.TryParse(split[0], out int x) - && int.TryParse(split[1], out int y) - && int.TryParse(split[2], out int z): - parsed = (T)Convert.ChangeType((Vector2Int)new Vector3Int(x, y, z), type); - return true; - } - } - else if (type == typeof(Vector3Int)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 2 - when int.TryParse(split[0], out int x) && int.TryParse(split[1], out int y): - parsed = (T)Convert.ChangeType(new Vector3Int(x, y), type); - return true; - case 3 - when int.TryParse(split[0], out int x) - && int.TryParse(split[1], out int y) - && int.TryParse(split[2], out int z): - parsed = (T)Convert.ChangeType(new Vector3Int(x, y, z), type); - return true; - } - } - else if (type == typeof(Color)) - { - string colorString = stringValue; - if (colorString.StartsWith("RGBA", StringComparison.OrdinalIgnoreCase)) - { - colorString = colorString.Replace( - "RGBA", - string.Empty, - StringComparison.OrdinalIgnoreCase - ); - } - - string[] split = StripAndSplit(colorString); - switch (split.Length) - { - case 3 - when float.TryParse(split[0], out float r) - && float.TryParse(split[1], out float g) - && float.TryParse(split[2], out float b): - parsed = (T)Convert.ChangeType(new Color(r, g, b), type); - return true; - case 4 - when float.TryParse(split[0], out float r) - && float.TryParse(split[1], out float g) - && float.TryParse(split[2], out float b) - && float.TryParse(split[3], out float a): - parsed = (T)Convert.ChangeType(new Color(r, g, b, a), type); - return true; - } - } - else if (type == typeof(Quaternion)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 4 - when float.TryParse(split[0], out float x) - && float.TryParse(split[1], out float y) - && float.TryParse(split[2], out float z) - && float.TryParse(split[3], out float w): - parsed = (T)Convert.ChangeType(new Quaternion(x, y, z, w), type); - return true; - } - } - else if (type == typeof(Rect)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 4 - when float.TryParse( - split[0] - .Replace("x:", string.Empty, StringComparison.OrdinalIgnoreCase), - out float x - ) - && float.TryParse( - split[1] - .Replace( - "y:", - string.Empty, - StringComparison.OrdinalIgnoreCase - ), - out float y - ) - && float.TryParse( - split[2] - .Replace( - "width:", - string.Empty, - StringComparison.OrdinalIgnoreCase - ), - out float width - ) - && float.TryParse( - split[3] - .Replace( - "height:", - string.Empty, - StringComparison.OrdinalIgnoreCase - ), - out float height - ): - parsed = (T)Convert.ChangeType(new Rect(x, y, width, height), type); - return true; - } - } - else if (type == typeof(RectInt)) - { - string[] split = StripAndSplit(stringValue); - switch (split.Length) - { - case 4 - when int.TryParse( - split[0] - .Replace("x:", string.Empty, StringComparison.OrdinalIgnoreCase), - out int x - ) - && int.TryParse( - split[1] - .Replace( - "y:", - string.Empty, - StringComparison.OrdinalIgnoreCase - ), - out int y - ) - && int.TryParse( - split[2] - .Replace( - "width:", - string.Empty, - StringComparison.OrdinalIgnoreCase - ), - out int width - ) - && int.TryParse( - split[3] - .Replace( - "height:", - string.Empty, - StringComparison.OrdinalIgnoreCase - ), - out int height - ): - parsed = (T)Convert.ChangeType(new RectInt(x, y, width, height), type); - return true; - } - } - - parsed = default; - return false; - - static bool InnerParse( - string input, - CommandArgParser typedParser, - out T parsed - ) - { - bool parseOk = typedParser(input, out TParsed value); - if (parseOk) - { - parsed = (T)Convert.ChangeType(value, typeof(T)); - } - else - { - parsed = default; - } - - return parseOk; - } - - static string[] StripAndSplit(string input) - { - string strippedInput = IgnoredValuesForComplexTypes - .Where(ignored => !string.IsNullOrEmpty(ignored)) - .Aggregate( - input, - (current, ignored) => - current.Replace( - ignored, - string.Empty, - StringComparison.OrdinalIgnoreCase - ) - ); - - foreach (char delimiter in Delimiters) - { - if (strippedInput.Contains(delimiter)) - { - return strippedInput.Split(delimiter); - } - } - - return new[] { strippedInput }; - } - - static bool TryGetTypeDefined(string input, out T value) - { - Type type = typeof(T); - if ( - !StaticProperties.TryGetValue( - type, - out Dictionary properties - ) - ) - { - properties = LoadStaticPropertiesForType(); - StaticProperties[type] = properties; - } - - if (properties.TryGetValue(input, out PropertyInfo property)) - { - object resolved = property.GetValue(null); - value = (T)Convert.ChangeType(resolved, type); - return true; - } - - if (!ConstFields.TryGetValue(type, out Dictionary fields)) - { - fields = LoadStaticFieldsForType(); - ConstFields[type] = fields; - } - - if (fields.TryGetValue(input, out FieldInfo field)) - { - object resolved = field.GetValue(null); - value = (T)Convert.ChangeType(resolved, type); - return true; - } - - value = default; - return false; - } - } - - public CommandArg(string contents, char? startQuote = null, char? endQuote = null) - { - this.contents = contents ?? string.Empty; - this.startQuote = startQuote; - this.endQuote = endQuote; - } - - public static bool RegisterParser(CommandArgParser parser, bool force = false) - { - if (parser == null) - { - return false; - } - - Type type = typeof(T); - if (force) - { - RegisteredParsers[type] = parser; - return true; - } - - return RegisteredParsers.TryAdd(type, parser); - } - - public static bool TryGetParser(out CommandArgParser parser) - { - if (RegisteredParsers.TryGetValue(typeof(T), out object untypedParser)) - { - parser = (CommandArgParser)untypedParser; - return true; - } - - parser = null; - return false; - } - - public static bool UnregisterParser() - { - return UnregisterParser(typeof(T)); - } - - public static bool UnregisterParser(Type type) - { - return RegisteredParsers.Remove(type); - } - - public static int UnregisterAllParsers() - { - int parserCount = RegisteredParsers.Count; - RegisteredParsers.Clear(); - return parserCount; - } - - private static Dictionary LoadStaticPropertiesForType() - { - Type type = typeof(T); - return type.GetProperties(BindingFlags.Static | BindingFlags.Public) - .Where(property => property.PropertyType == type) - .ToDictionary( - property => property.Name, - property => property, - StringComparer.OrdinalIgnoreCase - ); - } - - private static Dictionary LoadStaticFieldsForType() - { - Type type = typeof(T); - return type.GetFields(BindingFlags.Static | BindingFlags.Public) - .Where(field => field.FieldType == type) - .ToDictionary( - field => field.Name, - field => field, - StringComparer.OrdinalIgnoreCase - ); - } - - public override string ToString() - { - return contents; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using WallstopStudios.DxCommandTerminal.Backend.Parsers; + + public delegate bool CommandArgParser(string input, out T parsed); + + public readonly struct CommandArg + { + static CommandArg() + { + // Register built-in object parsers once for quick lookup + RegisterDefaultObjectParsers(); + } + + private static readonly Lazy TryGetMethod = new(() => + typeof(CommandArg) + .GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(method => method.Name == nameof(TryGet)) + .FirstOrDefault(method => method.GetParameters().Length == 1) + ); + private static readonly Dictionary RegisteredParsers = new(); + private static readonly Dictionary RegisteredObjectParsers = new(); + + // Removed caches for static members and enum values; moved to dedicated parsers + + // Public to allow custom-mutation, if desired + public static readonly HashSet Delimiters = new() { ',', ';', ':', '_', '/', '\\' }; + public static readonly List Quotes = new() { '"', '\'' }; + public static readonly HashSet IgnoredValuesForCleanedTypes = new() { "\r", "\n" }; + public static readonly HashSet DoNotCleanTypes = new() + { + typeof(string), + typeof(char), + typeof(DateTime), + typeof(DateTimeOffset), + }; + public static readonly HashSet IgnoredValuesForComplexTypes = new() + { + "(", + ")", + "[", + "]", + "'", + "`", + "|", + "{", + "}", + "<", + ">", + }; + + public readonly string contents; + public readonly char? startQuote; + public readonly char? endQuote; + + public string CleanedContents + { + get + { + string cleanedString = contents; + cleanedString = IgnoredValuesForCleanedTypes.Aggregate( + cleanedString, + (current, ignoredValue) => + current.Replace( + ignoredValue, + string.Empty, + StringComparison.OrdinalIgnoreCase + ) + ); + return cleanedString; + } + } + + public bool TryGet(Type type, out object parsed) + { + // TODO: Convert into delegates and cache for performance + MethodInfo genericMethod = TryGetMethod.Value; + if (genericMethod == null) + { + parsed = default; + return false; + } + + MethodInfo constructed = genericMethod.MakeGenericMethod(type); + object[] parameters = { null }; + bool success = (bool)constructed.Invoke(this, parameters); + parsed = parameters[0]; + return success; + } + + public bool TryGet(out T parsed) + { + return TryGet(out parsed, parserOverride: null); + } + + public bool TryGet(out T parsed, CommandArgParser parserOverride) + { + Type type = typeof(T); + string stringValue = DoNotCleanTypes.Contains(type) ? contents : CleanedContents; + + if (parserOverride != null) + { + return parserOverride(stringValue, out parsed); + } + + if (TryGetParser(out CommandArgParser parser)) + { + return parser(stringValue, out parsed); + } + + if (type == typeof(string)) + { + parsed = (T)Convert.ChangeType(stringValue, type); + return true; + } + if (StaticMemberParser.TryParse(stringValue, out parsed)) + { + return true; + } + + if (TryGetObjectParser(type, out IArgParser objectParser)) + { + if (objectParser.TryParse(stringValue, out object objectValue)) + { + parsed = (T)objectValue; + return true; + } + } + + // Enums (hot path via cached values) + if (type.IsEnum) + { + if (EnumArgParser.TryParse(type, stringValue, out object enumObject)) + { + parsed = (T)enumObject; + return true; + } + } + + parsed = default; + return false; + // static member resolution moved to StaticMemberParser + } + + // Consolidated parsing helpers moved to Backend.Parsers.CommandArgParserCommon + + public CommandArg(string contents, char? startQuote = null, char? endQuote = null) + { + this.contents = contents ?? string.Empty; + this.startQuote = startQuote; + this.endQuote = endQuote; + } + + public static bool RegisterParser(CommandArgParser parser, bool force = false) + { + if (parser == null) + { + return false; + } + + Type type = typeof(T); + if (force) + { + RegisteredParsers[type] = parser; + return true; + } + + return RegisteredParsers.TryAdd(type, parser); + } + + public static bool TryGetParser(out CommandArgParser parser) + { + if (RegisteredParsers.TryGetValue(typeof(T), out object untypedParser)) + { + parser = (CommandArgParser)untypedParser; + return true; + } + + parser = null; + return false; + } + + public static bool UnregisterParser() + { + return UnregisterParser(typeof(T)); + } + + public static bool UnregisterParser(Type type) + { + return RegisteredParsers.Remove(type); + } + + public static int UnregisterAllParsers() + { + int parserCount = RegisteredParsers.Count; + RegisteredParsers.Clear(); + return parserCount; + } + + // Object parser registration (IArgParser) + public static bool RegisterObjectParser(IArgParser parser, bool force = false) + { + if (parser == null || parser.TargetType == null) + { + return false; + } + + Type type = parser.TargetType; + if (force) + { + RegisteredObjectParsers[type] = parser; + return true; + } + + return RegisteredObjectParsers.TryAdd(type, parser); + } + + public static bool TryGetObjectParser(Type type, out IArgParser parser) + { + return RegisteredObjectParsers.TryGetValue(type, out parser); + } + + public static bool UnregisterObjectParser(Type type) + { + return RegisteredObjectParsers.Remove(type); + } + + public static int UnregisterAllObjectParsers() + { + int count = RegisteredObjectParsers.Count; + RegisteredObjectParsers.Clear(); + return count; + } + + public static IReadOnlyCollection GetRegisteredObjectParserTypes() + { + // Snapshot for thread-safety and immutability to callers + return RegisteredObjectParsers.Keys.ToArray(); + } + + public static int DiscoverAndRegisterParsers(bool replaceExisting = false) + { + int added = 0; + foreach (System.Reflection.Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + try + { + types = asm.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + types = e.Types.Where(t => t != null).ToArray(); + } + + foreach (Type t in types) + { + if (t == null || t.IsAbstract || t.IsGenericTypeDefinition) + { + continue; + } + if (!typeof(IArgParser).IsAssignableFrom(t)) + { + continue; + } + + IArgParser instance = null; + // Prefer public static Instance singleton if available + var instProp = t.GetProperty( + "Instance", + BindingFlags.Public | BindingFlags.Static + ); + if ( + instProp != null + && typeof(IArgParser).IsAssignableFrom(instProp.PropertyType) + ) + { + instance = (IArgParser)instProp.GetValue(null); + } + else + { + var instField = t.GetField( + "Instance", + BindingFlags.Public | BindingFlags.Static + ); + if ( + instField != null + && typeof(IArgParser).IsAssignableFrom(instField.FieldType) + ) + { + instance = (IArgParser)instField.GetValue(null); + } + } + + if (instance == null) + { + // Fall back to parameterless constructor + var ctor = t.GetConstructor(Type.EmptyTypes); + if (ctor != null) + { + instance = (IArgParser)Activator.CreateInstance(t); + } + } + + if (instance == null || instance.TargetType == null) + { + continue; + } + + if (RegisterObjectParser(instance, replaceExisting)) + { + added++; + } + } + } + return added; + } + + private static void RegisterDefaultObjectParsers() + { + // Numerics + RegisterObjectParser(BoolArgParser.Instance, true); + RegisterObjectParser(FloatArgParser.Instance, true); + RegisterObjectParser(IntArgParser.Instance, true); + RegisterObjectParser(UIntArgParser.Instance, true); + RegisterObjectParser(LongArgParser.Instance, true); + RegisterObjectParser(ULongArgParser.Instance, true); + RegisterObjectParser(DoubleArgParser.Instance, true); + RegisterObjectParser(ShortArgParser.Instance, true); + RegisterObjectParser(UShortArgParser.Instance, true); + RegisterObjectParser(ByteArgParser.Instance, true); + RegisterObjectParser(SByteArgParser.Instance, true); + RegisterObjectParser(DecimalArgParser.Instance, true); + RegisterObjectParser(BigIntegerArgParser.Instance, true); + + // Misc + RegisterObjectParser(GuidArgParser.Instance, true); + RegisterObjectParser(DateTimeArgParser.Instance, true); + RegisterObjectParser(DateTimeOffsetArgParser.Instance, true); + RegisterObjectParser(CharArgParser.Instance, true); + RegisterObjectParser(TimeSpanArgParser.Instance, true); + RegisterObjectParser(VersionArgParser.Instance, true); + RegisterObjectParser(IPAddressArgParser.Instance, true); + + // Unity types + RegisterObjectParser(Vector2ArgParser.Instance, true); + RegisterObjectParser(Vector3ArgParser.Instance, true); + RegisterObjectParser(Vector4ArgParser.Instance, true); + RegisterObjectParser(Vector2IntArgParser.Instance, true); + RegisterObjectParser(Vector3IntArgParser.Instance, true); + RegisterObjectParser(ColorArgParser.Instance, true); + RegisterObjectParser(QuaternionArgParser.Instance, true); + RegisterObjectParser(RectArgParser.Instance, true); + RegisterObjectParser(RectIntArgParser.Instance, true); + } + + // Static member reflection helpers moved to Parsers.StaticMemberParser + + public override string ToString() + { + return contents; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs index bb54e1b..f779a8d 100644 --- a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs +++ b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs @@ -49,24 +49,48 @@ List buffer } _duplicateBuffer.Clear(); buffer.Clear(); + + // Commands + foreach (string command in _shell.Commands.Keys) + { + string known = command.NeedsLowerInvariantConversion() + ? command.ToLowerInvariant() + : command; + if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (_duplicateBuffer.Add(known)) + { + buffer.Add(known); + } + } + + // Known words + foreach (string known in _knownWords) + { + if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (_duplicateBuffer.Add(known)) + { + buffer.Add(known); + } + } + + // History foreach ( - string known in _shell - .Commands.Keys.Select(command => - command.NeedsLowerInvariantConversion() - ? command.ToLowerInvariant() - : command - ) - .Concat(_knownWords) - .Concat( - _history.GetHistory(onlySuccess: onlySuccess, onlyErrorFree: onlyErrorFree) - ) + string known in _history.GetHistory( + onlySuccess: onlySuccess, + onlyErrorFree: onlyErrorFree + ) ) { if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) { continue; } - if (_duplicateBuffer.Add(known)) { buffer.Add(known); diff --git a/Runtime/CommandTerminal/Backend/CommandLog.cs b/Runtime/CommandTerminal/Backend/CommandLog.cs index f5fd2c3..8eba05d 100644 --- a/Runtime/CommandTerminal/Backend/CommandLog.cs +++ b/Runtime/CommandTerminal/Backend/CommandLog.cs @@ -1,6 +1,7 @@ namespace WallstopStudios.DxCommandTerminal.Backend { using System; + using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using DataStructures; @@ -33,8 +34,7 @@ public LogItem(TerminalLogType type, string message, string stackTrace) public sealed class CommandLog { - private static readonly string[] NewlineSeparators = { "\r\n", "\n", "\r" }; - private static readonly string JoinSeparator = Environment.NewLine; + private const string InternalNamespace = "WallstopStudios.DxCommandTerminal"; public IReadOnlyList Logs => _logs; public int Capacity => _logs.Capacity; @@ -46,6 +46,13 @@ public sealed class CommandLog private long _version; + private readonly ConcurrentQueue<( + string message, + string stackTrace, + TerminalLogType type, + bool includeStackTrace + )> _pending = new(); + public CommandLog(int maxItems, IEnumerable ignoredLogTypes = null) { _logs = new CyclicBuffer(maxItems); @@ -56,6 +63,7 @@ public CommandLog(int maxItems, IEnumerable ignoredLogTypes = n public bool HandleLog(string message, TerminalLogType type, bool includeStackTrace = true) { + // Main-thread direct path retained for back-compat string stackTrace = includeStackTrace ? GetAccurateStackTrace() : string.Empty; return HandleLog(message, stackTrace, type); } @@ -67,25 +75,58 @@ private static string GetAccurateStackTrace() { return fullStackTrace; } - - string[] lines = fullStackTrace.Split(NewlineSeparators, StringSplitOptions.None); - - int startIndex = 1; + int length = fullStackTrace.Length; + int index = 0; + // Skip the first line (StackTraceUtility includes a leading line) + while (index < length && fullStackTrace[index] != '\n' && fullStackTrace[index] != '\r') + { + index++; + } + // Consume newline chars while ( - startIndex < lines.Length - && lines[startIndex] - .Contains( - "WallstopStudios.DxCommandTerminal", - StringComparison.OrdinalIgnoreCase - ) + index < length && (fullStackTrace[index] == '\n' || fullStackTrace[index] == '\r') ) { - ++startIndex; + index++; } - return lines.Length <= startIndex - ? string.Empty - : string.Join(JoinSeparator, lines, startIndex, lines.Length - startIndex); + // Skip frames inside our own namespace for clearer logs + while (index < length) + { + int lineStart = index; + // Find end of line + while ( + index < length && fullStackTrace[index] != '\n' && fullStackTrace[index] != '\r' + ) + { + index++; + } + + int lineLen = index - lineStart; + bool isInternal = + fullStackTrace.IndexOf( + InternalNamespace, + lineStart, + lineLen, + StringComparison.OrdinalIgnoreCase + ) >= 0; + if (!isInternal) + { + // Return from this line onward + return fullStackTrace.Substring(lineStart); + } + + // Move to next line start (skip newline chars) + while ( + index < length + && (fullStackTrace[index] == '\n' || fullStackTrace[index] == '\r') + ) + { + index++; + } + } + + return string.Empty; } public bool HandleLog(string message, string stackTrace, TerminalLogType type) @@ -101,6 +142,42 @@ public bool HandleLog(string message, string stackTrace, TerminalLogType type) return true; } + public void EnqueueMessage(string message, TerminalLogType type, bool includeStackTrace) + { + if (ignoredLogTypes.Contains(type)) + { + return; + } + _pending.Enqueue((message ?? string.Empty, string.Empty, type, includeStackTrace)); + } + + public void EnqueueUnityLog(string message, string stackTrace, TerminalLogType type) + { + if (ignoredLogTypes.Contains(type)) + { + return; + } + _pending.Enqueue((message ?? string.Empty, stackTrace ?? string.Empty, type, false)); + } + + public int DrainPending() + { + int added = 0; + while (_pending.TryDequeue(out var item)) + { + string stack = item.includeStackTrace ? GetAccurateStackTrace() : item.stackTrace; + if (ignoredLogTypes.Contains(item.type)) + { + continue; + } + _version++; + _logs.Add(new LogItem(item.type, item.message, stack)); + added++; + } + + return added; + } + public int Clear() { int logCount = _logs.Count; diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index e81c048..5a00a0a 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -525,8 +525,19 @@ public static bool TryEatArgument(ref string stringValue, out CommandArg arg) { if (stringValue[i] == firstChar) { - closingQuoteIndex = i; - break; + // Check if this quote is escaped by an odd number of backslashes + int backslashCount = 0; + int j = i - 1; + while (1 <= j && stringValue[j] == '\\') + { + backslashCount++; + j--; + } + if ((backslashCount % 2) == 0) + { + closingQuoteIndex = i; + break; + } } } @@ -534,6 +545,14 @@ public static bool TryEatArgument(ref string stringValue, out CommandArg arg) { // No closing quote was found; consume the rest of the string (excluding the opening quote). string input = stringValue.Substring(1); + if (firstChar == '\'') + { + input = input.Replace("\\'", "'").Replace("\\\\", "\\"); + } + else if (firstChar == '"') + { + input = input.Replace("\\\"", "\"").Replace("\\\\", "\\"); + } arg = new CommandArg(input, firstChar); stringValue = string.Empty; } @@ -541,6 +560,15 @@ public static bool TryEatArgument(ref string stringValue, out CommandArg arg) { // Extract the argument inside the quotes. string input = stringValue.Substring(1, closingQuoteIndex - 1); + // Unescape the matching quote and backslashes + if (firstChar == '\'') + { + input = input.Replace("\\'", "'").Replace("\\\\", "\\"); + } + else if (firstChar == '"') + { + input = input.Replace("\\\"", "\"").Replace("\\\\", "\\"); + } arg = new CommandArg(input, firstChar, firstChar); // Remove the parsed argument (including the quotes) from the input. stringValue = stringValue.Substring(closingQuoteIndex + 1); diff --git a/Runtime/CommandTerminal/Backend/Parsers.meta b/Runtime/CommandTerminal/Backend/Parsers.meta new file mode 100644 index 0000000..4a4922a --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a2e552d3677ee7a4ea4582f6d73a5ff4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs b/Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs new file mode 100644 index 0000000..2a6310d --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs @@ -0,0 +1,12 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + public sealed class BoolArgParser : ArgParser + { + public static readonly BoolArgParser Instance = new BoolArgParser(); + + protected override bool TryParseTyped(string input, out bool value) + { + return bool.TryParse(input, out value); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs.meta new file mode 100644 index 0000000..c159ee9 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/BoolArgParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35e5bc89a2e639f4d8811660270ce540 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs b/Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs new file mode 100644 index 0000000..0dcc80f --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs @@ -0,0 +1,407 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using Backend; + + internal static class CommandArgParserCommon + { + public static bool TryParseFloatList( + ReadOnlySpan input, + out float a, + out float b, + out float c, + out float d, + out int count + ) + { + a = 0f; + b = 0f; + c = 0f; + d = 0f; + count = 0; + int i = 0; + while (i < input.Length) + { + char ch = input[i]; + if (char.IsWhiteSpace(ch) || IsIgnoredChar(ch)) + { + i++; + continue; + } + if (char.IsLetter(ch)) + { + while (i < input.Length && input[i] != ':') + { + i++; + } + if (i < input.Length && input[i] == ':') + { + i++; + } + continue; + } + if (CommandArg.Delimiters.Contains(ch)) + { + i++; + continue; + } + int start = i; + while (i < input.Length) + { + char cch = input[i]; + if ( + char.IsWhiteSpace(cch) + || CommandArg.Delimiters.Contains(cch) + || IsIgnoredChar(cch) + ) + { + break; + } + i++; + } + ReadOnlySpan slice = input.Slice(start, i - start); + if ( + !float.TryParse( + slice, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float value + ) + ) + { + return false; + } + switch (count) + { + case 0: + a = value; + break; + case 1: + b = value; + break; + case 2: + c = value; + break; + case 3: + d = value; + break; + default: + break; + } + count++; + } + return 0 < count; + } + + public static bool TryParseIntList( + ReadOnlySpan input, + out int a, + out int b, + out int c, + out int d, + out int count + ) + { + a = 0; + b = 0; + c = 0; + d = 0; + count = 0; + int i = 0; + while (i < input.Length) + { + char ch = input[i]; + if (char.IsWhiteSpace(ch) || IsIgnoredChar(ch)) + { + i++; + continue; + } + if (char.IsLetter(ch)) + { + while (i < input.Length && input[i] != ':') + { + i++; + } + if (i < input.Length && input[i] == ':') + { + i++; + } + continue; + } + if (CommandArg.Delimiters.Contains(ch)) + { + i++; + continue; + } + int start = i; + while (i < input.Length) + { + char cch = input[i]; + if ( + char.IsWhiteSpace(cch) + || CommandArg.Delimiters.Contains(cch) + || IsIgnoredChar(cch) + ) + { + break; + } + i++; + } + ReadOnlySpan slice = input.Slice(start, i - start); + if ( + !int.TryParse( + slice, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int value + ) + ) + { + return false; + } + switch (count) + { + case 0: + a = value; + break; + case 1: + b = value; + break; + case 2: + c = value; + break; + case 3: + d = value; + break; + default: + break; + } + count++; + } + return 0 < count; + } + + public static string[] StripAndSplit(string input) + { + string strippedInput = input; + foreach (string ignored in CommandArg.IgnoredValuesForComplexTypes) + { + if (!string.IsNullOrEmpty(ignored)) + { + strippedInput = strippedInput.Replace( + ignored, + string.Empty, + StringComparison.OrdinalIgnoreCase + ); + } + } + + foreach (char delimiter in CommandArg.Delimiters) + { + if (strippedInput.Contains(delimiter)) + { + return strippedInput.Split(delimiter); + } + } + + return new[] { strippedInput }; + } + + public static bool IsIgnoredChar(char ch) + { + return ch == '(' + || ch == ')' + || ch == '[' + || ch == ']' + || ch == '\'' + || ch == '`' + || ch == '|' + || ch == '{' + || ch == '}' + || ch == '<' + || ch == '>'; + } + + public static bool TryParseLabeledFloatMap( + ReadOnlySpan input, + out Dictionary values, + out bool malformed + ) + { + values = new Dictionary(StringComparer.OrdinalIgnoreCase); + malformed = false; + int i = 0; + bool foundAnyLabel = false; + while (i < input.Length) + { + char ch = input[i]; + if (char.IsWhiteSpace(ch) || IsIgnoredChar(ch)) + { + i++; + continue; + } + + if (!char.IsLetter(ch)) + { + i++; + continue; + } + + int labelStart = i; + while (i < input.Length && char.IsLetter(input[i])) + { + i++; + } + ReadOnlySpan labelSpan = input.Slice(labelStart, i - labelStart); + // Skip whitespace + while (i < input.Length && char.IsWhiteSpace(input[i])) + { + i++; + } + if (i >= input.Length || input[i] != ':') + { + // Not a label-value pair; continue scanning + continue; + } + i++; // skip colon + foundAnyLabel = true; + + // Skip whitespace and ignored chars after colon + while (i < input.Length && (char.IsWhiteSpace(input[i]) || IsIgnoredChar(input[i]))) + { + i++; + } + + int valueStart = i; + while ( + i < input.Length + && !char.IsWhiteSpace(input[i]) + && !IsIgnoredChar(input[i]) + && !CommandArg.Delimiters.Contains(input[i]) + && !char.IsLetter(input[i]) + ) + { + i++; + } + + ReadOnlySpan valueSpan = input.Slice(valueStart, i - valueStart); + if (valueSpan.Length == 0) + { + malformed = true; + continue; + } + + if ( + float.TryParse( + valueSpan, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float f + ) + ) + { + string label = labelSpan.ToString(); + values[label] = f; + } + else + { + malformed = true; + } + } + + return foundAnyLabel; + } + + public static bool TryParseLabeledIntMap( + ReadOnlySpan input, + out Dictionary values, + out bool malformed + ) + { + values = new Dictionary(StringComparer.OrdinalIgnoreCase); + malformed = false; + int i = 0; + bool foundAnyLabel = false; + while (i < input.Length) + { + char ch = input[i]; + if (char.IsWhiteSpace(ch) || IsIgnoredChar(ch)) + { + i++; + continue; + } + + if (!char.IsLetter(ch)) + { + i++; + continue; + } + + int labelStart = i; + while (i < input.Length && char.IsLetter(input[i])) + { + i++; + } + ReadOnlySpan labelSpan = input.Slice(labelStart, i - labelStart); + // Skip whitespace + while (i < input.Length && char.IsWhiteSpace(input[i])) + { + i++; + } + if (i >= input.Length || input[i] != ':') + { + // Not a label-value pair; continue scanning + continue; + } + i++; // skip colon + foundAnyLabel = true; + + // Skip whitespace and ignored chars after colon + while (i < input.Length && (char.IsWhiteSpace(input[i]) || IsIgnoredChar(input[i]))) + { + i++; + } + + int valueStart = i; + while ( + i < input.Length + && !char.IsWhiteSpace(input[i]) + && !IsIgnoredChar(input[i]) + && !CommandArg.Delimiters.Contains(input[i]) + && !char.IsLetter(input[i]) + ) + { + i++; + } + + ReadOnlySpan valueSpan = input.Slice(valueStart, i - valueStart); + if (valueSpan.Length == 0) + { + malformed = true; + continue; + } + + if ( + int.TryParse( + valueSpan, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int iv + ) + ) + { + string label = labelSpan.ToString(); + values[label] = iv; + } + else + { + malformed = true; + } + } + + return foundAnyLabel; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs.meta new file mode 100644 index 0000000..ea80b1e --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/CommandArgParserCommon.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: deab8afadbdbca14bae57fdc529a2455 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs b/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs new file mode 100644 index 0000000..ded08a4 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs @@ -0,0 +1,58 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + using System; + using System.Collections.Generic; + using System.Linq; + + public static class EnumArgParser + { + private static readonly Dictionary CachedValues = new(); + private static readonly Dictionary> CachedNames = new(); + + public static bool TryParse(Type enumType, string input, out object value) + { + if (!enumType.IsEnum) + { + value = null; + return false; + } + + // Fast name map (case-insensitive) + if (!CachedNames.TryGetValue(enumType, out Dictionary nameMap)) + { + nameMap = Enum.GetNames(enumType) + .ToDictionary( + n => n, + n => (object)Enum.Parse(enumType, n), + StringComparer.OrdinalIgnoreCase + ); + CachedNames[enumType] = nameMap; + } + + if (nameMap.TryGetValue(input, out object named)) + { + value = named; + return true; + } + + // Ordinal path + if (int.TryParse(input, out int ordinal)) + { + if (!CachedValues.TryGetValue(enumType, out Array values)) + { + values = Enum.GetValues(enumType); + CachedValues[enumType] = values; + } + + if (0 <= ordinal && ordinal < values.Length) + { + value = values.GetValue(ordinal); + return true; + } + } + + value = null; + return false; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs.meta new file mode 100644 index 0000000..3cadc86 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4a2ff59c83f1e544a72523ac98430ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs b/Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs new file mode 100644 index 0000000..7d992e7 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs @@ -0,0 +1,29 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + using System; + + public interface IArgParser + { + Type TargetType { get; } + bool TryParse(string input, out object value); + } + + public abstract class ArgParser : IArgParser + { + public Type TargetType => typeof(T); + + public bool TryParse(string input, out object value) + { + if (TryParseTyped(input, out T typed)) + { + value = typed; + return true; + } + + value = default; + return false; + } + + protected abstract bool TryParseTyped(string input, out T value); + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs.meta new file mode 100644 index 0000000..241a5f0 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/IArgParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0527b1ce5bbff7941867e7d429ef6607 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs b/Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs new file mode 100644 index 0000000..1c42d0a --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs @@ -0,0 +1,95 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + using System; + using System.Globalization; + using System.Net; + + public sealed class GuidArgParser : ArgParser + { + public static readonly GuidArgParser Instance = new GuidArgParser(); + + protected override bool TryParseTyped(string input, out Guid value) + { + return Guid.TryParse(input, out value); + } + } + + public sealed class DateTimeArgParser : ArgParser + { + public static readonly DateTimeArgParser Instance = new DateTimeArgParser(); + + protected override bool TryParseTyped(string input, out DateTime value) + { + bool ok = DateTime.TryParse( + input, + CultureInfo.InvariantCulture, + DateTimeStyles.RoundtripKind, + out value + ); + if (!ok) + { + return false; + } + if (value.Kind == DateTimeKind.Utc) + { + value = value.ToLocalTime(); + } + return true; + } + } + + public sealed class DateTimeOffsetArgParser : ArgParser + { + public static readonly DateTimeOffsetArgParser Instance = new DateTimeOffsetArgParser(); + + protected override bool TryParseTyped(string input, out DateTimeOffset value) + { + return DateTimeOffset.TryParse( + input, + CultureInfo.InvariantCulture, + DateTimeStyles.RoundtripKind, + out value + ); + } + } + + public sealed class CharArgParser : ArgParser + { + public static readonly CharArgParser Instance = new CharArgParser(); + + protected override bool TryParseTyped(string input, out char value) + { + return char.TryParse(input, out value); + } + } + + public sealed class TimeSpanArgParser : ArgParser + { + public static readonly TimeSpanArgParser Instance = new TimeSpanArgParser(); + + protected override bool TryParseTyped(string input, out TimeSpan value) + { + return TimeSpan.TryParse(input, CultureInfo.InvariantCulture, out value); + } + } + + public sealed class VersionArgParser : ArgParser + { + public static readonly VersionArgParser Instance = new VersionArgParser(); + + protected override bool TryParseTyped(string input, out Version value) + { + return Version.TryParse(input, out value); + } + } + + public sealed class IPAddressArgParser : ArgParser + { + public static readonly IPAddressArgParser Instance = new IPAddressArgParser(); + + protected override bool TryParseTyped(string input, out IPAddress value) + { + return IPAddress.TryParse(input, out value); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs.meta new file mode 100644 index 0000000..cb86c46 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/MiscArgParsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1705bc034b8860944a1bdf9395fdd88e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs b/Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs new file mode 100644 index 0000000..2c33a55 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs @@ -0,0 +1,185 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + using System.Globalization; + using System.Numerics; + + public sealed class FloatArgParser : ArgParser + { + public static readonly FloatArgParser Instance = new FloatArgParser(); + + protected override bool TryParseTyped(string input, out float value) + { + return float.TryParse( + input, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class IntArgParser : ArgParser + { + public static readonly IntArgParser Instance = new IntArgParser(); + + protected override bool TryParseTyped(string input, out int value) + { + return int.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class UIntArgParser : ArgParser + { + public static readonly UIntArgParser Instance = new UIntArgParser(); + + protected override bool TryParseTyped(string input, out uint value) + { + return uint.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class LongArgParser : ArgParser + { + public static readonly LongArgParser Instance = new LongArgParser(); + + protected override bool TryParseTyped(string input, out long value) + { + return long.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class ULongArgParser : ArgParser + { + public static readonly ULongArgParser Instance = new ULongArgParser(); + + protected override bool TryParseTyped(string input, out ulong value) + { + return ulong.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class DoubleArgParser : ArgParser + { + public static readonly DoubleArgParser Instance = new DoubleArgParser(); + + protected override bool TryParseTyped(string input, out double value) + { + return double.TryParse( + input, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class ShortArgParser : ArgParser + { + public static readonly ShortArgParser Instance = new ShortArgParser(); + + protected override bool TryParseTyped(string input, out short value) + { + return short.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class UShortArgParser : ArgParser + { + public static readonly UShortArgParser Instance = new UShortArgParser(); + + protected override bool TryParseTyped(string input, out ushort value) + { + return ushort.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class ByteArgParser : ArgParser + { + public static readonly ByteArgParser Instance = new ByteArgParser(); + + protected override bool TryParseTyped(string input, out byte value) + { + return byte.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class SByteArgParser : ArgParser + { + public static readonly SByteArgParser Instance = new SByteArgParser(); + + protected override bool TryParseTyped(string input, out sbyte value) + { + return sbyte.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class DecimalArgParser : ArgParser + { + public static readonly DecimalArgParser Instance = new DecimalArgParser(); + + protected override bool TryParseTyped(string input, out decimal value) + { + return decimal.TryParse( + input, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out value + ); + } + } + + public sealed class BigIntegerArgParser : ArgParser + { + public static readonly BigIntegerArgParser Instance = new BigIntegerArgParser(); + + protected override bool TryParseTyped(string input, out BigInteger value) + { + return BigInteger.TryParse( + input, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out value + ); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs.meta new file mode 100644 index 0000000..6a6dd68 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/NumericArgParsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d953dfd8daa61a46a629eb8dc7aff24 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs b/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs new file mode 100644 index 0000000..565a004 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs @@ -0,0 +1,55 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + public static class StaticMemberParser + { + private static bool initialized; + private static Dictionary Properties; + private static Dictionary Fields; + + private static void EnsureInitialized() + { + if (initialized) + { + return; + } + + Type type = typeof(T); + Properties = type.GetProperties(BindingFlags.Static | BindingFlags.Public) + .Where(p => p.PropertyType == type) + .ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase); + + Fields = type.GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(f => f.FieldType == type) + .ToDictionary(f => f.Name, f => f, StringComparer.OrdinalIgnoreCase); + + initialized = true; + } + + public static bool TryParse(string input, out T value) + { + EnsureInitialized(); + + if (Properties.TryGetValue(input, out PropertyInfo property)) + { + object resolved = property.GetValue(null); + value = (T)resolved; + return true; + } + + if (Fields.TryGetValue(input, out FieldInfo field)) + { + object resolved = field.GetValue(null); + value = (T)resolved; + return true; + } + + value = default; + return false; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs.meta new file mode 100644 index 0000000..f34ba06 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2309a9191602a72439c3dbf826dbe177 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs b/Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs new file mode 100644 index 0000000..530a537 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs @@ -0,0 +1,915 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Parsers +{ + using System; + using System.Globalization; + using UnityEngine; + + public sealed class Vector2ArgParser : ArgParser + { + public static readonly Vector2ArgParser Instance = new Vector2ArgParser(); + + protected override bool TryParseTyped(string input, out Vector2 value) + { + if ( + CommandArgParserCommon.TryParseLabeledFloatMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("x", out float lx) && labeled.TryGetValue("y", out float ly) + ) + { + value = new Vector2(lx, ly); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseFloatList( + input.AsSpan(), + out float f0, + out float f1, + out float f2, + out _, + out int cnt + ) + ) + { + if (cnt == 2) + { + value = new Vector2(f0, f1); + return true; + } + if (cnt == 3) + { + value = (Vector2)new Vector3(f0, f1, f2); + return true; + } + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + switch (split.Length) + { + case 2 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ): + value = new Vector2(x, y); + return true; + case 3 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ) + && float.TryParse( + split[2], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float z + ): + value = (Vector2)new Vector3(x, y, z); + return true; + } + + value = default; + return false; + } + } + + public sealed class Vector3ArgParser : ArgParser + { + public static readonly Vector3ArgParser Instance = new Vector3ArgParser(); + + protected override bool TryParseTyped(string input, out Vector3 value) + { + if ( + CommandArgParserCommon.TryParseLabeledFloatMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("x", out float lx) + && labeled.TryGetValue("y", out float ly) + && labeled.TryGetValue("z", out float lz) + ) + { + value = new Vector3(lx, ly, lz); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseFloatList( + input.AsSpan(), + out float f0, + out float f1, + out float f2, + out _, + out int cnt + ) + ) + { + if (cnt == 2) + { + value = new Vector3(f0, f1); + return true; + } + if (cnt == 3) + { + value = new Vector3(f0, f1, f2); + return true; + } + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + switch (split.Length) + { + case 2 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ): + value = new Vector3(x, y); + return true; + case 3 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ) + && float.TryParse( + split[2], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float z + ): + value = new Vector3(x, y, z); + return true; + } + + value = default; + return false; + } + } + + public sealed class Vector4ArgParser : ArgParser + { + public static readonly Vector4ArgParser Instance = new Vector4ArgParser(); + + protected override bool TryParseTyped(string input, out Vector4 value) + { + if ( + CommandArgParserCommon.TryParseLabeledFloatMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("x", out float lx) + && labeled.TryGetValue("y", out float ly) + && labeled.TryGetValue("z", out float lz) + && labeled.TryGetValue("w", out float lw) + ) + { + value = new Vector4(lx, ly, lz, lw); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseFloatList( + input.AsSpan(), + out float f0, + out float f1, + out float f2, + out float f3, + out int cnt + ) + ) + { + if (cnt == 2) + { + value = new Vector4(f0, f1); + return true; + } + if (cnt == 3) + { + value = new Vector4(f0, f1, f2); + return true; + } + if (cnt == 4) + { + value = new Vector4(f0, f1, f2, f3); + return true; + } + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + switch (split.Length) + { + case 2 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ): + value = new Vector4(x, y); + return true; + case 3 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ) + && float.TryParse( + split[2], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float z + ): + value = new Vector4(x, y, z); + return true; + case 4 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ) + && float.TryParse( + split[2], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float z + ) + && float.TryParse( + split[3], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float w + ): + value = new Vector4(x, y, z, w); + return true; + } + + value = default; + return false; + } + } + + public sealed class Vector2IntArgParser : ArgParser + { + public static readonly Vector2IntArgParser Instance = new Vector2IntArgParser(); + + protected override bool TryParseTyped(string input, out Vector2Int value) + { + if ( + CommandArgParserCommon.TryParseLabeledIntMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if (labeled.TryGetValue("x", out int lx) && labeled.TryGetValue("y", out int ly)) + { + value = new Vector2Int(lx, ly); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseIntList( + input.AsSpan(), + out int i0, + out int i1, + out int i2, + out _, + out int icnt + ) + ) + { + if (icnt == 2) + { + value = new Vector2Int(i0, i1); + return true; + } + if (icnt == 3) + { + value = (Vector2Int)new Vector3Int(i0, i1, i2); + return true; + } + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + switch (split.Length) + { + case 2 + when int.TryParse( + split[0], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int x + ) + && int.TryParse( + split[1], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int y + ): + value = new Vector2Int(x, y); + return true; + case 3 + when int.TryParse( + split[0], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int x + ) + && int.TryParse( + split[1], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int y + ) + && int.TryParse( + split[2], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int z + ): + value = (Vector2Int)new Vector3Int(x, y, z); + return true; + } + + value = default; + return false; + } + } + + public sealed class Vector3IntArgParser : ArgParser + { + public static readonly Vector3IntArgParser Instance = new Vector3IntArgParser(); + + protected override bool TryParseTyped(string input, out Vector3Int value) + { + if ( + CommandArgParserCommon.TryParseLabeledIntMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("x", out int lx) + && labeled.TryGetValue("y", out int ly) + && labeled.TryGetValue("z", out int lz) + ) + { + value = new Vector3Int(lx, ly, lz); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseIntList( + input.AsSpan(), + out int i0, + out int i1, + out int i2, + out _, + out int icnt + ) + ) + { + if (icnt == 2) + { + value = new Vector3Int(i0, i1); + return true; + } + if (icnt == 3) + { + value = new Vector3Int(i0, i1, i2); + return true; + } + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + switch (split.Length) + { + case 2 + when int.TryParse( + split[0], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int x + ) + && int.TryParse( + split[1], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int y + ): + value = new Vector3Int(x, y); + return true; + case 3 + when int.TryParse( + split[0], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int x + ) + && int.TryParse( + split[1], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int y + ) + && int.TryParse( + split[2], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int z + ): + value = new Vector3Int(x, y, z); + return true; + } + + value = default; + return false; + } + } + + public sealed class ColorArgParser : ArgParser + { + public static readonly ColorArgParser Instance = new ColorArgParser(); + + protected override bool TryParseTyped(string input, out Color value) + { + string colorString = input; + if (colorString.StartsWith("RGBA", StringComparison.OrdinalIgnoreCase)) + { + colorString = colorString.Replace( + "RGBA", + string.Empty, + StringComparison.OrdinalIgnoreCase + ); + } + + if ( + CommandArgParserCommon.TryParseLabeledFloatMap( + colorString.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("r", out float r) + && labeled.TryGetValue("g", out float g) + && labeled.TryGetValue("b", out float b) + ) + { + float a = labeled.TryGetValue("a", out float la) ? la : 1.0f; + value = new Color(r, g, b, a); + return true; + } + value = default; + return false; + } + + if ( + CommandArgParserCommon.TryParseFloatList( + colorString.AsSpan(), + out float cr, + out float cg, + out float cb, + out float ca, + out int ccnt + ) + ) + { + if (ccnt == 3) + { + value = new Color(cr, cg, cb); + return true; + } + if (ccnt == 4) + { + value = new Color(cr, cg, cb, ca); + return true; + } + } + + string[] split = CommandArgParserCommon.StripAndSplit(colorString); + switch (split.Length) + { + case 3 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float r + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float g + ) + && float.TryParse( + split[2], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float b + ): + value = new Color(r, g, b); + return true; + case 4 + when float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float r + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float g + ) + && float.TryParse( + split[2], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float b + ) + && float.TryParse( + split[3], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float a + ): + value = new Color(r, g, b, a); + return true; + } + + value = default; + return false; + } + } + + public sealed class QuaternionArgParser : ArgParser + { + public static readonly QuaternionArgParser Instance = new QuaternionArgParser(); + + protected override bool TryParseTyped(string input, out Quaternion value) + { + if ( + CommandArgParserCommon.TryParseLabeledFloatMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("x", out float lx) + && labeled.TryGetValue("y", out float ly) + && labeled.TryGetValue("z", out float lz) + && labeled.TryGetValue("w", out float lw) + ) + { + value = new Quaternion(lx, ly, lz, lw); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseFloatList( + input.AsSpan(), + out float qx, + out float qy, + out float qz, + out float qw, + out int qcnt + ) + && qcnt == 4 + ) + { + value = new Quaternion(qx, qy, qz, qw); + return true; + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + if ( + split.Length == 4 + && float.TryParse( + split[0], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ) + && float.TryParse( + split[2], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float z + ) + && float.TryParse( + split[3], + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float w + ) + ) + { + value = new Quaternion(x, y, z, w); + return true; + } + + value = default; + return false; + } + } + + public sealed class RectArgParser : ArgParser + { + public static readonly RectArgParser Instance = new RectArgParser(); + + protected override bool TryParseTyped(string input, out Rect value) + { + if ( + CommandArgParserCommon.TryParseLabeledFloatMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("x", out float lx) + && labeled.TryGetValue("y", out float ly) + && labeled.TryGetValue("width", out float lw) + && labeled.TryGetValue("height", out float lh) + ) + { + value = new Rect(lx, ly, lw, lh); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseFloatList( + input.AsSpan(), + out float rx, + out float ry, + out float rw, + out float rh, + out int rcnt + ) + && rcnt == 4 + ) + { + value = new Rect(rx, ry, rw, rh); + return true; + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + if ( + split.Length == 4 + && float.TryParse( + split[0].Replace("x:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float x + ) + && float.TryParse( + split[1].Replace("y:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float y + ) + && float.TryParse( + split[2].Replace("width:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float width + ) + && float.TryParse( + split[3].Replace("height:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out float height + ) + ) + { + value = new Rect(x, y, width, height); + return true; + } + + value = default; + return false; + } + } + + public sealed class RectIntArgParser : ArgParser + { + public static readonly RectIntArgParser Instance = new RectIntArgParser(); + + protected override bool TryParseTyped(string input, out RectInt value) + { + if ( + CommandArgParserCommon.TryParseLabeledIntMap( + input.AsSpan(), + out System.Collections.Generic.Dictionary labeled, + out bool malformed + ) + ) + { + if (malformed) + { + value = default; + return false; + } + if ( + labeled.TryGetValue("x", out int lx) + && labeled.TryGetValue("y", out int ly) + && labeled.TryGetValue("width", out int lw) + && labeled.TryGetValue("height", out int lh) + ) + { + value = new RectInt(lx, ly, lw, lh); + return true; + } + value = default; + return false; + } + if ( + CommandArgParserCommon.TryParseIntList( + input.AsSpan(), + out int rix, + out int riy, + out int riw, + out int rih, + out int ricnt + ) + && ricnt == 4 + ) + { + value = new RectInt(rix, riy, riw, rih); + return true; + } + + string[] split = CommandArgParserCommon.StripAndSplit(input); + if ( + split.Length == 4 + && int.TryParse( + split[0].Replace("x:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int x + ) + && int.TryParse( + split[1].Replace("y:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int y + ) + && int.TryParse( + split[2].Replace("width:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int width + ) + && int.TryParse( + split[3].Replace("height:", string.Empty, StringComparison.OrdinalIgnoreCase), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int height + ) + ) + { + value = new RectInt(x, y, width, height); + return true; + } + + value = default; + return false; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs.meta b/Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs.meta new file mode 100644 index 0000000..6f67e25 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Parsers/UnityArgParsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fd6c16f8e8411794eb5502853c0e4720 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Terminal.cs b/Runtime/CommandTerminal/Backend/Terminal.cs index 2b0ca2d..786c2c1 100644 --- a/Runtime/CommandTerminal/Backend/Terminal.cs +++ b/Runtime/CommandTerminal/Backend/Terminal.cs @@ -27,7 +27,8 @@ public static bool Log(TerminalLogType type, string format, params object[] para string formattedMessage = parameters is { Length: > 0 } ? string.Format(format, parameters) : format; - return buffer.HandleLog(formattedMessage, type); + buffer.EnqueueMessage(formattedMessage, type, includeStackTrace: true); + return true; } } } diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs new file mode 100644 index 0000000..74fbf75 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs @@ -0,0 +1,66 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using UnityEngine; + + [Flags] + public enum TerminalRuntimeModeFlags : int + { + [Obsolete("None disables all runtime features. Choose explicit modes.")] + None = 0, + Editor = 1, + Development = 2, + Production = 4, + All = Editor | Development | Production, + } + + public static class TerminalRuntimeConfig + { +#pragma warning disable CS0618 // Type or member is obsolete + public static TerminalRuntimeModeFlags Mode { get; private set; } = + TerminalRuntimeModeFlags.None; +#pragma warning restore CS0618 // Type or member is obsolete + public static bool EditorAutoDiscover { get; set; } + + public static void SetMode(TerminalRuntimeModeFlags mode) + { + Mode = mode; + } + + public static bool HasFlagNoAlloc( + TerminalRuntimeModeFlags value, + TerminalRuntimeModeFlags flag + ) + { + return ((int)value & (int)flag) == (int)flag; + } + + public static bool ShouldEnableEditorFeatures() + { +#if UNITY_EDITOR + return HasFlagNoAlloc(Mode, TerminalRuntimeModeFlags.Editor); +#else + return false; +#endif + } + + public static bool ShouldEnableDevelopmentFeatures() + { + return HasFlagNoAlloc(Mode, TerminalRuntimeModeFlags.Development) && Debug.isDebugBuild; + } + + public static bool ShouldEnableProductionFeatures() + { + return HasFlagNoAlloc(Mode, TerminalRuntimeModeFlags.Production) && !Debug.isDebugBuild; + } + + public static int TryAutoDiscoverParsers() + { + if (ShouldEnableEditorFeatures() && EditorAutoDiscover) + { + return CommandArg.DiscoverAndRegisterParsers(replaceExisting: false); + } + return 0; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs.meta b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs.meta new file mode 100644 index 0000000..1444045 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bb0a29c7af78db448b2e5832e23c79f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index d61d7c6..36457a8 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -166,6 +166,30 @@ private enum ScrollBarCaptureState private SerializedObject _serializedObject; #endif + // Editor integration +#if UNITY_EDITOR + [Header("Editor")] + [Tooltip( + "When enabled (and runtime mode allows Editor), the terminal will auto-discover IArgParser implementations on reload/start." + )] + [SerializeField] + private bool _autoDiscoverParsersInEditor; +#endif + + [Header("Runtime Mode")] + [Tooltip( + "Controls which environment-specific features are enabled. Choose explicit modes. None is obsolete." + )] + [SerializeField] +#pragma warning disable CS0618 // Type or member is obsolete + private Backend.TerminalRuntimeModeFlags _runtimeModes = Backend + .TerminalRuntimeModeFlags + .None; +#pragma warning restore CS0618 // Type or member is obsolete + + // Test helper to skip building UI entirely (prevents UI Toolkit panel updates) + internal bool disableUIForTests; + private TerminalState _state = TerminalState.Closed; private float _currentWindowHeight; private float _targetWindowHeight; @@ -225,6 +249,11 @@ public TerminalUI() private void Awake() { + Backend.TerminalRuntimeConfig.SetMode(_runtimeModes); +#if UNITY_EDITOR + Backend.TerminalRuntimeConfig.EditorAutoDiscover = _autoDiscoverParsersInEditor; +#endif + Backend.TerminalRuntimeConfig.TryAutoDiscoverParsers(); switch (_logBufferSize) { case <= 0: @@ -406,6 +435,8 @@ private void Start() private void LateUpdate() { ResetWindowIdempotent(); + // Drain any cross-thread logs into the main-thread buffer before refreshing UI + Terminal.Buffer?.DrainPending(); HandleHeightAnimation(); RefreshUI(); _commandIssuedThisFrame = false; @@ -720,6 +751,14 @@ private void ResetWindowIdempotent() private void SetupUI() { + if (disableUIForTests) + { + return; + } + if (_uiDocument == null) + { + _uiDocument = GetComponent(); + } if (_uiDocument == null) { Debug.LogError("No UIDocument assigned, cannot setup UI.", this); @@ -733,7 +772,6 @@ private void SetupUI() return; } - SetFont(_persistedFont); uiRoot.Clear(); VisualElement root = new(); uiRoot.Add(root); @@ -742,6 +780,11 @@ private void SetupUI() InitializeTheme(root); InitializeFont(); + // Ensure a font is set after initialization + if (CurrentFont != null) + { + SetFont(CurrentFont, persist: false); + } if (!string.IsNullOrWhiteSpace(_runtimeTheme)) { @@ -850,7 +893,7 @@ private void InitializeTheme(VisualElement root) { if (_themePack == null) { - Debug.LogError("No theme pack assigned, cannot initialize theme.", this); + Debug.LogWarning("No theme pack assigned, cannot initialize theme.", this); return; } @@ -912,15 +955,25 @@ private void InitializeTheme(VisualElement root) } else { - Debug.LogError("No available terminal themes.", this); + Debug.LogWarning("No available terminal themes.", this); } } + // Support method for tests and tooling to inject theme/font packs before enabling + public void InjectPacks( + Themes.TerminalThemePack themePack, + Themes.TerminalFontPack fontPack + ) + { + _themePack = themePack; + _fontPack = fontPack; + } + private void InitializeFont() { if (_fontPack == null) { - Debug.LogError("No font pack assigned, cannot initialize font.", this); + Debug.LogWarning("No font pack assigned, cannot initialize font.", this); return; } @@ -957,7 +1010,7 @@ private void InitializeFont() if (_runtimeFont == null) { - Debug.LogError("No font assigned, defaulting to Courier New 16pt", this); + Debug.LogWarning("No font assigned, defaulting to Courier New 16pt", this); _runtimeFont = Font.CreateDynamicFontFromOSFont("Courier New", 16); } else @@ -2136,7 +2189,7 @@ private void HandleHeightAnimation() private static void HandleUnityLog(string message, string stackTrace, LogType type) { - Terminal.Buffer?.HandleLog(message, stackTrace, (TerminalLogType)type); + Terminal.Buffer?.EnqueueUnityLog(message, stackTrace, (TerminalLogType)type); } } } diff --git a/Runtime/DataStructures/CyclicBuffer.cs b/Runtime/DataStructures/CyclicBuffer.cs index 8d2d878..1bed6ae 100644 --- a/Runtime/DataStructures/CyclicBuffer.cs +++ b/Runtime/DataStructures/CyclicBuffer.cs @@ -3,7 +3,6 @@ namespace WallstopStudios.DxCommandTerminal.DataStructures using System; using System.Collections; using System.Collections.Generic; - using System.Linq; using Extensions; [Serializable] @@ -78,10 +77,13 @@ public CyclicBuffer(int capacity, IEnumerable initialContents = null) Capacity = capacity; _position = 0; Count = 0; - _buffer = new List(); - foreach (T item in initialContents ?? Enumerable.Empty()) + _buffer = new List(capacity); + if (initialContents != null) { - Add(item); + foreach (T item in initialContents) + { + Add(item); + } } } diff --git a/Runtime/Extensions/SerializedPropertyExtensions.cs b/Runtime/Extensions/SerializedPropertyExtensions.cs index 6f9f8bc..3940f3e 100644 --- a/Runtime/Extensions/SerializedPropertyExtensions.cs +++ b/Runtime/Extensions/SerializedPropertyExtensions.cs @@ -13,7 +13,7 @@ internal static class FieldAccessorFactory { internal static Func CreateFieldGetter(FieldInfo field) { -#if WEB_GL +#if ENABLE_IL2CPP || UNITY_WEBGL return field.GetValue; #else DynamicMethod dynamicMethod = new( diff --git a/Runtime/Helper/DirectoryHelper.cs b/Runtime/Helper/DirectoryHelper.cs index 463140d..27b87fe 100644 --- a/Runtime/Helper/DirectoryHelper.cs +++ b/Runtime/Helper/DirectoryHelper.cs @@ -107,23 +107,19 @@ internal static string AbsoluteToUnityRelativePath(string absolutePath) if (absolutePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) { - // +1 to remove the leading slash only if projectRoot doesn't end with one int startIndex = projectRoot.EndsWith("/", StringComparison.OrdinalIgnoreCase) ? projectRoot.Length : projectRoot.Length + 1; - return absolutePath.Length > startIndex ? absolutePath[startIndex..] : string.Empty; - } - if (absolutePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) - { - int startIndex = projectRoot.EndsWith("/", StringComparison.OrdinalIgnoreCase) - ? projectRoot.Length - : projectRoot.Length + 1; - if (startIndex < absolutePath.Length) + string tail = + absolutePath.Length > startIndex ? absolutePath[startIndex..] : string.Empty; + if (string.IsNullOrEmpty(tail)) { - return "Assets/" + absolutePath[startIndex..]; + return string.Empty; } - - return "Assets"; + // Ensure path starts with Assets/ + return tail.StartsWith("Assets", StringComparison.OrdinalIgnoreCase) + ? tail + : ($"Assets/{tail}"); } return string.Empty; diff --git a/Runtime/Utils.meta b/Runtime/Utils.meta new file mode 100644 index 0000000..f068964 --- /dev/null +++ b/Runtime/Utils.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 421a9b98bbbe847408760e35de6326e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utils/DxArrayPools.cs b/Runtime/Utils/DxArrayPools.cs new file mode 100644 index 0000000..84954fb --- /dev/null +++ b/Runtime/Utils/DxArrayPools.cs @@ -0,0 +1,119 @@ +namespace WallstopStudios.DxCommandTerminal.Utils +{ + using System; + using System.Collections.Concurrent; + + internal static class DxArrayPool + { + private static readonly ConcurrentDictionary> Pool = new(); + private static readonly Action OnDispose = Release; + + internal static ArrayLease Get(int size) + { + return Get(size, out _); + } + + internal static ArrayLease Get(int size, out T[] array) + { + switch (size) + { + case < 0: + throw new ArgumentOutOfRangeException( + nameof(size), + size, + "Must be non-negative." + ); + case 0: + array = Array.Empty(); + return new ArrayLease(array, _ => { }); + } + + ConcurrentStack stack = Pool.GetOrAdd(size, _ => new ConcurrentStack()); + if (!stack.TryPop(out array)) + { + array = new T[size]; + } + + return new ArrayLease(array, OnDispose); + } + + private static void Release(T[] array) + { + int length = array.Length; + if (length == 0) + { + return; + } + Array.Clear(array, 0, length); + Pool.GetOrAdd(length, _ => new ConcurrentStack()).Push(array); + } + } + + internal static class DxFastArrayPool + { + private static readonly ConcurrentDictionary> Pool = new(); + private static readonly Action OnDispose = Release; + + internal static ArrayLease Get(int size) + { + return Get(size, out _); + } + + internal static ArrayLease Get(int size, out T[] array) + { + switch (size) + { + case < 0: + throw new ArgumentOutOfRangeException( + nameof(size), + size, + "Must be non-negative." + ); + case 0: + array = Array.Empty(); + return new ArrayLease(array, _ => { }); + } + + ConcurrentStack stack = Pool.GetOrAdd(size, _ => new ConcurrentStack()); + if (!stack.TryPop(out array)) + { + array = new T[size]; + } + + return new ArrayLease(array, OnDispose); + } + + private static void Release(T[] array) + { + int length = array.Length; + if (length == 0) + { + return; + } + Pool.GetOrAdd(length, _ => new ConcurrentStack()).Push(array); + } + } + + internal readonly struct ArrayLease : IDisposable + { + public readonly T[] Array; + private readonly Action _onDispose; + private readonly bool _initialized; + + internal ArrayLease(T[] array, Action onDispose) + { + _initialized = true; + Array = array ?? System.Array.Empty(); + _onDispose = onDispose; + } + + public void Dispose() + { + if (!_initialized) + { + return; + } + _onDispose?.Invoke(Array); + } + } +} diff --git a/Runtime/Utils/DxArrayPools.cs.meta b/Runtime/Utils/DxArrayPools.cs.meta new file mode 100644 index 0000000..7ce46d9 --- /dev/null +++ b/Runtime/Utils/DxArrayPools.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d670d623876361c4aabcca4b1d4e3823 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/ArrayPoolTests.cs b/Tests/Runtime/ArrayPoolTests.cs new file mode 100644 index 0000000..21bdaba --- /dev/null +++ b/Tests/Runtime/ArrayPoolTests.cs @@ -0,0 +1,125 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections; + using System.Linq; + using System.Threading.Tasks; + using NUnit.Framework; + using UI; + using UnityEngine.TestTools; + using Utils; + + public sealed class ArrayPoolTests + { + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + UnityEngine.Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + [Test] + public void ZeroSizeReturnsEmpty() + { + using ArrayLease lease = DxArrayPool.Get(0, out int[] arr); + Assert.AreSame(Array.Empty(), arr); + + using ArrayLease fastLease = DxFastArrayPool.Get(0, out int[] arr2); + Assert.AreSame(Array.Empty(), arr2); + } + + [Test] + public void LeaseReturnsCorrectSize() + { + using ArrayLease lease = DxArrayPool.Get(128, out byte[] arr); + Assert.AreEqual(128, arr.Length); + + using ArrayLease fastLease = DxFastArrayPool.Get(256, out byte[] arr2); + Assert.AreEqual(256, arr2.Length); + } + + [Test] + public void ClearingBehaviorForDxArrayPool() + { + byte[] original; + using (ArrayLease lease = DxArrayPool.Get(4, out original)) + { + for (int i = 0; i < original.Length; ++i) + { + original[i] = 0xFF; + } + } + using ArrayLease lease2 = DxArrayPool.Get(4, out byte[] second); + Assert.AreSame(original, second); + Assert.IsTrue(second.All(v => v == 0)); + } + + [Test] + public void FastPoolDoesNotClear() + { + byte[] original; + using (ArrayLease lease = DxFastArrayPool.Get(4, out original)) + { + for (int i = 0; i < original.Length; ++i) + { + original[i] = 0x7F; + } + } + using ArrayLease lease2 = DxFastArrayPool.Get(4, out byte[] second); + Assert.AreSame(original, second); + Assert.IsTrue(second.All(v => v == 0x7F)); + } + + [UnityTest] + public IEnumerator ConcurrencySanity() + { + // Spawn a terminal to match other playmode tests style (not strictly required) + yield return TerminalTests.SpawnTerminal(resetStateOnInit: true); + + int tasks = 4; + int iterations = 500; + Task[] workers = new Task[tasks]; + for (int t = 0; t < tasks; ++t) + { + workers[t] = Task.Run(() => + { + Random rand = new Random(Environment.TickCount + t); + for (int i = 0; i < iterations; ++i) + { + int size = rand.Next(1, 128); + using ArrayLease lease = DxArrayPool.Get(size, out int[] a); + if (a.Length > 0) + { + a[0] = 123; + } + } + }); + } + + while (true) + { + bool all = true; + foreach (Task w in workers) + { + all &= w.IsCompleted; + } + if (all) + { + break; + } + yield return null; + } + } + + [Test] + public void ReuseReturnsSameInstance() + { + int[] first; + using (ArrayLease l1 = DxArrayPool.Get(64, out first)) { } + using ArrayLease l2 = DxArrayPool.Get(64, out int[] second); + Assert.AreSame(first, second); + } + } +} diff --git a/Tests/Runtime/ArrayPoolTests.cs.meta b/Tests/Runtime/ArrayPoolTests.cs.meta new file mode 100644 index 0000000..3cb99e8 --- /dev/null +++ b/Tests/Runtime/ArrayPoolTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86e5598a34e445048a9dceab31a560a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/CommandShellEscapingTests.cs b/Tests/Runtime/CommandShellEscapingTests.cs new file mode 100644 index 0000000..460ce65 --- /dev/null +++ b/Tests/Runtime/CommandShellEscapingTests.cs @@ -0,0 +1,64 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + + public sealed class CommandShellEscapingTests + { + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + UnityEngine.Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + [UnityTest] + public IEnumerator EscapedQuotesInsideQuotedArgumentAreHandled() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + int logCount = 0; + Exception exception = null; + string expected = @"he said: ""hi"" \back\"; // he said: "hi" \\back\\ + string command = "log \"he said: \\\"hi\\\" \\\\back\\\\\""; + + Application.logMessageReceived += HandleMessageReceived; + try + { + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + shell.RunCommand(command); + Assert.AreEqual(1, logCount); + Assert.IsNull(exception); + } + finally + { + Application.logMessageReceived -= HandleMessageReceived; + } + + yield break; + + void HandleMessageReceived(string message, string stackTrace, LogType type) + { + ++logCount; + try + { + Assert.AreEqual(expected, message); + } + catch (Exception e) + { + exception = e; + throw; + } + } + } + } +} diff --git a/Tests/Runtime/CommandShellEscapingTests.cs.meta b/Tests/Runtime/CommandShellEscapingTests.cs.meta new file mode 100644 index 0000000..a99f8ec --- /dev/null +++ b/Tests/Runtime/CommandShellEscapingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eccbd0d58acefc04f9dd2acbd13027d4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/CommandShellTests.cs b/Tests/Runtime/CommandShellTests.cs index d67a555..1cba45a 100644 --- a/Tests/Runtime/CommandShellTests.cs +++ b/Tests/Runtime/CommandShellTests.cs @@ -5,6 +5,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime using System.Linq; using System.Text; using Backend; + using Components; using NUnit.Framework; using UI; using UnityEngine; @@ -25,7 +26,7 @@ public void TearDown() [UnityTest] public IEnumerator UnescapedQuotes() { - yield return TerminalTests.SpawnTerminal(resetStateOnInit: true); + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); int logCount = 0; Exception exception = null; @@ -98,7 +99,7 @@ void HandleMessageReceived(string message, string stackTrace, LogType type) [UnityTest] public IEnumerator RunCommandLineNominal() { - yield return TerminalTests.SpawnTerminal(resetStateOnInit: true); + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); int logCount = 0; Exception exception = null; diff --git a/Tests/Runtime/Components/TestPacks.cs b/Tests/Runtime/Components/TestPacks.cs new file mode 100644 index 0000000..32d6978 --- /dev/null +++ b/Tests/Runtime/Components/TestPacks.cs @@ -0,0 +1,24 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components +{ + using Themes; + using UnityEngine; + using UnityEngine.UIElements; + + // Test-only pack implementations to populate protected internal lists + public sealed class TestThemePack : TerminalThemePack + { + public void Add(StyleSheet sheet, string name) + { + _themes.Add(sheet); + _themeNames.Add(name); + } + } + + public sealed class TestFontPack : TerminalFontPack + { + public void Add(Font f) + { + _fonts.Add(f); + } + } +} diff --git a/Tests/Runtime/Components/TestPacks.cs.meta b/Tests/Runtime/Components/TestPacks.cs.meta new file mode 100644 index 0000000..f28e483 --- /dev/null +++ b/Tests/Runtime/Components/TestPacks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 881fc06f74e38794c9f6e7212e0a1516 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Components/TestSceneHelpers.cs b/Tests/Runtime/Components/TestSceneHelpers.cs new file mode 100644 index 0000000..ac3dfb5 --- /dev/null +++ b/Tests/Runtime/Components/TestSceneHelpers.cs @@ -0,0 +1,39 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components +{ + using System.Collections; + using UI; + using UnityEngine; + + public static class TestSceneHelpers + { + public static IEnumerator DestroyTerminalAndWait(int frames = 2) + { + if (TerminalUI.Instance != null) + { + Object.Destroy(TerminalUI.Instance.gameObject); + } + for (int i = 0; i < frames; ++i) + { + yield return null; + } + } + + public static IEnumerator CleanRestart(bool resetStateOnInit, int settleFrames = 2) + { + yield return DestroyTerminalAndWait(settleFrames); + yield return TerminalTests.SpawnTerminal(resetStateOnInit); + for (int i = 0; i < settleFrames; ++i) + { + yield return null; + } + } + + public static IEnumerator WaitFrames(int frames) + { + for (int i = 0; i < frames; ++i) + { + yield return null; + } + } + } +} diff --git a/Tests/Runtime/Components/TestSceneHelpers.cs.meta b/Tests/Runtime/Components/TestSceneHelpers.cs.meta new file mode 100644 index 0000000..1f16c41 --- /dev/null +++ b/Tests/Runtime/Components/TestSceneHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96f36b38e7cae664d8a707173098ecc9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/CultureParsingTests.cs b/Tests/Runtime/CultureParsingTests.cs new file mode 100644 index 0000000..4be8658 --- /dev/null +++ b/Tests/Runtime/CultureParsingTests.cs @@ -0,0 +1,70 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Globalization; + using System.Threading; + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class CultureParsingTests + { + [Test] + public void ParsesInvariantFloatsAndVectorsUnderNonUsCulture() + { + CultureInfo previous = Thread.CurrentThread.CurrentCulture; + try + { + Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); + + CommandArg arg = new CommandArg("3.14159"); + Assert.IsTrue(arg.TryGet(out float f)); + Assert.AreEqual(3.14159f, f, 1e-5f); + + arg = new CommandArg("1.5, 2.5, 3.5"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1.5f, v.x, 1e-5f); + Assert.AreEqual(2.5f, v.y, 1e-5f); + Assert.AreEqual(3.5f, v.z, 1e-5f); + } + finally + { + Thread.CurrentThread.CurrentCulture = previous; + } + } + + [Test] + public void ParsesRectQuaternionAndColorUnderNonUsCulture() + { + CultureInfo previous = Thread.CurrentThread.CurrentCulture; + try + { + Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); + + CommandArg rectArg = new CommandArg("1.5, 2.5, 3.5, 4.5"); + Assert.IsTrue(rectArg.TryGet(out Rect r)); + Assert.AreEqual(1.5f, r.x, 1e-5f); + Assert.AreEqual(2.5f, r.y, 1e-5f); + Assert.AreEqual(3.5f, r.width, 1e-5f); + Assert.AreEqual(4.5f, r.height, 1e-5f); + + CommandArg quatArg = new CommandArg("0.1, 0.2, 0.3, 0.4"); + Assert.IsTrue(quatArg.TryGet(out Quaternion q)); + Assert.AreEqual(0.1f, q.x, 1e-5f); + Assert.AreEqual(0.2f, q.y, 1e-5f); + Assert.AreEqual(0.3f, q.z, 1e-5f); + Assert.AreEqual(0.4f, q.w, 1e-5f); + + CommandArg colorArg = new CommandArg("RGBA(0.1,0.2,0.3,0.4)"); + Assert.IsTrue(colorArg.TryGet(out Color c)); + Assert.AreEqual(0.1f, c.r, 1e-5f); + Assert.AreEqual(0.2f, c.g, 1e-5f); + Assert.AreEqual(0.3f, c.b, 1e-5f); + Assert.AreEqual(0.4f, c.a, 1e-5f); + } + finally + { + Thread.CurrentThread.CurrentCulture = previous; + } + } + } +} diff --git a/Tests/Runtime/CultureParsingTests.cs.meta b/Tests/Runtime/CultureParsingTests.cs.meta new file mode 100644 index 0000000..3ecf764 --- /dev/null +++ b/Tests/Runtime/CultureParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 845a512d9f747b74696fabc7032037c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/LoggingThreadSafetyTests.cs b/Tests/Runtime/LoggingThreadSafetyTests.cs new file mode 100644 index 0000000..2adcbf0 --- /dev/null +++ b/Tests/Runtime/LoggingThreadSafetyTests.cs @@ -0,0 +1,70 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using System.Threading.Tasks; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine.TestTools; + + public sealed class LoggingThreadSafetyTests + { + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + UnityEngine.Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + [UnityTest] + public IEnumerator ConcurrentEnqueueDoesNotCrashAndDrains() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + CommandLog buffer = Terminal.Buffer; + Assert.IsNotNull(buffer); + + int initial = buffer.Logs.Count; + int toProduce = 100; + + Task[] tasks = new Task[4]; + for (int t = 0; t < tasks.Length; ++t) + { + tasks[t] = Task.Run(() => + { + for (int i = 0; i < toProduce; ++i) + { + Terminal.Log(TerminalLogType.Message, "threaded log {0}", i); + } + }); + } + + // Wait until all tasks complete + while (true) + { + bool allDone = true; + foreach (Task task in tasks) + { + allDone &= task.IsCompleted; + } + if (allDone) + { + break; + } + yield return null; + } + + // Give a few frames to drain the queue + for (int i = 0; i < 5; ++i) + { + yield return null; + } + + int finalCount = buffer.Logs.Count; + Assert.GreaterOrEqual(finalCount, initial + toProduce * tasks.Length); + } + } +} diff --git a/Tests/Runtime/LoggingThreadSafetyTests.cs.meta b/Tests/Runtime/LoggingThreadSafetyTests.cs.meta new file mode 100644 index 0000000..333e671 --- /dev/null +++ b/Tests/Runtime/LoggingThreadSafetyTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2189a16308565424fbae70f03e3ea398 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers.meta b/Tests/Runtime/Parsers.meta new file mode 100644 index 0000000..5eebaca --- /dev/null +++ b/Tests/Runtime/Parsers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 00154a7348bf5f443a2f28e5050e8126 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/BoolArgParserTests.cs b/Tests/Runtime/Parsers/BoolArgParserTests.cs new file mode 100644 index 0000000..4c1b36f --- /dev/null +++ b/Tests/Runtime/Parsers/BoolArgParserTests.cs @@ -0,0 +1,23 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using Backend.Parsers; + using NUnit.Framework; + + public sealed class BoolArgParserTests + { + [Test] + public void ParsesTrueFalse() + { + Assert.IsTrue(BoolArgParser.Instance.TryParse("true", out object v1)); + Assert.AreEqual(true, v1); + Assert.IsTrue(BoolArgParser.Instance.TryParse("False", out object v2)); + Assert.AreEqual(false, v2); + } + + [Test] + public void RejectsInvalid() + { + Assert.IsFalse(BoolArgParser.Instance.TryParse("notabool", out _)); + } + } +} diff --git a/Tests/Runtime/Parsers/BoolArgParserTests.cs.meta b/Tests/Runtime/Parsers/BoolArgParserTests.cs.meta new file mode 100644 index 0000000..316b4fe --- /dev/null +++ b/Tests/Runtime/Parsers/BoolArgParserTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46ff1d68cfed92e48b60811999c6704c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/EnumArgParserTests.cs b/Tests/Runtime/Parsers/EnumArgParserTests.cs new file mode 100644 index 0000000..dd9a51c --- /dev/null +++ b/Tests/Runtime/Parsers/EnumArgParserTests.cs @@ -0,0 +1,39 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using Backend.Parsers; + using NUnit.Framework; + + public sealed class EnumArgParserTests + { + private enum Sample + { + A = 0, + B = 1, + C = 2, + } + + [Test] + public void ParsesByNameAndOrdinal() + { + Assert.IsTrue(EnumArgParser.TryParse(typeof(Sample), "B", out object v1)); + Assert.AreEqual(Sample.B, v1); + + Assert.IsTrue(EnumArgParser.TryParse(typeof(Sample), "2", out object v2)); + Assert.AreEqual(Sample.C, v2); + } + + [Test] + public void ParsesCaseInsensitive() + { + Assert.IsTrue(EnumArgParser.TryParse(typeof(Sample), "b", out object v)); + Assert.AreEqual(Sample.B, v); + } + + [Test] + public void RejectsInvalid() + { + Assert.IsFalse(EnumArgParser.TryParse(typeof(Sample), "Z", out _)); + Assert.IsFalse(EnumArgParser.TryParse(typeof(Sample), "100", out _)); + } + } +} diff --git a/Tests/Runtime/Parsers/EnumArgParserTests.cs.meta b/Tests/Runtime/Parsers/EnumArgParserTests.cs.meta new file mode 100644 index 0000000..a12a2ba --- /dev/null +++ b/Tests/Runtime/Parsers/EnumArgParserTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d357aad5703762b4293ef728ae43b127 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/MiscArgParsersTests.cs b/Tests/Runtime/Parsers/MiscArgParsersTests.cs new file mode 100644 index 0000000..359c677 --- /dev/null +++ b/Tests/Runtime/Parsers/MiscArgParsersTests.cs @@ -0,0 +1,39 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using System; + using System.Net; + using Backend.Parsers; + using NUnit.Framework; + + public sealed class MiscArgParsersTests + { + [Test] + public void ParsesGuidAndTime() + { + Guid g = Guid.NewGuid(); + Assert.IsTrue(GuidArgParser.Instance.TryParse(g.ToString(), out object gv)); + Assert.AreEqual(g, gv); + + Assert.IsTrue(TimeSpanArgParser.Instance.TryParse("01:02:03", out object ts)); + Assert.AreEqual(TimeSpan.Parse("01:02:03"), ts); + } + + [Test] + public void ParsesDateTimeAndOffset() + { + string dt = "2021-08-01T12:34:56Z"; + Assert.IsTrue(DateTimeArgParser.Instance.TryParse(dt, out object dv)); + Assert.IsTrue(DateTimeOffsetArgParser.Instance.TryParse(dt, out object dov)); + } + + [Test] + public void ParsesVersionAndIPAddress() + { + Assert.IsTrue(VersionArgParser.Instance.TryParse("1.2.3.4", out object v)); + Assert.AreEqual(new Version(1, 2, 3, 4), v); + + Assert.IsTrue(IPAddressArgParser.Instance.TryParse("127.0.0.1", out object ip)); + Assert.AreEqual(IPAddress.Parse("127.0.0.1"), ip); + } + } +} diff --git a/Tests/Runtime/Parsers/MiscArgParsersTests.cs.meta b/Tests/Runtime/Parsers/MiscArgParsersTests.cs.meta new file mode 100644 index 0000000..cb21bb9 --- /dev/null +++ b/Tests/Runtime/Parsers/MiscArgParsersTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa9f656419eec514190773f5e666eef0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/NumericArgParsersTests.cs b/Tests/Runtime/Parsers/NumericArgParsersTests.cs new file mode 100644 index 0000000..265b2e6 --- /dev/null +++ b/Tests/Runtime/Parsers/NumericArgParsersTests.cs @@ -0,0 +1,38 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using Backend.Parsers; + using NUnit.Framework; + + public sealed class NumericArgParsersTests + { + [Test] + public void ParsesIntegers() + { + Assert.IsTrue(IntArgParser.Instance.TryParse("123", out object i)); + Assert.AreEqual(123, i); + + Assert.IsTrue(UIntArgParser.Instance.TryParse("456", out object ui)); + Assert.AreEqual(456u, ui); + + Assert.IsTrue(LongArgParser.Instance.TryParse("9223372036854775807", out object l)); + Assert.AreEqual(9223372036854775807L, l); + } + + [Test] + public void ParsesFloatsDoubles() + { + Assert.IsTrue(FloatArgParser.Instance.TryParse("3.14", out object f)); + Assert.AreEqual(3.14f, (float)f, 1e-4f); + + Assert.IsTrue(DoubleArgParser.Instance.TryParse("2.71828", out object d)); + Assert.AreEqual(2.71828d, d); + } + + [Test] + public void RejectsInvalidNumbers() + { + Assert.IsFalse(IntArgParser.Instance.TryParse("12x", out _)); + Assert.IsFalse(FloatArgParser.Instance.TryParse("x.y", out _)); + } + } +} diff --git a/Tests/Runtime/Parsers/NumericArgParsersTests.cs.meta b/Tests/Runtime/Parsers/NumericArgParsersTests.cs.meta new file mode 100644 index 0000000..dcfda0c --- /dev/null +++ b/Tests/Runtime/Parsers/NumericArgParsersTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 082709cade022f24fadd1b09b8fc6a57 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs new file mode 100644 index 0000000..140cbf5 --- /dev/null +++ b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs @@ -0,0 +1,46 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using Backend; + using Backend.Parsers; + using NUnit.Framework; + + public sealed class ObjectParserRegistryTests + { + [Test] + public void ListsRegisteredTypes() + { + // Ensure we have at least the defaults + CommandArg.UnregisterAllObjectParsers(); + CommandArg.RegisterObjectParser(IntArgParser.Instance, true); + CommandArg.RegisterObjectParser(FloatArgParser.Instance, true); + + var types = CommandArg.GetRegisteredObjectParserTypes(); + CollectionAssert.Contains(types, typeof(int)); + CollectionAssert.Contains(types, typeof(float)); + } + + private sealed class CustomType { } + + private sealed class CustomTypeParser : ArgParser + { + public static readonly CustomTypeParser Instance = new CustomTypeParser(); + + protected override bool TryParseTyped(string input, out CustomType value) + { + value = new CustomType(); + return true; + } + } + + [Test] + public void RegisterAndUnregisterObjectParser() + { + CommandArg.UnregisterObjectParser(typeof(CustomType)); + Assert.IsTrue(CommandArg.RegisterObjectParser(CustomTypeParser.Instance, false)); + var types = CommandArg.GetRegisteredObjectParserTypes(); + CollectionAssert.Contains(types, typeof(CustomType)); + + Assert.IsTrue(CommandArg.UnregisterObjectParser(typeof(CustomType))); + } + } +} diff --git a/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs.meta b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs.meta new file mode 100644 index 0000000..0b32739 --- /dev/null +++ b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6c29f933dd3e9e45b150b04c52ab31a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/ParserDiscoveryTests.cs b/Tests/Runtime/Parsers/ParserDiscoveryTests.cs new file mode 100644 index 0000000..c9a8c87 --- /dev/null +++ b/Tests/Runtime/Parsers/ParserDiscoveryTests.cs @@ -0,0 +1,27 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using Backend; + using NUnit.Framework; + + public sealed class ParserDiscoveryTests + { + [Test] + public void DiscoversAndRegistersBuiltInParsers() + { + int removed = CommandArg.UnregisterAllObjectParsers(); + Assert.GreaterOrEqual(removed, 0); + + // Without object parsers, numeric parsing should fail + CommandArg arg = new CommandArg("42"); + Assert.IsFalse(arg.TryGet(out int _)); + + // Discover and register all IArgParser implementations in loaded assemblies + int added = CommandArg.DiscoverAndRegisterParsers(replaceExisting: true); + Assert.Greater(added, 0); + + // Now parsing should succeed via discovered parsers + Assert.IsTrue(arg.TryGet(out int value)); + Assert.AreEqual(42, value); + } + } +} diff --git a/Tests/Runtime/Parsers/ParserDiscoveryTests.cs.meta b/Tests/Runtime/Parsers/ParserDiscoveryTests.cs.meta new file mode 100644 index 0000000..6b5c774 --- /dev/null +++ b/Tests/Runtime/Parsers/ParserDiscoveryTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 16f47271278527c4c93930941e39bcd6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/RuntimeConfigTests.cs b/Tests/Runtime/Parsers/RuntimeConfigTests.cs new file mode 100644 index 0000000..c512c0a --- /dev/null +++ b/Tests/Runtime/Parsers/RuntimeConfigTests.cs @@ -0,0 +1,43 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using Backend; + using NUnit.Framework; + + public sealed class RuntimeConfigTests + { + [Test] + public void HasFlagNoAlloc_Works() + { + TerminalRuntimeModeFlags m = + TerminalRuntimeModeFlags.Editor | TerminalRuntimeModeFlags.Development; + Assert.IsTrue(TerminalRuntimeConfig.HasFlagNoAlloc(m, TerminalRuntimeModeFlags.Editor)); + Assert.IsTrue( + TerminalRuntimeConfig.HasFlagNoAlloc(m, TerminalRuntimeModeFlags.Development) + ); + Assert.IsFalse( + TerminalRuntimeConfig.HasFlagNoAlloc(m, TerminalRuntimeModeFlags.Production) + ); + } + + [Test] + public void AutoDiscovery_GatedByConfig() + { + // Clean state + CommandArg.UnregisterAllObjectParsers(); + + TerminalRuntimeConfig.SetMode(TerminalRuntimeModeFlags.Editor); + TerminalRuntimeConfig.EditorAutoDiscover = false; + int added = TerminalRuntimeConfig.TryAutoDiscoverParsers(); + Assert.AreEqual(0, added); + + TerminalRuntimeConfig.EditorAutoDiscover = true; + added = TerminalRuntimeConfig.TryAutoDiscoverParsers(); + Assert.Greater(added, 0); + + // Validate a simple parse now succeeds via discovered parsers + CommandArg arg = new CommandArg("123"); + Assert.IsTrue(arg.TryGet(out int value)); + Assert.AreEqual(123, value); + } + } +} diff --git a/Tests/Runtime/Parsers/RuntimeConfigTests.cs.meta b/Tests/Runtime/Parsers/RuntimeConfigTests.cs.meta new file mode 100644 index 0000000..0aa80e5 --- /dev/null +++ b/Tests/Runtime/Parsers/RuntimeConfigTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4967411cd3177fd4384a1d4a622f4763 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/StaticMemberParserTests.cs b/Tests/Runtime/Parsers/StaticMemberParserTests.cs new file mode 100644 index 0000000..9021cff --- /dev/null +++ b/Tests/Runtime/Parsers/StaticMemberParserTests.cs @@ -0,0 +1,28 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using System.Net; + using Backend.Parsers; + using NUnit.Framework; + + public sealed class StaticMemberParserTests + { + [Test] + public void ParsesStaticMembersByName() + { + Assert.IsTrue(StaticMemberParser.TryParse("MaxValue", out int imax)); + Assert.AreEqual(int.MaxValue, imax); + + Assert.IsTrue(StaticMemberParser.TryParse("Any", out IPAddress ipAny)); + Assert.AreEqual(IPAddress.Any, ipAny); + } + + [Test] + public void TryGetUsesStaticMemberParser() + { + // Ensure no object parser interferes for IPAddress + // (StaticMemberParser should still work) + Assert.IsTrue(new Backend.CommandArg("Any").TryGet(out IPAddress any)); + Assert.AreEqual(IPAddress.Any, any); + } + } +} diff --git a/Tests/Runtime/Parsers/StaticMemberParserTests.cs.meta b/Tests/Runtime/Parsers/StaticMemberParserTests.cs.meta new file mode 100644 index 0000000..29120a5 --- /dev/null +++ b/Tests/Runtime/Parsers/StaticMemberParserTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4fadd1f59f9a9445bca0c7547f53d02 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Parsers/UnityArgParsersTests.cs b/Tests/Runtime/Parsers/UnityArgParsersTests.cs new file mode 100644 index 0000000..5a0639d --- /dev/null +++ b/Tests/Runtime/Parsers/UnityArgParsersTests.cs @@ -0,0 +1,30 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers +{ + using Backend.Parsers; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityArgParsersTests + { + [Test] + public void ParsesVector3FromDelimiters() + { + Assert.IsTrue(Vector3ArgParser.Instance.TryParse("1.1,2.2,3.3", out object v)); + Vector3 vec = (Vector3)v; + Assert.AreEqual(1.1f, vec.x, 1e-4f); + Assert.AreEqual(2.2f, vec.y, 1e-4f); + Assert.AreEqual(3.3f, vec.z, 1e-4f); + } + + [Test] + public void ParsesColorRgba() + { + Assert.IsTrue(ColorArgParser.Instance.TryParse("RGBA(0.1,0.2,0.3,0.4)", out object c)); + Color col = (Color)c; + Assert.AreEqual(0.1f, col.r, 1e-4f); + Assert.AreEqual(0.2f, col.g, 1e-4f); + Assert.AreEqual(0.3f, col.b, 1e-4f); + Assert.AreEqual(0.4f, col.a, 1e-4f); + } + } +} diff --git a/Tests/Runtime/Parsers/UnityArgParsersTests.cs.meta b/Tests/Runtime/Parsers/UnityArgParsersTests.cs.meta new file mode 100644 index 0000000..8dbb102 --- /dev/null +++ b/Tests/Runtime/Parsers/UnityArgParsersTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0f054b6fe983bd54b94fb0b83f345654 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index 572cb6e..8378472 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -3,12 +3,14 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime using System.Collections; using System.Collections.Generic; using System.Linq; + using System.Reflection; using Backend; using Components; using NUnit.Framework; using UI; using UnityEngine; using UnityEngine.TestTools; + using UnityEngine.UIElements; public sealed class TerminalTests { @@ -24,7 +26,7 @@ public void TearDown() [UnityTest] public IEnumerator ToggleResetsState() { - yield return SpawnTerminal(resetStateOnInit: true); + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); TerminalUI terminal = TerminalUI.Instance; CommandShell shell = Terminal.Shell; @@ -63,7 +65,7 @@ public IEnumerator ToggleResetsState() [UnityTest] public IEnumerator CleanConstruction() { - yield return SpawnTerminal(resetStateOnInit: true); + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); TerminalUI terminal1 = TerminalUI.Instance; Assert.IsNotNull(terminal1); @@ -76,7 +78,7 @@ public IEnumerator CleanConstruction() CommandAutoComplete autoComplete = Terminal.AutoComplete; Assert.IsNotNull(autoComplete); - yield return SpawnTerminal(resetStateOnInit: false); + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: false); TerminalUI terminal2 = TerminalUI.Instance; Assert.IsNotNull(TerminalUI.Instance); @@ -86,7 +88,7 @@ public IEnumerator CleanConstruction() Assert.AreSame(buffer, Terminal.Buffer); Assert.AreSame(autoComplete, Terminal.AutoComplete); - yield return SpawnTerminal(resetStateOnInit: true); + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); Assert.IsNotNull(TerminalUI.Instance); Assert.AreNotSame(terminal2, TerminalUI.Instance); @@ -104,11 +106,63 @@ public IEnumerator CleanConstruction() internal static IEnumerator SpawnTerminal(bool resetStateOnInit) { - GameObject go = new("Terminal", typeof(StartTracker), typeof(TerminalUI)); - TerminalUI terminal = go.GetComponent(); + GameObject go = new("Terminal"); + go.SetActive(false); + + // In tests we skip building UI entirely to avoid engine panel updates + + // Create lightweight test packs to avoid warnings + var themePack = ScriptableObject.CreateInstance(); + var style = ScriptableObject.CreateInstance(); + themePack.Add(style, "test-theme"); + + var fontPack = ScriptableObject.CreateInstance(); + // UI is disabled during tests; no need to add a real font asset + + StartTracker startTracker = go.AddComponent(); + + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + terminal.InjectPacks(themePack, fontPack); terminal.resetStateOnInit = resetStateOnInit; - StartTracker startTracker = go.GetComponent(); + + go.SetActive(true); yield return new WaitUntil(() => startTracker.Started); + // Ensure the buffer is large enough for concurrency tests + if (Terminal.Buffer != null) + { + Terminal.Buffer.Resize(4096); + } } + + [UnityTest] + public IEnumerator CleanRestartHelperWorks() + { + // Start with reset and capture instances + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + var shell1 = Terminal.Shell; + var history1 = Terminal.History; + var buffer1 = Terminal.Buffer; + var auto1 = Terminal.AutoComplete; + Assert.IsNotNull(shell1); + Assert.IsNotNull(history1); + + // Clean restart without reset should keep instances + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: false); + Assert.AreSame(shell1, Terminal.Shell); + Assert.AreSame(history1, Terminal.History); + Assert.AreSame(buffer1, Terminal.Buffer); + Assert.AreSame(auto1, Terminal.AutoComplete); + + // Clean restart with reset should replace instances + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + Assert.AreNotSame(shell1, Terminal.Shell); + Assert.AreNotSame(history1, Terminal.History); + Assert.AreNotSame(buffer1, Terminal.Buffer); + Assert.AreNotSame(auto1, Terminal.AutoComplete); + } + + // Test-only pack types moved to Components/TestPacks.cs } } diff --git a/Tests/Runtime/UnityBoundaryMalformedTests.cs b/Tests/Runtime/UnityBoundaryMalformedTests.cs new file mode 100644 index 0000000..a6fa3a3 --- /dev/null +++ b/Tests/Runtime/UnityBoundaryMalformedTests.cs @@ -0,0 +1,62 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityBoundaryMalformedTests + { + [Test] + public void Vector2IntOutOfRangePositive() + { + CommandArg arg = new CommandArg("2147483648,0"); // int.MaxValue + 1 + Assert.IsFalse(arg.TryGet(out Vector2Int _)); + } + + [Test] + public void Vector2IntOutOfRangeNegative() + { + CommandArg arg = new CommandArg("-2147483649,0"); // int.MinValue - 1 + Assert.IsFalse(arg.TryGet(out Vector2Int _)); + } + + [Test] + public void Vector3IntOutOfRangeComponent() + { + CommandArg arg = new CommandArg("0,999999999999999999999,0"); + Assert.IsFalse(arg.TryGet(out Vector3Int _)); + } + + [Test] + public void RectIntOutOfRangeWidth() + { + CommandArg arg = new CommandArg("0,0,2147483648,10"); + Assert.IsFalse(arg.TryGet(out RectInt _)); + } + + [Test] + public void RectIntOutOfRangeHeight() + { + CommandArg arg = new CommandArg("0,0,10,2147483648"); + Assert.IsFalse(arg.TryGet(out RectInt _)); + } + + [Test] + public void QuaternionTooManyComponents() + { + CommandArg arg = new CommandArg("0.1,0.2,0.3,0.4,0.5"); + Assert.IsFalse(arg.TryGet(out Quaternion _)); + } + + [Test] + public void ColorRgbaTrailingCommaParsesRgb() + { + CommandArg arg = new CommandArg("RGBA(0.1,0.2,0.3,)"); + Assert.IsTrue(arg.TryGet(out Color c)); + Assert.AreEqual(0.1f, c.r, 1e-4f); + Assert.AreEqual(0.2f, c.g, 1e-4f); + Assert.AreEqual(0.3f, c.b, 1e-4f); + Assert.AreEqual(1.0f, c.a, 1e-4f); + } + } +} diff --git a/Tests/Runtime/UnityBoundaryMalformedTests.cs.meta b/Tests/Runtime/UnityBoundaryMalformedTests.cs.meta new file mode 100644 index 0000000..c0fa3ac --- /dev/null +++ b/Tests/Runtime/UnityBoundaryMalformedTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3ed87c3c204db648b836631122dbc00 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UnityDelimiterParsingTests.cs b/Tests/Runtime/UnityDelimiterParsingTests.cs new file mode 100644 index 0000000..ebd3bcb --- /dev/null +++ b/Tests/Runtime/UnityDelimiterParsingTests.cs @@ -0,0 +1,130 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityDelimiterParsingTests + { + [Test] + public void ParsesVector2WithUnderscores() + { + CommandArg arg = new CommandArg("1.5_2.5"); + Assert.IsTrue(arg.TryGet(out Vector2 v)); + Assert.AreEqual(1.5f, v.x, 1e-4f); + Assert.AreEqual(2.5f, v.y, 1e-4f); + } + + [Test] + public void ParsesVector3WithForwardSlashes() + { + CommandArg arg = new CommandArg("1/2/3"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + } + + [Test] + public void ParsesVector3WithBackslashes() + { + CommandArg arg = new CommandArg("1\\2\\3"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + } + + [Test] + public void ParsesVector4WithIgnoredCharacters() + { + CommandArg arg = new CommandArg("`{(1|2|3|4)}`"); + Assert.IsTrue(arg.TryGet(out Vector4 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + Assert.AreEqual(4f, v.w, 1e-4f); + } + + [Test] + public void ParsesVector3WithLabeledAndMixedWrappers() + { + CommandArg arg = new CommandArg("[(x:1;y:2;z:3)]"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + } + + [Test] + public void ParsesRectWithLabeledAndMixedDelimiters() + { + CommandArg arg = new CommandArg("{x:10;y:20|width:100,height:50}"); + Assert.IsTrue(arg.TryGet(out Rect r)); + Assert.AreEqual(10f, r.x, 1e-4f); + Assert.AreEqual(20f, r.y, 1e-4f); + Assert.AreEqual(100f, r.width, 1e-4f); + Assert.AreEqual(50f, r.height, 1e-4f); + } + + [Test] + public void ParsesQuaternionWithLabelsAndWrappers() + { + CommandArg arg = new CommandArg("<(x:0.1;y:0.2|z:0.3,w:0.4)>"); + Assert.IsTrue(arg.TryGet(out Quaternion q)); + Assert.AreEqual(0.1f, q.x, 1e-4f); + Assert.AreEqual(0.2f, q.y, 1e-4f); + Assert.AreEqual(0.3f, q.z, 1e-4f); + Assert.AreEqual(0.4f, q.w, 1e-4f); + } + + [Test] + public void ParsesVector2IntWithWrappersAndUnderscore() + { + CommandArg arg = new CommandArg(""); + Assert.IsTrue(arg.TryGet(out Vector2Int v)); + Assert.AreEqual(-1, v.x); + Assert.AreEqual(2, v.y); + } + + [Test] + public void ParsesVector3WithSemicolons() + { + CommandArg arg = new CommandArg("1;2;3"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + } + + [Test] + public void ParsesVector2WithColons() + { + CommandArg arg = new CommandArg("1:2"); + Assert.IsTrue(arg.TryGet(out Vector2 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + } + + [Test] + public void ParsesRectWithSemicolons() + { + CommandArg arg = new CommandArg("1;2;3;4"); + Assert.IsTrue(arg.TryGet(out Rect r)); + Assert.AreEqual(1f, r.x, 1e-4f); + Assert.AreEqual(2f, r.y, 1e-4f); + Assert.AreEqual(3f, r.width, 1e-4f); + Assert.AreEqual(4f, r.height, 1e-4f); + } + + [Test] + public void ParsesVector3WithMixedDelimiters() + { + CommandArg arg = new CommandArg("1;2,3"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + } + } +} diff --git a/Tests/Runtime/UnityDelimiterParsingTests.cs.meta b/Tests/Runtime/UnityDelimiterParsingTests.cs.meta new file mode 100644 index 0000000..e1dc6d2 --- /dev/null +++ b/Tests/Runtime/UnityDelimiterParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5512dd8830c80b742a5778a430433aff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UnityExtremeParsingTests.cs b/Tests/Runtime/UnityExtremeParsingTests.cs new file mode 100644 index 0000000..e51df5c --- /dev/null +++ b/Tests/Runtime/UnityExtremeParsingTests.cs @@ -0,0 +1,110 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityExtremeParsingTests + { + [Test] + public void ExtremeVector3LargeMagnitudes() + { + CommandArg arg = new CommandArg("1e20,-2e20,3.4e10"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.IsFalse(float.IsNaN(v.x) || float.IsInfinity(v.x)); + Assert.IsFalse(float.IsNaN(v.y) || float.IsInfinity(v.y)); + Assert.IsFalse(float.IsNaN(v.z) || float.IsInfinity(v.z)); + Assert.Greater(v.x, 0f); + Assert.Less(v.y, 0f); + Assert.Greater(v.z, 0f); + } + + [Test] + public void ExtremeVector4LargeMagnitudes() + { + CommandArg arg = new CommandArg("-5e15,6e15,-7e15,8e15"); + Assert.IsTrue(arg.TryGet(out Vector4 v)); + Assert.IsFalse(float.IsNaN(v.x) || float.IsInfinity(v.x)); + Assert.IsFalse(float.IsNaN(v.y) || float.IsInfinity(v.y)); + Assert.IsFalse(float.IsNaN(v.z) || float.IsInfinity(v.z)); + Assert.IsFalse(float.IsNaN(v.w) || float.IsInfinity(v.w)); + Assert.Less(v.x, 0f); + Assert.Greater(v.y, 0f); + Assert.Less(v.z, 0f); + Assert.Greater(v.w, 0f); + } + + [Test] + public void ExtremeRectLargeMagnitudes() + { + CommandArg arg = new CommandArg("1e10,-1e10,2e10,3e10"); + Assert.IsTrue(arg.TryGet(out Rect r)); + Assert.IsFalse(float.IsNaN(r.x) || float.IsInfinity(r.x)); + Assert.IsFalse(float.IsNaN(r.y) || float.IsInfinity(r.y)); + Assert.IsFalse(float.IsNaN(r.width) || float.IsInfinity(r.width)); + Assert.IsFalse(float.IsNaN(r.height) || float.IsInfinity(r.height)); + Assert.Greater(r.x, 0f); + Assert.Less(r.y, 0f); + Assert.Greater(r.width, 0f); + Assert.Greater(r.height, 0f); + } + + [Test] + public void RectAcceptsNegativeDimensions() + { + CommandArg arg = new CommandArg("10,20,-5,-7"); + Assert.IsTrue(arg.TryGet(out Rect r)); + Assert.AreEqual(10f, r.x, 1e-4f); + Assert.AreEqual(20f, r.y, 1e-4f); + Assert.AreEqual(-5f, r.width, 1e-4f); + Assert.AreEqual(-7f, r.height, 1e-4f); + } + + [Test] + public void ExtremeQuaternionLargeMagnitudes() + { + CommandArg arg = new CommandArg("1e10,2e10,3e10,4e10"); + Assert.IsTrue(arg.TryGet(out Quaternion q)); + Assert.IsFalse(float.IsNaN(q.x) || float.IsInfinity(q.x)); + Assert.IsFalse(float.IsNaN(q.y) || float.IsInfinity(q.y)); + Assert.IsFalse(float.IsNaN(q.z) || float.IsInfinity(q.z)); + Assert.IsFalse(float.IsNaN(q.w) || float.IsInfinity(q.w)); + Assert.Greater(q.x, 0f); + Assert.Greater(q.y, 0f); + Assert.Greater(q.z, 0f); + Assert.Greater(q.w, 0f); + } + + [Test] + public void ReorderedLabelsVector3() + { + CommandArg arg = new CommandArg("z:3 y:2 x:1"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + } + + [Test] + public void ReorderedLabelsRect() + { + CommandArg arg = new CommandArg("width:100 height:50 y:20 x:10"); + Assert.IsTrue(arg.TryGet(out Rect r)); + Assert.AreEqual(10f, r.x, 1e-4f); + Assert.AreEqual(20f, r.y, 1e-4f); + Assert.AreEqual(100f, r.width, 1e-4f); + Assert.AreEqual(50f, r.height, 1e-4f); + } + + [Test] + public void ReorderedLabelsQuaternion() + { + CommandArg arg = new CommandArg("w:0.4 z:0.3 y:0.2 x:0.1"); + Assert.IsTrue(arg.TryGet(out Quaternion q)); + Assert.AreEqual(0.1f, q.x, 1e-4f); + Assert.AreEqual(0.2f, q.y, 1e-4f); + Assert.AreEqual(0.3f, q.z, 1e-4f); + Assert.AreEqual(0.4f, q.w, 1e-4f); + } + } +} diff --git a/Tests/Runtime/UnityExtremeParsingTests.cs.meta b/Tests/Runtime/UnityExtremeParsingTests.cs.meta new file mode 100644 index 0000000..9b5eaf8 --- /dev/null +++ b/Tests/Runtime/UnityExtremeParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5e67cc88f0a200e4d98dbc3b996a2fe3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UnityLabelPermutationSuccessTests.cs b/Tests/Runtime/UnityLabelPermutationSuccessTests.cs new file mode 100644 index 0000000..962d306 --- /dev/null +++ b/Tests/Runtime/UnityLabelPermutationSuccessTests.cs @@ -0,0 +1,59 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityLabelPermutationSuccessTests + { + [Test] + public void Vector2ReorderedLabels() + { + CommandArg arg = new CommandArg("y:2 x:1"); + Assert.IsTrue(arg.TryGet(out Vector2 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + } + + [Test] + public void Vector4ReorderedLabels() + { + CommandArg arg = new CommandArg("w:4 z:3 y:2 x:1"); + Assert.IsTrue(arg.TryGet(out Vector4 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + Assert.AreEqual(4f, v.w, 1e-4f); + } + + [Test] + public void ColorWithLabeledComponentsAndWrappers() + { + CommandArg arg = new CommandArg("{r:0.1 g:0.2 b:0.3 a:0.4}"); + Assert.IsTrue(arg.TryGet(out Color c)); + Assert.AreEqual(0.1f, c.r, 1e-4f); + Assert.AreEqual(0.2f, c.g, 1e-4f); + Assert.AreEqual(0.3f, c.b, 1e-4f); + Assert.AreEqual(0.4f, c.a, 1e-4f); + } + + [Test] + public void Vector2IntReorderedLabels() + { + CommandArg arg = new CommandArg("y:5 x:-3"); + Assert.IsTrue(arg.TryGet(out Vector2Int v)); + Assert.AreEqual(-3, v.x); + Assert.AreEqual(5, v.y); + } + + [Test] + public void Vector3IntReorderedLabels() + { + CommandArg arg = new CommandArg("z:9 y:8 x:7"); + Assert.IsTrue(arg.TryGet(out Vector3Int v)); + Assert.AreEqual(7, v.x); + Assert.AreEqual(8, v.y); + Assert.AreEqual(9, v.z); + } + } +} diff --git a/Tests/Runtime/UnityLabelPermutationSuccessTests.cs.meta b/Tests/Runtime/UnityLabelPermutationSuccessTests.cs.meta new file mode 100644 index 0000000..f2752f7 --- /dev/null +++ b/Tests/Runtime/UnityLabelPermutationSuccessTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d6a3a375ad878c459463269d1491976 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UnityLabeledParsingTests.cs b/Tests/Runtime/UnityLabeledParsingTests.cs new file mode 100644 index 0000000..42fabed --- /dev/null +++ b/Tests/Runtime/UnityLabeledParsingTests.cs @@ -0,0 +1,50 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityLabeledParsingTests + { + [Test] + public void ParsesVector3WithLabeledComponents() + { + CommandArg arg = new CommandArg("x:1.1 y:2.2 z:3.3"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1.1f, v.x, 1e-4f); + Assert.AreEqual(2.2f, v.y, 1e-4f); + Assert.AreEqual(3.3f, v.z, 1e-4f); + } + + [Test] + public void ParsesRectWithLabeledComponents() + { + CommandArg arg = new CommandArg("x:10 y:20 width:100 height:50"); + Assert.IsTrue(arg.TryGet(out Rect r)); + Assert.AreEqual(10f, r.x, 1e-4f); + Assert.AreEqual(20f, r.y, 1e-4f); + Assert.AreEqual(100f, r.width, 1e-4f); + Assert.AreEqual(50f, r.height, 1e-4f); + } + + [Test] + public void ParsesQuaternionWithLabeledComponents() + { + CommandArg arg = new CommandArg("x:0.1,y:0.2,z:0.3,w:0.4"); + Assert.IsTrue(arg.TryGet(out Quaternion q)); + Assert.AreEqual(0.1f, q.x, 1e-4f); + Assert.AreEqual(0.2f, q.y, 1e-4f); + Assert.AreEqual(0.3f, q.z, 1e-4f); + Assert.AreEqual(0.4f, q.w, 1e-4f); + } + + [Test] + public void ParsesVector2IntWithLabeledComponents() + { + CommandArg arg = new CommandArg("x:-3 y:5"); + Assert.IsTrue(arg.TryGet(out Vector2Int v)); + Assert.AreEqual(-3, v.x); + Assert.AreEqual(5, v.y); + } + } +} diff --git a/Tests/Runtime/UnityLabeledParsingTests.cs.meta b/Tests/Runtime/UnityLabeledParsingTests.cs.meta new file mode 100644 index 0000000..12d5cb2 --- /dev/null +++ b/Tests/Runtime/UnityLabeledParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78d127bfa9da4024b902cff3c251b701 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UnityMalformedParsingTests.cs b/Tests/Runtime/UnityMalformedParsingTests.cs new file mode 100644 index 0000000..fb0b965 --- /dev/null +++ b/Tests/Runtime/UnityMalformedParsingTests.cs @@ -0,0 +1,175 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityMalformedParsingTests + { + [Test] + public void Vector3MalformedTooFewComponents() + { + CommandArg arg = new CommandArg("x: y:2 z:"); + Assert.IsFalse(arg.TryGet(out Vector3 _)); + } + + [Test] + public void Vector2IntMalformedMissingNumeric() + { + CommandArg arg = new CommandArg("x: y:5"); + Assert.IsFalse(arg.TryGet(out Vector2Int _)); + } + + [Test] + public void QuaternionMalformedTooFewComponents() + { + CommandArg arg = new CommandArg("x:0.1 y:0.2 z:0.3"); + Assert.IsFalse(arg.TryGet(out Quaternion _)); + } + + [Test] + public void RectMalformedMissingWidth() + { + CommandArg arg = new CommandArg("x:10 y:20 width: height:50"); + Assert.IsFalse(arg.TryGet(out Rect _)); + } + + [Test] + public void RectIntMalformedTooFewNumbers() + { + CommandArg arg = new CommandArg("x:10 y:20"); + Assert.IsFalse(arg.TryGet(out RectInt _)); + } + + [Test] + public void ColorMalformedNonNumericRgba() + { + CommandArg arg = new CommandArg("RGBA(0.1, nope, 0.3, 0.4)"); + Assert.IsFalse(arg.TryGet(out Color _)); + } + + [Test] + public void Vector2MixedInvalidToken() + { + CommandArg arg = new CommandArg("1,foo"); + Assert.IsFalse(arg.TryGet(out Vector2 _)); + } + + [Test] + public void Vector3TooManyComponents() + { + CommandArg arg = new CommandArg("1,2,3,4"); + Assert.IsFalse(arg.TryGet(out Vector3 _)); + } + + [Test] + public void Vector4SingleComponentOnly() + { + CommandArg arg = new CommandArg("1"); + Assert.IsFalse(arg.TryGet(out Vector4 _)); + } + + [Test] + public void Vector2IntNonIntegerComponent() + { + CommandArg arg = new CommandArg("1,2.5"); + Assert.IsFalse(arg.TryGet(out Vector2Int _)); + } + + [Test] + public void Vector3IntTooManyComponents() + { + CommandArg arg = new CommandArg("1,2,3,4"); + Assert.IsFalse(arg.TryGet(out Vector3Int _)); + } + + [Test] + public void RectNonNumericComponent() + { + CommandArg arg = new CommandArg("1,2,three,4"); + Assert.IsFalse(arg.TryGet(out Rect _)); + } + + [Test] + public void RectIntNonIntegerComponent() + { + CommandArg arg = new CommandArg("1,2,3.5,4"); + Assert.IsFalse(arg.TryGet(out RectInt _)); + } + + [Test] + public void QuaternionNonNumericComponent() + { + CommandArg arg = new CommandArg("0.1, nope, 0.3, 0.4"); + Assert.IsFalse(arg.TryGet(out Quaternion _)); + } + + [Test] + public void Vector3OnlyWrappers() + { + CommandArg arg = new CommandArg("[]"); + Assert.IsFalse(arg.TryGet(out Vector3 _)); + arg = new CommandArg("<>"); + Assert.IsFalse(arg.TryGet(out Vector3 _)); + arg = new CommandArg("{}"); + Assert.IsFalse(arg.TryGet(out Vector3 _)); + arg = new CommandArg("()"); + Assert.IsFalse(arg.TryGet(out Vector3 _)); + } + + [Test] + public void RectOnlyDelimiters() + { + CommandArg arg = new CommandArg(",,,"); + Assert.IsFalse(arg.TryGet(out Rect _)); + arg = new CommandArg("___"); + Assert.IsFalse(arg.TryGet(out Rect _)); + arg = new CommandArg("///"); + Assert.IsFalse(arg.TryGet(out Rect _)); + arg = new CommandArg(";;;"); + Assert.IsFalse(arg.TryGet(out Rect _)); + } + + [Test] + public void Vector2OnlyDelimiter() + { + CommandArg arg = new CommandArg(":"); + Assert.IsFalse(arg.TryGet(out Vector2 _)); + } + + [Test] + public void ColorInvalidNamedComponents() + { + CommandArg arg = new CommandArg("red, green, blue"); + Assert.IsFalse(arg.TryGet(out Color _)); + } + + [Test] + public void Vector3DuplicateLabelMissingValue() + { + CommandArg arg = new CommandArg("x:1 x: y:3"); + Assert.IsFalse(arg.TryGet(out Vector3 _)); + } + + [Test] + public void RectDuplicateLabelMissingValue() + { + CommandArg arg = new CommandArg("x:10 y:20 width:100 width: height:50"); + Assert.IsFalse(arg.TryGet(out Rect _)); + } + + [Test] + public void QuaternionDuplicateLabelMissingValue() + { + CommandArg arg = new CommandArg("x:0.1 y:0.2 z:0.3 w:"); + Assert.IsFalse(arg.TryGet(out Quaternion _)); + } + + [Test] + public void Vector2IntDuplicateLabelMissingValue() + { + CommandArg arg = new CommandArg("x:1 x: y:2"); + Assert.IsFalse(arg.TryGet(out Vector2Int _)); + } + } +} diff --git a/Tests/Runtime/UnityMalformedParsingTests.cs.meta b/Tests/Runtime/UnityMalformedParsingTests.cs.meta new file mode 100644 index 0000000..042a239 --- /dev/null +++ b/Tests/Runtime/UnityMalformedParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a66612bd4f669f145ab648c353dd55eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UnityQuotedWrapperParsingTests.cs b/Tests/Runtime/UnityQuotedWrapperParsingTests.cs new file mode 100644 index 0000000..ce3d708 --- /dev/null +++ b/Tests/Runtime/UnityQuotedWrapperParsingTests.cs @@ -0,0 +1,30 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UnityQuotedWrapperParsingTests + { + [Test] + public void ParsesVector3WrappedInSingleQuotes() + { + CommandArg arg = new CommandArg("'[(1,2,3)]'"); + Assert.IsTrue(arg.TryGet(out Vector3 v)); + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(3f, v.z, 1e-4f); + } + + [Test] + public void ParsesRectWrappedInSingleQuotes() + { + CommandArg arg = new CommandArg("'{1;2;3;4}'"); + Assert.IsTrue(arg.TryGet(out Rect r)); + Assert.AreEqual(1f, r.x, 1e-4f); + Assert.AreEqual(2f, r.y, 1e-4f); + Assert.AreEqual(3f, r.width, 1e-4f); + Assert.AreEqual(4f, r.height, 1e-4f); + } + } +} diff --git a/Tests/Runtime/UnityQuotedWrapperParsingTests.cs.meta b/Tests/Runtime/UnityQuotedWrapperParsingTests.cs.meta new file mode 100644 index 0000000..1bb21c0 --- /dev/null +++ b/Tests/Runtime/UnityQuotedWrapperParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8566e18bc0021b74ba0b1d138dff3c58 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UntypedUnityParsingFailureTests.cs b/Tests/Runtime/UntypedUnityParsingFailureTests.cs new file mode 100644 index 0000000..5e62b1c --- /dev/null +++ b/Tests/Runtime/UntypedUnityParsingFailureTests.cs @@ -0,0 +1,62 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UntypedUnityParsingFailureTests + { + [Test] + public void Vector3UntypedTwoComponentsParses() + { + CommandArg arg = new CommandArg("1,2"); + Assert.IsTrue(arg.TryGet(typeof(Vector3), out object obj)); + Vector3 v = (Vector3)obj; + Assert.AreEqual(1f, v.x, 1e-4f); + Assert.AreEqual(2f, v.y, 1e-4f); + Assert.AreEqual(0f, v.z, 1e-4f); + } + + [Test] + public void RectUntypedNonNumeric() + { + CommandArg arg = new CommandArg("1,2,three,4"); + Assert.IsFalse(arg.TryGet(typeof(Rect), out object _)); + } + + [Test] + public void Vector2IntUntypedNonInteger() + { + CommandArg arg = new CommandArg("1,2.5"); + Assert.IsFalse(arg.TryGet(typeof(Vector2Int), out object _)); + } + + [Test] + public void QuaternionUntypedTooManyComponents() + { + CommandArg arg = new CommandArg("0.1,0.2,0.3,0.4,0.5"); + Assert.IsFalse(arg.TryGet(typeof(Quaternion), out object _)); + } + + [Test] + public void ColorUntypedNonNumericRgba() + { + CommandArg arg = new CommandArg("RGBA(0.1, nope, 0.3, 0.4)"); + Assert.IsFalse(arg.TryGet(typeof(Color), out object _)); + } + + [Test] + public void Vector4UntypedTooManyComponents() + { + CommandArg arg = new CommandArg("1,2,3,4,5"); + Assert.IsFalse(arg.TryGet(typeof(Vector4), out object _)); + } + + [Test] + public void ColorUntypedTooFewComponents() + { + CommandArg arg = new CommandArg("0.1,0.2"); + Assert.IsFalse(arg.TryGet(typeof(Color), out object _)); + } + } +} diff --git a/Tests/Runtime/UntypedUnityParsingFailureTests.cs.meta b/Tests/Runtime/UntypedUnityParsingFailureTests.cs.meta new file mode 100644 index 0000000..3e5364c --- /dev/null +++ b/Tests/Runtime/UntypedUnityParsingFailureTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c7548812f09f844c985d8a6db192e78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UntypedUnityParsingTests.cs b/Tests/Runtime/UntypedUnityParsingTests.cs new file mode 100644 index 0000000..d39c492 --- /dev/null +++ b/Tests/Runtime/UntypedUnityParsingTests.cs @@ -0,0 +1,77 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class UntypedUnityParsingTests + { + [Test] + public void ParsesUntypedUnityTypes() + { + CommandArg v3Arg = new CommandArg("1,2,3"); + Assert.IsTrue(v3Arg.TryGet(typeof(Vector3), out object v3Obj)); + Vector3 v3 = (Vector3)v3Obj; + Assert.AreEqual(1f, v3.x, 1e-4f); + Assert.AreEqual(2f, v3.y, 1e-4f); + Assert.AreEqual(3f, v3.z, 1e-4f); + + CommandArg rectArg = new CommandArg("1,2,3,4"); + Assert.IsTrue(rectArg.TryGet(typeof(Rect), out object rectObj)); + Rect r = (Rect)rectObj; + Assert.AreEqual(1f, r.x, 1e-4f); + Assert.AreEqual(2f, r.y, 1e-4f); + Assert.AreEqual(3f, r.width, 1e-4f); + Assert.AreEqual(4f, r.height, 1e-4f); + + CommandArg qArg = new CommandArg("0.1,0.2,0.3,0.4"); + Assert.IsTrue(qArg.TryGet(typeof(Quaternion), out object qObj)); + Quaternion q = (Quaternion)qObj; + Assert.AreEqual(0.1f, q.x, 1e-4f); + Assert.AreEqual(0.2f, q.y, 1e-4f); + Assert.AreEqual(0.3f, q.z, 1e-4f); + Assert.AreEqual(0.4f, q.w, 1e-4f); + + CommandArg riArg = new CommandArg("1,2,3,4"); + Assert.IsTrue(riArg.TryGet(typeof(RectInt), out object riObj)); + RectInt ri = (RectInt)riObj; + Assert.AreEqual(1, ri.x); + Assert.AreEqual(2, ri.y); + Assert.AreEqual(3, ri.width); + Assert.AreEqual(4, ri.height); + } + + [Test] + public void ParsesMoreUntypedUnityTypes() + { + CommandArg v4Arg = new CommandArg("1,2,3,4"); + Assert.IsTrue(v4Arg.TryGet(typeof(Vector4), out object v4Obj)); + Vector4 v4 = (Vector4)v4Obj; + Assert.AreEqual(1f, v4.x, 1e-4f); + Assert.AreEqual(2f, v4.y, 1e-4f); + Assert.AreEqual(3f, v4.z, 1e-4f); + Assert.AreEqual(4f, v4.w, 1e-4f); + + CommandArg v2iArg = new CommandArg("-1,2"); + Assert.IsTrue(v2iArg.TryGet(typeof(Vector2Int), out object v2iObj)); + Vector2Int v2i = (Vector2Int)v2iObj; + Assert.AreEqual(-1, v2i.x); + Assert.AreEqual(2, v2i.y); + + CommandArg v3iArg = new CommandArg("7,8,9"); + Assert.IsTrue(v3iArg.TryGet(typeof(Vector3Int), out object v3iObj)); + Vector3Int v3i = (Vector3Int)v3iObj; + Assert.AreEqual(7, v3i.x); + Assert.AreEqual(8, v3i.y); + Assert.AreEqual(9, v3i.z); + + CommandArg colorArg = new CommandArg("RGBA(0.1,0.2,0.3,0.4)"); + Assert.IsTrue(colorArg.TryGet(typeof(Color), out object cObj)); + Color c = (Color)cObj; + Assert.AreEqual(0.1f, c.r, 1e-4f); + Assert.AreEqual(0.2f, c.g, 1e-4f); + Assert.AreEqual(0.3f, c.b, 1e-4f); + Assert.AreEqual(0.4f, c.a, 1e-4f); + } + } +} diff --git a/Tests/Runtime/UntypedUnityParsingTests.cs.meta b/Tests/Runtime/UntypedUnityParsingTests.cs.meta new file mode 100644 index 0000000..f27cec9 --- /dev/null +++ b/Tests/Runtime/UntypedUnityParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 27150276592b47e4f9208b5e6942194e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UsageHelpTests.cs b/Tests/Runtime/UsageHelpTests.cs new file mode 100644 index 0000000..44c75f9 --- /dev/null +++ b/Tests/Runtime/UsageHelpTests.cs @@ -0,0 +1,57 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + + public sealed class UsageHelpTests + { + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + [UnityTest] + public IEnumerator HelpShowsUsage() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + + // Query help for a known command without hint but known args: time-scale has 1 arg + bool saw = false; + Application.logMessageReceived += OnLog; + try + { + shell.RunCommand("help time-scale"); + // let logs flush + yield return null; + Assert.IsTrue(saw); + } + finally + { + Application.logMessageReceived -= OnLog; + } + + void OnLog(string message, string stack, LogType type) + { + if ( + message != null + && message.ToLowerInvariant().Contains("usage:") + && message.ToLowerInvariant().Contains("time-scale") + ) + { + saw = true; + } + } + } + } +} diff --git a/Tests/Runtime/UsageHelpTests.cs.meta b/Tests/Runtime/UsageHelpTests.cs.meta new file mode 100644 index 0000000..6579695 --- /dev/null +++ b/Tests/Runtime/UsageHelpTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c7c7903ef500014ab78f118892c43a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/VectorParsingTests.cs b/Tests/Runtime/VectorParsingTests.cs new file mode 100644 index 0000000..75bd7c9 --- /dev/null +++ b/Tests/Runtime/VectorParsingTests.cs @@ -0,0 +1,36 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UnityEngine; + + public sealed class VectorParsingTests + { + [Test] + public void Vector3ParsesVariousDelimiters() + { + CommandArg arg = new CommandArg("1.1,2.2,3.3"); + Assert.IsTrue(arg.TryGet(out Vector3 v1)); + Assert.AreEqual(1.1f, v1.x, 1e-4f); + Assert.AreEqual(2.2f, v1.y, 1e-4f); + Assert.AreEqual(3.3f, v1.z, 1e-4f); + + arg = new CommandArg("(1.1;2.2;3.3)"); + Assert.IsTrue(arg.TryGet(out Vector3 v2)); + Assert.AreEqual(1.1f, v2.x, 1e-4f); + Assert.AreEqual(2.2f, v2.y, 1e-4f); + Assert.AreEqual(3.3f, v2.z, 1e-4f); + } + + [Test] + public void ColorParsesRgba() + { + CommandArg arg = new CommandArg("RGBA(0.1,0.2,0.3,0.4)"); + Assert.IsTrue(arg.TryGet(out Color c)); + Assert.AreEqual(0.1f, c.r, 1e-4f); + Assert.AreEqual(0.2f, c.g, 1e-4f); + Assert.AreEqual(0.3f, c.b, 1e-4f); + Assert.AreEqual(0.4f, c.a, 1e-4f); + } + } +} diff --git a/Tests/Runtime/VectorParsingTests.cs.meta b/Tests/Runtime/VectorParsingTests.cs.meta new file mode 100644 index 0000000..6b9ab4d --- /dev/null +++ b/Tests/Runtime/VectorParsingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d66eee6a7798836409e91095752134c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 4a461185e9e2cfd48e2f070aa7772fdf54e517c4 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 12 Oct 2025 20:52:33 -0700 Subject: [PATCH 02/69] PR updates --- .csharpierignore | 1 + .csharpierrc.json | 3 + .editorconfig | 2 +- .github/dependabot.yml | 26 +- .github/scripts/check-markdown-links.ps1 | 77 +++++ .github/scripts/check_markdown_links.py | 89 +++++ .../scripts/check_markdown_url_encoding.py | 74 +++++ .github/scripts/validate_markdown_links.py | 194 +++++++++++ .github/workflows/csharpier-autofix.yml | 152 +++++++++ .github/workflows/format-on-demand.yml | 305 ++++++++++++++++++ .github/workflows/lint-doc-links.yml | 30 ++ .github/workflows/markdown-json.yml | 45 +++ .github/workflows/npm-publish.yml | 2 +- .github/workflows/prettier-autofix.yml | 196 +++++++++++ .github/workflows/update-dotnet-tools.yml | 80 +++++ .github/workflows/yaml-format-lint.yml | 41 +++ .lychee.toml | 26 ++ .markdownlint.json | 21 ++ .markdownlint.jsonc | 21 ++ .markdownlintignore | 8 + .pre-commit-config.yaml | 29 +- .prettierignore | 22 ++ .prettierrc.json | 20 ++ .yamllint.yaml | 31 ++ 24 files changed, 1489 insertions(+), 6 deletions(-) create mode 100644 .csharpierignore create mode 100644 .csharpierrc.json create mode 100644 .github/scripts/check-markdown-links.ps1 create mode 100644 .github/scripts/check_markdown_links.py create mode 100644 .github/scripts/check_markdown_url_encoding.py create mode 100644 .github/scripts/validate_markdown_links.py create mode 100644 .github/workflows/csharpier-autofix.yml create mode 100644 .github/workflows/format-on-demand.yml create mode 100644 .github/workflows/lint-doc-links.yml create mode 100644 .github/workflows/markdown-json.yml create mode 100644 .github/workflows/prettier-autofix.yml create mode 100644 .github/workflows/update-dotnet-tools.yml create mode 100644 .github/workflows/yaml-format-lint.yml create mode 100644 .lychee.toml create mode 100644 .markdownlint.json create mode 100644 .markdownlint.jsonc create mode 100644 .markdownlintignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 .yamllint.yaml diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 0000000..54ac238 --- /dev/null +++ b/.csharpierignore @@ -0,0 +1 @@ +Runtime/Binaries/*.xml diff --git a/.csharpierrc.json b/.csharpierrc.json new file mode 100644 index 0000000..a024fc9 --- /dev/null +++ b/.csharpierrc.json @@ -0,0 +1,3 @@ +{ + "endOfLine": "crlf" +} diff --git a/.editorconfig b/.editorconfig index d88bef4..23dd18e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ [*] -charset = utf-8-bom +charset = utf-8 end_of_line = crlf trim_trailing_whitespace = false insert_final_newline = false diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 61664ba..e808065 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,10 +1,32 @@ version: 2 updates: + # GitHub Actions workflow updates - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "daily" assignees: - wallstop reviewers: - - wallstop \ No newline at end of file + - wallstop + + # NuGet: .csproj/props/targets and .config/dotnet-tools.json (local tools) + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "daily" + assignees: + - wallstop + reviewers: + - wallstop + + # npm/UPM: package.json at repo root (Unity package manifest) + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + versioning-strategy: increase + assignees: + - wallstop + reviewers: + - wallstop diff --git a/.github/scripts/check-markdown-links.ps1 b/.github/scripts/check-markdown-links.ps1 new file mode 100644 index 0000000..b12cf76 --- /dev/null +++ b/.github/scripts/check-markdown-links.ps1 @@ -0,0 +1,77 @@ +Param( + [string]$Root = "." +) + +$ErrorActionPreference = 'Stop' + +function Normalize-Name { + param([string]$s) + if ([string]::IsNullOrWhiteSpace($s)) { return "" } + # Remove extension (like .md), collapse non-alphanumerics, lowercase + $noExt = $s -replace '\.[^\.]+$','' + $normalized = ($noExt -replace '[^A-Za-z0-9]', '') + return $normalized.ToLowerInvariant() +} + +$issueCount = 0 + +# Exclude typical directories that shouldn't be scanned +$excludeDirs = @('.git', 'node_modules', '.vs') + +$mdFiles = Get-ChildItem -Path $Root -Recurse -File -Filter *.md | + Where-Object { $excludeDirs -notcontains $_.Directory.Name } + +# Regex for inline markdown links (exclude images), capture optional title +$pattern = '(?[^\]]+)\]\((?[^)\s]+)(?:\s+"[^"]*")?\)' + +foreach ($file in $mdFiles) { + $lines = Get-Content -LiteralPath $file.FullName -Encoding UTF8 + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + $matches = [System.Text.RegularExpressions.Regex]::Matches($line, $pattern) + foreach ($m in $matches) { + $text = $m.Groups['text'].Value.Trim() + $targetRaw = $m.Groups['target'].Value.Trim() + + # Skip anchors, external links, and mailto + if ($targetRaw -match '^(#|https?://|mailto:|tel:|data:)') { continue } + + # Remove query/anchor for file checks + $targetCore = $targetRaw -replace '[?#].*$','' + + # Decode URL-encoded chars + try { $targetCore = [uri]::UnescapeDataString($targetCore) } catch { } + + # Only care about links to markdown files + if (-not ($targetCore -match '\.md$')) { continue } + + $fileName = [System.IO.Path]::GetFileName($targetCore) + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($targetCore) + + # Fail when the visible link text is the raw file name + $isExactFileName = $text.Equals($fileName, [System.StringComparison]::OrdinalIgnoreCase) + + # Also fail when the visible text looks like a path or ends with .md + # contains path separators and no whitespace (heuristic for raw paths) + $looksLikePath = ($text -match '[\\/]' -and -not ($text -match '\\s')) + $looksLikeMarkdownFileName = $text.Trim().ToLowerInvariant().EndsWith('.md') + + if ($isExactFileName -or $looksLikePath -or $looksLikeMarkdownFileName) { + $issueCount++ + $lineNo = $i + 1 + $msg = "Link text '$text' should be human-readable, not a raw file name or path" + # GitHub Actions annotation + Write-Output "::error file=$($file.FullName),line=$lineNo::$msg (target: $targetRaw)" + } + } + } +} + +if ($issueCount -gt 0) { + Write-Host "Found $issueCount documentation link(s) with non-human-readable text." -ForegroundColor Red + Write-Host "Use a descriptive phrase instead of the raw file name." + exit 1 +} +else { + Write-Host "All markdown links have human-readable text." +} diff --git a/.github/scripts/check_markdown_links.py b/.github/scripts/check_markdown_links.py new file mode 100644 index 0000000..4fe61dd --- /dev/null +++ b/.github/scripts/check_markdown_links.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +import os +import re +import sys +import urllib.parse + + +EXCLUDE_DIRS = {".git", "node_modules", ".vs"} + + +def normalize_name(s: str) -> str: + if not s: + return "" + # remove extension, strip non-alphanumerics, lowercase + base = re.sub(r"\.[^.]+$", "", s) + return re.sub(r"[^A-Za-z0-9]", "", base).lower() + + +LINK_RE = re.compile(r"(?[^\]]+)\]\((?P[^)\s]+)(?:\s+\"[^\"]*\")?\)") + + +def should_check_target(target: str) -> bool: + if re.match(r"^(#|https?://|mailto:|tel:|data:)", target): + return False + # only check links that end in .md (ignoring anchors/query) + core = re.sub(r"[?#].*$", "", target) + try: + core = urllib.parse.unquote(core) + except Exception: + pass + return core.lower().endswith(".md") + + +def main(root: str) -> int: + issues = 0 + for dirpath, dirnames, filenames in os.walk(root): + # prune excluded directories + dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] + for filename in filenames: + if not filename.lower().endswith(".md"): + continue + path = os.path.join(dirpath, filename) + try: + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + except Exception: + continue + for idx, line in enumerate(lines, start=1): + for m in LINK_RE.finditer(line): + text = m.group("text").strip() + target_raw = m.group("target").strip() + if not should_check_target(target_raw): + continue + target_core = re.sub(r"[?#].*$", "", target_raw) + try: + target_core = urllib.parse.unquote(target_core) + except Exception: + pass + file_name = os.path.basename(target_core) + base_name, _ = os.path.splitext(file_name) + + is_exact_file_name = text.lower() == file_name.lower() + looks_like_path = (("/" in text) or ("\\" in text)) and not re.search(r"\s", text) + looks_like_markdown = text.strip().lower().endswith(".md") + + if ( + is_exact_file_name + or looks_like_path + or looks_like_markdown + ): + issues += 1 + msg = f"{path}:{idx}: Link text '{text}' should be human-readable, not a raw file name or path (target: {target_raw})" + print(msg) + + if issues: + print( + f"Found {issues} documentation link(s) with non-human-readable text.", + file=sys.stderr, + ) + print( + "Use a descriptive phrase instead of the raw file name.", file=sys.stderr + ) + return 1 + return 0 + + +if __name__ == "__main__": + root = sys.argv[1] if len(sys.argv) > 1 else "." + sys.exit(main(root)) diff --git a/.github/scripts/check_markdown_url_encoding.py b/.github/scripts/check_markdown_url_encoding.py new file mode 100644 index 0000000..a44da3e --- /dev/null +++ b/.github/scripts/check_markdown_url_encoding.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import os +import re +import sys + + +EXCLUDE_DIRS = {".git", "node_modules", ".vs", ".vscode", "Library", "Temp"} + + +# Inline markdown link or image: ![alt](target "title") or [text](target "title") +INLINE_LINK_RE = re.compile( + r"!?(?P\[(?P[^\]]+)\]\((?P[^)\s]+)(?:\s+\"[^\"]*\")?\))" +) + +# Reference-style link definitions: [id]: target "title" +REF_DEF_RE = re.compile(r"^\s*\[[^\]]+\]:\s*(?P\S+)(?:\s+\"[^\"]*\")?\s*$") + + +def is_external(target: str) -> bool: + return target.startswith("http://") or target.startswith("https://") or target.startswith("mailto:") or target.startswith("tel:") or target.startswith("data:") + + +def has_unencoded_chars(target: str) -> bool: + # Only flag raw spaces or plus signs in the path/query/fragment + return (" " in target) or ("+" in target) + + +def scan_file(path: str) -> int: + issues = 0 + try: + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + except Exception: + return 0 + + for idx, line in enumerate(lines, start=1): + # Inline links/images + for m in INLINE_LINK_RE.finditer(line): + target = m.group("target").strip() + if is_external(target): + continue + if has_unencoded_chars(target): + issues += 1 + print(f"{path}:{idx}: Unencoded character(s) in link target: '{target}'. Encode spaces as %20 and '+' as %2B.") + + # Reference-style link definitions + m = REF_DEF_RE.match(line) + if m: + target = m.group("target").strip() + if not is_external(target) and has_unencoded_chars(target): + issues += 1 + print(f"{path}:{idx}: Unencoded character(s) in link definition: '{target}'. Encode spaces as %20 and '+' as %2B.") + + return issues + + +def main(root: str) -> int: + issues = 0 + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] + for filename in filenames: + if filename.lower().endswith(".md"): + issues += scan_file(os.path.join(dirpath, filename)) + if issues: + print(f"Found {issues} markdown link(s) with unencoded spaces or plus signs.", file=sys.stderr) + print("Please URL-encode spaces as %20 and '+' as %2B in relative links.", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + root = sys.argv[1] if len(sys.argv) > 1 else "." + sys.exit(main(root)) + diff --git a/.github/scripts/validate_markdown_links.py b/.github/scripts/validate_markdown_links.py new file mode 100644 index 0000000..31498e3 --- /dev/null +++ b/.github/scripts/validate_markdown_links.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +import os +import re +import sys +import urllib.parse +from typing import Dict, List, Set, Tuple + + +EXCLUDE_DIRS = {".git", "node_modules", ".vs"} + + +LINK_RE = re.compile(r"(?[^\]]+)\]\((?P[^)\s]+)(?:\s+\"[^\"]*\")?\)") + + +def unescape_uri(s: str) -> str: + try: + return urllib.parse.unquote(s) + except Exception: + return s + + +def normalize_heading_to_id(text: str) -> str: + """ + Approximate GitHub-style anchor slug generation (GFM): + - Lowercase + - Strip Markdown formatting and code ticks + - Remove most punctuation + - Replace whitespace with hyphens + - Collapse multiple hyphens + - Trim leading/trailing hyphens + """ + if not text: + return "" + t = text + # Remove inline code ticks + t = t.replace("`", "") + # Remove Markdown emphasis markers + t = t.replace("*", "").replace("_", "").replace("~", "") + # Remove link/image markup inside headings e.g. [text](url) + t = re.sub(r"!?\[[^\]]*\]\([^)]*\)", "", t) + # Remove HTML tags + t = re.sub(r"<[^>]+>", "", t) + # Lowercase + t = t.lower() + # Replace whitespace with hyphens + t = re.sub(r"\s+", "-", t) + # Remove punctuation except hyphens and alphanumerics + t = re.sub(r"[^a-z0-9-]", "", t) + # Collapse multiple hyphens + t = re.sub(r"-+", "-", t) + # Trim hyphens + t = t.strip("-") + return t + + +def collect_heading_ids(file_path: str) -> Set[str]: + """ + Build the set of anchor IDs generated by headings within a markdown file. + Handles ATX (# ...) and Setext (underlined) headings. Accounts for duplicate + slugs by adding -1, -2, ... suffixes (GitHub behavior). + """ + try: + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + except Exception: + return set() + + ids: Set[str] = set() + slug_counts: Dict[str, int] = {} + + def add_slug_from_text(text: str): + slug = normalize_heading_to_id(text) + if slug == "": + return + count = slug_counts.get(slug, 0) + if count == 0: + final = slug + else: + final = f"{slug}-{count}" + slug_counts[slug] = count + 1 + ids.add(final) + + # ATX headings + atx_re = re.compile(r"^\s{0,3}#{1,6}\s+(.*)$") + + # Walk lines, handle setext headings by looking ahead + i = 0 + while i < len(lines): + line = lines[i].rstrip("\n") + m = atx_re.match(line) + if m: + add_slug_from_text(m.group(1).strip()) + i += 1 + continue + # Setext H1/H2 + if i + 1 < len(lines): + underline = lines[i + 1].rstrip("\n") + if re.match(r"^\s{0,3}=+\s*$", underline) or re.match(r"^\s{0,3}-+\s*$", underline): + add_slug_from_text(line.strip()) + i += 2 + continue + i += 1 + + return ids + + +def is_external(target: str) -> bool: + return bool(re.match(r"^(https?://|mailto:|tel:|data:)", target)) + + +def resolve_path(base_dir: str, target_path: str) -> str: + return os.path.normpath(os.path.join(base_dir, target_path)) + + +def check_internal_link(src_file: str, target_raw: str) -> Tuple[bool, str]: + # Separate fragment + if target_raw.startswith("#"): + # Anchor within same file + frag = target_raw[1:] + anchor = unescape_uri(frag) + anchor = anchor.strip() + anchor = anchor.lower() + ids = collect_heading_ids(src_file) + if anchor in ids: + return True, "" + return False, f"dangling anchor '#{frag}' (no matching heading)" + + # Split off query/fragment + core = re.sub(r"[?#].*$", "", target_raw) + core = unescape_uri(core) + base_dir = os.path.dirname(src_file) + target_fs = resolve_path(base_dir, core) + if not os.path.exists(target_fs): + return False, f"target file not found: {core}" + + # Fragment check if present + m = re.search(r"#(.+)$", target_raw) + if m: + frag = m.group(1) + anchor = unescape_uri(frag).strip().lower() + ids = collect_heading_ids(target_fs) + if anchor not in ids: + return False, f"dangling anchor '#{frag}' in {core}" + + return True, "" + + +def main(paths: List[str]) -> int: + issues = 0 + + def iter_markdown_files() -> List[str]: + files: List[str] = [] + for p in paths: + if os.path.isdir(p): + for dirpath, dirnames, filenames in os.walk(p): + dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS] + for filename in filenames: + if filename.lower().endswith(".md"): + files.append(os.path.join(dirpath, filename)) + else: + if p.lower().endswith(".md") and os.path.exists(p): + files.append(p) + return files + + files = iter_markdown_files() + for path in files: + try: + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + except Exception: + continue + for idx, line in enumerate(lines, start=1): + for m in LINK_RE.finditer(line): + target = m.group("target").strip() + # Skip images, external links, anchors we can't resolve externally here + if is_external(target): + continue + ok, reason = check_internal_link(path, target) + if not ok: + issues += 1 + print(f"{path}:{idx}: Broken link '{target}': {reason}") + + if issues: + print(f"Found {issues} broken internal markdown link(s).", file=sys.stderr) + print("Fix the paths or anchors so links resolve.", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + args = sys.argv[1:] + if not args: + args = ["."] + sys.exit(main(args)) diff --git a/.github/workflows/csharpier-autofix.yml b/.github/workflows/csharpier-autofix.yml new file mode 100644 index 0000000..a020c3d --- /dev/null +++ b/.github/workflows/csharpier-autofix.yml @@ -0,0 +1,152 @@ +name: CSharpier Auto Format + +on: + pull_request: + pull_request_target: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + format: + name: Format and propose changes + if: ${{ github.event_name != 'pull_request_target' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "8.0.x" + + - name: Restore .NET tools + run: dotnet tool restore + + - name: Run CSharpier (format repository) + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login == 'dependabot[bot]' }} + run: dotnet tool run csharpier format + + - name: Commit formatting changes to PR branch (Dependabot only) + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login == 'dependabot[bot]' }} + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore(format): apply CSharpier formatting" + branch: ${{ github.head_ref }} + file_pattern: | + **/*.cs + + - name: Verify formatting (CI gate) + run: dotnet tool run csharpier check . + + format_fork: + name: Fork PR bot formatting PR (Dependabot only) + if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && github.event.pull_request.user.login == 'dependabot[bot]' }} + runs-on: ubuntu-latest + steps: + - name: Checkout fork PR HEAD + uses: actions/checkout@v5 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + persist-credentials: false + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "8.0.x" + + - name: Install CSharpier (pinned) + run: dotnet tool install -g csharpier --version 1.1.2 + + - name: Run CSharpier (format repository) + run: ~/.dotnet/tools/csharpier . + + - name: Detect changes + id: changes + shell: bash + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Create bot branch and push to base repo + if: steps.changes.outputs.has_changes == 'true' + shell: bash + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + BRANCH="bot/csharpier/pr-${{ github.event.pull_request.number }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + # Only stage C# files to avoid touching workflow YAML (requires special permissions) + git add '**/*.cs' + git commit -m "chore(format): apply CSharpier formatting for PR #${{ github.event.pull_request.number }}" + git remote add upstream "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + git fetch upstream + git push upstream "$BRANCH" --force + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Open or update formatting PR in base repo + if: steps.changes.outputs.has_changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const baseRef = context.payload.pull_request.base.ref; + const headBranch = `bot/csharpier/pr-${prNumber}`; + const {owner, repo} = context.repo; + const title = `chore(format): Apply CSharpier to PR #${prNumber}`; + const body = [ + `This automated PR applies CSharpier formatting to the changes from PR #${prNumber}.`, + '', + `- Source PR (fork): #${prNumber}`, + `- Target branch: ${baseRef}`, + '', + 'If this PR is merged, it will include the contributor\'s changes plus required formatting.', + 'You can then close the original PR or ask the author to rebase.', + ].join('\n'); + + // Check if a PR from this branch already exists + const existing = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${headBranch}` }); + if (existing.data.length === 0) { + await github.rest.pulls.create({ owner, repo, head: headBranch, base: baseRef, title, body }); + } + + - name: Comment link on original PR + if: steps.changes.outputs.has_changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const {owner, repo} = context.repo; + const headBranch = `bot/csharpier/pr-${prNumber}`; + // Find the formatting PR we just created or updated + const resp = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${headBranch}` }); + if (resp.data.length > 0) { + const fmtPr = resp.data[0]; + const body = `A formatting PR has been opened: #${fmtPr.number} (applies CSharpier to this PR).`; + // Avoid duplicate comments by checking recent comments + const comments = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 50 }); + const already = comments.data.some( + (c) => + c.body && + c.body.includes(`#${fmtPr.number}`) && + c.user && + c.user.login === 'github-actions[bot]' + ); + if (!already) { + await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); + } + } diff --git a/.github/workflows/format-on-demand.yml b/.github/workflows/format-on-demand.yml new file mode 100644 index 0000000..2c3fa0e --- /dev/null +++ b/.github/workflows/format-on-demand.yml @@ -0,0 +1,305 @@ +name: Opt-in Formatting + +on: + issue_comment: + types: [created] + workflow_dispatch: + inputs: + pr_number: + description: PR number to format + required: true + +permissions: + contents: write + pull-requests: write + +jobs: + by_comment: + name: Run on /format comment + if: >- + ${{ github.event_name == 'issue_comment' && + github.event.action == 'created' && + github.event.issue.pull_request && + (contains(github.event.comment.body, '/format') || contains(github.event.comment.body, '/autofix') || contains(github.event.comment.body, '/lint-fix')) }} + runs-on: ubuntu-latest + steps: + - name: Resolve PR metadata and authorize request + id: meta + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.issue.number; + const pr = (await github.rest.pulls.get({ ...context.repo, pull_number: prNumber })).data; + const commenter = context.payload.comment.user.login; + const prAuthor = pr.user.login; + const assoc = context.payload.comment.author_association; // OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR, etc. + + const isMaintainer = ['OWNER','MEMBER','COLLABORATOR'].includes(assoc); + const isAuthor = commenter === prAuthor; + const allowed = isMaintainer || isAuthor; + + core.setOutput('allowed', String(allowed)); + core.setOutput('pr', String(prNumber)); + core.setOutput('base_ref', pr.base.ref); + core.setOutput('head_repo', pr.head.repo.full_name); + core.setOutput('head_ref', pr.head.ref); + core.setOutput('same_repo', String(pr.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`)); + + - name: Exit if not authorized + if: ${{ steps.meta.outputs.allowed != 'true' }} + run: | + echo "Commenter is not authorized to trigger formatting." 1>&2 + exit 1 + + - name: Checkout PR branch (same-repo) + if: ${{ steps.meta.outputs.same_repo == 'true' }} + uses: actions/checkout@v5 + with: + ref: ${{ steps.meta.outputs.head_ref }} + fetch-depth: 0 + + - name: Checkout PR fork head + if: ${{ steps.meta.outputs.same_repo != 'true' }} + uses: actions/checkout@v5 + with: + repository: ${{ steps.meta.outputs.head_repo }} + ref: ${{ steps.meta.outputs.head_ref }} + persist-credentials: false + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: '20' + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Restore .NET tools + run: dotnet tool restore + + - name: Apply Prettier fixes + run: | + npx --yes prettier@3.3.3 --write "**/*.{md,markdown}" + npx --yes prettier@3.3.3 --write "**/*.{json,asmdef,asmref}" + npx --yes prettier@3.3.3 --write "**/*.{yml,yaml}" + + - name: Apply markdownlint fixes + run: | + npx --yes markdownlint-cli@0.40.0 "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix + + - name: Apply CSharpier formatting + run: dotnet tool run csharpier format + + - name: Detect changes + id: changes + shell: bash + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit changes to PR branch (same-repo) + if: ${{ steps.meta.outputs.same_repo == 'true' && steps.changes.outputs.has_changes == 'true' }} + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore(format): apply requested formatting" + branch: ${{ steps.meta.outputs.head_ref }} + file_pattern: | + **/*.cs + **/*.md + **/*.markdown + **/*.json + **/*.asmdef + **/*.asmref + + - name: Create bot branch and PR (fork) + if: ${{ steps.meta.outputs.same_repo != 'true' && steps.changes.outputs.has_changes == 'true' }} + shell: bash + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + BRANCH="bot/format/pr-${{ steps.meta.outputs.pr }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + # Stage only supported text files; avoid workflows to prevent permission issues + git add '**/*.cs' '**/*.md' '**/*.markdown' '**/*.json' '**/*.asmdef' '**/*.asmref' + git commit -m "chore(format): apply requested formatting for PR #${{ steps.meta.outputs.pr }}" + git remote add upstream "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + git fetch upstream + git push upstream "$BRANCH" --force + + - name: Open/Update formatting PR (fork) + if: ${{ steps.meta.outputs.same_repo != 'true' && steps.changes.outputs.has_changes == 'true' }} + uses: actions/github-script@v7 + with: + script: | + const prNumber = Number(core.getInput('pr')) || Number('${{ steps.meta.outputs.pr }}'); + const baseRef = '${{ steps.meta.outputs.base_ref }}'; + const headBranch = `bot/format/pr-${prNumber}`; + const {owner, repo} = context.repo; + const title = `chore(format): Apply formatting to PR #${prNumber}`; + const body = [ + `This automated PR applies Prettier/markdownlint/CSharpier formatting to the changes from PR #${prNumber}.`, + '', + `- Source PR (fork): #${prNumber}`, + `- Target branch: ${baseRef}`, + ].join('\n'); + const existing = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${headBranch}` }); + if (existing.data.length === 0) { + await github.rest.pulls.create({ owner, repo, head: headBranch, base: baseRef, title, body }); + } + + - name: Comment result on PR + if: ${{ steps.changes.outputs.has_changes == 'true' }} + uses: actions/github-script@v7 + with: + script: | + const prNumber = Number('${{ steps.meta.outputs.pr }}'); + const sameRepo = '${{ steps.meta.outputs.same_repo }}' === 'true'; + const body = sameRepo + ? 'Applied formatting as requested and pushed commits to this PR branch.' + : 'Opened a formatting PR against the base repository with requested fixes.'; + await github.rest.issues.createComment({ ...context.repo, issue_number: prNumber, body }); + + - name: No-op comment (nothing to change) + if: ${{ steps.changes.outputs.has_changes != 'true' }} + uses: actions/github-script@v7 + with: + script: | + const prNumber = Number('${{ steps.meta.outputs.pr }}'); + const body = 'No formatting changes were necessary.'; + await github.rest.issues.createComment({ ...context.repo, issue_number: prNumber, body }); + + by_dispatch: + name: Run via manual dispatch + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + steps: + - name: Resolve PR metadata + id: meta + uses: actions/github-script@v7 + with: + script: | + const prNumber = Number(core.getInput('pr_number')); + if (!prNumber) core.setFailed('pr_number is required'); + const pr = (await github.rest.pulls.get({ ...context.repo, pull_number: prNumber })).data; + core.setOutput('pr', String(prNumber)); + core.setOutput('base_ref', pr.base.ref); + core.setOutput('head_repo', pr.head.repo.full_name); + core.setOutput('head_ref', pr.head.ref); + core.setOutput('same_repo', String(pr.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`)); + + - name: Checkout PR branch (same-repo) + if: ${{ steps.meta.outputs.same_repo == 'true' }} + uses: actions/checkout@v5 + with: + ref: ${{ steps.meta.outputs.head_ref }} + fetch-depth: 0 + + - name: Checkout PR fork head + if: ${{ steps.meta.outputs.same_repo != 'true' }} + uses: actions/checkout@v5 + with: + repository: ${{ steps.meta.outputs.head_repo }} + ref: ${{ steps.meta.outputs.head_ref }} + persist-credentials: false + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: '20' + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Restore .NET tools + run: dotnet tool restore + + - name: Apply Prettier fixes + run: | + npx --yes prettier@3.3.3 --write "**/*.{md,markdown}" + npx --yes prettier@3.3.3 --write "**/*.{json,asmdef,asmref}" + npx --yes prettier@3.3.3 --write "**/*.{yml,yaml}" + + - name: Apply markdownlint fixes + run: | + npx --yes markdownlint-cli@0.40.0 "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix + + - name: Apply CSharpier formatting + run: dotnet tool run csharpier format + + - name: Detect changes + id: changes + shell: bash + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit changes to PR branch (same-repo) + if: ${{ steps.meta.outputs.same_repo == 'true' && steps.changes.outputs.has_changes == 'true' }} + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore(format): apply requested formatting" + branch: ${{ steps.meta.outputs.head_ref }} + file_pattern: | + **/*.cs + **/*.md + **/*.markdown + **/*.json + **/*.asmdef + **/*.asmref + + - name: Create bot branch and PR (fork) + if: ${{ steps.meta.outputs.same_repo != 'true' && steps.changes.outputs.has_changes == 'true' }} + shell: bash + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + BRANCH="bot/format/pr-${{ steps.meta.outputs.pr }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + # Stage only supported text files; avoid workflows to prevent permission issues + git add '**/*.cs' '**/*.md' '**/*.markdown' '**/*.json' '**/*.asmdef' '**/*.asmref' + git commit -m "chore(format): apply requested formatting for PR #${{ steps.meta.outputs.pr }}" + git remote add upstream "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + git fetch upstream + git push upstream "$BRANCH" --force + + - name: Open/Update formatting PR (fork) + if: ${{ steps.meta.outputs.same_repo != 'true' && steps.changes.outputs.has_changes == 'true' }} + uses: actions/github-script@v7 + with: + script: | + const prNumber = Number('${{ steps.meta.outputs.pr }}'); + const baseRef = '${{ steps.meta.outputs.base_ref }}'; + const headBranch = `bot/format/pr-${prNumber}`; + const {owner, repo} = context.repo; + const title = `chore(format): Apply formatting to PR #${prNumber}`; + const body = [ + `This automated PR applies Prettier/markdownlint/CSharpier formatting to the changes from PR #${prNumber}.`, + '', + `- Source PR (fork): #${prNumber}`, + `- Target branch: ${baseRef}`, + ].join('\n'); + const existing = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${headBranch}` }); + if (existing.data.length === 0) { + await github.rest.pulls.create({ owner, repo, head: headBranch, base: baseRef, title, body }); + } + diff --git a/.github/workflows/lint-doc-links.yml b/.github/workflows/lint-doc-links.yml new file mode 100644 index 0000000..c1e9ef2 --- /dev/null +++ b/.github/workflows/lint-doc-links.yml @@ -0,0 +1,30 @@ +name: Lint Docs Links + +on: + push: + branches: + - main + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Run Markdown link linter + shell: pwsh + run: ./scripts/lint-doc-links.ps1 -VerboseOutput + + - name: Check dead links (lychee) + uses: lycheeverse/lychee-action@v2 + with: + args: >- + -c .lychee.toml + --no-progress + --include-fragments + --verbose + "./**/*.md" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/markdown-json.yml b/.github/workflows/markdown-json.yml new file mode 100644 index 0000000..279e583 --- /dev/null +++ b/.github/workflows/markdown-json.yml @@ -0,0 +1,45 @@ +name: Markdown & JSON Lint/Format + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: package.json + + - name: Install dependencies + run: | + if [ -f package-lock.json ]; then + npm ci + else + npm i --no-audit --no-fund + fi + + - name: Prettier check (Markdown) + run: npm run format:md:check + + - name: Prettier check (JSON / asmdef / asmref) + run: npm run format:json:check + + - name: Markdown lint + run: npm run lint:markdown + + - name: Enforce EOL (CRLF) and No BOM + shell: pwsh + run: ./scripts/check-eol.ps1 -VerboseOutput diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 20eb999..1c63eed 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -3,7 +3,7 @@ name: Publish to NPM on: push: branches: - - master + - main paths: - 'package.json' workflow_dispatch: diff --git a/.github/workflows/prettier-autofix.yml b/.github/workflows/prettier-autofix.yml new file mode 100644 index 0000000..235af50 --- /dev/null +++ b/.github/workflows/prettier-autofix.yml @@ -0,0 +1,196 @@ +name: Prettier Auto Fix + +on: + pull_request: + pull_request_target: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + autofix: + name: Format and propose changes + if: ${{ github.event_name != 'pull_request_target' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: package.json + + - name: Install dependencies + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + run: | + if [ -f package-lock.json ]; then + npm ci + else + npm i --no-audit --no-fund + fi + + - name: Run Prettier (write fixes) + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + run: | + npm run format:md + npm run format:json + npm run format:yaml + + - name: Markdownlint (auto-fix) + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + run: npx markdownlint "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix + + - name: Commit formatting changes to PR branch (Dependabot only) + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login == 'dependabot[bot]' }} + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore(format): apply Prettier/markdownlint fixes" + branch: ${{ github.head_ref }} + file_pattern: | + **/*.md + **/*.markdown + **/*.json + **/*.asmdef + **/*.asmref + **/*.yml + **/*.yaml + + - name: Prettier check (Markdown) + run: npm run format:md:check + + - name: Prettier check (JSON / asmdef / asmref) + run: npm run format:json:check + + - name: Prettier check (YAML) + run: npm run format:yaml:check + + - name: Markdown lint (CI gate) + run: npm run lint:markdown + + - name: Enforce EOL (CRLF) and No BOM + shell: pwsh + run: ./scripts/check-eol.ps1 -VerboseOutput + + autofix_fork: + name: Fork PR bot formatting PR (Dependabot only) + if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && github.event.pull_request.user.login == 'dependabot[bot]' }} + runs-on: ubuntu-latest + steps: + - name: Checkout fork PR HEAD + uses: actions/checkout@v5 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + persist-credentials: false + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: package.json + + - name: Install dependencies + run: | + if [ -f package-lock.json ]; then + npm ci + else + npm i --no-audit --no-fund + fi + + - name: Run Prettier (write fixes) + run: | + npm run format:md + npm run format:json + npm run format:yaml + + - name: Markdownlint (auto-fix) + run: npx markdownlint "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix + + - name: Detect changes + id: changes + shell: bash + run: | + if git diff --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + + - name: Create bot branch and push to base repo + if: steps.changes.outputs.has_changes == 'true' + shell: bash + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + BRANCH="bot/prettier/pr-${{ github.event.pull_request.number }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + # Stage only files we format; avoid workflows to prevent permission issues + git add '**/*.md' '**/*.markdown' '**/*.json' '**/*.asmdef' '**/*.asmref' '**/*.yml' '**/*.yaml' + git commit -m "chore(format): apply Prettier/markdownlint fixes for PR #${{ github.event.pull_request.number }}" + git remote add upstream "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + git fetch upstream + git push upstream "$BRANCH" --force + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Open or update formatting PR in base repo + if: steps.changes.outputs.has_changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const baseRef = context.payload.pull_request.base.ref; + const headBranch = `bot/prettier/pr-${prNumber}`; + const {owner, repo} = context.repo; + const title = `chore(format): Apply Prettier/markdownlint to PR #${prNumber}`; + const body = [ + `This automated PR applies Prettier and markdownlint fixes to the changes from PR #${prNumber}.`, + '', + `- Source PR (fork): #${prNumber}`, + `- Target branch: ${baseRef}`, + '', + 'If this PR is merged, it will include the contributor\'s changes plus required formatting.', + 'You can then close the original PR or ask the author to rebase.', + ].join('\n'); + + const existing = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${headBranch}` }); + if (existing.data.length === 0) { + await github.rest.pulls.create({ owner, repo, head: headBranch, base: baseRef, title, body }); + } + + - name: Comment link on original PR + if: steps.changes.outputs.has_changes == 'true' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const {owner, repo} = context.repo; + const headBranch = `bot/prettier/pr-${prNumber}`; + const resp = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${headBranch}` }); + if (resp.data.length > 0) { + const fmtPr = resp.data[0]; + const body = `A formatting PR has been opened: #${fmtPr.number} (applies Prettier/markdownlint fixes to this PR).`; + const comments = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 50 }); + const already = comments.data.some( + (c) => + c.body && + c.body.includes(`#${fmtPr.number}`) && + c.user && + c.user.login === 'github-actions[bot]' + ); + if (!already) { + await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); + } + } diff --git a/.github/workflows/update-dotnet-tools.yml b/.github/workflows/update-dotnet-tools.yml new file mode 100644 index 0000000..8613ee7 --- /dev/null +++ b/.github/workflows/update-dotnet-tools.yml @@ -0,0 +1,80 @@ +name: Update .NET Tools + +on: + schedule: + - cron: '25 5 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-tools: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Restore dotnet tools + run: dotnet tool restore + + - name: Update local dotnet tools + shell: pwsh + run: | + $manifest = Join-Path $PWD ".config/dotnet-tools.json" + if (!(Test-Path $manifest)) { + Write-Host "No dotnet tool manifest found. Skipping." + exit 0 + } + + $json = Get-Content $manifest -Raw | ConvertFrom-Json + $toolIds = @() + if ($json.tools) { + $toolIds = $json.tools.PSObject.Properties.Name + } + + if ($toolIds.Count -eq 0) { + Write-Host "No tools defined in manifest." + exit 0 + } + + foreach ($id in $toolIds) { + Write-Host "Updating $id..." + dotnet tool update $id --local + } + + - name: Detect manifest changes + id: git_changes + run: | + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create Pull Request + if: steps.git_changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v7 + with: + branch: chore/update-dotnet-tools + title: "chore: update .NET local tools" + commit-message: "chore(dotnet-tools): update .config/dotnet-tools.json" + body: | + Automated update of local .NET tools defined in `.config/dotnet-tools.json`. + labels: dependencies + assignees: wallstop + reviewers: wallstop + + - name: No changes summary + if: steps.git_changes.outputs.changed != 'true' + run: | + echo "## .NET tools are up to date" >> $GITHUB_STEP_SUMMARY + diff --git a/.github/workflows/yaml-format-lint.yml b/.github/workflows/yaml-format-lint.yml new file mode 100644 index 0000000..299188e --- /dev/null +++ b/.github/workflows/yaml-format-lint.yml @@ -0,0 +1,41 @@ +name: YAML Format + Lint + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +jobs: + yaml-checks: + name: Prettier and yamllint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node + uses: actions/setup-node@v5 + with: + node-version: "20" + cache: 'npm' + cache-dependency-path: package.json + + - name: Install dependencies + run: | + if [ -f package-lock.json ]; then + npm ci + else + npm i --no-audit --no-fund + fi + + - name: Prettier check (YAML) + run: npm run format:yaml:check + + - name: yamllint + uses: ibiqlik/action-yamllint@v3.1.1 + with: + file_or_dir: . + config_file: .yamllint.yaml + strict: true diff --git a/.lychee.toml b/.lychee.toml new file mode 100644 index 0000000..07ac01b --- /dev/null +++ b/.lychee.toml @@ -0,0 +1,26 @@ +verbosity = "info" +no_progress = true +max_concurrency = 4 +exclude_mail = true + +# Network tuning +timeout = 20 # seconds per request +retries = 3 # retry transient failures +retry_wait_time = 2 # seconds between retries +max_redirects = 10 +user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36" + +# Treat successes and rate-limiting as acceptable in CI +# Accept all 2xx as valid plus 429 (rate limited) +accept = ["200..=299", 429] + +# Only check web links +scheme = ["https", "http"] + +# Ignore common local/test URLs +exclude = [ + "^https?://localhost", + "^http://127\\.0\\.0\\.1", + "^https?://0\\.0\\.0\\.0", + "^file://" +] diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..35e1232 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,21 @@ +{ + "default": true, + // Allow long lines; Prettier handles wrapping policy + "MD013": false, + // Allow inline HTML when needed + "MD033": false, + // First line need not be a top-level heading (many docs start with badges) + "MD041": false, + // Use fenced code blocks consistently (matches Prettier) + "MD046": { "style": "fenced" }, + // Duplicate headings only matter among siblings + "MD024": { "siblings_only": true }, + // Accept ascending ordered list numbers (matches Prettier behavior) + "MD029": { "style": "ordered" }, + // Permit bare URLs; Prettier won’t auto-wrap, and link checks are handled separately + "MD034": false, + // Allow exactly one blank line between blocks (Prettier’s behavior) + "MD012": { "maximum": 1 }, + // Don’t flag the two trailing spaces used for hard line breaks in Markdown + "MD009": { "strict": false } +} diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..35e1232 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,21 @@ +{ + "default": true, + // Allow long lines; Prettier handles wrapping policy + "MD013": false, + // Allow inline HTML when needed + "MD033": false, + // First line need not be a top-level heading (many docs start with badges) + "MD041": false, + // Use fenced code blocks consistently (matches Prettier) + "MD046": { "style": "fenced" }, + // Duplicate headings only matter among siblings + "MD024": { "siblings_only": true }, + // Accept ascending ordered list numbers (matches Prettier behavior) + "MD029": { "style": "ordered" }, + // Permit bare URLs; Prettier won’t auto-wrap, and link checks are handled separately + "MD034": false, + // Allow exactly one blank line between blocks (Prettier’s behavior) + "MD012": { "maximum": 1 }, + // Don’t flag the two trailing spaces used for hard line breaks in Markdown + "MD009": { "strict": false } +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..96a60de --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,8 @@ +node_modules/ +.git/ +Library/ +obj/ +Temp/ +Samples~/ +/.github/ + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3311a9f..c067c2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,8 +15,33 @@ repos: description: Install the .NET tools listed at .config/dotnet-tools.json. - id: csharpier name: Run CSharpier on C# files - entry: dotnet tool run Csharpier format + entry: dotnet tool run csharpier format language: system types: - c# - description: CSharpier is an opinionated C# formatter inspired by Prettier. \ No newline at end of file + description: CSharpier is an opinionated C# formatter inspired by Prettier. + + - repo: local + hooks: + - id: prettier + name: Prettier (Markdown, JSON, asmdef, asmref, YAML) + entry: npx --yes prettier --write + language: system + files: '(?i)\.(md|markdown|json|asmdef|asmref|ya?ml)$' + description: Use the repo's Prettier version from package.json. + + - repo: local + hooks: + - id: markdownlint + name: markdownlint (respect repo config) + entry: npx --yes markdownlint --config .markdownlint.json --ignore-path .markdownlintignore + language: system + files: '(?i)\.(md|markdown)$' + + - repo: local + hooks: + - id: yamllint + name: yamllint (if available) + entry: bash -c 'if command -v yamllint >/dev/null 2>&1; then yamllint -c .yamllint.yaml "$@"; else echo "yamllint not installed; skipping"; fi' -- + language: system + files: '(?i)\.(ya?ml)$' diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a98a735 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,22 @@ +# Node / tooling +node_modules/ + +# Unity / build artifacts +Library/ +obj/ +Temp/ +Logs/ + +# GitHub +.github/ + +# Samples and external content +Samples~/ + +# Only format supported text/assets; Prettier globbing will still be constrained by scripts +**/*.meta +**/*.unity +**/*.prefab +**/*.mat +**/*.asset + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f165df7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "none", + "bracketSpacing": true, + "proseWrap": "preserve", + "endOfLine": "crlf", + "overrides": [ + { + "files": ["*.asmdef", "*.asmref"], + "options": { + "parser": "json" + } + } + ] +} diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..725d438 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,31 @@ +extends: default + +ignore: | + node_modules/ + .git/ + Library/ + obj/ + Temp/ + Samples~/ + +rules: + line-length: + max: 200 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + truthy: disable + document-start: disable + comments-indentation: disable + comments: + min-spaces-from-content: 1 + indentation: + spaces: 2 + indent-sequences: consistent + new-lines: + type: dos + new-line-at-end-of-file: enable + trailing-spaces: enable + empty-lines: + max: 1 + max-start: 0 + max-end: 1 From c05f8630fe49cedd07c99e91248d80c49d987085 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 12 Oct 2025 20:53:03 -0700 Subject: [PATCH 03/69] Reformatting --- Runtime/AssemblyInfo.cs | 2 +- Runtime/AssemblyInfo.cs.meta | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs index f0bc465..e852899 100644 --- a/Runtime/AssemblyInfo.cs +++ b/Runtime/AssemblyInfo.cs @@ -1,3 +1,3 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests.Runtime")] diff --git a/Runtime/AssemblyInfo.cs.meta b/Runtime/AssemblyInfo.cs.meta index feae1a0..4149535 100644 --- a/Runtime/AssemblyInfo.cs.meta +++ b/Runtime/AssemblyInfo.cs.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 4cbbf6127c01481d9ba98e9bad59e641 timeCreated: 1760315470 \ No newline at end of file From b7374c27af707480e2143cb0ca100a321c584cb7 Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 10:15:47 -0700 Subject: [PATCH 04/69] Auto-complete fixes --- .github/workflows/prettier-autofix.yml | 4 +- .gitignore | 3 +- .markdownlint.json | 2 + .pre-commit-config.yaml | 2 +- AGENTS.md | 8 + CHANGELOG.md | 13 +- Editor/Utils.meta | 8 + .../Utils/ScriptableObjectSingletonCreator.cs | 218 ++++++++++++++++++ .../ScriptableObjectSingletonCreator.cs.meta | 11 + README.md | 76 ++++-- .../Attributes/CommandCompleterAttribute.cs | 34 +++ .../CommandCompleterAttribute.cs.meta | 11 + .../Backend/BuiltinCommands.cs | 125 ++++++---- .../Backend/CommandAutoComplete.cs | 136 ++++++++++- .../CommandTerminal/Backend/CommandInfo.cs | 5 +- .../CommandTerminal/Backend/CommandShell.cs | 66 +++++- .../CommandTerminal/Backend/Completers.meta | 8 + .../Completers/FontArgumentCompleter.cs | 39 ++++ .../Completers/FontArgumentCompleter.cs.meta | 11 + .../Completers/ThemeArgumentCompleter.cs | 41 ++++ .../Completers/ThemeArgumentCompleter.cs.meta | 11 + .../Backend/IArgumentCompleter.cs | 42 ++++ .../Backend/IArgumentCompleter.cs.meta | 11 + .../Backend/TerminalRuntimeConfig.cs | 84 ++++++- Runtime/CommandTerminal/UI/TerminalUI.cs | 55 ++++- Runtime/DataStructures/CyclicBuffer.cs | 2 +- Runtime/Internal.meta | 8 + Runtime/Internal/ScriptableObjectSingleton.cs | 135 +++++++++++ .../ScriptableObjectSingleton.cs.meta | 11 + .../ScriptableSingletonPathAttribute.cs | 15 ++ .../ScriptableSingletonPathAttribute.cs.meta | 11 + .../WallstopStudios.DxCommandTerminal.asmdef | 28 ++- doc.md | 63 ++++- 33 files changed, 1182 insertions(+), 115 deletions(-) create mode 100644 Editor/Utils.meta create mode 100644 Editor/Utils/ScriptableObjectSingletonCreator.cs create mode 100644 Editor/Utils/ScriptableObjectSingletonCreator.cs.meta create mode 100644 Runtime/Attributes/CommandCompleterAttribute.cs create mode 100644 Runtime/Attributes/CommandCompleterAttribute.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Completers.meta create mode 100644 Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs create mode 100644 Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs create mode 100644 Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/IArgumentCompleter.cs create mode 100644 Runtime/CommandTerminal/Backend/IArgumentCompleter.cs.meta create mode 100644 Runtime/Internal.meta create mode 100644 Runtime/Internal/ScriptableObjectSingleton.cs create mode 100644 Runtime/Internal/ScriptableObjectSingleton.cs.meta create mode 100644 Runtime/Internal/ScriptableSingletonPathAttribute.cs create mode 100644 Runtime/Internal/ScriptableSingletonPathAttribute.cs.meta diff --git a/.github/workflows/prettier-autofix.yml b/.github/workflows/prettier-autofix.yml index 235af50..ee89e07 100644 --- a/.github/workflows/prettier-autofix.yml +++ b/.github/workflows/prettier-autofix.yml @@ -45,7 +45,7 @@ jobs: - name: Markdownlint (auto-fix) if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} - run: npx markdownlint "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix + run: npx --yes markdownlint-cli "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix - name: Commit formatting changes to PR branch (Dependabot only) if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login == 'dependabot[bot]' }} @@ -113,7 +113,7 @@ jobs: npm run format:yaml - name: Markdownlint (auto-fix) - run: npx markdownlint "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix + run: npx --yes markdownlint-cli "**/*.md" "**/*.markdown" --config .markdownlint.json --ignore-path .markdownlintignore --fix - name: Detect changes id: changes diff --git a/.gitignore b/.gitignore index 9eef072..5a50f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # NPM -node_modules/ \ No newline at end of file +node_modules/ +node_modules.meta \ No newline at end of file diff --git a/.markdownlint.json b/.markdownlint.json index 35e1232..c8c41f1 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,5 +1,7 @@ { "default": true, + // Prefer ATX-style headings ("#"), avoids mixed setext/atx issues + "MD003": { "style": "atx" }, // Allow long lines; Prettier handles wrapping policy "MD013": false, // Allow inline HTML when needed diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c067c2e..fac127c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: hooks: - id: markdownlint name: markdownlint (respect repo config) - entry: npx --yes markdownlint --config .markdownlint.json --ignore-path .markdownlintignore + entry: npx --yes markdownlint-cli --config .markdownlint.json --ignore-path .markdownlintignore language: system files: '(?i)\.(md|markdown)$' diff --git a/AGENTS.md b/AGENTS.md index 4ef91b6..12a26ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ # Repository Guidelines ## Project Structure & Module Organization + - `Runtime/` — core C# code (`WallstopStudios.DxCommandTerminal.asmdef`). - `Editor/` — editor tooling and drawers (`*.Editor.asmdef`). - `Tests/Runtime/` — Unity Test Framework (NUnit) tests (`*Tests.cs`, `*.asmdef`). @@ -9,6 +10,7 @@ - `package.json` — Unity package manifest + npm metadata. See `README.md` for usage. ## Build, Test, and Development Commands + - Install tools: `dotnet tool restore` - Format C#: `dotnet tool run csharpier .` - Optional hooks: install pre-commit — `pre-commit install` @@ -17,6 +19,7 @@ Or use Unity’s Test Runner UI. ## Coding Style & Naming Conventions + - Follow `.editorconfig`: - Indentation: spaces (C# 4), JSON/YAML/asmdef 2. - Line endings: CRLF; encoding: UTF-8 BOM. @@ -27,18 +30,23 @@ - Do not use regions, anywhere, ever. ## Testing Guidelines + - Framework: Unity Test Framework (NUnit) under `Tests/Runtime`. - Conventions: file names `*Tests.cs`, one feature per fixture, deterministic tests. - Run via Unity CLI (above) or Test Runner. Add/adjust tests when changing parsing, history, UI behavior, or input handling. - Do not use regions. - Try to use minimal comments and instead rely on expressive naming conventions and assertions. - Do not use Description annotations for tests. +- Do not create `async Task` test methods - the Unity test runner does not support this. Make do with `IEnumerator` based UnityTestMethods. +- Do not use `Assert.ThrowsAsync`, it does not exist. ## Commit & Pull Request Guidelines + - Commits: imperative, concise subject (≤72 chars), explain “what/why”. Link issues/PRs (`#123`). - PRs: include description, motivation, and test coverage. For UI/USS changes, add before/after screenshots. - Versioning/CI: do not change `package.json` version unless preparing a release; npm publishing is automated via GitHub Actions on version bumps. ## Security & Configuration Tips + - No secrets in repo; publishing uses `NPM_TOKEN` in GitHub secrets. - Target Unity `2021.3+`. Keep `asmdef` names and folder layout intact to preserve assembly boundaries. diff --git a/CHANGELOG.md b/CHANGELOG.md index ffcc7a0..3ddd1b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,19 @@ -Changelog -========= +# Changelog ## 1.02 `08a66da` - 2018-09-14 ### Added + - Variables: defined with `set name value`, accessed with `$name`. Run `set` with no arguments to display all variables and their values. - Option to change the alpha value of the input background texture. - Command hint argument for better error messages. Use `RegisterCommand(Hint = "Command $a")]` to show a command's usage. ### Changed + - Better autocompletion: autocomplete can now partially complete words when there are multiple suggestions available. ### Fixed + - Fix background texture being destroyed when loading a scene with the Terminal set to `DontDestroyOnLoad`. - Fix hotkeys bound to function keys causing the input to not register the first character. - Fix formatting on autocomplete suggestions. @@ -19,16 +21,19 @@ Changelog ## 1.01 `9a1b0b3` - 2018-08-09 ### Added + - Option to to change the position of the toggle GUI buttons. - Option to change the window size ratio between the partial and full window height. - Optional GUI button to run a command (useful for mobile devices). ### Changed + - Autocomplete now uses the last word in the input text, rather than just completing the first word. -## 1.0 `db07b43` - 2018-07-15 +## 1.0 `db07b43` - 2018-07-15 ### Added + - Customizable toggle hotkey. - Two new terminal colors (customizable). - Option to change prompt character (or remove it). @@ -38,8 +43,10 @@ Changelog - Option to customize the input background contrast. ### Fixed + - Input registering hotkey character when hotkey was pressed. - Inspector presentation. ### Removed + - `LS` command in favor of `HELP` with no arguments to list all registered commands. diff --git a/Editor/Utils.meta b/Editor/Utils.meta new file mode 100644 index 0000000..50a216d --- /dev/null +++ b/Editor/Utils.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 032967df8482c5340adb0e8ce2dee016 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Utils/ScriptableObjectSingletonCreator.cs b/Editor/Utils/ScriptableObjectSingletonCreator.cs new file mode 100644 index 0000000..c8c77fd --- /dev/null +++ b/Editor/Utils/ScriptableObjectSingletonCreator.cs @@ -0,0 +1,218 @@ +namespace WallstopStudios.DxCommandTerminal.Editor.Utils +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + using UnityEditor; + using UnityEngine; + using WallstopStudios.DxCommandTerminal.Internal; + + [InitializeOnLoad] + public static class ScriptableObjectSingletonCreator + { + private const string ResourcesRoot = "Assets/Resources"; + private static bool _ensuring; + + static ScriptableObjectSingletonCreator() + { + EnsureSingletonAssets(); + } + + private static bool IsDerivedFromScriptableSingleton(Type t) + { + if (t == null || t.IsAbstract || t.IsGenericType) + { + return false; + } + Type baseType = t; + while (baseType != null) + { + if (baseType.IsGenericType) + { + Type def = baseType.GetGenericTypeDefinition(); + if (def == typeof(ScriptableObjectSingleton<>)) + { + return true; + } + } + baseType = baseType.BaseType; + } + return false; + } + + public static void EnsureSingletonAssets() + { + if (_ensuring) + { + return; + } + + _ensuring = true; + try + { + EnsureFolder(ResourcesRoot); + + // Collect all concrete types deriving from our singleton base + List candidates = new List(); + foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + try + { + types = asm.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + types = ex.Types.Where(x => x != null).ToArray(); + } + + foreach (Type t in types) + { + if (IsDerivedFromScriptableSingleton(t)) + { + candidates.Add(t); + } + } + } + + // Simple collision detection by simple name + var collisions = candidates + .GroupBy(t => t.Name, StringComparer.OrdinalIgnoreCase) + .Where(g => g.Count() > 1) + .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase); + + foreach (Type type in candidates) + { + if (collisions.ContainsKey(type.Name)) + { + Debug.LogWarning( + $"ScriptableObjectSingletonCreator: Multiple types share the name '{type.Name}'. Skipping auto-creation. Add [ScriptableSingletonPath] to disambiguate or rename. Types: {string.Join(", ", collisions[type.Name].Select(x => x.FullName))}" + ); + continue; + } + + string sub = GetResourcesSubFolder(type); + string parent = string.IsNullOrWhiteSpace(sub) + ? ResourcesRoot + : PathCombine(ResourcesRoot, sub); + + EnsureFolder(parent); + + string assetPath = PathCombine(parent, type.Name + ".asset"); + + UnityEngine.Object atPath = AssetDatabase.LoadAssetAtPath(assetPath, type); + if (atPath != null) + { + continue; + } + + // Try to find any existing asset of exact type and move it + string[] guids = AssetDatabase.FindAssets("t:" + type.Name); + bool moved = false; + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrWhiteSpace(path)) + { + continue; + } + UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath(path, type); + if (obj == null) + { + continue; + } + + // Don't overwrite an existing file at the intended target + if ( + File.Exists(assetPath) + && !string.Equals(path, assetPath, StringComparison.OrdinalIgnoreCase) + ) + { + Debug.LogWarning( + $"ScriptableObjectSingletonCreator: Target path already occupied at {assetPath}. Skipping move for {type.FullName}." + ); + moved = true; // treat as handled to avoid creating duplicate + break; + } + + string result = AssetDatabase.MoveAsset(path, assetPath); + if (string.IsNullOrEmpty(result)) + { + moved = true; + break; + } + else + { + Debug.LogWarning( + $"ScriptableObjectSingletonCreator: Failed to move existing {type.FullName} from {path} to {assetPath}: {result}" + ); + } + } + + if (!moved) + { + ScriptableObject created = ScriptableObject.CreateInstance(type); + AssetDatabase.CreateAsset(created, assetPath); + } + } + + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + finally + { + _ensuring = false; + } + } + + private static string GetResourcesSubFolder(Type type) + { + ScriptableSingletonPathAttribute attr = + type.GetCustomAttribute(); + if (attr == null) + { + return string.Empty; + } + string p = (attr.resourcesPath ?? string.Empty).Trim(); + if (p.StartsWith("/")) + { + p = p.TrimStart('/'); + } + return p.Replace('\\', '/'); + } + + private static void EnsureFolder(string folder) + { + folder = folder.Replace('\\', '/'); + if (AssetDatabase.IsValidFolder(folder)) + { + return; + } + + string[] parts = folder.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + { + return; + } + string current = parts[0]; + for (int i = 1; i < parts.Length; i++) + { + string next = current + "/" + parts[i]; + if (!AssetDatabase.IsValidFolder(next)) + { + AssetDatabase.CreateFolder(current, parts[i]); + } + current = next; + } + } + + private static string PathCombine(string a, string b) + { + return (a.TrimEnd('/', '\\') + "/" + b.TrimStart('/', '\\')).Replace('\\', '/'); + } + } +#endif +} diff --git a/Editor/Utils/ScriptableObjectSingletonCreator.cs.meta b/Editor/Utils/ScriptableObjectSingletonCreator.cs.meta new file mode 100644 index 0000000..a482744 --- /dev/null +++ b/Editor/Utils/ScriptableObjectSingletonCreator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3ff06218af7bc043b9aa7bb1892633c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 21d4f48..3ddf95e 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,47 @@ -DX Command Terminal -====================== +# DX Command Terminal + +## CI/CD Status -# CI/CD Status ![Npm Publish](https://github.com/wallstop/dxcommandterminal/actions/workflows/npm-publish.yml/badge.svg) -# Notice +## Notice This is a fork of [Command Terminal](https://github.com/stillwwater/command_terminal) for Unity, mainly to address usability gaps and add maintenance -# Compatibility -| Platform | Compatible | -| --- | --- | +## Compatibility + +| Platform | Compatible | +| ---------- | -------------------- | | Unity 2021 | Likely, but untested | -| Unity 2022 | ✓ | -| Unity 2023 | ✓ | -| Unity 6 | ✓ | -| URP | ✓ | -| HDRP | ✓ | +| Unity 2022 | ✓ | +| Unity 2023 | ✓ | +| Unity 6 | ✓ | +| URP | ✓ | +| HDRP | ✓ | -# Installation +## Installation ## From Releases + Check out the latest [Releases](https://github.com/wallstop/DxCommandTerminal/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` + - 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](https://github.com/wallstop/DxCommandTerminal/archive/refs/heads/master.zip)) and copy the contents to your project's `Assets` folder. ## Improvements Over Baseline + - [Enhanced Auto-Complete + Hint system + styling](#hints) - Fixed Input handling bugs related to [WebGL](#web-gl) - Fully integrated with Unity's [new Input System](#new-input-system) @@ -74,6 +79,7 @@ Grab a copy of this repo (either `git clone` or [download a zip of the source](h - The concept of "FrontCommands" has been exterminated ## Code changes + - All code is formatted via [Csharpier](https://csharpier.com/) - All variables are now consistently named - Access modifiers have been explicitly applied to every field @@ -85,9 +91,11 @@ Grab a copy of this repo (either `git clone` or [download a zip of the source](h - 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 - A `command bar` for quick commands, instead of waiting for a terminal to fold into the screen @@ -116,7 +124,7 @@ Enter `help` in the console to view all available commands, use the up and down There are 2 options to register commands to be used in the Command Terminal. -### 1. Using the RegisterCommand attribute: +### 1. Using the RegisterCommand attribute The command method must be static (public or non-public). @@ -132,6 +140,7 @@ static void CommandAdd(CommandArg[] args) { 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`. @@ -140,7 +149,7 @@ In this case the command name (`add`) will be inferred from the method name, you [RegisterCommand(Name = "MyAdd", Help = "Adds 2 numbers", MinArgCount = 2, MaxArgCount = 2)] ``` -### 2. Manually adding Commands: +### 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. @@ -150,7 +159,8 @@ Terminal.Shell.AddCommand("add", CommandAdd, 2, 2, "Adds 2 numbers"); --- -# Custom Parsing +## 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). @@ -177,6 +187,7 @@ Assert.IsFalse(arg.TryGet(out int invalidInt)); // Failed to parse ``` `TryGet` supports the following types out of the box: + - string - char - bool @@ -197,6 +208,7 @@ Assert.IsFalse(arg.TryGet(out int invalidInt)); // Failed to parse - Enums **Unity Types**: + - Vector2 - Vector3 - Vector4 @@ -214,6 +226,7 @@ In addition to parsing values directly, `TryGet` will automatically attempt to m - "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)" @@ -225,6 +238,7 @@ For all built-in types, the parsers are guaranteed to work with the type's defau 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` has an overload that takes a nullable `CommandArgParser`, which is a function with the following definition: ```csharp @@ -277,11 +291,12 @@ or even int CommandArgParser.UnregisterAllParsers(); ``` -All of these unregistration functions will return you information on whether the unregistration functions were successful. +All of these unregistration functions will return you information on whether the unregistration functions were successful. Note: Built in parser functions cannot be unregistered. ### Object Parsers (IArgParser) + For stronger typing and lower allocations, you can also use object parsers that implement `IArgParser`. - Built-ins: All common numeric, date/time, IP, and Unity types ship with sealed parsers and public singletons, e.g. `IntArgParser.Instance`, `Vector3ArgParser.Instance`, `ColorArgParser.Instance`. @@ -326,6 +341,7 @@ public static void LoadFile(CommandArg a) Enum parsing uses a hot path with cached name and ordinal lookups (`EnumArgParser`) for fast, case-insensitive matching and integer ordinals. Editor convenience + - In the Unity Editor, parsers are auto-discovered on domain reload to ease development (`Editor/Parsers/ParserAutoDiscovery.cs`). This does not affect players. - Inspect what parsers are currently registered: @@ -335,6 +351,7 @@ foreach (var t in types) UnityEngine.Debug.Log($"Parser for type: {t}"); ``` Static members parsing + - Constant/Property name parsing (e.g., `MaxValue`, `IPAddress.Any`) is handled by dedicated static-member parsers and no longer lives inside `CommandArg`. ## Runtime Modes & Editor Toggle @@ -365,6 +382,7 @@ DxCommandTerminal exposes a runtime mode enum to control environment-specific be - `TerminalRuntimeConfig.TryAutoDiscoverParsers()` conditionally registers all IArgParser implementations based on mode + toggle. ## 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 @@ -377,22 +395,26 @@ By default, command parameter input is stripped of whitespace characters. This, **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 +## Hotkeys + All actions are now fully configurable by either explicit keybindings, for keyboard controls, or via [new Input System](#new-input-system) bindings. ![png](./Media/Hotkeys.png) Keyboard hotkey bindings are now intelligent as they can be about shift key interaction. There are three ways to create a `shift+`: + 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+`. `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+`. -# New Input System -DxCommandTerminal is now fully integrated with Unity's new Input System, if it is found in the project and enabled. +## 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. @@ -403,9 +425,11 @@ You can also use `PlayerInput` or similar to bind InputActions to all available - `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 @@ -417,14 +441,16 @@ When InputActions are not bound, there is an order of precedence for input check This order is irrelevant when using PlayerInput. -# Web GL +## Web GL + If you are relying on `RegisterCommandAttribute` to wire up your commands to the CommandShell instead of manually registering them, you will need to set `Managed Stripping Level` to `Low`, `Minimal`, or `None` under `Player > WebGL > Other Settings > Optimizations > Managed Stripping Level` in order for command registration to work. Settings of `Medium` or higher will break the reflection code that loads the commands, causing the terminal to forget about its capabilities. ![png](./Media/ManagedStrippingLevel.png) See [Unity docs on Managed Stripping Level](https://docs.unity3d.com/2022.3/Documentation/ScriptReference/ManagedStrippingLevel.html) for more details. -# Hints +## 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](./Media/AutoComplete.png) diff --git a/Runtime/Attributes/CommandCompleterAttribute.cs b/Runtime/Attributes/CommandCompleterAttribute.cs new file mode 100644 index 0000000..62eb0bf --- /dev/null +++ b/Runtime/Attributes/CommandCompleterAttribute.cs @@ -0,0 +1,34 @@ +namespace WallstopStudios.DxCommandTerminal.Attributes +{ + using System; + using Backend; + + /// + /// Attach to a command method to specify a dynamic argument completer. + /// Type must implement IArgumentCompleter and have either a public parameterless + /// constructor or a public static Instance property/field. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class CommandCompleterAttribute : Attribute + { + public Type CompleterType { get; } + + public CommandCompleterAttribute(Type completerType) + { + if (completerType == null) + { + throw new ArgumentNullException(nameof(completerType)); + } + + if (!typeof(IArgumentCompleter).IsAssignableFrom(completerType)) + { + throw new ArgumentException( + $"{completerType.FullName} must implement {nameof(IArgumentCompleter)}", + nameof(completerType) + ); + } + + CompleterType = completerType; + } + } +} diff --git a/Runtime/Attributes/CommandCompleterAttribute.cs.meta b/Runtime/Attributes/CommandCompleterAttribute.cs.meta new file mode 100644 index 0000000..91795e7 --- /dev/null +++ b/Runtime/Attributes/CommandCompleterAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b721ee9f6a3153547bce3b4af4cc2cf9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index 2ce92a0..4d44c79 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -78,8 +78,10 @@ public static void CommandListFonts(CommandArg[] args) Name = "set-theme", Help = "Sets the current Terminal UI theme", MinArgCount = 1, - MaxArgCount = 1 + MaxArgCount = 1, + Hint = "set-theme " )] + [CommandCompleter(typeof(Completers.ThemeArgumentCompleter))] public static void CommandSetTheme(CommandArg[] args) { TerminalUI terminal = TerminalUI.Instance; @@ -126,6 +128,51 @@ public static void CommandSetTheme(CommandArg[] args) Terminal.Log(TerminalLogType.Message, $"Theme '{theme}' set."); } + [RegisterCommand( + isDefault: true, + Name = "set-font", + Help = "Sets the current Terminal UI font", + MinArgCount = 1, + MaxArgCount = 1, + Hint = "set-font " + )] + [CommandCompleter(typeof(Completers.FontArgumentCompleter))] + public static void CommandSetFont(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + if (terminal._fontPack == null) + { + Terminal.Log(TerminalLogType.Warning, "No font pack found."); + return; + } + + string fontName = args[0].contents ?? string.Empty; + if (string.IsNullOrWhiteSpace(fontName)) + { + Terminal.Log(TerminalLogType.Warning, "Invalid font name."); + return; + } + + UnityEngine.Font font = terminal._fontPack._fonts.FirstOrDefault(f => + f != null && string.Equals(f.name, fontName, StringComparison.OrdinalIgnoreCase) + ); + + if (font == null) + { + Terminal.Log(TerminalLogType.Warning, $"Font '{fontName}' not found."); + return; + } + + terminal.SetFont(font); + Terminal.Log(TerminalLogType.Message, $"Font '{font.name}' set."); + } + [RegisterCommand( isDefault: true, Name = "get-theme", @@ -190,44 +237,44 @@ public static void CommandSetRandomTheme(CommandArg[] args) ); } - [RegisterCommand( - isDefault: true, - Name = "set-font", - Help = "Sets the current Terminal UI font", - MinArgCount = 1, - MaxArgCount = 1 - )] - public static void CommandSetFont(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - if (terminal._fontPack == null) - { - Terminal.Log(TerminalLogType.Warning, "No font pack found."); - return; - } - - string fontName = args[0].contents; - - int newFontIndex = terminal._fontPack._fonts.FindIndex(font => - string.Equals(font.name, fontName, StringComparison.OrdinalIgnoreCase) - ); - if (newFontIndex < 0) - { - Terminal.Log(TerminalLogType.Warning, $"Font '{fontName}' not found."); - return; - } - - Font font = terminal._fontPack._fonts[newFontIndex]; - - terminal.SetFont(font); - Terminal.Log(TerminalLogType.Message, $"Font '{font.name}' set."); - } + // [RegisterCommand( + // isDefault: true, + // Name = "set-font", + // Help = "Sets the current Terminal UI font", + // MinArgCount = 1, + // MaxArgCount = 1 + // )] + // public static void CommandSetFont(CommandArg[] args) + // { + // TerminalUI terminal = TerminalUI.Instance; + // if (terminal == null) + // { + // Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + // return; + // } + // + // if (terminal._fontPack == null) + // { + // Terminal.Log(TerminalLogType.Warning, "No font pack found."); + // return; + // } + // + // string fontName = args[0].contents; + // + // int newFontIndex = terminal._fontPack._fonts.FindIndex(font => + // string.Equals(font.name, fontName, StringComparison.OrdinalIgnoreCase) + // ); + // if (newFontIndex < 0) + // { + // Terminal.Log(TerminalLogType.Warning, $"Font '{fontName}' not found."); + // return; + // } + // + // Font font = terminal._fontPack._fonts[newFontIndex]; + // + // terminal.SetFont(font); + // Terminal.Log(TerminalLogType.Message, $"Font '{font.name}' set."); + // } [RegisterCommand( isDefault: true, diff --git a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs index f779a8d..d676f45 100644 --- a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs +++ b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs @@ -32,7 +32,141 @@ public string[] Complete(string text) public List Complete(string text, List buffer) { - WalkHistory(text, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + int caret = text?.Length ?? 0; + Complete(text, caret, buffer); + return buffer; + } + + public List Complete(string text, int caretIndex, List buffer) + { + string input = text ?? string.Empty; + buffer.Clear(); + _duplicateBuffer.Clear(); + + if (string.IsNullOrWhiteSpace(input)) + { + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + return buffer; + } + + int safeCaret = Math.Max(0, Math.Min(caretIndex, input.Length)); + string uptoCaret = + safeCaret <= 0 + ? string.Empty + : (safeCaret < input.Length ? input.Substring(0, safeCaret) : input); + + // Parse command + args up to caret + string working = uptoCaret; + if (!CommandShell.TryEatArgument(ref working, out CommandArg cmdArg)) + { + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + return buffer; + } + + string commandName = cmdArg.contents; + if (!_shell.Commands.TryGetValue(commandName, out CommandInfo cmdInfo)) + { + // Fall back to default behavior if not a known command yet + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + return buffer; + } + + // Collect args typed before cursor + List args = new(); + string lastToken = string.Empty; + bool trailingWhitespace = + uptoCaret.Length > 0 && char.IsWhiteSpace(uptoCaret[uptoCaret.Length - 1]); + while (CommandShell.TryEatArgument(ref working, out CommandArg arg)) + { + lastToken = arg.contents; + args.Add(arg); + } + + string partialArg = trailingWhitespace ? string.Empty : lastToken; + int argIndex = trailingWhitespace ? args.Count : (args.Count - 1); + if (!trailingWhitespace && 0 <= argIndex) + { + // Exclude the partial token from finalized args + args.RemoveAt(args.Count - 1); + } + + // Special case: caret is immediately after the command name with no space. + // Treat this as requesting suggestions for the first argument. + if (!trailingWhitespace && args.Count == 0) + { + partialArg = string.Empty; + argIndex = 0; + } + + // If the command provides a completer, ask it first + bool inArgContext = argIndex >= 0; + if (cmdInfo.completer != null) + { + CommandCompletionContext ctx = new( + input, + commandName, + args, + partialArg, + argIndex, + _shell + ); + + foreach ( + string suggestion in cmdInfo.completer.Complete(ctx) ?? Array.Empty() + ) + { + if (string.IsNullOrWhiteSpace(suggestion)) + { + continue; + } + + string prefix = commandName; + if (0 < args.Count) + { + prefix += " " + string.Join(" ", args.Select(a => a.contents)); + } + + if (argIndex >= 0) + { + prefix += " "; + } + + string insertion = suggestion; + bool needsQuoting = + !string.IsNullOrEmpty(insertion) && insertion.Any(char.IsWhiteSpace); + if (needsQuoting) + { + // Basic quoting to keep single argument with whitespace + // Escape embedded quotes minimally + insertion = "\"" + insertion.Replace("\"", "\\\"") + "\""; + } + + string full = prefix + insertion; + string key = full.NeedsLowerInvariantConversion() + ? full.ToLowerInvariant() + : full; + if (_duplicateBuffer.Add(key)) + { + buffer.Add(full); + } + } + + // If we got any results from the completer, return them. + if (0 < buffer.Count) + { + return buffer; + } + + // If we are in argument context for a command that supports completion, + // prefer context (even if empty) and do not fall back to history/known words. + if (inArgContext) + { + return buffer; + } + } + + // Fallback to built-in completion sources + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); return buffer; } diff --git a/Runtime/CommandTerminal/Backend/CommandInfo.cs b/Runtime/CommandTerminal/Backend/CommandInfo.cs index cde4b8f..d126e68 100644 --- a/Runtime/CommandTerminal/Backend/CommandInfo.cs +++ b/Runtime/CommandTerminal/Backend/CommandInfo.cs @@ -9,13 +9,15 @@ public readonly struct CommandInfo public readonly int maxArgCount; public readonly string help; public readonly string hint; + public readonly IArgumentCompleter completer; public CommandInfo( Action proc, int minArgCount, int maxArgCount, string help, - string hint + string hint, + IArgumentCompleter completer = null ) { this.proc = proc; @@ -23,6 +25,7 @@ string hint this.minArgCount = minArgCount; this.help = help; this.hint = hint; + this.completer = completer; } } } diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index 5a00a0a..ca048b5 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -228,13 +228,17 @@ public void InitializeAutoRegisteredCommands( Action proc = (Action) Delegate.CreateDelegate(typeof(Action), method); + // Try resolve optional completer via CommandCompleterAttribute + IArgumentCompleter completer = ResolveCompleter(method); + bool success = AddCommand( commandName, proc, attribute.MinArgCount, attribute.MaxArgCount, attribute.Help, - attribute.Hint + attribute.Hint, + completer ); if (success) { @@ -432,10 +436,11 @@ public bool AddCommand( int minArgs = 0, int maxArgs = -1, string help = "", - string hint = null + string hint = null, + IArgumentCompleter completer = null ) { - CommandInfo info = new(proc, minArgs, maxArgs, help, hint); + CommandInfo info = new(proc, minArgs, maxArgs, help, hint, completer); return AddCommand(name, info); } @@ -593,5 +598,60 @@ public static bool TryEatArgument(ref string stringValue, out CommandArg arg) return true; } + + private static IArgumentCompleter ResolveCompleter(MethodInfo method) + { + try + { + object attr = Attribute.GetCustomAttribute( + method, + typeof(Attributes.CommandCompleterAttribute) + ); + if (attr is not Attributes.CommandCompleterAttribute cca) + { + return null; + } + + Type t = cca.CompleterType; + // Prefer a public static Instance property + PropertyInfo instProp = t.GetProperty( + "Instance", + BindingFlags.Public | BindingFlags.Static + ); + if ( + instProp != null + && typeof(IArgumentCompleter).IsAssignableFrom(instProp.PropertyType) + ) + { + return (IArgumentCompleter)instProp.GetValue(null); + } + + // Or a public static Instance field + FieldInfo instField = t.GetField( + "Instance", + BindingFlags.Public | BindingFlags.Static + ); + if ( + instField != null + && typeof(IArgumentCompleter).IsAssignableFrom(instField.FieldType) + ) + { + return (IArgumentCompleter)instField.GetValue(null); + } + + // Else use parameterless constructor + ConstructorInfo ctor = t.GetConstructor(Type.EmptyTypes); + if (ctor != null) + { + return (IArgumentCompleter)Activator.CreateInstance(t); + } + } + catch (Exception) + { + // Swallow and treat as no-completer; errors surface in logs elsewhere + } + + return null; + } } } diff --git a/Runtime/CommandTerminal/Backend/Completers.meta b/Runtime/CommandTerminal/Backend/Completers.meta new file mode 100644 index 0000000..c8c73a9 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Completers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 631b78b1cb8ca704db3d15d7457345ac +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs b/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs new file mode 100644 index 0000000..53ad17b --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs @@ -0,0 +1,39 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Completers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using UI; + + public sealed class FontArgumentCompleter : IArgumentCompleter + { + public IEnumerable Complete(CommandCompletionContext context) + { + // Only complete the first argument (font name) + if (context.ArgIndex != 0) + { + return Array.Empty(); + } + + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null || terminal._fontPack == null) + { + return Array.Empty(); + } + + IEnumerable names = terminal + ._fontPack._fonts.Where(f => f != null && !string.IsNullOrWhiteSpace(f.name)) + .Select(f => f.name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(n => n, StringComparer.OrdinalIgnoreCase); + + string partial = context.PartialArg ?? string.Empty; + if (string.IsNullOrWhiteSpace(partial)) + { + return names; + } + + return names.Where(n => n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs.meta b/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs.meta new file mode 100644 index 0000000..0b1f466 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 15e03dc2d9132e64b94797812b695fec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs b/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs new file mode 100644 index 0000000..74bf033 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs @@ -0,0 +1,41 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Completers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Themes; + using UI; + + public sealed class ThemeArgumentCompleter : IArgumentCompleter + { + public IEnumerable Complete(CommandCompletionContext context) + { + // Only complete the first argument (theme) + if (context.ArgIndex != 0) + { + return Array.Empty(); + } + + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null || terminal._themePack == null) + { + return Array.Empty(); + } + + IEnumerable friendly = terminal + ._themePack._themeNames.Where(n => !string.IsNullOrWhiteSpace(n)) + .Select(ThemeNameHelper.GetFriendlyThemeName) + .Where(n => !string.IsNullOrWhiteSpace(n)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(n => n, StringComparer.OrdinalIgnoreCase); + + string partial = context.PartialArg ?? string.Empty; + if (string.IsNullOrWhiteSpace(partial)) + { + return friendly; + } + + return friendly.Where(n => n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs.meta b/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs.meta new file mode 100644 index 0000000..a92ea19 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 68121dfae15d1b541bc911995a429ef5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/IArgumentCompleter.cs b/Runtime/CommandTerminal/Backend/IArgumentCompleter.cs new file mode 100644 index 0000000..231325c --- /dev/null +++ b/Runtime/CommandTerminal/Backend/IArgumentCompleter.cs @@ -0,0 +1,42 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System.Collections.Generic; + + /// + /// Context for argument completion queries. + /// + public readonly struct CommandCompletionContext + { + public readonly string FullText; + public readonly string CommandName; + public readonly IReadOnlyList ArgsBeforeCursor; + public readonly string PartialArg; + public readonly int ArgIndex; + public readonly CommandShell Shell; + + public CommandCompletionContext( + string fullText, + string commandName, + IReadOnlyList argsBeforeCursor, + string partialArg, + int argIndex, + CommandShell shell + ) + { + FullText = fullText; + CommandName = commandName; + ArgsBeforeCursor = argsBeforeCursor; + PartialArg = partialArg; + ArgIndex = argIndex; + Shell = shell; + } + } + + /// + /// Implement to provide dynamic, argument-aware completions for a command. + /// + public interface IArgumentCompleter + { + IEnumerable Complete(CommandCompletionContext context); + } +} diff --git a/Runtime/CommandTerminal/Backend/IArgumentCompleter.cs.meta b/Runtime/CommandTerminal/Backend/IArgumentCompleter.cs.meta new file mode 100644 index 0000000..cde02bd --- /dev/null +++ b/Runtime/CommandTerminal/Backend/IArgumentCompleter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ba72911cf45887b4181ca32cded0ddd6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs index 74fbf75..a57dce2 100644 --- a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs @@ -2,30 +2,88 @@ namespace WallstopStudios.DxCommandTerminal.Backend { using System; using UnityEngine; + using WallstopStudios.DxCommandTerminal.Internal; [Flags] - public enum TerminalRuntimeModeFlags : int + public enum TerminalRuntimeModeFlags { [Obsolete("None disables all runtime features. Choose explicit modes.")] None = 0, - Editor = 1, - Development = 2, - Production = 4, + Editor = 1 << 0, + Development = 1 << 1, + Production = 1 << 2, All = Editor | Development | Production, } - public static class TerminalRuntimeConfig + // Auto-create under Assets/Resources/Wallstop Studios/DxCommandTerminal/TerminalRuntimeConfig.asset + [ScriptableSingletonPath("Wallstop Studios/DxCommandTerminal")] + public sealed class TerminalRuntimeConfig : ScriptableObjectSingleton { + // Fallbacks ensure static API remains usable even before an asset exists #pragma warning disable CS0618 // Type or member is obsolete - public static TerminalRuntimeModeFlags Mode { get; private set; } = - TerminalRuntimeModeFlags.None; + private static TerminalRuntimeModeFlags _fallbackMode = TerminalRuntimeModeFlags.None; #pragma warning restore CS0618 // Type or member is obsolete - public static bool EditorAutoDiscover { get; set; } + private static bool _fallbackEditorAutoDiscover; +#pragma warning disable CS0618 // Type or member is obsolete + [SerializeField] + private TerminalRuntimeModeFlags _mode = TerminalRuntimeModeFlags.None; +#pragma warning restore CS0618 // Type or member is obsolete + + [SerializeField] + private bool _editorAutoDiscover; + + // Instance-level accessors (for inspector/serialization) +#pragma warning disable CS0618 // Type or member is obsolete + public TerminalRuntimeModeFlags Mode + { + get => _mode; + set => _mode = value; + } +#pragma warning restore CS0618 // Type or member is obsolete + + public bool EditorAutoDiscoverInstance + { + get => _editorAutoDiscover; + set => _editorAutoDiscover = value; + } + + // Backwards-compatible static API +#pragma warning disable CS0618 // Type or member is obsolete public static void SetMode(TerminalRuntimeModeFlags mode) { - Mode = mode; + if (Instance != null) + { + Instance._mode = mode; + } + _fallbackMode = mode; } +#pragma warning restore CS0618 // Type or member is obsolete + + public static bool EditorAutoDiscover + { + get + { + if (Instance != null) + { + return Instance._editorAutoDiscover; + } + return _fallbackEditorAutoDiscover; + } + set + { + if (Instance != null) + { + Instance._editorAutoDiscover = value; + } + _fallbackEditorAutoDiscover = value; + } + } + +#pragma warning disable CS0618 // Type or member is obsolete + private static TerminalRuntimeModeFlags CurrentMode => + Instance != null ? Instance._mode : _fallbackMode; +#pragma warning restore CS0618 // Type or member is obsolete public static bool HasFlagNoAlloc( TerminalRuntimeModeFlags value, @@ -38,7 +96,7 @@ TerminalRuntimeModeFlags flag public static bool ShouldEnableEditorFeatures() { #if UNITY_EDITOR - return HasFlagNoAlloc(Mode, TerminalRuntimeModeFlags.Editor); + return HasFlagNoAlloc(CurrentMode, TerminalRuntimeModeFlags.Editor); #else return false; #endif @@ -46,12 +104,14 @@ public static bool ShouldEnableEditorFeatures() public static bool ShouldEnableDevelopmentFeatures() { - return HasFlagNoAlloc(Mode, TerminalRuntimeModeFlags.Development) && Debug.isDebugBuild; + return HasFlagNoAlloc(CurrentMode, TerminalRuntimeModeFlags.Development) + && Debug.isDebugBuild; } public static bool ShouldEnableProductionFeatures() { - return HasFlagNoAlloc(Mode, TerminalRuntimeModeFlags.Production) && !Debug.isDebugBuild; + return HasFlagNoAlloc(CurrentMode, TerminalRuntimeModeFlags.Production) + && !Debug.isDebugBuild; } public static int TryAutoDiscoverParsers() diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 36457a8..ad6f09e 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -668,8 +668,13 @@ private void ResetAutoComplete() if (hintDisplayMode == HintDisplayMode.Always) { _lastCompletionBufferTempCache.Clear(); + int caret = + _commandInput != null + ? _commandInput.cursorIndex + : (_lastKnownCommandText?.Length ?? 0); Terminal.AutoComplete?.Complete( _lastKnownCommandText, + caret, _lastCompletionBufferTempCache ); bool equivalent = @@ -857,12 +862,55 @@ private void SetupUI() { context._commandInput.value = context._input.CommandText; } + // Ensure subsequent user keystrokes (e.g., space) trigger recompute + // even if this event was caused by programmatic text changes (Tab, etc.). + context._isCommandFromCode = false; evt.StopPropagation(); return; } + // Assign input text context._input.CommandText = evt.newValue; + // If the user just typed a space right after a recognized command name, + // proactively clear the hint bar so stale command-name suggestions disappear + // before argument-context suggestions are computed/shown. + try + { + string prev = evt.previousValue ?? string.Empty; + string curr = evt.newValue ?? string.Empty; + bool justTypedSpace = curr.EndsWith(" ") && curr.Length == prev.Length + 1; + if (justTypedSpace && Backend.Terminal.Shell != null) + { + string check = curr; + // Remove trailing space(s) to isolate the command token + if (check.NeedsTrim()) + { + check = check.TrimEnd(); + } + + if ( + Backend.CommandShell.TryEatArgument( + ref check, + out Backend.CommandArg cmd + ) + ) + { + if (Backend.Terminal.Shell.Commands.ContainsKey(cmd.contents)) + { + // Clear existing suggestions immediately + context._lastCompletionIndex = null; + context._previousLastCompletionIndex = null; + context._lastCompletionBuffer.Clear(); + context._autoCompleteContainer?.Clear(); + } + } + } + } + catch + { /* non-fatal UI hint clearing */ + } + context._runButton.style.display = context.showGUIButtons && !string.IsNullOrWhiteSpace(context._input.CommandText) @@ -2016,10 +2064,15 @@ public void CompleteCommand(bool searchForward = true) try { - _lastKnownCommandText ??= _input.CommandText ?? string.Empty; + _lastKnownCommandText = _input.CommandText ?? string.Empty; _lastCompletionBufferTempCache.Clear(); + int caret = + _commandInput != null + ? _commandInput.cursorIndex + : (_lastKnownCommandText?.Length ?? 0); Terminal.AutoComplete?.Complete( _lastKnownCommandText, + caret, _lastCompletionBufferTempCache ); bool equivalentBuffers = true; diff --git a/Runtime/DataStructures/CyclicBuffer.cs b/Runtime/DataStructures/CyclicBuffer.cs index 1bed6ae..14ccc25 100644 --- a/Runtime/DataStructures/CyclicBuffer.cs +++ b/Runtime/DataStructures/CyclicBuffer.cs @@ -77,7 +77,7 @@ public CyclicBuffer(int capacity, IEnumerable initialContents = null) Capacity = capacity; _position = 0; Count = 0; - _buffer = new List(capacity); + _buffer = new List(); if (initialContents != null) { foreach (T item in initialContents) diff --git a/Runtime/Internal.meta b/Runtime/Internal.meta new file mode 100644 index 0000000..44bde1b --- /dev/null +++ b/Runtime/Internal.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8ed445f6e9cca3444bc567691804ef3c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/ScriptableObjectSingleton.cs b/Runtime/Internal/ScriptableObjectSingleton.cs new file mode 100644 index 0000000..3fc19a5 --- /dev/null +++ b/Runtime/Internal/ScriptableObjectSingleton.cs @@ -0,0 +1,135 @@ +namespace WallstopStudios.DxCommandTerminal.Internal +{ + using System; + using UnityEngine; +#if UNITY_EDITOR + using UnityEditor; +#endif + + /// + /// Lightweight ScriptableObject-based singleton loader that searches Resources + /// using an optional [ScriptableSingletonPath("...")] resources subfolder. + /// Includes editor fallbacks and keeps null-safe lazy initialization. + /// + /// Concrete ScriptableObject singleton type. + public abstract class ScriptableObjectSingleton : ScriptableObject + where T : ScriptableObjectSingleton + { + private static Lazy _lazy = CreateLazy(); + + public static T Instance => _lazy.Value; + + public static bool HasInstance => _lazy.IsValueCreated && _lazy.Value != null; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + internal static void ClearInstance() + { + if (!_lazy.IsValueCreated) + { + return; + } + + if (_lazy.Value != null) + { + Destroy(_lazy.Value); + } + _lazy = CreateLazy(); + } + + private static string GetResourcesPath() + { + Type type = typeof(T); + ScriptableSingletonPathAttribute attr = + Attribute.GetCustomAttribute(type, typeof(ScriptableSingletonPathAttribute)) + as ScriptableSingletonPathAttribute; + if (attr != null && !string.IsNullOrWhiteSpace(attr.resourcesPath)) + { + return attr.resourcesPath; + } + return string.Empty; + } + + private static Lazy CreateLazy() + { + return new Lazy(() => + { + Type type = typeof(T); + string path = GetResourcesPath(); + + // Primary: search in the specified subfolder (or root if empty) + T[] instances = Resources.LoadAll(path); + + // Fallback: try an exact-name load from root + if (instances == null || instances.Length == 0) + { + T named = Resources.Load(type.Name); + if (named != null) + { + instances = new[] { named }; + } + } + + // Fallback: search entire Resources if a subfolder was specified but nothing found + if ( + (instances == null || instances.Length == 0) + && !string.Equals(path, string.Empty, StringComparison.OrdinalIgnoreCase) + ) + { + instances = Resources.LoadAll(string.Empty); + } + + // Editor fallback: pick any already loaded instances + if (instances == null || instances.Length == 0) + { + T[] found = Resources.FindObjectsOfTypeAll(); + if (found is { Length: > 0 }) + { + instances = found; + } + } + +#if UNITY_EDITOR + // Editor-only direct path attempts under Assets/Resources + if (instances == null || instances.Length == 0) + { + string typeName = type.Name; + if (!string.IsNullOrWhiteSpace(path)) + { + string candidate = $"Assets/Resources/{path}/{typeName}.asset"; + T atPath = AssetDatabase.LoadAssetAtPath(candidate); + if (atPath != null) + { + instances = new[] { atPath }; + } + } + if (instances == null || instances.Length == 0) + { + string candidate = $"Assets/Resources/{typeName}.asset"; + T atPath = AssetDatabase.LoadAssetAtPath(candidate); + if (atPath != null) + { + instances = new[] { atPath }; + } + } + } +#endif + + if (instances == null || instances.Length == 0) + { + return null; + } + + if (instances.Length == 1) + { + return instances[0]; + } + + Array.Sort(instances, (a, b) => string.CompareOrdinal(a?.name, b?.name)); + Debug.LogWarning( + $"Found multiple ScriptableObjectSingletons of type {type.Name}, defaulting to first by name." + ); + return instances[0]; + }); + } + } +} diff --git a/Runtime/Internal/ScriptableObjectSingleton.cs.meta b/Runtime/Internal/ScriptableObjectSingleton.cs.meta new file mode 100644 index 0000000..844c086 --- /dev/null +++ b/Runtime/Internal/ScriptableObjectSingleton.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1996fcb971cb20541b035df6a2b45114 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/ScriptableSingletonPathAttribute.cs b/Runtime/Internal/ScriptableSingletonPathAttribute.cs new file mode 100644 index 0000000..e325493 --- /dev/null +++ b/Runtime/Internal/ScriptableSingletonPathAttribute.cs @@ -0,0 +1,15 @@ +namespace WallstopStudios.DxCommandTerminal.Internal +{ + using System; + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class ScriptableSingletonPathAttribute : Attribute + { + public readonly string resourcesPath; + + public ScriptableSingletonPathAttribute(string resourcesPath) + { + this.resourcesPath = resourcesPath ?? string.Empty; + } + } +} diff --git a/Runtime/Internal/ScriptableSingletonPathAttribute.cs.meta b/Runtime/Internal/ScriptableSingletonPathAttribute.cs.meta new file mode 100644 index 0000000..c913f96 --- /dev/null +++ b/Runtime/Internal/ScriptableSingletonPathAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6900e8fc68ef03f41b3d39e7152c40d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/WallstopStudios.DxCommandTerminal.asmdef b/Runtime/WallstopStudios.DxCommandTerminal.asmdef index 450f54c..af32757 100644 --- a/Runtime/WallstopStudios.DxCommandTerminal.asmdef +++ b/Runtime/WallstopStudios.DxCommandTerminal.asmdef @@ -1,16 +1,14 @@ { - "name": "WallstopStudios.DxCommandTerminal", - "rootNamespace": "WallstopStudios.DxCommandTerminal", - "references": [ - "Unity.InputSystem" - ], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false -} \ No newline at end of file + "name": "WallstopStudios.DxCommandTerminal", + "rootNamespace": "WallstopStudios.DxCommandTerminal", + "references": ["Unity.InputSystem"], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/doc.md b/doc.md index 239dc0b..9b83cb5 100644 --- a/doc.md +++ b/doc.md @@ -1,15 +1,15 @@ -*Documentation for internal features of the Terminal. For documentation on how to register commands, please refer to the [README](./README.md).* +_Documentation for internal features of the Terminal. For documentation on how to register commands, please refer to the [README](./README.md)._ -### Terminal structure: +### Terminal structure | | Description | -|:-------------|:-------------------------------------------------------------------| +| :----------- | :----------------------------------------------------------------- | | Buffer | Handles incoming logs | | Autocomplete | Keeps a list of known words and uses it to autocomplete text | | Shell | Responsible for parsing and executing commands | | History | Keeps a list of issued commands and can traverse through that list | -### Variables: +### Variables ```csharp Terminal.Shell.SetVariable("level", SceneManager.GetActiveScene().name); @@ -17,7 +17,7 @@ Terminal.Shell.SetVariable("level", SceneManager.GetActiveScene().name); In the console: -``` +```text > print $level Main @@ -30,31 +30,31 @@ LEVEL : Main GREET : Hello World! ``` -### Add words to autocomplete: +### Add words to autocomplete ```csharp Terminal.Autocomplete.Register("foo"); ``` -### Run a command: +### Run a command ```csharp Terminal.Shell.RunCommand("print Hello World!")); ``` -### Log without adding to Unity debug logs: +### Log without adding to Unity debug logs ```csharp Terminal.Log("Value of foo: {0}", foo); ``` -### Clear logs: +### Clear logs ```csharp Terminal.Buffer.Clear(); ``` -### Modify the command history: +### Modify the command history ```csharp Terminal.History.Clear(); // Clear history @@ -63,3 +63,46 @@ Terminal.History.Push("foo"); // Add item to history string a = Terminal.History.Next(); // Get next item string b = Terminal.History.Previous(); // Get previous item ``` + +### Argument-aware completion (new) + +Commands can provide dynamic argument suggestions that the UI uses for tab completion and hints. + +- Implement `Backend.IArgumentCompleter` and return context-aware suggestions. +- Attach with `[CommandCompleter(typeof(YourCompleter))]` on the same method that has `[RegisterCommand]`. +- Existing commands without completers keep working; history/known words remain as fallback. +- Argument suggestions are only triggered after the command name is complete and followed by a space, or when editing a specific argument token. If you press Tab while the caret is still inside the command name (no trailing space), the system completes command names as before. + +Example: + +```csharp +using WallstopStudios.DxCommandTerminal.Attributes; +using WallstopStudios.DxCommandTerminal.Backend; + +public static class MyCommands +{ + [RegisterCommand("warp", MinArgCount = 1, MaxArgCount = 1, Help = "Warp to a target", Hint = "warp ")] + [CommandCompleter(typeof(WarpCompleter))] + private static void Warp(CommandArg[] args) + { + // Execute warp to args[0]... + } +} + +public sealed class WarpCompleter : IArgumentCompleter +{ + public IEnumerable Complete(CommandCompletionContext ctx) + { + // Complete only first argument + if (ctx.ArgIndex != 0) + { + return Array.Empty(); + } + + // Domain-specific query. Filter by ctx.PartialArg + var targets = new[] { "alpha", "beta-station", "gamma" }; + return targets.Where(t => string.IsNullOrEmpty(ctx.PartialArg) + || t.StartsWith(ctx.PartialArg, StringComparison.OrdinalIgnoreCase)); + } +} +``` From cd9c824885876e776a7afce302e37c8826284e7a Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 10:16:01 -0700 Subject: [PATCH 05/69] Remove doc.md --- doc.md | 108 ---------------------------------------------------- doc.md.meta | 7 ---- 2 files changed, 115 deletions(-) delete mode 100644 doc.md delete mode 100644 doc.md.meta diff --git a/doc.md b/doc.md deleted file mode 100644 index 9b83cb5..0000000 --- a/doc.md +++ /dev/null @@ -1,108 +0,0 @@ -_Documentation for internal features of the Terminal. For documentation on how to register commands, please refer to the [README](./README.md)._ - -### Terminal structure - -| | Description | -| :----------- | :----------------------------------------------------------------- | -| Buffer | Handles incoming logs | -| Autocomplete | Keeps a list of known words and uses it to autocomplete text | -| Shell | Responsible for parsing and executing commands | -| History | Keeps a list of issued commands and can traverse through that list | - -### Variables - -```csharp -Terminal.Shell.SetVariable("level", SceneManager.GetActiveScene().name); -``` - -In the console: - -```text -> print $level -Main - -> set greet Hello World! -> print $greet -Hello World! - -> set -LEVEL : Main -GREET : Hello World! -``` - -### Add words to autocomplete - -```csharp -Terminal.Autocomplete.Register("foo"); -``` - -### Run a command - -```csharp -Terminal.Shell.RunCommand("print Hello World!")); -``` - -### Log without adding to Unity debug logs - -```csharp -Terminal.Log("Value of foo: {0}", foo); -``` - -### Clear logs - -```csharp -Terminal.Buffer.Clear(); -``` - -### Modify the command history - -```csharp -Terminal.History.Clear(); // Clear history -Terminal.History.Push("foo"); // Add item to history - -string a = Terminal.History.Next(); // Get next item -string b = Terminal.History.Previous(); // Get previous item -``` - -### Argument-aware completion (new) - -Commands can provide dynamic argument suggestions that the UI uses for tab completion and hints. - -- Implement `Backend.IArgumentCompleter` and return context-aware suggestions. -- Attach with `[CommandCompleter(typeof(YourCompleter))]` on the same method that has `[RegisterCommand]`. -- Existing commands without completers keep working; history/known words remain as fallback. -- Argument suggestions are only triggered after the command name is complete and followed by a space, or when editing a specific argument token. If you press Tab while the caret is still inside the command name (no trailing space), the system completes command names as before. - -Example: - -```csharp -using WallstopStudios.DxCommandTerminal.Attributes; -using WallstopStudios.DxCommandTerminal.Backend; - -public static class MyCommands -{ - [RegisterCommand("warp", MinArgCount = 1, MaxArgCount = 1, Help = "Warp to a target", Hint = "warp ")] - [CommandCompleter(typeof(WarpCompleter))] - private static void Warp(CommandArg[] args) - { - // Execute warp to args[0]... - } -} - -public sealed class WarpCompleter : IArgumentCompleter -{ - public IEnumerable Complete(CommandCompletionContext ctx) - { - // Complete only first argument - if (ctx.ArgIndex != 0) - { - return Array.Empty(); - } - - // Domain-specific query. Filter by ctx.PartialArg - var targets = new[] { "alpha", "beta-station", "gamma" }; - return targets.Where(t => string.IsNullOrEmpty(ctx.PartialArg) - || t.StartsWith(ctx.PartialArg, StringComparison.OrdinalIgnoreCase)); - } -} -``` diff --git a/doc.md.meta b/doc.md.meta deleted file mode 100644 index 3b792c1..0000000 --- a/doc.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 97dd482203a34cd43afbdc5d9f452813 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: From 18836b6f1115ed379c2532a78dfb7d1adee2db8b Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 10:25:56 -0700 Subject: [PATCH 06/69] Add check-eol --- package-lock.json | 798 +++++++++++++++++++++++++++++++++++++ package-lock.json.meta | 7 + package.json | 82 ++-- scripts.meta | 8 + scripts/check-eol.ps1 | 70 ++++ scripts/check-eol.ps1.meta | 7 + 6 files changed, 933 insertions(+), 39 deletions(-) create mode 100644 package-lock.json create mode 100644 package-lock.json.meta create mode 100644 scripts.meta create mode 100644 scripts/check-eol.ps1 create mode 100644 scripts/check-eol.ps1.meta diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a5e5f72 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,798 @@ +{ + "name": "com.wallstop-studios.dxcommandterminal", + "version": "1.0.0-rc24.7", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "com.wallstop-studios.dxcommandterminal", + "version": "1.0.0-rc24.7", + "license": "MIT", + "devDependencies": { + "markdownlint-cli": "0.40.0", + "prettier": "3.3.3" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.3.16", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", + "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdownlint": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.34.0.tgz", + "integrity": "sha512-qwGyuyKwjkEMOJ10XN6OTKNOVYvOIi35RNvDLNxTof5s8UmyGHlCdpngRHoRGNvQVGuxO3BJ7uNSgdeX166WXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "markdown-it": "14.1.0", + "markdownlint-micromark": "0.1.9" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.40.0.tgz", + "integrity": "sha512-JXhI3dRQcaqwiFYpPz6VJ7aKYheD53GmTz9y4D/d0F1MbZDGOp9pqKlbOfUX/pHP/iAoeiE4wYRmk8/kjLakxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "~12.0.0", + "get-stdin": "~9.0.0", + "glob": "~10.3.12", + "ignore": "~5.3.1", + "js-yaml": "^4.1.0", + "jsonc-parser": "~3.2.1", + "jsonpointer": "5.0.1", + "markdownlint": "~0.34.0", + "minimatch": "~9.0.4", + "run-con": "~1.3.2", + "toml": "~3.0.0" + }, + "bin": { + "markdownlint": "markdownlint.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/markdownlint-micromark": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.9.tgz", + "integrity": "sha512-5hVs/DzAFa8XqYosbEAEg6ok6MF2smDj89ztn9pKkCtdKHVdPQuGMH7frFfYL9mLkvfFe4pTyAMffLbjf3/EyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/run-con": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz", + "integrity": "sha512-CcfE+mYiTcKEzg0IqS08+efdnH0oJ3zV0wSUFBNrMHMuxCtXvBCLzCJHatwuXDcu/RlhjTziTo/a1ruQik6/Yg==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~4.1.0", + "minimist": "^1.2.8", + "strip-json-comments": "~3.1.1" + }, + "bin": { + "run-con": "cli.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package-lock.json.meta b/package-lock.json.meta new file mode 100644 index 0000000..66f990e --- /dev/null +++ b/package-lock.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e6408683a0d58654394486d75498b70c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 5d766cf..2d28009 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,43 @@ -{ - "name": "com.wallstop-studios.dxcommandterminal", - "version": "1.0.0-rc24.7", - "displayName": "DxCommandTerminal", - "description": "Wallstop Studios fork of Command Terminal for Unity", - "dependencies": {}, - "unity": "2021.3", - "keywords": [ - "console", - "command", - "commands", - "terminal", - "command terminal", - "library", - "utility", - "cheats" - ], - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/wallstop/DxCommandTerminal.git" - }, - "bugs": { - "url": "https://github.com/wallstop/DxCommandTerminal/issues" - }, - "author": "wallstop studios (https://wallstopstudios.com)", - "homepage": "https://github.com/wallstop/DxCommandTerminal/blob/master/README.md", - "main": "README.md", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - } -} - - - - - - - +{ + "name": "com.wallstop-studios.dxcommandterminal", + "version": "1.0.0-rc24.7", + "displayName": "DxCommandTerminal", + "description": "Wallstop Studios fork of Command Terminal for Unity", + "dependencies": {}, + "unity": "2021.3", + "keywords": [ + "console", + "command", + "commands", + "terminal", + "command terminal", + "library", + "utility", + "cheats" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/wallstop/DxCommandTerminal.git" + }, + "bugs": { + "url": "https://github.com/wallstop/DxCommandTerminal/issues" + }, + "author": "wallstop studios (https://wallstopstudios.com)", + "homepage": "https://github.com/wallstop/DxCommandTerminal/blob/master/README.md", + "main": "README.md", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "format:md": "prettier --write \"**/*.{md,markdown}\"", + "format:md:check": "prettier --check \"**/*.{md,markdown}\"", + "format:json": "prettier --write \"**/*.{json,asmdef,asmref}\"", + "format:json:check": "prettier --check \"**/*.{json,asmdef,asmref}\"", + "format:yaml": "prettier --write \"**/*.{yml,yaml}\"", + "format:yaml:check": "prettier --check \"**/*.{yml,yaml}\"", + "lint:markdown": "markdownlint \"**/*.md\" \"**/*.markdown\" --config .markdownlint.json --ignore-path .markdownlintignore" + }, + "devDependencies": { + "markdownlint-cli": "0.40.0", + "prettier": "3.3.3" + } +} diff --git a/scripts.meta b/scripts.meta new file mode 100644 index 0000000..d2b7172 --- /dev/null +++ b/scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 57828e3f0e6be024fa677ebf2835cc71 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/scripts/check-eol.ps1 b/scripts/check-eol.ps1 new file mode 100644 index 0000000..37eb17d --- /dev/null +++ b/scripts/check-eol.ps1 @@ -0,0 +1,70 @@ +$ErrorActionPreference = 'Stop' + +param( + [string] $Root = '.', + [switch] $VerboseOutput +) + +function Write-VerboseLine($msg) { + if ($VerboseOutput) { Write-Host $msg } +} + +# File extensions to check for EOL (CRLF) and no BOM +$extensions = @('md','markdown','json','asmdef','asmref','yml','yaml') + +$badBom = New-Object System.Collections.Generic.List[string] +$badEol = New-Object System.Collections.Generic.List[string] + +$files = Get-ChildItem -Path $Root -Recurse -File | + Where-Object { + $ext = $_.Extension.TrimStart('.') + $extensions -contains $ext + } | + Where-Object { + # Skip typical vendor/build dirs + $_.FullName -notmatch "(?:\\|/)(node_modules|.git)(?:\\|/)" + } + +foreach ($f in $files) { + Write-VerboseLine "Checking: $($f.FullName)" + $bytes = [System.IO.File]::ReadAllBytes($f.FullName) + + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + $badBom.Add($f.FullName) + continue + } + + # Verify all LF (0x0A) are preceded by CR (0x0D) + $hasLfWithoutCr = $false + for ($i = 0; $i -lt $bytes.Length; $i++) { + if ($bytes[$i] -eq 0x0A) { + if ($i -eq 0 -or $bytes[$i - 1] -ne 0x0D) { + $hasLfWithoutCr = $true + break + } + } + } + + if ($hasLfWithoutCr) { + $badEol.Add($f.FullName) + } +} + +if ($badBom.Count -eq 0 -and $badEol.Count -eq 0) { + Write-Host "EOL/BOM check passed: All checked files use CRLF and no BOM." + exit 0 +} + +if ($badBom.Count -gt 0) { + Write-Host "Files with UTF-8 BOM (should be without BOM):" + $badBom | ForEach-Object { Write-Host " - $_" } +} + +if ($badEol.Count -gt 0) { + Write-Host "Files with LF-only line endings (should be CRLF):" + $badEol | ForEach-Object { Write-Host " - $_" } +} + +Write-Error "EOL/BOM validation failed. See lists above." +exit 1 + diff --git a/scripts/check-eol.ps1.meta b/scripts/check-eol.ps1.meta new file mode 100644 index 0000000..3ba828f --- /dev/null +++ b/scripts/check-eol.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0fb8e691c0c0706478455b7b547e5ff7 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 97046528d1e45a20445cd343506e9bbd2b23a4df Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 10:37:38 -0700 Subject: [PATCH 07/69] Linter updates --- .config/dotnet-tools.json | 20 +- .editorconfig | 368 +- .gitattributes | 42 + .github/workflows/npm-publish.yml | 150 +- .gitignore | 6 +- .pre-commit-config.yaml | 98 +- AGENTS.md | 2 +- .../CustomEditors/TerminalFontPackEditor.cs | 564 +- .../CustomEditors/TerminalThemePackEditor.cs | 418 +- Editor/CustomEditors/TerminalUIEditor.cs | 2718 ++++----- Editor/DxShowIfPropertyDrawer.cs | 124 +- .../Helper/TerminalThemeStyleSheetHelper.cs | 312 +- Editor/TerminalAssetPackPostProcessor.cs | 90 +- ...topStudios.DxCommandTerminal.Editor.asmdef | 31 +- Fonts/Anonymous_Pro/OFL.txt | 188 +- Fonts/Atkinson_Hyperlegible_Mono/OFL.txt | 186 +- Fonts/Azeret_Mono/OFL.txt | 186 +- Fonts/B612_Mono/OFL.txt | 186 +- Fonts/Barlow_Condensed/OFL.txt | 186 +- Fonts/Courier_Prime/OFL.txt | 186 +- Fonts/Cousine/LICENSE.txt | 404 +- Fonts/Cutive_Mono/OFL.txt | 186 +- Fonts/DM_Mono/OFL.txt | 186 +- Fonts/Fira_Code/OFL.txt | 186 +- Fonts/Fira_Mono/OFL.txt | 186 +- Fonts/Fragment_Mono/OFL.txt | 186 +- Fonts/Geist_Mono/OFL.txt | 186 +- Fonts/IBM_Plex_Mono/OFL.txt | 186 +- Fonts/Inconsolata/OFL.txt | 186 +- Fonts/JetBrains_Mono/OFL.txt | 186 +- Fonts/Kode_Mono/OFL.txt | 186 +- Fonts/M_PLUS_1_Code/OFL.txt | 186 +- Fonts/M_PLUS_Code_Latin/OFL.txt | 186 +- Fonts/Major_Mono_Display/OFL.txt | 186 +- Fonts/Martian_Mono/OFL.txt | 186 +- Fonts/Nanum_Gothic_Coding/OFL.txt | 192 +- Fonts/Noto_Sans_Mono/OFL.txt | 186 +- Fonts/Nova_Mono/OFL.txt | 188 +- Fonts/Overpass_Mono/OFL.txt | 186 +- Fonts/Oxygen_Mono/OFL.txt | 186 +- Fonts/PT_Mono/OFL.txt | 188 +- Fonts/Press_Start_2P/OFL.txt | 186 +- Fonts/Red_Hat_Mono/OFL.txt | 186 +- Fonts/Roboto_Condensed/OFL.txt | 186 +- Fonts/Roboto_Mono/LICENSE.txt | 404 +- Fonts/Rubik_Mono_One/OFL.txt | 186 +- Fonts/Share_Tech_Mono/OFL.txt | 186 +- Fonts/Silkscreen/OFL.txt | 186 +- Fonts/Sono/OFL.txt | 186 +- Fonts/Source_Code_Pro/OFL.txt | 186 +- Fonts/Space_Mono/OFL.txt | 186 +- Fonts/Syne_Mono/OFL.txt | 186 +- Fonts/Ubuntu_Mono/UFL.txt | 192 +- Fonts/VT323/OFL.txt | 186 +- Fonts/Varela_Round/OFL.txt | 186 +- Fonts/Xanh_Mono/OFL.txt | 186 +- README.md | 3 + Runtime/Attributes/DxShowIfAttribute.cs | 32 +- .../Attributes/RegisterCommandAttribute.cs | 112 +- .../Backend/BuiltinCommands.cs | 1438 ++--- .../Backend/CommandAutoComplete.cs | 470 +- .../CommandTerminal/Backend/CommandHistory.cs | 260 +- .../CommandTerminal/Backend/CommandInfo.cs | 62 +- Runtime/CommandTerminal/Backend/CommandLog.cs | 396 +- .../CommandTerminal/Backend/CommandShell.cs | 1314 ++-- .../Backend/HintDisplayMode.cs | 26 +- Runtime/CommandTerminal/Backend/Terminal.cs | 68 +- .../Input/DefaultTerminalInput.cs | 22 +- .../CommandTerminal/Input/IInputHandler.cs | 14 +- .../CommandTerminal/Input/ITerminalInput.cs | 14 +- Runtime/CommandTerminal/Input/InputHelpers.cs | 800 +-- Runtime/CommandTerminal/Input/InputMode.cs | 40 +- .../Input/TerminalControlTypes.cs | 36 +- .../Input/TerminalKeyboardController.cs | 644 +- .../Input/TerminalPlayerInputController.cs | 326 +- .../Persistence/TerminalThemeConfiguration.cs | 98 +- .../TerminalThemeConfigurations.cs | 162 +- .../Persistence/TerminalThemePersister.cs | 616 +- .../Themes/TerminalFontPack.cs | 36 +- .../Themes/TerminalThemePack.cs | 46 +- .../CommandTerminal/Themes/ThemeNameHelper.cs | 84 +- Runtime/CommandTerminal/UI/TerminalState.cs | 26 +- Runtime/CommandTerminal/UI/TerminalUI.cs | 4496 +++++++------- Runtime/DataStructures/CyclicBuffer.cs | 380 +- Runtime/Extensions/IListExtensions.cs | 192 +- .../SerializedPropertyExtensions.cs | 528 +- Runtime/Extensions/StringExtensions.cs | 56 +- Runtime/Helper/DirectoryHelper.cs | 256 +- Runtime/Helper/ThreadLocalRandom.cs | 24 +- Styles/BaseStyles.uss | 500 +- Styles/TerminalThemeSettings-Base.tss | 8 +- Styles/Themes/AlienJungleTheme.uss | 50 +- Styles/Themes/AmberGlowTheme.uss | 50 +- Styles/Themes/AndromedaTheme.uss | 50 +- Styles/Themes/ArcticNightTheme.uss | 50 +- Styles/Themes/ArcticWhiteTheme.uss | 50 +- Styles/Themes/AtomLightTheme.uss | 50 +- Styles/Themes/AtomOneDarkTheme.uss | 50 +- Styles/Themes/AuroraTheme.uss | 50 +- Styles/Themes/AutumnLeavesTheme.uss | 50 +- Styles/Themes/AyuLightTheme.uss | 50 +- Styles/Themes/AyuMirageTheme.uss | 50 +- Styles/Themes/Base16TomorrowNightTheme.uss | 50 +- Styles/Themes/BerrySmoothieTheme.uss | 50 +- Styles/Themes/BiohazardTheme.uss | 50 +- Styles/Themes/BlackboardTheme.uss | 50 +- Styles/Themes/BlueprintTheme.uss | 50 +- Styles/Themes/BreathOfTheWildTheme.uss | 50 +- Styles/Themes/BrogrammerTheme.uss | 50 +- Styles/Themes/BrushedMetalTheme.uss | 50 +- Styles/Themes/BubbleGumTheme.uss | 50 +- Styles/Themes/CarbonFiberTheme.uss | 50 +- Styles/Themes/CatppuccinTheme.uss | 50 +- Styles/Themes/ClassicGreenTheme.uss | 50 +- Styles/Themes/Cobalt2Theme.uss | 50 +- Styles/Themes/CoffeeShopTheme.uss | 50 +- Styles/Themes/Comodore64Theme.uss | 50 +- Styles/Themes/CoolBlueTheme.uss | 50 +- Styles/Themes/CoralReefTheme.uss | 50 +- Styles/Themes/CreamTheme.uss | 50 +- Styles/Themes/CyberpunkNeonTheme.uss | 50 +- Styles/Themes/DarculaTheme.uss | 50 +- Styles/Themes/DarkTheme.uss | 50 +- Styles/Themes/DeepForestTheme.uss | 50 +- Styles/Themes/DesertNightTheme.uss | 50 +- Styles/Themes/DesertOasisTheme.uss | 50 +- Styles/Themes/DimmedTheme.uss | 50 +- Styles/Themes/DoomEternalTheme.uss | 50 +- Styles/Themes/DraculaTheme.uss | 50 +- Styles/Themes/DuskTheme.uss | 50 +- Styles/Themes/EldritchHorrorTheme.uss | 50 +- Styles/Themes/EverforestDarkTheme.uss | 50 +- Styles/Themes/FlatUITheme.uss | 50 +- Styles/Themes/ForestFloorTheme.uss | 50 +- Styles/Themes/ForestNightTheme.uss | 50 +- Styles/Themes/GithubDarkTheme.uss | 50 +- Styles/Themes/GithubLightTheme.uss | 50 +- Styles/Themes/GlitchTheme.uss | 50 +- Styles/Themes/GraphiteTheme.uss | 50 +- Styles/Themes/GruvBoxDarkTheme.uss | 50 +- Styles/Themes/GruvBoxLightTheme.uss | 50 +- Styles/Themes/HackerMatrixTheme.uss | 50 +- Styles/Themes/HighContrastBlackTheme.uss | 50 +- Styles/Themes/HighContrastLightTheme.uss | 50 +- Styles/Themes/HoloInterfaceTheme.uss | 50 +- Styles/Themes/HotPinkNeonTheme.uss | 50 +- Styles/Themes/IcebergDarkTheme.uss | 50 +- Styles/Themes/IcebergLightTheme.uss | 50 +- Styles/Themes/JungleTheme.uss | 50 +- Styles/Themes/KimbieDarkTheme.uss | 50 +- Styles/Themes/LightTheme.uss | 50 +- Styles/Themes/LinenTheme.uss | 50 +- Styles/Themes/MatchaTheme.uss | 50 +- Styles/Themes/MaterialDarkerTheme.uss | 50 +- Styles/Themes/MaterialLightTheme.uss | 50 +- Styles/Themes/MaterialOceanicTheme.uss | 50 +- Styles/Themes/MaterialPaleNightTheme.uss | 50 +- Styles/Themes/MignightCityTheme.uss | 50 +- Styles/Themes/MolokaiTheme.uss | 50 +- Styles/Themes/MonokaiTheme.uss | 50 +- Styles/Themes/MountainPeakTheme.uss | 50 +- Styles/Themes/NeonNoirTheme.uss | 50 +- Styles/Themes/NightOwlTheme.uss | 50 +- Styles/Themes/NoctisTheme.uss | 50 +- Styles/Themes/NordTheme.uss | 50 +- Styles/Themes/OblivionTheme.uss | 50 +- Styles/Themes/ObsidianTheme.uss | 50 +- Styles/Themes/OceanDeepTheme.uss | 50 +- Styles/Themes/OceanicNextTheme.uss | 50 +- Styles/Themes/OldPaperTheme.uss | 50 +- Styles/Themes/OneDarkProTheme.uss | 50 +- Styles/Themes/OneDarkTheme.uss | 50 +- Styles/Themes/OneLightTheme.uss | 50 +- Styles/Themes/OneMonokaiTheme.uss | 50 +- Styles/Themes/OutrunElectricTheme.uss | 50 +- Styles/Themes/PaleNightTheme.uss | 50 +- Styles/Themes/PandaSyntaxTheme.uss | 50 +- Styles/Themes/PaperColorLightTheme.uss | 50 +- Styles/Themes/PaperWhiteTheme.uss | 50 +- Styles/Themes/PastelDreamsTheme.uss | 50 +- Styles/Themes/PipBoyTheme.uss | 50 +- Styles/Themes/PortalTheme.uss | 50 +- Styles/Themes/PureBlackWhiteTheme.uss | 50 +- Styles/Themes/PurpleHazeTheme.uss | 50 +- Styles/Themes/RedAlertTheme.uss | 52 +- Styles/Themes/RedmondTheme.uss | 50 +- Styles/Themes/RedwoodTheme.uss | 50 +- Styles/Themes/RosePineTheme.uss | 50 +- Styles/Themes/SavannaTheme.uss | 50 +- Styles/Themes/SepiaTheme.uss | 50 +- Styles/Themes/ShadesOfPurpleTheme.uss | 50 +- Styles/Themes/SlateTheme.uss | 50 +- Styles/Themes/SlimeTheme.uss | 50 +- Styles/Themes/SolarizedDarkTheme.uss | 50 +- Styles/Themes/SolarizedLightTheme.uss | 50 +- Styles/Themes/StardewValleyTheme.uss | 50 +- Styles/Themes/StarfieldTheme.uss | 50 +- Styles/Themes/SteampunkTheme.uss | 50 +- Styles/Themes/SublimeTheme.uss | 50 +- Styles/Themes/SubtleGreyTheme.uss | 50 +- Styles/Themes/SunsetTheme.uss | 50 +- Styles/Themes/SwampTheme.uss | 50 +- Styles/Themes/Synthwave84Theme.uss | 50 +- Styles/Themes/TokyoNightTheme.uss | 50 +- Styles/Themes/TomorrowLightTheme.uss | 50 +- Styles/Themes/TomorrowNightTheme.uss | 50 +- Styles/Themes/UbuntuTheme.uss | 50 +- Styles/Themes/UnderwaterTheme.uss | 50 +- Styles/Themes/VS2019DarkTheme.uss | 50 +- Styles/Themes/VaporwaveTheme.uss | 50 +- Styles/Themes/VisualStudioDarkTheme.uss | 50 +- Styles/Themes/VisualStudioLightTheme.uss | 50 +- Styles/Themes/VolcanoTheme.uss | 50 +- Styles/Themes/WinterDarkTheme.uss | 50 +- Styles/Themes/WinterLightTheme.uss | 50 +- Styles/Themes/WinterSkyTheme.uss | 50 +- Styles/Themes/ZenBurnTheme.uss | 50 +- Tests/Runtime/CommandArgTests.cs | 5436 ++++++++--------- Tests/Runtime/CommandShellTests.cs | 706 +-- Tests/Runtime/Components/StartTracker.cs | 30 +- .../Components/TerminalInputHandler.cs | 18 +- Tests/Runtime/Components/TestCommands.cs | 152 +- Tests/Runtime/TerminalTests.cs | 336 +- ...ios.DxCommandTerminal.Tests.Runtime.asmdef | 33 +- package.json | 86 +- 225 files changed, 19965 insertions(+), 19928 deletions(-) create mode 100644 .gitattributes diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9860f54..852656f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -1,10 +1,10 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "CSharpier": { - "version": "1.1.2", - "commands": ["csharpier"] - } - } -} \ No newline at end of file +{ + "version": 1, + "isRoot": true, + "tools": { + "CSharpier": { + "version": "1.1.2", + "commands": ["csharpier"] + } + } +} diff --git a/.editorconfig b/.editorconfig index 23dd18e..6b8819e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,184 +1,184 @@ - -[*] -charset = utf-8 -end_of_line = crlf -trim_trailing_whitespace = false -insert_final_newline = false -indent_style = space -indent_size = 4 - -# Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers = false -csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion -csharp_prefer_braces = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_var_elsewhere = false:suggestion -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = false:suggestion -csharp_using_directive_placement = inside_namespace:silent -dotnet_naming_rule.event_rule.import_to_resharper = True -dotnet_naming_rule.event_rule.resharper_description = Events -dotnet_naming_rule.event_rule.resharper_guid = 0c4c6401-2a1f-4db1-a21f-562f51542cf8 -dotnet_naming_rule.event_rule.severity = warning -dotnet_naming_rule.event_rule.style = on_upper_camel_case_style -dotnet_naming_rule.event_rule.symbols = event_symbols -dotnet_naming_rule.interfaces_rule.import_to_resharper = True -dotnet_naming_rule.interfaces_rule.resharper_description = Interfaces -dotnet_naming_rule.interfaces_rule.resharper_guid = a7a3339e-4e89-4319-9735-a9dc4cb74cc7 -dotnet_naming_rule.interfaces_rule.severity = warning -dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style -dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols -dotnet_naming_rule.local_constants_rule.import_to_resharper = True -dotnet_naming_rule.local_constants_rule.resharper_description = Local constants -dotnet_naming_rule.local_constants_rule.resharper_guid = a4f433b8-abcd-4e55-a08f-82e78cef0f0c -dotnet_naming_rule.local_constants_rule.resharper_style = AaBb, aaBb -dotnet_naming_rule.local_constants_rule.severity = warning -dotnet_naming_rule.local_constants_rule.style = upper_camel_case_style -dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols -dotnet_naming_rule.public_fields_override_rule.import_to_resharper = False -dotnet_naming_rule.public_fields_override_rule.severity = warning -dotnet_naming_rule.public_fields_override_rule.style = upper_camel_case_style -dotnet_naming_rule.public_fields_override_rule.symbols = public_fields_override_symbols -dotnet_naming_rule.public_fields_override_rule_1.import_to_resharper = False -dotnet_naming_rule.public_fields_override_rule_1.severity = warning -dotnet_naming_rule.public_fields_override_rule_1.style = upper_camel_case_style -dotnet_naming_rule.public_fields_override_rule_1.symbols = public_fields_override_symbols_1 -dotnet_naming_rule.public_fields_rule.import_to_resharper = True -dotnet_naming_rule.public_fields_rule.resharper_description = Instance fields (not private) -dotnet_naming_rule.public_fields_rule.resharper_guid = 53eecf85-d821-40e8-ac97-fdb734542b84 -dotnet_naming_rule.public_fields_rule.severity = warning -dotnet_naming_rule.public_fields_rule.style = lower_camel_case_style -dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols -dotnet_naming_rule.public_static_fields_rule.import_to_resharper = True -dotnet_naming_rule.public_static_fields_rule.resharper_description = Static fields (not private) -dotnet_naming_rule.public_static_fields_rule.resharper_guid = 70345118-4b40-4ece-937c-bbeb7a0b2e70 -dotnet_naming_rule.public_static_fields_rule.severity = warning -dotnet_naming_rule.public_static_fields_rule.style = upper_camel_case_style -dotnet_naming_rule.public_static_fields_rule.symbols = public_static_fields_symbols -dotnet_naming_rule.type_parameters_rule.import_to_resharper = True -dotnet_naming_rule.type_parameters_rule.resharper_description = Type parameters -dotnet_naming_rule.type_parameters_rule.resharper_guid = 2c62818f-621b-4425-adc9-78611099bfcb -dotnet_naming_rule.type_parameters_rule.severity = warning -dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style -dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols -dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True -dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field -dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef -dotnet_naming_rule.unity_serialized_field_rule.severity = none -dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style -dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols -dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case -dotnet_naming_style.i_upper_camel_case_style.required_prefix = I -dotnet_naming_style.lower_camel_case_style.capitalization = camel_case -dotnet_naming_style.on_upper_camel_case_style.capitalization = pascal_case -dotnet_naming_style.on_upper_camel_case_style.required_prefix = On -dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case -dotnet_naming_style.t_upper_camel_case_style.required_prefix = T -dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case -dotnet_naming_symbols.event_symbols.applicable_accessibilities = * -dotnet_naming_symbols.event_symbols.applicable_kinds = event -dotnet_naming_symbols.event_symbols.resharper_applicable_kinds = event -dotnet_naming_symbols.event_symbols.resharper_required_modifiers = any -dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = * -dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface -dotnet_naming_symbols.interfaces_symbols.resharper_applicable_kinds = interface -dotnet_naming_symbols.interfaces_symbols.resharper_required_modifiers = any -dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = * -dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local -dotnet_naming_symbols.local_constants_symbols.required_modifiers = const -dotnet_naming_symbols.local_constants_symbols.resharper_applicable_kinds = local_constant -dotnet_naming_symbols.local_constants_symbols.resharper_required_modifiers = any -dotnet_naming_symbols.public_fields_override_symbols.applicable_accessibilities = public -dotnet_naming_symbols.public_fields_override_symbols.applicable_kinds = field -dotnet_naming_symbols.public_fields_override_symbols.required_modifiers = const -dotnet_naming_symbols.public_fields_override_symbols_1.applicable_accessibilities = public -dotnet_naming_symbols.public_fields_override_symbols_1.applicable_kinds = field -dotnet_naming_symbols.public_fields_override_symbols_1.required_modifiers = readonly,static -dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public -dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field -dotnet_naming_symbols.public_fields_symbols.resharper_applicable_kinds = field,readonly_field -dotnet_naming_symbols.public_fields_symbols.resharper_required_modifiers = instance -dotnet_naming_symbols.public_static_fields_symbols.applicable_accessibilities = public -dotnet_naming_symbols.public_static_fields_symbols.applicable_kinds = field -dotnet_naming_symbols.public_static_fields_symbols.required_modifiers = static -dotnet_naming_symbols.public_static_fields_symbols.resharper_applicable_kinds = field -dotnet_naming_symbols.public_static_fields_symbols.resharper_required_modifiers = static -dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * -dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter -dotnet_naming_symbols.type_parameters_symbols.resharper_applicable_kinds = type_parameter -dotnet_naming_symbols.type_parameters_symbols.resharper_required_modifiers = any -dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * -dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = -dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field -dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance -dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none -dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion -dotnet_style_qualification_for_event = false:suggestion -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion - -# ReSharper properties -resharper_autodetect_indent_settings = true -resharper_braces_redundant = true -resharper_csharp_wrap_lines = false -resharper_formatter_off_tag = @formatter:off -resharper_formatter_on_tag = @formatter:on -resharper_formatter_tags_enabled = true -resharper_indent_preprocessor_directives = normal -resharper_keep_existing_attribute_arrangement = true -resharper_place_attribute_on_same_line = false -resharper_show_autodetect_configure_formatting_tip = false -resharper_use_indent_from_vs = false - -# ReSharper inspection severities -resharper_arrange_redundant_parentheses_highlighting = hint -resharper_arrange_this_qualifier_highlighting = hint -resharper_arrange_trailing_comma_in_multiline_lists_highlighting = none -resharper_arrange_type_member_modifiers_highlighting = hint -resharper_arrange_type_modifiers_highlighting = hint -resharper_built_in_type_reference_style_for_member_access_highlighting = hint -resharper_built_in_type_reference_style_highlighting = hint -resharper_mvc_action_not_resolved_highlighting = warning -resharper_mvc_area_not_resolved_highlighting = warning -resharper_mvc_controller_not_resolved_highlighting = warning -resharper_mvc_masterpage_not_resolved_highlighting = warning -resharper_mvc_partial_view_not_resolved_highlighting = warning -resharper_mvc_template_not_resolved_highlighting = warning -resharper_mvc_view_component_not_resolved_highlighting = warning -resharper_mvc_view_component_view_not_resolved_highlighting = warning -resharper_mvc_view_not_resolved_highlighting = warning -resharper_prefer_concrete_value_over_default_highlighting = none -resharper_razor_assembly_not_resolved_highlighting = warning -resharper_redundant_base_qualifier_highlighting = warning -resharper_suggest_var_or_type_built_in_types_highlighting = hint -resharper_suggest_var_or_type_elsewhere_highlighting = hint -resharper_suggest_var_or_type_simple_types_highlighting = hint -resharper_web_config_module_not_resolved_highlighting = warning -resharper_web_config_type_not_resolved_highlighting = warning -resharper_web_config_wrong_module_highlighting = warning - -[{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] -indent_style = space -indent_size = 2 - -[{*.yaml,*.yml}] -indent_style = space -indent_size = 2 - -[*.asmdef] -indent_style = space -indent_size = 2 - -[*.asmref] -indent_style = space -indent_size = 2 - -[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] -indent_style = space -indent_size = 4 -tab_width = 4 + +[*] +charset = utf-8 +end_of_line = crlf +trim_trailing_whitespace = false +insert_final_newline = false +indent_style = space +indent_size = 4 + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_prefer_braces = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = false:suggestion +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:suggestion +csharp_using_directive_placement = inside_namespace:silent +dotnet_naming_rule.event_rule.import_to_resharper = True +dotnet_naming_rule.event_rule.resharper_description = Events +dotnet_naming_rule.event_rule.resharper_guid = 0c4c6401-2a1f-4db1-a21f-562f51542cf8 +dotnet_naming_rule.event_rule.severity = warning +dotnet_naming_rule.event_rule.style = on_upper_camel_case_style +dotnet_naming_rule.event_rule.symbols = event_symbols +dotnet_naming_rule.interfaces_rule.import_to_resharper = True +dotnet_naming_rule.interfaces_rule.resharper_description = Interfaces +dotnet_naming_rule.interfaces_rule.resharper_guid = a7a3339e-4e89-4319-9735-a9dc4cb74cc7 +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols +dotnet_naming_rule.local_constants_rule.import_to_resharper = True +dotnet_naming_rule.local_constants_rule.resharper_description = Local constants +dotnet_naming_rule.local_constants_rule.resharper_guid = a4f433b8-abcd-4e55-a08f-82e78cef0f0c +dotnet_naming_rule.local_constants_rule.resharper_style = AaBb, aaBb +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols +dotnet_naming_rule.public_fields_override_rule.import_to_resharper = False +dotnet_naming_rule.public_fields_override_rule.severity = warning +dotnet_naming_rule.public_fields_override_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_override_rule.symbols = public_fields_override_symbols +dotnet_naming_rule.public_fields_override_rule_1.import_to_resharper = False +dotnet_naming_rule.public_fields_override_rule_1.severity = warning +dotnet_naming_rule.public_fields_override_rule_1.style = upper_camel_case_style +dotnet_naming_rule.public_fields_override_rule_1.symbols = public_fields_override_symbols_1 +dotnet_naming_rule.public_fields_rule.import_to_resharper = True +dotnet_naming_rule.public_fields_rule.resharper_description = Instance fields (not private) +dotnet_naming_rule.public_fields_rule.resharper_guid = 53eecf85-d821-40e8-ac97-fdb734542b84 +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols +dotnet_naming_rule.public_static_fields_rule.import_to_resharper = True +dotnet_naming_rule.public_static_fields_rule.resharper_description = Static fields (not private) +dotnet_naming_rule.public_static_fields_rule.resharper_guid = 70345118-4b40-4ece-937c-bbeb7a0b2e70 +dotnet_naming_rule.public_static_fields_rule.severity = warning +dotnet_naming_rule.public_static_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_static_fields_rule.symbols = public_static_fields_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper = True +dotnet_naming_rule.type_parameters_rule.resharper_description = Type parameters +dotnet_naming_rule.type_parameters_rule.resharper_guid = 2c62818f-621b-4425-adc9-78611099bfcb +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = none +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix = I +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.on_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.on_upper_camel_case_style.required_prefix = On +dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix = T +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.event_symbols.applicable_accessibilities = * +dotnet_naming_symbols.event_symbols.applicable_kinds = event +dotnet_naming_symbols.event_symbols.resharper_applicable_kinds = event +dotnet_naming_symbols.event_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = * +dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface +dotnet_naming_symbols.interfaces_symbols.resharper_applicable_kinds = interface +dotnet_naming_symbols.interfaces_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = * +dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local +dotnet_naming_symbols.local_constants_symbols.required_modifiers = const +dotnet_naming_symbols.local_constants_symbols.resharper_applicable_kinds = local_constant +dotnet_naming_symbols.local_constants_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.public_fields_override_symbols.applicable_accessibilities = public +dotnet_naming_symbols.public_fields_override_symbols.applicable_kinds = field +dotnet_naming_symbols.public_fields_override_symbols.required_modifiers = const +dotnet_naming_symbols.public_fields_override_symbols_1.applicable_accessibilities = public +dotnet_naming_symbols.public_fields_override_symbols_1.applicable_kinds = field +dotnet_naming_symbols.public_fields_override_symbols_1.required_modifiers = readonly,static +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public +dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.public_fields_symbols.resharper_applicable_kinds = field,readonly_field +dotnet_naming_symbols.public_fields_symbols.resharper_required_modifiers = instance +dotnet_naming_symbols.public_static_fields_symbols.applicable_accessibilities = public +dotnet_naming_symbols.public_static_fields_symbols.applicable_kinds = field +dotnet_naming_symbols.public_static_fields_symbols.required_modifiers = static +dotnet_naming_symbols.public_static_fields_symbols.resharper_applicable_kinds = field +dotnet_naming_symbols.public_static_fields_symbols.resharper_required_modifiers = static +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters_symbols.resharper_applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters_symbols.resharper_required_modifiers = any +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_autodetect_indent_settings = true +resharper_braces_redundant = true +resharper_csharp_wrap_lines = false +resharper_formatter_off_tag = @formatter:off +resharper_formatter_on_tag = @formatter:on +resharper_formatter_tags_enabled = true +resharper_indent_preprocessor_directives = normal +resharper_keep_existing_attribute_arrangement = true +resharper_place_attribute_on_same_line = false +resharper_show_autodetect_configure_formatting_tip = false +resharper_use_indent_from_vs = false + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_trailing_comma_in_multiline_lists_highlighting = none +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_mvc_action_not_resolved_highlighting = warning +resharper_mvc_area_not_resolved_highlighting = warning +resharper_mvc_controller_not_resolved_highlighting = warning +resharper_mvc_masterpage_not_resolved_highlighting = warning +resharper_mvc_partial_view_not_resolved_highlighting = warning +resharper_mvc_template_not_resolved_highlighting = warning +resharper_mvc_view_component_not_resolved_highlighting = warning +resharper_mvc_view_component_view_not_resolved_highlighting = warning +resharper_mvc_view_not_resolved_highlighting = warning +resharper_prefer_concrete_value_over_default_highlighting = none +resharper_razor_assembly_not_resolved_highlighting = warning +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_style = space +indent_size = 2 + +[{*.yaml,*.yml}] +indent_style = space +indent_size = 2 + +[*.asmdef] +indent_style = space +indent_size = 2 + +[*.asmref] +indent_style = space +indent_size = 2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2e3ef54 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,42 @@ +# Normalize text and enforce CRLF in working tree for key files +* text=auto + +# Enforce CRLF for source and docs (matches Prettier/CSharpier + CI checks) +*.cs text eol=crlf +*.csproj text eol=crlf +*.sln text eol=crlf +*.props text eol=crlf +*.targets text eol=crlf + +*.md text eol=crlf +*.markdown text eol=crlf + +*.json text eol=crlf +*.asmdef text eol=crlf +*.asmref text eol=crlf + +*.yml text eol=crlf +*.yaml text eol=crlf + +# Common config treated as text with CRLF +.editorconfig text eol=crlf +.prettierrc text eol=crlf +.prettierrc.json text eol=crlf +.markdownlint.json text eol=crlf +.yamllint.yaml text eol=crlf + +# Unity / assets and binaries (do not modify EOL) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.mp4 binary +*.otf binary +*.ttf binary +*.woff binary +*.woff2 binary +*.unity binary +*.prefab binary +*.mat binary +*.asset binary +*.dll binary diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 1c63eed..f9ac52e 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,75 +1,75 @@ -name: Publish to NPM - -on: - push: - branches: - - main - paths: - - 'package.json' - workflow_dispatch: - -jobs: - publish_npm: - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 # Ensure full commit history for version comparison - - - name: Set up Node.js - uses: actions/setup-node@v5 - with: - node-version: 18 - registry-url: 'https://registry.npmjs.org/' - - - name: Check if version changed - id: version_check - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "Manual trigger detected. Skipping version check." - NEW_VERSION=$(jq -r '.version' package.json) - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV - echo "should_publish=true" >> $GITHUB_ENV - if [[ "$NEW_VERSION" == *"rc"* ]]; then - echo "This is a pre-release (next tag)." - echo "NPM_TAG=next" >> $GITHUB_ENV - else - echo "This is a stable release (latest tag)." - echo "NPM_TAG=latest" >> $GITHUB_ENV - fi - else - PREV_VERSION=$(git show HEAD~1:package.json | jq -r '.version' || echo "0.0.0") - NEW_VERSION=$(jq -r '.version' package.json) - echo "Previous version: $PREV_VERSION" - echo "New version: $NEW_VERSION" - - if [ "$PREV_VERSION" != "$NEW_VERSION" ]; then - echo "Version changed, proceeding..." - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV - echo "should_publish=true" >> $GITHUB_ENV - - # Detect pre-releases (versions with "rc" or similar tags) - if [[ "$NEW_VERSION" == *"rc"* ]]; then - echo "This is a pre-release (next tag)." - echo "NPM_TAG=next" >> $GITHUB_ENV - else - echo "This is a stable release (latest tag)." - echo "NPM_TAG=latest" >> $GITHUB_ENV - fi - else - echo "Version did not change, skipping..." - echo "should_publish=false" >> $GITHUB_ENV - fi - fi - - - name: Install Dependencies - if: env.should_publish == 'true' - run: npm install - - - name: Publish to NPM - if: env.should_publish == 'true' - run: npm publish --access public --tag ${{ env.NPM_TAG }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} +name: Publish to NPM + +on: + push: + branches: + - main + paths: + - 'package.json' + workflow_dispatch: + +jobs: + publish_npm: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 # Ensure full commit history for version comparison + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 18 + registry-url: 'https://registry.npmjs.org/' + + - name: Check if version changed + id: version_check + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "Manual trigger detected. Skipping version check." + NEW_VERSION=$(jq -r '.version' package.json) + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + echo "should_publish=true" >> $GITHUB_ENV + if [[ "$NEW_VERSION" == *"rc"* ]]; then + echo "This is a pre-release (next tag)." + echo "NPM_TAG=next" >> $GITHUB_ENV + else + echo "This is a stable release (latest tag)." + echo "NPM_TAG=latest" >> $GITHUB_ENV + fi + else + PREV_VERSION=$(git show HEAD~1:package.json | jq -r '.version' || echo "0.0.0") + NEW_VERSION=$(jq -r '.version' package.json) + echo "Previous version: $PREV_VERSION" + echo "New version: $NEW_VERSION" + + if [ "$PREV_VERSION" != "$NEW_VERSION" ]; then + echo "Version changed, proceeding..." + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + echo "should_publish=true" >> $GITHUB_ENV + + # Detect pre-releases (versions with "rc" or similar tags) + if [[ "$NEW_VERSION" == *"rc"* ]]; then + echo "This is a pre-release (next tag)." + echo "NPM_TAG=next" >> $GITHUB_ENV + else + echo "This is a stable release (latest tag)." + echo "NPM_TAG=latest" >> $GITHUB_ENV + fi + else + echo "Version did not change, skipping..." + echo "should_publish=false" >> $GITHUB_ENV + fi + fi + + - name: Install Dependencies + if: env.should_publish == 'true' + run: npm install + + - name: Publish to NPM + if: env.should_publish == 'true' + run: npm publish --access public --tag ${{ env.NPM_TAG }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 5a50f4a..fb447e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - -# NPM -node_modules/ + +# NPM +node_modules/ node_modules.meta \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fac127c..415414f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,47 +1,51 @@ -repos: - - repo: local - hooks: - - id: dotnet-tool-restore - name: Install .NET tools - entry: dotnet tool restore - language: system - always_run: true - pass_filenames: false - stages: - - pre-commit - - pre-push - - post-checkout - - post-rewrite - description: Install the .NET tools listed at .config/dotnet-tools.json. - - id: csharpier - name: Run CSharpier on C# files - entry: dotnet tool run csharpier format - language: system - types: - - c# - description: CSharpier is an opinionated C# formatter inspired by Prettier. - - - repo: local - hooks: - - id: prettier - name: Prettier (Markdown, JSON, asmdef, asmref, YAML) - entry: npx --yes prettier --write - language: system - files: '(?i)\.(md|markdown|json|asmdef|asmref|ya?ml)$' - description: Use the repo's Prettier version from package.json. - - - repo: local - hooks: - - id: markdownlint - name: markdownlint (respect repo config) - entry: npx --yes markdownlint-cli --config .markdownlint.json --ignore-path .markdownlintignore - language: system - files: '(?i)\.(md|markdown)$' - - - repo: local - hooks: - - id: yamllint - name: yamllint (if available) - entry: bash -c 'if command -v yamllint >/dev/null 2>&1; then yamllint -c .yamllint.yaml "$@"; else echo "yamllint not installed; skipping"; fi' -- - language: system - files: '(?i)\.(ya?ml)$' +repos: + - repo: local + hooks: + - id: dotnet-tool-restore + name: Install .NET tools + entry: dotnet tool restore + language: system + always_run: true + pass_filenames: false + stages: + - pre-commit + - pre-push + - post-checkout + - post-rewrite + description: Install the .NET tools listed at .config/dotnet-tools.json. + - id: csharpier + name: Run CSharpier on C# files + entry: dotnet tool run csharpier format . + language: system + # Run across the whole repo so it matches CI's full-repo check + pass_filenames: false + always_run: true + description: CSharpier is an opinionated C# formatter inspired by Prettier. + + - repo: local + hooks: + - id: prettier + name: Prettier (Markdown, JSON, asmdef, asmref, YAML) + entry: npx --yes prettier --write . + language: system + # Run across the whole repo to match CI (filter still documents scope) + pass_filenames: false + always_run: true + files: '(?i)\.(md|markdown|json|asmdef|asmref|ya?ml)$' + description: Use the repo's Prettier version from package.json. + + - repo: local + hooks: + - id: markdownlint + name: markdownlint (respect repo config) + entry: npx --yes markdownlint-cli --config .markdownlint.json --ignore-path .markdownlintignore + language: system + files: '(?i)\.(md|markdown)$' + + - repo: local + hooks: + - id: yamllint + name: yamllint (if available) + entry: bash -c 'if command -v yamllint >/dev/null 2>&1; then yamllint -c .yamllint.yaml "$@"; else echo "yamllint not installed; skipping"; fi' -- + language: system + files: '(?i)\.(ya?ml)$' diff --git a/AGENTS.md b/AGENTS.md index 12a26ad..9503aa9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ - Follow `.editorconfig`: - Indentation: spaces (C# 4), JSON/YAML/asmdef 2. - - Line endings: CRLF; encoding: UTF-8 BOM. + - Line endings: CRLF; encoding: UTF-8 (no BOM). - C#: prefer braces; explicit types over `var` unless obvious; `using` inside namespace. - Naming: Interfaces `IType`, type params `TType`, events/types/methods PascalCase; tests end with `Tests`. - Use CSharpier for formatting before committing. diff --git a/Editor/CustomEditors/TerminalFontPackEditor.cs b/Editor/CustomEditors/TerminalFontPackEditor.cs index 22c45be..fca36ec 100644 --- a/Editor/CustomEditors/TerminalFontPackEditor.cs +++ b/Editor/CustomEditors/TerminalFontPackEditor.cs @@ -1,282 +1,282 @@ -namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors -{ -#if UNITY_EDITOR - using System; - using System.Collections.Generic; - using System.IO; - using DxCommandTerminal.Helper; - using Extensions; - using Themes; - using UnityEditor; - using UnityEngine; - using Object = UnityEngine.Object; - - [CustomEditor(typeof(TerminalFontPack))] - public sealed class TerminalFontPackEditor : Editor - { - [Flags] - private enum FontType - { - None = 0, - Normal = 1 << 0, - Bold = 1 << 1, - Italic = 1 << 2, - BoldItalic = 1 << 3, - ExtraBold = 1 << 4, - ExtraBoldItalic = 1 << 5, - ExtraLight = 1 << 6, - ExtraLightItalic = 1 << 7, - Light = 1 << 8, - LightItalic = 1 << 9, - Medium = 1 << 10, - MediumItalic = 1 << 11, - SemiBold = 1 << 12, - SemiBoldItalic = 1 << 13, - Thin = 1 << 14, - ThinItalic = 1 << 15, - Black = 1 << 16, - BlackItalic = 1 << 17, - Regular = 1 << 18, - Variable = 1 << 19, - Monospace = 1 << 20, - Condensed = 1 << 21, - CondensedBold = 1 << 22, - CondensedExtraBold = 1 << 23, - CondensedExtraLight = 1 << 24, - CondensedLight = 1 << 25, - CondensedMedium = 1 << 26, - CondensedSemiBold = 1 << 27, - CondensedThin = 1 << 28, - VariableFont_wght = 1 << 29, - VariableFont_width = 1 << 30, - } - - private readonly HashSet _fontCache = new(); - private FontType _fontRemovalType = FontType.None; - private FontType _fontAdditionType = FontType.None; - private string _lastSelectedDirectory; - private GUIStyle _impactButtonStyle; - - private void OnEnable() - { - _fontCache.Clear(); - _fontRemovalType = FontType.None; - } - - public override void OnInspectorGUI() - { - _impactButtonStyle ??= new GUIStyle(GUI.skin.button) - { - normal = { textColor = Color.yellow }, - fontStyle = FontStyle.Bold, - }; - - serializedObject.Update(); - TerminalFontPack fontPack = target as TerminalFontPack; - base.OnInspectorGUI(); - - if (fontPack == null) - { - return; - } - - bool anyChanged = false; - if (fontPack._fonts == null) - { - anyChanged = true; - fontPack._fonts = new List(); - } - - _fontCache.Clear(); - bool anyNullFont = false; - foreach (Font font in fontPack._fonts) - { - if (font == null) - { - anyNullFont = true; - } - _fontCache.Add(font); - } - - EditorGUILayout.Space(10); - EditorGUILayout.LabelField("Data Manipulation", EditorStyles.boldLabel); - - if (anyNullFont || _fontCache.Count != fontPack._fonts.Count) - { - if (GUILayout.Button("Fix Invalid Fonts", _impactButtonStyle)) - { - _fontCache.Clear(); - for (int i = fontPack._fonts.Count - 1; 0 <= i; --i) - { - Font font = fontPack._fonts[i]; - if (font != null && _fontCache.Add(font)) - { - continue; - } - - anyChanged = true; - fontPack._fonts.RemoveAt(i); - } - } - } - - if (0 < fontPack._fonts.Count) - { - EditorGUILayout.BeginHorizontal(); - try - { - _fontRemovalType = (FontType)EditorGUILayout.EnumFlagsField(_fontRemovalType); - if (GUILayout.Button("Remove Fonts Of Type", _impactButtonStyle)) - { - foreach (FontType fontType in Enum.GetValues(typeof(FontType))) - { - int removed = fontPack._fonts.RemoveAll(font => - Matches(font, fontType) - ); - anyChanged |= removed != 0; - } - } - } - finally - { - EditorGUILayout.EndHorizontal(); - } - } - - EditorGUILayout.BeginHorizontal(); - try - { - _fontAdditionType = (FontType)EditorGUILayout.EnumFlagsField(_fontAdditionType); - Object activeObject = Selection.activeObject; - if (activeObject == null) - { - activeObject = fontPack; - } - - string assetPath = AssetDatabase.GetAssetPath(activeObject); - string dataPath = Application.dataPath; - if ( - dataPath.EndsWith("/", StringComparison.OrdinalIgnoreCase) - || dataPath.EndsWith("\\", StringComparison.OrdinalIgnoreCase) - ) - { - dataPath = dataPath.Substring(0, dataPath.Length - 1); - } - if (dataPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase)) - { - dataPath = dataPath.Substring(0, dataPath.Length - "Assets".Length); - } - - if (!Directory.Exists(Path.Combine(dataPath, assetPath))) - { - assetPath = Path.GetDirectoryName(assetPath) ?? assetPath; - } - - assetPath = assetPath.Replace('\\', '/'); - - GUIContent loadFromCurrentDirectoryContent = new( - "Load From Current Directory", - $"Loads all fonts from '{assetPath}'" - ); - - if (GUILayout.Button(loadFromCurrentDirectoryContent)) - { - UpdateFromDirectory(assetPath); - } - else if (GUILayout.Button("Load From Directory (Select)")) - { - _lastSelectedDirectory = EditorUtility.OpenFolderPanel( - "Select Directory", - string.IsNullOrWhiteSpace(_lastSelectedDirectory) - ? Application.dataPath - : _lastSelectedDirectory, - string.Empty - ); - UpdateFromDirectory(_lastSelectedDirectory); - } - } - finally - { - EditorGUILayout.EndHorizontal(); - } - - if ( - anyChanged - || ( - !fontPack._fonts.IsSorted(UnityObjectNameComparer.Instance) - && GUILayout.Button("Sort Fonts") - ) - ) - { - fontPack._fonts.SortByName(); - EditorUtility.SetDirty(fontPack); - serializedObject.ApplyModifiedProperties(); - } - - return; - - void UpdateFromDirectory(string directory) - { - if (string.IsNullOrWhiteSpace(directory)) - { - return; - } - - DirectoryInfo directoryInfo = new(directory); - if (!directoryInfo.Exists) - { - return; - } - - directory = DirectoryHelper.AbsoluteToUnityRelativePath(directoryInfo.FullName); - - string[] fontGuids = AssetDatabase.FindAssets("t:Font", new[] { directory }); - foreach (string guid in fontGuids) - { - string assetPath = AssetDatabase.GUIDToAssetPath(guid); - - if (string.IsNullOrWhiteSpace(assetPath)) - { - continue; - } - - Font font = AssetDatabase.LoadAssetAtPath(assetPath); - if (Matches(font, _fontAdditionType) && _fontCache.Add(font)) - { - anyChanged = true; - fontPack._fonts.Add(font); - } - } - } - } - - private static bool Matches(Font font, FontType toCheck) - { - if (font == null) - { - return false; - } - - if (toCheck == FontType.None) - { - return false; - } - - foreach (FontType fontType in Enum.GetValues(typeof(FontType))) - { - if ((fontType & toCheck) == 0) - { - continue; - } - - if (font.name.EndsWith(fontType.ToString(), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - } -#endif -} +namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using System.IO; + using DxCommandTerminal.Helper; + using Extensions; + using Themes; + using UnityEditor; + using UnityEngine; + using Object = UnityEngine.Object; + + [CustomEditor(typeof(TerminalFontPack))] + public sealed class TerminalFontPackEditor : Editor + { + [Flags] + private enum FontType + { + None = 0, + Normal = 1 << 0, + Bold = 1 << 1, + Italic = 1 << 2, + BoldItalic = 1 << 3, + ExtraBold = 1 << 4, + ExtraBoldItalic = 1 << 5, + ExtraLight = 1 << 6, + ExtraLightItalic = 1 << 7, + Light = 1 << 8, + LightItalic = 1 << 9, + Medium = 1 << 10, + MediumItalic = 1 << 11, + SemiBold = 1 << 12, + SemiBoldItalic = 1 << 13, + Thin = 1 << 14, + ThinItalic = 1 << 15, + Black = 1 << 16, + BlackItalic = 1 << 17, + Regular = 1 << 18, + Variable = 1 << 19, + Monospace = 1 << 20, + Condensed = 1 << 21, + CondensedBold = 1 << 22, + CondensedExtraBold = 1 << 23, + CondensedExtraLight = 1 << 24, + CondensedLight = 1 << 25, + CondensedMedium = 1 << 26, + CondensedSemiBold = 1 << 27, + CondensedThin = 1 << 28, + VariableFont_wght = 1 << 29, + VariableFont_width = 1 << 30, + } + + private readonly HashSet _fontCache = new(); + private FontType _fontRemovalType = FontType.None; + private FontType _fontAdditionType = FontType.None; + private string _lastSelectedDirectory; + private GUIStyle _impactButtonStyle; + + private void OnEnable() + { + _fontCache.Clear(); + _fontRemovalType = FontType.None; + } + + public override void OnInspectorGUI() + { + _impactButtonStyle ??= new GUIStyle(GUI.skin.button) + { + normal = { textColor = Color.yellow }, + fontStyle = FontStyle.Bold, + }; + + serializedObject.Update(); + TerminalFontPack fontPack = target as TerminalFontPack; + base.OnInspectorGUI(); + + if (fontPack == null) + { + return; + } + + bool anyChanged = false; + if (fontPack._fonts == null) + { + anyChanged = true; + fontPack._fonts = new List(); + } + + _fontCache.Clear(); + bool anyNullFont = false; + foreach (Font font in fontPack._fonts) + { + if (font == null) + { + anyNullFont = true; + } + _fontCache.Add(font); + } + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("Data Manipulation", EditorStyles.boldLabel); + + if (anyNullFont || _fontCache.Count != fontPack._fonts.Count) + { + if (GUILayout.Button("Fix Invalid Fonts", _impactButtonStyle)) + { + _fontCache.Clear(); + for (int i = fontPack._fonts.Count - 1; 0 <= i; --i) + { + Font font = fontPack._fonts[i]; + if (font != null && _fontCache.Add(font)) + { + continue; + } + + anyChanged = true; + fontPack._fonts.RemoveAt(i); + } + } + } + + if (0 < fontPack._fonts.Count) + { + EditorGUILayout.BeginHorizontal(); + try + { + _fontRemovalType = (FontType)EditorGUILayout.EnumFlagsField(_fontRemovalType); + if (GUILayout.Button("Remove Fonts Of Type", _impactButtonStyle)) + { + foreach (FontType fontType in Enum.GetValues(typeof(FontType))) + { + int removed = fontPack._fonts.RemoveAll(font => + Matches(font, fontType) + ); + anyChanged |= removed != 0; + } + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + } + + EditorGUILayout.BeginHorizontal(); + try + { + _fontAdditionType = (FontType)EditorGUILayout.EnumFlagsField(_fontAdditionType); + Object activeObject = Selection.activeObject; + if (activeObject == null) + { + activeObject = fontPack; + } + + string assetPath = AssetDatabase.GetAssetPath(activeObject); + string dataPath = Application.dataPath; + if ( + dataPath.EndsWith("/", StringComparison.OrdinalIgnoreCase) + || dataPath.EndsWith("\\", StringComparison.OrdinalIgnoreCase) + ) + { + dataPath = dataPath.Substring(0, dataPath.Length - 1); + } + if (dataPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase)) + { + dataPath = dataPath.Substring(0, dataPath.Length - "Assets".Length); + } + + if (!Directory.Exists(Path.Combine(dataPath, assetPath))) + { + assetPath = Path.GetDirectoryName(assetPath) ?? assetPath; + } + + assetPath = assetPath.Replace('\\', '/'); + + GUIContent loadFromCurrentDirectoryContent = new( + "Load From Current Directory", + $"Loads all fonts from '{assetPath}'" + ); + + if (GUILayout.Button(loadFromCurrentDirectoryContent)) + { + UpdateFromDirectory(assetPath); + } + else if (GUILayout.Button("Load From Directory (Select)")) + { + _lastSelectedDirectory = EditorUtility.OpenFolderPanel( + "Select Directory", + string.IsNullOrWhiteSpace(_lastSelectedDirectory) + ? Application.dataPath + : _lastSelectedDirectory, + string.Empty + ); + UpdateFromDirectory(_lastSelectedDirectory); + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + + if ( + anyChanged + || ( + !fontPack._fonts.IsSorted(UnityObjectNameComparer.Instance) + && GUILayout.Button("Sort Fonts") + ) + ) + { + fontPack._fonts.SortByName(); + EditorUtility.SetDirty(fontPack); + serializedObject.ApplyModifiedProperties(); + } + + return; + + void UpdateFromDirectory(string directory) + { + if (string.IsNullOrWhiteSpace(directory)) + { + return; + } + + DirectoryInfo directoryInfo = new(directory); + if (!directoryInfo.Exists) + { + return; + } + + directory = DirectoryHelper.AbsoluteToUnityRelativePath(directoryInfo.FullName); + + string[] fontGuids = AssetDatabase.FindAssets("t:Font", new[] { directory }); + foreach (string guid in fontGuids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + + if (string.IsNullOrWhiteSpace(assetPath)) + { + continue; + } + + Font font = AssetDatabase.LoadAssetAtPath(assetPath); + if (Matches(font, _fontAdditionType) && _fontCache.Add(font)) + { + anyChanged = true; + fontPack._fonts.Add(font); + } + } + } + } + + private static bool Matches(Font font, FontType toCheck) + { + if (font == null) + { + return false; + } + + if (toCheck == FontType.None) + { + return false; + } + + foreach (FontType fontType in Enum.GetValues(typeof(FontType))) + { + if ((fontType & toCheck) == 0) + { + continue; + } + + if (font.name.EndsWith(fontType.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + } +#endif +} diff --git a/Editor/CustomEditors/TerminalThemePackEditor.cs b/Editor/CustomEditors/TerminalThemePackEditor.cs index 0a72655..b5af8a7 100644 --- a/Editor/CustomEditors/TerminalThemePackEditor.cs +++ b/Editor/CustomEditors/TerminalThemePackEditor.cs @@ -1,209 +1,209 @@ -namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors -{ -#if UNITY_EDITOR - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using DxCommandTerminal.Helper; - using Extensions; - using Helper; - using Themes; - using UnityEditor; - using UnityEngine; - using UnityEngine.UIElements; - using Object = UnityEngine.Object; - - [CustomEditor(typeof(TerminalThemePack))] - public sealed class TerminalThemePackEditor : Editor - { - private readonly HashSet _styleCache = new(); - private readonly HashSet _invalidStyles = new(); - private string _lastSelectedDirectory; - private GUIStyle _impactButtonStyle; - - private void OnEnable() - { - _styleCache.Clear(); - _invalidStyles.Clear(); - } - - public override void OnInspectorGUI() - { - _impactButtonStyle ??= new GUIStyle(GUI.skin.button) - { - normal = { textColor = Color.yellow }, - fontStyle = FontStyle.Bold, - }; - - serializedObject.Update(); - TerminalThemePack themePack = target as TerminalThemePack; - DrawPropertiesExcluding( - serializedObject, - "m_Script", - nameof(TerminalThemePack._themeNames) - ); - - if (themePack == null) - { - return; - } - - bool anyChanged = false; - if (themePack._themes == null) - { - anyChanged = true; - themePack._themes = new List(); - } - - _styleCache.Clear(); - _invalidStyles.Clear(); - bool anyInvalidTheme = false; - foreach (StyleSheet theme in themePack._themes) - { - if (theme == null) - { - anyInvalidTheme = true; - continue; - } - _styleCache.Add(theme); - if (!TerminalThemeStyleSheetHelper.GetAvailableThemes(theme).Any()) - { - _invalidStyles.Add(theme); - } - } - - EditorGUILayout.Space(10); - EditorGUILayout.LabelField("Data Manipulation", EditorStyles.boldLabel); - - if ( - anyInvalidTheme - || _styleCache.Count != themePack._themes.Count - || _invalidStyles.Any() - ) - { - if (GUILayout.Button("Fix Invalid Themes", _impactButtonStyle)) - { - anyChanged = true; - _styleCache.Clear(); - themePack._themes.RemoveAll(theme => theme == null || !_styleCache.Add(theme)); - } - } - - Object activeObject = Selection.activeObject; - if (activeObject == null) - { - activeObject = themePack; - } - - string assetPath = AssetDatabase.GetAssetPath(activeObject); - string dataPath = Application.dataPath; - if ( - dataPath.EndsWith("/", StringComparison.OrdinalIgnoreCase) - || dataPath.EndsWith("\\", StringComparison.OrdinalIgnoreCase) - ) - { - dataPath = dataPath.Substring(0, dataPath.Length - 1); - } - if (dataPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase)) - { - dataPath = dataPath.Substring(0, dataPath.Length - "Assets".Length); - } - - if (!Directory.Exists(Path.Combine(dataPath, assetPath))) - { - assetPath = Path.GetDirectoryName(assetPath) ?? assetPath; - } - - assetPath = assetPath.Replace('\\', '/'); - - GUIContent loadFromCurrentDirectoryContent = new( - "Load From Current Directory", - $"Loads all themes from '{assetPath}'" - ); - - if (GUILayout.Button(loadFromCurrentDirectoryContent)) - { - UpdateFromDirectory(assetPath); - } - else if (GUILayout.Button("Load From Directory (Select)")) - { - _lastSelectedDirectory = EditorUtility.OpenFolderPanel( - "Select Directory", - string.IsNullOrWhiteSpace(_lastSelectedDirectory) - ? Application.dataPath - : _lastSelectedDirectory, - string.Empty - ); - UpdateFromDirectory(_lastSelectedDirectory); - } - - if ( - anyChanged - || ( - !themePack._themes.IsSorted(UnityObjectNameComparer.Instance) - && GUILayout.Button("Sort Themes") - ) - || (themePack._themes.Count != themePack._themeNames.Count) - ) - { - SortThemes(); - EditorUtility.SetDirty(themePack); - serializedObject.ApplyModifiedProperties(); - } - - return; - - void SortThemes() - { - themePack._themes.SortByName(); - themePack._themeNames ??= new List(); - themePack._themeNames.Clear(); - themePack._themeNames.AddRange( - themePack._themes.SelectMany(TerminalThemeStyleSheetHelper.GetAvailableThemes) - ); - } - - void UpdateFromDirectory(string directory) - { - if (string.IsNullOrWhiteSpace(directory)) - { - return; - } - - DirectoryInfo directoryInfo = new(directory); - if (!directoryInfo.Exists) - { - return; - } - - directory = DirectoryHelper.AbsoluteToUnityRelativePath(directoryInfo.FullName); - - string[] fontGuids = AssetDatabase.FindAssets("t:StyleSheet", new[] { directory }); - foreach (string guid in fontGuids) - { - string styleAssetPath = AssetDatabase.GUIDToAssetPath(guid); - - if (string.IsNullOrWhiteSpace(styleAssetPath)) - { - continue; - } - - StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath( - styleAssetPath - ); - if ( - styleSheet != null - && TerminalThemeStyleSheetHelper.GetAvailableThemes(styleSheet).Any() - && _styleCache.Add(styleSheet) - ) - { - anyChanged = true; - themePack._themes.Add(styleSheet); - } - } - } - } - } -#endif -} +namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using DxCommandTerminal.Helper; + using Extensions; + using Helper; + using Themes; + using UnityEditor; + using UnityEngine; + using UnityEngine.UIElements; + using Object = UnityEngine.Object; + + [CustomEditor(typeof(TerminalThemePack))] + public sealed class TerminalThemePackEditor : Editor + { + private readonly HashSet _styleCache = new(); + private readonly HashSet _invalidStyles = new(); + private string _lastSelectedDirectory; + private GUIStyle _impactButtonStyle; + + private void OnEnable() + { + _styleCache.Clear(); + _invalidStyles.Clear(); + } + + public override void OnInspectorGUI() + { + _impactButtonStyle ??= new GUIStyle(GUI.skin.button) + { + normal = { textColor = Color.yellow }, + fontStyle = FontStyle.Bold, + }; + + serializedObject.Update(); + TerminalThemePack themePack = target as TerminalThemePack; + DrawPropertiesExcluding( + serializedObject, + "m_Script", + nameof(TerminalThemePack._themeNames) + ); + + if (themePack == null) + { + return; + } + + bool anyChanged = false; + if (themePack._themes == null) + { + anyChanged = true; + themePack._themes = new List(); + } + + _styleCache.Clear(); + _invalidStyles.Clear(); + bool anyInvalidTheme = false; + foreach (StyleSheet theme in themePack._themes) + { + if (theme == null) + { + anyInvalidTheme = true; + continue; + } + _styleCache.Add(theme); + if (!TerminalThemeStyleSheetHelper.GetAvailableThemes(theme).Any()) + { + _invalidStyles.Add(theme); + } + } + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("Data Manipulation", EditorStyles.boldLabel); + + if ( + anyInvalidTheme + || _styleCache.Count != themePack._themes.Count + || _invalidStyles.Any() + ) + { + if (GUILayout.Button("Fix Invalid Themes", _impactButtonStyle)) + { + anyChanged = true; + _styleCache.Clear(); + themePack._themes.RemoveAll(theme => theme == null || !_styleCache.Add(theme)); + } + } + + Object activeObject = Selection.activeObject; + if (activeObject == null) + { + activeObject = themePack; + } + + string assetPath = AssetDatabase.GetAssetPath(activeObject); + string dataPath = Application.dataPath; + if ( + dataPath.EndsWith("/", StringComparison.OrdinalIgnoreCase) + || dataPath.EndsWith("\\", StringComparison.OrdinalIgnoreCase) + ) + { + dataPath = dataPath.Substring(0, dataPath.Length - 1); + } + if (dataPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase)) + { + dataPath = dataPath.Substring(0, dataPath.Length - "Assets".Length); + } + + if (!Directory.Exists(Path.Combine(dataPath, assetPath))) + { + assetPath = Path.GetDirectoryName(assetPath) ?? assetPath; + } + + assetPath = assetPath.Replace('\\', '/'); + + GUIContent loadFromCurrentDirectoryContent = new( + "Load From Current Directory", + $"Loads all themes from '{assetPath}'" + ); + + if (GUILayout.Button(loadFromCurrentDirectoryContent)) + { + UpdateFromDirectory(assetPath); + } + else if (GUILayout.Button("Load From Directory (Select)")) + { + _lastSelectedDirectory = EditorUtility.OpenFolderPanel( + "Select Directory", + string.IsNullOrWhiteSpace(_lastSelectedDirectory) + ? Application.dataPath + : _lastSelectedDirectory, + string.Empty + ); + UpdateFromDirectory(_lastSelectedDirectory); + } + + if ( + anyChanged + || ( + !themePack._themes.IsSorted(UnityObjectNameComparer.Instance) + && GUILayout.Button("Sort Themes") + ) + || (themePack._themes.Count != themePack._themeNames.Count) + ) + { + SortThemes(); + EditorUtility.SetDirty(themePack); + serializedObject.ApplyModifiedProperties(); + } + + return; + + void SortThemes() + { + themePack._themes.SortByName(); + themePack._themeNames ??= new List(); + themePack._themeNames.Clear(); + themePack._themeNames.AddRange( + themePack._themes.SelectMany(TerminalThemeStyleSheetHelper.GetAvailableThemes) + ); + } + + void UpdateFromDirectory(string directory) + { + if (string.IsNullOrWhiteSpace(directory)) + { + return; + } + + DirectoryInfo directoryInfo = new(directory); + if (!directoryInfo.Exists) + { + return; + } + + directory = DirectoryHelper.AbsoluteToUnityRelativePath(directoryInfo.FullName); + + string[] fontGuids = AssetDatabase.FindAssets("t:StyleSheet", new[] { directory }); + foreach (string guid in fontGuids) + { + string styleAssetPath = AssetDatabase.GUIDToAssetPath(guid); + + if (string.IsNullOrWhiteSpace(styleAssetPath)) + { + continue; + } + + StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath( + styleAssetPath + ); + if ( + styleSheet != null + && TerminalThemeStyleSheetHelper.GetAvailableThemes(styleSheet).Any() + && _styleCache.Add(styleSheet) + ) + { + anyChanged = true; + themePack._themes.Add(styleSheet); + } + } + } + } + } +#endif +} diff --git a/Editor/CustomEditors/TerminalUIEditor.cs b/Editor/CustomEditors/TerminalUIEditor.cs index 07f3e24..f0e49cc 100644 --- a/Editor/CustomEditors/TerminalUIEditor.cs +++ b/Editor/CustomEditors/TerminalUIEditor.cs @@ -1,1359 +1,1359 @@ -namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors -{ -#if UNITY_EDITOR - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.IO; - using System.Linq; - using Backend; - using DxCommandTerminal.Helper; - using UnityEditor; - using UnityEngine; - using UnityEngine.UIElements; - using Input; - using Persistence; - using Themes; - using UI; - using Object = UnityEngine.Object; -#if ENABLE_INPUT_SYSTEM - using UnityEngine.InputSystem; -#endif - - [InitializeOnLoad] - [CustomEditor(typeof(TerminalUI))] - public sealed class TerminalUIEditor : Editor - { - private static readonly TimeSpan CycleInterval = TimeSpan.FromSeconds(0.75); - - private int _commandIndex; - private TerminalUI _lastSeen; - - private readonly HashSet _allCommands = new(StringComparer.OrdinalIgnoreCase); - private readonly HashSet _defaultCommands = new(StringComparer.OrdinalIgnoreCase); - private readonly HashSet _nonDefaultCommands = new( - StringComparer.OrdinalIgnoreCase - ); - private readonly HashSet _seenCommands = new(StringComparer.OrdinalIgnoreCase); - private readonly SortedSet _intermediateResults = new( - StringComparer.OrdinalIgnoreCase - ); - private readonly SortedDictionary> _fontsByPrefix = - new(StringComparer.OrdinalIgnoreCase); - - private readonly Dictionary _seenLogTypes = new(); - private readonly List _fontPacks = new(); - private readonly List _themePacks = new(); - - private int _themeIndex = -1; - private int _fontKey = -1; - private int _secondFontKey = -1; - private int _themePackIndex = -1; - private int _fontPackIndex = -1; - private bool _isCyclingThemes; - private bool _isCyclingFonts; - private bool _persistThemeChanges; - - private TimeSpan? _lastFontCycleTime; - private TimeSpan? _lastThemeCycleTime; - - private readonly Stopwatch _timer = Stopwatch.StartNew(); - - private bool _editorUpdateAttached; - private GUIStyle _impactButtonStyle; - private GUIStyle _impactLabelStyle; - - static TerminalUIEditor() - { - ObjectFactory.componentWasAdded += HandleComponentAdded; - } - - private static void HandleComponentAdded(Component addedComponent) - { - bool anyChange = false; - if (addedComponent is TerminalUI terminal && terminal != null) - { - CheckForUIDocumentProblems(terminal); - - if (terminal._themePack == null) - { - TerminalThemePack[] themePacks = LoadAll(); - int themePackIndex = Array.FindIndex( - themePacks, - themePack => - string.Equals( - themePack.name, - "Minimal", - StringComparison.OrdinalIgnoreCase - ) - ); - if (themePackIndex < 0) - { - themePackIndex = themePacks.Length - 1; - } - - if (0 <= themePackIndex && themePackIndex < themePacks.Length) - { - TerminalThemePack themePack = themePacks[themePackIndex]; - terminal._themePack = themePack; - _ = TrySetupDefaultTheme(terminal); - } - } - - if (terminal._fontPack == null) - { - TerminalFontPack[] fontPacks = LoadAll(); - int fontPackIndex = Array.FindIndex( - fontPacks, - fontPack => - string.Equals( - fontPack.name, - "Minimal", - StringComparison.OrdinalIgnoreCase - ) - ); - if (fontPackIndex < 0) - { - fontPackIndex = fontPacks.Length - 1; - } - - if (0 <= fontPackIndex && fontPackIndex < fontPacks.Length) - { - TerminalFontPack fontPack = fontPacks[fontPackIndex]; - terminal._fontPack = fontPack; - _ = TrySetupDefaultFont(terminal); - } - } - - terminal.gameObject.AddComponent(); - terminal.gameObject.AddComponent(); - - EditorUtility.SetDirty(terminal); - EditorUtility.SetDirty(terminal.gameObject); - anyChange = true; - } - else - { - switch (addedComponent) - { - case TerminalKeyboardController keyboardController - when keyboardController != null: - { - if (keyboardController.TryGetComponent(out terminal)) - { - keyboardController.terminal = terminal; - EditorUtility.SetDirty(keyboardController); - anyChange = true; - } - - break; - } - case TerminalThemePersister themePersister when themePersister != null: - { - if (themePersister.TryGetComponent(out terminal)) - { - themePersister.terminal = terminal; - EditorUtility.SetDirty(themePersister); - anyChange = true; - } - - break; - } -#if ENABLE_INPUT_SYSTEM - case PlayerInput playerInput when playerInput != null: - { - if (playerInput.TryGetComponent(out terminal)) - { - if ( - !playerInput.TryGetComponent( - out TerminalPlayerInputController playerInputController - ) - ) - { - playerInputController = - playerInput.gameObject.AddComponent(); - playerInputController.terminal = terminal; - EditorUtility.SetDirty(playerInputController); - EditorUtility.SetDirty(playerInput.gameObject); - anyChange = true; - } - - if ( - playerInput.TryGetComponent( - out TerminalKeyboardController keyboardController - ) - ) - { - keyboardController.enabled = false; - EditorUtility.SetDirty(keyboardController); - anyChange = true; - } - } - - break; - } -#endif - } - } - - if (anyChange) - { - AssetDatabase.SaveAssets(); - } - } - - private void OnEnable() - { - _allCommands.Clear(); - _allCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Select(attribute => attribute.Name) - ); - _defaultCommands.Clear(); - _defaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => tuple.Default) - .Select(attribute => attribute.Name) - ); - _nonDefaultCommands.Clear(); - _nonDefaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => !tuple.Default) - .Select(attribute => attribute.Name) - ); - _fontsByPrefix.Clear(); - - ResetStateIdempotent(force: true); - - if (!_editorUpdateAttached) - { - EditorApplication.update += EditorUpdate; - _editorUpdateAttached = true; - } - } - - private static T[] LoadAll() - where T : Object - { - List directories = new(); - - string directory = DirectoryHelper.GetCallerScriptDirectory(); - if (!string.IsNullOrWhiteSpace(directory)) - { - DirectoryInfo directoryInfo = new(directory); - if (directoryInfo.Exists) - { - directory = DirectoryHelper.FindPackageRootPath(directory); - directory = DirectoryHelper.AbsoluteToUnityRelativePath(directory); - directories.Add(directory); - } - } - - if (Directory.Exists(Path.Combine(Application.dataPath, "Packages"))) - { - directories.Add("Packages"); - } - - if (Directory.Exists(Path.Combine(Application.dataPath, "Library"))) - { - directories.Add("Library"); - } - - directories.Add("Assets"); - - HashSet unique = new(); - List ordered = new(); - string[] assetGuids = AssetDatabase.FindAssets( - $"t:{typeof(T).Name}", - directories.ToArray() - ); - foreach (string guid in assetGuids) - { - string assetPath = AssetDatabase.GUIDToAssetPath(guid); - - if (string.IsNullOrWhiteSpace(assetPath)) - { - continue; - } - - T item = AssetDatabase.LoadAssetAtPath(assetPath); - if (item != null && unique.Add(item)) - { - ordered.Add(item); - } - } - return ordered.ToArray(); - } - - private void OnDisable() - { - if (_editorUpdateAttached) - { - EditorApplication.update -= EditorUpdate; - _editorUpdateAttached = false; - } - } - - private void ResetStateIdempotent(bool force) - { - foreach (TerminalThemePack themePack in TerminalAssetPackPostProcessor.NewThemePacks) - { - _themePacks.Add(themePack); - } - TerminalAssetPackPostProcessor.NewThemePacks.Clear(); - foreach (TerminalFontPack fontPack in TerminalAssetPackPostProcessor.NewFontPacks) - { - _fontPacks.Add(fontPack); - } - TerminalAssetPackPostProcessor.NewFontPacks.Clear(); - - if (!_fontPacks.Any()) - { - _fontPacks.Clear(); - _fontPacks.AddRange(LoadAll()); - } - - if (!_themePacks.Any()) - { - _themePacks.Clear(); - _themePacks.AddRange(LoadAll()); - } - - TerminalUI terminal = target as TerminalUI; - if (!force && _lastSeen == terminal) - { - return; - } - - _fontsByPrefix.Clear(); - CollectFonts(terminal, _fontsByPrefix); - - _persistThemeChanges = false; - StopCyclingFonts(); - StopCyclingThemes(); - _themeIndex = -1; - _fontKey = -1; - _secondFontKey = -1; - _lastSeen = terminal; - } - - private void EditorUpdate() - { - if (_isCyclingThemes) - { - if (_timer.Elapsed < _lastThemeCycleTime + CycleInterval) - { - return; - } - - TerminalUI terminal = target as TerminalUI; - if (terminal != null && terminal._themePack._themeNames is { Count: > 0 }) - { - int newThemeIndex = (_themeIndex + 1) % terminal._themePack._themeNames.Count; - newThemeIndex = - (newThemeIndex + terminal._themePack._themeNames.Count) - % terminal._themePack._themeNames.Count; - terminal.SetTheme( - terminal._themePack._themeNames[newThemeIndex], - persist: _persistThemeChanges - ); - _themeIndex = newThemeIndex; - } - - _lastThemeCycleTime = _timer.Elapsed; - } - - if (_isCyclingFonts) - { - if (_timer.Elapsed < _lastFontCycleTime + CycleInterval) - { - return; - } - - TerminalUI terminal = target as TerminalUI; - if (terminal != null && terminal._fontPack._fonts is { Count: > 0 }) - { - Font currentFont = GetCurrentlySelectedFont(terminal); - int fontIndex = terminal._fontPack._fonts.IndexOf(currentFont); - int newFontIndex = (fontIndex + 1) % terminal._fontPack._fonts.Count; - Font newFont = terminal._fontPack._fonts[newFontIndex]; - terminal.SetFont(newFont, persist: _persistThemeChanges); - TrySetFontKeysFromFont(newFont); - } - - _lastFontCycleTime = _timer.Elapsed; - } - } - - private Font GetCurrentlySelectedFont(TerminalUI terminal) - { - if (_fontKey < 0 || _secondFontKey < 0) - { - return terminal.CurrentFont; - } - - try - { - return _fontsByPrefix.ToArray()[_fontKey].Value.ToArray()[_secondFontKey].Value; - } - catch - { - return terminal.CurrentFont; - } - } - - public override void OnInspectorGUI() - { - _impactButtonStyle ??= new GUIStyle(GUI.skin.button) - { - normal = { textColor = Color.yellow }, - fontStyle = FontStyle.Bold, - }; - _impactLabelStyle ??= new GUIStyle(GUI.skin.label) - { - normal = { textColor = new Color(1f, 0.3f, 0.3f, 1f) }, - fontStyle = FontStyle.Bold, - }; - - if (_allCommands.Count == 0 || _defaultCommands.Count == 0) - { - HydrateCommandCaches(); - } - - TerminalUI terminal = target as TerminalUI; - if (terminal == null) - { - return; - } - - serializedObject.Update(); - ResetStateIdempotent(force: false); - - bool anyChanged = false; - - bool uiDocumentChanged = CheckForUIDocumentProblems(terminal); - anyChanged |= uiDocumentChanged; - - bool themesChanged = CheckForThemingAndFontChanges(terminal); - anyChanged |= themesChanged; - - RenderCyclingPreviews(); - - DrawPropertiesExcluding( - serializedObject, - "m_Script", - nameof(TerminalUI._themePack), - nameof(TerminalUI._fontPack), - nameof(TerminalUI._persistedTheme), - nameof(TerminalUI._uiDocument) - ); - - bool propertiesDirty = CheckForSimpleProperties(terminal); - anyChanged |= propertiesDirty; - - RenderCommandManipulationHeader(); - - bool ignoredCommandsUpdated = CheckForIgnoredCommandUpdates(terminal); - anyChanged |= ignoredCommandsUpdated; - - bool commandsUpdated = CheckForDisabledCommandProblems(terminal); - anyChanged |= commandsUpdated; - - serializedObject.ApplyModifiedProperties(); - if (anyChanged) - { - EditorUtility.SetDirty(terminal); - } - } - - private void HydrateCommandCaches() - { - _allCommands.Clear(); - _allCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Select(attribute => attribute.Name) - ); - _defaultCommands.Clear(); - _defaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => tuple.Default) - .Select(attribute => attribute.Name) - ); - _nonDefaultCommands.Clear(); - _nonDefaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => !tuple.Default) - .Select(attribute => attribute.Name) - ); - } - - private void RenderCyclingPreviews() - { - EditorGUILayout.Space(10); - EditorGUILayout.BeginHorizontal(); - try - { - EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel); - bool oldPersistThemeChanges = _persistThemeChanges; - _persistThemeChanges = GUILayout.Toggle( - _persistThemeChanges, - "Persist Theme Changes" - ); - - if (oldPersistThemeChanges != _persistThemeChanges) - { - StopCyclingFonts(); - StopCyclingThemes(); - } - } - finally - { - EditorGUILayout.EndHorizontal(); - } - - if (Application.isPlaying) - { - EditorGUILayout.BeginHorizontal(); - try - { - TryCyclingThemes(); - TryCyclingFonts(); - } - finally - { - EditorGUILayout.EndHorizontal(); - } - } - - EditorGUILayout.BeginHorizontal(); - try - { - TrySetRandomTheme(); - TrySetRandomFont(); - } - finally - { - EditorGUILayout.EndHorizontal(); - } - - EditorGUILayout.Space(); - } - - private void TrySetRandomTheme() - { - if (_isCyclingThemes) - { - return; - } - - bool clicked = _persistThemeChanges - ? GUILayout.Button("Set Random Theme", _impactButtonStyle) - : GUILayout.Button("Set Random Theme "); - - if (clicked) - { - TerminalUI terminal = target as TerminalUI; - if ( - terminal != null - && terminal._themePack != null - && terminal._themePack._themeNames is { Count: > 0 } - ) - { - int newThemeIndex; - do - { - newThemeIndex = ThreadLocalRandom.Instance.Next( - terminal._themePack._themeNames.Count - ); - } while ( - newThemeIndex == _themeIndex && terminal._themePack._themeNames.Count != 1 - ); - terminal.SetTheme( - terminal._themePack._themeNames[newThemeIndex], - persist: _persistThemeChanges - ); - _themeIndex = newThemeIndex; - } - } - } - - private void TrySetRandomFont() - { - if (_isCyclingFonts) - { - return; - } - - bool clicked = _persistThemeChanges - ? GUILayout.Button("Set Random Font", _impactButtonStyle) - : GUILayout.Button("Set Random Font "); - - if (clicked) - { - TerminalUI terminal = target as TerminalUI; - if ( - terminal != null - && terminal._fontPack != null - && terminal._fontPack._fonts is { Count: > 0 } - ) - { - Font currentlySelectedFont = GetCurrentlySelectedFont(terminal); - int oldFontIndex = terminal._fontPack._fonts.IndexOf(currentlySelectedFont); - int newFontIndex; - do - { - newFontIndex = ThreadLocalRandom.Instance.Next( - terminal._fontPack._fonts.Count - ); - } while (newFontIndex == oldFontIndex && terminal._fontPack._fonts.Count != 1); - - Font newFont = terminal._fontPack._fonts[newFontIndex]; - terminal.SetFont(newFont, persist: _persistThemeChanges); - TrySetFontKeysFromFont(newFont); - } - } - } - - private void TryCyclingFonts() - { - if (_isCyclingFonts) - { - if (GUILayout.Button("Stop Cycling Fonts")) - { - StopCyclingFonts(); - } - } - else - { - bool clicked = _persistThemeChanges - ? GUILayout.Button("Start Cycling Fonts", _impactButtonStyle) - : GUILayout.Button("Start Cycling Fonts"); - if (clicked) - { - StartCyclingFonts(); - } - } - } - - private void StartCyclingFonts() - { - if (_isCyclingFonts) - { - return; - } - _isCyclingFonts = true; - _lastFontCycleTime = null; - } - - private void StopCyclingFonts() - { - _isCyclingFonts = false; - } - - private void TryCyclingThemes() - { - if (_isCyclingThemes) - { - if (GUILayout.Button("Stop Cycling Themes")) - { - StopCyclingThemes(); - } - } - else - { - bool clicked = _persistThemeChanges - ? GUILayout.Button("Start Cycling Themes", _impactButtonStyle) - : GUILayout.Button("Start Cycling Themes"); - if (clicked) - { - StartCyclingThemes(); - } - } - } - - private void StartCyclingThemes() - { - if (_isCyclingThemes) - { - return; - } - - _isCyclingThemes = true; - _lastThemeCycleTime = null; - } - - private void StopCyclingThemes() - { - _isCyclingThemes = false; - } - - private void TrySetFontKeysFromFont(Font font) - { - int firstIndex = 0; - bool foundFont = false; - foreach ( - KeyValuePair> fontKeyEntry in _fontsByPrefix - ) - { - int secondIndex = 0; - foreach (KeyValuePair secondFontKeyEntry in fontKeyEntry.Value) - { - if (secondFontKeyEntry.Value == font) - { - _fontKey = firstIndex; - _secondFontKey = secondIndex; - foundFont = true; - break; - } - - ++secondIndex; - } - - if (foundFont) - { - break; - } - - ++firstIndex; - } - } - - private bool CheckForThemingAndFontChanges(TerminalUI terminal) - { - bool anyChanged = false; - EditorGUILayout.Space(10); - EditorGUILayout.LabelField("Pack Selection", EditorStyles.boldLabel); - - EditorGUILayout.BeginHorizontal(); - try - { - if (!_themePacks.Any()) - { - GUILayout.Label("NO THEME PACKS", _impactLabelStyle); - } - else - { - if (_themePackIndex < 0) - { - _themePackIndex = _themePacks.IndexOf(terminal._themePack); - } - - if (_themePackIndex < 0) - { - GUILayout.Label("Select Theme Pack:"); - } - - _themePackIndex = EditorGUILayout.Popup( - _themePackIndex, - _themePacks.Select(themePack => themePack.name).ToArray() - ); - if (0 <= _themePackIndex && _themePackIndex < _themePacks.Count) - { - TerminalThemePack themePack = _themePacks[_themePackIndex]; - bool clicked = - themePack != terminal._themePack - ? GUILayout.Button("Set Theme Pack", _impactButtonStyle) - : GUILayout.Button("Set Theme Pack"); - if (clicked) - { - if (themePack != terminal._themePack) - { - terminal._themePack = themePack; - _themeIndex = themePack._themeNames.IndexOf(terminal.CurrentTheme); - if (_themeIndex < 0) - { - terminal._persistedTheme = string.Empty; - } - - anyChanged = true; - } - } - } - } - } - finally - { - EditorGUILayout.EndHorizontal(); - } - - EditorGUILayout.BeginHorizontal(); - try - { - if (!_fontPacks.Any()) - { - GUILayout.Label("NO FONT PACKS", _impactLabelStyle); - } - else - { - if (_fontPackIndex < 0) - { - _fontPackIndex = _fontPacks.IndexOf(terminal._fontPack); - } - - if (_fontPackIndex < 0) - { - GUILayout.Label("Select Font Pack:"); - } - - _fontPackIndex = EditorGUILayout.Popup( - _fontPackIndex, - _fontPacks.Select(fontPack => fontPack.name).ToArray() - ); - if (0 <= _fontPackIndex && _fontPackIndex < _fontPacks.Count) - { - TerminalFontPack fontPack = _fontPacks[_fontPackIndex]; - bool clicked = - fontPack != terminal._fontPack - ? GUILayout.Button("Set Font Pack", _impactButtonStyle) - : GUILayout.Button("Set Font Pack"); - if (clicked) - { - if (fontPack != terminal._fontPack) - { - _fontsByPrefix.Clear(); - _fontKey = -1; - _secondFontKey = -1; - terminal._fontPack = fontPack; - if (!terminal._fontPack._fonts.Contains(terminal.CurrentFont)) - { - terminal._persistedFont = null; - } - - anyChanged = true; - } - } - } - } - } - finally - { - EditorGUILayout.EndHorizontal(); - } - - EditorGUILayout.Space(10); - EditorGUILayout.LabelField("Theming", EditorStyles.boldLabel); - - EditorGUILayout.BeginHorizontal(); - try - { - if (terminal._themePack != null) - { - if (_themeIndex < 0) - { - _themeIndex = terminal._themePack._themeNames.IndexOf( - terminal.CurrentTheme - ); - } - - if (_themeIndex < 0) - { - GUILayout.Label("Select Theme:"); - } - - _themeIndex = EditorGUILayout.Popup( - _themeIndex, - terminal - ._themePack._themeNames.Select(theme => - theme - .Replace( - "-theme", - string.Empty, - StringComparison.OrdinalIgnoreCase - ) - .Replace( - "theme-", - string.Empty, - StringComparison.OrdinalIgnoreCase - ) - ) - .ToArray() - ); - - if (0 <= _themeIndex && _themeIndex < terminal._themePack._themeNames.Count) - { - string selectedTheme = terminal._themePack._themeNames[_themeIndex]; - GUIContent setThemeContent = new( - "Set Theme", - $"Will set the current theme to {selectedTheme}" - ); - bool clicked = !string.Equals( - selectedTheme, - terminal._persistedTheme, - StringComparison.OrdinalIgnoreCase - ) - ? GUILayout.Button(setThemeContent, _impactButtonStyle) - : GUILayout.Button(setThemeContent); - if (clicked) - { - terminal.SetTheme(selectedTheme, persist: true); - anyChanged = true; - } - } - } - } - finally - { - EditorGUILayout.EndHorizontal(); - } - - CollectFonts(terminal, _fontsByPrefix); - bool fontsUpdated = RenderSelectableFonts(terminal); - return anyChanged || fontsUpdated; - } - - private bool CheckForSimpleProperties(TerminalUI terminal) - { - bool anyChanged = false; - - if (terminal._ignoredLogTypes == null) - { - terminal._ignoredLogTypes = new List(); - anyChanged = true; - } - - if (terminal._disabledCommands == null) - { - terminal._disabledCommands = new List(); - anyChanged = true; - } - - _seenLogTypes.Clear(); - for (int i = terminal._ignoredLogTypes.Count - 1; 0 <= i; --i) - { - TerminalLogType logType = terminal._ignoredLogTypes[i]; - int count = 0; - if ( - Enum.IsDefined(typeof(TerminalLogType), logType) - && (!_seenLogTypes.TryGetValue(logType, out count) || count <= 1) - ) - { - _seenLogTypes[logType] = count + 1; - continue; - } - - _seenLogTypes[logType] = count + 1; - anyChanged = true; - terminal._ignoredLogTypes.RemoveAt(i); - } - - return anyChanged; - } - - private static bool CheckForUIDocumentProblems(TerminalUI terminal) - { - bool anyChanged = false; - if (terminal._uiDocument == null) - { - terminal._uiDocument = terminal.TryGetComponent(out UIDocument uiDocument) - ? uiDocument - : terminal.gameObject.AddComponent(); - anyChanged = true; - } - - if (terminal._uiDocument.panelSettings != null) - { - return anyChanged; - } - - string[] panelSettingGuids; - string absoluteStylesPath = DirectoryHelper.FindAbsolutePathToDirectory("Styles"); - if (!string.IsNullOrWhiteSpace(absoluteStylesPath)) - { - panelSettingGuids = AssetDatabase.FindAssets( - "t:PanelSettings", - new[] { absoluteStylesPath } - ); - TryFindTerminalSettings(); - if (terminal._uiDocument.panelSettings != null) - { - return true; - } - } - - List directories = new(); - if (Directory.Exists(Path.Combine(Application.dataPath, "Library"))) - { - directories.Add("Library"); - } - - if (Directory.Exists(Path.Combine(Application.dataPath, "Packages"))) - { - directories.Add("Packages"); - } - - directories.Add("Assets"); - panelSettingGuids = AssetDatabase.FindAssets("t:PanelSettings", directories.ToArray()); - TryFindTerminalSettings(); - return anyChanged; - - void TryFindTerminalSettings() - { - foreach (string guid in panelSettingGuids) - { - string assetPath = AssetDatabase.GUIDToAssetPath(guid); - if (string.IsNullOrWhiteSpace(assetPath)) - { - continue; - } - - if (!assetPath.Contains("TerminalSettings", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - PanelSettings panelSettings = AssetDatabase.LoadAssetAtPath( - assetPath - ); - - if (panelSettings == null) - { - continue; - } - - terminal._uiDocument.panelSettings = panelSettings; - anyChanged = true; - return; - } - } - } - - private static void RenderCommandManipulationHeader() - { - EditorGUILayout.Space(); - EditorGUILayout.Space(10); - EditorGUILayout.LabelField("Command Manipulation", EditorStyles.boldLabel); - } - - private bool CheckForIgnoredCommandUpdates(TerminalUI terminal) - { - bool anyChanged = false; - _intermediateResults.Clear(); - _intermediateResults.UnionWith(_nonDefaultCommands); - if (!terminal.ignoreDefaultCommands) - { - _intermediateResults.UnionWith(_defaultCommands); - } - _intermediateResults.ExceptWith(terminal._disabledCommands); - - if (0 < _intermediateResults.Count) - { - string[] ignorableCommands = _intermediateResults.ToArray(); - - EditorGUILayout.BeginHorizontal(); - try - { - _commandIndex = EditorGUILayout.Popup(_commandIndex, ignorableCommands); - - if (0 <= _commandIndex && _commandIndex < ignorableCommands.Length) - { - GUIContent ignoreContent = new( - "Ignore Command", - $"Ignores the {ignorableCommands[_commandIndex]} command" - ); - if (GUILayout.Button(ignoreContent)) - { - string command = ignorableCommands[_commandIndex]; - terminal._disabledCommands.Add(command); - anyChanged = true; - } - } - } - finally - { - EditorGUILayout.EndHorizontal(); - } - } - - return anyChanged; - } - - private bool CheckForDisabledCommandProblems(TerminalUI terminal) - { - bool anyChanged = false; - _seenCommands.Clear(); - _seenCommands.UnionWith(terminal._disabledCommands); - - if ( - _seenCommands.Count != terminal._disabledCommands.Count - || terminal._disabledCommands.Exists(command => !_allCommands.Contains(command)) - ) - { - EditorGUILayout.BeginHorizontal(); - try - { - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Cleanup Disabled Commands")) - { - _seenCommands.Clear(); - for (int i = terminal._disabledCommands.Count - 1; 0 <= i; --i) - { - string command = terminal._disabledCommands[i]; - if (!_seenCommands.Add(command)) - { - terminal._disabledCommands.RemoveAt(i); - anyChanged = true; - continue; - } - - if (!_allCommands.Contains(command)) - { - terminal._disabledCommands.RemoveAt(i); - anyChanged = true; - } - } - } - GUILayout.FlexibleSpace(); - } - finally - { - EditorGUILayout.EndHorizontal(); - } - } - - return anyChanged; - } - - private static void CollectFonts( - TerminalUI terminal, - SortedDictionary> fontsByPrefix - ) - { - if ( - terminal == null - || terminal._fontPack == null - || terminal._fontPack._fonts is not { Count: > 0 } - ) - { - return; - } - - if (fontsByPrefix.Count != 0) - { - return; - } - - foreach (Font font in terminal._fontPack._fonts) - { - string fontName = font.name; - int indexOfSplit = fontName.IndexOf('-', StringComparison.OrdinalIgnoreCase); - if (indexOfSplit < 0) - { - indexOfSplit = fontName.IndexOf('_', StringComparison.OrdinalIgnoreCase); - } - - string key; - string secondKey; - if (0 <= indexOfSplit) - { - key = fontName[..indexOfSplit]; - secondKey = fontName[Mathf.Min(indexOfSplit + 1, fontName.Length)..]; - } - else - { - key = fontName; - secondKey = string.Empty; - } - - if (!fontsByPrefix.TryGetValue(key, out SortedDictionary fontMapping)) - { - fontMapping = new SortedDictionary( - StringComparer.OrdinalIgnoreCase - ); - fontsByPrefix[key] = fontMapping; - } - - fontMapping[secondKey] = font; - } - } - - private void TryMatchExistingFont(TerminalUI terminal) - { - if (0 <= _fontKey || 0 <= _secondFontKey || terminal.CurrentFont == null) - { - return; - } - - TrySetFontKeysFromFont(terminal.CurrentFont); - } - - private static bool TrySetupDefaultTheme(TerminalUI terminal) - { - if ( - !string.IsNullOrWhiteSpace(terminal.CurrentTheme) - && terminal._themePack != null - && terminal._themePack._themeNames.Contains( - terminal.CurrentTheme, - StringComparer.OrdinalIgnoreCase - ) - ) - { - return false; - } - - if (terminal._themePack == null || terminal._themePack._themeNames.Count == 0) - { - return false; - } - - string defaultTheme = terminal._themePack._themeNames.FirstOrDefault(theme => - theme.Contains("Dark", StringComparison.OrdinalIgnoreCase) - ); - if (string.IsNullOrWhiteSpace(defaultTheme)) - { - defaultTheme = terminal._themePack._themeNames.FirstOrDefault(theme => - theme.Contains("Light", StringComparison.OrdinalIgnoreCase) - ); - } - - if (string.IsNullOrWhiteSpace(defaultTheme)) - { - defaultTheme = terminal._themePack._themeNames.FirstOrDefault(); - } - - terminal.SetTheme(defaultTheme, persist: true); - return true; - } - - private static bool TrySetupDefaultFont(TerminalUI terminal) - { - if ( - terminal.CurrentFont != null - && terminal._fontPack != null - && terminal._fontPack._fonts.Contains(terminal.CurrentFont) - ) - { - return false; - } - - if (terminal._fontPack == null || terminal._fontPack._fonts is not { Count: > 0 }) - { - return false; - } - - Font defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => - font.name.Contains("SourceCodePro", StringComparison.OrdinalIgnoreCase) - && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) - ); - if (defaultFont == null) - { - defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => - font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) - && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) - ); - } - if (defaultFont == null) - { - defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => - font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) - ); - } - if (defaultFont == null) - { - defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => - font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) - ); - } - if (defaultFont == null) - { - defaultFont = terminal._fontPack._fonts.FirstOrDefault(); - } - - terminal.SetFont(defaultFont, persist: true); - return true; - } - - private bool RenderSelectableFonts(TerminalUI terminal) - { - if (_fontsByPrefix is not { Count: > 0 }) - { - return false; - } - - TryMatchExistingFont(terminal); - - bool anyChanged = false; - int currentFontKey = _fontKey; - EditorGUILayout.BeginHorizontal(); - try - { - if (terminal._fontPack != null) - { - if (_fontKey < 0 || _secondFontKey < 0) - { - GUILayout.Label("Select Font:"); - } - - string[] fontKeys = _fontsByPrefix.Keys.ToArray(); - _fontKey = EditorGUILayout.Popup(_fontKey, fontKeys); - - if (currentFontKey != _fontKey) - { - _secondFontKey = -1; - } - - if (0 <= _fontKey && _fontKey < fontKeys.Length) - { - string selectedFontKey = fontKeys[_fontKey]; - SortedDictionary availableFonts = _fontsByPrefix[ - selectedFontKey - ]; - string[] secondFontKeys = availableFonts.Keys.ToArray(); - Font selectedFont = null; - switch (secondFontKeys.Length) - { - case > 1: - { - _secondFontKey = EditorGUILayout.Popup( - _secondFontKey, - secondFontKeys - ); - - if (0 <= _secondFontKey && _secondFontKey < secondFontKeys.Length) - { - selectedFont = availableFonts[secondFontKeys[_secondFontKey]]; - } - - break; - } - case 1: - { - selectedFont = availableFonts.Values.Single(); - break; - } - } - - if (selectedFont != null) - { - GUIContent setFontContent = new( - "Set Font", - $"Update the terminal's font to {selectedFont.name}" - ); - bool clicked = - selectedFont != terminal._persistedFont - ? GUILayout.Button(setFontContent, _impactButtonStyle) - : GUILayout.Button(setFontContent); - if (clicked) - { - terminal.SetFont(selectedFont, persist: true); - anyChanged = true; - } - } - } - } - } - finally - { - GUILayout.EndHorizontal(); - } - - return anyChanged; - } - } -#endif -} +namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using Backend; + using DxCommandTerminal.Helper; + using UnityEditor; + using UnityEngine; + using UnityEngine.UIElements; + using Input; + using Persistence; + using Themes; + using UI; + using Object = UnityEngine.Object; +#if ENABLE_INPUT_SYSTEM + using UnityEngine.InputSystem; +#endif + + [InitializeOnLoad] + [CustomEditor(typeof(TerminalUI))] + public sealed class TerminalUIEditor : Editor + { + private static readonly TimeSpan CycleInterval = TimeSpan.FromSeconds(0.75); + + private int _commandIndex; + private TerminalUI _lastSeen; + + private readonly HashSet _allCommands = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _defaultCommands = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _nonDefaultCommands = new( + StringComparer.OrdinalIgnoreCase + ); + private readonly HashSet _seenCommands = new(StringComparer.OrdinalIgnoreCase); + private readonly SortedSet _intermediateResults = new( + StringComparer.OrdinalIgnoreCase + ); + private readonly SortedDictionary> _fontsByPrefix = + new(StringComparer.OrdinalIgnoreCase); + + private readonly Dictionary _seenLogTypes = new(); + private readonly List _fontPacks = new(); + private readonly List _themePacks = new(); + + private int _themeIndex = -1; + private int _fontKey = -1; + private int _secondFontKey = -1; + private int _themePackIndex = -1; + private int _fontPackIndex = -1; + private bool _isCyclingThemes; + private bool _isCyclingFonts; + private bool _persistThemeChanges; + + private TimeSpan? _lastFontCycleTime; + private TimeSpan? _lastThemeCycleTime; + + private readonly Stopwatch _timer = Stopwatch.StartNew(); + + private bool _editorUpdateAttached; + private GUIStyle _impactButtonStyle; + private GUIStyle _impactLabelStyle; + + static TerminalUIEditor() + { + ObjectFactory.componentWasAdded += HandleComponentAdded; + } + + private static void HandleComponentAdded(Component addedComponent) + { + bool anyChange = false; + if (addedComponent is TerminalUI terminal && terminal != null) + { + CheckForUIDocumentProblems(terminal); + + if (terminal._themePack == null) + { + TerminalThemePack[] themePacks = LoadAll(); + int themePackIndex = Array.FindIndex( + themePacks, + themePack => + string.Equals( + themePack.name, + "Minimal", + StringComparison.OrdinalIgnoreCase + ) + ); + if (themePackIndex < 0) + { + themePackIndex = themePacks.Length - 1; + } + + if (0 <= themePackIndex && themePackIndex < themePacks.Length) + { + TerminalThemePack themePack = themePacks[themePackIndex]; + terminal._themePack = themePack; + _ = TrySetupDefaultTheme(terminal); + } + } + + if (terminal._fontPack == null) + { + TerminalFontPack[] fontPacks = LoadAll(); + int fontPackIndex = Array.FindIndex( + fontPacks, + fontPack => + string.Equals( + fontPack.name, + "Minimal", + StringComparison.OrdinalIgnoreCase + ) + ); + if (fontPackIndex < 0) + { + fontPackIndex = fontPacks.Length - 1; + } + + if (0 <= fontPackIndex && fontPackIndex < fontPacks.Length) + { + TerminalFontPack fontPack = fontPacks[fontPackIndex]; + terminal._fontPack = fontPack; + _ = TrySetupDefaultFont(terminal); + } + } + + terminal.gameObject.AddComponent(); + terminal.gameObject.AddComponent(); + + EditorUtility.SetDirty(terminal); + EditorUtility.SetDirty(terminal.gameObject); + anyChange = true; + } + else + { + switch (addedComponent) + { + case TerminalKeyboardController keyboardController + when keyboardController != null: + { + if (keyboardController.TryGetComponent(out terminal)) + { + keyboardController.terminal = terminal; + EditorUtility.SetDirty(keyboardController); + anyChange = true; + } + + break; + } + case TerminalThemePersister themePersister when themePersister != null: + { + if (themePersister.TryGetComponent(out terminal)) + { + themePersister.terminal = terminal; + EditorUtility.SetDirty(themePersister); + anyChange = true; + } + + break; + } +#if ENABLE_INPUT_SYSTEM + case PlayerInput playerInput when playerInput != null: + { + if (playerInput.TryGetComponent(out terminal)) + { + if ( + !playerInput.TryGetComponent( + out TerminalPlayerInputController playerInputController + ) + ) + { + playerInputController = + playerInput.gameObject.AddComponent(); + playerInputController.terminal = terminal; + EditorUtility.SetDirty(playerInputController); + EditorUtility.SetDirty(playerInput.gameObject); + anyChange = true; + } + + if ( + playerInput.TryGetComponent( + out TerminalKeyboardController keyboardController + ) + ) + { + keyboardController.enabled = false; + EditorUtility.SetDirty(keyboardController); + anyChange = true; + } + } + + break; + } +#endif + } + } + + if (anyChange) + { + AssetDatabase.SaveAssets(); + } + } + + private void OnEnable() + { + _allCommands.Clear(); + _allCommands.UnionWith( + CommandShell + .RegisteredCommands.Value.Select(tuple => tuple.attribute) + .Select(attribute => attribute.Name) + ); + _defaultCommands.Clear(); + _defaultCommands.UnionWith( + CommandShell + .RegisteredCommands.Value.Select(tuple => tuple.attribute) + .Where(tuple => tuple.Default) + .Select(attribute => attribute.Name) + ); + _nonDefaultCommands.Clear(); + _nonDefaultCommands.UnionWith( + CommandShell + .RegisteredCommands.Value.Select(tuple => tuple.attribute) + .Where(tuple => !tuple.Default) + .Select(attribute => attribute.Name) + ); + _fontsByPrefix.Clear(); + + ResetStateIdempotent(force: true); + + if (!_editorUpdateAttached) + { + EditorApplication.update += EditorUpdate; + _editorUpdateAttached = true; + } + } + + private static T[] LoadAll() + where T : Object + { + List directories = new(); + + string directory = DirectoryHelper.GetCallerScriptDirectory(); + if (!string.IsNullOrWhiteSpace(directory)) + { + DirectoryInfo directoryInfo = new(directory); + if (directoryInfo.Exists) + { + directory = DirectoryHelper.FindPackageRootPath(directory); + directory = DirectoryHelper.AbsoluteToUnityRelativePath(directory); + directories.Add(directory); + } + } + + if (Directory.Exists(Path.Combine(Application.dataPath, "Packages"))) + { + directories.Add("Packages"); + } + + if (Directory.Exists(Path.Combine(Application.dataPath, "Library"))) + { + directories.Add("Library"); + } + + directories.Add("Assets"); + + HashSet unique = new(); + List ordered = new(); + string[] assetGuids = AssetDatabase.FindAssets( + $"t:{typeof(T).Name}", + directories.ToArray() + ); + foreach (string guid in assetGuids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + + if (string.IsNullOrWhiteSpace(assetPath)) + { + continue; + } + + T item = AssetDatabase.LoadAssetAtPath(assetPath); + if (item != null && unique.Add(item)) + { + ordered.Add(item); + } + } + return ordered.ToArray(); + } + + private void OnDisable() + { + if (_editorUpdateAttached) + { + EditorApplication.update -= EditorUpdate; + _editorUpdateAttached = false; + } + } + + private void ResetStateIdempotent(bool force) + { + foreach (TerminalThemePack themePack in TerminalAssetPackPostProcessor.NewThemePacks) + { + _themePacks.Add(themePack); + } + TerminalAssetPackPostProcessor.NewThemePacks.Clear(); + foreach (TerminalFontPack fontPack in TerminalAssetPackPostProcessor.NewFontPacks) + { + _fontPacks.Add(fontPack); + } + TerminalAssetPackPostProcessor.NewFontPacks.Clear(); + + if (!_fontPacks.Any()) + { + _fontPacks.Clear(); + _fontPacks.AddRange(LoadAll()); + } + + if (!_themePacks.Any()) + { + _themePacks.Clear(); + _themePacks.AddRange(LoadAll()); + } + + TerminalUI terminal = target as TerminalUI; + if (!force && _lastSeen == terminal) + { + return; + } + + _fontsByPrefix.Clear(); + CollectFonts(terminal, _fontsByPrefix); + + _persistThemeChanges = false; + StopCyclingFonts(); + StopCyclingThemes(); + _themeIndex = -1; + _fontKey = -1; + _secondFontKey = -1; + _lastSeen = terminal; + } + + private void EditorUpdate() + { + if (_isCyclingThemes) + { + if (_timer.Elapsed < _lastThemeCycleTime + CycleInterval) + { + return; + } + + TerminalUI terminal = target as TerminalUI; + if (terminal != null && terminal._themePack._themeNames is { Count: > 0 }) + { + int newThemeIndex = (_themeIndex + 1) % terminal._themePack._themeNames.Count; + newThemeIndex = + (newThemeIndex + terminal._themePack._themeNames.Count) + % terminal._themePack._themeNames.Count; + terminal.SetTheme( + terminal._themePack._themeNames[newThemeIndex], + persist: _persistThemeChanges + ); + _themeIndex = newThemeIndex; + } + + _lastThemeCycleTime = _timer.Elapsed; + } + + if (_isCyclingFonts) + { + if (_timer.Elapsed < _lastFontCycleTime + CycleInterval) + { + return; + } + + TerminalUI terminal = target as TerminalUI; + if (terminal != null && terminal._fontPack._fonts is { Count: > 0 }) + { + Font currentFont = GetCurrentlySelectedFont(terminal); + int fontIndex = terminal._fontPack._fonts.IndexOf(currentFont); + int newFontIndex = (fontIndex + 1) % terminal._fontPack._fonts.Count; + Font newFont = terminal._fontPack._fonts[newFontIndex]; + terminal.SetFont(newFont, persist: _persistThemeChanges); + TrySetFontKeysFromFont(newFont); + } + + _lastFontCycleTime = _timer.Elapsed; + } + } + + private Font GetCurrentlySelectedFont(TerminalUI terminal) + { + if (_fontKey < 0 || _secondFontKey < 0) + { + return terminal.CurrentFont; + } + + try + { + return _fontsByPrefix.ToArray()[_fontKey].Value.ToArray()[_secondFontKey].Value; + } + catch + { + return terminal.CurrentFont; + } + } + + public override void OnInspectorGUI() + { + _impactButtonStyle ??= new GUIStyle(GUI.skin.button) + { + normal = { textColor = Color.yellow }, + fontStyle = FontStyle.Bold, + }; + _impactLabelStyle ??= new GUIStyle(GUI.skin.label) + { + normal = { textColor = new Color(1f, 0.3f, 0.3f, 1f) }, + fontStyle = FontStyle.Bold, + }; + + if (_allCommands.Count == 0 || _defaultCommands.Count == 0) + { + HydrateCommandCaches(); + } + + TerminalUI terminal = target as TerminalUI; + if (terminal == null) + { + return; + } + + serializedObject.Update(); + ResetStateIdempotent(force: false); + + bool anyChanged = false; + + bool uiDocumentChanged = CheckForUIDocumentProblems(terminal); + anyChanged |= uiDocumentChanged; + + bool themesChanged = CheckForThemingAndFontChanges(terminal); + anyChanged |= themesChanged; + + RenderCyclingPreviews(); + + DrawPropertiesExcluding( + serializedObject, + "m_Script", + nameof(TerminalUI._themePack), + nameof(TerminalUI._fontPack), + nameof(TerminalUI._persistedTheme), + nameof(TerminalUI._uiDocument) + ); + + bool propertiesDirty = CheckForSimpleProperties(terminal); + anyChanged |= propertiesDirty; + + RenderCommandManipulationHeader(); + + bool ignoredCommandsUpdated = CheckForIgnoredCommandUpdates(terminal); + anyChanged |= ignoredCommandsUpdated; + + bool commandsUpdated = CheckForDisabledCommandProblems(terminal); + anyChanged |= commandsUpdated; + + serializedObject.ApplyModifiedProperties(); + if (anyChanged) + { + EditorUtility.SetDirty(terminal); + } + } + + private void HydrateCommandCaches() + { + _allCommands.Clear(); + _allCommands.UnionWith( + CommandShell + .RegisteredCommands.Value.Select(tuple => tuple.attribute) + .Select(attribute => attribute.Name) + ); + _defaultCommands.Clear(); + _defaultCommands.UnionWith( + CommandShell + .RegisteredCommands.Value.Select(tuple => tuple.attribute) + .Where(tuple => tuple.Default) + .Select(attribute => attribute.Name) + ); + _nonDefaultCommands.Clear(); + _nonDefaultCommands.UnionWith( + CommandShell + .RegisteredCommands.Value.Select(tuple => tuple.attribute) + .Where(tuple => !tuple.Default) + .Select(attribute => attribute.Name) + ); + } + + private void RenderCyclingPreviews() + { + EditorGUILayout.Space(10); + EditorGUILayout.BeginHorizontal(); + try + { + EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel); + bool oldPersistThemeChanges = _persistThemeChanges; + _persistThemeChanges = GUILayout.Toggle( + _persistThemeChanges, + "Persist Theme Changes" + ); + + if (oldPersistThemeChanges != _persistThemeChanges) + { + StopCyclingFonts(); + StopCyclingThemes(); + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + + if (Application.isPlaying) + { + EditorGUILayout.BeginHorizontal(); + try + { + TryCyclingThemes(); + TryCyclingFonts(); + } + finally + { + EditorGUILayout.EndHorizontal(); + } + } + + EditorGUILayout.BeginHorizontal(); + try + { + TrySetRandomTheme(); + TrySetRandomFont(); + } + finally + { + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(); + } + + private void TrySetRandomTheme() + { + if (_isCyclingThemes) + { + return; + } + + bool clicked = _persistThemeChanges + ? GUILayout.Button("Set Random Theme", _impactButtonStyle) + : GUILayout.Button("Set Random Theme "); + + if (clicked) + { + TerminalUI terminal = target as TerminalUI; + if ( + terminal != null + && terminal._themePack != null + && terminal._themePack._themeNames is { Count: > 0 } + ) + { + int newThemeIndex; + do + { + newThemeIndex = ThreadLocalRandom.Instance.Next( + terminal._themePack._themeNames.Count + ); + } while ( + newThemeIndex == _themeIndex && terminal._themePack._themeNames.Count != 1 + ); + terminal.SetTheme( + terminal._themePack._themeNames[newThemeIndex], + persist: _persistThemeChanges + ); + _themeIndex = newThemeIndex; + } + } + } + + private void TrySetRandomFont() + { + if (_isCyclingFonts) + { + return; + } + + bool clicked = _persistThemeChanges + ? GUILayout.Button("Set Random Font", _impactButtonStyle) + : GUILayout.Button("Set Random Font "); + + if (clicked) + { + TerminalUI terminal = target as TerminalUI; + if ( + terminal != null + && terminal._fontPack != null + && terminal._fontPack._fonts is { Count: > 0 } + ) + { + Font currentlySelectedFont = GetCurrentlySelectedFont(terminal); + int oldFontIndex = terminal._fontPack._fonts.IndexOf(currentlySelectedFont); + int newFontIndex; + do + { + newFontIndex = ThreadLocalRandom.Instance.Next( + terminal._fontPack._fonts.Count + ); + } while (newFontIndex == oldFontIndex && terminal._fontPack._fonts.Count != 1); + + Font newFont = terminal._fontPack._fonts[newFontIndex]; + terminal.SetFont(newFont, persist: _persistThemeChanges); + TrySetFontKeysFromFont(newFont); + } + } + } + + private void TryCyclingFonts() + { + if (_isCyclingFonts) + { + if (GUILayout.Button("Stop Cycling Fonts")) + { + StopCyclingFonts(); + } + } + else + { + bool clicked = _persistThemeChanges + ? GUILayout.Button("Start Cycling Fonts", _impactButtonStyle) + : GUILayout.Button("Start Cycling Fonts"); + if (clicked) + { + StartCyclingFonts(); + } + } + } + + private void StartCyclingFonts() + { + if (_isCyclingFonts) + { + return; + } + _isCyclingFonts = true; + _lastFontCycleTime = null; + } + + private void StopCyclingFonts() + { + _isCyclingFonts = false; + } + + private void TryCyclingThemes() + { + if (_isCyclingThemes) + { + if (GUILayout.Button("Stop Cycling Themes")) + { + StopCyclingThemes(); + } + } + else + { + bool clicked = _persistThemeChanges + ? GUILayout.Button("Start Cycling Themes", _impactButtonStyle) + : GUILayout.Button("Start Cycling Themes"); + if (clicked) + { + StartCyclingThemes(); + } + } + } + + private void StartCyclingThemes() + { + if (_isCyclingThemes) + { + return; + } + + _isCyclingThemes = true; + _lastThemeCycleTime = null; + } + + private void StopCyclingThemes() + { + _isCyclingThemes = false; + } + + private void TrySetFontKeysFromFont(Font font) + { + int firstIndex = 0; + bool foundFont = false; + foreach ( + KeyValuePair> fontKeyEntry in _fontsByPrefix + ) + { + int secondIndex = 0; + foreach (KeyValuePair secondFontKeyEntry in fontKeyEntry.Value) + { + if (secondFontKeyEntry.Value == font) + { + _fontKey = firstIndex; + _secondFontKey = secondIndex; + foundFont = true; + break; + } + + ++secondIndex; + } + + if (foundFont) + { + break; + } + + ++firstIndex; + } + } + + private bool CheckForThemingAndFontChanges(TerminalUI terminal) + { + bool anyChanged = false; + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("Pack Selection", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + try + { + if (!_themePacks.Any()) + { + GUILayout.Label("NO THEME PACKS", _impactLabelStyle); + } + else + { + if (_themePackIndex < 0) + { + _themePackIndex = _themePacks.IndexOf(terminal._themePack); + } + + if (_themePackIndex < 0) + { + GUILayout.Label("Select Theme Pack:"); + } + + _themePackIndex = EditorGUILayout.Popup( + _themePackIndex, + _themePacks.Select(themePack => themePack.name).ToArray() + ); + if (0 <= _themePackIndex && _themePackIndex < _themePacks.Count) + { + TerminalThemePack themePack = _themePacks[_themePackIndex]; + bool clicked = + themePack != terminal._themePack + ? GUILayout.Button("Set Theme Pack", _impactButtonStyle) + : GUILayout.Button("Set Theme Pack"); + if (clicked) + { + if (themePack != terminal._themePack) + { + terminal._themePack = themePack; + _themeIndex = themePack._themeNames.IndexOf(terminal.CurrentTheme); + if (_themeIndex < 0) + { + terminal._persistedTheme = string.Empty; + } + + anyChanged = true; + } + } + } + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.BeginHorizontal(); + try + { + if (!_fontPacks.Any()) + { + GUILayout.Label("NO FONT PACKS", _impactLabelStyle); + } + else + { + if (_fontPackIndex < 0) + { + _fontPackIndex = _fontPacks.IndexOf(terminal._fontPack); + } + + if (_fontPackIndex < 0) + { + GUILayout.Label("Select Font Pack:"); + } + + _fontPackIndex = EditorGUILayout.Popup( + _fontPackIndex, + _fontPacks.Select(fontPack => fontPack.name).ToArray() + ); + if (0 <= _fontPackIndex && _fontPackIndex < _fontPacks.Count) + { + TerminalFontPack fontPack = _fontPacks[_fontPackIndex]; + bool clicked = + fontPack != terminal._fontPack + ? GUILayout.Button("Set Font Pack", _impactButtonStyle) + : GUILayout.Button("Set Font Pack"); + if (clicked) + { + if (fontPack != terminal._fontPack) + { + _fontsByPrefix.Clear(); + _fontKey = -1; + _secondFontKey = -1; + terminal._fontPack = fontPack; + if (!terminal._fontPack._fonts.Contains(terminal.CurrentFont)) + { + terminal._persistedFont = null; + } + + anyChanged = true; + } + } + } + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("Theming", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + try + { + if (terminal._themePack != null) + { + if (_themeIndex < 0) + { + _themeIndex = terminal._themePack._themeNames.IndexOf( + terminal.CurrentTheme + ); + } + + if (_themeIndex < 0) + { + GUILayout.Label("Select Theme:"); + } + + _themeIndex = EditorGUILayout.Popup( + _themeIndex, + terminal + ._themePack._themeNames.Select(theme => + theme + .Replace( + "-theme", + string.Empty, + StringComparison.OrdinalIgnoreCase + ) + .Replace( + "theme-", + string.Empty, + StringComparison.OrdinalIgnoreCase + ) + ) + .ToArray() + ); + + if (0 <= _themeIndex && _themeIndex < terminal._themePack._themeNames.Count) + { + string selectedTheme = terminal._themePack._themeNames[_themeIndex]; + GUIContent setThemeContent = new( + "Set Theme", + $"Will set the current theme to {selectedTheme}" + ); + bool clicked = !string.Equals( + selectedTheme, + terminal._persistedTheme, + StringComparison.OrdinalIgnoreCase + ) + ? GUILayout.Button(setThemeContent, _impactButtonStyle) + : GUILayout.Button(setThemeContent); + if (clicked) + { + terminal.SetTheme(selectedTheme, persist: true); + anyChanged = true; + } + } + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + + CollectFonts(terminal, _fontsByPrefix); + bool fontsUpdated = RenderSelectableFonts(terminal); + return anyChanged || fontsUpdated; + } + + private bool CheckForSimpleProperties(TerminalUI terminal) + { + bool anyChanged = false; + + if (terminal._ignoredLogTypes == null) + { + terminal._ignoredLogTypes = new List(); + anyChanged = true; + } + + if (terminal._disabledCommands == null) + { + terminal._disabledCommands = new List(); + anyChanged = true; + } + + _seenLogTypes.Clear(); + for (int i = terminal._ignoredLogTypes.Count - 1; 0 <= i; --i) + { + TerminalLogType logType = terminal._ignoredLogTypes[i]; + int count = 0; + if ( + Enum.IsDefined(typeof(TerminalLogType), logType) + && (!_seenLogTypes.TryGetValue(logType, out count) || count <= 1) + ) + { + _seenLogTypes[logType] = count + 1; + continue; + } + + _seenLogTypes[logType] = count + 1; + anyChanged = true; + terminal._ignoredLogTypes.RemoveAt(i); + } + + return anyChanged; + } + + private static bool CheckForUIDocumentProblems(TerminalUI terminal) + { + bool anyChanged = false; + if (terminal._uiDocument == null) + { + terminal._uiDocument = terminal.TryGetComponent(out UIDocument uiDocument) + ? uiDocument + : terminal.gameObject.AddComponent(); + anyChanged = true; + } + + if (terminal._uiDocument.panelSettings != null) + { + return anyChanged; + } + + string[] panelSettingGuids; + string absoluteStylesPath = DirectoryHelper.FindAbsolutePathToDirectory("Styles"); + if (!string.IsNullOrWhiteSpace(absoluteStylesPath)) + { + panelSettingGuids = AssetDatabase.FindAssets( + "t:PanelSettings", + new[] { absoluteStylesPath } + ); + TryFindTerminalSettings(); + if (terminal._uiDocument.panelSettings != null) + { + return true; + } + } + + List directories = new(); + if (Directory.Exists(Path.Combine(Application.dataPath, "Library"))) + { + directories.Add("Library"); + } + + if (Directory.Exists(Path.Combine(Application.dataPath, "Packages"))) + { + directories.Add("Packages"); + } + + directories.Add("Assets"); + panelSettingGuids = AssetDatabase.FindAssets("t:PanelSettings", directories.ToArray()); + TryFindTerminalSettings(); + return anyChanged; + + void TryFindTerminalSettings() + { + foreach (string guid in panelSettingGuids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrWhiteSpace(assetPath)) + { + continue; + } + + if (!assetPath.Contains("TerminalSettings", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + PanelSettings panelSettings = AssetDatabase.LoadAssetAtPath( + assetPath + ); + + if (panelSettings == null) + { + continue; + } + + terminal._uiDocument.panelSettings = panelSettings; + anyChanged = true; + return; + } + } + } + + private static void RenderCommandManipulationHeader() + { + EditorGUILayout.Space(); + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("Command Manipulation", EditorStyles.boldLabel); + } + + private bool CheckForIgnoredCommandUpdates(TerminalUI terminal) + { + bool anyChanged = false; + _intermediateResults.Clear(); + _intermediateResults.UnionWith(_nonDefaultCommands); + if (!terminal.ignoreDefaultCommands) + { + _intermediateResults.UnionWith(_defaultCommands); + } + _intermediateResults.ExceptWith(terminal._disabledCommands); + + if (0 < _intermediateResults.Count) + { + string[] ignorableCommands = _intermediateResults.ToArray(); + + EditorGUILayout.BeginHorizontal(); + try + { + _commandIndex = EditorGUILayout.Popup(_commandIndex, ignorableCommands); + + if (0 <= _commandIndex && _commandIndex < ignorableCommands.Length) + { + GUIContent ignoreContent = new( + "Ignore Command", + $"Ignores the {ignorableCommands[_commandIndex]} command" + ); + if (GUILayout.Button(ignoreContent)) + { + string command = ignorableCommands[_commandIndex]; + terminal._disabledCommands.Add(command); + anyChanged = true; + } + } + } + finally + { + EditorGUILayout.EndHorizontal(); + } + } + + return anyChanged; + } + + private bool CheckForDisabledCommandProblems(TerminalUI terminal) + { + bool anyChanged = false; + _seenCommands.Clear(); + _seenCommands.UnionWith(terminal._disabledCommands); + + if ( + _seenCommands.Count != terminal._disabledCommands.Count + || terminal._disabledCommands.Exists(command => !_allCommands.Contains(command)) + ) + { + EditorGUILayout.BeginHorizontal(); + try + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Cleanup Disabled Commands")) + { + _seenCommands.Clear(); + for (int i = terminal._disabledCommands.Count - 1; 0 <= i; --i) + { + string command = terminal._disabledCommands[i]; + if (!_seenCommands.Add(command)) + { + terminal._disabledCommands.RemoveAt(i); + anyChanged = true; + continue; + } + + if (!_allCommands.Contains(command)) + { + terminal._disabledCommands.RemoveAt(i); + anyChanged = true; + } + } + } + GUILayout.FlexibleSpace(); + } + finally + { + EditorGUILayout.EndHorizontal(); + } + } + + return anyChanged; + } + + private static void CollectFonts( + TerminalUI terminal, + SortedDictionary> fontsByPrefix + ) + { + if ( + terminal == null + || terminal._fontPack == null + || terminal._fontPack._fonts is not { Count: > 0 } + ) + { + return; + } + + if (fontsByPrefix.Count != 0) + { + return; + } + + foreach (Font font in terminal._fontPack._fonts) + { + string fontName = font.name; + int indexOfSplit = fontName.IndexOf('-', StringComparison.OrdinalIgnoreCase); + if (indexOfSplit < 0) + { + indexOfSplit = fontName.IndexOf('_', StringComparison.OrdinalIgnoreCase); + } + + string key; + string secondKey; + if (0 <= indexOfSplit) + { + key = fontName[..indexOfSplit]; + secondKey = fontName[Mathf.Min(indexOfSplit + 1, fontName.Length)..]; + } + else + { + key = fontName; + secondKey = string.Empty; + } + + if (!fontsByPrefix.TryGetValue(key, out SortedDictionary fontMapping)) + { + fontMapping = new SortedDictionary( + StringComparer.OrdinalIgnoreCase + ); + fontsByPrefix[key] = fontMapping; + } + + fontMapping[secondKey] = font; + } + } + + private void TryMatchExistingFont(TerminalUI terminal) + { + if (0 <= _fontKey || 0 <= _secondFontKey || terminal.CurrentFont == null) + { + return; + } + + TrySetFontKeysFromFont(terminal.CurrentFont); + } + + private static bool TrySetupDefaultTheme(TerminalUI terminal) + { + if ( + !string.IsNullOrWhiteSpace(terminal.CurrentTheme) + && terminal._themePack != null + && terminal._themePack._themeNames.Contains( + terminal.CurrentTheme, + StringComparer.OrdinalIgnoreCase + ) + ) + { + return false; + } + + if (terminal._themePack == null || terminal._themePack._themeNames.Count == 0) + { + return false; + } + + string defaultTheme = terminal._themePack._themeNames.FirstOrDefault(theme => + theme.Contains("Dark", StringComparison.OrdinalIgnoreCase) + ); + if (string.IsNullOrWhiteSpace(defaultTheme)) + { + defaultTheme = terminal._themePack._themeNames.FirstOrDefault(theme => + theme.Contains("Light", StringComparison.OrdinalIgnoreCase) + ); + } + + if (string.IsNullOrWhiteSpace(defaultTheme)) + { + defaultTheme = terminal._themePack._themeNames.FirstOrDefault(); + } + + terminal.SetTheme(defaultTheme, persist: true); + return true; + } + + private static bool TrySetupDefaultFont(TerminalUI terminal) + { + if ( + terminal.CurrentFont != null + && terminal._fontPack != null + && terminal._fontPack._fonts.Contains(terminal.CurrentFont) + ) + { + return false; + } + + if (terminal._fontPack == null || terminal._fontPack._fonts is not { Count: > 0 }) + { + return false; + } + + Font defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => + font.name.Contains("SourceCodePro", StringComparison.OrdinalIgnoreCase) + && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) + ); + if (defaultFont == null) + { + defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => + font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) + && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) + ); + } + if (defaultFont == null) + { + defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => + font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) + ); + } + if (defaultFont == null) + { + defaultFont = terminal._fontPack._fonts.FirstOrDefault(font => + font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) + ); + } + if (defaultFont == null) + { + defaultFont = terminal._fontPack._fonts.FirstOrDefault(); + } + + terminal.SetFont(defaultFont, persist: true); + return true; + } + + private bool RenderSelectableFonts(TerminalUI terminal) + { + if (_fontsByPrefix is not { Count: > 0 }) + { + return false; + } + + TryMatchExistingFont(terminal); + + bool anyChanged = false; + int currentFontKey = _fontKey; + EditorGUILayout.BeginHorizontal(); + try + { + if (terminal._fontPack != null) + { + if (_fontKey < 0 || _secondFontKey < 0) + { + GUILayout.Label("Select Font:"); + } + + string[] fontKeys = _fontsByPrefix.Keys.ToArray(); + _fontKey = EditorGUILayout.Popup(_fontKey, fontKeys); + + if (currentFontKey != _fontKey) + { + _secondFontKey = -1; + } + + if (0 <= _fontKey && _fontKey < fontKeys.Length) + { + string selectedFontKey = fontKeys[_fontKey]; + SortedDictionary availableFonts = _fontsByPrefix[ + selectedFontKey + ]; + string[] secondFontKeys = availableFonts.Keys.ToArray(); + Font selectedFont = null; + switch (secondFontKeys.Length) + { + case > 1: + { + _secondFontKey = EditorGUILayout.Popup( + _secondFontKey, + secondFontKeys + ); + + if (0 <= _secondFontKey && _secondFontKey < secondFontKeys.Length) + { + selectedFont = availableFonts[secondFontKeys[_secondFontKey]]; + } + + break; + } + case 1: + { + selectedFont = availableFonts.Values.Single(); + break; + } + } + + if (selectedFont != null) + { + GUIContent setFontContent = new( + "Set Font", + $"Update the terminal's font to {selectedFont.name}" + ); + bool clicked = + selectedFont != terminal._persistedFont + ? GUILayout.Button(setFontContent, _impactButtonStyle) + : GUILayout.Button(setFontContent); + if (clicked) + { + terminal.SetFont(selectedFont, persist: true); + anyChanged = true; + } + } + } + } + } + finally + { + GUILayout.EndHorizontal(); + } + + return anyChanged; + } + } +#endif +} diff --git a/Editor/DxShowIfPropertyDrawer.cs b/Editor/DxShowIfPropertyDrawer.cs index 5d184a8..fd751ac 100644 --- a/Editor/DxShowIfPropertyDrawer.cs +++ b/Editor/DxShowIfPropertyDrawer.cs @@ -1,62 +1,62 @@ -namespace WallstopStudios.DxCommandTerminal.Editor -{ -#if UNITY_EDITOR - using System.Reflection; - using Attributes; - using Extensions; - using UnityEditor; - using UnityEngine; - - [CustomPropertyDrawer(typeof(DxShowIfAttribute))] - public sealed class DxShowIfPropertyDrawer : PropertyDrawer - { - public override float GetPropertyHeight(SerializedProperty property, GUIContent label) - { - return !ShouldShow(property) ? 0f : EditorGUI.GetPropertyHeight(property, label, true); - } - - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - if (ShouldShow(property)) - { - EditorGUI.PropertyField(position, property, label, true); - } - } - - private bool ShouldShow(SerializedProperty property) - { - if (attribute is not DxShowIfAttribute showIf) - { - return true; - } - - SerializedProperty conditionProperty = property.serializedObject.FindProperty( - showIf.conditionField - ); - if (conditionProperty is not { propertyType: SerializedPropertyType.Boolean }) - { - if (conditionProperty != null) - { - return true; - } - - object enclosingObject = property.GetEnclosingObject(out _); - FieldInfo conditionField = enclosingObject - ?.GetType() - .GetField( - showIf.conditionField, - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic - ); - if (conditionField?.GetValue(enclosingObject) is bool maybeCondition) - { - return showIf.inverse ? !maybeCondition : maybeCondition; - } - return true; - } - - bool condition = conditionProperty.boolValue; - return showIf.inverse ? !condition : condition; - } - } -#endif -} +namespace WallstopStudios.DxCommandTerminal.Editor +{ +#if UNITY_EDITOR + using System.Reflection; + using Attributes; + using Extensions; + using UnityEditor; + using UnityEngine; + + [CustomPropertyDrawer(typeof(DxShowIfAttribute))] + public sealed class DxShowIfPropertyDrawer : PropertyDrawer + { + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return !ShouldShow(property) ? 0f : EditorGUI.GetPropertyHeight(property, label, true); + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + if (ShouldShow(property)) + { + EditorGUI.PropertyField(position, property, label, true); + } + } + + private bool ShouldShow(SerializedProperty property) + { + if (attribute is not DxShowIfAttribute showIf) + { + return true; + } + + SerializedProperty conditionProperty = property.serializedObject.FindProperty( + showIf.conditionField + ); + if (conditionProperty is not { propertyType: SerializedPropertyType.Boolean }) + { + if (conditionProperty != null) + { + return true; + } + + object enclosingObject = property.GetEnclosingObject(out _); + FieldInfo conditionField = enclosingObject + ?.GetType() + .GetField( + showIf.conditionField, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic + ); + if (conditionField?.GetValue(enclosingObject) is bool maybeCondition) + { + return showIf.inverse ? !maybeCondition : maybeCondition; + } + return true; + } + + bool condition = conditionProperty.boolValue; + return showIf.inverse ? !condition : condition; + } + } +#endif +} diff --git a/Editor/Helper/TerminalThemeStyleSheetHelper.cs b/Editor/Helper/TerminalThemeStyleSheetHelper.cs index eecfb6f..2915de8 100644 --- a/Editor/Helper/TerminalThemeStyleSheetHelper.cs +++ b/Editor/Helper/TerminalThemeStyleSheetHelper.cs @@ -1,156 +1,156 @@ -namespace WallstopStudios.DxCommandTerminal.Editor.Helper -{ -#if UNITY_EDITOR - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text.RegularExpressions; - using Themes; - using UnityEditor; - using UnityEngine; - using UnityEngine.UIElements; - - public static class TerminalThemeStyleSheetHelper - { - private static readonly List RequiredVariables = new() - { - "--terminal-bg", - "--button-bg", - "--input-field-bg", - "--button-selected-bg", - "--button-hover-bg", - "--scroll-bg", - "--scroll-inverse-bg", - "--scroll-active-bg", - "--button-text", - "--button-selected-text", - "--button-hover-text", - "--input-text-color", - "--text-message", - "--text-warning", - "--text-input-echo", - "--text-shell", - "--text-error", - "--scroll-color", - "--caret-color", - }; - - public static string[] GetAvailableThemes(StyleSheet styleSheetAsset) - { - if (styleSheetAsset == null) - { - return Array.Empty(); - } - - string assetPath = AssetDatabase.GetAssetPath(styleSheetAsset); - if ( - string.IsNullOrWhiteSpace(assetPath) - || !assetPath.EndsWith(".uss", StringComparison.OrdinalIgnoreCase) - ) - { - return Array.Empty(); - } - - try - { - string ussContent = File.ReadAllText(assetPath); - - // Remove block comments /* ... */ - ussContent = Regex.Replace( - ussContent, - @"/\*.*?\*/", - string.Empty, - RegexOptions.Singleline - ); - - // Remove line comments // ... - ussContent = Regex.Replace( - ussContent, - "//.*$", - string.Empty, - RegexOptions.Multiline - ); - - SortedSet selectors = new(StringComparer.OrdinalIgnoreCase); - - int lastIndex = 0; - while (lastIndex < ussContent.Length) - { - int braceIndex = ussContent.IndexOf('{', lastIndex); - if (braceIndex < 0) - { - break; - } - - int previousRuleEnd = ussContent.LastIndexOf( - '}', - braceIndex - 1, - braceIndex - lastIndex - ); - int selectorStartIndex = previousRuleEnd < 0 ? lastIndex : previousRuleEnd + 1; - - string selectorPart = ussContent - .Substring(selectorStartIndex, braceIndex - selectorStartIndex) - .Trim(); - - if (!string.IsNullOrWhiteSpace(selectorPart)) - { - string[] individualSelectors = selectorPart.Split(','); - foreach (string sel in individualSelectors) - { - string trimmedSelector = sel.Trim(); - if (trimmedSelector.StartsWith('.')) - { - trimmedSelector = trimmedSelector[1..]; - } - - if (string.IsNullOrWhiteSpace(trimmedSelector)) - { - continue; - } - - if (ThemeNameHelper.IsThemeName(trimmedSelector)) - { - int nextObjectBraceIndex = ussContent.IndexOf('}', braceIndex + 1); - if (nextObjectBraceIndex < 0) - { - nextObjectBraceIndex = ussContent.Length; - } - string objectContents = ussContent.Substring( - selectorStartIndex, - nextObjectBraceIndex - selectorStartIndex - ); - - if ( - RequiredVariables.Exists(requiredVariable => - !objectContents.Contains( - requiredVariable, - StringComparison.OrdinalIgnoreCase - ) - ) - ) - { - return Array.Empty(); - } - - selectors.Add(trimmedSelector); - } - } - } - - int nextBraceIndex = ussContent.IndexOf('}', braceIndex + 1); - lastIndex = nextBraceIndex < 0 ? ussContent.Length : nextBraceIndex + 1; - } - - return selectors.ToArray(); - } - catch (Exception e) - { - Debug.LogException(e); - return Array.Empty(); - } - } - } -#endif -} +namespace WallstopStudios.DxCommandTerminal.Editor.Helper +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using Themes; + using UnityEditor; + using UnityEngine; + using UnityEngine.UIElements; + + public static class TerminalThemeStyleSheetHelper + { + private static readonly List RequiredVariables = new() + { + "--terminal-bg", + "--button-bg", + "--input-field-bg", + "--button-selected-bg", + "--button-hover-bg", + "--scroll-bg", + "--scroll-inverse-bg", + "--scroll-active-bg", + "--button-text", + "--button-selected-text", + "--button-hover-text", + "--input-text-color", + "--text-message", + "--text-warning", + "--text-input-echo", + "--text-shell", + "--text-error", + "--scroll-color", + "--caret-color", + }; + + public static string[] GetAvailableThemes(StyleSheet styleSheetAsset) + { + if (styleSheetAsset == null) + { + return Array.Empty(); + } + + string assetPath = AssetDatabase.GetAssetPath(styleSheetAsset); + if ( + string.IsNullOrWhiteSpace(assetPath) + || !assetPath.EndsWith(".uss", StringComparison.OrdinalIgnoreCase) + ) + { + return Array.Empty(); + } + + try + { + string ussContent = File.ReadAllText(assetPath); + + // Remove block comments /* ... */ + ussContent = Regex.Replace( + ussContent, + @"/\*.*?\*/", + string.Empty, + RegexOptions.Singleline + ); + + // Remove line comments // ... + ussContent = Regex.Replace( + ussContent, + "//.*$", + string.Empty, + RegexOptions.Multiline + ); + + SortedSet selectors = new(StringComparer.OrdinalIgnoreCase); + + int lastIndex = 0; + while (lastIndex < ussContent.Length) + { + int braceIndex = ussContent.IndexOf('{', lastIndex); + if (braceIndex < 0) + { + break; + } + + int previousRuleEnd = ussContent.LastIndexOf( + '}', + braceIndex - 1, + braceIndex - lastIndex + ); + int selectorStartIndex = previousRuleEnd < 0 ? lastIndex : previousRuleEnd + 1; + + string selectorPart = ussContent + .Substring(selectorStartIndex, braceIndex - selectorStartIndex) + .Trim(); + + if (!string.IsNullOrWhiteSpace(selectorPart)) + { + string[] individualSelectors = selectorPart.Split(','); + foreach (string sel in individualSelectors) + { + string trimmedSelector = sel.Trim(); + if (trimmedSelector.StartsWith('.')) + { + trimmedSelector = trimmedSelector[1..]; + } + + if (string.IsNullOrWhiteSpace(trimmedSelector)) + { + continue; + } + + if (ThemeNameHelper.IsThemeName(trimmedSelector)) + { + int nextObjectBraceIndex = ussContent.IndexOf('}', braceIndex + 1); + if (nextObjectBraceIndex < 0) + { + nextObjectBraceIndex = ussContent.Length; + } + string objectContents = ussContent.Substring( + selectorStartIndex, + nextObjectBraceIndex - selectorStartIndex + ); + + if ( + RequiredVariables.Exists(requiredVariable => + !objectContents.Contains( + requiredVariable, + StringComparison.OrdinalIgnoreCase + ) + ) + ) + { + return Array.Empty(); + } + + selectors.Add(trimmedSelector); + } + } + } + + int nextBraceIndex = ussContent.IndexOf('}', braceIndex + 1); + lastIndex = nextBraceIndex < 0 ? ussContent.Length : nextBraceIndex + 1; + } + + return selectors.ToArray(); + } + catch (Exception e) + { + Debug.LogException(e); + return Array.Empty(); + } + } + } +#endif +} diff --git a/Editor/TerminalAssetPackPostProcessor.cs b/Editor/TerminalAssetPackPostProcessor.cs index 1386c7a..f0b979c 100644 --- a/Editor/TerminalAssetPackPostProcessor.cs +++ b/Editor/TerminalAssetPackPostProcessor.cs @@ -1,45 +1,45 @@ -namespace WallstopStudios.DxCommandTerminal.Editor -{ - using System; - using System.Collections.Generic; - using Themes; - using UnityEditor; - - internal sealed class TerminalAssetPackPostProcessor : AssetPostprocessor - { - internal static readonly List NewThemePacks = new(); - internal static readonly List NewFontPacks = new(); - - private static void OnPostprocessAllAssets( - string[] importedAssets, - string[] deletedAssets, - string[] movedAssets, - string[] movedFromAssetPaths - ) - { - foreach (string importedAsset in importedAssets) - { - if (!importedAsset.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - TerminalThemePack themePack = AssetDatabase.LoadAssetAtPath( - importedAsset - ); - if (themePack != null) - { - NewThemePacks.Add(themePack); - } - - TerminalFontPack fontPack = AssetDatabase.LoadAssetAtPath( - importedAsset - ); - if (fontPack != null) - { - NewFontPacks.Add(fontPack); - } - } - } - } -} +namespace WallstopStudios.DxCommandTerminal.Editor +{ + using System; + using System.Collections.Generic; + using Themes; + using UnityEditor; + + internal sealed class TerminalAssetPackPostProcessor : AssetPostprocessor + { + internal static readonly List NewThemePacks = new(); + internal static readonly List NewFontPacks = new(); + + private static void OnPostprocessAllAssets( + string[] importedAssets, + string[] deletedAssets, + string[] movedAssets, + string[] movedFromAssetPaths + ) + { + foreach (string importedAsset in importedAssets) + { + if (!importedAsset.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + TerminalThemePack themePack = AssetDatabase.LoadAssetAtPath( + importedAsset + ); + if (themePack != null) + { + NewThemePacks.Add(themePack); + } + + TerminalFontPack fontPack = AssetDatabase.LoadAssetAtPath( + importedAsset + ); + if (fontPack != null) + { + NewFontPacks.Add(fontPack); + } + } + } + } +} diff --git a/Editor/WallstopStudios.DxCommandTerminal.Editor.asmdef b/Editor/WallstopStudios.DxCommandTerminal.Editor.asmdef index 6727904..53017c1 100644 --- a/Editor/WallstopStudios.DxCommandTerminal.Editor.asmdef +++ b/Editor/WallstopStudios.DxCommandTerminal.Editor.asmdef @@ -1,19 +1,14 @@ { - "name": "WallstopStudios.DxCommandTerminal.Editor", - "rootNamespace": "WallstopStudios.DxCommandTerminal", - "references": [ - "WallstopStudios.DxCommandTerminal", - "Unity.InputSystem" - ], - "includePlatforms": [ - "Editor" - ], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false -} \ No newline at end of file + "name": "WallstopStudios.DxCommandTerminal.Editor", + "rootNamespace": "WallstopStudios.DxCommandTerminal", + "references": ["WallstopStudios.DxCommandTerminal", "Unity.InputSystem"], + "includePlatforms": ["Editor"], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Fonts/Anonymous_Pro/OFL.txt b/Fonts/Anonymous_Pro/OFL.txt index d89cf45..d62ff04 100644 --- a/Fonts/Anonymous_Pro/OFL.txt +++ b/Fonts/Anonymous_Pro/OFL.txt @@ -1,94 +1,94 @@ -Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com), -with Reserved Font Name Anonymous Pro. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com), +with Reserved Font Name Anonymous Pro. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Atkinson_Hyperlegible_Mono/OFL.txt b/Fonts/Atkinson_Hyperlegible_Mono/OFL.txt index de65832..63eb4ba 100644 --- a/Fonts/Atkinson_Hyperlegible_Mono/OFL.txt +++ b/Fonts/Atkinson_Hyperlegible_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Azeret_Mono/OFL.txt b/Fonts/Azeret_Mono/OFL.txt index c561cb1..736e22b 100644 --- a/Fonts/Azeret_Mono/OFL.txt +++ b/Fonts/Azeret_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2021 The Azeret Project Authors (https://github.com/displaay/azeret) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2021 The Azeret Project Authors (https://github.com/displaay/azeret) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/B612_Mono/OFL.txt b/Fonts/B612_Mono/OFL.txt index 0b5d530..1c257f7 100644 --- a/Fonts/B612_Mono/OFL.txt +++ b/Fonts/B612_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2012 The B612 Project Authors (https://github.com/polarsys/b612) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2012 The B612 Project Authors (https://github.com/polarsys/b612) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Barlow_Condensed/OFL.txt b/Fonts/Barlow_Condensed/OFL.txt index 500b734..175e823 100644 --- a/Fonts/Barlow_Condensed/OFL.txt +++ b/Fonts/Barlow_Condensed/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2017 The Barlow Project Authors (https://github.com/jpt/barlow) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2017 The Barlow Project Authors (https://github.com/jpt/barlow) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Courier_Prime/OFL.txt b/Fonts/Courier_Prime/OFL.txt index 7072510..28f87c1 100644 --- a/Fonts/Courier_Prime/OFL.txt +++ b/Fonts/Courier_Prime/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2015 The Courier Prime Project Authors (https://github.com/quoteunquoteapps/CourierPrime). - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2015 The Courier Prime Project Authors (https://github.com/quoteunquoteapps/CourierPrime). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Cousine/LICENSE.txt b/Fonts/Cousine/LICENSE.txt index 75b5248..d645695 100644 --- a/Fonts/Cousine/LICENSE.txt +++ b/Fonts/Cousine/LICENSE.txt @@ -1,202 +1,202 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Fonts/Cutive_Mono/OFL.txt b/Fonts/Cutive_Mono/OFL.txt index 835e350..ef10074 100644 --- a/Fonts/Cutive_Mono/OFL.txt +++ b/Fonts/Cutive_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2012 The Cutive Project Authors (https://github.com/googlefonts/cutivemono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2012 The Cutive Project Authors (https://github.com/googlefonts/cutivemono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/DM_Mono/OFL.txt b/Fonts/DM_Mono/OFL.txt index 14fde1b..5b17f0c 100644 --- a/Fonts/DM_Mono/OFL.txt +++ b/Fonts/DM_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2020 The DM Mono Project Authors (https://www.github.com/googlefonts/dm-mono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2020 The DM Mono Project Authors (https://www.github.com/googlefonts/dm-mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Fira_Code/OFL.txt b/Fonts/Fira_Code/OFL.txt index 0e38b88..b1540b0 100644 --- a/Fonts/Fira_Code/OFL.txt +++ b/Fonts/Fira_Code/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2014-2020 The Fira Code Project Authors (https://github.com/tonsky/FiraCode) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2014-2020 The Fira Code Project Authors (https://github.com/tonsky/FiraCode) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Fira_Mono/OFL.txt b/Fonts/Fira_Mono/OFL.txt index 8014188..029b036 100644 --- a/Fonts/Fira_Mono/OFL.txt +++ b/Fonts/Fira_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Fragment_Mono/OFL.txt b/Fonts/Fragment_Mono/OFL.txt index ac1fd0e..0a08c50 100644 --- a/Fonts/Fragment_Mono/OFL.txt +++ b/Fonts/Fragment_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2022 The Fragment-Mono Project Authors (https://github.com/weiweihuanghuang/fragment-mono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2022 The Fragment-Mono Project Authors (https://github.com/weiweihuanghuang/fragment-mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Geist_Mono/OFL.txt b/Fonts/Geist_Mono/OFL.txt index b48941f..679a685 100644 --- a/Fonts/Geist_Mono/OFL.txt +++ b/Fonts/Geist_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/IBM_Plex_Mono/OFL.txt b/Fonts/IBM_Plex_Mono/OFL.txt index 5bb330e..e423b74 100644 --- a/Fonts/IBM_Plex_Mono/OFL.txt +++ b/Fonts/IBM_Plex_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Inconsolata/OFL.txt b/Fonts/Inconsolata/OFL.txt index 55533e1..1089cbf 100644 --- a/Fonts/Inconsolata/OFL.txt +++ b/Fonts/Inconsolata/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2006 The Inconsolata Project Authors - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2006 The Inconsolata Project Authors + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/JetBrains_Mono/OFL.txt b/Fonts/JetBrains_Mono/OFL.txt index 3f34847..5ceee00 100644 --- a/Fonts/JetBrains_Mono/OFL.txt +++ b/Fonts/JetBrains_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Kode_Mono/OFL.txt b/Fonts/Kode_Mono/OFL.txt index 20c4909..2631492 100644 --- a/Fonts/Kode_Mono/OFL.txt +++ b/Fonts/Kode_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2023 The Kode Mono Project Authors (https://github.com/isaozler/kode-mono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2023 The Kode Mono Project Authors (https://github.com/isaozler/kode-mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/M_PLUS_1_Code/OFL.txt b/Fonts/M_PLUS_1_Code/OFL.txt index 05e42fb..c0131b3 100644 --- a/Fonts/M_PLUS_1_Code/OFL.txt +++ b/Fonts/M_PLUS_1_Code/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2021 The M+ FONTS Project Authors (https://github.com/coz-m/MPLUS_FONTS) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2021 The M+ FONTS Project Authors (https://github.com/coz-m/MPLUS_FONTS) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/M_PLUS_Code_Latin/OFL.txt b/Fonts/M_PLUS_Code_Latin/OFL.txt index 05e42fb..c0131b3 100644 --- a/Fonts/M_PLUS_Code_Latin/OFL.txt +++ b/Fonts/M_PLUS_Code_Latin/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2021 The M+ FONTS Project Authors (https://github.com/coz-m/MPLUS_FONTS) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2021 The M+ FONTS Project Authors (https://github.com/coz-m/MPLUS_FONTS) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Major_Mono_Display/OFL.txt b/Fonts/Major_Mono_Display/OFL.txt index 30961b9..d32c8d6 100644 --- a/Fonts/Major_Mono_Display/OFL.txt +++ b/Fonts/Major_Mono_Display/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2018 The Major Mono Project Authors (https://github.com/googlefonts/majormono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2018 The Major Mono Project Authors (https://github.com/googlefonts/majormono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Martian_Mono/OFL.txt b/Fonts/Martian_Mono/OFL.txt index adffdd1..e83393d 100644 --- a/Fonts/Martian_Mono/OFL.txt +++ b/Fonts/Martian_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2021 The Martian Mono Project Authors (https://github.com/evilmartians/mono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2021 The Martian Mono Project Authors (https://github.com/evilmartians/mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Nanum_Gothic_Coding/OFL.txt b/Fonts/Nanum_Gothic_Coding/OFL.txt index abbf68d..42085a7 100644 --- a/Fonts/Nanum_Gothic_Coding/OFL.txt +++ b/Fonts/Nanum_Gothic_Coding/OFL.txt @@ -1,96 +1,96 @@ -Copyright (c) 2010, NHN Corporation (http://www.nhncorp.com), -with Reserved Font Name Nanum, Naver Nanum, NanumGothic, Naver -NanumGothic, NanumMyeongjo, Naver NanumMyeongjo, NanumBrush, Naver -NanumBrush, NanumPen, Naver NanumPen. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2010, NHN Corporation (http://www.nhncorp.com), +with Reserved Font Name Nanum, Naver Nanum, NanumGothic, Naver +NanumGothic, NanumMyeongjo, Naver NanumMyeongjo, NanumBrush, Naver +NanumBrush, NanumPen, Naver NanumPen. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Noto_Sans_Mono/OFL.txt b/Fonts/Noto_Sans_Mono/OFL.txt index 09f020b..0373e14 100644 --- a/Fonts/Noto_Sans_Mono/OFL.txt +++ b/Fonts/Noto_Sans_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Nova_Mono/OFL.txt b/Fonts/Nova_Mono/OFL.txt index 15d4aa3..0522b35 100644 --- a/Fonts/Nova_Mono/OFL.txt +++ b/Fonts/Nova_Mono/OFL.txt @@ -1,94 +1,94 @@ -Copyright (c) 2011, wmk69 (wmk69@o2.pl), -with Reserved Font Name NovaMono. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2011, wmk69 (wmk69@o2.pl), +with Reserved Font Name NovaMono. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Overpass_Mono/OFL.txt b/Fonts/Overpass_Mono/OFL.txt index 40284ee..b875eef 100644 --- a/Fonts/Overpass_Mono/OFL.txt +++ b/Fonts/Overpass_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2021 The Overpass Project Authors (https://github.com/RedHatOfficial/Overpass) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2021 The Overpass Project Authors (https://github.com/RedHatOfficial/Overpass) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Oxygen_Mono/OFL.txt b/Fonts/Oxygen_Mono/OFL.txt index 5ab613f..fec3ac5 100644 --- a/Fonts/Oxygen_Mono/OFL.txt +++ b/Fonts/Oxygen_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright (c) 2012, vernon adams (vern@newtypography.co.uk), with Reserved Font Names 'Oxygen' - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2012, vernon adams (vern@newtypography.co.uk), with Reserved Font Names 'Oxygen' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/PT_Mono/OFL.txt b/Fonts/PT_Mono/OFL.txt index e2939d9..ff1880c 100644 --- a/Fonts/PT_Mono/OFL.txt +++ b/Fonts/PT_Mono/OFL.txt @@ -1,94 +1,94 @@ -Copyright (c) 2011, ParaType Ltd. (http://www.paratype.com/public), -with Reserved Font Names "PT Sans", "PT Serif", "PT Mono" and "ParaType". - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2011, ParaType Ltd. (http://www.paratype.com/public), +with Reserved Font Names "PT Sans", "PT Serif", "PT Mono" and "ParaType". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Press_Start_2P/OFL.txt b/Fonts/Press_Start_2P/OFL.txt index 70041e1..ea3079c 100644 --- a/Fonts/Press_Start_2P/OFL.txt +++ b/Fonts/Press_Start_2P/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P". - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Red_Hat_Mono/OFL.txt b/Fonts/Red_Hat_Mono/OFL.txt index 1856021..16cf394 100644 --- a/Fonts/Red_Hat_Mono/OFL.txt +++ b/Fonts/Red_Hat_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Roboto_Condensed/OFL.txt b/Fonts/Roboto_Condensed/OFL.txt index a417551..9c48e05 100644 --- a/Fonts/Roboto_Condensed/OFL.txt +++ b/Fonts/Roboto_Condensed/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Roboto_Mono/LICENSE.txt b/Fonts/Roboto_Mono/LICENSE.txt index 75b5248..d645695 100644 --- a/Fonts/Roboto_Mono/LICENSE.txt +++ b/Fonts/Roboto_Mono/LICENSE.txt @@ -1,202 +1,202 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Fonts/Rubik_Mono_One/OFL.txt b/Fonts/Rubik_Mono_One/OFL.txt index f6684df..cb5ac7d 100644 --- a/Fonts/Rubik_Mono_One/OFL.txt +++ b/Fonts/Rubik_Mono_One/OFL.txt @@ -1,93 +1,93 @@ -Copyright (c) 2013, 2014, Hubert and Fischer, Philipp Hubert (philipp@hubertfischer.com), Sebastian Fischer (sebastian@hubertfischer.com) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2013, 2014, Hubert and Fischer, Philipp Hubert (philipp@hubertfischer.com), Sebastian Fischer (sebastian@hubertfischer.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Share_Tech_Mono/OFL.txt b/Fonts/Share_Tech_Mono/OFL.txt index db759a6..1c3c287 100644 --- a/Fonts/Share_Tech_Mono/OFL.txt +++ b/Fonts/Share_Tech_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright (c) 2012, Carrois Type Design, Ralph du Carrois (post@carrois.com www.carrois.com), with Reserved Font Name 'Share' - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2012, Carrois Type Design, Ralph du Carrois (post@carrois.com www.carrois.com), with Reserved Font Name 'Share' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Silkscreen/OFL.txt b/Fonts/Silkscreen/OFL.txt index bf7d153..a1fe7d5 100644 --- a/Fonts/Silkscreen/OFL.txt +++ b/Fonts/Silkscreen/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Sono/OFL.txt b/Fonts/Sono/OFL.txt index 8ff5120..199edaf 100644 --- a/Fonts/Sono/OFL.txt +++ b/Fonts/Sono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2020 The Sono Project Authors (https://github.com/sursly/sono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2020 The Sono Project Authors (https://github.com/sursly/sono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Source_Code_Pro/OFL.txt b/Fonts/Source_Code_Pro/OFL.txt index 54da4a4..653ff7d 100644 --- a/Fonts/Source_Code_Pro/OFL.txt +++ b/Fonts/Source_Code_Pro/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Space_Mono/OFL.txt b/Fonts/Space_Mono/OFL.txt index 2c5ae56..2805642 100644 --- a/Fonts/Space_Mono/OFL.txt +++ b/Fonts/Space_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Syne_Mono/OFL.txt b/Fonts/Syne_Mono/OFL.txt index 02fcbaf..146e2e0 100644 --- a/Fonts/Syne_Mono/OFL.txt +++ b/Fonts/Syne_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2017 The Syne Project Authors (https://gitlab.com/bonjour-monde/fonderie/syne-typeface) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2017 The Syne Project Authors (https://gitlab.com/bonjour-monde/fonderie/syne-typeface) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Ubuntu_Mono/UFL.txt b/Fonts/Ubuntu_Mono/UFL.txt index 6e722c8..ae78a8f 100644 --- a/Fonts/Ubuntu_Mono/UFL.txt +++ b/Fonts/Ubuntu_Mono/UFL.txt @@ -1,96 +1,96 @@ -------------------------------- -UBUNTU FONT LICENCE Version 1.0 -------------------------------- - -PREAMBLE -This licence allows the licensed fonts to be used, studied, modified and -redistributed freely. The fonts, including any derivative works, can be -bundled, embedded, and redistributed provided the terms of this licence -are met. The fonts and derivatives, however, cannot be released under -any other licence. The requirement for fonts to remain under this -licence does not require any document created using the fonts or their -derivatives to be published under this licence, as long as the primary -purpose of the document is not to be a vehicle for the distribution of -the fonts. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this licence and clearly marked as such. This may -include source files, build scripts and documentation. - -"Original Version" refers to the collection of Font Software components -as received under this licence. - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to -a new environment. - -"Copyright Holder(s)" refers to all individuals and companies who have a -copyright ownership of the Font Software. - -"Substantially Changed" refers to Modified Versions which can be easily -identified as dissimilar to the Font Software by users of the Font -Software comparing the Original Version with the Modified Version. - -To "Propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification and with or without charging -a redistribution fee), making available to the public, and in some -countries other activities as well. - -PERMISSION & CONDITIONS -This licence does not grant any rights under trademark law and all such -rights are reserved. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of the Font Software, to propagate the Font Software, subject to -the below conditions: - -1) Each copy of the Font Software must contain the above copyright -notice and this licence. These can be included either as stand-alone -text files, human-readable headers or in the appropriate machine- -readable metadata fields within text or binary files as long as those -fields can be easily viewed by the user. - -2) The font name complies with the following: -(a) The Original Version must retain its name, unmodified. -(b) Modified Versions which are Substantially Changed must be renamed to -avoid use of the name of the Original Version or similar names entirely. -(c) Modified Versions which are not Substantially Changed must be -renamed to both (i) retain the name of the Original Version and (ii) add -additional naming elements to distinguish the Modified Version from the -Original Version. The name of such Modified Versions must be the name of -the Original Version, with "derivative X" where X represents the name of -the new work, appended to that name. - -3) The name(s) of the Copyright Holder(s) and any contributor to the -Font Software shall not be used to promote, endorse or advertise any -Modified Version, except (i) as required by this licence, (ii) to -acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with -their explicit written permission. - -4) The Font Software, modified or unmodified, in part or in whole, must -be distributed entirely under this licence, and must not be distributed -under any other licence. The requirement for fonts to remain under this -licence does not affect any document created using the Font Software, -except any version of the Font Software extracted from a document -created using the Font Software may only be distributed under this -licence. - -TERMINATION -This licence becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF -COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER -DEALINGS IN THE FONT SOFTWARE. +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/VT323/OFL.txt b/Fonts/VT323/OFL.txt index e229272..2011c93 100644 --- a/Fonts/VT323/OFL.txt +++ b/Fonts/VT323/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2011, The VT323 Project Authors (peter.hull@oikoi.com) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2011, The VT323 Project Authors (peter.hull@oikoi.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Varela_Round/OFL.txt b/Fonts/Varela_Round/OFL.txt index cca0a98..a2dc7ea 100644 --- a/Fonts/Varela_Round/OFL.txt +++ b/Fonts/Varela_Round/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2023 The Varela Round Project Authors (https://github.com/alefalefalef/Varela-Round-Hebrew/), with Reserved Font Names 'Varela' and ‘Varela Round’. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2023 The Varela Round Project Authors (https://github.com/alefalefalef/Varela-Round-Hebrew/), with Reserved Font Names 'Varela' and ‘Varela Round’. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Fonts/Xanh_Mono/OFL.txt b/Fonts/Xanh_Mono/OFL.txt index 06202ab..0766b08 100644 --- a/Fonts/Xanh_Mono/OFL.txt +++ b/Fonts/Xanh_Mono/OFL.txt @@ -1,93 +1,93 @@ -Copyright 2020 The XanhMono Project Authors (https://github.com/yellow-type-foundry/xanhmono). - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -https://openfontlicense.org - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright 2020 The XanhMono Project Authors (https://github.com/yellow-type-foundry/xanhmono). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/README.md b/README.md index 3ddf95e..b5a8de1 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,7 @@ Static members parsing DxCommandTerminal exposes a runtime mode enum to control environment-specific behavior (e.g., parser auto-discovery), plus an Editor toggle wired through the Terminal component. - Enum: `TerminalRuntimeModeFlags` (flags, explicit numeric values) + - `None` (0) — Obsolete; choose explicit modes. - `Editor` (1) — Enable editor-only features (when running in Editor). - `Development` (2) — Enable features only for development builds. @@ -366,10 +367,12 @@ DxCommandTerminal exposes a runtime mode enum to control environment-specific be - `All` (7) — Enable all. - Set mode on `TerminalUI` (serialized): + - `Runtime Mode` controls active modes. - `Editor > Auto-Discover Parsers` toggles automatic parser discovery in Editor when `Editor` mode is active. - Editor Menu: + - Tools > DxCommandTerminal > Runtime Mode > [Editor | Development | Production | Editor+Development | All] - Tools > DxCommandTerminal > Runtime Mode > Toggle Auto-Discover Parsers - Acts on selected `TerminalUI` components (in the Hierarchy). diff --git a/Runtime/Attributes/DxShowIfAttribute.cs b/Runtime/Attributes/DxShowIfAttribute.cs index 0d00ab6..db0143a 100644 --- a/Runtime/Attributes/DxShowIfAttribute.cs +++ b/Runtime/Attributes/DxShowIfAttribute.cs @@ -1,16 +1,16 @@ -namespace WallstopStudios.DxCommandTerminal.Attributes -{ - using UnityEngine; - - public sealed class DxShowIfAttribute : PropertyAttribute - { - public readonly string conditionField; - public readonly bool inverse; - - public DxShowIfAttribute(string conditionField, bool inverse = false) - { - this.conditionField = conditionField; - this.inverse = inverse; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Attributes +{ + using UnityEngine; + + public sealed class DxShowIfAttribute : PropertyAttribute + { + public readonly string conditionField; + public readonly bool inverse; + + public DxShowIfAttribute(string conditionField, bool inverse = false) + { + this.conditionField = conditionField; + this.inverse = inverse; + } + } +} diff --git a/Runtime/Attributes/RegisterCommandAttribute.cs b/Runtime/Attributes/RegisterCommandAttribute.cs index b9ace52..1e778b6 100644 --- a/Runtime/Attributes/RegisterCommandAttribute.cs +++ b/Runtime/Attributes/RegisterCommandAttribute.cs @@ -1,56 +1,56 @@ -namespace WallstopStudios.DxCommandTerminal.Attributes -{ - using System; - using System.Reflection; - - [AttributeUsage(AttributeTargets.Method)] - public sealed class RegisterCommandAttribute : Attribute - { - public string Name { get; set; } - public int MinArgCount { get; set; } = 0; - public int MaxArgCount { get; set; } = -1; - public string Help { get; set; } - public string Hint { get; set; } - - // Should not be used by client code - internal flag to indicate that this is a "Default", or in-built command - internal bool Default { get; set; } - - public bool EditorOnly { get; set; } - - public bool DevelopmentOnly { get; set; } - - public RegisterCommandAttribute(string commandName = null) - { - commandName = commandName?.Replace(" ", string.Empty).Trim(); - Name = commandName; - } - - internal RegisterCommandAttribute(bool isDefault) - : this(string.Empty) - { - Default = isDefault; - } - - public void NormalizeName(MethodInfo method) - { - if (string.IsNullOrWhiteSpace(Name)) - { - Name = InferCommandName(method.Name); - } - - Name = Name.Replace(" ", string.Empty).Trim(); - } - - private static string InferCommandName(string methodName) - { - const string commandId = "COMMAND"; - int index = methodName.IndexOf(commandId, StringComparison.OrdinalIgnoreCase); - - // Method is prefixed, suffixed with, or contains "COMMAND". - string commandName = - 0 <= index ? methodName.Remove(index, commandId.Length) : methodName; - - return commandName; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Attributes +{ + using System; + using System.Reflection; + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RegisterCommandAttribute : Attribute + { + public string Name { get; set; } + public int MinArgCount { get; set; } = 0; + public int MaxArgCount { get; set; } = -1; + public string Help { get; set; } + public string Hint { get; set; } + + // Should not be used by client code - internal flag to indicate that this is a "Default", or in-built command + internal bool Default { get; set; } + + public bool EditorOnly { get; set; } + + public bool DevelopmentOnly { get; set; } + + public RegisterCommandAttribute(string commandName = null) + { + commandName = commandName?.Replace(" ", string.Empty).Trim(); + Name = commandName; + } + + internal RegisterCommandAttribute(bool isDefault) + : this(string.Empty) + { + Default = isDefault; + } + + public void NormalizeName(MethodInfo method) + { + if (string.IsNullOrWhiteSpace(Name)) + { + Name = InferCommandName(method.Name); + } + + Name = Name.Replace(" ", string.Empty).Trim(); + } + + private static string InferCommandName(string methodName) + { + const string commandId = "COMMAND"; + int index = methodName.IndexOf(commandId, StringComparison.OrdinalIgnoreCase); + + // Method is prefixed, suffixed with, or contains "COMMAND". + string commandName = + 0 <= index ? methodName.Remove(index, commandId.Length) : methodName; + + return commandName; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index 4d44c79..cd7c2dd 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -1,719 +1,719 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Text; - using Attributes; - using Themes; - using UI; - using UnityEngine; - - // ReSharper disable once UnusedType.Global - public static class BuiltInCommands - { - private const string BulkSeparator = " "; - - private static readonly StringBuilder StringBuilder = new(); - - [RegisterCommand( - isDefault: true, - Name = "list-themes", - Help = "Lists all currently available themes", - MaxArgCount = 0 - )] - public static void CommandListThemes(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - if (terminal._themePack == null) - { - Terminal.Log(TerminalLogType.Warning, "No theme pack found."); - return; - } - - string themes = string.Join( - BulkSeparator, - terminal._themePack._themeNames.Select(ThemeNameHelper.GetFriendlyThemeName) - ); - Terminal.Log(TerminalLogType.Message, themes); - } - - [RegisterCommand( - isDefault: true, - Name = "list-fonts", - Help = "Lists all currently available fonts", - MaxArgCount = 0 - )] - public static void CommandListFonts(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - if (terminal._fontPack == null) - { - Terminal.Log(TerminalLogType.Warning, "No font pack found."); - return; - } - - string themes = string.Join( - BulkSeparator, - terminal._fontPack._fonts.Select(font => font.name) - ); - Terminal.Log(TerminalLogType.Message, themes); - } - - [RegisterCommand( - isDefault: true, - Name = "set-theme", - Help = "Sets the current Terminal UI theme", - MinArgCount = 1, - MaxArgCount = 1, - Hint = "set-theme " - )] - [CommandCompleter(typeof(Completers.ThemeArgumentCompleter))] - public static void CommandSetTheme(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - string theme = args[0].contents; - string friendlyThemeName = ThemeNameHelper.GetFriendlyThemeName(theme); - - if ( - string.Equals( - friendlyThemeName, - terminal.CurrentFriendlyTheme, - StringComparison.OrdinalIgnoreCase - ) - ) - { - Terminal.Log(TerminalLogType.Message, $"Theme '{theme}' is already set."); - return; - } - - int newThemeIndex = -1; - foreach (string themeName in ThemeNameHelper.GetPossibleThemeNames(theme)) - { - newThemeIndex = terminal._themePack._themeNames.FindIndex(existingTheme => - string.Equals(existingTheme, themeName, StringComparison.OrdinalIgnoreCase) - ); - if (0 <= newThemeIndex) - { - break; - } - } - - if (newThemeIndex < 0) - { - Terminal.Log(TerminalLogType.Warning, $"Theme '{theme}' not found."); - return; - } - - terminal.SetTheme(theme); - Terminal.Log(TerminalLogType.Message, $"Theme '{theme}' set."); - } - - [RegisterCommand( - isDefault: true, - Name = "set-font", - Help = "Sets the current Terminal UI font", - MinArgCount = 1, - MaxArgCount = 1, - Hint = "set-font " - )] - [CommandCompleter(typeof(Completers.FontArgumentCompleter))] - public static void CommandSetFont(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - if (terminal._fontPack == null) - { - Terminal.Log(TerminalLogType.Warning, "No font pack found."); - return; - } - - string fontName = args[0].contents ?? string.Empty; - if (string.IsNullOrWhiteSpace(fontName)) - { - Terminal.Log(TerminalLogType.Warning, "Invalid font name."); - return; - } - - UnityEngine.Font font = terminal._fontPack._fonts.FirstOrDefault(f => - f != null && string.Equals(f.name, fontName, StringComparison.OrdinalIgnoreCase) - ); - - if (font == null) - { - Terminal.Log(TerminalLogType.Warning, $"Font '{fontName}' not found."); - return; - } - - terminal.SetFont(font); - Terminal.Log(TerminalLogType.Message, $"Font '{font.name}' set."); - } - - [RegisterCommand( - isDefault: true, - Name = "get-theme", - Help = "Gets the current Terminal UI theme" - )] - public static void CommandGetTheme(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - Terminal.Log( - TerminalLogType.Message, - $"Current terminal theme is '{terminal.CurrentFriendlyTheme}'." - ); - } - - [RegisterCommand( - isDefault: true, - Name = "get-font", - Help = "Gets the current Terminal UI font" - )] - public static void CommandGetFont(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - Font currentFont = terminal.CurrentFont; - Terminal.Log( - TerminalLogType.Message, - $"Current terminal font is '{(currentFont == null ? "null" : currentFont.name)}'." - ); - } - - [RegisterCommand( - isDefault: true, - Name = "set-random-theme", - Help = "Sets the current Terminal UI theme to a randomly selected one", - MinArgCount = 0, - MaxArgCount = 0 - )] - public static void CommandSetRandomTheme(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - string newTheme = terminal.SetRandomTheme(); - Terminal.Log( - TerminalLogType.Message, - $"Randomly selected and set theme to '{newTheme}'." - ); - } - - // [RegisterCommand( - // isDefault: true, - // Name = "set-font", - // Help = "Sets the current Terminal UI font", - // MinArgCount = 1, - // MaxArgCount = 1 - // )] - // public static void CommandSetFont(CommandArg[] args) - // { - // TerminalUI terminal = TerminalUI.Instance; - // if (terminal == null) - // { - // Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - // return; - // } - // - // if (terminal._fontPack == null) - // { - // Terminal.Log(TerminalLogType.Warning, "No font pack found."); - // return; - // } - // - // string fontName = args[0].contents; - // - // int newFontIndex = terminal._fontPack._fonts.FindIndex(font => - // string.Equals(font.name, fontName, StringComparison.OrdinalIgnoreCase) - // ); - // if (newFontIndex < 0) - // { - // Terminal.Log(TerminalLogType.Warning, $"Font '{fontName}' not found."); - // return; - // } - // - // Font font = terminal._fontPack._fonts[newFontIndex]; - // - // terminal.SetFont(font); - // Terminal.Log(TerminalLogType.Message, $"Font '{font.name}' set."); - // } - - [RegisterCommand( - isDefault: true, - Name = "set-random-font", - Help = "Sets the current Terminal UI font to a randomly selected one", - MinArgCount = 0, - MaxArgCount = 0 - )] - public static void CommandSetRandomFont(CommandArg[] args) - { - TerminalUI terminal = TerminalUI.Instance; - if (terminal == null) - { - Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); - return; - } - - Font font = terminal.SetRandomFont(); - Terminal.Log( - TerminalLogType.Message, - $"Randomly selected and set font to '{font.name}'." - ); - } - - [RegisterCommand( - isDefault: true, - Name = "clear-console", - Help = "Clear the command console", - MaxArgCount = 0 - )] - public static void CommandClearConsole(CommandArg[] args) - { - Terminal.Buffer?.Clear(); - } - - [RegisterCommand( - isDefault: true, - Name = "clear-history", - Help = "Clear the command console's history", - MaxArgCount = 0 - )] - public static void CommandClearHistory(CommandArg[] args) - { - Terminal.History?.Clear(); - } - - [RegisterCommand( - isDefault: true, - Name = "help", - Help = "Display help information about a command", - MaxArgCount = 1 - )] - public static void CommandHelp(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - if (args.Length == 0) - { - foreach (KeyValuePair command in shell.Commands) - { - string name = command.Key; - CommandInfo info = command.Value; - string usage = BuildUsage(name, info.minArgCount, info.maxArgCount, info.hint); - string helpLine = - $"{name.ToUpperInvariant(), -16}: {info.help}\n -> {usage}"; - Terminal.Log(helpLine); - UnityEngine.Debug.Log(helpLine); - } - return; - } - else - { - string commandName = args[0].contents ?? string.Empty; - - if (!shell.Commands.TryGetValue(commandName, out CommandInfo info)) - { - shell.IssueErrorMessage($"Command {commandName} could not be found."); - return; - } - - string usageLine = BuildUsage( - commandName, - info.minArgCount, - info.maxArgCount, - info.hint - ); - if (string.IsNullOrWhiteSpace(info.help)) - { - string line = $"{commandName}: {usageLine}"; - Terminal.Log(line); - UnityEngine.Debug.Log(line); - } - else - { - string line = $"{info.help}\n{usageLine}"; - Terminal.Log(line); - UnityEngine.Debug.Log(line); - } - } - } - - private static string BuildUsage(string name, int minArgs, int maxArgs, string hint) - { - if (!string.IsNullOrWhiteSpace(hint)) - { - return $"Usage: {hint}"; - } - - StringBuilder.Clear(); - StringBuilder.Append("Usage: "); - StringBuilder.Append(name); - if (minArgs <= 0 && (maxArgs == 0 || maxArgs < 0)) - { - return StringBuilder.ToString(); - } - - int max = maxArgs < 0 ? minArgs : Math.Max(minArgs, maxArgs); - for (int i = 1; i <= minArgs; ++i) - { - StringBuilder.Append(' '); - StringBuilder.Append('<'); - StringBuilder.Append("arg"); - StringBuilder.Append(i); - StringBuilder.Append('>'); - } - for (int i = minArgs + 1; i <= max; ++i) - { - StringBuilder.Append(' '); - StringBuilder.Append('['); - StringBuilder.Append("arg"); - StringBuilder.Append(i); - StringBuilder.Append(']'); - } - if (maxArgs < 0) - { - StringBuilder.Append(' '); - StringBuilder.Append("[args...]"); - } - - return StringBuilder.ToString(); - } - - [RegisterCommand( - isDefault: true, - Name = "time", - Help = "Time the execution of a command", - MinArgCount = 1 - )] - public static void CommandTime(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - Stopwatch sw = Stopwatch.StartNew(); - shell.RunCommand(JoinArguments(args)); - sw.Stop(); - Terminal.Log($"Time: {sw.ElapsedMilliseconds}ms"); - } - - [RegisterCommand( - isDefault: true, - Name = "time-scale", - Help = "Sets Time.timeScale", - MinArgCount = 1, - MaxArgCount = 1 - )] - public static void CommandTimeScale(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - CommandArg arg = args[0]; - if (!arg.TryGet(out float timeScale)) - { - Terminal.Log(TerminalLogType.Warning, $"Invalid time scale {arg}."); - return; - } - - Time.timeScale = timeScale; - } - - [RegisterCommand( - isDefault: true, - Name = "log-terminal", - Help = "Output message via Terminal.Log" - )] - public static void CommandLogTerminal(CommandArg[] args) - { - Terminal.Log(JoinArguments(args)); - } - - [RegisterCommand(isDefault: true, Name = "log", Help = "Output message via Debug.Log")] - public static void CommandLog(CommandArg[] args) - { - UnityEngine.Debug.Log(JoinArguments(args)); - } - - [RegisterCommand( - isDefault: true, - Name = "trace", - Help = "Output the stack trace of the previous message", - MaxArgCount = 0 - )] - public static void CommandTrace(CommandArg[] args) - { - CommandLog buffer = Terminal.Buffer; - if (buffer == null) - { - return; - } - - int logCount = buffer.Logs.Count; - - if (logCount - 2 < 0) - { - Terminal.Log(TerminalLogType.Warning, "Nothing to trace."); - return; - } - - LogItem logItem = buffer.Logs[logCount - 2]; - - if (string.IsNullOrWhiteSpace(logItem.stackTrace)) - { - Terminal.Log( - logItem.message.EndsWith(" (no trace)", StringComparison.OrdinalIgnoreCase) - ? logItem.message - : $"{logItem.message} (no trace)" - ); - } - else - { - Terminal.Log(logItem.stackTrace); - } - } - - [RegisterCommand( - isDefault: true, - Name = "clear-variable", - Help = "Clears a variable value", - MinArgCount = 1, - MaxArgCount = 1 - )] - public static void CommandClearVariable(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - string variableName = args[0].contents; - bool cleared = shell.ClearVariable(variableName); - if (cleared) - { - Terminal.Log($"Variable '{variableName}' cleared successfully."); - } - else - { - Terminal.Log( - TerminalLogType.Warning, - $"Warning: Variable '{variableName}' not found." - ); - } - } - - [RegisterCommand( - isDefault: true, - Name = "clear-all-variables", - Help = "Clears all variable values", - MinArgCount = 0, - MaxArgCount = 0 - )] - public static void CommandClearAllVariable(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - int variableCount = shell.Variables.Count; - foreach (string variable in shell.Variables.Keys.ToArray()) - { - shell.ClearVariable(variable); - } - - Terminal.Log( - variableCount == 0 - ? "No variables found - nothing to clear." - : $"Cleared {variableCount} variables." - ); - } - - [RegisterCommand( - isDefault: true, - Name = "set-variable", - Help = "Sets a variable value", - MinArgCount = 2, - MaxArgCount = 2 - )] - public static void CommandSetVariable(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - string variableName = args[0].contents; - - if (string.IsNullOrWhiteSpace(variableName) || variableName.StartsWith('$')) - { - Terminal.Log( - TerminalLogType.Warning, - $"Warning: Possibly invalid variable name '{variableName}'." - ); - } - - string variableValue = JoinArguments(args, 1); - bool set = shell.SetVariable(variableName, variableValue); - if (set) - { - Terminal.Log($"Variable '{variableName}' set to '{variableValue}' successfully."); - } - else if (shell.Variables.TryGetValue(variableName, out CommandArg existingVariable)) - { - Terminal.Log( - TerminalLogType.Warning, - $"Variable '{variableName}' failed to set. Existing value: {existingVariable}." - ); - } - else - { - Terminal.Log( - TerminalLogType.Warning, - $"Variable '{variableName}' failed to set. No existing value found." - ); - } - } - - [RegisterCommand( - isDefault: true, - Name = "get-variable", - Help = "Gets a variable value", - MinArgCount = 1, - MaxArgCount = 1 - )] - public static void CommandGetVariable(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - string variableName = args[0].contents; - - if (shell.Variables.TryGetValue(variableName, out CommandArg variable)) - { - Terminal.Log($"Variable '{variableName}' is set to '{variable}'."); - } - else - { - Terminal.Log(TerminalLogType.Warning, $"Variable '{variableName}' not found."); - } - } - - [RegisterCommand( - isDefault: true, - Name = "list-variables", - Help = "Gets all variables and their associated values", - MinArgCount = 0, - MaxArgCount = 0 - )] - public static void CommandGetAllVariables(CommandArg[] args) - { - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - if (!shell.Variables.Any()) - { - Terminal.Log(TerminalLogType.Warning, "No variables found."); - return; - } - - foreach (KeyValuePair entry in shell.Variables) - { - Terminal.Log($"Variable '{entry.Key}' is set to '{entry.Value}'."); - } - } - - [RegisterCommand(isDefault: true, Name = "no-op", Help = "No operation")] - public static void CommandNoOperation(CommandArg[] args) - { - // No-op - } - - [RegisterCommand( - isDefault: true, - Name = "quit", - Help = "Quit running application", - MaxArgCount = 0 - )] - public static void CommandQuit(CommandArg[] args) - { -#if UNITY_EDITOR - UnityEditor.EditorApplication.isPlaying = false; -#else - UnityEngine.Application.Quit(); -#endif - } - - private static string JoinArguments(CommandArg[] args, int start = 0) - { - StringBuilder.Clear(); - for (int i = start; i < args.Length; i++) - { - StringBuilder.Append(args[i].contents); - - if (i < args.Length - 1) - { - StringBuilder.Append(' '); - } - } - - return StringBuilder.ToString(); - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using Attributes; + using Themes; + using UI; + using UnityEngine; + + // ReSharper disable once UnusedType.Global + public static class BuiltInCommands + { + private const string BulkSeparator = " "; + + private static readonly StringBuilder StringBuilder = new(); + + [RegisterCommand( + isDefault: true, + Name = "list-themes", + Help = "Lists all currently available themes", + MaxArgCount = 0 + )] + public static void CommandListThemes(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + if (terminal._themePack == null) + { + Terminal.Log(TerminalLogType.Warning, "No theme pack found."); + return; + } + + string themes = string.Join( + BulkSeparator, + terminal._themePack._themeNames.Select(ThemeNameHelper.GetFriendlyThemeName) + ); + Terminal.Log(TerminalLogType.Message, themes); + } + + [RegisterCommand( + isDefault: true, + Name = "list-fonts", + Help = "Lists all currently available fonts", + MaxArgCount = 0 + )] + public static void CommandListFonts(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + if (terminal._fontPack == null) + { + Terminal.Log(TerminalLogType.Warning, "No font pack found."); + return; + } + + string themes = string.Join( + BulkSeparator, + terminal._fontPack._fonts.Select(font => font.name) + ); + Terminal.Log(TerminalLogType.Message, themes); + } + + [RegisterCommand( + isDefault: true, + Name = "set-theme", + Help = "Sets the current Terminal UI theme", + MinArgCount = 1, + MaxArgCount = 1, + Hint = "set-theme " + )] + [CommandCompleter(typeof(Completers.ThemeArgumentCompleter))] + public static void CommandSetTheme(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + string theme = args[0].contents; + string friendlyThemeName = ThemeNameHelper.GetFriendlyThemeName(theme); + + if ( + string.Equals( + friendlyThemeName, + terminal.CurrentFriendlyTheme, + StringComparison.OrdinalIgnoreCase + ) + ) + { + Terminal.Log(TerminalLogType.Message, $"Theme '{theme}' is already set."); + return; + } + + int newThemeIndex = -1; + foreach (string themeName in ThemeNameHelper.GetPossibleThemeNames(theme)) + { + newThemeIndex = terminal._themePack._themeNames.FindIndex(existingTheme => + string.Equals(existingTheme, themeName, StringComparison.OrdinalIgnoreCase) + ); + if (0 <= newThemeIndex) + { + break; + } + } + + if (newThemeIndex < 0) + { + Terminal.Log(TerminalLogType.Warning, $"Theme '{theme}' not found."); + return; + } + + terminal.SetTheme(theme); + Terminal.Log(TerminalLogType.Message, $"Theme '{theme}' set."); + } + + [RegisterCommand( + isDefault: true, + Name = "set-font", + Help = "Sets the current Terminal UI font", + MinArgCount = 1, + MaxArgCount = 1, + Hint = "set-font " + )] + [CommandCompleter(typeof(Completers.FontArgumentCompleter))] + public static void CommandSetFont(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + if (terminal._fontPack == null) + { + Terminal.Log(TerminalLogType.Warning, "No font pack found."); + return; + } + + string fontName = args[0].contents ?? string.Empty; + if (string.IsNullOrWhiteSpace(fontName)) + { + Terminal.Log(TerminalLogType.Warning, "Invalid font name."); + return; + } + + UnityEngine.Font font = terminal._fontPack._fonts.FirstOrDefault(f => + f != null && string.Equals(f.name, fontName, StringComparison.OrdinalIgnoreCase) + ); + + if (font == null) + { + Terminal.Log(TerminalLogType.Warning, $"Font '{fontName}' not found."); + return; + } + + terminal.SetFont(font); + Terminal.Log(TerminalLogType.Message, $"Font '{font.name}' set."); + } + + [RegisterCommand( + isDefault: true, + Name = "get-theme", + Help = "Gets the current Terminal UI theme" + )] + public static void CommandGetTheme(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + Terminal.Log( + TerminalLogType.Message, + $"Current terminal theme is '{terminal.CurrentFriendlyTheme}'." + ); + } + + [RegisterCommand( + isDefault: true, + Name = "get-font", + Help = "Gets the current Terminal UI font" + )] + public static void CommandGetFont(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + Font currentFont = terminal.CurrentFont; + Terminal.Log( + TerminalLogType.Message, + $"Current terminal font is '{(currentFont == null ? "null" : currentFont.name)}'." + ); + } + + [RegisterCommand( + isDefault: true, + Name = "set-random-theme", + Help = "Sets the current Terminal UI theme to a randomly selected one", + MinArgCount = 0, + MaxArgCount = 0 + )] + public static void CommandSetRandomTheme(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + string newTheme = terminal.SetRandomTheme(); + Terminal.Log( + TerminalLogType.Message, + $"Randomly selected and set theme to '{newTheme}'." + ); + } + + // [RegisterCommand( + // isDefault: true, + // Name = "set-font", + // Help = "Sets the current Terminal UI font", + // MinArgCount = 1, + // MaxArgCount = 1 + // )] + // public static void CommandSetFont(CommandArg[] args) + // { + // TerminalUI terminal = TerminalUI.Instance; + // if (terminal == null) + // { + // Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + // return; + // } + // + // if (terminal._fontPack == null) + // { + // Terminal.Log(TerminalLogType.Warning, "No font pack found."); + // return; + // } + // + // string fontName = args[0].contents; + // + // int newFontIndex = terminal._fontPack._fonts.FindIndex(font => + // string.Equals(font.name, fontName, StringComparison.OrdinalIgnoreCase) + // ); + // if (newFontIndex < 0) + // { + // Terminal.Log(TerminalLogType.Warning, $"Font '{fontName}' not found."); + // return; + // } + // + // Font font = terminal._fontPack._fonts[newFontIndex]; + // + // terminal.SetFont(font); + // Terminal.Log(TerminalLogType.Message, $"Font '{font.name}' set."); + // } + + [RegisterCommand( + isDefault: true, + Name = "set-random-font", + Help = "Sets the current Terminal UI font to a randomly selected one", + MinArgCount = 0, + MaxArgCount = 0 + )] + public static void CommandSetRandomFont(CommandArg[] args) + { + TerminalUI terminal = TerminalUI.Instance; + if (terminal == null) + { + Terminal.Log(TerminalLogType.Warning, "No Terminal UI found."); + return; + } + + Font font = terminal.SetRandomFont(); + Terminal.Log( + TerminalLogType.Message, + $"Randomly selected and set font to '{font.name}'." + ); + } + + [RegisterCommand( + isDefault: true, + Name = "clear-console", + Help = "Clear the command console", + MaxArgCount = 0 + )] + public static void CommandClearConsole(CommandArg[] args) + { + Terminal.Buffer?.Clear(); + } + + [RegisterCommand( + isDefault: true, + Name = "clear-history", + Help = "Clear the command console's history", + MaxArgCount = 0 + )] + public static void CommandClearHistory(CommandArg[] args) + { + Terminal.History?.Clear(); + } + + [RegisterCommand( + isDefault: true, + Name = "help", + Help = "Display help information about a command", + MaxArgCount = 1 + )] + public static void CommandHelp(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + if (args.Length == 0) + { + foreach (KeyValuePair command in shell.Commands) + { + string name = command.Key; + CommandInfo info = command.Value; + string usage = BuildUsage(name, info.minArgCount, info.maxArgCount, info.hint); + string helpLine = + $"{name.ToUpperInvariant(), -16}: {info.help}\n -> {usage}"; + Terminal.Log(helpLine); + UnityEngine.Debug.Log(helpLine); + } + return; + } + else + { + string commandName = args[0].contents ?? string.Empty; + + if (!shell.Commands.TryGetValue(commandName, out CommandInfo info)) + { + shell.IssueErrorMessage($"Command {commandName} could not be found."); + return; + } + + string usageLine = BuildUsage( + commandName, + info.minArgCount, + info.maxArgCount, + info.hint + ); + if (string.IsNullOrWhiteSpace(info.help)) + { + string line = $"{commandName}: {usageLine}"; + Terminal.Log(line); + UnityEngine.Debug.Log(line); + } + else + { + string line = $"{info.help}\n{usageLine}"; + Terminal.Log(line); + UnityEngine.Debug.Log(line); + } + } + } + + private static string BuildUsage(string name, int minArgs, int maxArgs, string hint) + { + if (!string.IsNullOrWhiteSpace(hint)) + { + return $"Usage: {hint}"; + } + + StringBuilder.Clear(); + StringBuilder.Append("Usage: "); + StringBuilder.Append(name); + if (minArgs <= 0 && (maxArgs == 0 || maxArgs < 0)) + { + return StringBuilder.ToString(); + } + + int max = maxArgs < 0 ? minArgs : Math.Max(minArgs, maxArgs); + for (int i = 1; i <= minArgs; ++i) + { + StringBuilder.Append(' '); + StringBuilder.Append('<'); + StringBuilder.Append("arg"); + StringBuilder.Append(i); + StringBuilder.Append('>'); + } + for (int i = minArgs + 1; i <= max; ++i) + { + StringBuilder.Append(' '); + StringBuilder.Append('['); + StringBuilder.Append("arg"); + StringBuilder.Append(i); + StringBuilder.Append(']'); + } + if (maxArgs < 0) + { + StringBuilder.Append(' '); + StringBuilder.Append("[args...]"); + } + + return StringBuilder.ToString(); + } + + [RegisterCommand( + isDefault: true, + Name = "time", + Help = "Time the execution of a command", + MinArgCount = 1 + )] + public static void CommandTime(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + Stopwatch sw = Stopwatch.StartNew(); + shell.RunCommand(JoinArguments(args)); + sw.Stop(); + Terminal.Log($"Time: {sw.ElapsedMilliseconds}ms"); + } + + [RegisterCommand( + isDefault: true, + Name = "time-scale", + Help = "Sets Time.timeScale", + MinArgCount = 1, + MaxArgCount = 1 + )] + public static void CommandTimeScale(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + CommandArg arg = args[0]; + if (!arg.TryGet(out float timeScale)) + { + Terminal.Log(TerminalLogType.Warning, $"Invalid time scale {arg}."); + return; + } + + Time.timeScale = timeScale; + } + + [RegisterCommand( + isDefault: true, + Name = "log-terminal", + Help = "Output message via Terminal.Log" + )] + public static void CommandLogTerminal(CommandArg[] args) + { + Terminal.Log(JoinArguments(args)); + } + + [RegisterCommand(isDefault: true, Name = "log", Help = "Output message via Debug.Log")] + public static void CommandLog(CommandArg[] args) + { + UnityEngine.Debug.Log(JoinArguments(args)); + } + + [RegisterCommand( + isDefault: true, + Name = "trace", + Help = "Output the stack trace of the previous message", + MaxArgCount = 0 + )] + public static void CommandTrace(CommandArg[] args) + { + CommandLog buffer = Terminal.Buffer; + if (buffer == null) + { + return; + } + + int logCount = buffer.Logs.Count; + + if (logCount - 2 < 0) + { + Terminal.Log(TerminalLogType.Warning, "Nothing to trace."); + return; + } + + LogItem logItem = buffer.Logs[logCount - 2]; + + if (string.IsNullOrWhiteSpace(logItem.stackTrace)) + { + Terminal.Log( + logItem.message.EndsWith(" (no trace)", StringComparison.OrdinalIgnoreCase) + ? logItem.message + : $"{logItem.message} (no trace)" + ); + } + else + { + Terminal.Log(logItem.stackTrace); + } + } + + [RegisterCommand( + isDefault: true, + Name = "clear-variable", + Help = "Clears a variable value", + MinArgCount = 1, + MaxArgCount = 1 + )] + public static void CommandClearVariable(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + string variableName = args[0].contents; + bool cleared = shell.ClearVariable(variableName); + if (cleared) + { + Terminal.Log($"Variable '{variableName}' cleared successfully."); + } + else + { + Terminal.Log( + TerminalLogType.Warning, + $"Warning: Variable '{variableName}' not found." + ); + } + } + + [RegisterCommand( + isDefault: true, + Name = "clear-all-variables", + Help = "Clears all variable values", + MinArgCount = 0, + MaxArgCount = 0 + )] + public static void CommandClearAllVariable(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + int variableCount = shell.Variables.Count; + foreach (string variable in shell.Variables.Keys.ToArray()) + { + shell.ClearVariable(variable); + } + + Terminal.Log( + variableCount == 0 + ? "No variables found - nothing to clear." + : $"Cleared {variableCount} variables." + ); + } + + [RegisterCommand( + isDefault: true, + Name = "set-variable", + Help = "Sets a variable value", + MinArgCount = 2, + MaxArgCount = 2 + )] + public static void CommandSetVariable(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + string variableName = args[0].contents; + + if (string.IsNullOrWhiteSpace(variableName) || variableName.StartsWith('$')) + { + Terminal.Log( + TerminalLogType.Warning, + $"Warning: Possibly invalid variable name '{variableName}'." + ); + } + + string variableValue = JoinArguments(args, 1); + bool set = shell.SetVariable(variableName, variableValue); + if (set) + { + Terminal.Log($"Variable '{variableName}' set to '{variableValue}' successfully."); + } + else if (shell.Variables.TryGetValue(variableName, out CommandArg existingVariable)) + { + Terminal.Log( + TerminalLogType.Warning, + $"Variable '{variableName}' failed to set. Existing value: {existingVariable}." + ); + } + else + { + Terminal.Log( + TerminalLogType.Warning, + $"Variable '{variableName}' failed to set. No existing value found." + ); + } + } + + [RegisterCommand( + isDefault: true, + Name = "get-variable", + Help = "Gets a variable value", + MinArgCount = 1, + MaxArgCount = 1 + )] + public static void CommandGetVariable(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + string variableName = args[0].contents; + + if (shell.Variables.TryGetValue(variableName, out CommandArg variable)) + { + Terminal.Log($"Variable '{variableName}' is set to '{variable}'."); + } + else + { + Terminal.Log(TerminalLogType.Warning, $"Variable '{variableName}' not found."); + } + } + + [RegisterCommand( + isDefault: true, + Name = "list-variables", + Help = "Gets all variables and their associated values", + MinArgCount = 0, + MaxArgCount = 0 + )] + public static void CommandGetAllVariables(CommandArg[] args) + { + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + if (!shell.Variables.Any()) + { + Terminal.Log(TerminalLogType.Warning, "No variables found."); + return; + } + + foreach (KeyValuePair entry in shell.Variables) + { + Terminal.Log($"Variable '{entry.Key}' is set to '{entry.Value}'."); + } + } + + [RegisterCommand(isDefault: true, Name = "no-op", Help = "No operation")] + public static void CommandNoOperation(CommandArg[] args) + { + // No-op + } + + [RegisterCommand( + isDefault: true, + Name = "quit", + Help = "Quit running application", + MaxArgCount = 0 + )] + public static void CommandQuit(CommandArg[] args) + { +#if UNITY_EDITOR + UnityEditor.EditorApplication.isPlaying = false; +#else + UnityEngine.Application.Quit(); +#endif + } + + private static string JoinArguments(CommandArg[] args, int start = 0) + { + StringBuilder.Clear(); + for (int i = start; i < args.Length; i++) + { + StringBuilder.Append(args[i].contents); + + if (i < args.Length - 1) + { + StringBuilder.Append(' '); + } + } + + return StringBuilder.ToString(); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs index d676f45..79baabb 100644 --- a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs +++ b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs @@ -1,235 +1,235 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Extensions; - - public sealed class CommandAutoComplete - { - private readonly SortedSet _knownWords = new(StringComparer.OrdinalIgnoreCase); - private readonly HashSet _duplicateBuffer = new(StringComparer.OrdinalIgnoreCase); - private readonly List _buffer = new(); - - private readonly CommandHistory _history; - private readonly CommandShell _shell; - - public CommandAutoComplete( - CommandHistory history, - CommandShell shell, - IEnumerable commands = null - ) - { - _history = history ?? throw new ArgumentNullException(nameof(history)); - _shell = shell ?? throw new ArgumentNullException(nameof(shell)); - _knownWords.UnionWith(commands ?? Enumerable.Empty()); - } - - public string[] Complete(string text) - { - return Complete(text: text, buffer: _buffer).ToArray(); - } - - public List Complete(string text, List buffer) - { - int caret = text?.Length ?? 0; - Complete(text, caret, buffer); - return buffer; - } - - public List Complete(string text, int caretIndex, List buffer) - { - string input = text ?? string.Empty; - buffer.Clear(); - _duplicateBuffer.Clear(); - - if (string.IsNullOrWhiteSpace(input)) - { - WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); - return buffer; - } - - int safeCaret = Math.Max(0, Math.Min(caretIndex, input.Length)); - string uptoCaret = - safeCaret <= 0 - ? string.Empty - : (safeCaret < input.Length ? input.Substring(0, safeCaret) : input); - - // Parse command + args up to caret - string working = uptoCaret; - if (!CommandShell.TryEatArgument(ref working, out CommandArg cmdArg)) - { - WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); - return buffer; - } - - string commandName = cmdArg.contents; - if (!_shell.Commands.TryGetValue(commandName, out CommandInfo cmdInfo)) - { - // Fall back to default behavior if not a known command yet - WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); - return buffer; - } - - // Collect args typed before cursor - List args = new(); - string lastToken = string.Empty; - bool trailingWhitespace = - uptoCaret.Length > 0 && char.IsWhiteSpace(uptoCaret[uptoCaret.Length - 1]); - while (CommandShell.TryEatArgument(ref working, out CommandArg arg)) - { - lastToken = arg.contents; - args.Add(arg); - } - - string partialArg = trailingWhitespace ? string.Empty : lastToken; - int argIndex = trailingWhitespace ? args.Count : (args.Count - 1); - if (!trailingWhitespace && 0 <= argIndex) - { - // Exclude the partial token from finalized args - args.RemoveAt(args.Count - 1); - } - - // Special case: caret is immediately after the command name with no space. - // Treat this as requesting suggestions for the first argument. - if (!trailingWhitespace && args.Count == 0) - { - partialArg = string.Empty; - argIndex = 0; - } - - // If the command provides a completer, ask it first - bool inArgContext = argIndex >= 0; - if (cmdInfo.completer != null) - { - CommandCompletionContext ctx = new( - input, - commandName, - args, - partialArg, - argIndex, - _shell - ); - - foreach ( - string suggestion in cmdInfo.completer.Complete(ctx) ?? Array.Empty() - ) - { - if (string.IsNullOrWhiteSpace(suggestion)) - { - continue; - } - - string prefix = commandName; - if (0 < args.Count) - { - prefix += " " + string.Join(" ", args.Select(a => a.contents)); - } - - if (argIndex >= 0) - { - prefix += " "; - } - - string insertion = suggestion; - bool needsQuoting = - !string.IsNullOrEmpty(insertion) && insertion.Any(char.IsWhiteSpace); - if (needsQuoting) - { - // Basic quoting to keep single argument with whitespace - // Escape embedded quotes minimally - insertion = "\"" + insertion.Replace("\"", "\\\"") + "\""; - } - - string full = prefix + insertion; - string key = full.NeedsLowerInvariantConversion() - ? full.ToLowerInvariant() - : full; - if (_duplicateBuffer.Add(key)) - { - buffer.Add(full); - } - } - - // If we got any results from the completer, return them. - if (0 < buffer.Count) - { - return buffer; - } - - // If we are in argument context for a command that supports completion, - // prefer context (even if empty) and do not fall back to history/known words. - if (inArgContext) - { - return buffer; - } - } - - // Fallback to built-in completion sources - WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); - return buffer; - } - - private void WalkHistory( - string input, - bool onlySuccess, - bool onlyErrorFree, - List buffer - ) - { - if (input.NeedsTrim()) - { - input = input.Trim(); - } - _duplicateBuffer.Clear(); - buffer.Clear(); - - // Commands - foreach (string command in _shell.Commands.Keys) - { - string known = command.NeedsLowerInvariantConversion() - ? command.ToLowerInvariant() - : command; - if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (_duplicateBuffer.Add(known)) - { - buffer.Add(known); - } - } - - // Known words - foreach (string known in _knownWords) - { - if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (_duplicateBuffer.Add(known)) - { - buffer.Add(known); - } - } - - // History - foreach ( - string known in _history.GetHistory( - onlySuccess: onlySuccess, - onlyErrorFree: onlyErrorFree - ) - ) - { - if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (_duplicateBuffer.Add(known)) - { - buffer.Add(known); - } - } - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Extensions; + + public sealed class CommandAutoComplete + { + private readonly SortedSet _knownWords = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _duplicateBuffer = new(StringComparer.OrdinalIgnoreCase); + private readonly List _buffer = new(); + + private readonly CommandHistory _history; + private readonly CommandShell _shell; + + public CommandAutoComplete( + CommandHistory history, + CommandShell shell, + IEnumerable commands = null + ) + { + _history = history ?? throw new ArgumentNullException(nameof(history)); + _shell = shell ?? throw new ArgumentNullException(nameof(shell)); + _knownWords.UnionWith(commands ?? Enumerable.Empty()); + } + + public string[] Complete(string text) + { + return Complete(text: text, buffer: _buffer).ToArray(); + } + + public List Complete(string text, List buffer) + { + int caret = text?.Length ?? 0; + Complete(text, caret, buffer); + return buffer; + } + + public List Complete(string text, int caretIndex, List buffer) + { + string input = text ?? string.Empty; + buffer.Clear(); + _duplicateBuffer.Clear(); + + if (string.IsNullOrWhiteSpace(input)) + { + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + return buffer; + } + + int safeCaret = Math.Max(0, Math.Min(caretIndex, input.Length)); + string uptoCaret = + safeCaret <= 0 + ? string.Empty + : (safeCaret < input.Length ? input.Substring(0, safeCaret) : input); + + // Parse command + args up to caret + string working = uptoCaret; + if (!CommandShell.TryEatArgument(ref working, out CommandArg cmdArg)) + { + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + return buffer; + } + + string commandName = cmdArg.contents; + if (!_shell.Commands.TryGetValue(commandName, out CommandInfo cmdInfo)) + { + // Fall back to default behavior if not a known command yet + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + return buffer; + } + + // Collect args typed before cursor + List args = new(); + string lastToken = string.Empty; + bool trailingWhitespace = + uptoCaret.Length > 0 && char.IsWhiteSpace(uptoCaret[uptoCaret.Length - 1]); + while (CommandShell.TryEatArgument(ref working, out CommandArg arg)) + { + lastToken = arg.contents; + args.Add(arg); + } + + string partialArg = trailingWhitespace ? string.Empty : lastToken; + int argIndex = trailingWhitespace ? args.Count : (args.Count - 1); + if (!trailingWhitespace && 0 <= argIndex) + { + // Exclude the partial token from finalized args + args.RemoveAt(args.Count - 1); + } + + // Special case: caret is immediately after the command name with no space. + // Treat this as requesting suggestions for the first argument. + if (!trailingWhitespace && args.Count == 0) + { + partialArg = string.Empty; + argIndex = 0; + } + + // If the command provides a completer, ask it first + bool inArgContext = argIndex >= 0; + if (cmdInfo.completer != null) + { + CommandCompletionContext ctx = new( + input, + commandName, + args, + partialArg, + argIndex, + _shell + ); + + foreach ( + string suggestion in cmdInfo.completer.Complete(ctx) ?? Array.Empty() + ) + { + if (string.IsNullOrWhiteSpace(suggestion)) + { + continue; + } + + string prefix = commandName; + if (0 < args.Count) + { + prefix += " " + string.Join(" ", args.Select(a => a.contents)); + } + + if (argIndex >= 0) + { + prefix += " "; + } + + string insertion = suggestion; + bool needsQuoting = + !string.IsNullOrEmpty(insertion) && insertion.Any(char.IsWhiteSpace); + if (needsQuoting) + { + // Basic quoting to keep single argument with whitespace + // Escape embedded quotes minimally + insertion = "\"" + insertion.Replace("\"", "\\\"") + "\""; + } + + string full = prefix + insertion; + string key = full.NeedsLowerInvariantConversion() + ? full.ToLowerInvariant() + : full; + if (_duplicateBuffer.Add(key)) + { + buffer.Add(full); + } + } + + // If we got any results from the completer, return them. + if (0 < buffer.Count) + { + return buffer; + } + + // If we are in argument context for a command that supports completion, + // prefer context (even if empty) and do not fall back to history/known words. + if (inArgContext) + { + return buffer; + } + } + + // Fallback to built-in completion sources + WalkHistory(input, onlySuccess: true, onlyErrorFree: false, buffer: buffer); + return buffer; + } + + private void WalkHistory( + string input, + bool onlySuccess, + bool onlyErrorFree, + List buffer + ) + { + if (input.NeedsTrim()) + { + input = input.Trim(); + } + _duplicateBuffer.Clear(); + buffer.Clear(); + + // Commands + foreach (string command in _shell.Commands.Keys) + { + string known = command.NeedsLowerInvariantConversion() + ? command.ToLowerInvariant() + : command; + if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (_duplicateBuffer.Add(known)) + { + buffer.Add(known); + } + } + + // Known words + foreach (string known in _knownWords) + { + if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (_duplicateBuffer.Add(known)) + { + buffer.Add(known); + } + } + + // History + foreach ( + string known in _history.GetHistory( + onlySuccess: onlySuccess, + onlyErrorFree: onlyErrorFree + ) + ) + { + if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (_duplicateBuffer.Add(known)) + { + buffer.Add(known); + } + } + } + } +} diff --git a/Runtime/CommandTerminal/Backend/CommandHistory.cs b/Runtime/CommandTerminal/Backend/CommandHistory.cs index eb5b0f3..4471efd 100644 --- a/Runtime/CommandTerminal/Backend/CommandHistory.cs +++ b/Runtime/CommandTerminal/Backend/CommandHistory.cs @@ -1,130 +1,130 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - using System.Collections.Generic; - using System.Linq; - using DataStructures; - - public sealed class CommandHistory - { - public int Capacity => _history.Capacity; - - private readonly CyclicBuffer<(string text, bool? success, bool? errorFree)> _history; - - private int _position; - - public CommandHistory(int capacity) - { - _history = new CyclicBuffer<(string text, bool? success, bool? errorFree)>(capacity); - } - - public IEnumerable GetHistory(bool onlySuccess, bool onlyErrorFree) - { - return _history - .Where(value => !onlySuccess || value.success == true) - .Where(value => !onlyErrorFree || value.errorFree == true) - .Select(value => value.text); - } - - public void Resize(int newCapacity) - { - _history.Resize(newCapacity); - } - - public bool Push(string commandString, bool? success, bool? errorFree) - { - if (string.IsNullOrWhiteSpace(commandString)) - { - return false; - } - - _history.Add((commandString, success, errorFree)); - _position = _history.Count; - return true; - } - - public string Next(bool skipSameCommands) - { - int initialPosition = _position; - ++_position; - - while ( - skipSameCommands - && 0 <= initialPosition - && initialPosition < _history.Count - && 0 <= _position - && _position < _history.Count - ) - { - if ( - string.Equals( - _history[initialPosition].text, - _history[_position].text, - StringComparison.OrdinalIgnoreCase - ) - ) - { - ++_position; - } - else - { - break; - } - } - - if (0 <= _position && _position < _history.Count) - { - return _history[_position].text; - } - - _position = _history.Count; - return string.Empty; - } - - public string Previous(bool skipSameCommands) - { - int initialPosition = _position; - --_position; - - while ( - skipSameCommands - && 0 <= initialPosition - && initialPosition < _history.Count - && 0 <= _position - && _position < _history.Count - ) - { - if ( - string.Equals( - _history[initialPosition].text, - _history[_position].text, - StringComparison.OrdinalIgnoreCase - ) - ) - { - --_position; - } - else - { - break; - } - } - - if (0 <= _position && _position < _history.Count) - { - return _history[_position].text; - } - - _position = -1; - return string.Empty; - } - - public int Clear() - { - int count = _history.Count; - _history.Clear(); - _position = 0; - return count; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using System.Collections.Generic; + using System.Linq; + using DataStructures; + + public sealed class CommandHistory + { + public int Capacity => _history.Capacity; + + private readonly CyclicBuffer<(string text, bool? success, bool? errorFree)> _history; + + private int _position; + + public CommandHistory(int capacity) + { + _history = new CyclicBuffer<(string text, bool? success, bool? errorFree)>(capacity); + } + + public IEnumerable GetHistory(bool onlySuccess, bool onlyErrorFree) + { + return _history + .Where(value => !onlySuccess || value.success == true) + .Where(value => !onlyErrorFree || value.errorFree == true) + .Select(value => value.text); + } + + public void Resize(int newCapacity) + { + _history.Resize(newCapacity); + } + + public bool Push(string commandString, bool? success, bool? errorFree) + { + if (string.IsNullOrWhiteSpace(commandString)) + { + return false; + } + + _history.Add((commandString, success, errorFree)); + _position = _history.Count; + return true; + } + + public string Next(bool skipSameCommands) + { + int initialPosition = _position; + ++_position; + + while ( + skipSameCommands + && 0 <= initialPosition + && initialPosition < _history.Count + && 0 <= _position + && _position < _history.Count + ) + { + if ( + string.Equals( + _history[initialPosition].text, + _history[_position].text, + StringComparison.OrdinalIgnoreCase + ) + ) + { + ++_position; + } + else + { + break; + } + } + + if (0 <= _position && _position < _history.Count) + { + return _history[_position].text; + } + + _position = _history.Count; + return string.Empty; + } + + public string Previous(bool skipSameCommands) + { + int initialPosition = _position; + --_position; + + while ( + skipSameCommands + && 0 <= initialPosition + && initialPosition < _history.Count + && 0 <= _position + && _position < _history.Count + ) + { + if ( + string.Equals( + _history[initialPosition].text, + _history[_position].text, + StringComparison.OrdinalIgnoreCase + ) + ) + { + --_position; + } + else + { + break; + } + } + + if (0 <= _position && _position < _history.Count) + { + return _history[_position].text; + } + + _position = -1; + return string.Empty; + } + + public int Clear() + { + int count = _history.Count; + _history.Clear(); + _position = 0; + return count; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/CommandInfo.cs b/Runtime/CommandTerminal/Backend/CommandInfo.cs index d126e68..c1b0f2a 100644 --- a/Runtime/CommandTerminal/Backend/CommandInfo.cs +++ b/Runtime/CommandTerminal/Backend/CommandInfo.cs @@ -1,31 +1,31 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - - public readonly struct CommandInfo - { - public readonly Action proc; - public readonly int minArgCount; - public readonly int maxArgCount; - public readonly string help; - public readonly string hint; - public readonly IArgumentCompleter completer; - - public CommandInfo( - Action proc, - int minArgCount, - int maxArgCount, - string help, - string hint, - IArgumentCompleter completer = null - ) - { - this.proc = proc; - this.maxArgCount = maxArgCount; - this.minArgCount = minArgCount; - this.help = help; - this.hint = hint; - this.completer = completer; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + + public readonly struct CommandInfo + { + public readonly Action proc; + public readonly int minArgCount; + public readonly int maxArgCount; + public readonly string help; + public readonly string hint; + public readonly IArgumentCompleter completer; + + public CommandInfo( + Action proc, + int minArgCount, + int maxArgCount, + string help, + string hint, + IArgumentCompleter completer = null + ) + { + this.proc = proc; + this.maxArgCount = maxArgCount; + this.minArgCount = minArgCount; + this.help = help; + this.hint = hint; + this.completer = completer; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/CommandLog.cs b/Runtime/CommandTerminal/Backend/CommandLog.cs index 8eba05d..7eb11bb 100644 --- a/Runtime/CommandTerminal/Backend/CommandLog.cs +++ b/Runtime/CommandTerminal/Backend/CommandLog.cs @@ -1,198 +1,198 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using DataStructures; - using UnityEngine; - - public enum TerminalLogType - { - Error = LogType.Error, - Assert = LogType.Assert, - Warning = LogType.Warning, - Message = LogType.Log, - Exception = LogType.Exception, - Input, - ShellMessage, - } - - public readonly struct LogItem - { - public readonly TerminalLogType type; - public readonly string message; - public readonly string stackTrace; - - public LogItem(TerminalLogType type, string message, string stackTrace) - { - this.type = type; - this.message = message ?? string.Empty; - this.stackTrace = stackTrace ?? string.Empty; - } - } - - public sealed class CommandLog - { - private const string InternalNamespace = "WallstopStudios.DxCommandTerminal"; - - public IReadOnlyList Logs => _logs; - public int Capacity => _logs.Capacity; - public long Version => _version; - - public readonly HashSet ignoredLogTypes; - - private readonly CyclicBuffer _logs; - - private long _version; - - private readonly ConcurrentQueue<( - string message, - string stackTrace, - TerminalLogType type, - bool includeStackTrace - )> _pending = new(); - - public CommandLog(int maxItems, IEnumerable ignoredLogTypes = null) - { - _logs = new CyclicBuffer(maxItems); - this.ignoredLogTypes = new HashSet( - ignoredLogTypes ?? Enumerable.Empty() - ); - } - - public bool HandleLog(string message, TerminalLogType type, bool includeStackTrace = true) - { - // Main-thread direct path retained for back-compat - string stackTrace = includeStackTrace ? GetAccurateStackTrace() : string.Empty; - return HandleLog(message, stackTrace, type); - } - - private static string GetAccurateStackTrace() - { - string fullStackTrace = StackTraceUtility.ExtractStackTrace(); - if (string.IsNullOrWhiteSpace(fullStackTrace)) - { - return fullStackTrace; - } - int length = fullStackTrace.Length; - int index = 0; - // Skip the first line (StackTraceUtility includes a leading line) - while (index < length && fullStackTrace[index] != '\n' && fullStackTrace[index] != '\r') - { - index++; - } - // Consume newline chars - while ( - index < length && (fullStackTrace[index] == '\n' || fullStackTrace[index] == '\r') - ) - { - index++; - } - - // Skip frames inside our own namespace for clearer logs - while (index < length) - { - int lineStart = index; - // Find end of line - while ( - index < length && fullStackTrace[index] != '\n' && fullStackTrace[index] != '\r' - ) - { - index++; - } - - int lineLen = index - lineStart; - bool isInternal = - fullStackTrace.IndexOf( - InternalNamespace, - lineStart, - lineLen, - StringComparison.OrdinalIgnoreCase - ) >= 0; - if (!isInternal) - { - // Return from this line onward - return fullStackTrace.Substring(lineStart); - } - - // Move to next line start (skip newline chars) - while ( - index < length - && (fullStackTrace[index] == '\n' || fullStackTrace[index] == '\r') - ) - { - index++; - } - } - - return string.Empty; - } - - public bool HandleLog(string message, string stackTrace, TerminalLogType type) - { - if (ignoredLogTypes.Contains(type)) - { - return false; - } - - _version++; - LogItem log = new(type, message, stackTrace); - _logs.Add(log); - return true; - } - - public void EnqueueMessage(string message, TerminalLogType type, bool includeStackTrace) - { - if (ignoredLogTypes.Contains(type)) - { - return; - } - _pending.Enqueue((message ?? string.Empty, string.Empty, type, includeStackTrace)); - } - - public void EnqueueUnityLog(string message, string stackTrace, TerminalLogType type) - { - if (ignoredLogTypes.Contains(type)) - { - return; - } - _pending.Enqueue((message ?? string.Empty, stackTrace ?? string.Empty, type, false)); - } - - public int DrainPending() - { - int added = 0; - while (_pending.TryDequeue(out var item)) - { - string stack = item.includeStackTrace ? GetAccurateStackTrace() : item.stackTrace; - if (ignoredLogTypes.Contains(item.type)) - { - continue; - } - _version++; - _logs.Add(new LogItem(item.type, item.message, stack)); - added++; - } - - return added; - } - - public int Clear() - { - int logCount = _logs.Count; - _logs.Clear(); - _version++; - return logCount; - } - - public void Resize(int newCapacity) - { - if (newCapacity < _logs.Count) - { - _version++; - } - _logs.Resize(newCapacity); - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using DataStructures; + using UnityEngine; + + public enum TerminalLogType + { + Error = LogType.Error, + Assert = LogType.Assert, + Warning = LogType.Warning, + Message = LogType.Log, + Exception = LogType.Exception, + Input, + ShellMessage, + } + + public readonly struct LogItem + { + public readonly TerminalLogType type; + public readonly string message; + public readonly string stackTrace; + + public LogItem(TerminalLogType type, string message, string stackTrace) + { + this.type = type; + this.message = message ?? string.Empty; + this.stackTrace = stackTrace ?? string.Empty; + } + } + + public sealed class CommandLog + { + private const string InternalNamespace = "WallstopStudios.DxCommandTerminal"; + + public IReadOnlyList Logs => _logs; + public int Capacity => _logs.Capacity; + public long Version => _version; + + public readonly HashSet ignoredLogTypes; + + private readonly CyclicBuffer _logs; + + private long _version; + + private readonly ConcurrentQueue<( + string message, + string stackTrace, + TerminalLogType type, + bool includeStackTrace + )> _pending = new(); + + public CommandLog(int maxItems, IEnumerable ignoredLogTypes = null) + { + _logs = new CyclicBuffer(maxItems); + this.ignoredLogTypes = new HashSet( + ignoredLogTypes ?? Enumerable.Empty() + ); + } + + public bool HandleLog(string message, TerminalLogType type, bool includeStackTrace = true) + { + // Main-thread direct path retained for back-compat + string stackTrace = includeStackTrace ? GetAccurateStackTrace() : string.Empty; + return HandleLog(message, stackTrace, type); + } + + private static string GetAccurateStackTrace() + { + string fullStackTrace = StackTraceUtility.ExtractStackTrace(); + if (string.IsNullOrWhiteSpace(fullStackTrace)) + { + return fullStackTrace; + } + int length = fullStackTrace.Length; + int index = 0; + // Skip the first line (StackTraceUtility includes a leading line) + while (index < length && fullStackTrace[index] != '\n' && fullStackTrace[index] != '\r') + { + index++; + } + // Consume newline chars + while ( + index < length && (fullStackTrace[index] == '\n' || fullStackTrace[index] == '\r') + ) + { + index++; + } + + // Skip frames inside our own namespace for clearer logs + while (index < length) + { + int lineStart = index; + // Find end of line + while ( + index < length && fullStackTrace[index] != '\n' && fullStackTrace[index] != '\r' + ) + { + index++; + } + + int lineLen = index - lineStart; + bool isInternal = + fullStackTrace.IndexOf( + InternalNamespace, + lineStart, + lineLen, + StringComparison.OrdinalIgnoreCase + ) >= 0; + if (!isInternal) + { + // Return from this line onward + return fullStackTrace.Substring(lineStart); + } + + // Move to next line start (skip newline chars) + while ( + index < length + && (fullStackTrace[index] == '\n' || fullStackTrace[index] == '\r') + ) + { + index++; + } + } + + return string.Empty; + } + + public bool HandleLog(string message, string stackTrace, TerminalLogType type) + { + if (ignoredLogTypes.Contains(type)) + { + return false; + } + + _version++; + LogItem log = new(type, message, stackTrace); + _logs.Add(log); + return true; + } + + public void EnqueueMessage(string message, TerminalLogType type, bool includeStackTrace) + { + if (ignoredLogTypes.Contains(type)) + { + return; + } + _pending.Enqueue((message ?? string.Empty, string.Empty, type, includeStackTrace)); + } + + public void EnqueueUnityLog(string message, string stackTrace, TerminalLogType type) + { + if (ignoredLogTypes.Contains(type)) + { + return; + } + _pending.Enqueue((message ?? string.Empty, stackTrace ?? string.Empty, type, false)); + } + + public int DrainPending() + { + int added = 0; + while (_pending.TryDequeue(out var item)) + { + string stack = item.includeStackTrace ? GetAccurateStackTrace() : item.stackTrace; + if (ignoredLogTypes.Contains(item.type)) + { + continue; + } + _version++; + _logs.Add(new LogItem(item.type, item.message, stack)); + added++; + } + + return added; + } + + public int Clear() + { + int logCount = _logs.Count; + _logs.Clear(); + _version++; + return logCount; + } + + public void Resize(int newCapacity) + { + if (newCapacity < _logs.Count) + { + _version++; + } + _logs.Resize(newCapacity); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index ca048b5..96f8bf6 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -1,657 +1,657 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Reflection; - using System.Text; - using Attributes; - using UnityEngine; - - public sealed class CommandShell - { - private static readonly string[] IgnoredTypes = { "JetBrains.Rider" }; - - public static readonly Lazy<( - MethodInfo method, - RegisterCommandAttribute attribute - )[]> RegisteredCommands = new(() => - { - List<(MethodInfo, RegisterCommandAttribute)> commands = new(); - const BindingFlags methodFlags = - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; - - Assembly[] ourAssembly = { typeof(BuiltInCommands).Assembly }; - foreach ( - Type type in AppDomain - .CurrentDomain.GetAssemblies() - /* - Force our assembly to be processed last so user commands, - if they conflict with in-built ones, are always registered first. - */ - .Except(ourAssembly) - .Concat(ourAssembly) - .SelectMany(assembly => assembly.GetTypes()) - ) - { - try - { - foreach (MethodInfo method in type.GetMethods(methodFlags)) - { - try - { - if ( - Attribute.GetCustomAttribute( - method, - typeof(RegisterCommandAttribute) - ) - is not RegisterCommandAttribute attribute - ) - { - continue; - } - - attribute.NormalizeName(method); - commands.Add((method, attribute)); - } - catch (Exception e) - { - if (ShouldIgnoreExceptionForType(type)) - { - continue; - } - - Debug.LogError( - $"Failed to resolve method {method.Name} of type {type.FullName} with exception {e}" - ); - } - } - } - catch (Exception e) - { - if (ShouldIgnoreExceptionForType(type)) - { - continue; - } - - Debug.LogError( - $"Failed to resolve methods for type {type.FullName} with exception {e}" - ); - } - } - - return commands.ToArray(); - }); - - private readonly List _arguments = new(); // Cache for performance - - private readonly HashSet _autoRegisteredCommands = new( - StringComparer.OrdinalIgnoreCase - ); - - private readonly StringBuilder _commandBuilder = new(); - - private readonly SortedDictionary _commands = new( - StringComparer.OrdinalIgnoreCase - ); - - private readonly Queue _errorMessages = new(); - - private readonly CommandHistory _history; - private readonly HashSet _ignoredCommands = new(StringComparer.OrdinalIgnoreCase); - - private readonly SortedDictionary _rejectedCommands = new( - StringComparer.OrdinalIgnoreCase - ); - - private readonly SortedDictionary _variables = new( - StringComparer.OrdinalIgnoreCase - ); - - public CommandShell(CommandHistory history) - { - _history = history ?? throw new ArgumentNullException(nameof(history)); - } - - public IReadOnlyDictionary Commands => _commands; - public IReadOnlyDictionary Variables => _variables; - - public ImmutableHashSet AutoRegisteredCommands { get; private set; } = - ImmutableHashSet.Empty; - - public ImmutableHashSet IgnoredCommands { get; private set; } = - ImmutableHashSet.Empty; - - public bool IgnoringDefaultCommands { get; private set; } - - public bool HasErrors => 0 < _errorMessages.Count; - - private static bool ShouldIgnoreExceptionForType(Type type) - { - foreach (string ignoredType in IgnoredTypes) - { - if (type.FullName?.IndexOf(ignoredType, StringComparison.OrdinalIgnoreCase) >= 0) - { - return true; - } - } - - return false; - } - - public bool TryConsumeErrorMessage(out string errorMessage) - { - return _errorMessages.TryDequeue(out errorMessage); - } - - public int ClearAllCommands() - { - return ClearAutoRegisteredCommands() + ClearCustomCommands(); - } - - public int ClearCustomCommands() - { - int count = _commands.Count; - _commands.Clear(); - return count; - } - - //public bool RemoveCommand - - public int ClearAutoRegisteredCommands() - { - int count = _autoRegisteredCommands.Count; - foreach (string command in _autoRegisteredCommands) - { - _commands.Remove(command); - } - - _autoRegisteredCommands.Clear(); - AutoRegisteredCommands = ImmutableHashSet.Empty; - return count; - } - - public void InitializeAutoRegisteredCommands( - IEnumerable ignoredCommands = null, - bool ignoreDefaultCommands = false - ) - { - IgnoringDefaultCommands = ignoreDefaultCommands; - ClearAutoRegisteredCommands(); - _ignoredCommands.Clear(); - _ignoredCommands.UnionWith(ignoredCommands ?? Enumerable.Empty()); - foreach (string ignoredCommand in _ignoredCommands) - { - _commands.Remove(ignoredCommand); - } - - IgnoredCommands = _ignoredCommands.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); - _rejectedCommands.Clear(); - - foreach ( - (MethodInfo method, RegisterCommandAttribute attribute) in RegisteredCommands.Value - ) - { - string commandName = attribute.Name; - if (_ignoredCommands.Contains(commandName)) - { - continue; - } - - if (ignoreDefaultCommands && attribute.Default) - { - continue; - } - - if (attribute.EditorOnly && !Application.isEditor) - { - continue; - } - - if (attribute.DevelopmentOnly && !Application.isEditor && !Debug.isDebugBuild) - { - continue; - } - - ParameterInfo[] methodsParams = method.GetParameters(); - if ( - methodsParams.Length != 1 - || methodsParams[0].ParameterType != typeof(CommandArg[]) - ) - { - _rejectedCommands.TryAdd(commandName, method); - continue; - } - - // Perf boost, much cheaper than running reflection on invoking the method - Action proc = - (Action) - Delegate.CreateDelegate(typeof(Action), method); - // Try resolve optional completer via CommandCompleterAttribute - IArgumentCompleter completer = ResolveCompleter(method); - - bool success = AddCommand( - commandName, - proc, - attribute.MinArgCount, - attribute.MaxArgCount, - attribute.Help, - attribute.Hint, - completer - ); - if (success) - { - _autoRegisteredCommands.Add(commandName); - } - } - - AutoRegisteredCommands = _autoRegisteredCommands.ToImmutableHashSet( - StringComparer.OrdinalIgnoreCase - ); - - foreach (KeyValuePair command in _rejectedCommands) - { - IssueErrorMessage( - $"{command.Key} has an invalid signature. " - + $"Expected: {command.Value.Name}(CommandArg[]). " - + $"Found: {command.Value.Name}({string.Join(",", command.Value.GetParameters().Select(p => p.ParameterType.Name))})" - ); - } - } - - /// - /// Parses an input line into a command and runs that command. - /// - public bool RunCommand(string line) - { - string remaining = line; - _arguments.Clear(); - - while (!string.IsNullOrWhiteSpace(remaining)) - { - if (!TryEatArgument(ref remaining, out CommandArg argument)) - { - continue; - } - - string argumentString = argument.contents; - if (argument.endQuote == null) - { - if (string.IsNullOrWhiteSpace(argumentString)) - { - continue; - } - - if (argumentString.StartsWith('$')) - { - string variableName = argumentString[1..]; - - if (_variables.TryGetValue(variableName, out CommandArg variable)) - { - // Replace variable argument if it's defined - argument = variable; - } - } - } - - _arguments.Add(argument); - } - - if (_arguments.Count == 0) - { - _history.Push(line, false, true); - return false; - } - - string commandName = _arguments[0].contents ?? string.Empty; - // Remove command name from arguments - _arguments.RemoveAt(0); - - return RunCommand( - commandName, - _arguments.Count == 0 ? Array.Empty() : _arguments.ToArray() - ); - } - - public bool RunCommand(string commandName, CommandArg[] arguments) - { - _commandBuilder.Clear(); - _commandBuilder.Append(commandName); - if (arguments.Length != 0) - { - _commandBuilder.Append(' '); - } - - for (int i = 0; i < arguments.Length; ++i) - { - CommandArg argument = arguments[i]; - if (argument.startQuote != null) - { - _commandBuilder.Append(argument.startQuote.Value); - } - - _commandBuilder.Append(argument.contents); - if (argument.endQuote != null) - { - _commandBuilder.Append(argument.endQuote.Value); - } - - if (i != arguments.Length - 1) - { - _commandBuilder.Append(' '); - } - } - - string line = _commandBuilder.ToString(); - - if (string.IsNullOrWhiteSpace(commandName)) - { - IssueErrorMessage($"Invalid command name '{commandName}'"); - // Don't log empty commands - return false; - } - - if (commandName.Contains(' ')) - { - commandName = commandName.Replace( - " ", - string.Empty, - StringComparison.OrdinalIgnoreCase - ); - } - - if (!_commands.TryGetValue(commandName, out CommandInfo command)) - { - IssueErrorMessage($"Command {commandName} not found"); - _history.Push(line, false, false); - return false; - } - - int argCount = arguments.Length; - string errorMessage = null; - int requiredArg = 0; - - if (argCount < command.minArgCount) - { - errorMessage = command.minArgCount == command.maxArgCount ? "exactly" : "at least"; - requiredArg = command.minArgCount; - } - else if (0 <= command.maxArgCount && command.maxArgCount < argCount) - { - // Do not check max allowed number of arguments if it is -1 - errorMessage = command.minArgCount == command.maxArgCount ? "exactly" : "at most"; - requiredArg = command.maxArgCount; - } - - if (!string.IsNullOrEmpty(errorMessage)) - { - string pluralFix = requiredArg == 1 ? "" : "s"; - - string invalidMessage = - $"{commandName} requires {errorMessage} {requiredArg} argument{pluralFix}"; - if (!string.IsNullOrWhiteSpace(command.hint)) - { - invalidMessage += $"\n -> Usage: {command.hint}"; - } - - _errorMessages.Enqueue(invalidMessage); - _history.Push(line, false, false); - return false; - } - - int errorCount = _errorMessages.Count; - command.proc?.Invoke(arguments); - _history.Push(line, true, errorCount == _errorMessages.Count); - return true; - } - - // ReSharper disable once MemberCanBePrivate.Global - public bool AddCommand(string name, CommandInfo info) - { - if (string.IsNullOrWhiteSpace(name)) - { - IssueErrorMessage($"Invalid Command Name: {name}"); - return false; - } - - if (name.Contains(' ')) - { - name = name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); - } - - if (!_commands.TryAdd(name, info)) - { - IssueErrorMessage($"Command {name} is already defined."); - return false; - } - - return true; - } - - // ReSharper disable once MemberCanBePrivate.Global - public bool AddCommand( - string name, - Action proc, - int minArgs = 0, - int maxArgs = -1, - string help = "", - string hint = null, - IArgumentCompleter completer = null - ) - { - CommandInfo info = new(proc, minArgs, maxArgs, help, hint, completer); - return AddCommand(name, info); - } - - public bool SetVariable(string name, string value) - { - value ??= string.Empty; - return SetVariable(name, new CommandArg(value)); - } - - public bool ClearVariable(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - IssueErrorMessage($"Invalid Variable Name: {name}"); - return false; - } - - if (name.Contains(' ')) - { - name = name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); - } - - return _variables.Remove(name); - } - - // ReSharper disable once MemberCanBePrivate.Global - public bool SetVariable(string name, CommandArg value) - { - if (string.IsNullOrWhiteSpace(name)) - { - IssueErrorMessage($"Invalid Variable Name: {name}"); - return false; - } - - if (name.Contains(' ')) - { - name = name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); - } - - _variables[name] = value; - return true; - } - - // ReSharper disable once UnusedMember.Global - public bool TryGetVariable(string name, out CommandArg variable) - { - if (string.IsNullOrWhiteSpace(name)) - { - variable = default; - return false; - } - - if (name.Contains(' ')) - { - name = - name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase) - ?? string.Empty; - } - - return _variables.TryGetValue(name, out variable); - } - - public void IssueErrorMessage(string format, params object[] parameters) - { - string formattedMessage = - (parameters is { Length: > 0 } ? string.Format(format, parameters) : format) - ?? string.Empty; - _errorMessages.Enqueue(formattedMessage); - } - - public static bool TryEatArgument(ref string stringValue, out CommandArg arg) - { - stringValue = stringValue.TrimStart(); - if (stringValue.Length == 0) - { - arg = default; - return false; - } - - char firstChar = stringValue[0]; - if (CommandArg.Quotes.Contains(firstChar)) - { - int closingQuoteIndex = -1; - - // Find the matching closing quote. - for (int i = 1; i < stringValue.Length; ++i) - { - if (stringValue[i] == firstChar) - { - // Check if this quote is escaped by an odd number of backslashes - int backslashCount = 0; - int j = i - 1; - while (1 <= j && stringValue[j] == '\\') - { - backslashCount++; - j--; - } - if ((backslashCount % 2) == 0) - { - closingQuoteIndex = i; - break; - } - } - } - - if (closingQuoteIndex < 0) - { - // No closing quote was found; consume the rest of the string (excluding the opening quote). - string input = stringValue.Substring(1); - if (firstChar == '\'') - { - input = input.Replace("\\'", "'").Replace("\\\\", "\\"); - } - else if (firstChar == '"') - { - input = input.Replace("\\\"", "\"").Replace("\\\\", "\\"); - } - arg = new CommandArg(input, firstChar); - stringValue = string.Empty; - } - else - { - // Extract the argument inside the quotes. - string input = stringValue.Substring(1, closingQuoteIndex - 1); - // Unescape the matching quote and backslashes - if (firstChar == '\'') - { - input = input.Replace("\\'", "'").Replace("\\\\", "\\"); - } - else if (firstChar == '"') - { - input = input.Replace("\\\"", "\"").Replace("\\\\", "\\"); - } - arg = new CommandArg(input, firstChar, firstChar); - // Remove the parsed argument (including the quotes) from the input. - stringValue = stringValue.Substring(closingQuoteIndex + 1); - } - } - else - { - // Unquoted argument: find the next space. - int spaceIndex = stringValue.IndexOf(' '); - if (spaceIndex < 0) - { - arg = new CommandArg(stringValue); - stringValue = string.Empty; - } - else - { - string input = stringValue.Substring(0, spaceIndex); - arg = new CommandArg(input); - stringValue = stringValue.Substring(spaceIndex + 1); - } - } - - return true; - } - - private static IArgumentCompleter ResolveCompleter(MethodInfo method) - { - try - { - object attr = Attribute.GetCustomAttribute( - method, - typeof(Attributes.CommandCompleterAttribute) - ); - if (attr is not Attributes.CommandCompleterAttribute cca) - { - return null; - } - - Type t = cca.CompleterType; - // Prefer a public static Instance property - PropertyInfo instProp = t.GetProperty( - "Instance", - BindingFlags.Public | BindingFlags.Static - ); - if ( - instProp != null - && typeof(IArgumentCompleter).IsAssignableFrom(instProp.PropertyType) - ) - { - return (IArgumentCompleter)instProp.GetValue(null); - } - - // Or a public static Instance field - FieldInfo instField = t.GetField( - "Instance", - BindingFlags.Public | BindingFlags.Static - ); - if ( - instField != null - && typeof(IArgumentCompleter).IsAssignableFrom(instField.FieldType) - ) - { - return (IArgumentCompleter)instField.GetValue(null); - } - - // Else use parameterless constructor - ConstructorInfo ctor = t.GetConstructor(Type.EmptyTypes); - if (ctor != null) - { - return (IArgumentCompleter)Activator.CreateInstance(t); - } - } - catch (Exception) - { - // Swallow and treat as no-completer; errors surface in logs elsewhere - } - - return null; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Reflection; + using System.Text; + using Attributes; + using UnityEngine; + + public sealed class CommandShell + { + private static readonly string[] IgnoredTypes = { "JetBrains.Rider" }; + + public static readonly Lazy<( + MethodInfo method, + RegisterCommandAttribute attribute + )[]> RegisteredCommands = new(() => + { + List<(MethodInfo, RegisterCommandAttribute)> commands = new(); + const BindingFlags methodFlags = + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + + Assembly[] ourAssembly = { typeof(BuiltInCommands).Assembly }; + foreach ( + Type type in AppDomain + .CurrentDomain.GetAssemblies() + /* + Force our assembly to be processed last so user commands, + if they conflict with in-built ones, are always registered first. + */ + .Except(ourAssembly) + .Concat(ourAssembly) + .SelectMany(assembly => assembly.GetTypes()) + ) + { + try + { + foreach (MethodInfo method in type.GetMethods(methodFlags)) + { + try + { + if ( + Attribute.GetCustomAttribute( + method, + typeof(RegisterCommandAttribute) + ) + is not RegisterCommandAttribute attribute + ) + { + continue; + } + + attribute.NormalizeName(method); + commands.Add((method, attribute)); + } + catch (Exception e) + { + if (ShouldIgnoreExceptionForType(type)) + { + continue; + } + + Debug.LogError( + $"Failed to resolve method {method.Name} of type {type.FullName} with exception {e}" + ); + } + } + } + catch (Exception e) + { + if (ShouldIgnoreExceptionForType(type)) + { + continue; + } + + Debug.LogError( + $"Failed to resolve methods for type {type.FullName} with exception {e}" + ); + } + } + + return commands.ToArray(); + }); + + private readonly List _arguments = new(); // Cache for performance + + private readonly HashSet _autoRegisteredCommands = new( + StringComparer.OrdinalIgnoreCase + ); + + private readonly StringBuilder _commandBuilder = new(); + + private readonly SortedDictionary _commands = new( + StringComparer.OrdinalIgnoreCase + ); + + private readonly Queue _errorMessages = new(); + + private readonly CommandHistory _history; + private readonly HashSet _ignoredCommands = new(StringComparer.OrdinalIgnoreCase); + + private readonly SortedDictionary _rejectedCommands = new( + StringComparer.OrdinalIgnoreCase + ); + + private readonly SortedDictionary _variables = new( + StringComparer.OrdinalIgnoreCase + ); + + public CommandShell(CommandHistory history) + { + _history = history ?? throw new ArgumentNullException(nameof(history)); + } + + public IReadOnlyDictionary Commands => _commands; + public IReadOnlyDictionary Variables => _variables; + + public ImmutableHashSet AutoRegisteredCommands { get; private set; } = + ImmutableHashSet.Empty; + + public ImmutableHashSet IgnoredCommands { get; private set; } = + ImmutableHashSet.Empty; + + public bool IgnoringDefaultCommands { get; private set; } + + public bool HasErrors => 0 < _errorMessages.Count; + + private static bool ShouldIgnoreExceptionForType(Type type) + { + foreach (string ignoredType in IgnoredTypes) + { + if (type.FullName?.IndexOf(ignoredType, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + + return false; + } + + public bool TryConsumeErrorMessage(out string errorMessage) + { + return _errorMessages.TryDequeue(out errorMessage); + } + + public int ClearAllCommands() + { + return ClearAutoRegisteredCommands() + ClearCustomCommands(); + } + + public int ClearCustomCommands() + { + int count = _commands.Count; + _commands.Clear(); + return count; + } + + //public bool RemoveCommand + + public int ClearAutoRegisteredCommands() + { + int count = _autoRegisteredCommands.Count; + foreach (string command in _autoRegisteredCommands) + { + _commands.Remove(command); + } + + _autoRegisteredCommands.Clear(); + AutoRegisteredCommands = ImmutableHashSet.Empty; + return count; + } + + public void InitializeAutoRegisteredCommands( + IEnumerable ignoredCommands = null, + bool ignoreDefaultCommands = false + ) + { + IgnoringDefaultCommands = ignoreDefaultCommands; + ClearAutoRegisteredCommands(); + _ignoredCommands.Clear(); + _ignoredCommands.UnionWith(ignoredCommands ?? Enumerable.Empty()); + foreach (string ignoredCommand in _ignoredCommands) + { + _commands.Remove(ignoredCommand); + } + + IgnoredCommands = _ignoredCommands.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + _rejectedCommands.Clear(); + + foreach ( + (MethodInfo method, RegisterCommandAttribute attribute) in RegisteredCommands.Value + ) + { + string commandName = attribute.Name; + if (_ignoredCommands.Contains(commandName)) + { + continue; + } + + if (ignoreDefaultCommands && attribute.Default) + { + continue; + } + + if (attribute.EditorOnly && !Application.isEditor) + { + continue; + } + + if (attribute.DevelopmentOnly && !Application.isEditor && !Debug.isDebugBuild) + { + continue; + } + + ParameterInfo[] methodsParams = method.GetParameters(); + if ( + methodsParams.Length != 1 + || methodsParams[0].ParameterType != typeof(CommandArg[]) + ) + { + _rejectedCommands.TryAdd(commandName, method); + continue; + } + + // Perf boost, much cheaper than running reflection on invoking the method + Action proc = + (Action) + Delegate.CreateDelegate(typeof(Action), method); + // Try resolve optional completer via CommandCompleterAttribute + IArgumentCompleter completer = ResolveCompleter(method); + + bool success = AddCommand( + commandName, + proc, + attribute.MinArgCount, + attribute.MaxArgCount, + attribute.Help, + attribute.Hint, + completer + ); + if (success) + { + _autoRegisteredCommands.Add(commandName); + } + } + + AutoRegisteredCommands = _autoRegisteredCommands.ToImmutableHashSet( + StringComparer.OrdinalIgnoreCase + ); + + foreach (KeyValuePair command in _rejectedCommands) + { + IssueErrorMessage( + $"{command.Key} has an invalid signature. " + + $"Expected: {command.Value.Name}(CommandArg[]). " + + $"Found: {command.Value.Name}({string.Join(",", command.Value.GetParameters().Select(p => p.ParameterType.Name))})" + ); + } + } + + /// + /// Parses an input line into a command and runs that command. + /// + public bool RunCommand(string line) + { + string remaining = line; + _arguments.Clear(); + + while (!string.IsNullOrWhiteSpace(remaining)) + { + if (!TryEatArgument(ref remaining, out CommandArg argument)) + { + continue; + } + + string argumentString = argument.contents; + if (argument.endQuote == null) + { + if (string.IsNullOrWhiteSpace(argumentString)) + { + continue; + } + + if (argumentString.StartsWith('$')) + { + string variableName = argumentString[1..]; + + if (_variables.TryGetValue(variableName, out CommandArg variable)) + { + // Replace variable argument if it's defined + argument = variable; + } + } + } + + _arguments.Add(argument); + } + + if (_arguments.Count == 0) + { + _history.Push(line, false, true); + return false; + } + + string commandName = _arguments[0].contents ?? string.Empty; + // Remove command name from arguments + _arguments.RemoveAt(0); + + return RunCommand( + commandName, + _arguments.Count == 0 ? Array.Empty() : _arguments.ToArray() + ); + } + + public bool RunCommand(string commandName, CommandArg[] arguments) + { + _commandBuilder.Clear(); + _commandBuilder.Append(commandName); + if (arguments.Length != 0) + { + _commandBuilder.Append(' '); + } + + for (int i = 0; i < arguments.Length; ++i) + { + CommandArg argument = arguments[i]; + if (argument.startQuote != null) + { + _commandBuilder.Append(argument.startQuote.Value); + } + + _commandBuilder.Append(argument.contents); + if (argument.endQuote != null) + { + _commandBuilder.Append(argument.endQuote.Value); + } + + if (i != arguments.Length - 1) + { + _commandBuilder.Append(' '); + } + } + + string line = _commandBuilder.ToString(); + + if (string.IsNullOrWhiteSpace(commandName)) + { + IssueErrorMessage($"Invalid command name '{commandName}'"); + // Don't log empty commands + return false; + } + + if (commandName.Contains(' ')) + { + commandName = commandName.Replace( + " ", + string.Empty, + StringComparison.OrdinalIgnoreCase + ); + } + + if (!_commands.TryGetValue(commandName, out CommandInfo command)) + { + IssueErrorMessage($"Command {commandName} not found"); + _history.Push(line, false, false); + return false; + } + + int argCount = arguments.Length; + string errorMessage = null; + int requiredArg = 0; + + if (argCount < command.minArgCount) + { + errorMessage = command.minArgCount == command.maxArgCount ? "exactly" : "at least"; + requiredArg = command.minArgCount; + } + else if (0 <= command.maxArgCount && command.maxArgCount < argCount) + { + // Do not check max allowed number of arguments if it is -1 + errorMessage = command.minArgCount == command.maxArgCount ? "exactly" : "at most"; + requiredArg = command.maxArgCount; + } + + if (!string.IsNullOrEmpty(errorMessage)) + { + string pluralFix = requiredArg == 1 ? "" : "s"; + + string invalidMessage = + $"{commandName} requires {errorMessage} {requiredArg} argument{pluralFix}"; + if (!string.IsNullOrWhiteSpace(command.hint)) + { + invalidMessage += $"\n -> Usage: {command.hint}"; + } + + _errorMessages.Enqueue(invalidMessage); + _history.Push(line, false, false); + return false; + } + + int errorCount = _errorMessages.Count; + command.proc?.Invoke(arguments); + _history.Push(line, true, errorCount == _errorMessages.Count); + return true; + } + + // ReSharper disable once MemberCanBePrivate.Global + public bool AddCommand(string name, CommandInfo info) + { + if (string.IsNullOrWhiteSpace(name)) + { + IssueErrorMessage($"Invalid Command Name: {name}"); + return false; + } + + if (name.Contains(' ')) + { + name = name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (!_commands.TryAdd(name, info)) + { + IssueErrorMessage($"Command {name} is already defined."); + return false; + } + + return true; + } + + // ReSharper disable once MemberCanBePrivate.Global + public bool AddCommand( + string name, + Action proc, + int minArgs = 0, + int maxArgs = -1, + string help = "", + string hint = null, + IArgumentCompleter completer = null + ) + { + CommandInfo info = new(proc, minArgs, maxArgs, help, hint, completer); + return AddCommand(name, info); + } + + public bool SetVariable(string name, string value) + { + value ??= string.Empty; + return SetVariable(name, new CommandArg(value)); + } + + public bool ClearVariable(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + IssueErrorMessage($"Invalid Variable Name: {name}"); + return false; + } + + if (name.Contains(' ')) + { + name = name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + return _variables.Remove(name); + } + + // ReSharper disable once MemberCanBePrivate.Global + public bool SetVariable(string name, CommandArg value) + { + if (string.IsNullOrWhiteSpace(name)) + { + IssueErrorMessage($"Invalid Variable Name: {name}"); + return false; + } + + if (name.Contains(' ')) + { + name = name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + _variables[name] = value; + return true; + } + + // ReSharper disable once UnusedMember.Global + public bool TryGetVariable(string name, out CommandArg variable) + { + if (string.IsNullOrWhiteSpace(name)) + { + variable = default; + return false; + } + + if (name.Contains(' ')) + { + name = + name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase) + ?? string.Empty; + } + + return _variables.TryGetValue(name, out variable); + } + + public void IssueErrorMessage(string format, params object[] parameters) + { + string formattedMessage = + (parameters is { Length: > 0 } ? string.Format(format, parameters) : format) + ?? string.Empty; + _errorMessages.Enqueue(formattedMessage); + } + + public static bool TryEatArgument(ref string stringValue, out CommandArg arg) + { + stringValue = stringValue.TrimStart(); + if (stringValue.Length == 0) + { + arg = default; + return false; + } + + char firstChar = stringValue[0]; + if (CommandArg.Quotes.Contains(firstChar)) + { + int closingQuoteIndex = -1; + + // Find the matching closing quote. + for (int i = 1; i < stringValue.Length; ++i) + { + if (stringValue[i] == firstChar) + { + // Check if this quote is escaped by an odd number of backslashes + int backslashCount = 0; + int j = i - 1; + while (1 <= j && stringValue[j] == '\\') + { + backslashCount++; + j--; + } + if ((backslashCount % 2) == 0) + { + closingQuoteIndex = i; + break; + } + } + } + + if (closingQuoteIndex < 0) + { + // No closing quote was found; consume the rest of the string (excluding the opening quote). + string input = stringValue.Substring(1); + if (firstChar == '\'') + { + input = input.Replace("\\'", "'").Replace("\\\\", "\\"); + } + else if (firstChar == '"') + { + input = input.Replace("\\\"", "\"").Replace("\\\\", "\\"); + } + arg = new CommandArg(input, firstChar); + stringValue = string.Empty; + } + else + { + // Extract the argument inside the quotes. + string input = stringValue.Substring(1, closingQuoteIndex - 1); + // Unescape the matching quote and backslashes + if (firstChar == '\'') + { + input = input.Replace("\\'", "'").Replace("\\\\", "\\"); + } + else if (firstChar == '"') + { + input = input.Replace("\\\"", "\"").Replace("\\\\", "\\"); + } + arg = new CommandArg(input, firstChar, firstChar); + // Remove the parsed argument (including the quotes) from the input. + stringValue = stringValue.Substring(closingQuoteIndex + 1); + } + } + else + { + // Unquoted argument: find the next space. + int spaceIndex = stringValue.IndexOf(' '); + if (spaceIndex < 0) + { + arg = new CommandArg(stringValue); + stringValue = string.Empty; + } + else + { + string input = stringValue.Substring(0, spaceIndex); + arg = new CommandArg(input); + stringValue = stringValue.Substring(spaceIndex + 1); + } + } + + return true; + } + + private static IArgumentCompleter ResolveCompleter(MethodInfo method) + { + try + { + object attr = Attribute.GetCustomAttribute( + method, + typeof(Attributes.CommandCompleterAttribute) + ); + if (attr is not Attributes.CommandCompleterAttribute cca) + { + return null; + } + + Type t = cca.CompleterType; + // Prefer a public static Instance property + PropertyInfo instProp = t.GetProperty( + "Instance", + BindingFlags.Public | BindingFlags.Static + ); + if ( + instProp != null + && typeof(IArgumentCompleter).IsAssignableFrom(instProp.PropertyType) + ) + { + return (IArgumentCompleter)instProp.GetValue(null); + } + + // Or a public static Instance field + FieldInfo instField = t.GetField( + "Instance", + BindingFlags.Public | BindingFlags.Static + ); + if ( + instField != null + && typeof(IArgumentCompleter).IsAssignableFrom(instField.FieldType) + ) + { + return (IArgumentCompleter)instField.GetValue(null); + } + + // Else use parameterless constructor + ConstructorInfo ctor = t.GetConstructor(Type.EmptyTypes); + if (ctor != null) + { + return (IArgumentCompleter)Activator.CreateInstance(t); + } + } + catch (Exception) + { + // Swallow and treat as no-completer; errors surface in logs elsewhere + } + + return null; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/HintDisplayMode.cs b/Runtime/CommandTerminal/Backend/HintDisplayMode.cs index 51e9999..00c51c3 100644 --- a/Runtime/CommandTerminal/Backend/HintDisplayMode.cs +++ b/Runtime/CommandTerminal/Backend/HintDisplayMode.cs @@ -1,13 +1,13 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using System; - - public enum HintDisplayMode - { - [Obsolete("Use a valid value")] - Unknown = 0, - Always = 1, - AutoCompleteOnly = 2, - Never = 3, - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + + public enum HintDisplayMode + { + [Obsolete("Use a valid value")] + Unknown = 0, + Always = 1, + AutoCompleteOnly = 2, + Never = 3, + } +} diff --git a/Runtime/CommandTerminal/Backend/Terminal.cs b/Runtime/CommandTerminal/Backend/Terminal.cs index 786c2c1..a6b132d 100644 --- a/Runtime/CommandTerminal/Backend/Terminal.cs +++ b/Runtime/CommandTerminal/Backend/Terminal.cs @@ -1,34 +1,34 @@ -namespace WallstopStudios.DxCommandTerminal.Backend -{ - using JetBrains.Annotations; - - public static class Terminal - { - public static CommandLog Buffer { get; internal set; } - public static CommandShell Shell { get; internal set; } - public static CommandHistory History { get; internal set; } - public static CommandAutoComplete AutoComplete { get; internal set; } - - [StringFormatMethod("format")] - public static bool Log(string format, params object[] parameters) - { - return Log(TerminalLogType.ShellMessage, format, parameters); - } - - [StringFormatMethod("format")] - public static bool Log(TerminalLogType type, string format, params object[] parameters) - { - CommandLog buffer = Buffer; - if (buffer == null) - { - return false; - } - - string formattedMessage = parameters is { Length: > 0 } - ? string.Format(format, parameters) - : format; - buffer.EnqueueMessage(formattedMessage, type, includeStackTrace: true); - return true; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using JetBrains.Annotations; + + public static class Terminal + { + public static CommandLog Buffer { get; internal set; } + public static CommandShell Shell { get; internal set; } + public static CommandHistory History { get; internal set; } + public static CommandAutoComplete AutoComplete { get; internal set; } + + [StringFormatMethod("format")] + public static bool Log(string format, params object[] parameters) + { + return Log(TerminalLogType.ShellMessage, format, parameters); + } + + [StringFormatMethod("format")] + public static bool Log(TerminalLogType type, string format, params object[] parameters) + { + CommandLog buffer = Buffer; + if (buffer == null) + { + return false; + } + + string formattedMessage = parameters is { Length: > 0 } + ? string.Format(format, parameters) + : format; + buffer.EnqueueMessage(formattedMessage, type, includeStackTrace: true); + return true; + } + } +} diff --git a/Runtime/CommandTerminal/Input/DefaultTerminalInput.cs b/Runtime/CommandTerminal/Input/DefaultTerminalInput.cs index eaf9027..5c1e798 100644 --- a/Runtime/CommandTerminal/Input/DefaultTerminalInput.cs +++ b/Runtime/CommandTerminal/Input/DefaultTerminalInput.cs @@ -1,11 +1,11 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ - public sealed class DefaultTerminalInput : ITerminalInput - { - public static readonly DefaultTerminalInput Instance = new(); - - public string CommandText { get; set; } = string.Empty; - - private DefaultTerminalInput() { } - } -} +namespace WallstopStudios.DxCommandTerminal.Input +{ + public sealed class DefaultTerminalInput : ITerminalInput + { + public static readonly DefaultTerminalInput Instance = new(); + + public string CommandText { get; set; } = string.Empty; + + private DefaultTerminalInput() { } + } +} diff --git a/Runtime/CommandTerminal/Input/IInputHandler.cs b/Runtime/CommandTerminal/Input/IInputHandler.cs index 33deb93..97af0fe 100644 --- a/Runtime/CommandTerminal/Input/IInputHandler.cs +++ b/Runtime/CommandTerminal/Input/IInputHandler.cs @@ -1,7 +1,7 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ - public interface IInputHandler - { - bool ShouldHandleInputThisFrame { get; } - } -} +namespace WallstopStudios.DxCommandTerminal.Input +{ + public interface IInputHandler + { + bool ShouldHandleInputThisFrame { get; } + } +} diff --git a/Runtime/CommandTerminal/Input/ITerminalInput.cs b/Runtime/CommandTerminal/Input/ITerminalInput.cs index 8ff85a9..854569b 100644 --- a/Runtime/CommandTerminal/Input/ITerminalInput.cs +++ b/Runtime/CommandTerminal/Input/ITerminalInput.cs @@ -1,7 +1,7 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ - public interface ITerminalInput - { - string CommandText { get; set; } - } -} +namespace WallstopStudios.DxCommandTerminal.Input +{ + public interface ITerminalInput + { + string CommandText { get; set; } + } +} diff --git a/Runtime/CommandTerminal/Input/InputHelpers.cs b/Runtime/CommandTerminal/Input/InputHelpers.cs index 4fe6fce..358de98 100644 --- a/Runtime/CommandTerminal/Input/InputHelpers.cs +++ b/Runtime/CommandTerminal/Input/InputHelpers.cs @@ -1,400 +1,400 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ - using System; - using System.Collections.Generic; - using Extensions; - using UnityEngine; -#if ENABLE_INPUT_SYSTEM - using UnityEngine.InputSystem; - using UnityEngine.InputSystem.Controls; -#endif - - public static class InputHelpers - { - private static readonly string[] ShiftModifiers = { "shift+", "#" }; - - private static readonly Dictionary CachedSubstrings = new(); - - private static readonly Dictionary KeyCodeMapping = new( - StringComparer.OrdinalIgnoreCase - ) - { - { "~", KeyCode.BackQuote }, - { "`", KeyCode.BackQuote }, - { "!", KeyCode.Alpha1 }, - { "@", KeyCode.Alpha2 }, - { "#", KeyCode.Alpha3 }, - { "$", KeyCode.Alpha4 }, - { "%", KeyCode.Alpha5 }, - { "^", KeyCode.Alpha6 }, - { "&", KeyCode.Alpha7 }, - { "*", KeyCode.Alpha8 }, - { "(", KeyCode.Alpha9 }, - { ")", KeyCode.Alpha0 }, - { "-", KeyCode.Minus }, - { "_", KeyCode.Minus }, - { "=", KeyCode.Equals }, - { "+", KeyCode.Equals }, - { "[", KeyCode.LeftBracket }, - { "{", KeyCode.LeftBracket }, - { "]", KeyCode.RightBracket }, - { "}", KeyCode.RightBracket }, - { "\\", KeyCode.Backslash }, - { "|", KeyCode.Backslash }, - { ";", KeyCode.Semicolon }, - { ":", KeyCode.Semicolon }, - { "'", KeyCode.Quote }, - { "\"", KeyCode.Quote }, - { ",", KeyCode.Comma }, - { "<", KeyCode.Comma }, - { ".", KeyCode.Period }, - { ">", KeyCode.Period }, - { "/", KeyCode.Slash }, - { "?", KeyCode.Slash }, - { "1", KeyCode.Alpha1 }, - { "2", KeyCode.Alpha2 }, - { "3", KeyCode.Alpha3 }, - { "4", KeyCode.Alpha4 }, - { "5", KeyCode.Alpha5 }, - { "6", KeyCode.Alpha6 }, - { "7", KeyCode.Alpha7 }, - { "8", KeyCode.Alpha8 }, - { "9", KeyCode.Alpha9 }, - { "0", KeyCode.Alpha0 }, - { "numpad1", KeyCode.Keypad1 }, - { "keypad1", KeyCode.Keypad1 }, - { "numpad2", KeyCode.Keypad2 }, - { "keypad2", KeyCode.Keypad2 }, - { "numpad3", KeyCode.Keypad3 }, - { "keypad3", KeyCode.Keypad3 }, - { "numpad4", KeyCode.Keypad4 }, - { "keypad4", KeyCode.Keypad4 }, - { "numpad5", KeyCode.Keypad5 }, - { "keypad5", KeyCode.Keypad5 }, - { "numpad6", KeyCode.Keypad6 }, - { "keypad6", KeyCode.Keypad6 }, - { "numpad7", KeyCode.Keypad7 }, - { "keypad7", KeyCode.Keypad7 }, - { "numpad8", KeyCode.Keypad8 }, - { "keypad8", KeyCode.Keypad8 }, - { "numpad9", KeyCode.Keypad9 }, - { "keypad9", KeyCode.Keypad9 }, - { "numpad0", KeyCode.Keypad0 }, - { "keypad0", KeyCode.Keypad0 }, - { "numpadplus", KeyCode.KeypadPlus }, - { "keypadplus", KeyCode.KeypadPlus }, - { "numpad+", KeyCode.KeypadPlus }, - { "numpadminus", KeyCode.KeypadMinus }, - { "keypadminus", KeyCode.KeypadMinus }, - { "numpad-", KeyCode.KeypadMinus }, - { "numpadmultiply", KeyCode.KeypadMultiply }, - { "keypadmultiply", KeyCode.KeypadMultiply }, - { "numpad*", KeyCode.KeypadMultiply }, - { "numpaddivide", KeyCode.KeypadDivide }, - { "keypaddivide", KeyCode.KeypadDivide }, - { "numpad/", KeyCode.KeypadDivide }, - { "numpadenter", KeyCode.KeypadEnter }, - { "keypadenter", KeyCode.KeypadEnter }, - { "numpadperiod", KeyCode.KeypadPeriod }, - { "keypadperiod", KeyCode.KeypadPeriod }, - { "numpad.", KeyCode.KeypadPeriod }, - { "numpaddecimal", KeyCode.KeypadPeriod }, - { "numpadequals", KeyCode.KeypadEquals }, - { "keypadequals", KeyCode.KeypadEquals }, - { "numpad=", KeyCode.KeypadEquals }, - { "esc", KeyCode.Escape }, - { "escape", KeyCode.Escape }, - { "return", KeyCode.Return }, - { "enter", KeyCode.Return }, - { "space", KeyCode.Space }, - { "spacebar", KeyCode.Space }, - { "del", KeyCode.Delete }, - { "delete", KeyCode.Delete }, - { "ins", KeyCode.Insert }, - { "insert", KeyCode.Insert }, - { "pageup", KeyCode.PageUp }, - { "pgup", KeyCode.PageUp }, - { "pagedown", KeyCode.PageDown }, - { "pgdn", KeyCode.PageDown }, - { "pagedn", KeyCode.PageDown }, - { "lshift", KeyCode.LeftShift }, - { "rshift", KeyCode.RightShift }, - { "leftshift", KeyCode.LeftShift }, - { "rightshift", KeyCode.RightShift }, - { "lctrl", KeyCode.LeftControl }, - { "rctrl", KeyCode.RightControl }, - { "lcontrol", KeyCode.LeftControl }, - { "rcontrol", KeyCode.RightControl }, - { "leftctrl", KeyCode.LeftControl }, - { "rightctrl", KeyCode.RightControl }, - { "leftcontrol", KeyCode.LeftControl }, - { "rightcontrol", KeyCode.RightControl }, - { "lalt", KeyCode.LeftAlt }, - { "ralt", KeyCode.RightAlt }, - { "leftalt", KeyCode.LeftAlt }, - { "rightalt", KeyCode.RightAlt }, - { "lcmd", KeyCode.LeftCommand }, - { "rcmd", KeyCode.RightCommand }, - { "lcommand", KeyCode.LeftCommand }, - { "rcommand", KeyCode.RightCommand }, - { "leftcmd", KeyCode.LeftCommand }, - { "rightcmd", KeyCode.RightCommand }, - { "leftcommand", KeyCode.LeftCommand }, - { "rightcommand", KeyCode.RightCommand }, - { "lwin", KeyCode.LeftWindows }, - { "rwin", KeyCode.RightWindows }, - { "leftwindows", KeyCode.LeftWindows }, - { "rightwindows", KeyCode.RightWindows }, - { "leftwin", KeyCode.LeftWindows }, - { "rightwin", KeyCode.RightWindows }, - { "capslock", KeyCode.CapsLock }, - { "numlock", KeyCode.Numlock }, - { "scrolllock", KeyCode.ScrollLock }, - { "prtscn", KeyCode.Print }, - { "printscreen", KeyCode.Print }, - { "pausebreak", KeyCode.Pause }, - { "pause", KeyCode.Pause }, - { "up", KeyCode.UpArrow }, - { "uparrow", KeyCode.UpArrow }, - { "down", KeyCode.DownArrow }, - { "downarrow", KeyCode.DownArrow }, - { "left", KeyCode.LeftArrow }, - { "leftarrow", KeyCode.LeftArrow }, - { "right", KeyCode.RightArrow }, - { "rightarrow", KeyCode.RightArrow }, - { "f1", KeyCode.F1 }, - { "f2", KeyCode.F2 }, - { "f3", KeyCode.F3 }, - { "f4", KeyCode.F4 }, - { "f5", KeyCode.F5 }, - { "f6", KeyCode.F6 }, - { "f7", KeyCode.F7 }, - { "f8", KeyCode.F8 }, - { "f9", KeyCode.F9 }, - { "f10", KeyCode.F10 }, - { "f11", KeyCode.F11 }, - { "f12", KeyCode.F12 }, - { "f13", KeyCode.F13 }, - { "f14", KeyCode.F14 }, - { "f15", KeyCode.F15 }, - { "mouse0", KeyCode.Mouse0 }, - { "leftmouse", KeyCode.Mouse0 }, - { "lmb", KeyCode.Mouse0 }, - { "mouse1", KeyCode.Mouse1 }, - { "rightmouse", KeyCode.Mouse1 }, - { "rmb", KeyCode.Mouse1 }, - { "mouse2", KeyCode.Mouse2 }, - { "middlemouse", KeyCode.Mouse2 }, - { "mmb", KeyCode.Mouse2 }, - { "mouse3", KeyCode.Mouse3 }, - { "mouse4", KeyCode.Mouse4 }, - { "mouse5", KeyCode.Mouse5 }, - { "mouse6", KeyCode.Mouse6 }, - { "none", KeyCode.None }, - }; - - private static readonly Dictionary SpecialKeyCodeMap = new( - StringComparer.OrdinalIgnoreCase - ) - { - { "`", "backquote" }, - { "-", "minus" }, - { "=", "equals" }, - { "[", "leftBracket" }, - { "]", "rightBracket" }, - { ";", "semicolon" }, - { "'", "quote" }, - { "\\", "backslash" }, - { ",", "comma" }, - { ".", "period" }, - { "/", "slash" }, - { "1", "digit1" }, - { "2", "digit2" }, - { "3", "digit3" }, - { "4", "digit4" }, - { "5", "digit5" }, - { "6", "digit6" }, - { "7", "digit7" }, - { "8", "digit8" }, - { "9", "digit9" }, - { "0", "digit0" }, - { "up", "upArrow" }, - { "left", "leftArrow" }, - { "right", "rightArrow" }, - { "down", "downArrow" }, - { " ", "space" }, - }; - - private static readonly Dictionary SpecialShiftedKeyCodeMap = new( - StringComparer.OrdinalIgnoreCase - ) - { - { "~", "backquote" }, - { "!", "digit1" }, - { "@", "digit2" }, - { "#", "digit3" }, - { "$", "digit4" }, - { "^", "digit6" }, - { "%", "digit5" }, - { "&", "digit7" }, - { "*", "digit8" }, - { "(", "digit9" }, - { ")", "digit0" }, - { "_", "minus" }, - { "+", "equals" }, - { "{", "leftBracket" }, - { "}", "rightBracket" }, - { ":", "semicolon" }, - { "\"", "quote" }, - { "|", "backslash" }, - { "<", "comma" }, - { ">", "period" }, - { "?", "slash" }, - }; - - private static readonly Dictionary AlternativeSpecialShiftedKeyCodeMap = - new(StringComparer.OrdinalIgnoreCase) - { - { "!", "1" }, - { "@", "2" }, - { "#", "3" }, - { "$", "4" }, - { "^", "5" }, - { "%", "6" }, - { "&", "7" }, - { "*", "8" }, - { "(", "9" }, - { ")", "0" }, - }; - - public static bool IsKeyPressed(string key, InputMode inputMode) - { -#pragma warning disable CS0612 // Type or member is obsolete - if (inputMode == InputMode.None) -#pragma warning restore CS0612 // Type or member is obsolete - { - return false; - } - - if (string.IsNullOrEmpty(key)) - { - return false; - } - - bool shiftRequired = false; - string keyName = key; - int startIndex = 0; - - foreach (string shiftModifier in ShiftModifiers) - { - if ( - key.StartsWith(shiftModifier, StringComparison.OrdinalIgnoreCase) - && key != shiftModifier - ) - { - shiftRequired = true; - startIndex = shiftModifier.Length; - break; - } - } - - if (!shiftRequired && key.Length == 1) - { - char keyChar = key[0]; - if (char.IsUpper(keyChar) && char.IsLetter(keyChar)) - { - shiftRequired = true; - } - else if ( - AlternativeSpecialShiftedKeyCodeMap.TryGetValue( - key, - out string legacyShiftedKeyName - ) - ) - { - shiftRequired = true; - keyName = legacyShiftedKeyName; - } - } - - if (0 < startIndex) - { - if (!CachedSubstrings.TryGetValue(key, out keyName)) - { - keyName = key[startIndex..]; - if (keyName.NeedsTrim()) - { - keyName = keyName.Trim(); - } - - if (keyName.Length == 1 && keyName.NeedsLowerInvariantConversion()) - { - keyName = keyName.ToLowerInvariant(); - } - - CachedSubstrings[key] = keyName; - } - } - - if (string.IsNullOrWhiteSpace(keyName)) - { - return false; - } -#pragma warning disable CS0612 // Type or member is obsolete - if (inputMode == InputMode.LegacyInputSystem) -#pragma warning restore CS0612 // Type or member is obsolete - { -#if ENABLE_LEGACY_INPUT_MANAGER - if ( - Enum.TryParse(keyName, ignoreCase: true, out KeyCode keyCode) - || KeyCodeMapping.TryGetValue(keyName, out keyCode) - ) - { - return Input.GetKeyDown(keyCode) - && ( - !shiftRequired - || Input.GetKey(KeyCode.LeftShift) - || Input.GetKey(KeyCode.LeftShift) - || Input.GetKey(KeyCode.RightShift) - || Input.GetKey(KeyCode.RightShift) - ); - } -#endif - - return false; - } -#pragma warning disable CS0612 // Type or member is obsolete - if (inputMode == InputMode.NewInputSystem) -#pragma warning restore CS0612 // Type or member is obsolete - { -#if ENABLE_INPUT_SYSTEM - if ( - !shiftRequired - && ( - AlternativeSpecialShiftedKeyCodeMap.TryGetValue( - keyName, - out string shiftedKeyName - ) || SpecialShiftedKeyCodeMap.TryGetValue(keyName, out shiftedKeyName) - ) - ) - { - shiftRequired = true; - keyName = shiftedKeyName; - } - - Keyboard currentKeyboard = Keyboard.current; - return (!shiftRequired || currentKeyboard.shiftKey.isPressed) - && ( - currentKeyboard.TryGetChildControl( - SpecialKeyCodeMap.GetValueOrDefault(keyName, keyName) - ) - is { wasPressedThisFrame: true } - || currentKeyboard.TryGetChildControl(keyName) - is { wasPressedThisFrame: true } - ); -#endif - } - return false; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Input +{ + using System; + using System.Collections.Generic; + using Extensions; + using UnityEngine; +#if ENABLE_INPUT_SYSTEM + using UnityEngine.InputSystem; + using UnityEngine.InputSystem.Controls; +#endif + + public static class InputHelpers + { + private static readonly string[] ShiftModifiers = { "shift+", "#" }; + + private static readonly Dictionary CachedSubstrings = new(); + + private static readonly Dictionary KeyCodeMapping = new( + StringComparer.OrdinalIgnoreCase + ) + { + { "~", KeyCode.BackQuote }, + { "`", KeyCode.BackQuote }, + { "!", KeyCode.Alpha1 }, + { "@", KeyCode.Alpha2 }, + { "#", KeyCode.Alpha3 }, + { "$", KeyCode.Alpha4 }, + { "%", KeyCode.Alpha5 }, + { "^", KeyCode.Alpha6 }, + { "&", KeyCode.Alpha7 }, + { "*", KeyCode.Alpha8 }, + { "(", KeyCode.Alpha9 }, + { ")", KeyCode.Alpha0 }, + { "-", KeyCode.Minus }, + { "_", KeyCode.Minus }, + { "=", KeyCode.Equals }, + { "+", KeyCode.Equals }, + { "[", KeyCode.LeftBracket }, + { "{", KeyCode.LeftBracket }, + { "]", KeyCode.RightBracket }, + { "}", KeyCode.RightBracket }, + { "\\", KeyCode.Backslash }, + { "|", KeyCode.Backslash }, + { ";", KeyCode.Semicolon }, + { ":", KeyCode.Semicolon }, + { "'", KeyCode.Quote }, + { "\"", KeyCode.Quote }, + { ",", KeyCode.Comma }, + { "<", KeyCode.Comma }, + { ".", KeyCode.Period }, + { ">", KeyCode.Period }, + { "/", KeyCode.Slash }, + { "?", KeyCode.Slash }, + { "1", KeyCode.Alpha1 }, + { "2", KeyCode.Alpha2 }, + { "3", KeyCode.Alpha3 }, + { "4", KeyCode.Alpha4 }, + { "5", KeyCode.Alpha5 }, + { "6", KeyCode.Alpha6 }, + { "7", KeyCode.Alpha7 }, + { "8", KeyCode.Alpha8 }, + { "9", KeyCode.Alpha9 }, + { "0", KeyCode.Alpha0 }, + { "numpad1", KeyCode.Keypad1 }, + { "keypad1", KeyCode.Keypad1 }, + { "numpad2", KeyCode.Keypad2 }, + { "keypad2", KeyCode.Keypad2 }, + { "numpad3", KeyCode.Keypad3 }, + { "keypad3", KeyCode.Keypad3 }, + { "numpad4", KeyCode.Keypad4 }, + { "keypad4", KeyCode.Keypad4 }, + { "numpad5", KeyCode.Keypad5 }, + { "keypad5", KeyCode.Keypad5 }, + { "numpad6", KeyCode.Keypad6 }, + { "keypad6", KeyCode.Keypad6 }, + { "numpad7", KeyCode.Keypad7 }, + { "keypad7", KeyCode.Keypad7 }, + { "numpad8", KeyCode.Keypad8 }, + { "keypad8", KeyCode.Keypad8 }, + { "numpad9", KeyCode.Keypad9 }, + { "keypad9", KeyCode.Keypad9 }, + { "numpad0", KeyCode.Keypad0 }, + { "keypad0", KeyCode.Keypad0 }, + { "numpadplus", KeyCode.KeypadPlus }, + { "keypadplus", KeyCode.KeypadPlus }, + { "numpad+", KeyCode.KeypadPlus }, + { "numpadminus", KeyCode.KeypadMinus }, + { "keypadminus", KeyCode.KeypadMinus }, + { "numpad-", KeyCode.KeypadMinus }, + { "numpadmultiply", KeyCode.KeypadMultiply }, + { "keypadmultiply", KeyCode.KeypadMultiply }, + { "numpad*", KeyCode.KeypadMultiply }, + { "numpaddivide", KeyCode.KeypadDivide }, + { "keypaddivide", KeyCode.KeypadDivide }, + { "numpad/", KeyCode.KeypadDivide }, + { "numpadenter", KeyCode.KeypadEnter }, + { "keypadenter", KeyCode.KeypadEnter }, + { "numpadperiod", KeyCode.KeypadPeriod }, + { "keypadperiod", KeyCode.KeypadPeriod }, + { "numpad.", KeyCode.KeypadPeriod }, + { "numpaddecimal", KeyCode.KeypadPeriod }, + { "numpadequals", KeyCode.KeypadEquals }, + { "keypadequals", KeyCode.KeypadEquals }, + { "numpad=", KeyCode.KeypadEquals }, + { "esc", KeyCode.Escape }, + { "escape", KeyCode.Escape }, + { "return", KeyCode.Return }, + { "enter", KeyCode.Return }, + { "space", KeyCode.Space }, + { "spacebar", KeyCode.Space }, + { "del", KeyCode.Delete }, + { "delete", KeyCode.Delete }, + { "ins", KeyCode.Insert }, + { "insert", KeyCode.Insert }, + { "pageup", KeyCode.PageUp }, + { "pgup", KeyCode.PageUp }, + { "pagedown", KeyCode.PageDown }, + { "pgdn", KeyCode.PageDown }, + { "pagedn", KeyCode.PageDown }, + { "lshift", KeyCode.LeftShift }, + { "rshift", KeyCode.RightShift }, + { "leftshift", KeyCode.LeftShift }, + { "rightshift", KeyCode.RightShift }, + { "lctrl", KeyCode.LeftControl }, + { "rctrl", KeyCode.RightControl }, + { "lcontrol", KeyCode.LeftControl }, + { "rcontrol", KeyCode.RightControl }, + { "leftctrl", KeyCode.LeftControl }, + { "rightctrl", KeyCode.RightControl }, + { "leftcontrol", KeyCode.LeftControl }, + { "rightcontrol", KeyCode.RightControl }, + { "lalt", KeyCode.LeftAlt }, + { "ralt", KeyCode.RightAlt }, + { "leftalt", KeyCode.LeftAlt }, + { "rightalt", KeyCode.RightAlt }, + { "lcmd", KeyCode.LeftCommand }, + { "rcmd", KeyCode.RightCommand }, + { "lcommand", KeyCode.LeftCommand }, + { "rcommand", KeyCode.RightCommand }, + { "leftcmd", KeyCode.LeftCommand }, + { "rightcmd", KeyCode.RightCommand }, + { "leftcommand", KeyCode.LeftCommand }, + { "rightcommand", KeyCode.RightCommand }, + { "lwin", KeyCode.LeftWindows }, + { "rwin", KeyCode.RightWindows }, + { "leftwindows", KeyCode.LeftWindows }, + { "rightwindows", KeyCode.RightWindows }, + { "leftwin", KeyCode.LeftWindows }, + { "rightwin", KeyCode.RightWindows }, + { "capslock", KeyCode.CapsLock }, + { "numlock", KeyCode.Numlock }, + { "scrolllock", KeyCode.ScrollLock }, + { "prtscn", KeyCode.Print }, + { "printscreen", KeyCode.Print }, + { "pausebreak", KeyCode.Pause }, + { "pause", KeyCode.Pause }, + { "up", KeyCode.UpArrow }, + { "uparrow", KeyCode.UpArrow }, + { "down", KeyCode.DownArrow }, + { "downarrow", KeyCode.DownArrow }, + { "left", KeyCode.LeftArrow }, + { "leftarrow", KeyCode.LeftArrow }, + { "right", KeyCode.RightArrow }, + { "rightarrow", KeyCode.RightArrow }, + { "f1", KeyCode.F1 }, + { "f2", KeyCode.F2 }, + { "f3", KeyCode.F3 }, + { "f4", KeyCode.F4 }, + { "f5", KeyCode.F5 }, + { "f6", KeyCode.F6 }, + { "f7", KeyCode.F7 }, + { "f8", KeyCode.F8 }, + { "f9", KeyCode.F9 }, + { "f10", KeyCode.F10 }, + { "f11", KeyCode.F11 }, + { "f12", KeyCode.F12 }, + { "f13", KeyCode.F13 }, + { "f14", KeyCode.F14 }, + { "f15", KeyCode.F15 }, + { "mouse0", KeyCode.Mouse0 }, + { "leftmouse", KeyCode.Mouse0 }, + { "lmb", KeyCode.Mouse0 }, + { "mouse1", KeyCode.Mouse1 }, + { "rightmouse", KeyCode.Mouse1 }, + { "rmb", KeyCode.Mouse1 }, + { "mouse2", KeyCode.Mouse2 }, + { "middlemouse", KeyCode.Mouse2 }, + { "mmb", KeyCode.Mouse2 }, + { "mouse3", KeyCode.Mouse3 }, + { "mouse4", KeyCode.Mouse4 }, + { "mouse5", KeyCode.Mouse5 }, + { "mouse6", KeyCode.Mouse6 }, + { "none", KeyCode.None }, + }; + + private static readonly Dictionary SpecialKeyCodeMap = new( + StringComparer.OrdinalIgnoreCase + ) + { + { "`", "backquote" }, + { "-", "minus" }, + { "=", "equals" }, + { "[", "leftBracket" }, + { "]", "rightBracket" }, + { ";", "semicolon" }, + { "'", "quote" }, + { "\\", "backslash" }, + { ",", "comma" }, + { ".", "period" }, + { "/", "slash" }, + { "1", "digit1" }, + { "2", "digit2" }, + { "3", "digit3" }, + { "4", "digit4" }, + { "5", "digit5" }, + { "6", "digit6" }, + { "7", "digit7" }, + { "8", "digit8" }, + { "9", "digit9" }, + { "0", "digit0" }, + { "up", "upArrow" }, + { "left", "leftArrow" }, + { "right", "rightArrow" }, + { "down", "downArrow" }, + { " ", "space" }, + }; + + private static readonly Dictionary SpecialShiftedKeyCodeMap = new( + StringComparer.OrdinalIgnoreCase + ) + { + { "~", "backquote" }, + { "!", "digit1" }, + { "@", "digit2" }, + { "#", "digit3" }, + { "$", "digit4" }, + { "^", "digit6" }, + { "%", "digit5" }, + { "&", "digit7" }, + { "*", "digit8" }, + { "(", "digit9" }, + { ")", "digit0" }, + { "_", "minus" }, + { "+", "equals" }, + { "{", "leftBracket" }, + { "}", "rightBracket" }, + { ":", "semicolon" }, + { "\"", "quote" }, + { "|", "backslash" }, + { "<", "comma" }, + { ">", "period" }, + { "?", "slash" }, + }; + + private static readonly Dictionary AlternativeSpecialShiftedKeyCodeMap = + new(StringComparer.OrdinalIgnoreCase) + { + { "!", "1" }, + { "@", "2" }, + { "#", "3" }, + { "$", "4" }, + { "^", "5" }, + { "%", "6" }, + { "&", "7" }, + { "*", "8" }, + { "(", "9" }, + { ")", "0" }, + }; + + public static bool IsKeyPressed(string key, InputMode inputMode) + { +#pragma warning disable CS0612 // Type or member is obsolete + if (inputMode == InputMode.None) +#pragma warning restore CS0612 // Type or member is obsolete + { + return false; + } + + if (string.IsNullOrEmpty(key)) + { + return false; + } + + bool shiftRequired = false; + string keyName = key; + int startIndex = 0; + + foreach (string shiftModifier in ShiftModifiers) + { + if ( + key.StartsWith(shiftModifier, StringComparison.OrdinalIgnoreCase) + && key != shiftModifier + ) + { + shiftRequired = true; + startIndex = shiftModifier.Length; + break; + } + } + + if (!shiftRequired && key.Length == 1) + { + char keyChar = key[0]; + if (char.IsUpper(keyChar) && char.IsLetter(keyChar)) + { + shiftRequired = true; + } + else if ( + AlternativeSpecialShiftedKeyCodeMap.TryGetValue( + key, + out string legacyShiftedKeyName + ) + ) + { + shiftRequired = true; + keyName = legacyShiftedKeyName; + } + } + + if (0 < startIndex) + { + if (!CachedSubstrings.TryGetValue(key, out keyName)) + { + keyName = key[startIndex..]; + if (keyName.NeedsTrim()) + { + keyName = keyName.Trim(); + } + + if (keyName.Length == 1 && keyName.NeedsLowerInvariantConversion()) + { + keyName = keyName.ToLowerInvariant(); + } + + CachedSubstrings[key] = keyName; + } + } + + if (string.IsNullOrWhiteSpace(keyName)) + { + return false; + } +#pragma warning disable CS0612 // Type or member is obsolete + if (inputMode == InputMode.LegacyInputSystem) +#pragma warning restore CS0612 // Type or member is obsolete + { +#if ENABLE_LEGACY_INPUT_MANAGER + if ( + Enum.TryParse(keyName, ignoreCase: true, out KeyCode keyCode) + || KeyCodeMapping.TryGetValue(keyName, out keyCode) + ) + { + return Input.GetKeyDown(keyCode) + && ( + !shiftRequired + || Input.GetKey(KeyCode.LeftShift) + || Input.GetKey(KeyCode.LeftShift) + || Input.GetKey(KeyCode.RightShift) + || Input.GetKey(KeyCode.RightShift) + ); + } +#endif + + return false; + } +#pragma warning disable CS0612 // Type or member is obsolete + if (inputMode == InputMode.NewInputSystem) +#pragma warning restore CS0612 // Type or member is obsolete + { +#if ENABLE_INPUT_SYSTEM + if ( + !shiftRequired + && ( + AlternativeSpecialShiftedKeyCodeMap.TryGetValue( + keyName, + out string shiftedKeyName + ) || SpecialShiftedKeyCodeMap.TryGetValue(keyName, out shiftedKeyName) + ) + ) + { + shiftRequired = true; + keyName = shiftedKeyName; + } + + Keyboard currentKeyboard = Keyboard.current; + return (!shiftRequired || currentKeyboard.shiftKey.isPressed) + && ( + currentKeyboard.TryGetChildControl( + SpecialKeyCodeMap.GetValueOrDefault(keyName, keyName) + ) + is { wasPressedThisFrame: true } + || currentKeyboard.TryGetChildControl(keyName) + is { wasPressedThisFrame: true } + ); +#endif + } + return false; + } + } +} diff --git a/Runtime/CommandTerminal/Input/InputMode.cs b/Runtime/CommandTerminal/Input/InputMode.cs index e363cca..bedd383 100644 --- a/Runtime/CommandTerminal/Input/InputMode.cs +++ b/Runtime/CommandTerminal/Input/InputMode.cs @@ -1,20 +1,20 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ - using System; - - public enum InputMode - { - [Obsolete] - None = 0, -#if !ENABLE_LEGACY_INPUT_MANAGER - [Obsolete] -#endif - LegacyInputSystem = 1 << 0 - , -#if !ENABLE_INPUT_SYSTEM - [Obsolete] -#endif - NewInputSystem = 1 << 1 - , - } -} +namespace WallstopStudios.DxCommandTerminal.Input +{ + using System; + + public enum InputMode + { + [Obsolete] + None = 0, +#if !ENABLE_LEGACY_INPUT_MANAGER + [Obsolete] +#endif + LegacyInputSystem = 1 << 0 + , +#if !ENABLE_INPUT_SYSTEM + [Obsolete] +#endif + NewInputSystem = 1 << 1 + , + } +} diff --git a/Runtime/CommandTerminal/Input/TerminalControlTypes.cs b/Runtime/CommandTerminal/Input/TerminalControlTypes.cs index b8835bc..2b96ee2 100644 --- a/Runtime/CommandTerminal/Input/TerminalControlTypes.cs +++ b/Runtime/CommandTerminal/Input/TerminalControlTypes.cs @@ -1,18 +1,18 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ - using System; - - public enum TerminalControlTypes - { - [Obsolete] - None = 0, - Close = 1, - EnterCommand = 2, - Previous = 3, - Next = 4, - ToggleFull = 5, - ToggleSmall = 6, - CompleteForward = 7, - CompleteBackward = 8, - } -} +namespace WallstopStudios.DxCommandTerminal.Input +{ + using System; + + public enum TerminalControlTypes + { + [Obsolete] + None = 0, + Close = 1, + EnterCommand = 2, + Previous = 3, + Next = 4, + ToggleFull = 5, + ToggleSmall = 6, + CompleteForward = 7, + CompleteBackward = 8, + } +} diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index 6bdd3f3..5d2e910 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -1,322 +1,322 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ - using System; - using System.Collections.Generic; - using System.Linq; - using UI; - using UnityEngine; - - [DisallowMultipleComponent] - public class TerminalKeyboardController : MonoBehaviour, IInputHandler - { - protected static readonly TerminalControlTypes[] ControlTypes = Enum.GetValues( - typeof(TerminalControlTypes) - ) - .OfType() -#pragma warning disable CS0612 // Type or member is obsolete - .Except(new[] { TerminalControlTypes.None }) -#pragma warning restore CS0612 // Type or member is obsolete - .ToArray(); - - public bool ShouldHandleInputThisFrame - { - get - { - foreach (TerminalControlTypes controlType in _controlOrder) - { - if (!_inputChecks.TryGetValue(controlType, out Func inputCheck)) - { - continue; - } - if (inputCheck()) - { - return true; - } - } - - return false; - } - } - - [Header("System")] - public InputMode inputMode = -#if ENABLE_INPUT_SYSTEM - InputMode.NewInputSystem; -#else - InputMode.LegacyInputSystem; -#endif - public TerminalUI terminal; - - [Header("Hotkeys")] - [SerializeField] - public string toggleHotkey = "`"; - - [SerializeField] - public string toggleFullHotkey = "#`"; - - [SerializeField] - public string completeHotkey = "tab"; - - [SerializeField] - public string reverseCompleteHotkey = "#tab"; - - [SerializeField] - public string previousHotkey = "up"; - - [SerializeField] - public List _completeCommandHotkeys = new() { "enter", "return" }; - - [SerializeField] - public string closeHotkey = "escape"; - - [SerializeField] - public string nextHotkey = "down"; - - [SerializeField] - [Tooltip("Re-order these to choose what priority you want input to be checked in")] - protected List _controlOrder = new() - { - TerminalControlTypes.Close, - TerminalControlTypes.EnterCommand, - TerminalControlTypes.Previous, - TerminalControlTypes.Next, - TerminalControlTypes.ToggleFull, - TerminalControlTypes.ToggleSmall, - TerminalControlTypes.CompleteBackward, - TerminalControlTypes.CompleteForward, - }; - - protected readonly Dictionary> _inputChecks = new(); - protected readonly Dictionary _controlHandlerActions = new(); - - public TerminalKeyboardController() - { - _inputChecks.Clear(); - _inputChecks[TerminalControlTypes.Close] = IsClosePressed; - _inputChecks[TerminalControlTypes.EnterCommand] = IsEnterCommandPressed; - _inputChecks[TerminalControlTypes.Previous] = IsPreviousPressed; - _inputChecks[TerminalControlTypes.Next] = IsNextPressed; - _inputChecks[TerminalControlTypes.ToggleFull] = IsToggleFullPressed; - _inputChecks[TerminalControlTypes.ToggleSmall] = IsToggleSmallPressed; - _inputChecks[TerminalControlTypes.CompleteBackward] = IsCompleteBackwardPressed; - _inputChecks[TerminalControlTypes.CompleteForward] = IsCompletePressed; - - _controlHandlerActions.Clear(); - _controlHandlerActions[TerminalControlTypes.Close] = Close; - _controlHandlerActions[TerminalControlTypes.EnterCommand] = EnterCommand; - _controlHandlerActions[TerminalControlTypes.Previous] = Previous; - _controlHandlerActions[TerminalControlTypes.Next] = Next; - _controlHandlerActions[TerminalControlTypes.ToggleFull] = ToggleFull; - _controlHandlerActions[TerminalControlTypes.ToggleSmall] = ToggleSmall; - _controlHandlerActions[TerminalControlTypes.CompleteBackward] = CompleteBackward; - _controlHandlerActions[TerminalControlTypes.CompleteForward] = Complete; - } - - protected virtual void Awake() - { - if (terminal != null) - { - return; - } - - if (!TryGetComponent(out terminal)) - { - Debug.LogError("Failed to find TerminalUI, Input will not work.", this); - } - - if (_controlOrder is not { Count: > 0 }) - { - Debug.LogError("No controls specified, Input will not work.", this); - } - else - { - VerifyControlOrderIntegrity(); - } - } - - protected virtual void OnValidate() - { - if (!Application.isPlaying) - { - VerifyControlOrderIntegrity(); - } - } - - private void VerifyControlOrderIntegrity() - { - if (!_controlOrder.ToHashSet().SetEquals(ControlTypes)) - { - Debug.LogWarning( - $"Control Order is missing the following controls: [{string.Join(", ", ControlTypes.Except(_controlOrder))}]. " - + "Input for these will not be handled. Is this intentional?", - this - ); - } - } - - protected virtual void Update() - { - if (_controlOrder is not { Count: > 0 }) - { - return; - } - - foreach (TerminalControlTypes controlType in _controlOrder) - { - if (!_inputChecks.TryGetValue(controlType, out Func inputCheck)) - { - continue; - } - - if (!inputCheck()) - { - continue; - } - - if (!_controlHandlerActions.TryGetValue(controlType, out Action action)) - { - continue; - } - - action(); - break; - } - } - - #region Commands - - protected virtual void Close() - { - if (terminal == null) - { - return; - } - - terminal.Close(); - } - - protected virtual void EnterCommand() - { - if (terminal == null) - { - return; - } - terminal.EnterCommand(); - } - - protected virtual void Previous() - { - if (terminal == null) - { - return; - } - terminal.HandlePrevious(); - } - - protected virtual void Next() - { - if (terminal == null) - { - return; - } - terminal.HandleNext(); - } - - protected virtual void ToggleFull() - { - if (terminal == null) - { - return; - } - terminal.ToggleFull(); - } - - protected virtual void ToggleSmall() - { - if (terminal == null) - { - return; - } - terminal.ToggleSmall(); - } - - protected virtual void Complete() - { - if (terminal == null) - { - return; - } - - terminal.CompleteCommand(searchForward: true); - } - - protected virtual void CompleteBackward() - { - if (terminal == null) - { - return; - } - terminal.CompleteCommand(searchForward: false); - } - - #endregion - - - #region Control Checks - - protected virtual bool IsClosePressed() - { - return InputHelpers.IsKeyPressed(closeHotkey, inputMode); - } - - protected virtual bool IsPreviousPressed() - { - return InputHelpers.IsKeyPressed(previousHotkey, inputMode); - } - - protected virtual bool IsNextPressed() - { - return InputHelpers.IsKeyPressed(nextHotkey, inputMode); - } - - protected virtual bool IsToggleFullPressed() - { - return InputHelpers.IsKeyPressed(toggleFullHotkey, inputMode); - } - - protected virtual bool IsToggleSmallPressed() - { - return InputHelpers.IsKeyPressed(toggleHotkey, inputMode); - } - - protected virtual bool IsCompleteBackwardPressed() - { - return InputHelpers.IsKeyPressed(reverseCompleteHotkey, inputMode); - } - - protected virtual bool IsCompletePressed() - { - return InputHelpers.IsKeyPressed(completeHotkey, inputMode); - } - - protected virtual bool IsEnterCommandPressed() - { - if (_completeCommandHotkeys is not { Count: > 0 }) - { - return false; - } - - foreach (string command in _completeCommandHotkeys) - { - if (InputHelpers.IsKeyPressed(command, inputMode)) - { - return true; - } - } - - return false; - } - - #endregion - } -} +namespace WallstopStudios.DxCommandTerminal.Input +{ + using System; + using System.Collections.Generic; + using System.Linq; + using UI; + using UnityEngine; + + [DisallowMultipleComponent] + public class TerminalKeyboardController : MonoBehaviour, IInputHandler + { + protected static readonly TerminalControlTypes[] ControlTypes = Enum.GetValues( + typeof(TerminalControlTypes) + ) + .OfType() +#pragma warning disable CS0612 // Type or member is obsolete + .Except(new[] { TerminalControlTypes.None }) +#pragma warning restore CS0612 // Type or member is obsolete + .ToArray(); + + public bool ShouldHandleInputThisFrame + { + get + { + foreach (TerminalControlTypes controlType in _controlOrder) + { + if (!_inputChecks.TryGetValue(controlType, out Func inputCheck)) + { + continue; + } + if (inputCheck()) + { + return true; + } + } + + return false; + } + } + + [Header("System")] + public InputMode inputMode = +#if ENABLE_INPUT_SYSTEM + InputMode.NewInputSystem; +#else + InputMode.LegacyInputSystem; +#endif + public TerminalUI terminal; + + [Header("Hotkeys")] + [SerializeField] + public string toggleHotkey = "`"; + + [SerializeField] + public string toggleFullHotkey = "#`"; + + [SerializeField] + public string completeHotkey = "tab"; + + [SerializeField] + public string reverseCompleteHotkey = "#tab"; + + [SerializeField] + public string previousHotkey = "up"; + + [SerializeField] + public List _completeCommandHotkeys = new() { "enter", "return" }; + + [SerializeField] + public string closeHotkey = "escape"; + + [SerializeField] + public string nextHotkey = "down"; + + [SerializeField] + [Tooltip("Re-order these to choose what priority you want input to be checked in")] + protected List _controlOrder = new() + { + TerminalControlTypes.Close, + TerminalControlTypes.EnterCommand, + TerminalControlTypes.Previous, + TerminalControlTypes.Next, + TerminalControlTypes.ToggleFull, + TerminalControlTypes.ToggleSmall, + TerminalControlTypes.CompleteBackward, + TerminalControlTypes.CompleteForward, + }; + + protected readonly Dictionary> _inputChecks = new(); + protected readonly Dictionary _controlHandlerActions = new(); + + public TerminalKeyboardController() + { + _inputChecks.Clear(); + _inputChecks[TerminalControlTypes.Close] = IsClosePressed; + _inputChecks[TerminalControlTypes.EnterCommand] = IsEnterCommandPressed; + _inputChecks[TerminalControlTypes.Previous] = IsPreviousPressed; + _inputChecks[TerminalControlTypes.Next] = IsNextPressed; + _inputChecks[TerminalControlTypes.ToggleFull] = IsToggleFullPressed; + _inputChecks[TerminalControlTypes.ToggleSmall] = IsToggleSmallPressed; + _inputChecks[TerminalControlTypes.CompleteBackward] = IsCompleteBackwardPressed; + _inputChecks[TerminalControlTypes.CompleteForward] = IsCompletePressed; + + _controlHandlerActions.Clear(); + _controlHandlerActions[TerminalControlTypes.Close] = Close; + _controlHandlerActions[TerminalControlTypes.EnterCommand] = EnterCommand; + _controlHandlerActions[TerminalControlTypes.Previous] = Previous; + _controlHandlerActions[TerminalControlTypes.Next] = Next; + _controlHandlerActions[TerminalControlTypes.ToggleFull] = ToggleFull; + _controlHandlerActions[TerminalControlTypes.ToggleSmall] = ToggleSmall; + _controlHandlerActions[TerminalControlTypes.CompleteBackward] = CompleteBackward; + _controlHandlerActions[TerminalControlTypes.CompleteForward] = Complete; + } + + protected virtual void Awake() + { + if (terminal != null) + { + return; + } + + if (!TryGetComponent(out terminal)) + { + Debug.LogError("Failed to find TerminalUI, Input will not work.", this); + } + + if (_controlOrder is not { Count: > 0 }) + { + Debug.LogError("No controls specified, Input will not work.", this); + } + else + { + VerifyControlOrderIntegrity(); + } + } + + protected virtual void OnValidate() + { + if (!Application.isPlaying) + { + VerifyControlOrderIntegrity(); + } + } + + private void VerifyControlOrderIntegrity() + { + if (!_controlOrder.ToHashSet().SetEquals(ControlTypes)) + { + Debug.LogWarning( + $"Control Order is missing the following controls: [{string.Join(", ", ControlTypes.Except(_controlOrder))}]. " + + "Input for these will not be handled. Is this intentional?", + this + ); + } + } + + protected virtual void Update() + { + if (_controlOrder is not { Count: > 0 }) + { + return; + } + + foreach (TerminalControlTypes controlType in _controlOrder) + { + if (!_inputChecks.TryGetValue(controlType, out Func inputCheck)) + { + continue; + } + + if (!inputCheck()) + { + continue; + } + + if (!_controlHandlerActions.TryGetValue(controlType, out Action action)) + { + continue; + } + + action(); + break; + } + } + + #region Commands + + protected virtual void Close() + { + if (terminal == null) + { + return; + } + + terminal.Close(); + } + + protected virtual void EnterCommand() + { + if (terminal == null) + { + return; + } + terminal.EnterCommand(); + } + + protected virtual void Previous() + { + if (terminal == null) + { + return; + } + terminal.HandlePrevious(); + } + + protected virtual void Next() + { + if (terminal == null) + { + return; + } + terminal.HandleNext(); + } + + protected virtual void ToggleFull() + { + if (terminal == null) + { + return; + } + terminal.ToggleFull(); + } + + protected virtual void ToggleSmall() + { + if (terminal == null) + { + return; + } + terminal.ToggleSmall(); + } + + protected virtual void Complete() + { + if (terminal == null) + { + return; + } + + terminal.CompleteCommand(searchForward: true); + } + + protected virtual void CompleteBackward() + { + if (terminal == null) + { + return; + } + terminal.CompleteCommand(searchForward: false); + } + + #endregion + + + #region Control Checks + + protected virtual bool IsClosePressed() + { + return InputHelpers.IsKeyPressed(closeHotkey, inputMode); + } + + protected virtual bool IsPreviousPressed() + { + return InputHelpers.IsKeyPressed(previousHotkey, inputMode); + } + + protected virtual bool IsNextPressed() + { + return InputHelpers.IsKeyPressed(nextHotkey, inputMode); + } + + protected virtual bool IsToggleFullPressed() + { + return InputHelpers.IsKeyPressed(toggleFullHotkey, inputMode); + } + + protected virtual bool IsToggleSmallPressed() + { + return InputHelpers.IsKeyPressed(toggleHotkey, inputMode); + } + + protected virtual bool IsCompleteBackwardPressed() + { + return InputHelpers.IsKeyPressed(reverseCompleteHotkey, inputMode); + } + + protected virtual bool IsCompletePressed() + { + return InputHelpers.IsKeyPressed(completeHotkey, inputMode); + } + + protected virtual bool IsEnterCommandPressed() + { + if (_completeCommandHotkeys is not { Count: > 0 }) + { + return false; + } + + foreach (string command in _completeCommandHotkeys) + { + if (InputHelpers.IsKeyPressed(command, inputMode)) + { + return true; + } + } + + return false; + } + + #endregion + } +} diff --git a/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs b/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs index aa96e1d..a527154 100644 --- a/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs +++ b/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs @@ -1,163 +1,163 @@ -namespace WallstopStudios.DxCommandTerminal.Input -{ -#if ENABLE_INPUT_SYSTEM - using UI; - using UnityEngine; - using UnityEngine.InputSystem; - - [DisallowMultipleComponent] - public class TerminalPlayerInputController : MonoBehaviour - { - [Header("System")] - public bool enableWarnings = true; - - public TerminalUI terminal; - - protected bool _enabled; - - [SerializeField] - protected PlayerInput _serializedPlayerInput; - - protected PlayerInput _playerInput; - - protected virtual void Awake() - { - _playerInput = _serializedPlayerInput; - if (_playerInput == null) - { - if (!TryGetComponent(out _playerInput) && enableWarnings) - { - Debug.LogWarning( - "No PlayerInput attached, events may not work (which is the point of this component).", - this - ); - } - } - - if (terminal != null) - { - return; - } - - if (!TryGetComponent(out terminal)) - { - Debug.LogError("Failed to find TerminalUI, Input will not work.", this); - } - } - - protected virtual void OnEnable() - { - _enabled = true; - } - - protected virtual void OnDisable() - { - _enabled = false; - } - - public virtual void OnHandlePrevious(InputValue inputValue) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.HandlePrevious(); - } - - public virtual void OnHandleNext(InputValue inputValue) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.HandleNext(); - } - - public virtual void OnClose(InputValue inputValue) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.Close(); - } - - public virtual void OnToggleSmall(InputValue inputValue) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.ToggleSmall(); - } - - public virtual void OnToggleFull(InputValue inputValue) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.ToggleFull(); - } - - public virtual void OnCompleteCommand(InputValue input) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.CompleteCommand(searchForward: true); - } - - public virtual void OnReverseCompleteCommand(InputValue input) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.CompleteCommand(searchForward: false); - } - - public virtual void OnEnterCommand(InputValue inputValue) - { - if (!_enabled) - { - return; - } - if (terminal == null) - { - return; - } - terminal.EnterCommand(); - } - } -#endif -} +namespace WallstopStudios.DxCommandTerminal.Input +{ +#if ENABLE_INPUT_SYSTEM + using UI; + using UnityEngine; + using UnityEngine.InputSystem; + + [DisallowMultipleComponent] + public class TerminalPlayerInputController : MonoBehaviour + { + [Header("System")] + public bool enableWarnings = true; + + public TerminalUI terminal; + + protected bool _enabled; + + [SerializeField] + protected PlayerInput _serializedPlayerInput; + + protected PlayerInput _playerInput; + + protected virtual void Awake() + { + _playerInput = _serializedPlayerInput; + if (_playerInput == null) + { + if (!TryGetComponent(out _playerInput) && enableWarnings) + { + Debug.LogWarning( + "No PlayerInput attached, events may not work (which is the point of this component).", + this + ); + } + } + + if (terminal != null) + { + return; + } + + if (!TryGetComponent(out terminal)) + { + Debug.LogError("Failed to find TerminalUI, Input will not work.", this); + } + } + + protected virtual void OnEnable() + { + _enabled = true; + } + + protected virtual void OnDisable() + { + _enabled = false; + } + + public virtual void OnHandlePrevious(InputValue inputValue) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.HandlePrevious(); + } + + public virtual void OnHandleNext(InputValue inputValue) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.HandleNext(); + } + + public virtual void OnClose(InputValue inputValue) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.Close(); + } + + public virtual void OnToggleSmall(InputValue inputValue) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.ToggleSmall(); + } + + public virtual void OnToggleFull(InputValue inputValue) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.ToggleFull(); + } + + public virtual void OnCompleteCommand(InputValue input) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.CompleteCommand(searchForward: true); + } + + public virtual void OnReverseCompleteCommand(InputValue input) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.CompleteCommand(searchForward: false); + } + + public virtual void OnEnterCommand(InputValue inputValue) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.EnterCommand(); + } + } +#endif +} diff --git a/Runtime/CommandTerminal/Persistence/TerminalThemeConfiguration.cs b/Runtime/CommandTerminal/Persistence/TerminalThemeConfiguration.cs index ee48b4c..2557583 100644 --- a/Runtime/CommandTerminal/Persistence/TerminalThemeConfiguration.cs +++ b/Runtime/CommandTerminal/Persistence/TerminalThemeConfiguration.cs @@ -1,49 +1,49 @@ -namespace WallstopStudios.DxCommandTerminal.Persistence -{ - using System; - - [Serializable] - public struct TerminalThemeConfiguration : IEquatable - { - public const int HashBase = 397; - - public string terminalId; - public string font; - public string theme; - - public bool Equals(TerminalThemeConfiguration other) - { - return string.Equals(terminalId, other.terminalId, StringComparison.OrdinalIgnoreCase) - && string.Equals(font, other.font, StringComparison.OrdinalIgnoreCase) - && string.Equals(theme, other.theme, StringComparison.OrdinalIgnoreCase); - } - - public override bool Equals(object obj) - { - if (obj is TerminalThemeConfiguration config) - { - return Equals(config); - } - - return false; - } - - public override int GetHashCode() - { - // Auto generated garbage - unchecked - { - int hashCode = ( - terminalId != null ? terminalId.ToLowerInvariant().GetHashCode() : 0 - ); - hashCode = - (hashCode * HashBase) - ^ (font != null ? font.ToLowerInvariant().GetHashCode() : 0); - hashCode = - (hashCode * HashBase) - ^ (theme != null ? theme.ToLowerInvariant().GetHashCode() : 0); - return hashCode; - } - } - } -} +namespace WallstopStudios.DxCommandTerminal.Persistence +{ + using System; + + [Serializable] + public struct TerminalThemeConfiguration : IEquatable + { + public const int HashBase = 397; + + public string terminalId; + public string font; + public string theme; + + public bool Equals(TerminalThemeConfiguration other) + { + return string.Equals(terminalId, other.terminalId, StringComparison.OrdinalIgnoreCase) + && string.Equals(font, other.font, StringComparison.OrdinalIgnoreCase) + && string.Equals(theme, other.theme, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + if (obj is TerminalThemeConfiguration config) + { + return Equals(config); + } + + return false; + } + + public override int GetHashCode() + { + // Auto generated garbage + unchecked + { + int hashCode = ( + terminalId != null ? terminalId.ToLowerInvariant().GetHashCode() : 0 + ); + hashCode = + (hashCode * HashBase) + ^ (font != null ? font.ToLowerInvariant().GetHashCode() : 0); + hashCode = + (hashCode * HashBase) + ^ (theme != null ? theme.ToLowerInvariant().GetHashCode() : 0); + return hashCode; + } + } + } +} diff --git a/Runtime/CommandTerminal/Persistence/TerminalThemeConfigurations.cs b/Runtime/CommandTerminal/Persistence/TerminalThemeConfigurations.cs index 5589008..56bb3c4 100644 --- a/Runtime/CommandTerminal/Persistence/TerminalThemeConfigurations.cs +++ b/Runtime/CommandTerminal/Persistence/TerminalThemeConfigurations.cs @@ -1,81 +1,81 @@ -namespace WallstopStudios.DxCommandTerminal.Persistence -{ - using System; - using System.Collections.Generic; - using UI; - - [Serializable] - public sealed class TerminalThemeConfigurations - { - public List configurations = new(); - - public bool TryGetConfiguration( - TerminalUI terminal, - out TerminalThemeConfiguration configuration - ) - { - int existingIndex = -1; - if (terminal != null) - { - for (int i = 0; i < configurations.Count; ++i) - { - if ( - string.Equals( - terminal.id, - configurations[i].terminalId, - StringComparison.OrdinalIgnoreCase - ) - ) - { - existingIndex = i; - break; - } - } - } - - bool exists = 0 <= existingIndex; - configuration = exists ? configurations[existingIndex] : default; - - return exists; - } - - /// - /// - /// - /// - /// True if a mutation happened, false if it was a no-op - public bool AddOrUpdate(TerminalThemeConfiguration configuration) - { - int existingIndex = -1; - for (int i = 0; i < configurations.Count; ++i) - { - if ( - string.Equals( - configuration.terminalId, - configurations[i].terminalId, - StringComparison.OrdinalIgnoreCase - ) - ) - { - existingIndex = i; - break; - } - } - - if (0 <= existingIndex) - { - if (configurations[existingIndex].Equals(configuration)) - { - return false; - } - configurations[existingIndex] = configuration; - } - else - { - configurations.Add(configuration); - } - - return true; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Persistence +{ + using System; + using System.Collections.Generic; + using UI; + + [Serializable] + public sealed class TerminalThemeConfigurations + { + public List configurations = new(); + + public bool TryGetConfiguration( + TerminalUI terminal, + out TerminalThemeConfiguration configuration + ) + { + int existingIndex = -1; + if (terminal != null) + { + for (int i = 0; i < configurations.Count; ++i) + { + if ( + string.Equals( + terminal.id, + configurations[i].terminalId, + StringComparison.OrdinalIgnoreCase + ) + ) + { + existingIndex = i; + break; + } + } + } + + bool exists = 0 <= existingIndex; + configuration = exists ? configurations[existingIndex] : default; + + return exists; + } + + /// + /// + /// + /// + /// True if a mutation happened, false if it was a no-op + public bool AddOrUpdate(TerminalThemeConfiguration configuration) + { + int existingIndex = -1; + for (int i = 0; i < configurations.Count; ++i) + { + if ( + string.Equals( + configuration.terminalId, + configurations[i].terminalId, + StringComparison.OrdinalIgnoreCase + ) + ) + { + existingIndex = i; + break; + } + } + + if (0 <= existingIndex) + { + if (configurations[existingIndex].Equals(configuration)) + { + return false; + } + configurations[existingIndex] = configuration; + } + else + { + configurations.Add(configuration); + } + + return true; + } + } +} diff --git a/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs b/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs index 931bff5..6515f3c 100644 --- a/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs +++ b/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs @@ -1,308 +1,308 @@ -namespace WallstopStudios.DxCommandTerminal.Persistence -{ - using System; - using System.Collections; - using System.IO; - using System.Threading.Tasks; - using Attributes; - using UI; - using UnityEngine; - - [DisallowMultipleComponent] - public class TerminalThemePersister : MonoBehaviour - { - protected virtual string ThemeFile => - Path.Join(Application.persistentDataPath, "DxCommandTerminal", "TerminalTheme.json"); - - [Header("System")] - public TerminalUI terminal; - - [Header("Config")] - public bool savePeriodically = true; - - [DxShowIf(nameof(savePeriodically))] - public float savePeriod = 1f; - - protected Font _lastSeenFont; - protected string _lastSeenTheme; - protected float? _nextUpdateTime; - - protected bool _persisting; - protected Coroutine _persistence; - - protected virtual void Awake() - { - if (terminal != null) - { - return; - } - - if (!TryGetComponent(out terminal)) - { - Debug.LogError("Failed to find TerminalUI, Theme persistence will not work.", this); - } - } - - protected virtual IEnumerator Start() - { - if (terminal == null) - { - yield break; - } - - string themeFile = ThemeFile; - Debug.Log($"Attempting to initialize from {themeFile}...", this); - yield return CheckAndPersistAnyChanges(hydrate: true); - } - - protected virtual void Update() - { - if (!savePeriodically) - { - return; - } - - if (Time.time <= _nextUpdateTime) - { - return; - } - - if (terminal == null) - { - return; - } - - if ( - _lastSeenFont == terminal.CurrentFont - && string.Equals( - _lastSeenTheme, - terminal.CurrentTheme, - StringComparison.OrdinalIgnoreCase - ) - ) - { - _nextUpdateTime = Time.time + savePeriod; - return; - } - - if (_persisting) - { - return; - } - - if (_persistence != null) - { - return; - } - - _persistence = StartCoroutine(CheckAndPersistAnyChanges(hydrate: false)); - } - - protected virtual IEnumerator CheckAndPersistAnyChanges(bool hydrate) - { - _lastSeenFont = terminal.CurrentFont; - _lastSeenTheme = terminal.CurrentTheme; - _persisting = true; - try - { - if (terminal == null) - { - yield break; - } - - string themeFile = ThemeFile; - string directoryPath = Path.GetDirectoryName(themeFile); - if (!string.IsNullOrWhiteSpace(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } - - TerminalThemeConfigurations configurations; - if (File.Exists(themeFile)) - { - Task readerTask = File.ReadAllTextAsync(themeFile); - while (!readerTask.IsCompleted) - { - yield return null; - } - - if (!readerTask.IsCompletedSuccessfully) - { - _lastSeenFont = null; - _lastSeenTheme = null; - Debug.LogError( - $"Failed to read theme file {themeFile}: {readerTask.Exception}.", - this - ); - yield break; - } - - string inputJson = readerTask.Result; - try - { - configurations = JsonUtility.FromJson( - inputJson - ); - } - catch (Exception e) - { - Debug.LogError( - $"Failed to parse theme file {themeFile}, defaulting to empty theme file: {e}", - this - ); - configurations = new TerminalThemeConfigurations(); - } - } - else - { - Debug.Log( - $"Creating new theme file {themeFile} for terminal {terminal.id} ...", - this - ); - configurations = new TerminalThemeConfigurations(); - } - - if (hydrate) - { - if ( - configurations.TryGetConfiguration( - terminal, - out TerminalThemeConfiguration existingConfiguration - ) - ) - { - int fontIndex; - if (terminal._fontPack == null) - { - fontIndex = -1; - } - else - { - fontIndex = terminal._fontPack._fonts.FindIndex(font => - string.Equals( - font.name, - existingConfiguration.font, - StringComparison.OrdinalIgnoreCase - ) - ); - } - - if (0 <= fontIndex) - { - _lastSeenFont = terminal._fontPack._fonts[fontIndex]; - terminal.SetFont(_lastSeenFont, persist: true); - } - else - { - Debug.LogWarning( - $"Failed to find persisted font {existingConfiguration.font} for terminal {terminal.id} while hydrating.", - this - ); - } - - int themeIndex; - if (terminal._themePack == null) - { - themeIndex = -1; - } - else - { - themeIndex = terminal._themePack._themeNames.FindIndex(theme => - string.Equals( - theme, - existingConfiguration.theme, - StringComparison.OrdinalIgnoreCase - ) - ); - } - - if (0 <= themeIndex) - { - _lastSeenTheme = terminal._themePack._themeNames[themeIndex]; - terminal.SetTheme(_lastSeenTheme, persist: true); - } - else - { - Debug.LogWarning( - $"Failed to find persisted theme {existingConfiguration.theme} for terminal {terminal.id} while hydrating.", - this - ); - } - - yield break; - } - else - { - Debug.Log( - $"Failed to find persisted configuration for terminal {terminal.id} while hydrating, defaulting to Prefab configuration.", - this - ); - } - } - - TerminalThemeConfiguration? maybeCurrentConfiguration = GetConfiguration(); - if (maybeCurrentConfiguration == null) - { - yield break; - } - - TerminalThemeConfiguration currentConfiguration = maybeCurrentConfiguration.Value; - if (!configurations.AddOrUpdate(currentConfiguration)) - { - yield break; - } - - string outputJson = JsonUtility.ToJson(configurations, prettyPrint: true); - Debug.Log( - $"Writing theme file {themeFile} with contents:{Environment.NewLine}{outputJson}", - this - ); - Task writerTask = File.WriteAllTextAsync(themeFile, outputJson); - while (!writerTask.IsCompleted) - { - yield return null; - } - - if (!writerTask.IsCompletedSuccessfully) - { - _lastSeenFont = null; - _lastSeenTheme = null; - Debug.LogError( - $"Failed to write theme file {themeFile} (terminal {terminal.id}): {writerTask.Exception}", - this - ); - } - else - { - Debug.Log($"Theme file {themeFile} successfully updated.", this); - } - } - finally - { - _nextUpdateTime = Time.time + savePeriod; - _persisting = false; - _persistence = null; - } - } - - public virtual TerminalThemeConfiguration? GetConfiguration() - { - if (terminal == null) - { - return null; - } - - if (string.IsNullOrWhiteSpace(terminal.id)) - { - return null; - } - - return new TerminalThemeConfiguration - { - terminalId = terminal.id, - font = terminal.CurrentFont == null ? string.Empty : terminal.CurrentFont.name, - theme = terminal.CurrentTheme ?? string.Empty, - }; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Persistence +{ + using System; + using System.Collections; + using System.IO; + using System.Threading.Tasks; + using Attributes; + using UI; + using UnityEngine; + + [DisallowMultipleComponent] + public class TerminalThemePersister : MonoBehaviour + { + protected virtual string ThemeFile => + Path.Join(Application.persistentDataPath, "DxCommandTerminal", "TerminalTheme.json"); + + [Header("System")] + public TerminalUI terminal; + + [Header("Config")] + public bool savePeriodically = true; + + [DxShowIf(nameof(savePeriodically))] + public float savePeriod = 1f; + + protected Font _lastSeenFont; + protected string _lastSeenTheme; + protected float? _nextUpdateTime; + + protected bool _persisting; + protected Coroutine _persistence; + + protected virtual void Awake() + { + if (terminal != null) + { + return; + } + + if (!TryGetComponent(out terminal)) + { + Debug.LogError("Failed to find TerminalUI, Theme persistence will not work.", this); + } + } + + protected virtual IEnumerator Start() + { + if (terminal == null) + { + yield break; + } + + string themeFile = ThemeFile; + Debug.Log($"Attempting to initialize from {themeFile}...", this); + yield return CheckAndPersistAnyChanges(hydrate: true); + } + + protected virtual void Update() + { + if (!savePeriodically) + { + return; + } + + if (Time.time <= _nextUpdateTime) + { + return; + } + + if (terminal == null) + { + return; + } + + if ( + _lastSeenFont == terminal.CurrentFont + && string.Equals( + _lastSeenTheme, + terminal.CurrentTheme, + StringComparison.OrdinalIgnoreCase + ) + ) + { + _nextUpdateTime = Time.time + savePeriod; + return; + } + + if (_persisting) + { + return; + } + + if (_persistence != null) + { + return; + } + + _persistence = StartCoroutine(CheckAndPersistAnyChanges(hydrate: false)); + } + + protected virtual IEnumerator CheckAndPersistAnyChanges(bool hydrate) + { + _lastSeenFont = terminal.CurrentFont; + _lastSeenTheme = terminal.CurrentTheme; + _persisting = true; + try + { + if (terminal == null) + { + yield break; + } + + string themeFile = ThemeFile; + string directoryPath = Path.GetDirectoryName(themeFile); + if (!string.IsNullOrWhiteSpace(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + TerminalThemeConfigurations configurations; + if (File.Exists(themeFile)) + { + Task readerTask = File.ReadAllTextAsync(themeFile); + while (!readerTask.IsCompleted) + { + yield return null; + } + + if (!readerTask.IsCompletedSuccessfully) + { + _lastSeenFont = null; + _lastSeenTheme = null; + Debug.LogError( + $"Failed to read theme file {themeFile}: {readerTask.Exception}.", + this + ); + yield break; + } + + string inputJson = readerTask.Result; + try + { + configurations = JsonUtility.FromJson( + inputJson + ); + } + catch (Exception e) + { + Debug.LogError( + $"Failed to parse theme file {themeFile}, defaulting to empty theme file: {e}", + this + ); + configurations = new TerminalThemeConfigurations(); + } + } + else + { + Debug.Log( + $"Creating new theme file {themeFile} for terminal {terminal.id} ...", + this + ); + configurations = new TerminalThemeConfigurations(); + } + + if (hydrate) + { + if ( + configurations.TryGetConfiguration( + terminal, + out TerminalThemeConfiguration existingConfiguration + ) + ) + { + int fontIndex; + if (terminal._fontPack == null) + { + fontIndex = -1; + } + else + { + fontIndex = terminal._fontPack._fonts.FindIndex(font => + string.Equals( + font.name, + existingConfiguration.font, + StringComparison.OrdinalIgnoreCase + ) + ); + } + + if (0 <= fontIndex) + { + _lastSeenFont = terminal._fontPack._fonts[fontIndex]; + terminal.SetFont(_lastSeenFont, persist: true); + } + else + { + Debug.LogWarning( + $"Failed to find persisted font {existingConfiguration.font} for terminal {terminal.id} while hydrating.", + this + ); + } + + int themeIndex; + if (terminal._themePack == null) + { + themeIndex = -1; + } + else + { + themeIndex = terminal._themePack._themeNames.FindIndex(theme => + string.Equals( + theme, + existingConfiguration.theme, + StringComparison.OrdinalIgnoreCase + ) + ); + } + + if (0 <= themeIndex) + { + _lastSeenTheme = terminal._themePack._themeNames[themeIndex]; + terminal.SetTheme(_lastSeenTheme, persist: true); + } + else + { + Debug.LogWarning( + $"Failed to find persisted theme {existingConfiguration.theme} for terminal {terminal.id} while hydrating.", + this + ); + } + + yield break; + } + else + { + Debug.Log( + $"Failed to find persisted configuration for terminal {terminal.id} while hydrating, defaulting to Prefab configuration.", + this + ); + } + } + + TerminalThemeConfiguration? maybeCurrentConfiguration = GetConfiguration(); + if (maybeCurrentConfiguration == null) + { + yield break; + } + + TerminalThemeConfiguration currentConfiguration = maybeCurrentConfiguration.Value; + if (!configurations.AddOrUpdate(currentConfiguration)) + { + yield break; + } + + string outputJson = JsonUtility.ToJson(configurations, prettyPrint: true); + Debug.Log( + $"Writing theme file {themeFile} with contents:{Environment.NewLine}{outputJson}", + this + ); + Task writerTask = File.WriteAllTextAsync(themeFile, outputJson); + while (!writerTask.IsCompleted) + { + yield return null; + } + + if (!writerTask.IsCompletedSuccessfully) + { + _lastSeenFont = null; + _lastSeenTheme = null; + Debug.LogError( + $"Failed to write theme file {themeFile} (terminal {terminal.id}): {writerTask.Exception}", + this + ); + } + else + { + Debug.Log($"Theme file {themeFile} successfully updated.", this); + } + } + finally + { + _nextUpdateTime = Time.time + savePeriod; + _persisting = false; + _persistence = null; + } + } + + public virtual TerminalThemeConfiguration? GetConfiguration() + { + if (terminal == null) + { + return null; + } + + if (string.IsNullOrWhiteSpace(terminal.id)) + { + return null; + } + + return new TerminalThemeConfiguration + { + terminalId = terminal.id, + font = terminal.CurrentFont == null ? string.Empty : terminal.CurrentFont.name, + theme = terminal.CurrentTheme ?? string.Empty, + }; + } + } +} diff --git a/Runtime/CommandTerminal/Themes/TerminalFontPack.cs b/Runtime/CommandTerminal/Themes/TerminalFontPack.cs index 2517c07..e1178d0 100644 --- a/Runtime/CommandTerminal/Themes/TerminalFontPack.cs +++ b/Runtime/CommandTerminal/Themes/TerminalFontPack.cs @@ -1,18 +1,18 @@ -namespace WallstopStudios.DxCommandTerminal.Themes -{ - using System.Collections.Generic; - using UnityEngine; - - [CreateAssetMenu( - menuName = "Wallstop Studios/DxCommandTerminal/Font Pack", - fileName = nameof(TerminalFontPack), - order = 1_111_123 - )] - public class TerminalFontPack : ScriptableObject - { - public virtual IReadOnlyList Fonts => _fonts; - - [SerializeField] - protected internal List _fonts = new(); - } -} +namespace WallstopStudios.DxCommandTerminal.Themes +{ + using System.Collections.Generic; + using UnityEngine; + + [CreateAssetMenu( + menuName = "Wallstop Studios/DxCommandTerminal/Font Pack", + fileName = nameof(TerminalFontPack), + order = 1_111_123 + )] + public class TerminalFontPack : ScriptableObject + { + public virtual IReadOnlyList Fonts => _fonts; + + [SerializeField] + protected internal List _fonts = new(); + } +} diff --git a/Runtime/CommandTerminal/Themes/TerminalThemePack.cs b/Runtime/CommandTerminal/Themes/TerminalThemePack.cs index a9356b9..3d9d4e0 100644 --- a/Runtime/CommandTerminal/Themes/TerminalThemePack.cs +++ b/Runtime/CommandTerminal/Themes/TerminalThemePack.cs @@ -1,23 +1,23 @@ -namespace WallstopStudios.DxCommandTerminal.Themes -{ - using System.Collections.Generic; - using UnityEngine; - using UnityEngine.UIElements; - - [CreateAssetMenu( - menuName = "Wallstop Studios/DxCommandTerminal/Theme Pack", - fileName = nameof(TerminalThemePack), - order = 1_111_123 - )] - public class TerminalThemePack : ScriptableObject - { - public virtual IReadOnlyList Themes => _themes; - public virtual IReadOnlyList ThemeNames => _themeNames; - - [SerializeField] - protected internal List _themes = new(); - - [SerializeField] - protected internal List _themeNames = new(); - } -} +namespace WallstopStudios.DxCommandTerminal.Themes +{ + using System.Collections.Generic; + using UnityEngine; + using UnityEngine.UIElements; + + [CreateAssetMenu( + menuName = "Wallstop Studios/DxCommandTerminal/Theme Pack", + fileName = nameof(TerminalThemePack), + order = 1_111_123 + )] + public class TerminalThemePack : ScriptableObject + { + public virtual IReadOnlyList Themes => _themes; + public virtual IReadOnlyList ThemeNames => _themeNames; + + [SerializeField] + protected internal List _themes = new(); + + [SerializeField] + protected internal List _themeNames = new(); + } +} diff --git a/Runtime/CommandTerminal/Themes/ThemeNameHelper.cs b/Runtime/CommandTerminal/Themes/ThemeNameHelper.cs index c65b4ef..81d00fa 100644 --- a/Runtime/CommandTerminal/Themes/ThemeNameHelper.cs +++ b/Runtime/CommandTerminal/Themes/ThemeNameHelper.cs @@ -1,42 +1,42 @@ -namespace WallstopStudios.DxCommandTerminal.Themes -{ - using System; - using System.Collections.Generic; - - public static class ThemeNameHelper - { - public static IEnumerable GetPossibleThemeNames(string theme) - { - theme = GetFriendlyThemeName(theme); - if (string.IsNullOrWhiteSpace(theme)) - { - yield break; - } - - yield return theme + "-theme"; - yield return "theme-" + theme; - } - - public static bool IsThemeName(string theme) - { - if (string.IsNullOrWhiteSpace(theme)) - { - return false; - } - - return theme.Contains("-theme", StringComparison.OrdinalIgnoreCase) - || theme.Contains("theme-", StringComparison.OrdinalIgnoreCase); - } - - public static string GetFriendlyThemeName(string theme) - { - if (string.IsNullOrWhiteSpace(theme)) - { - return theme; - } - return theme - .Replace("theme-", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace("-theme", string.Empty, StringComparison.OrdinalIgnoreCase); - } - } -} +namespace WallstopStudios.DxCommandTerminal.Themes +{ + using System; + using System.Collections.Generic; + + public static class ThemeNameHelper + { + public static IEnumerable GetPossibleThemeNames(string theme) + { + theme = GetFriendlyThemeName(theme); + if (string.IsNullOrWhiteSpace(theme)) + { + yield break; + } + + yield return theme + "-theme"; + yield return "theme-" + theme; + } + + public static bool IsThemeName(string theme) + { + if (string.IsNullOrWhiteSpace(theme)) + { + return false; + } + + return theme.Contains("-theme", StringComparison.OrdinalIgnoreCase) + || theme.Contains("theme-", StringComparison.OrdinalIgnoreCase); + } + + public static string GetFriendlyThemeName(string theme) + { + if (string.IsNullOrWhiteSpace(theme)) + { + return theme; + } + return theme + .Replace("theme-", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("-theme", string.Empty, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalState.cs b/Runtime/CommandTerminal/UI/TerminalState.cs index 5c0aea3..bb79cfc 100644 --- a/Runtime/CommandTerminal/UI/TerminalState.cs +++ b/Runtime/CommandTerminal/UI/TerminalState.cs @@ -1,13 +1,13 @@ -namespace WallstopStudios.DxCommandTerminal.UI -{ - using System; - - public enum TerminalState - { - [Obsolete("Use a valid value")] - Unknown = 0, - Closed = 1, - OpenSmall = 2, - OpenFull = 3, - } -} +namespace WallstopStudios.DxCommandTerminal.UI +{ + using System; + + public enum TerminalState + { + [Obsolete("Use a valid value")] + Unknown = 0, + Closed = 1, + OpenSmall = 2, + OpenFull = 3, + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index ad6f09e..dcf2d8b 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -1,2248 +1,2248 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Editor")] - -namespace WallstopStudios.DxCommandTerminal.UI -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using Attributes; - using Backend; - using Extensions; - using Helper; - using Input; - using Themes; - using UnityEditor; - using UnityEngine; - using UnityEngine.UIElements; - - [DisallowMultipleComponent] - public sealed class TerminalUI : MonoBehaviour - { - private const string TerminalRootName = "TerminalRoot"; - - private enum ScrollBarCaptureState - { - None = 0, - DraggerActive = 1, - TrackerActive = 2, - } - - // Cache log callback to reduce allocations - private static readonly Application.LogCallback UnityLogCallback = HandleUnityLog; - - public static TerminalUI Instance { get; private set; } - - // ReSharper disable once MemberCanBePrivate.Global - public bool IsClosed => - _state != TerminalState.OpenFull - && _state != TerminalState.OpenSmall - && Mathf.Approximately(_currentWindowHeight, _targetWindowHeight); - - public string CurrentTheme => - !string.IsNullOrWhiteSpace(_runtimeTheme) ? _runtimeTheme : _persistedTheme; - - public string CurrentFriendlyTheme => ThemeNameHelper.GetFriendlyThemeName(CurrentTheme); - - public Font CurrentFont => _runtimeFont != null ? _runtimeFont : _persistedFont; - - [SerializeField] - [Tooltip("Unique Id for this terminal, mainly for use with persisted configuration")] - internal string id = Guid.NewGuid().ToString(); - - [SerializeField] - internal UIDocument _uiDocument; - - [SerializeField] - internal string _persistedTheme = "dark-theme"; - - [Header("Window")] - [Range(0, 1)] - public float maxHeight = 0.7f; - - [SerializeField] - [Range(0, 1)] - public float smallTerminalRatio = 0.4714285f; - - [Tooltip("Curve the console follows to go from closed -> open")] - public AnimationCurve easeOutCurve = new() - { - keys = new[] { new Keyframe(0, 0), new Keyframe(1, 1) }, - }; - - [Tooltip("Duration for the ease-out animation in seconds")] - public float easeOutTime = 0.5f; - - [Tooltip("Curve the console follows to go from open -> closed")] - public AnimationCurve easeInCurve = new() - { - keys = new[] { new Keyframe(0, 0), new Keyframe(1, 1) }, - }; - - [Tooltip("Duration for the ease-in animation in seconds")] - public float easeInTime = 0.5f; - - [Header("System")] - [SerializeField] - private int _logBufferSize = 256; - - [SerializeField] - private int _historyBufferSize = 512; - - [Header("Input")] - [SerializeField] - internal Font _persistedFont; - - [SerializeField] - private string _inputCaret = ">"; - - [Header("Buttons")] - public bool showGUIButtons; - - [DxShowIf(nameof(showGUIButtons))] - public string runButtonText = "run"; - - [DxShowIf(nameof(showGUIButtons))] - [SerializeField] - public string closeButtonText = "close"; - - [DxShowIf(nameof(showGUIButtons))] - public string smallButtonText = "small"; - - [DxShowIf(nameof(showGUIButtons))] - public string fullButtonText = "full"; - - [Header("Hints")] - public HintDisplayMode hintDisplayMode = HintDisplayMode.AutoCompleteOnly; - - public bool makeHintsClickable = true; - - [Header("System")] - [SerializeField] - private int _cursorBlinkRateMilliseconds = 666; - -#if UNITY_EDITOR - [SerializeField] - private bool _trackChangesInEditor = true; -#endif - - [Tooltip("Will reset static command state in OnEnable and Start when set to true")] - public bool resetStateOnInit; - - public bool skipSameCommandsInHistory = true; - - [SerializeField] - public bool ignoreDefaultCommands; - - [SerializeField] - private bool _logUnityMessages; - - [SerializeField] - internal List _ignoredLogTypes = new(); - - [SerializeField] - internal List _disabledCommands = new(); - - [SerializeField] - internal TerminalFontPack _fontPack; - - [SerializeField] - internal TerminalThemePack _themePack; - - private IInputHandler[] _inputHandlers; - -#if UNITY_EDITOR - private readonly Dictionary _propertyValues = new(); - private readonly List _uiProperties = new(); - private readonly List _themeProperties = new(); - private readonly List _cursorBlinkProperties = new(); - private readonly List _fontProperties = new(); - private readonly List _staticStateProperties = new(); - private readonly List _windowProperties = new(); - private readonly List _logUnityMessageProperties = new(); - private readonly List _autoCompleteProperties = new(); - private SerializedObject _serializedObject; -#endif - - // Editor integration -#if UNITY_EDITOR - [Header("Editor")] - [Tooltip( - "When enabled (and runtime mode allows Editor), the terminal will auto-discover IArgParser implementations on reload/start." - )] - [SerializeField] - private bool _autoDiscoverParsersInEditor; -#endif - - [Header("Runtime Mode")] - [Tooltip( - "Controls which environment-specific features are enabled. Choose explicit modes. None is obsolete." - )] - [SerializeField] -#pragma warning disable CS0618 // Type or member is obsolete - private Backend.TerminalRuntimeModeFlags _runtimeModes = Backend - .TerminalRuntimeModeFlags - .None; -#pragma warning restore CS0618 // Type or member is obsolete - - // Test helper to skip building UI entirely (prevents UI Toolkit panel updates) - internal bool disableUIForTests; - - private TerminalState _state = TerminalState.Closed; - private float _currentWindowHeight; - private float _targetWindowHeight; - private float _realWindowHeight; - private bool _unityLogAttached; - private bool _started; - private bool _needsFocus; - private bool _needsScrollToEnd; - private bool _needsAutoCompleteReset; - private long? _lastSeenBufferVersion; - private string _lastKnownCommandText; - private int? _lastCompletionIndex; - private int? _previousLastCompletionIndex; - private string _focusedControl; - private bool _isCommandFromCode; - private bool _initialResetStateOnInit; - private bool _commandIssuedThisFrame; - private string _runtimeTheme; - private Font _runtimeFont; - private float _initialWindowHeight; - private float _animationTimer; - private bool _isAnimating; - - private VisualElement _terminalContainer; - private ScrollView _logScrollView; - private ScrollView _autoCompleteContainer; - private VisualElement _inputContainer; - private TextField _commandInput; - private Button _runButton; - private VisualElement _stateButtonContainer; - private VisualElement _textInput; - private Label _inputCaretLabel; - private bool _lastKnownHintsClickable; - private IVisualElementScheduledItem _cursorBlinkSchedule; - - private readonly List _lastCompletionBuffer = new(); - private readonly List _lastCompletionBufferTempCache = new(); - private readonly HashSet _lastCompletionBufferTempSet = new( - StringComparer.OrdinalIgnoreCase - ); - private readonly List _autoCompleteChildren = new(); - - // Cached for performance (avoids allocations) - private readonly Action _focusInput; -#if UNITY_EDITOR - private readonly EditorApplication.CallbackFunction _checkForChanges; -#endif - private ITerminalInput _input; - - public TerminalUI() - { - _focusInput = FocusInput; -#if UNITY_EDITOR - _checkForChanges = CheckForChanges; -#endif - } - - private void Awake() - { - Backend.TerminalRuntimeConfig.SetMode(_runtimeModes); -#if UNITY_EDITOR - Backend.TerminalRuntimeConfig.EditorAutoDiscover = _autoDiscoverParsersInEditor; -#endif - Backend.TerminalRuntimeConfig.TryAutoDiscoverParsers(); - switch (_logBufferSize) - { - case <= 0: - Debug.LogError( - $"Invalid buffer size '{_logBufferSize}', must be greater than zero. Defaulting to 0 (empty buffer).", - this - ); - break; - case < 10: - Debug.LogWarning( - $"Unsupported buffer size '{_logBufferSize}', recommended size is > 10.", - this - ); - break; - } - - switch (_historyBufferSize) - { - case <= 0: - Debug.LogError( - $"Invalid buffer size '{_historyBufferSize}', must be greater than zero. Defaulting to 0 (empty buffer).", - this - ); - break; - case < 10: - Debug.LogWarning( - $"Unsupported buffer size '{_historyBufferSize}', recommended size is > 10.", - this - ); - break; - } - - _inputHandlers = GetComponents(); - - if (!TryGetComponent(out _input)) - { - _input = DefaultTerminalInput.Instance; - } - - Instance = this; - -#if UNITY_EDITOR - _serializedObject = new SerializedObject(this); - - string[] uiPropertiesTracked = { nameof(_uiDocument) }; - TrackProperties(uiPropertiesTracked, _uiProperties); - - string[] themePropertiesTracked = { nameof(_themePack) }; - TrackProperties(themePropertiesTracked, _themeProperties); - - string[] cursorBlinkPropertiesTracked = { nameof(_cursorBlinkRateMilliseconds) }; - TrackProperties(cursorBlinkPropertiesTracked, _cursorBlinkProperties); - - string[] fontPropertiesTracked = { nameof(_persistedFont) }; - TrackProperties(fontPropertiesTracked, _fontProperties); - - string[] staticStaticPropertiesTracked = - { - nameof(_logBufferSize), - nameof(_historyBufferSize), - nameof(_ignoredLogTypes), - nameof(_disabledCommands), - nameof(ignoreDefaultCommands), - nameof(_fontPack), - }; - TrackProperties(staticStaticPropertiesTracked, _staticStateProperties); - - string[] windowPropertiesTracked = { nameof(maxHeight), nameof(smallTerminalRatio) }; - TrackProperties(windowPropertiesTracked, _windowProperties); - - string[] logUnityMessagePropertiesTracked = { nameof(_logUnityMessages) }; - TrackProperties(logUnityMessagePropertiesTracked, _logUnityMessageProperties); - - string[] autoCompletePropertiesTracked = - { - nameof(hintDisplayMode), - nameof(_disabledCommands), - nameof(ignoreDefaultCommands), - }; - TrackProperties(autoCompletePropertiesTracked, _autoCompleteProperties); - - void TrackProperties(string[] properties, List storage) - { - foreach (string propertyName in properties) - { - SerializedProperty property = _serializedObject.FindProperty(propertyName); - if (property != null) - { - storage.Add(property); - object value = property.GetValue(); - switch (value) - { - case List stringList: - value = stringList.ToList(); - break; - case List logTypeList: - value = logTypeList.ToList(); - break; - case List fontList: - value = fontList.ToList(); - break; - } - _propertyValues[property.name] = value; - } - else - { - Debug.LogWarning( - $"Failed to track/find window property {propertyName}, updates to this property will be ignored.", - this - ); - } - } - } -#endif - } - - private void OnEnable() - { - RefreshStaticState(force: resetStateOnInit); - ConsumeAndLogErrors(); - - if (_logUnityMessages && !_unityLogAttached) - { - Application.logMessageReceivedThreaded += UnityLogCallback; - _unityLogAttached = true; - } - - SetupUI(); - -#if UNITY_EDITOR - EditorApplication.update += _checkForChanges; -#endif - } - - private void OnDisable() - { -#if UNITY_EDITOR - EditorApplication.update -= _checkForChanges; -#endif - if (_uiDocument != null) - { - _uiDocument.rootVisualElement?.Clear(); - } - - if (_unityLogAttached) - { - Application.logMessageReceivedThreaded -= UnityLogCallback; - _unityLogAttached = false; - } - - SetState(TerminalState.Closed); - } - - private void OnDestroy() - { - if (Instance != this) - { - return; - } - - Instance = null; - } - - private void Start() - { - if (_started) - { - SetState(TerminalState.Closed); - } - - RefreshStaticState(force: resetStateOnInit); - ResetWindowIdempotent(); - ConsumeAndLogErrors(); - ResetAutoComplete(); - _started = true; - _lastKnownHintsClickable = makeHintsClickable; - } - - private void LateUpdate() - { - ResetWindowIdempotent(); - // Drain any cross-thread logs into the main-thread buffer before refreshing UI - Terminal.Buffer?.DrainPending(); - HandleHeightAnimation(); - RefreshUI(); - _commandIssuedThisFrame = false; - } - - private void RefreshStaticState(bool force) - { - int logBufferSize = Mathf.Max(0, _logBufferSize); - if (force || Terminal.Buffer == null) - { - Terminal.Buffer = new CommandLog(logBufferSize, _ignoredLogTypes); - } - else - { - if (Terminal.Buffer.Capacity != logBufferSize) - { - Terminal.Buffer.Resize(logBufferSize); - } - if ( - !Terminal.Buffer.ignoredLogTypes.SetEquals( - _ignoredLogTypes ?? Enumerable.Empty() - ) - ) - { - Terminal.Buffer.ignoredLogTypes.Clear(); - Terminal.Buffer.ignoredLogTypes.UnionWith( - _ignoredLogTypes ?? Enumerable.Empty() - ); - } - } - - int historyBufferSize = Mathf.Max(0, _historyBufferSize); - if (force || Terminal.History == null) - { - Terminal.History = new CommandHistory(historyBufferSize); - } - else if (Terminal.History.Capacity != historyBufferSize) - { - Terminal.History.Resize(historyBufferSize); - } - - if (force || Terminal.Shell == null) - { - Terminal.Shell = new CommandShell(Terminal.History); - } - - if (force || Terminal.AutoComplete == null) - { - Terminal.AutoComplete = new CommandAutoComplete(Terminal.History, Terminal.Shell); - } - - if ( - Terminal.Shell.IgnoringDefaultCommands != ignoreDefaultCommands - || Terminal.Shell.Commands.Count <= 0 - || !Terminal.Shell.IgnoredCommands.SetEquals( - _disabledCommands ?? Enumerable.Empty() - ) - ) - { - Terminal.Shell.ClearAutoRegisteredCommands(); - Terminal.Shell.InitializeAutoRegisteredCommands( - ignoredCommands: _disabledCommands, - ignoreDefaultCommands: ignoreDefaultCommands - ); - - if (_started) - { - ResetAutoComplete(); - } - } - } - -#if UNITY_EDITOR - private void CheckForChanges() - { - if (!_trackChangesInEditor) - { - return; - } - - if (!_started) - { - return; - } - - if (Instance != this) - { - return; - } - - _serializedObject.Update(); - if (CheckForRefresh(_uiProperties)) - { - SetupUI(); - } - - if (CheckForRefresh(_themeProperties)) - { - if (_uiDocument != null) - { - InitializeTheme( - _uiDocument.rootVisualElement?.Q(TerminalRootName) - ); - } - } - - if (CheckForRefresh(_cursorBlinkProperties)) - { - ScheduleBlinkingCursor(); - } - - if (CheckForRefresh(_fontProperties)) - { - SetFont(_persistedFont); - } - - if (CheckForRefresh(_staticStateProperties)) - { - RefreshStaticState(force: false); - } - - if (CheckForRefresh(_windowProperties)) - { - ResetWindowIdempotent(); - } - - if (CheckForRefresh(_logUnityMessageProperties)) - { - if (_logUnityMessages && !_unityLogAttached) - { - _unityLogAttached = true; - Application.logMessageReceivedThreaded += HandleUnityLog; - } - else if (!_logUnityMessages && _unityLogAttached) - { - Application.logMessageReceivedThreaded -= HandleUnityLog; - _unityLogAttached = false; - } - } - - if (CheckForRefresh(_autoCompleteProperties)) - { - _autoCompleteContainer?.Clear(); - ResetAutoComplete(); - } - - return; - - bool CheckForRefresh(List properties) - { - bool needRefresh = false; - foreach (SerializedProperty property in properties) - { - object propertyValue = property.GetValue(); - object previousValue = _propertyValues[property.name]; - if ( - propertyValue is List currentStringList - && previousValue is List previousStringList - ) - { - if (!currentStringList.SequenceEqual(previousStringList)) - { - needRefresh = true; - _propertyValues[property.name] = currentStringList.ToList(); - } - - continue; - } - if ( - propertyValue is List currentLogTypeList - && previousValue is List previousLogTypeList - ) - { - if (!currentLogTypeList.SequenceEqual(previousLogTypeList)) - { - needRefresh = true; - _propertyValues[property.name] = currentLogTypeList.ToList(); - } - - continue; - } - - if (Equals(propertyValue, previousValue)) - { - continue; - } - - needRefresh = true; - _propertyValues[property.name] = propertyValue; - } - - return needRefresh; - } - } -#endif - - public void ToggleState(TerminalState newState) - { - SetState(_state == newState ? TerminalState.Closed : newState); - } - - public void SetState(TerminalState newState) - { - _commandIssuedThisFrame = true; - _state = newState; - ResetWindowIdempotent(); - if (_state != TerminalState.Closed) - { - _needsFocus = true; - } - else - { - _input.CommandText = string.Empty; - ResetAutoComplete(); - } - } - - private static void ConsumeAndLogErrors() - { - while (Terminal.Shell?.TryConsumeErrorMessage(out string error) == true) - { - Terminal.Log(TerminalLogType.Error, $"Error: {error}"); - } - } - - private void ResetAutoComplete() - { - _lastKnownCommandText = _input.CommandText ?? string.Empty; - if (hintDisplayMode == HintDisplayMode.Always) - { - _lastCompletionBufferTempCache.Clear(); - int caret = - _commandInput != null - ? _commandInput.cursorIndex - : (_lastKnownCommandText?.Length ?? 0); - Terminal.AutoComplete?.Complete( - _lastKnownCommandText, - caret, - _lastCompletionBufferTempCache - ); - bool equivalent = - _lastCompletionBufferTempCache.Count == _lastCompletionBuffer.Count; - if (equivalent) - { - _lastCompletionBufferTempSet.Clear(); - foreach (string completion in _lastCompletionBuffer) - { - _lastCompletionBufferTempSet.Add(completion); - } - - foreach (string completion in _lastCompletionBufferTempCache) - { - if (!_lastCompletionBufferTempSet.Contains(completion)) - { - equivalent = false; - break; - } - } - } - - if (!equivalent) - { - _lastCompletionIndex = null; - _previousLastCompletionIndex = null; - _lastCompletionBuffer.Clear(); - foreach (string completion in _lastCompletionBufferTempCache) - { - _lastCompletionBuffer.Add(completion); - } - } - } - else - { - _lastCompletionIndex = null; - _previousLastCompletionIndex = null; - _lastCompletionBuffer.Clear(); - } - } - - private void ResetWindowIdempotent() - { - int height = Screen.height; - float oldTargetHeight = _targetWindowHeight; - try - { - switch (_state) - { - case TerminalState.OpenSmall: - { - _realWindowHeight = height * maxHeight * smallTerminalRatio; - _targetWindowHeight = _realWindowHeight; - break; - } - case TerminalState.OpenFull: - { - _realWindowHeight = height * maxHeight; - _targetWindowHeight = _realWindowHeight; - break; - } - default: - { - _realWindowHeight = height * maxHeight * smallTerminalRatio; - _targetWindowHeight = 0; - break; - } - } - } - finally - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (oldTargetHeight != _targetWindowHeight) - { - StartHeightAnimation(); - } - } - } - - private void SetupUI() - { - if (disableUIForTests) - { - return; - } - if (_uiDocument == null) - { - _uiDocument = GetComponent(); - } - if (_uiDocument == null) - { - Debug.LogError("No UIDocument assigned, cannot setup UI.", this); - return; - } - - VisualElement uiRoot = _uiDocument.rootVisualElement; - if (uiRoot == null) - { - Debug.LogError("No UI root element assigned, cannot setup UI.", this); - return; - } - - uiRoot.Clear(); - VisualElement root = new(); - uiRoot.Add(root); - root.name = TerminalRootName; - root.AddToClassList("terminal-root"); - - InitializeTheme(root); - InitializeFont(); - // Ensure a font is set after initialization - if (CurrentFont != null) - { - SetFont(CurrentFont, persist: false); - } - - if (!string.IsNullOrWhiteSpace(_runtimeTheme)) - { - root.AddToClassList(_runtimeTheme); - } - else - { - Debug.LogError("Failed to load any themes!", this); - } - - _terminalContainer = new VisualElement { name = "TerminalContainer" }; - _terminalContainer.AddToClassList("terminal-container"); - _uiDocument.rootVisualElement.style.height = new StyleLength(_realWindowHeight); - _terminalContainer.style.height = new StyleLength(_realWindowHeight); - root.Add(_terminalContainer); - - _logScrollView = new ScrollView(); - InitializeScrollView(_logScrollView); - _logScrollView.name = "LogScrollView"; - _logScrollView.AddToClassList("log-scroll-view"); - _terminalContainer.Add(_logScrollView); - - _autoCompleteContainer = new ScrollView(ScrollViewMode.Horizontal) - { - name = "AutoCompletePopup", - }; - _autoCompleteContainer.AddToClassList("autocomplete-popup"); - _terminalContainer.Add(_autoCompleteContainer); - - _inputContainer = new VisualElement { name = "InputContainer" }; - _inputContainer.AddToClassList("input-container"); - _terminalContainer.Add(_inputContainer); - - _runButton = new Button(EnterCommand) - { - text = runButtonText, - name = "RunCommandButton", - }; - _runButton.AddToClassList("terminal-button"); - _runButton.AddToClassList("terminal-button-run"); - _runButton.style.display = DisplayStyle.None; - _runButton.style.marginLeft = 6; - _runButton.style.marginRight = 4; - _runButton.style.paddingTop = 2; - _runButton.style.paddingBottom = 2; - _inputContainer.Add(_runButton); - - _inputCaretLabel = new Label(_inputCaret) { name = "InputCaret" }; - _inputCaretLabel.AddToClassList("terminal-input-caret"); - _inputContainer.Add(_inputCaretLabel); - - _commandInput = new TextField(); - ScheduleBlinkingCursor(); - _commandInput.name = "CommandInput"; - _commandInput.AddToClassList("terminal-input-field"); - _commandInput.pickingMode = PickingMode.Position; - _commandInput.value = _input.CommandText; - _commandInput.RegisterCallback, TerminalUI>( - (evt, context) => - { - if ( - context._commandIssuedThisFrame - || Array.Exists( - context._inputHandlers, - handler => handler.ShouldHandleInputThisFrame - ) - ) - { - if (!string.Equals(context._commandInput.value, context._input.CommandText)) - { - context._commandInput.value = context._input.CommandText; - } - // Ensure subsequent user keystrokes (e.g., space) trigger recompute - // even if this event was caused by programmatic text changes (Tab, etc.). - context._isCommandFromCode = false; - evt.StopPropagation(); - return; - } - - // Assign input text - context._input.CommandText = evt.newValue; - - // If the user just typed a space right after a recognized command name, - // proactively clear the hint bar so stale command-name suggestions disappear - // before argument-context suggestions are computed/shown. - try - { - string prev = evt.previousValue ?? string.Empty; - string curr = evt.newValue ?? string.Empty; - bool justTypedSpace = curr.EndsWith(" ") && curr.Length == prev.Length + 1; - if (justTypedSpace && Backend.Terminal.Shell != null) - { - string check = curr; - // Remove trailing space(s) to isolate the command token - if (check.NeedsTrim()) - { - check = check.TrimEnd(); - } - - if ( - Backend.CommandShell.TryEatArgument( - ref check, - out Backend.CommandArg cmd - ) - ) - { - if (Backend.Terminal.Shell.Commands.ContainsKey(cmd.contents)) - { - // Clear existing suggestions immediately - context._lastCompletionIndex = null; - context._previousLastCompletionIndex = null; - context._lastCompletionBuffer.Clear(); - context._autoCompleteContainer?.Clear(); - } - } - } - } - catch - { /* non-fatal UI hint clearing */ - } - - context._runButton.style.display = - context.showGUIButtons - && !string.IsNullOrWhiteSpace(context._input.CommandText) - && !string.IsNullOrWhiteSpace(context.runButtonText) - ? DisplayStyle.Flex - : DisplayStyle.None; - if (!context._isCommandFromCode) - { - context.ResetAutoComplete(); - } - - context._isCommandFromCode = false; - }, - userArgs: this, - useTrickleDown: TrickleDown.TrickleDown - ); - - _inputContainer.Add(_commandInput); - _textInput = _commandInput.Q("unity-text-input"); - - _stateButtonContainer = new VisualElement { name = "StateButtonContainer" }; - _stateButtonContainer.AddToClassList("state-button-container"); - root.Add(_stateButtonContainer); - RefreshStateButtons(); - } - - private void InitializeTheme(VisualElement root) - { - if (_themePack == null) - { - Debug.LogWarning("No theme pack assigned, cannot initialize theme.", this); - return; - } - - if (root != null) - { - for (int i = root.styleSheets.count - 1; 0 <= i; --i) - { - StyleSheet styleSheet = root.styleSheets[i]; - if ( - styleSheet == null - || styleSheet.name.Contains("Theme", StringComparison.OrdinalIgnoreCase) - ) - { - root.styleSheets.Remove(styleSheet); - } - } - - foreach (StyleSheet styleSheet in _themePack._themes) - { - if (styleSheet == null) - { - continue; - } - - root.styleSheets.Add(styleSheet); - } - } - else - { - Debug.LogWarning( - "No root element assigned, theme initialization may be broken.", - this - ); - } - - _runtimeTheme = _persistedTheme; - List themeNames = _themePack._themeNames; - if (themeNames.Contains(_runtimeTheme)) - { - return; - } - - if (themeNames is { Count: > 0 }) - { - _runtimeTheme = themeNames.FirstOrDefault(theme => - theme.Contains("dark", StringComparison.OrdinalIgnoreCase) - ); - if (_runtimeTheme == null) - { - _runtimeTheme = themeNames.FirstOrDefault(theme => - theme.Contains("light", StringComparison.OrdinalIgnoreCase) - ); - } - if (_runtimeTheme == null) - { - _runtimeTheme = themeNames.FirstOrDefault(); - } - Debug.LogWarning($"Persisted theme not found, defaulting to '{_runtimeTheme}'."); - } - else - { - Debug.LogWarning("No available terminal themes.", this); - } - } - - // Support method for tests and tooling to inject theme/font packs before enabling - public void InjectPacks( - Themes.TerminalThemePack themePack, - Themes.TerminalFontPack fontPack - ) - { - _themePack = themePack; - _fontPack = fontPack; - } - - private void InitializeFont() - { - if (_fontPack == null) - { - Debug.LogWarning("No font pack assigned, cannot initialize font.", this); - return; - } - - _runtimeFont = _persistedFont; - if (_runtimeFont != null) - { - return; - } - - List loadedFonts = _fontPack._fonts; - if (loadedFonts is { Count: > 0 }) - { - _runtimeFont = loadedFonts.FirstOrDefault(font => - font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) - && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) - ); - if (_runtimeFont == null) - { - _runtimeFont = loadedFonts.FirstOrDefault(font => - font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) - ); - } - if (_runtimeFont == null) - { - _runtimeFont = loadedFonts.FirstOrDefault(font => - font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) - ); - } - if (_runtimeFont == null) - { - _runtimeFont = loadedFonts.FirstOrDefault(); - } - } - - if (_runtimeFont == null) - { - Debug.LogWarning("No font assigned, defaulting to Courier New 16pt", this); - _runtimeFont = Font.CreateDynamicFontFromOSFont("Courier New", 16); - } - else - { - Debug.LogWarning($"No font assigned, defaulting to {_runtimeFont.name}.", this); - } - } - - private void ScheduleBlinkingCursor() - { - _cursorBlinkSchedule?.Pause(); - _cursorBlinkSchedule = null; - - if (_commandInput == null) - { - return; - } - - bool shouldRenderCursor = true; - _cursorBlinkSchedule = _commandInput - .schedule.Execute(() => - { - _commandInput.EnableInClassList("transparent-cursor", shouldRenderCursor); - _commandInput.EnableInClassList("styled-cursor", !shouldRenderCursor); - shouldRenderCursor = !shouldRenderCursor; - }) - .Every(_cursorBlinkRateMilliseconds); - } - - private static void InitializeScrollView(ScrollView scrollView) - { - VisualElement parent = scrollView.Q( - className: "unity-scroller--vertical" - ); - if (parent == null) - { - scrollView.RegisterCallback(ReInitialize); - return; - - void ReInitialize(GeometryChangedEvent evt) - { - InitializeScrollView(scrollView); - scrollView.UnregisterCallback(ReInitialize); - } - } - VisualElement trackerElement = parent.Q( - className: "unity-base-slider__tracker" - ); - VisualElement draggerElement = parent.Q( - className: "unity-base-slider__dragger" - ); - - ScrollBarCaptureState scrollBarCaptureState = ScrollBarCaptureState.None; - - RegisterCallbacks(); - return; - - void RegisterCallbacks() - { - // Hover Events - trackerElement.RegisterCallback(OnTrackerMouseEnter); - trackerElement.RegisterCallback(OnTrackerMouseLeave); - draggerElement.RegisterCallback(OnDraggerMouseEnter); - draggerElement.RegisterCallback(OnDraggerMouseLeave); - - trackerElement.RegisterCallback(OnTrackerPointerDown); - trackerElement.RegisterCallback(OnTrackerPointerUp); - draggerElement.RegisterCallback(OnDraggerPointerDown); - parent.RegisterCallback(OnDraggerPointerCaptureOut); - } - - void OnTrackerPointerDown(PointerDownEvent evt) - { - scrollBarCaptureState = ScrollBarCaptureState.TrackerActive; - draggerElement.AddToClassList("tracker-active"); - draggerElement.RemoveFromClassList("tracker-hovered"); - } - - void OnTrackerPointerUp(PointerUpEvent evt) - { - scrollBarCaptureState = ScrollBarCaptureState.None; - draggerElement.RemoveFromClassList("tracker-active"); - } - - void OnDraggerPointerDown(PointerDownEvent evt) - { - scrollBarCaptureState = ScrollBarCaptureState.DraggerActive; - trackerElement.AddToClassList("dragger-active"); - draggerElement.AddToClassList("dragger-active"); - trackerElement.RemoveFromClassList("dragger-hovered"); - } - - void OnDraggerPointerCaptureOut(PointerCaptureOutEvent evt) - { - scrollBarCaptureState = ScrollBarCaptureState.None; - trackerElement.RemoveFromClassList("dragger-active"); - draggerElement.RemoveFromClassList("tracker-active"); - draggerElement.RemoveFromClassList("dragger-active"); - } - - void OnTrackerMouseEnter(MouseEnterEvent evt) - { - if (scrollBarCaptureState == ScrollBarCaptureState.None) - { - draggerElement.AddToClassList("tracker-hovered"); - } - } - - void OnTrackerMouseLeave(MouseLeaveEvent evt) - { - if (scrollBarCaptureState == ScrollBarCaptureState.None) - { - draggerElement.RemoveFromClassList("tracker-hovered"); - } - } - - void OnDraggerMouseEnter(MouseEnterEvent evt) - { - if (scrollBarCaptureState == ScrollBarCaptureState.None) - { - trackerElement.AddToClassList("dragger-hovered"); - } - } - - void OnDraggerMouseLeave(MouseLeaveEvent evt) - { - if (scrollBarCaptureState == ScrollBarCaptureState.None) - { - trackerElement.RemoveFromClassList("dragger-hovered"); - } - } - } - - private void RefreshUI() - { - if (_terminalContainer == null) - { - return; - } - - if (_commandIssuedThisFrame) - { - return; - } - - _uiDocument.rootVisualElement.style.height = _currentWindowHeight; - _terminalContainer.style.height = _currentWindowHeight; - _terminalContainer.style.width = Screen.width; - DisplayStyle commandInputStyle = - _currentWindowHeight <= 30 ? DisplayStyle.None : DisplayStyle.Flex; - - _needsFocus |= - _inputContainer.resolvedStyle.display != commandInputStyle - && commandInputStyle == DisplayStyle.Flex; - _inputContainer.style.display = commandInputStyle; - - RefreshLogs(); - RefreshAutoCompleteHints(); - string commandInput = _input.CommandText; - if (!string.Equals(_commandInput.value, commandInput)) - { - _isCommandFromCode = true; - _commandInput.value = commandInput; - } - else if ( - _needsFocus - && _textInput.focusable - && _textInput.resolvedStyle.display != DisplayStyle.None - && _commandInput.resolvedStyle.display != DisplayStyle.None - ) - { - if (_textInput.focusController.focusedElement != _textInput) - { - _textInput.schedule.Execute(_focusInput).ExecuteLater(0); - FocusInput(); - } - - _needsFocus = false; - } - else if ( - _needsScrollToEnd - && _logScrollView != null - && _logScrollView.style.display != DisplayStyle.None - ) - { - ScrollToEnd(); - _needsScrollToEnd = false; - } - RefreshStateButtons(); - } - - private void FocusInput() - { - if (_textInput == null) - { - return; - } - - _textInput.Focus(); - int textEndPosition = _commandInput.value.Length; - _commandInput.cursorIndex = textEndPosition; - _commandInput.selectIndex = textEndPosition; - } - - private void RefreshLogs() - { - IReadOnlyList logs = Terminal.Buffer?.Logs; - if (logs == null) - { - return; - } - - if (_logScrollView == null) - { - return; - } - - VisualElement content = _logScrollView.contentContainer; - bool dirty = _lastSeenBufferVersion != Terminal.Buffer.Version; - if (content.childCount != logs.Count) - { - dirty = true; - if (content.childCount < logs.Count) - { - for (int i = 0; i < logs.Count - content.childCount; ++i) - { - Label logText = new(); - logText.AddToClassList("terminal-output-label"); - content.Add(logText); - } - } - else if (logs.Count < content.childCount) - { - for (int i = content.childCount - 1; logs.Count <= i; --i) - { - content.RemoveAt(i); - } - } - - _needsScrollToEnd = true; - } - - if (dirty) - { - for (int i = 0; i < logs.Count && i < content.childCount; ++i) - { - VisualElement item = content[i]; - switch (item) - { - case TextField logText: - { - LogItem logItem = logs[i]; - SetupLogText(logText, logItem); - logText.value = logItem.message; - break; - } - case Label logLabel: - { - LogItem logItem = logs[i]; - SetupLogText(logLabel, logItem); - logLabel.text = logItem.message; - break; - } - case Button button: - { - LogItem logItem = logs[i]; - SetupLogText(button, logItem); - button.text = logItem.message; - break; - } - } - } - - if (logs.Count == content.childCount) - { - _lastSeenBufferVersion = Terminal.Buffer.Version; - } - } - return; - - static void SetupLogText(VisualElement logText, LogItem log) - { - logText.EnableInClassList( - "terminal-output-label--shell", - log.type == TerminalLogType.ShellMessage - ); - logText.EnableInClassList( - "terminal-output-label--error", - log.type - is TerminalLogType.Exception - or TerminalLogType.Error - or TerminalLogType.Assert - ); - logText.EnableInClassList( - "terminal-output-label--warning", - log.type == TerminalLogType.Warning - ); - logText.EnableInClassList( - "terminal-output-label--message", - log.type == TerminalLogType.Message - ); - logText.EnableInClassList( - "terminal-output-label--input", - log.type == TerminalLogType.Input - ); - } - } - - private void ScrollToEnd() - { - if (0 < _logScrollView?.verticalScroller.highValue) - { - _logScrollView.verticalScroller.value = _logScrollView.verticalScroller.highValue; - } - } - - private void RefreshAutoCompleteHints() - { - bool shouldDisplay = - 0 < _lastCompletionBuffer.Count - && hintDisplayMode is HintDisplayMode.Always or HintDisplayMode.AutoCompleteOnly - && _autoCompleteContainer != null; - - if (!shouldDisplay) - { - if (0 < _autoCompleteContainer?.childCount) - { - _autoCompleteContainer.Clear(); - } - - _previousLastCompletionIndex = null; - return; - } - - int bufferLength = _lastCompletionBuffer.Count; - if (_lastKnownHintsClickable != makeHintsClickable) - { - _autoCompleteContainer.Clear(); - _lastKnownHintsClickable = makeHintsClickable; - } - - int currentChildCount = _autoCompleteContainer.childCount; - - bool dirty = _lastCompletionIndex != _previousLastCompletionIndex; - bool contentsChanged = currentChildCount != bufferLength; - if (contentsChanged) - { - dirty = true; - if (currentChildCount < bufferLength) - { - for (int i = currentChildCount; i < bufferLength; ++i) - { - string hint = _lastCompletionBuffer[i]; - VisualElement hintElement; - - if (makeHintsClickable) - { - int currentIndex = i; - string currentHint = hint; - Button hintButton = new(() => - { - _input.CommandText = currentHint; - _lastCompletionIndex = currentIndex; - _needsFocus = true; - }) - { - text = hint, - }; - hintElement = hintButton; - } - else - { - Label hintText = new(hint); - hintElement = hintText; - } - - hintElement.name = $"SuggestionText{i}"; - _autoCompleteContainer.Add(hintElement); - - bool isSelected = i == _lastCompletionIndex; - hintElement.AddToClassList("terminal-button"); - hintElement.EnableInClassList("autocomplete-item-selected", isSelected); - hintElement.EnableInClassList("autocomplete-item", !isSelected); - } - } - else if (bufferLength < currentChildCount) - { - for (int i = currentChildCount - 1; bufferLength <= i; --i) - { - _autoCompleteContainer.RemoveAt(i); - } - } - } - - bool shouldUpdateCompletionIndex = false; - try - { - shouldUpdateCompletionIndex = _autoCompleteContainer.childCount == bufferLength; - if (shouldUpdateCompletionIndex) - { - UpdateAutoCompleteView(); - } - - if (dirty) - { - for (int i = 0; i < _autoCompleteContainer.childCount && i < bufferLength; ++i) - { - VisualElement hintElement = _autoCompleteContainer[i]; - switch (hintElement) - { - case Button button: - button.text = _lastCompletionBuffer[i]; - break; - case Label label: - label.text = _lastCompletionBuffer[i]; - break; - case TextField textField: - textField.value = _lastCompletionBuffer[i]; - break; - } - - bool isSelected = i == _lastCompletionIndex; - - hintElement.EnableInClassList("autocomplete-item-selected", isSelected); - hintElement.EnableInClassList("autocomplete-item", !isSelected); - } - } - } - finally - { - if (shouldUpdateCompletionIndex) - { - _previousLastCompletionIndex = _lastCompletionIndex; - } - } - } - - private void UpdateAutoCompleteView() - { - if (_lastCompletionIndex == null) - { - return; - } - - if (_autoCompleteContainer?.contentContainer == null) - { - return; - } - - int childCount = _autoCompleteContainer.childCount; - if (childCount == 0) - { - return; - } - - if (childCount <= _lastCompletionIndex) - { - _lastCompletionIndex = - (_lastCompletionIndex % childCount + childCount) % childCount; - } - - if (_previousLastCompletionIndex == _lastCompletionIndex) - { - return; - } - - VisualElement current = _autoCompleteContainer[_lastCompletionIndex.Value]; - float viewportWidth = _autoCompleteContainer.contentViewport.resolvedStyle.width; - - // Use layout properties relative to the content container - float targetElementLeft = current.layout.x; - float targetElementWidth = current.layout.width; - float targetElementRight = targetElementLeft + targetElementWidth; - - const float epsilon = 0.01f; - - bool isFullyVisible = - epsilon <= targetElementLeft && targetElementRight <= viewportWidth + epsilon; - - if (isFullyVisible) - { - return; - } - - bool isIncrementing; - if (_previousLastCompletionIndex == childCount - 1 && _lastCompletionIndex == 0) - { - isIncrementing = true; - } - else if (_previousLastCompletionIndex == 0 && _lastCompletionIndex == childCount - 1) - { - isIncrementing = false; - } - else - { - isIncrementing = _previousLastCompletionIndex < _lastCompletionIndex; - } - - _autoCompleteChildren.Clear(); - for (int i = 0; i < childCount; ++i) - { - _autoCompleteChildren.Add(_autoCompleteContainer[i]); - } - - int shiftAmount; - if (isIncrementing) - { - shiftAmount = -1 * _lastCompletionIndex.Value; - _lastCompletionIndex = 0; - } - else - { - shiftAmount = 0; - float accumulatedWidth = 0; - for (int i = 1; i <= childCount; ++i) - { - shiftAmount++; - int index = -i % childCount; - index = (index + childCount) % childCount; - VisualElement element = _autoCompleteChildren[index]; - accumulatedWidth += - element.resolvedStyle.width - + element.resolvedStyle.marginLeft - + element.resolvedStyle.marginRight - + element.resolvedStyle.borderLeftWidth - + element.resolvedStyle.borderRightWidth; - - if (accumulatedWidth <= viewportWidth) - { - continue; - } - - if (element != current) - { - --shiftAmount; - } - - break; - } - - _lastCompletionIndex = (shiftAmount - 1 + childCount) % childCount; - } - - _autoCompleteChildren.Shift(shiftAmount); - _lastCompletionBuffer.Shift(shiftAmount); - - _autoCompleteContainer.Clear(); - foreach (VisualElement element in _autoCompleteChildren) - { - _autoCompleteContainer.Add(element); - } - } - - private void RefreshStateButtons() - { - if (_stateButtonContainer == null) - { - return; - } - - _stateButtonContainer.style.top = _currentWindowHeight; - DisplayStyle displayStyle = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; - - for (int i = 0; i < _stateButtonContainer.childCount; ++i) - { - VisualElement child = _stateButtonContainer[i]; - child.style.display = displayStyle; - } - - if (!showGUIButtons) - { - return; - } - - Button firstButton; - Button secondButton; - if (_stateButtonContainer.childCount == 0) - { - firstButton = new Button(FirstClicked) { name = "StateButton1" }; - firstButton.AddToClassList("terminal-button"); - firstButton.style.display = displayStyle; - _stateButtonContainer.Add(firstButton); - - secondButton = new Button(SecondClicked) { name = "StateButton2" }; - secondButton.AddToClassList("terminal-button"); - secondButton.style.display = displayStyle; - _stateButtonContainer.Add(secondButton); - } - else - { - firstButton = _stateButtonContainer[0] as Button; - if (firstButton == null) - { - return; - } - secondButton = _stateButtonContainer[1] as Button; - if (secondButton == null) - { - return; - } - } - - _inputCaretLabel.text = _inputCaret; - - switch (_state) - { - case TerminalState.Closed: - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - firstButton.text = smallButtonText; - } - if (!string.IsNullOrWhiteSpace(fullButtonText)) - { - secondButton.text = fullButtonText; - } - break; - case TerminalState.OpenSmall: - if (!string.IsNullOrWhiteSpace(closeButtonText)) - { - firstButton.text = closeButtonText; - } - if (!string.IsNullOrWhiteSpace(fullButtonText)) - { - secondButton.text = fullButtonText; - } - break; - case TerminalState.OpenFull: - if (!string.IsNullOrWhiteSpace(closeButtonText)) - { - firstButton.text = closeButtonText; - } - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - secondButton.text = smallButtonText; - } - break; - default: - throw new InvalidEnumArgumentException( - nameof(_state), - (int)_state, - typeof(TerminalState) - ); - } - return; - - void FirstClicked() - { - switch (_state) - { - case TerminalState.Closed: - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - SetState(TerminalState.OpenSmall); - } - break; - case TerminalState.OpenSmall: - case TerminalState.OpenFull: - if (!string.IsNullOrWhiteSpace(closeButtonText)) - { - SetState(TerminalState.Closed); - } - break; - default: - throw new InvalidEnumArgumentException( - nameof(_state), - (int)_state, - typeof(TerminalState) - ); - } - } - - void SecondClicked() - { - switch (_state) - { - case TerminalState.Closed: - case TerminalState.OpenSmall: - if (!string.IsNullOrWhiteSpace(fullButtonText)) - { - SetState(TerminalState.OpenFull); - } - break; - case TerminalState.OpenFull: - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - SetState(TerminalState.OpenSmall); - } - break; - default: - throw new InvalidEnumArgumentException( - nameof(_state), - (int)_state, - typeof(TerminalState) - ); - } - } - } - - public Font SetRandomFont(bool persist = false) - { - if (_fontPack == null) - { - return _runtimeFont; - } - - List loadedFonts = _fontPack._fonts; - if (loadedFonts is not { Count: > 0 }) - { - return _runtimeFont; - } - - int currentFontIndex = loadedFonts.IndexOf(_runtimeFont); - - int newFontIndex; - do - { - newFontIndex = ThreadLocalRandom.Instance.Next(loadedFonts.Count); - } while (newFontIndex == currentFontIndex && loadedFonts.Count != 1); - - Font newFont = loadedFonts[newFontIndex]; - SetFont(newFont, persist); - return newFont; - } - - public void SetFont(Font font, bool persist = false) - { - SetRuntimeFont(font); - if (!persist && CurrentFont == font) - { - return; - } - - if (font == null) - { - Debug.LogError("Cannot set null font.", this); - return; - } - - if (_uiDocument == null) - { - Debug.LogError("Cannot set font, no UIDocument assigned."); - return; - } - - Font currentFont = _persistedFont; - _runtimeFont = font; - if (currentFont != font) - { - Debug.Log( - currentFont == null - ? $"Setting font to {font.name}." - : $"Changing font from {currentFont.name} to {font.name}.", - this - ); - } - - if (persist) - { - _persistedFont = font; - } - - return; - - void SetRuntimeFont(Font toSet) - { - if (toSet == null) - { - return; - } - - if (!Application.isPlaying) - { - return; - } - - if (_uiDocument == null) - { - return; - } - - VisualElement root = _uiDocument.rootVisualElement; - if (root == null) - { - return; - } - - root.style.unityFontDefinition = new StyleFontDefinition(toSet); - } - } - - public string SetRandomTheme(bool persist = false) - { - if (_themePack == null) - { - return _runtimeTheme; - } - - List loadedThemes = _themePack._themeNames; - if (loadedThemes is not { Count: > 0 }) - { - return _runtimeTheme; - } - - int currentThemeIndex = loadedThemes.IndexOf(_runtimeTheme); - - int newThemeIndex; - do - { - newThemeIndex = ThreadLocalRandom.Instance.Next(loadedThemes.Count); - } while (newThemeIndex == currentThemeIndex && loadedThemes.Count != 1); - - string newTheme = loadedThemes[newThemeIndex]; - SetTheme(newTheme, persist); - return newTheme; - } - - public void SetTheme(string theme, bool persist = false) - { - string friendlyThemeName = ThemeNameHelper.GetFriendlyThemeName(theme); - SetRuntimeTheme(); - if ( - !persist - && string.Equals( - friendlyThemeName, - CurrentFriendlyTheme, - StringComparison.OrdinalIgnoreCase - ) - ) - { - return; - } - - if (!IsValidTheme(out string validatedTheme)) - { - return; - } - - string currentTheme = ThemeNameHelper.GetFriendlyThemeName(CurrentTheme); - _runtimeTheme = validatedTheme; - if (!string.Equals(currentTheme, friendlyThemeName, StringComparison.OrdinalIgnoreCase)) - { - Debug.Log($"Changing theme from {currentTheme} to {friendlyThemeName}.", this); - } - - if (persist) - { - _persistedTheme = validatedTheme; - } - - return; - - bool IsValidTheme(out string validTheme) - { - if (string.IsNullOrWhiteSpace(theme) || _themePack == null) - { - validTheme = default; - return false; - } - - List themeNames = _themePack._themeNames; - if (themeNames.Contains(theme, StringComparer.OrdinalIgnoreCase)) - { - validTheme = theme; - return true; - } - - foreach (string themeName in ThemeNameHelper.GetPossibleThemeNames(theme)) - { - if (themeNames.Contains(themeName, StringComparer.OrdinalIgnoreCase)) - { - validTheme = themeName; - return true; - } - } - - validTheme = default; - return false; - } - - void SetRuntimeTheme() - { - if (!Application.isPlaying) - { - return; - } - - if (!IsValidTheme(out validatedTheme)) - { - return; - } - - if (_uiDocument == null) - { - return; - } - - VisualElement terminalRoot = _uiDocument.rootVisualElement?.Q( - TerminalRootName - ); - if (terminalRoot == null) - { - return; - } - - string[] loadedThemes = terminalRoot - .GetClasses() - .Where(ThemeNameHelper.IsThemeName) - .ToArray(); - - foreach (string loadedTheme in loadedThemes) - { - terminalRoot.RemoveFromClassList(loadedTheme); - } - - terminalRoot.AddToClassList(validatedTheme); - } - } - - public void HandlePrevious() - { - if (_state == TerminalState.Closed) - { - return; - } - - _input.CommandText = - Terminal.History?.Previous(skipSameCommandsInHistory) ?? string.Empty; - ResetAutoComplete(); - _needsFocus = true; - } - - public void HandleNext() - { - if (_state == TerminalState.Closed) - { - return; - } - - _input.CommandText = Terminal.History?.Next(skipSameCommandsInHistory) ?? string.Empty; - ResetAutoComplete(); - _needsFocus = true; - } - - public void Close() - { - SetState(TerminalState.Closed); - } - - public void ToggleSmall() - { - ToggleState(TerminalState.OpenSmall); - } - - public void ToggleFull() - { - ToggleState(TerminalState.OpenFull); - } - - public void EnterCommand() - { - if (_state == TerminalState.Closed) - { - return; - } - - string commandText = _input.CommandText ?? string.Empty; - if (commandText.NeedsTrim()) - { - commandText = commandText.Trim(); - } - - _input.CommandText = commandText; - try - { - if (string.IsNullOrWhiteSpace(commandText)) - { - return; - } - - Terminal.Log(TerminalLogType.Input, commandText); - Terminal.Shell?.RunCommand(commandText); - while (Terminal.Shell?.TryConsumeErrorMessage(out string error) == true) - { - Terminal.Log(TerminalLogType.Error, $"Error: {error}"); - } - - _input.CommandText = string.Empty; - _needsFocus = true; - _needsScrollToEnd = true; - } - finally - { - ResetAutoComplete(); - } - } - - public void CompleteCommand(bool searchForward = true) - { - if (_state == TerminalState.Closed) - { - return; - } - - try - { - _lastKnownCommandText = _input.CommandText ?? string.Empty; - _lastCompletionBufferTempCache.Clear(); - int caret = - _commandInput != null - ? _commandInput.cursorIndex - : (_lastKnownCommandText?.Length ?? 0); - Terminal.AutoComplete?.Complete( - _lastKnownCommandText, - caret, - _lastCompletionBufferTempCache - ); - bool equivalentBuffers = true; - try - { - int completionLength = _lastCompletionBufferTempCache.Count; - equivalentBuffers = - _lastCompletionBuffer.Count == _lastCompletionBufferTempCache.Count; - if (equivalentBuffers) - { - _lastCompletionBufferTempSet.Clear(); - foreach (string item in _lastCompletionBuffer) - { - _lastCompletionBufferTempSet.Add(item); - } - - foreach (string newCompletionItem in _lastCompletionBufferTempCache) - { - if (!_lastCompletionBufferTempSet.Contains(newCompletionItem)) - { - equivalentBuffers = false; - break; - } - } - } - if (equivalentBuffers) - { - if (0 < completionLength) - { - if (_lastCompletionIndex == null) - { - _lastCompletionIndex = 0; - } - else if (searchForward) - { - _lastCompletionIndex = - (_lastCompletionIndex + 1) % completionLength; - } - else - { - _lastCompletionIndex = - (_lastCompletionIndex - 1 + completionLength) - % completionLength; - } - - _input.CommandText = _lastCompletionBuffer[_lastCompletionIndex.Value]; - } - else - { - _lastCompletionIndex = null; - } - } - else - { - if (0 < completionLength) - { - _lastCompletionIndex = 0; - _input.CommandText = _lastCompletionBufferTempCache[0]; - } - else - { - _lastCompletionIndex = null; - } - } - } - finally - { - if (!equivalentBuffers) - { - _lastCompletionBuffer.Clear(); - foreach (string item in _lastCompletionBufferTempCache) - { - _lastCompletionBuffer.Add(item); - } - _previousLastCompletionIndex = null; - } - - _previousLastCompletionIndex ??= _lastCompletionIndex; - } - } - finally - { - _needsFocus = true; - } - } - - private void StartHeightAnimation() - { - if (Mathf.Approximately(_currentWindowHeight, _targetWindowHeight)) - { - _isAnimating = false; - return; - } - - _initialWindowHeight = _currentWindowHeight; - _animationTimer = 0f; - _isAnimating = true; - } - - private void HandleHeightAnimation() - { - if (!_isAnimating) - { - return; - } - - _animationTimer += Time.unscaledDeltaTime; - - AnimationCurve selectedCurve; - float animationDuration; - bool isExpanding = _targetWindowHeight > _initialWindowHeight; - - if (isExpanding) - { - selectedCurve = easeOutCurve; - animationDuration = easeOutTime; - } - else - { - selectedCurve = easeInCurve; - animationDuration = easeInTime; - } - - if (animationDuration <= 0f) - { - _currentWindowHeight = _targetWindowHeight; - _isAnimating = false; - return; - } - - float normalizedTime = Mathf.Clamp01(_animationTimer / animationDuration); - - float curveValue = selectedCurve.Evaluate(normalizedTime); - - _currentWindowHeight = Mathf.LerpUnclamped( - _initialWindowHeight, - _targetWindowHeight, - curveValue - ); - - if (isExpanding) - { - _currentWindowHeight = Mathf.Clamp( - _currentWindowHeight, - _initialWindowHeight, - _targetWindowHeight - ); - } - else - { - _currentWindowHeight = Mathf.Clamp( - _currentWindowHeight, - _targetWindowHeight, - _initialWindowHeight - ); - } - - if ( - Mathf.Approximately(_currentWindowHeight, _targetWindowHeight) - || animationDuration <= _animationTimer - ) - { - _currentWindowHeight = _targetWindowHeight; - _isAnimating = false; - } - } - - private static void HandleUnityLog(string message, string stackTrace, LogType type) - { - Terminal.Buffer?.EnqueueUnityLog(message, stackTrace, (TerminalLogType)type); - } - } -} +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Editor")] + +namespace WallstopStudios.DxCommandTerminal.UI +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Linq; + using Attributes; + using Backend; + using Extensions; + using Helper; + using Input; + using Themes; + using UnityEditor; + using UnityEngine; + using UnityEngine.UIElements; + + [DisallowMultipleComponent] + public sealed class TerminalUI : MonoBehaviour + { + private const string TerminalRootName = "TerminalRoot"; + + private enum ScrollBarCaptureState + { + None = 0, + DraggerActive = 1, + TrackerActive = 2, + } + + // Cache log callback to reduce allocations + private static readonly Application.LogCallback UnityLogCallback = HandleUnityLog; + + public static TerminalUI Instance { get; private set; } + + // ReSharper disable once MemberCanBePrivate.Global + public bool IsClosed => + _state != TerminalState.OpenFull + && _state != TerminalState.OpenSmall + && Mathf.Approximately(_currentWindowHeight, _targetWindowHeight); + + public string CurrentTheme => + !string.IsNullOrWhiteSpace(_runtimeTheme) ? _runtimeTheme : _persistedTheme; + + public string CurrentFriendlyTheme => ThemeNameHelper.GetFriendlyThemeName(CurrentTheme); + + public Font CurrentFont => _runtimeFont != null ? _runtimeFont : _persistedFont; + + [SerializeField] + [Tooltip("Unique Id for this terminal, mainly for use with persisted configuration")] + internal string id = Guid.NewGuid().ToString(); + + [SerializeField] + internal UIDocument _uiDocument; + + [SerializeField] + internal string _persistedTheme = "dark-theme"; + + [Header("Window")] + [Range(0, 1)] + public float maxHeight = 0.7f; + + [SerializeField] + [Range(0, 1)] + public float smallTerminalRatio = 0.4714285f; + + [Tooltip("Curve the console follows to go from closed -> open")] + public AnimationCurve easeOutCurve = new() + { + keys = new[] { new Keyframe(0, 0), new Keyframe(1, 1) }, + }; + + [Tooltip("Duration for the ease-out animation in seconds")] + public float easeOutTime = 0.5f; + + [Tooltip("Curve the console follows to go from open -> closed")] + public AnimationCurve easeInCurve = new() + { + keys = new[] { new Keyframe(0, 0), new Keyframe(1, 1) }, + }; + + [Tooltip("Duration for the ease-in animation in seconds")] + public float easeInTime = 0.5f; + + [Header("System")] + [SerializeField] + private int _logBufferSize = 256; + + [SerializeField] + private int _historyBufferSize = 512; + + [Header("Input")] + [SerializeField] + internal Font _persistedFont; + + [SerializeField] + private string _inputCaret = ">"; + + [Header("Buttons")] + public bool showGUIButtons; + + [DxShowIf(nameof(showGUIButtons))] + public string runButtonText = "run"; + + [DxShowIf(nameof(showGUIButtons))] + [SerializeField] + public string closeButtonText = "close"; + + [DxShowIf(nameof(showGUIButtons))] + public string smallButtonText = "small"; + + [DxShowIf(nameof(showGUIButtons))] + public string fullButtonText = "full"; + + [Header("Hints")] + public HintDisplayMode hintDisplayMode = HintDisplayMode.AutoCompleteOnly; + + public bool makeHintsClickable = true; + + [Header("System")] + [SerializeField] + private int _cursorBlinkRateMilliseconds = 666; + +#if UNITY_EDITOR + [SerializeField] + private bool _trackChangesInEditor = true; +#endif + + [Tooltip("Will reset static command state in OnEnable and Start when set to true")] + public bool resetStateOnInit; + + public bool skipSameCommandsInHistory = true; + + [SerializeField] + public bool ignoreDefaultCommands; + + [SerializeField] + private bool _logUnityMessages; + + [SerializeField] + internal List _ignoredLogTypes = new(); + + [SerializeField] + internal List _disabledCommands = new(); + + [SerializeField] + internal TerminalFontPack _fontPack; + + [SerializeField] + internal TerminalThemePack _themePack; + + private IInputHandler[] _inputHandlers; + +#if UNITY_EDITOR + private readonly Dictionary _propertyValues = new(); + private readonly List _uiProperties = new(); + private readonly List _themeProperties = new(); + private readonly List _cursorBlinkProperties = new(); + private readonly List _fontProperties = new(); + private readonly List _staticStateProperties = new(); + private readonly List _windowProperties = new(); + private readonly List _logUnityMessageProperties = new(); + private readonly List _autoCompleteProperties = new(); + private SerializedObject _serializedObject; +#endif + + // Editor integration +#if UNITY_EDITOR + [Header("Editor")] + [Tooltip( + "When enabled (and runtime mode allows Editor), the terminal will auto-discover IArgParser implementations on reload/start." + )] + [SerializeField] + private bool _autoDiscoverParsersInEditor; +#endif + + [Header("Runtime Mode")] + [Tooltip( + "Controls which environment-specific features are enabled. Choose explicit modes. None is obsolete." + )] + [SerializeField] +#pragma warning disable CS0618 // Type or member is obsolete + private Backend.TerminalRuntimeModeFlags _runtimeModes = Backend + .TerminalRuntimeModeFlags + .None; +#pragma warning restore CS0618 // Type or member is obsolete + + // Test helper to skip building UI entirely (prevents UI Toolkit panel updates) + internal bool disableUIForTests; + + private TerminalState _state = TerminalState.Closed; + private float _currentWindowHeight; + private float _targetWindowHeight; + private float _realWindowHeight; + private bool _unityLogAttached; + private bool _started; + private bool _needsFocus; + private bool _needsScrollToEnd; + private bool _needsAutoCompleteReset; + private long? _lastSeenBufferVersion; + private string _lastKnownCommandText; + private int? _lastCompletionIndex; + private int? _previousLastCompletionIndex; + private string _focusedControl; + private bool _isCommandFromCode; + private bool _initialResetStateOnInit; + private bool _commandIssuedThisFrame; + private string _runtimeTheme; + private Font _runtimeFont; + private float _initialWindowHeight; + private float _animationTimer; + private bool _isAnimating; + + private VisualElement _terminalContainer; + private ScrollView _logScrollView; + private ScrollView _autoCompleteContainer; + private VisualElement _inputContainer; + private TextField _commandInput; + private Button _runButton; + private VisualElement _stateButtonContainer; + private VisualElement _textInput; + private Label _inputCaretLabel; + private bool _lastKnownHintsClickable; + private IVisualElementScheduledItem _cursorBlinkSchedule; + + private readonly List _lastCompletionBuffer = new(); + private readonly List _lastCompletionBufferTempCache = new(); + private readonly HashSet _lastCompletionBufferTempSet = new( + StringComparer.OrdinalIgnoreCase + ); + private readonly List _autoCompleteChildren = new(); + + // Cached for performance (avoids allocations) + private readonly Action _focusInput; +#if UNITY_EDITOR + private readonly EditorApplication.CallbackFunction _checkForChanges; +#endif + private ITerminalInput _input; + + public TerminalUI() + { + _focusInput = FocusInput; +#if UNITY_EDITOR + _checkForChanges = CheckForChanges; +#endif + } + + private void Awake() + { + Backend.TerminalRuntimeConfig.SetMode(_runtimeModes); +#if UNITY_EDITOR + Backend.TerminalRuntimeConfig.EditorAutoDiscover = _autoDiscoverParsersInEditor; +#endif + Backend.TerminalRuntimeConfig.TryAutoDiscoverParsers(); + switch (_logBufferSize) + { + case <= 0: + Debug.LogError( + $"Invalid buffer size '{_logBufferSize}', must be greater than zero. Defaulting to 0 (empty buffer).", + this + ); + break; + case < 10: + Debug.LogWarning( + $"Unsupported buffer size '{_logBufferSize}', recommended size is > 10.", + this + ); + break; + } + + switch (_historyBufferSize) + { + case <= 0: + Debug.LogError( + $"Invalid buffer size '{_historyBufferSize}', must be greater than zero. Defaulting to 0 (empty buffer).", + this + ); + break; + case < 10: + Debug.LogWarning( + $"Unsupported buffer size '{_historyBufferSize}', recommended size is > 10.", + this + ); + break; + } + + _inputHandlers = GetComponents(); + + if (!TryGetComponent(out _input)) + { + _input = DefaultTerminalInput.Instance; + } + + Instance = this; + +#if UNITY_EDITOR + _serializedObject = new SerializedObject(this); + + string[] uiPropertiesTracked = { nameof(_uiDocument) }; + TrackProperties(uiPropertiesTracked, _uiProperties); + + string[] themePropertiesTracked = { nameof(_themePack) }; + TrackProperties(themePropertiesTracked, _themeProperties); + + string[] cursorBlinkPropertiesTracked = { nameof(_cursorBlinkRateMilliseconds) }; + TrackProperties(cursorBlinkPropertiesTracked, _cursorBlinkProperties); + + string[] fontPropertiesTracked = { nameof(_persistedFont) }; + TrackProperties(fontPropertiesTracked, _fontProperties); + + string[] staticStaticPropertiesTracked = + { + nameof(_logBufferSize), + nameof(_historyBufferSize), + nameof(_ignoredLogTypes), + nameof(_disabledCommands), + nameof(ignoreDefaultCommands), + nameof(_fontPack), + }; + TrackProperties(staticStaticPropertiesTracked, _staticStateProperties); + + string[] windowPropertiesTracked = { nameof(maxHeight), nameof(smallTerminalRatio) }; + TrackProperties(windowPropertiesTracked, _windowProperties); + + string[] logUnityMessagePropertiesTracked = { nameof(_logUnityMessages) }; + TrackProperties(logUnityMessagePropertiesTracked, _logUnityMessageProperties); + + string[] autoCompletePropertiesTracked = + { + nameof(hintDisplayMode), + nameof(_disabledCommands), + nameof(ignoreDefaultCommands), + }; + TrackProperties(autoCompletePropertiesTracked, _autoCompleteProperties); + + void TrackProperties(string[] properties, List storage) + { + foreach (string propertyName in properties) + { + SerializedProperty property = _serializedObject.FindProperty(propertyName); + if (property != null) + { + storage.Add(property); + object value = property.GetValue(); + switch (value) + { + case List stringList: + value = stringList.ToList(); + break; + case List logTypeList: + value = logTypeList.ToList(); + break; + case List fontList: + value = fontList.ToList(); + break; + } + _propertyValues[property.name] = value; + } + else + { + Debug.LogWarning( + $"Failed to track/find window property {propertyName}, updates to this property will be ignored.", + this + ); + } + } + } +#endif + } + + private void OnEnable() + { + RefreshStaticState(force: resetStateOnInit); + ConsumeAndLogErrors(); + + if (_logUnityMessages && !_unityLogAttached) + { + Application.logMessageReceivedThreaded += UnityLogCallback; + _unityLogAttached = true; + } + + SetupUI(); + +#if UNITY_EDITOR + EditorApplication.update += _checkForChanges; +#endif + } + + private void OnDisable() + { +#if UNITY_EDITOR + EditorApplication.update -= _checkForChanges; +#endif + if (_uiDocument != null) + { + _uiDocument.rootVisualElement?.Clear(); + } + + if (_unityLogAttached) + { + Application.logMessageReceivedThreaded -= UnityLogCallback; + _unityLogAttached = false; + } + + SetState(TerminalState.Closed); + } + + private void OnDestroy() + { + if (Instance != this) + { + return; + } + + Instance = null; + } + + private void Start() + { + if (_started) + { + SetState(TerminalState.Closed); + } + + RefreshStaticState(force: resetStateOnInit); + ResetWindowIdempotent(); + ConsumeAndLogErrors(); + ResetAutoComplete(); + _started = true; + _lastKnownHintsClickable = makeHintsClickable; + } + + private void LateUpdate() + { + ResetWindowIdempotent(); + // Drain any cross-thread logs into the main-thread buffer before refreshing UI + Terminal.Buffer?.DrainPending(); + HandleHeightAnimation(); + RefreshUI(); + _commandIssuedThisFrame = false; + } + + private void RefreshStaticState(bool force) + { + int logBufferSize = Mathf.Max(0, _logBufferSize); + if (force || Terminal.Buffer == null) + { + Terminal.Buffer = new CommandLog(logBufferSize, _ignoredLogTypes); + } + else + { + if (Terminal.Buffer.Capacity != logBufferSize) + { + Terminal.Buffer.Resize(logBufferSize); + } + if ( + !Terminal.Buffer.ignoredLogTypes.SetEquals( + _ignoredLogTypes ?? Enumerable.Empty() + ) + ) + { + Terminal.Buffer.ignoredLogTypes.Clear(); + Terminal.Buffer.ignoredLogTypes.UnionWith( + _ignoredLogTypes ?? Enumerable.Empty() + ); + } + } + + int historyBufferSize = Mathf.Max(0, _historyBufferSize); + if (force || Terminal.History == null) + { + Terminal.History = new CommandHistory(historyBufferSize); + } + else if (Terminal.History.Capacity != historyBufferSize) + { + Terminal.History.Resize(historyBufferSize); + } + + if (force || Terminal.Shell == null) + { + Terminal.Shell = new CommandShell(Terminal.History); + } + + if (force || Terminal.AutoComplete == null) + { + Terminal.AutoComplete = new CommandAutoComplete(Terminal.History, Terminal.Shell); + } + + if ( + Terminal.Shell.IgnoringDefaultCommands != ignoreDefaultCommands + || Terminal.Shell.Commands.Count <= 0 + || !Terminal.Shell.IgnoredCommands.SetEquals( + _disabledCommands ?? Enumerable.Empty() + ) + ) + { + Terminal.Shell.ClearAutoRegisteredCommands(); + Terminal.Shell.InitializeAutoRegisteredCommands( + ignoredCommands: _disabledCommands, + ignoreDefaultCommands: ignoreDefaultCommands + ); + + if (_started) + { + ResetAutoComplete(); + } + } + } + +#if UNITY_EDITOR + private void CheckForChanges() + { + if (!_trackChangesInEditor) + { + return; + } + + if (!_started) + { + return; + } + + if (Instance != this) + { + return; + } + + _serializedObject.Update(); + if (CheckForRefresh(_uiProperties)) + { + SetupUI(); + } + + if (CheckForRefresh(_themeProperties)) + { + if (_uiDocument != null) + { + InitializeTheme( + _uiDocument.rootVisualElement?.Q(TerminalRootName) + ); + } + } + + if (CheckForRefresh(_cursorBlinkProperties)) + { + ScheduleBlinkingCursor(); + } + + if (CheckForRefresh(_fontProperties)) + { + SetFont(_persistedFont); + } + + if (CheckForRefresh(_staticStateProperties)) + { + RefreshStaticState(force: false); + } + + if (CheckForRefresh(_windowProperties)) + { + ResetWindowIdempotent(); + } + + if (CheckForRefresh(_logUnityMessageProperties)) + { + if (_logUnityMessages && !_unityLogAttached) + { + _unityLogAttached = true; + Application.logMessageReceivedThreaded += HandleUnityLog; + } + else if (!_logUnityMessages && _unityLogAttached) + { + Application.logMessageReceivedThreaded -= HandleUnityLog; + _unityLogAttached = false; + } + } + + if (CheckForRefresh(_autoCompleteProperties)) + { + _autoCompleteContainer?.Clear(); + ResetAutoComplete(); + } + + return; + + bool CheckForRefresh(List properties) + { + bool needRefresh = false; + foreach (SerializedProperty property in properties) + { + object propertyValue = property.GetValue(); + object previousValue = _propertyValues[property.name]; + if ( + propertyValue is List currentStringList + && previousValue is List previousStringList + ) + { + if (!currentStringList.SequenceEqual(previousStringList)) + { + needRefresh = true; + _propertyValues[property.name] = currentStringList.ToList(); + } + + continue; + } + if ( + propertyValue is List currentLogTypeList + && previousValue is List previousLogTypeList + ) + { + if (!currentLogTypeList.SequenceEqual(previousLogTypeList)) + { + needRefresh = true; + _propertyValues[property.name] = currentLogTypeList.ToList(); + } + + continue; + } + + if (Equals(propertyValue, previousValue)) + { + continue; + } + + needRefresh = true; + _propertyValues[property.name] = propertyValue; + } + + return needRefresh; + } + } +#endif + + public void ToggleState(TerminalState newState) + { + SetState(_state == newState ? TerminalState.Closed : newState); + } + + public void SetState(TerminalState newState) + { + _commandIssuedThisFrame = true; + _state = newState; + ResetWindowIdempotent(); + if (_state != TerminalState.Closed) + { + _needsFocus = true; + } + else + { + _input.CommandText = string.Empty; + ResetAutoComplete(); + } + } + + private static void ConsumeAndLogErrors() + { + while (Terminal.Shell?.TryConsumeErrorMessage(out string error) == true) + { + Terminal.Log(TerminalLogType.Error, $"Error: {error}"); + } + } + + private void ResetAutoComplete() + { + _lastKnownCommandText = _input.CommandText ?? string.Empty; + if (hintDisplayMode == HintDisplayMode.Always) + { + _lastCompletionBufferTempCache.Clear(); + int caret = + _commandInput != null + ? _commandInput.cursorIndex + : (_lastKnownCommandText?.Length ?? 0); + Terminal.AutoComplete?.Complete( + _lastKnownCommandText, + caret, + _lastCompletionBufferTempCache + ); + bool equivalent = + _lastCompletionBufferTempCache.Count == _lastCompletionBuffer.Count; + if (equivalent) + { + _lastCompletionBufferTempSet.Clear(); + foreach (string completion in _lastCompletionBuffer) + { + _lastCompletionBufferTempSet.Add(completion); + } + + foreach (string completion in _lastCompletionBufferTempCache) + { + if (!_lastCompletionBufferTempSet.Contains(completion)) + { + equivalent = false; + break; + } + } + } + + if (!equivalent) + { + _lastCompletionIndex = null; + _previousLastCompletionIndex = null; + _lastCompletionBuffer.Clear(); + foreach (string completion in _lastCompletionBufferTempCache) + { + _lastCompletionBuffer.Add(completion); + } + } + } + else + { + _lastCompletionIndex = null; + _previousLastCompletionIndex = null; + _lastCompletionBuffer.Clear(); + } + } + + private void ResetWindowIdempotent() + { + int height = Screen.height; + float oldTargetHeight = _targetWindowHeight; + try + { + switch (_state) + { + case TerminalState.OpenSmall: + { + _realWindowHeight = height * maxHeight * smallTerminalRatio; + _targetWindowHeight = _realWindowHeight; + break; + } + case TerminalState.OpenFull: + { + _realWindowHeight = height * maxHeight; + _targetWindowHeight = _realWindowHeight; + break; + } + default: + { + _realWindowHeight = height * maxHeight * smallTerminalRatio; + _targetWindowHeight = 0; + break; + } + } + } + finally + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (oldTargetHeight != _targetWindowHeight) + { + StartHeightAnimation(); + } + } + } + + private void SetupUI() + { + if (disableUIForTests) + { + return; + } + if (_uiDocument == null) + { + _uiDocument = GetComponent(); + } + if (_uiDocument == null) + { + Debug.LogError("No UIDocument assigned, cannot setup UI.", this); + return; + } + + VisualElement uiRoot = _uiDocument.rootVisualElement; + if (uiRoot == null) + { + Debug.LogError("No UI root element assigned, cannot setup UI.", this); + return; + } + + uiRoot.Clear(); + VisualElement root = new(); + uiRoot.Add(root); + root.name = TerminalRootName; + root.AddToClassList("terminal-root"); + + InitializeTheme(root); + InitializeFont(); + // Ensure a font is set after initialization + if (CurrentFont != null) + { + SetFont(CurrentFont, persist: false); + } + + if (!string.IsNullOrWhiteSpace(_runtimeTheme)) + { + root.AddToClassList(_runtimeTheme); + } + else + { + Debug.LogError("Failed to load any themes!", this); + } + + _terminalContainer = new VisualElement { name = "TerminalContainer" }; + _terminalContainer.AddToClassList("terminal-container"); + _uiDocument.rootVisualElement.style.height = new StyleLength(_realWindowHeight); + _terminalContainer.style.height = new StyleLength(_realWindowHeight); + root.Add(_terminalContainer); + + _logScrollView = new ScrollView(); + InitializeScrollView(_logScrollView); + _logScrollView.name = "LogScrollView"; + _logScrollView.AddToClassList("log-scroll-view"); + _terminalContainer.Add(_logScrollView); + + _autoCompleteContainer = new ScrollView(ScrollViewMode.Horizontal) + { + name = "AutoCompletePopup", + }; + _autoCompleteContainer.AddToClassList("autocomplete-popup"); + _terminalContainer.Add(_autoCompleteContainer); + + _inputContainer = new VisualElement { name = "InputContainer" }; + _inputContainer.AddToClassList("input-container"); + _terminalContainer.Add(_inputContainer); + + _runButton = new Button(EnterCommand) + { + text = runButtonText, + name = "RunCommandButton", + }; + _runButton.AddToClassList("terminal-button"); + _runButton.AddToClassList("terminal-button-run"); + _runButton.style.display = DisplayStyle.None; + _runButton.style.marginLeft = 6; + _runButton.style.marginRight = 4; + _runButton.style.paddingTop = 2; + _runButton.style.paddingBottom = 2; + _inputContainer.Add(_runButton); + + _inputCaretLabel = new Label(_inputCaret) { name = "InputCaret" }; + _inputCaretLabel.AddToClassList("terminal-input-caret"); + _inputContainer.Add(_inputCaretLabel); + + _commandInput = new TextField(); + ScheduleBlinkingCursor(); + _commandInput.name = "CommandInput"; + _commandInput.AddToClassList("terminal-input-field"); + _commandInput.pickingMode = PickingMode.Position; + _commandInput.value = _input.CommandText; + _commandInput.RegisterCallback, TerminalUI>( + (evt, context) => + { + if ( + context._commandIssuedThisFrame + || Array.Exists( + context._inputHandlers, + handler => handler.ShouldHandleInputThisFrame + ) + ) + { + if (!string.Equals(context._commandInput.value, context._input.CommandText)) + { + context._commandInput.value = context._input.CommandText; + } + // Ensure subsequent user keystrokes (e.g., space) trigger recompute + // even if this event was caused by programmatic text changes (Tab, etc.). + context._isCommandFromCode = false; + evt.StopPropagation(); + return; + } + + // Assign input text + context._input.CommandText = evt.newValue; + + // If the user just typed a space right after a recognized command name, + // proactively clear the hint bar so stale command-name suggestions disappear + // before argument-context suggestions are computed/shown. + try + { + string prev = evt.previousValue ?? string.Empty; + string curr = evt.newValue ?? string.Empty; + bool justTypedSpace = curr.EndsWith(" ") && curr.Length == prev.Length + 1; + if (justTypedSpace && Backend.Terminal.Shell != null) + { + string check = curr; + // Remove trailing space(s) to isolate the command token + if (check.NeedsTrim()) + { + check = check.TrimEnd(); + } + + if ( + Backend.CommandShell.TryEatArgument( + ref check, + out Backend.CommandArg cmd + ) + ) + { + if (Backend.Terminal.Shell.Commands.ContainsKey(cmd.contents)) + { + // Clear existing suggestions immediately + context._lastCompletionIndex = null; + context._previousLastCompletionIndex = null; + context._lastCompletionBuffer.Clear(); + context._autoCompleteContainer?.Clear(); + } + } + } + } + catch + { /* non-fatal UI hint clearing */ + } + + context._runButton.style.display = + context.showGUIButtons + && !string.IsNullOrWhiteSpace(context._input.CommandText) + && !string.IsNullOrWhiteSpace(context.runButtonText) + ? DisplayStyle.Flex + : DisplayStyle.None; + if (!context._isCommandFromCode) + { + context.ResetAutoComplete(); + } + + context._isCommandFromCode = false; + }, + userArgs: this, + useTrickleDown: TrickleDown.TrickleDown + ); + + _inputContainer.Add(_commandInput); + _textInput = _commandInput.Q("unity-text-input"); + + _stateButtonContainer = new VisualElement { name = "StateButtonContainer" }; + _stateButtonContainer.AddToClassList("state-button-container"); + root.Add(_stateButtonContainer); + RefreshStateButtons(); + } + + private void InitializeTheme(VisualElement root) + { + if (_themePack == null) + { + Debug.LogWarning("No theme pack assigned, cannot initialize theme.", this); + return; + } + + if (root != null) + { + for (int i = root.styleSheets.count - 1; 0 <= i; --i) + { + StyleSheet styleSheet = root.styleSheets[i]; + if ( + styleSheet == null + || styleSheet.name.Contains("Theme", StringComparison.OrdinalIgnoreCase) + ) + { + root.styleSheets.Remove(styleSheet); + } + } + + foreach (StyleSheet styleSheet in _themePack._themes) + { + if (styleSheet == null) + { + continue; + } + + root.styleSheets.Add(styleSheet); + } + } + else + { + Debug.LogWarning( + "No root element assigned, theme initialization may be broken.", + this + ); + } + + _runtimeTheme = _persistedTheme; + List themeNames = _themePack._themeNames; + if (themeNames.Contains(_runtimeTheme)) + { + return; + } + + if (themeNames is { Count: > 0 }) + { + _runtimeTheme = themeNames.FirstOrDefault(theme => + theme.Contains("dark", StringComparison.OrdinalIgnoreCase) + ); + if (_runtimeTheme == null) + { + _runtimeTheme = themeNames.FirstOrDefault(theme => + theme.Contains("light", StringComparison.OrdinalIgnoreCase) + ); + } + if (_runtimeTheme == null) + { + _runtimeTheme = themeNames.FirstOrDefault(); + } + Debug.LogWarning($"Persisted theme not found, defaulting to '{_runtimeTheme}'."); + } + else + { + Debug.LogWarning("No available terminal themes.", this); + } + } + + // Support method for tests and tooling to inject theme/font packs before enabling + public void InjectPacks( + Themes.TerminalThemePack themePack, + Themes.TerminalFontPack fontPack + ) + { + _themePack = themePack; + _fontPack = fontPack; + } + + private void InitializeFont() + { + if (_fontPack == null) + { + Debug.LogWarning("No font pack assigned, cannot initialize font.", this); + return; + } + + _runtimeFont = _persistedFont; + if (_runtimeFont != null) + { + return; + } + + List loadedFonts = _fontPack._fonts; + if (loadedFonts is { Count: > 0 }) + { + _runtimeFont = loadedFonts.FirstOrDefault(font => + font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) + && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) + ); + if (_runtimeFont == null) + { + _runtimeFont = loadedFonts.FirstOrDefault(font => + font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) + ); + } + if (_runtimeFont == null) + { + _runtimeFont = loadedFonts.FirstOrDefault(font => + font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) + ); + } + if (_runtimeFont == null) + { + _runtimeFont = loadedFonts.FirstOrDefault(); + } + } + + if (_runtimeFont == null) + { + Debug.LogWarning("No font assigned, defaulting to Courier New 16pt", this); + _runtimeFont = Font.CreateDynamicFontFromOSFont("Courier New", 16); + } + else + { + Debug.LogWarning($"No font assigned, defaulting to {_runtimeFont.name}.", this); + } + } + + private void ScheduleBlinkingCursor() + { + _cursorBlinkSchedule?.Pause(); + _cursorBlinkSchedule = null; + + if (_commandInput == null) + { + return; + } + + bool shouldRenderCursor = true; + _cursorBlinkSchedule = _commandInput + .schedule.Execute(() => + { + _commandInput.EnableInClassList("transparent-cursor", shouldRenderCursor); + _commandInput.EnableInClassList("styled-cursor", !shouldRenderCursor); + shouldRenderCursor = !shouldRenderCursor; + }) + .Every(_cursorBlinkRateMilliseconds); + } + + private static void InitializeScrollView(ScrollView scrollView) + { + VisualElement parent = scrollView.Q( + className: "unity-scroller--vertical" + ); + if (parent == null) + { + scrollView.RegisterCallback(ReInitialize); + return; + + void ReInitialize(GeometryChangedEvent evt) + { + InitializeScrollView(scrollView); + scrollView.UnregisterCallback(ReInitialize); + } + } + VisualElement trackerElement = parent.Q( + className: "unity-base-slider__tracker" + ); + VisualElement draggerElement = parent.Q( + className: "unity-base-slider__dragger" + ); + + ScrollBarCaptureState scrollBarCaptureState = ScrollBarCaptureState.None; + + RegisterCallbacks(); + return; + + void RegisterCallbacks() + { + // Hover Events + trackerElement.RegisterCallback(OnTrackerMouseEnter); + trackerElement.RegisterCallback(OnTrackerMouseLeave); + draggerElement.RegisterCallback(OnDraggerMouseEnter); + draggerElement.RegisterCallback(OnDraggerMouseLeave); + + trackerElement.RegisterCallback(OnTrackerPointerDown); + trackerElement.RegisterCallback(OnTrackerPointerUp); + draggerElement.RegisterCallback(OnDraggerPointerDown); + parent.RegisterCallback(OnDraggerPointerCaptureOut); + } + + void OnTrackerPointerDown(PointerDownEvent evt) + { + scrollBarCaptureState = ScrollBarCaptureState.TrackerActive; + draggerElement.AddToClassList("tracker-active"); + draggerElement.RemoveFromClassList("tracker-hovered"); + } + + void OnTrackerPointerUp(PointerUpEvent evt) + { + scrollBarCaptureState = ScrollBarCaptureState.None; + draggerElement.RemoveFromClassList("tracker-active"); + } + + void OnDraggerPointerDown(PointerDownEvent evt) + { + scrollBarCaptureState = ScrollBarCaptureState.DraggerActive; + trackerElement.AddToClassList("dragger-active"); + draggerElement.AddToClassList("dragger-active"); + trackerElement.RemoveFromClassList("dragger-hovered"); + } + + void OnDraggerPointerCaptureOut(PointerCaptureOutEvent evt) + { + scrollBarCaptureState = ScrollBarCaptureState.None; + trackerElement.RemoveFromClassList("dragger-active"); + draggerElement.RemoveFromClassList("tracker-active"); + draggerElement.RemoveFromClassList("dragger-active"); + } + + void OnTrackerMouseEnter(MouseEnterEvent evt) + { + if (scrollBarCaptureState == ScrollBarCaptureState.None) + { + draggerElement.AddToClassList("tracker-hovered"); + } + } + + void OnTrackerMouseLeave(MouseLeaveEvent evt) + { + if (scrollBarCaptureState == ScrollBarCaptureState.None) + { + draggerElement.RemoveFromClassList("tracker-hovered"); + } + } + + void OnDraggerMouseEnter(MouseEnterEvent evt) + { + if (scrollBarCaptureState == ScrollBarCaptureState.None) + { + trackerElement.AddToClassList("dragger-hovered"); + } + } + + void OnDraggerMouseLeave(MouseLeaveEvent evt) + { + if (scrollBarCaptureState == ScrollBarCaptureState.None) + { + trackerElement.RemoveFromClassList("dragger-hovered"); + } + } + } + + private void RefreshUI() + { + if (_terminalContainer == null) + { + return; + } + + if (_commandIssuedThisFrame) + { + return; + } + + _uiDocument.rootVisualElement.style.height = _currentWindowHeight; + _terminalContainer.style.height = _currentWindowHeight; + _terminalContainer.style.width = Screen.width; + DisplayStyle commandInputStyle = + _currentWindowHeight <= 30 ? DisplayStyle.None : DisplayStyle.Flex; + + _needsFocus |= + _inputContainer.resolvedStyle.display != commandInputStyle + && commandInputStyle == DisplayStyle.Flex; + _inputContainer.style.display = commandInputStyle; + + RefreshLogs(); + RefreshAutoCompleteHints(); + string commandInput = _input.CommandText; + if (!string.Equals(_commandInput.value, commandInput)) + { + _isCommandFromCode = true; + _commandInput.value = commandInput; + } + else if ( + _needsFocus + && _textInput.focusable + && _textInput.resolvedStyle.display != DisplayStyle.None + && _commandInput.resolvedStyle.display != DisplayStyle.None + ) + { + if (_textInput.focusController.focusedElement != _textInput) + { + _textInput.schedule.Execute(_focusInput).ExecuteLater(0); + FocusInput(); + } + + _needsFocus = false; + } + else if ( + _needsScrollToEnd + && _logScrollView != null + && _logScrollView.style.display != DisplayStyle.None + ) + { + ScrollToEnd(); + _needsScrollToEnd = false; + } + RefreshStateButtons(); + } + + private void FocusInput() + { + if (_textInput == null) + { + return; + } + + _textInput.Focus(); + int textEndPosition = _commandInput.value.Length; + _commandInput.cursorIndex = textEndPosition; + _commandInput.selectIndex = textEndPosition; + } + + private void RefreshLogs() + { + IReadOnlyList logs = Terminal.Buffer?.Logs; + if (logs == null) + { + return; + } + + if (_logScrollView == null) + { + return; + } + + VisualElement content = _logScrollView.contentContainer; + bool dirty = _lastSeenBufferVersion != Terminal.Buffer.Version; + if (content.childCount != logs.Count) + { + dirty = true; + if (content.childCount < logs.Count) + { + for (int i = 0; i < logs.Count - content.childCount; ++i) + { + Label logText = new(); + logText.AddToClassList("terminal-output-label"); + content.Add(logText); + } + } + else if (logs.Count < content.childCount) + { + for (int i = content.childCount - 1; logs.Count <= i; --i) + { + content.RemoveAt(i); + } + } + + _needsScrollToEnd = true; + } + + if (dirty) + { + for (int i = 0; i < logs.Count && i < content.childCount; ++i) + { + VisualElement item = content[i]; + switch (item) + { + case TextField logText: + { + LogItem logItem = logs[i]; + SetupLogText(logText, logItem); + logText.value = logItem.message; + break; + } + case Label logLabel: + { + LogItem logItem = logs[i]; + SetupLogText(logLabel, logItem); + logLabel.text = logItem.message; + break; + } + case Button button: + { + LogItem logItem = logs[i]; + SetupLogText(button, logItem); + button.text = logItem.message; + break; + } + } + } + + if (logs.Count == content.childCount) + { + _lastSeenBufferVersion = Terminal.Buffer.Version; + } + } + return; + + static void SetupLogText(VisualElement logText, LogItem log) + { + logText.EnableInClassList( + "terminal-output-label--shell", + log.type == TerminalLogType.ShellMessage + ); + logText.EnableInClassList( + "terminal-output-label--error", + log.type + is TerminalLogType.Exception + or TerminalLogType.Error + or TerminalLogType.Assert + ); + logText.EnableInClassList( + "terminal-output-label--warning", + log.type == TerminalLogType.Warning + ); + logText.EnableInClassList( + "terminal-output-label--message", + log.type == TerminalLogType.Message + ); + logText.EnableInClassList( + "terminal-output-label--input", + log.type == TerminalLogType.Input + ); + } + } + + private void ScrollToEnd() + { + if (0 < _logScrollView?.verticalScroller.highValue) + { + _logScrollView.verticalScroller.value = _logScrollView.verticalScroller.highValue; + } + } + + private void RefreshAutoCompleteHints() + { + bool shouldDisplay = + 0 < _lastCompletionBuffer.Count + && hintDisplayMode is HintDisplayMode.Always or HintDisplayMode.AutoCompleteOnly + && _autoCompleteContainer != null; + + if (!shouldDisplay) + { + if (0 < _autoCompleteContainer?.childCount) + { + _autoCompleteContainer.Clear(); + } + + _previousLastCompletionIndex = null; + return; + } + + int bufferLength = _lastCompletionBuffer.Count; + if (_lastKnownHintsClickable != makeHintsClickable) + { + _autoCompleteContainer.Clear(); + _lastKnownHintsClickable = makeHintsClickable; + } + + int currentChildCount = _autoCompleteContainer.childCount; + + bool dirty = _lastCompletionIndex != _previousLastCompletionIndex; + bool contentsChanged = currentChildCount != bufferLength; + if (contentsChanged) + { + dirty = true; + if (currentChildCount < bufferLength) + { + for (int i = currentChildCount; i < bufferLength; ++i) + { + string hint = _lastCompletionBuffer[i]; + VisualElement hintElement; + + if (makeHintsClickable) + { + int currentIndex = i; + string currentHint = hint; + Button hintButton = new(() => + { + _input.CommandText = currentHint; + _lastCompletionIndex = currentIndex; + _needsFocus = true; + }) + { + text = hint, + }; + hintElement = hintButton; + } + else + { + Label hintText = new(hint); + hintElement = hintText; + } + + hintElement.name = $"SuggestionText{i}"; + _autoCompleteContainer.Add(hintElement); + + bool isSelected = i == _lastCompletionIndex; + hintElement.AddToClassList("terminal-button"); + hintElement.EnableInClassList("autocomplete-item-selected", isSelected); + hintElement.EnableInClassList("autocomplete-item", !isSelected); + } + } + else if (bufferLength < currentChildCount) + { + for (int i = currentChildCount - 1; bufferLength <= i; --i) + { + _autoCompleteContainer.RemoveAt(i); + } + } + } + + bool shouldUpdateCompletionIndex = false; + try + { + shouldUpdateCompletionIndex = _autoCompleteContainer.childCount == bufferLength; + if (shouldUpdateCompletionIndex) + { + UpdateAutoCompleteView(); + } + + if (dirty) + { + for (int i = 0; i < _autoCompleteContainer.childCount && i < bufferLength; ++i) + { + VisualElement hintElement = _autoCompleteContainer[i]; + switch (hintElement) + { + case Button button: + button.text = _lastCompletionBuffer[i]; + break; + case Label label: + label.text = _lastCompletionBuffer[i]; + break; + case TextField textField: + textField.value = _lastCompletionBuffer[i]; + break; + } + + bool isSelected = i == _lastCompletionIndex; + + hintElement.EnableInClassList("autocomplete-item-selected", isSelected); + hintElement.EnableInClassList("autocomplete-item", !isSelected); + } + } + } + finally + { + if (shouldUpdateCompletionIndex) + { + _previousLastCompletionIndex = _lastCompletionIndex; + } + } + } + + private void UpdateAutoCompleteView() + { + if (_lastCompletionIndex == null) + { + return; + } + + if (_autoCompleteContainer?.contentContainer == null) + { + return; + } + + int childCount = _autoCompleteContainer.childCount; + if (childCount == 0) + { + return; + } + + if (childCount <= _lastCompletionIndex) + { + _lastCompletionIndex = + (_lastCompletionIndex % childCount + childCount) % childCount; + } + + if (_previousLastCompletionIndex == _lastCompletionIndex) + { + return; + } + + VisualElement current = _autoCompleteContainer[_lastCompletionIndex.Value]; + float viewportWidth = _autoCompleteContainer.contentViewport.resolvedStyle.width; + + // Use layout properties relative to the content container + float targetElementLeft = current.layout.x; + float targetElementWidth = current.layout.width; + float targetElementRight = targetElementLeft + targetElementWidth; + + const float epsilon = 0.01f; + + bool isFullyVisible = + epsilon <= targetElementLeft && targetElementRight <= viewportWidth + epsilon; + + if (isFullyVisible) + { + return; + } + + bool isIncrementing; + if (_previousLastCompletionIndex == childCount - 1 && _lastCompletionIndex == 0) + { + isIncrementing = true; + } + else if (_previousLastCompletionIndex == 0 && _lastCompletionIndex == childCount - 1) + { + isIncrementing = false; + } + else + { + isIncrementing = _previousLastCompletionIndex < _lastCompletionIndex; + } + + _autoCompleteChildren.Clear(); + for (int i = 0; i < childCount; ++i) + { + _autoCompleteChildren.Add(_autoCompleteContainer[i]); + } + + int shiftAmount; + if (isIncrementing) + { + shiftAmount = -1 * _lastCompletionIndex.Value; + _lastCompletionIndex = 0; + } + else + { + shiftAmount = 0; + float accumulatedWidth = 0; + for (int i = 1; i <= childCount; ++i) + { + shiftAmount++; + int index = -i % childCount; + index = (index + childCount) % childCount; + VisualElement element = _autoCompleteChildren[index]; + accumulatedWidth += + element.resolvedStyle.width + + element.resolvedStyle.marginLeft + + element.resolvedStyle.marginRight + + element.resolvedStyle.borderLeftWidth + + element.resolvedStyle.borderRightWidth; + + if (accumulatedWidth <= viewportWidth) + { + continue; + } + + if (element != current) + { + --shiftAmount; + } + + break; + } + + _lastCompletionIndex = (shiftAmount - 1 + childCount) % childCount; + } + + _autoCompleteChildren.Shift(shiftAmount); + _lastCompletionBuffer.Shift(shiftAmount); + + _autoCompleteContainer.Clear(); + foreach (VisualElement element in _autoCompleteChildren) + { + _autoCompleteContainer.Add(element); + } + } + + private void RefreshStateButtons() + { + if (_stateButtonContainer == null) + { + return; + } + + _stateButtonContainer.style.top = _currentWindowHeight; + DisplayStyle displayStyle = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; + + for (int i = 0; i < _stateButtonContainer.childCount; ++i) + { + VisualElement child = _stateButtonContainer[i]; + child.style.display = displayStyle; + } + + if (!showGUIButtons) + { + return; + } + + Button firstButton; + Button secondButton; + if (_stateButtonContainer.childCount == 0) + { + firstButton = new Button(FirstClicked) { name = "StateButton1" }; + firstButton.AddToClassList("terminal-button"); + firstButton.style.display = displayStyle; + _stateButtonContainer.Add(firstButton); + + secondButton = new Button(SecondClicked) { name = "StateButton2" }; + secondButton.AddToClassList("terminal-button"); + secondButton.style.display = displayStyle; + _stateButtonContainer.Add(secondButton); + } + else + { + firstButton = _stateButtonContainer[0] as Button; + if (firstButton == null) + { + return; + } + secondButton = _stateButtonContainer[1] as Button; + if (secondButton == null) + { + return; + } + } + + _inputCaretLabel.text = _inputCaret; + + switch (_state) + { + case TerminalState.Closed: + if (!string.IsNullOrWhiteSpace(smallButtonText)) + { + firstButton.text = smallButtonText; + } + if (!string.IsNullOrWhiteSpace(fullButtonText)) + { + secondButton.text = fullButtonText; + } + break; + case TerminalState.OpenSmall: + if (!string.IsNullOrWhiteSpace(closeButtonText)) + { + firstButton.text = closeButtonText; + } + if (!string.IsNullOrWhiteSpace(fullButtonText)) + { + secondButton.text = fullButtonText; + } + break; + case TerminalState.OpenFull: + if (!string.IsNullOrWhiteSpace(closeButtonText)) + { + firstButton.text = closeButtonText; + } + if (!string.IsNullOrWhiteSpace(smallButtonText)) + { + secondButton.text = smallButtonText; + } + break; + default: + throw new InvalidEnumArgumentException( + nameof(_state), + (int)_state, + typeof(TerminalState) + ); + } + return; + + void FirstClicked() + { + switch (_state) + { + case TerminalState.Closed: + if (!string.IsNullOrWhiteSpace(smallButtonText)) + { + SetState(TerminalState.OpenSmall); + } + break; + case TerminalState.OpenSmall: + case TerminalState.OpenFull: + if (!string.IsNullOrWhiteSpace(closeButtonText)) + { + SetState(TerminalState.Closed); + } + break; + default: + throw new InvalidEnumArgumentException( + nameof(_state), + (int)_state, + typeof(TerminalState) + ); + } + } + + void SecondClicked() + { + switch (_state) + { + case TerminalState.Closed: + case TerminalState.OpenSmall: + if (!string.IsNullOrWhiteSpace(fullButtonText)) + { + SetState(TerminalState.OpenFull); + } + break; + case TerminalState.OpenFull: + if (!string.IsNullOrWhiteSpace(smallButtonText)) + { + SetState(TerminalState.OpenSmall); + } + break; + default: + throw new InvalidEnumArgumentException( + nameof(_state), + (int)_state, + typeof(TerminalState) + ); + } + } + } + + public Font SetRandomFont(bool persist = false) + { + if (_fontPack == null) + { + return _runtimeFont; + } + + List loadedFonts = _fontPack._fonts; + if (loadedFonts is not { Count: > 0 }) + { + return _runtimeFont; + } + + int currentFontIndex = loadedFonts.IndexOf(_runtimeFont); + + int newFontIndex; + do + { + newFontIndex = ThreadLocalRandom.Instance.Next(loadedFonts.Count); + } while (newFontIndex == currentFontIndex && loadedFonts.Count != 1); + + Font newFont = loadedFonts[newFontIndex]; + SetFont(newFont, persist); + return newFont; + } + + public void SetFont(Font font, bool persist = false) + { + SetRuntimeFont(font); + if (!persist && CurrentFont == font) + { + return; + } + + if (font == null) + { + Debug.LogError("Cannot set null font.", this); + return; + } + + if (_uiDocument == null) + { + Debug.LogError("Cannot set font, no UIDocument assigned."); + return; + } + + Font currentFont = _persistedFont; + _runtimeFont = font; + if (currentFont != font) + { + Debug.Log( + currentFont == null + ? $"Setting font to {font.name}." + : $"Changing font from {currentFont.name} to {font.name}.", + this + ); + } + + if (persist) + { + _persistedFont = font; + } + + return; + + void SetRuntimeFont(Font toSet) + { + if (toSet == null) + { + return; + } + + if (!Application.isPlaying) + { + return; + } + + if (_uiDocument == null) + { + return; + } + + VisualElement root = _uiDocument.rootVisualElement; + if (root == null) + { + return; + } + + root.style.unityFontDefinition = new StyleFontDefinition(toSet); + } + } + + public string SetRandomTheme(bool persist = false) + { + if (_themePack == null) + { + return _runtimeTheme; + } + + List loadedThemes = _themePack._themeNames; + if (loadedThemes is not { Count: > 0 }) + { + return _runtimeTheme; + } + + int currentThemeIndex = loadedThemes.IndexOf(_runtimeTheme); + + int newThemeIndex; + do + { + newThemeIndex = ThreadLocalRandom.Instance.Next(loadedThemes.Count); + } while (newThemeIndex == currentThemeIndex && loadedThemes.Count != 1); + + string newTheme = loadedThemes[newThemeIndex]; + SetTheme(newTheme, persist); + return newTheme; + } + + public void SetTheme(string theme, bool persist = false) + { + string friendlyThemeName = ThemeNameHelper.GetFriendlyThemeName(theme); + SetRuntimeTheme(); + if ( + !persist + && string.Equals( + friendlyThemeName, + CurrentFriendlyTheme, + StringComparison.OrdinalIgnoreCase + ) + ) + { + return; + } + + if (!IsValidTheme(out string validatedTheme)) + { + return; + } + + string currentTheme = ThemeNameHelper.GetFriendlyThemeName(CurrentTheme); + _runtimeTheme = validatedTheme; + if (!string.Equals(currentTheme, friendlyThemeName, StringComparison.OrdinalIgnoreCase)) + { + Debug.Log($"Changing theme from {currentTheme} to {friendlyThemeName}.", this); + } + + if (persist) + { + _persistedTheme = validatedTheme; + } + + return; + + bool IsValidTheme(out string validTheme) + { + if (string.IsNullOrWhiteSpace(theme) || _themePack == null) + { + validTheme = default; + return false; + } + + List themeNames = _themePack._themeNames; + if (themeNames.Contains(theme, StringComparer.OrdinalIgnoreCase)) + { + validTheme = theme; + return true; + } + + foreach (string themeName in ThemeNameHelper.GetPossibleThemeNames(theme)) + { + if (themeNames.Contains(themeName, StringComparer.OrdinalIgnoreCase)) + { + validTheme = themeName; + return true; + } + } + + validTheme = default; + return false; + } + + void SetRuntimeTheme() + { + if (!Application.isPlaying) + { + return; + } + + if (!IsValidTheme(out validatedTheme)) + { + return; + } + + if (_uiDocument == null) + { + return; + } + + VisualElement terminalRoot = _uiDocument.rootVisualElement?.Q( + TerminalRootName + ); + if (terminalRoot == null) + { + return; + } + + string[] loadedThemes = terminalRoot + .GetClasses() + .Where(ThemeNameHelper.IsThemeName) + .ToArray(); + + foreach (string loadedTheme in loadedThemes) + { + terminalRoot.RemoveFromClassList(loadedTheme); + } + + terminalRoot.AddToClassList(validatedTheme); + } + } + + public void HandlePrevious() + { + if (_state == TerminalState.Closed) + { + return; + } + + _input.CommandText = + Terminal.History?.Previous(skipSameCommandsInHistory) ?? string.Empty; + ResetAutoComplete(); + _needsFocus = true; + } + + public void HandleNext() + { + if (_state == TerminalState.Closed) + { + return; + } + + _input.CommandText = Terminal.History?.Next(skipSameCommandsInHistory) ?? string.Empty; + ResetAutoComplete(); + _needsFocus = true; + } + + public void Close() + { + SetState(TerminalState.Closed); + } + + public void ToggleSmall() + { + ToggleState(TerminalState.OpenSmall); + } + + public void ToggleFull() + { + ToggleState(TerminalState.OpenFull); + } + + public void EnterCommand() + { + if (_state == TerminalState.Closed) + { + return; + } + + string commandText = _input.CommandText ?? string.Empty; + if (commandText.NeedsTrim()) + { + commandText = commandText.Trim(); + } + + _input.CommandText = commandText; + try + { + if (string.IsNullOrWhiteSpace(commandText)) + { + return; + } + + Terminal.Log(TerminalLogType.Input, commandText); + Terminal.Shell?.RunCommand(commandText); + while (Terminal.Shell?.TryConsumeErrorMessage(out string error) == true) + { + Terminal.Log(TerminalLogType.Error, $"Error: {error}"); + } + + _input.CommandText = string.Empty; + _needsFocus = true; + _needsScrollToEnd = true; + } + finally + { + ResetAutoComplete(); + } + } + + public void CompleteCommand(bool searchForward = true) + { + if (_state == TerminalState.Closed) + { + return; + } + + try + { + _lastKnownCommandText = _input.CommandText ?? string.Empty; + _lastCompletionBufferTempCache.Clear(); + int caret = + _commandInput != null + ? _commandInput.cursorIndex + : (_lastKnownCommandText?.Length ?? 0); + Terminal.AutoComplete?.Complete( + _lastKnownCommandText, + caret, + _lastCompletionBufferTempCache + ); + bool equivalentBuffers = true; + try + { + int completionLength = _lastCompletionBufferTempCache.Count; + equivalentBuffers = + _lastCompletionBuffer.Count == _lastCompletionBufferTempCache.Count; + if (equivalentBuffers) + { + _lastCompletionBufferTempSet.Clear(); + foreach (string item in _lastCompletionBuffer) + { + _lastCompletionBufferTempSet.Add(item); + } + + foreach (string newCompletionItem in _lastCompletionBufferTempCache) + { + if (!_lastCompletionBufferTempSet.Contains(newCompletionItem)) + { + equivalentBuffers = false; + break; + } + } + } + if (equivalentBuffers) + { + if (0 < completionLength) + { + if (_lastCompletionIndex == null) + { + _lastCompletionIndex = 0; + } + else if (searchForward) + { + _lastCompletionIndex = + (_lastCompletionIndex + 1) % completionLength; + } + else + { + _lastCompletionIndex = + (_lastCompletionIndex - 1 + completionLength) + % completionLength; + } + + _input.CommandText = _lastCompletionBuffer[_lastCompletionIndex.Value]; + } + else + { + _lastCompletionIndex = null; + } + } + else + { + if (0 < completionLength) + { + _lastCompletionIndex = 0; + _input.CommandText = _lastCompletionBufferTempCache[0]; + } + else + { + _lastCompletionIndex = null; + } + } + } + finally + { + if (!equivalentBuffers) + { + _lastCompletionBuffer.Clear(); + foreach (string item in _lastCompletionBufferTempCache) + { + _lastCompletionBuffer.Add(item); + } + _previousLastCompletionIndex = null; + } + + _previousLastCompletionIndex ??= _lastCompletionIndex; + } + } + finally + { + _needsFocus = true; + } + } + + private void StartHeightAnimation() + { + if (Mathf.Approximately(_currentWindowHeight, _targetWindowHeight)) + { + _isAnimating = false; + return; + } + + _initialWindowHeight = _currentWindowHeight; + _animationTimer = 0f; + _isAnimating = true; + } + + private void HandleHeightAnimation() + { + if (!_isAnimating) + { + return; + } + + _animationTimer += Time.unscaledDeltaTime; + + AnimationCurve selectedCurve; + float animationDuration; + bool isExpanding = _targetWindowHeight > _initialWindowHeight; + + if (isExpanding) + { + selectedCurve = easeOutCurve; + animationDuration = easeOutTime; + } + else + { + selectedCurve = easeInCurve; + animationDuration = easeInTime; + } + + if (animationDuration <= 0f) + { + _currentWindowHeight = _targetWindowHeight; + _isAnimating = false; + return; + } + + float normalizedTime = Mathf.Clamp01(_animationTimer / animationDuration); + + float curveValue = selectedCurve.Evaluate(normalizedTime); + + _currentWindowHeight = Mathf.LerpUnclamped( + _initialWindowHeight, + _targetWindowHeight, + curveValue + ); + + if (isExpanding) + { + _currentWindowHeight = Mathf.Clamp( + _currentWindowHeight, + _initialWindowHeight, + _targetWindowHeight + ); + } + else + { + _currentWindowHeight = Mathf.Clamp( + _currentWindowHeight, + _targetWindowHeight, + _initialWindowHeight + ); + } + + if ( + Mathf.Approximately(_currentWindowHeight, _targetWindowHeight) + || animationDuration <= _animationTimer + ) + { + _currentWindowHeight = _targetWindowHeight; + _isAnimating = false; + } + } + + private static void HandleUnityLog(string message, string stackTrace, LogType type) + { + Terminal.Buffer?.EnqueueUnityLog(message, stackTrace, (TerminalLogType)type); + } + } +} diff --git a/Runtime/DataStructures/CyclicBuffer.cs b/Runtime/DataStructures/CyclicBuffer.cs index 14ccc25..7e10291 100644 --- a/Runtime/DataStructures/CyclicBuffer.cs +++ b/Runtime/DataStructures/CyclicBuffer.cs @@ -1,190 +1,190 @@ -namespace WallstopStudios.DxCommandTerminal.DataStructures -{ - using System; - using System.Collections; - using System.Collections.Generic; - using Extensions; - - [Serializable] - internal sealed class CyclicBuffer : IReadOnlyList - { - public struct CyclicBufferEnumerator : IEnumerator - { - private readonly CyclicBuffer _buffer; - - private int _index; - private T _current; - - internal CyclicBufferEnumerator(CyclicBuffer buffer) - { - _buffer = buffer; - _index = -1; - _current = default; - } - - public bool MoveNext() - { - if (++_index < _buffer.Count) - { - _current = _buffer._buffer[_buffer.AdjustedIndexFor(_index)]; - return true; - } - - _current = default; - return false; - } - - public T Current => _current; - - object IEnumerator.Current => Current; - - public void Reset() - { - _index = -1; - _current = default; - } - - public void Dispose() { } - } - - public int Capacity { get; private set; } - public int Count { get; private set; } - - private readonly List _buffer; - private int _position; - - public T this[int index] - { - get - { - BoundsCheck(index); - return _buffer[AdjustedIndexFor(index)]; - } - set - { - BoundsCheck(index); - _buffer[AdjustedIndexFor(index)] = value; - } - } - - public CyclicBuffer(int capacity, IEnumerable initialContents = null) - { - if (capacity < 0) - { - throw new ArgumentException(nameof(capacity)); - } - - Capacity = capacity; - _position = 0; - Count = 0; - _buffer = new List(); - if (initialContents != null) - { - foreach (T item in initialContents) - { - Add(item); - } - } - } - - public CyclicBufferEnumerator GetEnumerator() - { - return new CyclicBufferEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public void Add(T item) - { - if (Capacity == 0) - { - return; - } - - if (_position < _buffer.Count) - { - _buffer[_position] = item; - } - else - { - _buffer.Add(item); - } - - _position = (_position + 1) % Capacity; - if (Count < Capacity) - { - ++Count; - } - } - - public void Clear() - { - /* Simply reset state */ - Count = 0; - _position = 0; - _buffer.Clear(); - } - - public void Resize(int newCapacity) - { - if (newCapacity < 0) - { - throw new ArgumentException(nameof(newCapacity)); - } - - int oldCapacity = Capacity; - Capacity = newCapacity; - _buffer.Shift(-_position); - if (newCapacity < _buffer.Count) - { - _buffer.RemoveRange(newCapacity, _buffer.Count - newCapacity); - } - - _position = - newCapacity < oldCapacity && newCapacity <= _buffer.Count ? 0 : _buffer.Count; - Count = Math.Min(newCapacity, Count); - } - - public bool Contains(T item) - { - return _buffer.Contains(item); - } - - private int AdjustedIndexFor(int index) - { - long longCapacity = Capacity; - if (longCapacity == 0L) - { - return 0; - } - unchecked - { - int adjustedIndex = (int)( - (_position - 1L + longCapacity - (_buffer.Count - 1 - index)) % longCapacity - ); - return adjustedIndex; - } - } - - private void BoundsCheck(int index) - { - if (!InBounds(index)) - { - throw new IndexOutOfRangeException($"{index} is outside of bounds [0, {Count})"); - } - } - - private bool InBounds(int index) - { - return 0 <= index && index < Count; - } - } -} +namespace WallstopStudios.DxCommandTerminal.DataStructures +{ + using System; + using System.Collections; + using System.Collections.Generic; + using Extensions; + + [Serializable] + internal sealed class CyclicBuffer : IReadOnlyList + { + public struct CyclicBufferEnumerator : IEnumerator + { + private readonly CyclicBuffer _buffer; + + private int _index; + private T _current; + + internal CyclicBufferEnumerator(CyclicBuffer buffer) + { + _buffer = buffer; + _index = -1; + _current = default; + } + + public bool MoveNext() + { + if (++_index < _buffer.Count) + { + _current = _buffer._buffer[_buffer.AdjustedIndexFor(_index)]; + return true; + } + + _current = default; + return false; + } + + public T Current => _current; + + object IEnumerator.Current => Current; + + public void Reset() + { + _index = -1; + _current = default; + } + + public void Dispose() { } + } + + public int Capacity { get; private set; } + public int Count { get; private set; } + + private readonly List _buffer; + private int _position; + + public T this[int index] + { + get + { + BoundsCheck(index); + return _buffer[AdjustedIndexFor(index)]; + } + set + { + BoundsCheck(index); + _buffer[AdjustedIndexFor(index)] = value; + } + } + + public CyclicBuffer(int capacity, IEnumerable initialContents = null) + { + if (capacity < 0) + { + throw new ArgumentException(nameof(capacity)); + } + + Capacity = capacity; + _position = 0; + Count = 0; + _buffer = new List(); + if (initialContents != null) + { + foreach (T item in initialContents) + { + Add(item); + } + } + } + + public CyclicBufferEnumerator GetEnumerator() + { + return new CyclicBufferEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(T item) + { + if (Capacity == 0) + { + return; + } + + if (_position < _buffer.Count) + { + _buffer[_position] = item; + } + else + { + _buffer.Add(item); + } + + _position = (_position + 1) % Capacity; + if (Count < Capacity) + { + ++Count; + } + } + + public void Clear() + { + /* Simply reset state */ + Count = 0; + _position = 0; + _buffer.Clear(); + } + + public void Resize(int newCapacity) + { + if (newCapacity < 0) + { + throw new ArgumentException(nameof(newCapacity)); + } + + int oldCapacity = Capacity; + Capacity = newCapacity; + _buffer.Shift(-_position); + if (newCapacity < _buffer.Count) + { + _buffer.RemoveRange(newCapacity, _buffer.Count - newCapacity); + } + + _position = + newCapacity < oldCapacity && newCapacity <= _buffer.Count ? 0 : _buffer.Count; + Count = Math.Min(newCapacity, Count); + } + + public bool Contains(T item) + { + return _buffer.Contains(item); + } + + private int AdjustedIndexFor(int index) + { + long longCapacity = Capacity; + if (longCapacity == 0L) + { + return 0; + } + unchecked + { + int adjustedIndex = (int)( + (_position - 1L + longCapacity - (_buffer.Count - 1 - index)) % longCapacity + ); + return adjustedIndex; + } + } + + private void BoundsCheck(int index) + { + if (!InBounds(index)) + { + throw new IndexOutOfRangeException($"{index} is outside of bounds [0, {Count})"); + } + } + + private bool InBounds(int index) + { + return 0 <= index && index < Count; + } + } +} diff --git a/Runtime/Extensions/IListExtensions.cs b/Runtime/Extensions/IListExtensions.cs index 21d4f78..7f3a878 100644 --- a/Runtime/Extensions/IListExtensions.cs +++ b/Runtime/Extensions/IListExtensions.cs @@ -1,96 +1,96 @@ -namespace WallstopStudios.DxCommandTerminal.Extensions -{ - using System; - using System.Collections.Generic; - using Object = UnityEngine.Object; - - internal sealed class UnityObjectNameComparer : IComparer - { - public static readonly UnityObjectNameComparer Instance = new(); - - private UnityObjectNameComparer() { } - - public int Compare(Object x, Object y) - { - if (x == y) - { - return 0; - } - - if (y == null) - { - return 1; - } - - if (x == null) - { - return -1; - } - - return string.Compare(x.name, y.name, StringComparison.OrdinalIgnoreCase); - } - } - - internal static class IListExtensions - { - internal static void Shift(this IList list, int amount) - { - int count = list.Count; - if (count <= 1) - { - return; - } - - amount %= count; - amount += count; - amount %= count; - - if (amount == 0) - { - return; - } - - Reverse(list, 0, count - 1); - Reverse(list, 0, amount - 1); - Reverse(list, amount, count - 1); - } - - internal static void Reverse(this IList list, int start, int end) - { - while (start < end) - { - (list[start], list[end]) = (list[end], list[start]); - start++; - end--; - } - } - - internal static void SortByName(this List list) - where T : Object - { - list.Sort(UnityObjectNameComparer.Instance); - } - - internal static bool IsSorted(this IList list, IComparer comparer = null) - { - if (list.Count <= 1) - { - return true; - } - - comparer ??= Comparer.Default; - - T previous = list[0]; - for (int i = 1; i < list.Count; ++i) - { - T current = list[i]; - if (comparer.Compare(previous, current) > 0) - { - return false; - } - } - - return true; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Extensions +{ + using System; + using System.Collections.Generic; + using Object = UnityEngine.Object; + + internal sealed class UnityObjectNameComparer : IComparer + { + public static readonly UnityObjectNameComparer Instance = new(); + + private UnityObjectNameComparer() { } + + public int Compare(Object x, Object y) + { + if (x == y) + { + return 0; + } + + if (y == null) + { + return 1; + } + + if (x == null) + { + return -1; + } + + return string.Compare(x.name, y.name, StringComparison.OrdinalIgnoreCase); + } + } + + internal static class IListExtensions + { + internal static void Shift(this IList list, int amount) + { + int count = list.Count; + if (count <= 1) + { + return; + } + + amount %= count; + amount += count; + amount %= count; + + if (amount == 0) + { + return; + } + + Reverse(list, 0, count - 1); + Reverse(list, 0, amount - 1); + Reverse(list, amount, count - 1); + } + + internal static void Reverse(this IList list, int start, int end) + { + while (start < end) + { + (list[start], list[end]) = (list[end], list[start]); + start++; + end--; + } + } + + internal static void SortByName(this List list) + where T : Object + { + list.Sort(UnityObjectNameComparer.Instance); + } + + internal static bool IsSorted(this IList list, IComparer comparer = null) + { + if (list.Count <= 1) + { + return true; + } + + comparer ??= Comparer.Default; + + T previous = list[0]; + for (int i = 1; i < list.Count; ++i) + { + T current = list[i]; + if (comparer.Compare(previous, current) > 0) + { + return false; + } + } + + return true; + } + } +} diff --git a/Runtime/Extensions/SerializedPropertyExtensions.cs b/Runtime/Extensions/SerializedPropertyExtensions.cs index 3940f3e..d7f2f9f 100644 --- a/Runtime/Extensions/SerializedPropertyExtensions.cs +++ b/Runtime/Extensions/SerializedPropertyExtensions.cs @@ -1,264 +1,264 @@ -namespace WallstopStudios.DxCommandTerminal.Extensions -{ -#if UNITY_EDITOR - using System; - using System.Collections; - using System.Collections.Generic; - using System.Reflection; - using System.Reflection.Emit; - using UnityEditor; - using UnityEngine; - - internal static class FieldAccessorFactory - { - internal static Func CreateFieldGetter(FieldInfo field) - { -#if ENABLE_IL2CPP || UNITY_WEBGL - return field.GetValue; -#else - DynamicMethod dynamicMethod = new( - $"Get{field.Name}", - typeof(object), - new[] { typeof(object) }, - field.DeclaringType, - true - ); - - ILGenerator il = dynamicMethod.GetILGenerator(); - il.Emit(OpCodes.Ldarg_0); - il.Emit( - field.DeclaringType.IsValueType ? OpCodes.Unbox : OpCodes.Castclass, - field.DeclaringType - ); - il.Emit(OpCodes.Ldfld, field); - if (field.FieldType.IsValueType) - { - il.Emit(OpCodes.Box, field.FieldType); - } - - il.Emit(OpCodes.Ret); - return (Func)dynamicMethod.CreateDelegate(typeof(Func)); -#endif - } - } - - internal static class SerializedPropertyExtensions - { - private static readonly Dictionary< - Type, - Dictionary> - > FieldProducersByType = new(); - private static readonly PropertyInfo GradientProperty = - typeof(SerializedProperty).GetProperty( - "gradientValue", - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic - ); - - public static object GetValue(this SerializedProperty property) - { - switch (property.propertyType) - { - case SerializedPropertyType.Integer: - return property.intValue; - case SerializedPropertyType.Boolean: - return property.boolValue; - case SerializedPropertyType.Float: - return property.floatValue; - case SerializedPropertyType.String: - return property.stringValue; - case SerializedPropertyType.Color: - return property.colorValue; - case SerializedPropertyType.ObjectReference: - return property.objectReferenceValue; - case SerializedPropertyType.LayerMask: - return (LayerMask)property.intValue; - case SerializedPropertyType.Enum: - return property.enumValueIndex; - case SerializedPropertyType.Vector2: - return property.vector2Value; - case SerializedPropertyType.Vector3: - return property.vector3Value; - case SerializedPropertyType.Vector4: - return property.vector4Value; - case SerializedPropertyType.Rect: - return property.rectValue; - case SerializedPropertyType.Character: - return (char)property.intValue; - case SerializedPropertyType.AnimationCurve: - return property.animationCurveValue; - case SerializedPropertyType.Bounds: - return property.boundsValue; - case SerializedPropertyType.Gradient: - return GetGradientValue(property); - case SerializedPropertyType.Quaternion: - return property.quaternionValue; - case SerializedPropertyType.Vector2Int: - return property.vector2IntValue; - case SerializedPropertyType.Vector3Int: - return property.vector3IntValue; - case SerializedPropertyType.RectInt: - return property.rectIntValue; - case SerializedPropertyType.BoundsInt: - return property.boundsIntValue; - case SerializedPropertyType.ManagedReference: - return property.managedReferenceValue; - case SerializedPropertyType.Generic: - { - object obj = property.serializedObject.targetObject; - string[] propertyNames = property.propertyPath.Split('.'); - - foreach (string name in propertyNames) - { - if (obj == null) - { - return null; - } - - Type type = obj.GetType(); - if ( - !FieldProducersByType.TryGetValue( - type, - out Dictionary> fieldProducersByName - ) - ) - { - fieldProducersByName = new Dictionary>(); - FieldProducersByType[type] = fieldProducersByName; - } - - if ( - !fieldProducersByName.TryGetValue( - name, - out Func fieldProducer - ) - ) - { - FieldInfo field = type.GetField( - name, - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic - ); - fieldProducer = - field == null - ? _ => null - : FieldAccessorFactory.CreateFieldGetter(field); - fieldProducersByName[name] = fieldProducer; - } - - obj = fieldProducer(obj); - } - - return obj; - } - default: - return null; - } - } - - /// - /// Gets the instance object that contains the given SerializedProperty. - /// - /// The SerializedProperty. - /// Outputs the FieldInfo of the referenced field. - /// The instance object that owns the field. - internal static object GetEnclosingObject( - this SerializedProperty property, - out FieldInfo fieldInfo - ) - { - fieldInfo = null; - object obj = property.serializedObject.targetObject; - if (obj == null) - { - return null; - } - Type type = obj.GetType(); - string[] pathParts = property.propertyPath.Split('.'); - - // Traverse the path but stop at the second-to-last field - for (int i = 0; i < pathParts.Length - 1; ++i) - { - string fieldName = pathParts[i]; - - if (fieldName == "Array") - { - // Move to "data[i]" - ++i; - if (pathParts.Length <= i) - { - break; - } - - int index = int.Parse(pathParts[i].Replace("data[", "").Replace("]", "")); - obj = GetElementAtIndex(obj, index); - type = obj?.GetType(); - continue; - } - - fieldInfo = type?.GetField( - fieldName, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance - ); - if (fieldInfo == null) - { - return null; - } - - // Move deeper but stop before the last property in the path - if (i < pathParts.Length - 2) - { - obj = fieldInfo.GetValue(obj); - type = fieldInfo.FieldType; - } - } - - return obj; - } - - private static object GetElementAtIndex(object obj, int index) - { - if (index < 0) - { - return null; - } - - switch (obj) - { - case IList list when index < list.Count: - return list[index]; - case IEnumerable enumerable: - { - int count = 0; - IEnumerator enumerator = enumerable.GetEnumerator(); - try - { - while (enumerator.MoveNext()) - { - if (index == count++) - { - return enumerator.Current; - } - } - } - finally - { - if (enumerator is IDisposable disposable) - { - disposable.Dispose(); - } - } - - break; - } - } - - return null; - } - - // Special handling for Gradients, since Unity doesn't expose gradientValue in SerializedProperty - private static Gradient GetGradientValue(SerializedProperty property) - { - return GradientProperty?.GetValue(property) as Gradient; - } - } -#endif -} +namespace WallstopStudios.DxCommandTerminal.Extensions +{ +#if UNITY_EDITOR + using System; + using System.Collections; + using System.Collections.Generic; + using System.Reflection; + using System.Reflection.Emit; + using UnityEditor; + using UnityEngine; + + internal static class FieldAccessorFactory + { + internal static Func CreateFieldGetter(FieldInfo field) + { +#if ENABLE_IL2CPP || UNITY_WEBGL + return field.GetValue; +#else + DynamicMethod dynamicMethod = new( + $"Get{field.Name}", + typeof(object), + new[] { typeof(object) }, + field.DeclaringType, + true + ); + + ILGenerator il = dynamicMethod.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit( + field.DeclaringType.IsValueType ? OpCodes.Unbox : OpCodes.Castclass, + field.DeclaringType + ); + il.Emit(OpCodes.Ldfld, field); + if (field.FieldType.IsValueType) + { + il.Emit(OpCodes.Box, field.FieldType); + } + + il.Emit(OpCodes.Ret); + return (Func)dynamicMethod.CreateDelegate(typeof(Func)); +#endif + } + } + + internal static class SerializedPropertyExtensions + { + private static readonly Dictionary< + Type, + Dictionary> + > FieldProducersByType = new(); + private static readonly PropertyInfo GradientProperty = + typeof(SerializedProperty).GetProperty( + "gradientValue", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic + ); + + public static object GetValue(this SerializedProperty property) + { + switch (property.propertyType) + { + case SerializedPropertyType.Integer: + return property.intValue; + case SerializedPropertyType.Boolean: + return property.boolValue; + case SerializedPropertyType.Float: + return property.floatValue; + case SerializedPropertyType.String: + return property.stringValue; + case SerializedPropertyType.Color: + return property.colorValue; + case SerializedPropertyType.ObjectReference: + return property.objectReferenceValue; + case SerializedPropertyType.LayerMask: + return (LayerMask)property.intValue; + case SerializedPropertyType.Enum: + return property.enumValueIndex; + case SerializedPropertyType.Vector2: + return property.vector2Value; + case SerializedPropertyType.Vector3: + return property.vector3Value; + case SerializedPropertyType.Vector4: + return property.vector4Value; + case SerializedPropertyType.Rect: + return property.rectValue; + case SerializedPropertyType.Character: + return (char)property.intValue; + case SerializedPropertyType.AnimationCurve: + return property.animationCurveValue; + case SerializedPropertyType.Bounds: + return property.boundsValue; + case SerializedPropertyType.Gradient: + return GetGradientValue(property); + case SerializedPropertyType.Quaternion: + return property.quaternionValue; + case SerializedPropertyType.Vector2Int: + return property.vector2IntValue; + case SerializedPropertyType.Vector3Int: + return property.vector3IntValue; + case SerializedPropertyType.RectInt: + return property.rectIntValue; + case SerializedPropertyType.BoundsInt: + return property.boundsIntValue; + case SerializedPropertyType.ManagedReference: + return property.managedReferenceValue; + case SerializedPropertyType.Generic: + { + object obj = property.serializedObject.targetObject; + string[] propertyNames = property.propertyPath.Split('.'); + + foreach (string name in propertyNames) + { + if (obj == null) + { + return null; + } + + Type type = obj.GetType(); + if ( + !FieldProducersByType.TryGetValue( + type, + out Dictionary> fieldProducersByName + ) + ) + { + fieldProducersByName = new Dictionary>(); + FieldProducersByType[type] = fieldProducersByName; + } + + if ( + !fieldProducersByName.TryGetValue( + name, + out Func fieldProducer + ) + ) + { + FieldInfo field = type.GetField( + name, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic + ); + fieldProducer = + field == null + ? _ => null + : FieldAccessorFactory.CreateFieldGetter(field); + fieldProducersByName[name] = fieldProducer; + } + + obj = fieldProducer(obj); + } + + return obj; + } + default: + return null; + } + } + + /// + /// Gets the instance object that contains the given SerializedProperty. + /// + /// The SerializedProperty. + /// Outputs the FieldInfo of the referenced field. + /// The instance object that owns the field. + internal static object GetEnclosingObject( + this SerializedProperty property, + out FieldInfo fieldInfo + ) + { + fieldInfo = null; + object obj = property.serializedObject.targetObject; + if (obj == null) + { + return null; + } + Type type = obj.GetType(); + string[] pathParts = property.propertyPath.Split('.'); + + // Traverse the path but stop at the second-to-last field + for (int i = 0; i < pathParts.Length - 1; ++i) + { + string fieldName = pathParts[i]; + + if (fieldName == "Array") + { + // Move to "data[i]" + ++i; + if (pathParts.Length <= i) + { + break; + } + + int index = int.Parse(pathParts[i].Replace("data[", "").Replace("]", "")); + obj = GetElementAtIndex(obj, index); + type = obj?.GetType(); + continue; + } + + fieldInfo = type?.GetField( + fieldName, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance + ); + if (fieldInfo == null) + { + return null; + } + + // Move deeper but stop before the last property in the path + if (i < pathParts.Length - 2) + { + obj = fieldInfo.GetValue(obj); + type = fieldInfo.FieldType; + } + } + + return obj; + } + + private static object GetElementAtIndex(object obj, int index) + { + if (index < 0) + { + return null; + } + + switch (obj) + { + case IList list when index < list.Count: + return list[index]; + case IEnumerable enumerable: + { + int count = 0; + IEnumerator enumerator = enumerable.GetEnumerator(); + try + { + while (enumerator.MoveNext()) + { + if (index == count++) + { + return enumerator.Current; + } + } + } + finally + { + if (enumerator is IDisposable disposable) + { + disposable.Dispose(); + } + } + + break; + } + } + + return null; + } + + // Special handling for Gradients, since Unity doesn't expose gradientValue in SerializedProperty + private static Gradient GetGradientValue(SerializedProperty property) + { + return GradientProperty?.GetValue(property) as Gradient; + } + } +#endif +} diff --git a/Runtime/Extensions/StringExtensions.cs b/Runtime/Extensions/StringExtensions.cs index bf64e26..62c3367 100644 --- a/Runtime/Extensions/StringExtensions.cs +++ b/Runtime/Extensions/StringExtensions.cs @@ -1,28 +1,28 @@ -namespace WallstopStudios.DxCommandTerminal.Extensions -{ - internal static class StringExtensions - { - internal static bool NeedsLowerInvariantConversion(this string input) - { - foreach (char inputCharacter in input) - { - if (char.ToLowerInvariant(inputCharacter) != inputCharacter) - { - return true; - } - } - - return false; - } - - internal static bool NeedsTrim(this string input) - { - if (string.IsNullOrEmpty(input)) - { - return false; - } - - return char.IsWhiteSpace(input[0]) || char.IsWhiteSpace(input[^1]); - } - } -} +namespace WallstopStudios.DxCommandTerminal.Extensions +{ + internal static class StringExtensions + { + internal static bool NeedsLowerInvariantConversion(this string input) + { + foreach (char inputCharacter in input) + { + if (char.ToLowerInvariant(inputCharacter) != inputCharacter) + { + return true; + } + } + + return false; + } + + internal static bool NeedsTrim(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return false; + } + + return char.IsWhiteSpace(input[0]) || char.IsWhiteSpace(input[^1]); + } + } +} diff --git a/Runtime/Helper/DirectoryHelper.cs b/Runtime/Helper/DirectoryHelper.cs index 27b87fe..731ec00 100644 --- a/Runtime/Helper/DirectoryHelper.cs +++ b/Runtime/Helper/DirectoryHelper.cs @@ -1,128 +1,128 @@ -namespace WallstopStudios.DxCommandTerminal.Helper -{ - using System; - using System.IO; - using System.Runtime.CompilerServices; - using UnityEngine; - - internal static class DirectoryHelper - { - internal static string GetCallerScriptDirectory([CallerFilePath] string sourceFilePath = "") - { - return string.IsNullOrWhiteSpace(sourceFilePath) - ? string.Empty - : Path.GetDirectoryName(sourceFilePath); - } - - internal static string FindPackageRootPath(string startDirectory) - { - return FindRootPath( - startDirectory, - path => File.Exists(Path.Combine(path, "package.json")) - ); - } - - internal static string FindRootPath( - string startDirectory, - Func terminalCondition - ) - { - string currentPath = startDirectory; - while (!string.IsNullOrWhiteSpace(currentPath)) - { - try - { - if (terminalCondition(currentPath)) - { - DirectoryInfo directoryInfo = new(currentPath); - if (!directoryInfo.Exists) - { - return currentPath; - } - - return directoryInfo.FullName; - } - } - catch - { - return currentPath; - } - - try - { - string parentPath = Path.GetDirectoryName(currentPath); - if (string.Equals(parentPath, currentPath, StringComparison.OrdinalIgnoreCase)) - { - break; - } - - currentPath = parentPath; - } - catch - { - return currentPath; - } - } - - return string.Empty; - } - - internal static string FindAbsolutePathToDirectory(string directory) - { - string scriptDirectory = GetCallerScriptDirectory(); - if (string.IsNullOrEmpty(scriptDirectory)) - { - return string.Empty; - } - - string packageRootAbsolute = FindPackageRootPath(scriptDirectory); - if (string.IsNullOrEmpty(packageRootAbsolute)) - { - return string.Empty; - } - - string targetPathAbsolute = Path.Combine( - packageRootAbsolute, - directory.Replace('/', Path.DirectorySeparatorChar) - ); - - return AbsoluteToUnityRelativePath(targetPathAbsolute); - } - - internal static string AbsoluteToUnityRelativePath(string absolutePath) - { - if (string.IsNullOrWhiteSpace(absolutePath)) - { - return string.Empty; - } - - absolutePath = absolutePath.Replace('\\', '/'); - string projectRoot = Application.dataPath.Replace('\\', '/'); - - projectRoot = Path.GetDirectoryName(projectRoot)?.Replace('\\', '/'); - if (string.IsNullOrWhiteSpace(projectRoot)) - { - return string.Empty; - } - - if (absolutePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) - { - int startIndex = projectRoot.EndsWith("/", StringComparison.OrdinalIgnoreCase) - ? projectRoot.Length - : projectRoot.Length + 1; - string tail = - absolutePath.Length > startIndex ? absolutePath[startIndex..] : string.Empty; - if (string.IsNullOrEmpty(tail)) - { - return string.Empty; - } - // Ensure path starts with Assets/ - return tail.StartsWith("Assets", StringComparison.OrdinalIgnoreCase) - ? tail - : ($"Assets/{tail}"); - } - - return string.Empty; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Helper +{ + using System; + using System.IO; + using System.Runtime.CompilerServices; + using UnityEngine; + + internal static class DirectoryHelper + { + internal static string GetCallerScriptDirectory([CallerFilePath] string sourceFilePath = "") + { + return string.IsNullOrWhiteSpace(sourceFilePath) + ? string.Empty + : Path.GetDirectoryName(sourceFilePath); + } + + internal static string FindPackageRootPath(string startDirectory) + { + return FindRootPath( + startDirectory, + path => File.Exists(Path.Combine(path, "package.json")) + ); + } + + internal static string FindRootPath( + string startDirectory, + Func terminalCondition + ) + { + string currentPath = startDirectory; + while (!string.IsNullOrWhiteSpace(currentPath)) + { + try + { + if (terminalCondition(currentPath)) + { + DirectoryInfo directoryInfo = new(currentPath); + if (!directoryInfo.Exists) + { + return currentPath; + } + + return directoryInfo.FullName; + } + } + catch + { + return currentPath; + } + + try + { + string parentPath = Path.GetDirectoryName(currentPath); + if (string.Equals(parentPath, currentPath, StringComparison.OrdinalIgnoreCase)) + { + break; + } + + currentPath = parentPath; + } + catch + { + return currentPath; + } + } + + return string.Empty; + } + + internal static string FindAbsolutePathToDirectory(string directory) + { + string scriptDirectory = GetCallerScriptDirectory(); + if (string.IsNullOrEmpty(scriptDirectory)) + { + return string.Empty; + } + + string packageRootAbsolute = FindPackageRootPath(scriptDirectory); + if (string.IsNullOrEmpty(packageRootAbsolute)) + { + return string.Empty; + } + + string targetPathAbsolute = Path.Combine( + packageRootAbsolute, + directory.Replace('/', Path.DirectorySeparatorChar) + ); + + return AbsoluteToUnityRelativePath(targetPathAbsolute); + } + + internal static string AbsoluteToUnityRelativePath(string absolutePath) + { + if (string.IsNullOrWhiteSpace(absolutePath)) + { + return string.Empty; + } + + absolutePath = absolutePath.Replace('\\', '/'); + string projectRoot = Application.dataPath.Replace('\\', '/'); + + projectRoot = Path.GetDirectoryName(projectRoot)?.Replace('\\', '/'); + if (string.IsNullOrWhiteSpace(projectRoot)) + { + return string.Empty; + } + + if (absolutePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) + { + int startIndex = projectRoot.EndsWith("/", StringComparison.OrdinalIgnoreCase) + ? projectRoot.Length + : projectRoot.Length + 1; + string tail = + absolutePath.Length > startIndex ? absolutePath[startIndex..] : string.Empty; + if (string.IsNullOrEmpty(tail)) + { + return string.Empty; + } + // Ensure path starts with Assets/ + return tail.StartsWith("Assets", StringComparison.OrdinalIgnoreCase) + ? tail + : ($"Assets/{tail}"); + } + + return string.Empty; + } + } +} diff --git a/Runtime/Helper/ThreadLocalRandom.cs b/Runtime/Helper/ThreadLocalRandom.cs index 11f7c17..23af1ac 100644 --- a/Runtime/Helper/ThreadLocalRandom.cs +++ b/Runtime/Helper/ThreadLocalRandom.cs @@ -1,12 +1,12 @@ -namespace WallstopStudios.DxCommandTerminal.Helper -{ - using System; - using System.Threading; - - internal static class ThreadLocalRandom - { - internal static Random Instance => LocalInstance.Value; - - internal static readonly ThreadLocal LocalInstance = new(() => new Random()); - } -} +namespace WallstopStudios.DxCommandTerminal.Helper +{ + using System; + using System.Threading; + + internal static class ThreadLocalRandom + { + internal static Random Instance => LocalInstance.Value; + + internal static readonly ThreadLocal LocalInstance = new(() => new Random()); + } +} diff --git a/Styles/BaseStyles.uss b/Styles/BaseStyles.uss index 3a403f4..afee1f7 100644 --- a/Styles/BaseStyles.uss +++ b/Styles/BaseStyles.uss @@ -1,251 +1,251 @@ -.transparent-cursor { - --unity-cursor-color: transparent; -} - -.terminal-root { - flex-grow: 1; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.terminal-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - flex-direction: column; - flex-grow: 1; - flex-shrink: 1; - min-width: 0; -} - -.log-scroll-view { - flex-grow: 1; - flex-shrink: 1; - width: 100%; -} - -.log-scroll-view .unity-scroller--vertical { - width: 12px; - min-width: 0; -} - -.log-scroll-view .unity-scroller--vertical .unity-base-slider--vertical { - border-color: transparent; - width: 10px; - min-width: 0; - border-top-width: 5px; - border-bottom-width: 5px; -} - -.log-scroll-view .unity-base-slider { - margin: 0; -} - -.log-scroll-view .unity-scroller .unity-base-slider__tracker { - border-width: 0; - border-radius: 4px; -} - -.log-scroll-view .unity-scroller .unity-base-slider__dragger { - border-radius: 4px; - border-width: 0; - min-width: 0; - width: 10px; - left: 0; -} - -.log-scroll-view .unity-scroller .unity-scroller__low-button, -.log-scroll-view .unity-scroller .unity-scroller__high-button { - display: none; - border-width: 0; -} - -.log-scroll-view .unity-scroller .unity-base-slider__tracker { - background-color: transparent; - transition: background-color 0.1s ease; -} - -.log-scroll-view .unity-scroller .unity-base-slider__tracker.dragger-hovered, -.log-scroll-view .unity-scroller .unity-base-slider__tracker.dragger-active { - background-color: transparent; -} - -.autocomplete-popup { - background-color: transparent; - flex-direction: row; - flex-shrink: 0; - left: 2px; -} - -.autocomplete-popup #unity-low-button, -.autocomplete-popup #unity-high-button, -.autocomplete-popup #unity-slider { - display: none; -} - -.input-container { - flex-direction: row; - flex-shrink: 0; - padding: 0; - margin: 0; - height: 30px; - align-items: center; -} - -.state-button-container { - position: absolute; - flex-direction: row; - left: 2px; -} - -.terminal-output-label { - white-space: normal; - margin: 0; - padding: 0; -} - -.terminal-output-label #unity-text-input { - background-color: transparent; - margin: 0; - border-width: 0; - padding: 0; -} - -.terminal-button { - margin: 4px 2px; - padding: 0 4px; - border-radius: 4px; - border-width: 0; -} - -.terminal-input-caret { - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - flex-grow: 0; - flex-shrink: 0; - height: 22px; - align-items: center; - padding: 2px 0; - margin: 0; -} - -.terminal-input-field { - background-color: transparent; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - height: 22px; - flex-grow: 1; - flex-shrink: 1; - padding: 0; - margin: 0; -} - -.terminal-input-field > #unity-text-input { - background-color: var(--input-field-bg); - border-top-width: 0; - border-bottom-width: 0; - border-left-width: 0; - border-right-width: 0; - padding: 2px 0; - margin: 0; -} - -/* -- Theming -- */ - -.styled-cursor { - --unity-cursor-color: var(--input-text-color); -} - -.terminal-container, -.log-scroll-view .unity-scroller .unity-scroller__dragger { - background-color: var(--terminal-bg); -} - -.log-scroll-view .unity-scroller .unity-base-slider__dragger { - background-color: var(--scroll-color); - transition: background-color 0.1s ease; -} - -.log-scroll-view .unity-scroller .unity-base-slider__dragger:hover, -.log-scroll-view .unity-scroller .unity-base-slider__tracker:hover, -.log-scroll-view .unity-scroller .unity-base-slider__dragger:active, -.log-scroll-view .unity-scroller .unity-base-slider__tracker:active, -.log-scroll-view .unity-scroller .unity-base-slider__dragger.dragger-active { - background-color: var(--scroll-active-bg); -} - -.log-scroll-view .unity-scroller .unity-base-slider__dragger.tracker-hovered, -.log-scroll-view .unity-scroller .unity-base-slider__dragger.tracker-active { - background-color: var(--scroll-inverse-bg); -} - -.autocomplete-item { - background-color: var(--button-bg); - transition: background-color 0.1s ease; - color: var(--button-text); -} - -.autocomplete-item:hover { - background-color: var(--button-hover-bg); - color: var(--button-hover-text); -} - -.autocomplete-item-selected { - background-color: var(--button-selected-bg); - color: var(--button-selected-text); -} - -.autocomplete-item-selected:hover { - background-color: var(--button-hover-bg); - color: var(--button-hover-text); -} - -.terminal-button-run { - background-color: var(--button-bg); - color: var(--button-text); -} - -.terminal-input-caret { - color: var(--input-text-color); - background-color: var(--input-field-bg); -} - -.terminal-input-field > #unity-text-input, -.terminal-input-field > #unity-text-input > .unity-text-element { - color: var(--input-text-color); -} - -.state-button-container .terminal-button { - background-color: var(--button-bg); - color: var(--button-text); -} - -.state-button-container .terminal-button:hover { - background-color: var(--button-selected-bg); - color: var(--button-selected-text); -} - -.terminal-output-label--message { - color: var(--text-message); -} - -.terminal-output-label--warning { - color: var(--text-warning); -} - -.terminal-output-label--input { - color: var(--text-input-echo); -} - -.terminal-output-label--shell { - color: var(--text-shell); -} - -.terminal-output-label--error { - color: var(--text-error); +.transparent-cursor { + --unity-cursor-color: transparent; +} + +.terminal-root { + flex-grow: 1; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.terminal-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + min-width: 0; +} + +.log-scroll-view { + flex-grow: 1; + flex-shrink: 1; + width: 100%; +} + +.log-scroll-view .unity-scroller--vertical { + width: 12px; + min-width: 0; +} + +.log-scroll-view .unity-scroller--vertical .unity-base-slider--vertical { + border-color: transparent; + width: 10px; + min-width: 0; + border-top-width: 5px; + border-bottom-width: 5px; +} + +.log-scroll-view .unity-base-slider { + margin: 0; +} + +.log-scroll-view .unity-scroller .unity-base-slider__tracker { + border-width: 0; + border-radius: 4px; +} + +.log-scroll-view .unity-scroller .unity-base-slider__dragger { + border-radius: 4px; + border-width: 0; + min-width: 0; + width: 10px; + left: 0; +} + +.log-scroll-view .unity-scroller .unity-scroller__low-button, +.log-scroll-view .unity-scroller .unity-scroller__high-button { + display: none; + border-width: 0; +} + +.log-scroll-view .unity-scroller .unity-base-slider__tracker { + background-color: transparent; + transition: background-color 0.1s ease; +} + +.log-scroll-view .unity-scroller .unity-base-slider__tracker.dragger-hovered, +.log-scroll-view .unity-scroller .unity-base-slider__tracker.dragger-active { + background-color: transparent; +} + +.autocomplete-popup { + background-color: transparent; + flex-direction: row; + flex-shrink: 0; + left: 2px; +} + +.autocomplete-popup #unity-low-button, +.autocomplete-popup #unity-high-button, +.autocomplete-popup #unity-slider { + display: none; +} + +.input-container { + flex-direction: row; + flex-shrink: 0; + padding: 0; + margin: 0; + height: 30px; + align-items: center; +} + +.state-button-container { + position: absolute; + flex-direction: row; + left: 2px; +} + +.terminal-output-label { + white-space: normal; + margin: 0; + padding: 0; +} + +.terminal-output-label #unity-text-input { + background-color: transparent; + margin: 0; + border-width: 0; + padding: 0; +} + +.terminal-button { + margin: 4px 2px; + padding: 0 4px; + border-radius: 4px; + border-width: 0; +} + +.terminal-input-caret { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + flex-grow: 0; + flex-shrink: 0; + height: 22px; + align-items: center; + padding: 2px 0; + margin: 0; +} + +.terminal-input-field { + background-color: transparent; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 22px; + flex-grow: 1; + flex-shrink: 1; + padding: 0; + margin: 0; +} + +.terminal-input-field > #unity-text-input { + background-color: var(--input-field-bg); + border-top-width: 0; + border-bottom-width: 0; + border-left-width: 0; + border-right-width: 0; + padding: 2px 0; + margin: 0; +} + +/* -- Theming -- */ + +.styled-cursor { + --unity-cursor-color: var(--input-text-color); +} + +.terminal-container, +.log-scroll-view .unity-scroller .unity-scroller__dragger { + background-color: var(--terminal-bg); +} + +.log-scroll-view .unity-scroller .unity-base-slider__dragger { + background-color: var(--scroll-color); + transition: background-color 0.1s ease; +} + +.log-scroll-view .unity-scroller .unity-base-slider__dragger:hover, +.log-scroll-view .unity-scroller .unity-base-slider__tracker:hover, +.log-scroll-view .unity-scroller .unity-base-slider__dragger:active, +.log-scroll-view .unity-scroller .unity-base-slider__tracker:active, +.log-scroll-view .unity-scroller .unity-base-slider__dragger.dragger-active { + background-color: var(--scroll-active-bg); +} + +.log-scroll-view .unity-scroller .unity-base-slider__dragger.tracker-hovered, +.log-scroll-view .unity-scroller .unity-base-slider__dragger.tracker-active { + background-color: var(--scroll-inverse-bg); +} + +.autocomplete-item { + background-color: var(--button-bg); + transition: background-color 0.1s ease; + color: var(--button-text); +} + +.autocomplete-item:hover { + background-color: var(--button-hover-bg); + color: var(--button-hover-text); +} + +.autocomplete-item-selected { + background-color: var(--button-selected-bg); + color: var(--button-selected-text); +} + +.autocomplete-item-selected:hover { + background-color: var(--button-hover-bg); + color: var(--button-hover-text); +} + +.terminal-button-run { + background-color: var(--button-bg); + color: var(--button-text); +} + +.terminal-input-caret { + color: var(--input-text-color); + background-color: var(--input-field-bg); +} + +.terminal-input-field > #unity-text-input, +.terminal-input-field > #unity-text-input > .unity-text-element { + color: var(--input-text-color); +} + +.state-button-container .terminal-button { + background-color: var(--button-bg); + color: var(--button-text); +} + +.state-button-container .terminal-button:hover { + background-color: var(--button-selected-bg); + color: var(--button-selected-text); +} + +.terminal-output-label--message { + color: var(--text-message); +} + +.terminal-output-label--warning { + color: var(--text-warning); +} + +.terminal-output-label--input { + color: var(--text-input-echo); +} + +.terminal-output-label--shell { + color: var(--text-shell); +} + +.terminal-output-label--error { + color: var(--text-error); } \ No newline at end of file diff --git a/Styles/TerminalThemeSettings-Base.tss b/Styles/TerminalThemeSettings-Base.tss index f17cfd7..e677d89 100644 --- a/Styles/TerminalThemeSettings-Base.tss +++ b/Styles/TerminalThemeSettings-Base.tss @@ -1,4 +1,4 @@ -@import url("UnityDefaultRuntimeTheme.tss"); - -@import url("BaseStyles.uss"); - +@import url("UnityDefaultRuntimeTheme.tss"); + +@import url("BaseStyles.uss"); + diff --git a/Styles/Themes/AlienJungleTheme.uss b/Styles/Themes/AlienJungleTheme.uss index 7d157ca..ea92c13 100644 --- a/Styles/Themes/AlienJungleTheme.uss +++ b/Styles/Themes/AlienJungleTheme.uss @@ -1,26 +1,26 @@ -.alien-jungle-theme { - /* Backgrounds */ - --terminal-bg: rgba(50, 20, 70, 0.9); - --button-bg: rgba(75, 30, 95, 1); - --input-field-bg: rgba(40, 15, 60, 0.7); - --button-selected-bg: rgba(100, 255, 150, 0.85); - --button-hover-bg: rgba(95, 45, 115, 0.7); - --scroll-bg: rgba(85, 40, 105, 1); - --scroll-inverse-bg: rgba(75, 30, 95, 1); - --scroll-active-bg: rgba(115, 60, 135, 1); - - /* Text & Foreground */ - --button-text: rgba(180, 255, 200, 0.9); - --button-selected-text: rgba(30, 80, 40, 1); - --button-hover-text: rgba(210, 255, 220, 1); - --input-text-color: rgba(180, 255, 200, 1.0); - --text-message: rgba(180, 255, 200, 1); - --text-warning: rgba(255, 255, 100, 1); - --text-input-echo: rgba(150, 150, 255, 1); - --text-shell: rgba(160, 100, 180, 1); - --text-error: rgba(255, 80, 200, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 255, 150, 1); - --caret-color: rgba(255, 255, 100, 1.0); +.alien-jungle-theme { + /* Backgrounds */ + --terminal-bg: rgba(50, 20, 70, 0.9); + --button-bg: rgba(75, 30, 95, 1); + --input-field-bg: rgba(40, 15, 60, 0.7); + --button-selected-bg: rgba(100, 255, 150, 0.85); + --button-hover-bg: rgba(95, 45, 115, 0.7); + --scroll-bg: rgba(85, 40, 105, 1); + --scroll-inverse-bg: rgba(75, 30, 95, 1); + --scroll-active-bg: rgba(115, 60, 135, 1); + + /* Text & Foreground */ + --button-text: rgba(180, 255, 200, 0.9); + --button-selected-text: rgba(30, 80, 40, 1); + --button-hover-text: rgba(210, 255, 220, 1); + --input-text-color: rgba(180, 255, 200, 1.0); + --text-message: rgba(180, 255, 200, 1); + --text-warning: rgba(255, 255, 100, 1); + --text-input-echo: rgba(150, 150, 255, 1); + --text-shell: rgba(160, 100, 180, 1); + --text-error: rgba(255, 80, 200, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 255, 150, 1); + --caret-color: rgba(255, 255, 100, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/AmberGlowTheme.uss b/Styles/Themes/AmberGlowTheme.uss index dbcd4bc..0027977 100644 --- a/Styles/Themes/AmberGlowTheme.uss +++ b/Styles/Themes/AmberGlowTheme.uss @@ -1,26 +1,26 @@ -.amber-glow-theme { - /* Backgrounds */ - --terminal-bg: rgba(25, 15, 5, 0.8); - --button-bg: rgba(40, 25, 10, 1); - --input-field-bg: rgba(20, 10, 0, 0.7); - --button-selected-bg: rgba(255, 176, 0, 0.85); - --button-hover-bg: rgba(60, 40, 20, 0.7); - --scroll-bg: rgba(68, 48, 28, 1); - --scroll-inverse-bg: rgba(59, 39, 19, 1); - --scroll-active-bg: rgba(100, 80, 60, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 176, 0, 0.9); - --button-selected-text: rgba(25, 15, 5, 1); - --button-hover-text: rgba(255, 196, 40, 1); - --input-text-color: rgba(255, 176, 0, 1.0); - --text-message: rgba(255, 176, 0, 1); - --text-warning: rgba(255, 216, 0, 1); - --text-input-echo: rgba(255, 200, 100, 1); - --text-shell: rgba(200, 140, 0, 1); - --text-error: rgba(255, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 160, 0, 1); - --caret-color: rgba(255, 176, 0, 1.0); +.amber-glow-theme { + /* Backgrounds */ + --terminal-bg: rgba(25, 15, 5, 0.8); + --button-bg: rgba(40, 25, 10, 1); + --input-field-bg: rgba(20, 10, 0, 0.7); + --button-selected-bg: rgba(255, 176, 0, 0.85); + --button-hover-bg: rgba(60, 40, 20, 0.7); + --scroll-bg: rgba(68, 48, 28, 1); + --scroll-inverse-bg: rgba(59, 39, 19, 1); + --scroll-active-bg: rgba(100, 80, 60, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 176, 0, 0.9); + --button-selected-text: rgba(25, 15, 5, 1); + --button-hover-text: rgba(255, 196, 40, 1); + --input-text-color: rgba(255, 176, 0, 1.0); + --text-message: rgba(255, 176, 0, 1); + --text-warning: rgba(255, 216, 0, 1); + --text-input-echo: rgba(255, 200, 100, 1); + --text-shell: rgba(200, 140, 0, 1); + --text-error: rgba(255, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 160, 0, 1); + --caret-color: rgba(255, 176, 0, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/AndromedaTheme.uss b/Styles/Themes/AndromedaTheme.uss index 460ccfc..d29edd6 100644 --- a/Styles/Themes/AndromedaTheme.uss +++ b/Styles/Themes/AndromedaTheme.uss @@ -1,26 +1,26 @@ -.andromeda-theme { - /* Backgrounds */ - --terminal-bg: rgba(35, 41, 54, 0.9); - --button-bg: rgba(52, 62, 80, 1); - --input-field-bg: rgba(42, 49, 64, 0.8); - --button-selected-bg: rgba(126, 198, 230, 0.85); - --button-hover-bg: rgba(62, 74, 95, 1); - --scroll-bg: rgba(52, 62, 80, 1); - --scroll-inverse-bg: rgba(70, 85, 110, 1); - --scroll-active-bg: rgba(90, 105, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(224, 224, 224, 0.9); - --button-selected-text: rgba(35, 41, 54, 1); - --button-hover-text: rgba(224, 224, 224, 1); - --input-text-color: rgba(224, 224, 224, 1.0); - --text-message: rgba(224, 224, 224, 1); - --text-warning: rgba(255, 204, 0, 1); - --text-input-echo: rgba(126, 198, 230, 1); - --text-shell: rgba(140, 150, 170, 1); - --text-error: rgba(255, 100, 100, 1); - - /* Other UI Elements */ - --scroll-color: rgba(224, 224, 224, 1); - --caret-color: rgba(126, 198, 230, 1.0); +.andromeda-theme { + /* Backgrounds */ + --terminal-bg: rgba(35, 41, 54, 0.9); + --button-bg: rgba(52, 62, 80, 1); + --input-field-bg: rgba(42, 49, 64, 0.8); + --button-selected-bg: rgba(126, 198, 230, 0.85); + --button-hover-bg: rgba(62, 74, 95, 1); + --scroll-bg: rgba(52, 62, 80, 1); + --scroll-inverse-bg: rgba(70, 85, 110, 1); + --scroll-active-bg: rgba(90, 105, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(224, 224, 224, 0.9); + --button-selected-text: rgba(35, 41, 54, 1); + --button-hover-text: rgba(224, 224, 224, 1); + --input-text-color: rgba(224, 224, 224, 1.0); + --text-message: rgba(224, 224, 224, 1); + --text-warning: rgba(255, 204, 0, 1); + --text-input-echo: rgba(126, 198, 230, 1); + --text-shell: rgba(140, 150, 170, 1); + --text-error: rgba(255, 100, 100, 1); + + /* Other UI Elements */ + --scroll-color: rgba(224, 224, 224, 1); + --caret-color: rgba(126, 198, 230, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/ArcticNightTheme.uss b/Styles/Themes/ArcticNightTheme.uss index ae6185f..c3cb366 100644 --- a/Styles/Themes/ArcticNightTheme.uss +++ b/Styles/Themes/ArcticNightTheme.uss @@ -1,26 +1,26 @@ -.arctic-night-theme { - /* Backgrounds */ - --terminal-bg: rgba(15, 20, 30, 0.92); - --button-bg: rgba(25, 35, 50, 1); - --input-field-bg: rgba(10, 15, 25, 0.7); - --button-selected-bg: rgba(180, 220, 255, 0.85); - --button-hover-bg: rgba(35, 50, 70, 0.7); - --scroll-bg: rgba(30, 40, 55, 1); - --scroll-inverse-bg: rgba(25, 35, 50, 1); - --scroll-active-bg: rgba(50, 65, 85, 1); - - /* Text & Foreground */ - --button-text: rgba(200, 230, 255, 0.9); - --button-selected-text: rgba(15, 20, 30, 1); - --button-hover-text: rgba(225, 245, 255, 1); - --input-text-color: rgba(200, 230, 255, 1.0); - --text-message: rgba(200, 230, 255, 1); - --text-warning: rgba(150, 230, 210, 1); - --text-input-echo: rgba(160, 190, 220, 1); - --text-shell: rgba(130, 150, 170, 1); - --text-error: rgba(220, 140, 180, 1); - - /* Other UI Elements */ - --scroll-color: rgba(180, 220, 255, 1); - --caret-color: rgba(200, 230, 255, 1.0); +.arctic-night-theme { + /* Backgrounds */ + --terminal-bg: rgba(15, 20, 30, 0.92); + --button-bg: rgba(25, 35, 50, 1); + --input-field-bg: rgba(10, 15, 25, 0.7); + --button-selected-bg: rgba(180, 220, 255, 0.85); + --button-hover-bg: rgba(35, 50, 70, 0.7); + --scroll-bg: rgba(30, 40, 55, 1); + --scroll-inverse-bg: rgba(25, 35, 50, 1); + --scroll-active-bg: rgba(50, 65, 85, 1); + + /* Text & Foreground */ + --button-text: rgba(200, 230, 255, 0.9); + --button-selected-text: rgba(15, 20, 30, 1); + --button-hover-text: rgba(225, 245, 255, 1); + --input-text-color: rgba(200, 230, 255, 1.0); + --text-message: rgba(200, 230, 255, 1); + --text-warning: rgba(150, 230, 210, 1); + --text-input-echo: rgba(160, 190, 220, 1); + --text-shell: rgba(130, 150, 170, 1); + --text-error: rgba(220, 140, 180, 1); + + /* Other UI Elements */ + --scroll-color: rgba(180, 220, 255, 1); + --caret-color: rgba(200, 230, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/ArcticWhiteTheme.uss b/Styles/Themes/ArcticWhiteTheme.uss index cc39182..8297b0e 100644 --- a/Styles/Themes/ArcticWhiteTheme.uss +++ b/Styles/Themes/ArcticWhiteTheme.uss @@ -1,26 +1,26 @@ -.arctic-white-theme { - /* Backgrounds */ - --terminal-bg: rgba(235, 245, 255, 0.7); - --button-bg: rgba(210, 225, 240, 1); - --input-field-bg: rgba(250, 252, 255, 0.7); - --button-selected-bg: rgba(60, 120, 180, 0.85); - --button-hover-bg: rgba(180, 200, 220, 0.7); - --scroll-bg: rgba(198, 208, 218, 1); - --scroll-inverse-bg: rgba(209, 219, 229, 1); - --scroll-active-bg: rgba(100, 110, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(30, 50, 70, 0.9); - --button-selected-text: rgba(235, 245, 255, 1); - --button-hover-text: rgba(10, 30, 50, 1); - --input-text-color: rgba(20, 40, 60, 1.0); - --text-message: rgba(20, 40, 60, 1); - --text-warning: rgba(200, 100, 0, 1); - --text-input-echo: rgba(0, 80, 150, 1); - --text-shell: rgba(90, 110, 130, 1); - --text-error: rgba(180, 30, 40, 1); - - /* Other UI Elements */ - --scroll-color: rgba(50, 80, 110, 1); - --caret-color: rgba(20, 40, 60, 1.0); +.arctic-white-theme { + /* Backgrounds */ + --terminal-bg: rgba(235, 245, 255, 0.7); + --button-bg: rgba(210, 225, 240, 1); + --input-field-bg: rgba(250, 252, 255, 0.7); + --button-selected-bg: rgba(60, 120, 180, 0.85); + --button-hover-bg: rgba(180, 200, 220, 0.7); + --scroll-bg: rgba(198, 208, 218, 1); + --scroll-inverse-bg: rgba(209, 219, 229, 1); + --scroll-active-bg: rgba(100, 110, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(30, 50, 70, 0.9); + --button-selected-text: rgba(235, 245, 255, 1); + --button-hover-text: rgba(10, 30, 50, 1); + --input-text-color: rgba(20, 40, 60, 1.0); + --text-message: rgba(20, 40, 60, 1); + --text-warning: rgba(200, 100, 0, 1); + --text-input-echo: rgba(0, 80, 150, 1); + --text-shell: rgba(90, 110, 130, 1); + --text-error: rgba(180, 30, 40, 1); + + /* Other UI Elements */ + --scroll-color: rgba(50, 80, 110, 1); + --caret-color: rgba(20, 40, 60, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/AtomLightTheme.uss b/Styles/Themes/AtomLightTheme.uss index 45db4bd..b6e6e7a 100644 --- a/Styles/Themes/AtomLightTheme.uss +++ b/Styles/Themes/AtomLightTheme.uss @@ -1,26 +1,26 @@ -.atom-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(250, 250, 250, 0.9); - --button-bg: rgba(238, 238, 238, 1); - --input-field-bg: rgba(245, 245, 245, 0.8); - --button-selected-bg: rgba(64, 120, 224, 0.85); - --button-hover-bg: rgba(224, 224, 224, 1); - --scroll-bg: rgba(238, 238, 238, 1); - --scroll-inverse-bg: rgba(210, 210, 210, 1); - --scroll-active-bg: rgba(190, 190, 190, 1); - - /* Text & Foreground */ - --button-text: rgba(58, 58, 58, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(58, 58, 58, 1); - --input-text-color: rgba(58, 58, 58, 1.0); - --text-message: rgba(58, 58, 58, 1); - --text-warning: rgba(240, 194, 25, 1); - --text-input-echo: rgba(64, 120, 224, 1); - --text-shell: rgba(160, 161, 167, 1); - --text-error: rgba(228, 86, 73, 1); - - /* Other UI Elements */ - --scroll-color: rgba(58, 58, 58, 1); - --caret-color: rgba(64, 120, 224, 1.0); +.atom-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(250, 250, 250, 0.9); + --button-bg: rgba(238, 238, 238, 1); + --input-field-bg: rgba(245, 245, 245, 0.8); + --button-selected-bg: rgba(64, 120, 224, 0.85); + --button-hover-bg: rgba(224, 224, 224, 1); + --scroll-bg: rgba(238, 238, 238, 1); + --scroll-inverse-bg: rgba(210, 210, 210, 1); + --scroll-active-bg: rgba(190, 190, 190, 1); + + /* Text & Foreground */ + --button-text: rgba(58, 58, 58, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(58, 58, 58, 1); + --input-text-color: rgba(58, 58, 58, 1.0); + --text-message: rgba(58, 58, 58, 1); + --text-warning: rgba(240, 194, 25, 1); + --text-input-echo: rgba(64, 120, 224, 1); + --text-shell: rgba(160, 161, 167, 1); + --text-error: rgba(228, 86, 73, 1); + + /* Other UI Elements */ + --scroll-color: rgba(58, 58, 58, 1); + --caret-color: rgba(64, 120, 224, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/AtomOneDarkTheme.uss b/Styles/Themes/AtomOneDarkTheme.uss index d91881b..342b8c1 100644 --- a/Styles/Themes/AtomOneDarkTheme.uss +++ b/Styles/Themes/AtomOneDarkTheme.uss @@ -1,26 +1,26 @@ -.atom-one-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 44, 52, 0.9); - --button-bg: rgba(48, 52, 60, 1); - --input-field-bg: rgba(33, 37, 43, 0.8); - --button-selected-bg: rgba(97, 175, 239, 0.85); - --button-hover-bg: rgba(58, 63, 72, 1); - --scroll-bg: rgba(48, 52, 60, 1); - --scroll-inverse-bg: rgba(76, 82, 91, 1); - --scroll-active-bg: rgba(91, 98, 109, 1); - - /* Text & Foreground */ - --button-text: rgba(171, 178, 191, 0.9); - --button-selected-text: rgba(40, 44, 52, 1); - --button-hover-text: rgba(200, 207, 219, 1); - --input-text-color: rgba(171, 178, 191, 1.0); - --text-message: rgba(171, 178, 191, 1); - --text-warning: rgba(229, 192, 123, 1); - --text-input-echo: rgba(198, 120, 221, 1); - --text-shell: rgba(92, 99, 112, 1); - --text-error: rgba(224, 108, 117, 1); - - /* Other UI Elements */ - --scroll-color: rgba(171, 178, 191, 1); - --caret-color: rgba(86, 182, 194, 1.0); +.atom-one-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 44, 52, 0.9); + --button-bg: rgba(48, 52, 60, 1); + --input-field-bg: rgba(33, 37, 43, 0.8); + --button-selected-bg: rgba(97, 175, 239, 0.85); + --button-hover-bg: rgba(58, 63, 72, 1); + --scroll-bg: rgba(48, 52, 60, 1); + --scroll-inverse-bg: rgba(76, 82, 91, 1); + --scroll-active-bg: rgba(91, 98, 109, 1); + + /* Text & Foreground */ + --button-text: rgba(171, 178, 191, 0.9); + --button-selected-text: rgba(40, 44, 52, 1); + --button-hover-text: rgba(200, 207, 219, 1); + --input-text-color: rgba(171, 178, 191, 1.0); + --text-message: rgba(171, 178, 191, 1); + --text-warning: rgba(229, 192, 123, 1); + --text-input-echo: rgba(198, 120, 221, 1); + --text-shell: rgba(92, 99, 112, 1); + --text-error: rgba(224, 108, 117, 1); + + /* Other UI Elements */ + --scroll-color: rgba(171, 178, 191, 1); + --caret-color: rgba(86, 182, 194, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/AuroraTheme.uss b/Styles/Themes/AuroraTheme.uss index c8fe1cf..9ddc177 100644 --- a/Styles/Themes/AuroraTheme.uss +++ b/Styles/Themes/AuroraTheme.uss @@ -1,26 +1,26 @@ -.aurora-theme { - /* Backgrounds */ - --terminal-bg: rgba(10, 20, 30, 0.92); - --button-bg: rgba(20, 35, 50, 1); - --input-field-bg: rgba(5, 15, 25, 0.7); - --button-selected-bg: rgba(80, 250, 180, 0.85); - --button-hover-bg: rgba(30, 50, 70, 0.7); - --scroll-bg: rgba(40, 55, 70, 1); - --scroll-inverse-bg: rgba(30, 45, 60, 1); - --scroll-active-bg: rgba(60, 80, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(180, 255, 220, 0.9); - --button-selected-text: rgba(10, 20, 30, 1); - --button-hover-text: rgba(210, 255, 235, 1); - --input-text-color: rgba(180, 255, 220, 1.0); - --text-message: rgba(180, 255, 220, 1); - --text-warning: rgba(255, 220, 150, 1); - --text-input-echo: rgba(150, 200, 255, 1); - --text-shell: rgba(180, 180, 220, 1); - --text-error: rgba(255, 100, 180, 1); - - /* Other UI Elements */ - --scroll-color: rgba(80, 250, 180, 1); - --caret-color: rgba(180, 255, 220, 1.0); +.aurora-theme { + /* Backgrounds */ + --terminal-bg: rgba(10, 20, 30, 0.92); + --button-bg: rgba(20, 35, 50, 1); + --input-field-bg: rgba(5, 15, 25, 0.7); + --button-selected-bg: rgba(80, 250, 180, 0.85); + --button-hover-bg: rgba(30, 50, 70, 0.7); + --scroll-bg: rgba(40, 55, 70, 1); + --scroll-inverse-bg: rgba(30, 45, 60, 1); + --scroll-active-bg: rgba(60, 80, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(180, 255, 220, 0.9); + --button-selected-text: rgba(10, 20, 30, 1); + --button-hover-text: rgba(210, 255, 235, 1); + --input-text-color: rgba(180, 255, 220, 1.0); + --text-message: rgba(180, 255, 220, 1); + --text-warning: rgba(255, 220, 150, 1); + --text-input-echo: rgba(150, 200, 255, 1); + --text-shell: rgba(180, 180, 220, 1); + --text-error: rgba(255, 100, 180, 1); + + /* Other UI Elements */ + --scroll-color: rgba(80, 250, 180, 1); + --caret-color: rgba(180, 255, 220, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/AutumnLeavesTheme.uss b/Styles/Themes/AutumnLeavesTheme.uss index 1e51cee..050ba90 100644 --- a/Styles/Themes/AutumnLeavesTheme.uss +++ b/Styles/Themes/AutumnLeavesTheme.uss @@ -1,26 +1,26 @@ -.autumn-leaves-theme { - /* Backgrounds */ - --terminal-bg: rgba(70, 40, 20, 0.85); - --button-bg: rgba(95, 55, 30, 1); - --input-field-bg: rgba(60, 30, 15, 0.7); - --button-selected-bg: rgba(255, 140, 0, 0.85); - --button-hover-bg: rgba(120, 75, 45, 0.7); - --scroll-bg: rgba(110, 65, 40, 1); - --scroll-inverse-bg: rgba(95, 55, 30, 1); - --scroll-active-bg: rgba(140, 95, 65, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 200, 150, 0.9); - --button-selected-text: rgba(70, 40, 20, 1); - --button-hover-text: rgba(255, 220, 180, 1); - --input-text-color: rgba(255, 200, 150, 1.0); - --text-message: rgba(255, 200, 150, 1); - --text-warning: rgba(255, 215, 0, 1); - --text-input-echo: rgba(210, 105, 30, 1); - --text-shell: rgba(180, 120, 80, 1); - --text-error: rgba(200, 60, 60, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 140, 0, 1); - --caret-color: rgba(255, 200, 150, 1.0); +.autumn-leaves-theme { + /* Backgrounds */ + --terminal-bg: rgba(70, 40, 20, 0.85); + --button-bg: rgba(95, 55, 30, 1); + --input-field-bg: rgba(60, 30, 15, 0.7); + --button-selected-bg: rgba(255, 140, 0, 0.85); + --button-hover-bg: rgba(120, 75, 45, 0.7); + --scroll-bg: rgba(110, 65, 40, 1); + --scroll-inverse-bg: rgba(95, 55, 30, 1); + --scroll-active-bg: rgba(140, 95, 65, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 200, 150, 0.9); + --button-selected-text: rgba(70, 40, 20, 1); + --button-hover-text: rgba(255, 220, 180, 1); + --input-text-color: rgba(255, 200, 150, 1.0); + --text-message: rgba(255, 200, 150, 1); + --text-warning: rgba(255, 215, 0, 1); + --text-input-echo: rgba(210, 105, 30, 1); + --text-shell: rgba(180, 120, 80, 1); + --text-error: rgba(200, 60, 60, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 140, 0, 1); + --caret-color: rgba(255, 200, 150, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/AyuLightTheme.uss b/Styles/Themes/AyuLightTheme.uss index eeddf80..5b3968f 100644 --- a/Styles/Themes/AyuLightTheme.uss +++ b/Styles/Themes/AyuLightTheme.uss @@ -1,26 +1,26 @@ -.ayu-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(250, 250, 250, 0.9); - --button-bg: rgba(242, 242, 242, 1); - --input-field-bg: rgba(245, 245, 245, 0.8); - --button-selected-bg: rgba(242, 150, 56, 0.85); - --button-hover-bg: rgba(232, 232, 232, 1); - --scroll-bg: rgba(242, 242, 242, 1); - --scroll-inverse-bg: rgba(255, 255, 255, 1); - --scroll-active-bg: rgba(171, 178, 191, 1); - - /* Text & Foreground */ - --button-text: rgba(87, 95, 107, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(67, 75, 87, 1); - --input-text-color: rgba(87, 95, 107, 1); - --text-message: rgba(87, 95, 107, 1); - --text-warning: rgba(210, 130, 40, 1); - --text-input-echo: rgba(56, 154, 206, 1); - --text-shell: rgba(171, 178, 191, 1); - --text-error: rgba(206, 86, 86, 1); - - /* Other UI Elements */ - --scroll-color: rgba(87, 95, 107, 1); - --caret-color: rgba(242, 150, 56, 1); +.ayu-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(250, 250, 250, 0.9); + --button-bg: rgba(242, 242, 242, 1); + --input-field-bg: rgba(245, 245, 245, 0.8); + --button-selected-bg: rgba(242, 150, 56, 0.85); + --button-hover-bg: rgba(232, 232, 232, 1); + --scroll-bg: rgba(242, 242, 242, 1); + --scroll-inverse-bg: rgba(255, 255, 255, 1); + --scroll-active-bg: rgba(171, 178, 191, 1); + + /* Text & Foreground */ + --button-text: rgba(87, 95, 107, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(67, 75, 87, 1); + --input-text-color: rgba(87, 95, 107, 1); + --text-message: rgba(87, 95, 107, 1); + --text-warning: rgba(210, 130, 40, 1); + --text-input-echo: rgba(56, 154, 206, 1); + --text-shell: rgba(171, 178, 191, 1); + --text-error: rgba(206, 86, 86, 1); + + /* Other UI Elements */ + --scroll-color: rgba(87, 95, 107, 1); + --caret-color: rgba(242, 150, 56, 1); } \ No newline at end of file diff --git a/Styles/Themes/AyuMirageTheme.uss b/Styles/Themes/AyuMirageTheme.uss index b2441dc..6277c88 100644 --- a/Styles/Themes/AyuMirageTheme.uss +++ b/Styles/Themes/AyuMirageTheme.uss @@ -1,26 +1,26 @@ -.ayu-mirage-theme { - /* Backgrounds */ - --terminal-bg: rgba(29, 31, 38, 0.9); - --button-bg: rgba(35, 38, 48, 1); - --input-field-bg: rgba(25, 27, 33, 0.8); - --button-selected-bg: rgba(255, 198, 109, 0.85); - --button-hover-bg: rgba(45, 49, 61, 1); - --scroll-bg: rgba(35, 38, 48, 1); - --scroll-inverse-bg: rgba(54, 58, 71, 1); - --scroll-active-bg: rgba(66, 71, 86, 1); - - /* Text & Foreground */ - --button-text: rgba(203, 208, 220, 0.9); - --button-selected-text: rgba(29, 31, 38, 1); - --button-hover-text: rgba(220, 225, 237, 1); - --input-text-color: rgba(203, 208, 220, 1.0); - --text-message: rgba(203, 208, 220, 1); - --text-warning: rgba(255, 180, 84, 1); - --text-input-echo: rgba(90, 217, 255, 1); - --text-shell: rgba(84, 89, 104, 1); - --text-error: rgba(255, 115, 115, 1); - - /* Other UI Elements */ - --scroll-color: rgba(203, 208, 220, 1); - --caret-color: rgba(255, 198, 109, 1.0); +.ayu-mirage-theme { + /* Backgrounds */ + --terminal-bg: rgba(29, 31, 38, 0.9); + --button-bg: rgba(35, 38, 48, 1); + --input-field-bg: rgba(25, 27, 33, 0.8); + --button-selected-bg: rgba(255, 198, 109, 0.85); + --button-hover-bg: rgba(45, 49, 61, 1); + --scroll-bg: rgba(35, 38, 48, 1); + --scroll-inverse-bg: rgba(54, 58, 71, 1); + --scroll-active-bg: rgba(66, 71, 86, 1); + + /* Text & Foreground */ + --button-text: rgba(203, 208, 220, 0.9); + --button-selected-text: rgba(29, 31, 38, 1); + --button-hover-text: rgba(220, 225, 237, 1); + --input-text-color: rgba(203, 208, 220, 1.0); + --text-message: rgba(203, 208, 220, 1); + --text-warning: rgba(255, 180, 84, 1); + --text-input-echo: rgba(90, 217, 255, 1); + --text-shell: rgba(84, 89, 104, 1); + --text-error: rgba(255, 115, 115, 1); + + /* Other UI Elements */ + --scroll-color: rgba(203, 208, 220, 1); + --caret-color: rgba(255, 198, 109, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/Base16TomorrowNightTheme.uss b/Styles/Themes/Base16TomorrowNightTheme.uss index 1ebc954..f229626 100644 --- a/Styles/Themes/Base16TomorrowNightTheme.uss +++ b/Styles/Themes/Base16TomorrowNightTheme.uss @@ -1,26 +1,26 @@ -.base16-tomorrow-night-theme { - /* Backgrounds */ - --terminal-bg: rgba(29, 31, 33, 0.9); - --button-bg: rgba(42, 44, 46, 1); - --input-field-bg: rgba(24, 26, 27, 0.8); - --button-selected-bg: rgba(179, 143, 188, 0.85); - --button-hover-bg: rgba(57, 59, 61, 1); - --scroll-bg: rgba(42, 44, 46, 1); - --scroll-inverse-bg: rgba(76, 79, 81, 1); - --scroll-active-bg: rgba(94, 97, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(197, 200, 198, 0.9); - --button-selected-text: rgba(29, 31, 33, 1); - --button-hover-text: rgba(224, 224, 224, 1); - --input-text-color: rgba(197, 200, 198, 1.0); - --text-message: rgba(197, 200, 198, 1); - --text-warning: rgba(240, 198, 116, 1); - --text-input-echo: rgba(138, 190, 183, 1); - --text-shell: rgba(150, 152, 150, 1); - --text-error: rgba(204, 102, 102, 1); - - /* Other UI Elements */ - --scroll-color: rgba(197, 200, 198, 1); - --caret-color: rgba(197, 200, 198, 1.0); +.base16-tomorrow-night-theme { + /* Backgrounds */ + --terminal-bg: rgba(29, 31, 33, 0.9); + --button-bg: rgba(42, 44, 46, 1); + --input-field-bg: rgba(24, 26, 27, 0.8); + --button-selected-bg: rgba(179, 143, 188, 0.85); + --button-hover-bg: rgba(57, 59, 61, 1); + --scroll-bg: rgba(42, 44, 46, 1); + --scroll-inverse-bg: rgba(76, 79, 81, 1); + --scroll-active-bg: rgba(94, 97, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(197, 200, 198, 0.9); + --button-selected-text: rgba(29, 31, 33, 1); + --button-hover-text: rgba(224, 224, 224, 1); + --input-text-color: rgba(197, 200, 198, 1.0); + --text-message: rgba(197, 200, 198, 1); + --text-warning: rgba(240, 198, 116, 1); + --text-input-echo: rgba(138, 190, 183, 1); + --text-shell: rgba(150, 152, 150, 1); + --text-error: rgba(204, 102, 102, 1); + + /* Other UI Elements */ + --scroll-color: rgba(197, 200, 198, 1); + --caret-color: rgba(197, 200, 198, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/BerrySmoothieTheme.uss b/Styles/Themes/BerrySmoothieTheme.uss index d0e02cf..78f33a8 100644 --- a/Styles/Themes/BerrySmoothieTheme.uss +++ b/Styles/Themes/BerrySmoothieTheme.uss @@ -1,26 +1,26 @@ -.berry-smoothie-theme { - /* Backgrounds */ - --terminal-bg: rgba(180, 150, 190, 0.8); - --button-bg: rgba(160, 130, 170, 1); - --input-field-bg: rgba(200, 170, 210, 0.7); - --button-selected-bg: rgba(80, 60, 120, 0.85); - --button-hover-bg: rgba(140, 110, 150, 0.7); - --scroll-bg: rgba(150, 120, 160, 1); - --scroll-inverse-bg: rgba(160, 130, 170, 1); - --scroll-active-bg: rgba(110, 90, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(245, 240, 250, 0.9); - --button-selected-text: rgba(230, 220, 240, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(245, 240, 250, 1.0); - --text-message: rgba(245, 240, 250, 1); - --text-warning: rgba(180, 200, 240, 1); - --text-input-echo: rgba(210, 180, 220, 1); - --text-shell: rgba(140, 110, 150, 1); - --text-error: rgba(220, 100, 120, 1); - - /* Other UI Elements */ - --scroll-color: rgba(80, 60, 120, 1); - --caret-color: rgba(245, 240, 250, 1.0); +.berry-smoothie-theme { + /* Backgrounds */ + --terminal-bg: rgba(180, 150, 190, 0.8); + --button-bg: rgba(160, 130, 170, 1); + --input-field-bg: rgba(200, 170, 210, 0.7); + --button-selected-bg: rgba(80, 60, 120, 0.85); + --button-hover-bg: rgba(140, 110, 150, 0.7); + --scroll-bg: rgba(150, 120, 160, 1); + --scroll-inverse-bg: rgba(160, 130, 170, 1); + --scroll-active-bg: rgba(110, 90, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(245, 240, 250, 0.9); + --button-selected-text: rgba(230, 220, 240, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(245, 240, 250, 1.0); + --text-message: rgba(245, 240, 250, 1); + --text-warning: rgba(180, 200, 240, 1); + --text-input-echo: rgba(210, 180, 220, 1); + --text-shell: rgba(140, 110, 150, 1); + --text-error: rgba(220, 100, 120, 1); + + /* Other UI Elements */ + --scroll-color: rgba(80, 60, 120, 1); + --caret-color: rgba(245, 240, 250, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/BiohazardTheme.uss b/Styles/Themes/BiohazardTheme.uss index af8f545..baef179 100644 --- a/Styles/Themes/BiohazardTheme.uss +++ b/Styles/Themes/BiohazardTheme.uss @@ -1,26 +1,26 @@ -.biohazard-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 50, 30, 0.88); - --button-bg: rgba(60, 75, 45, 1); - --input-field-bg: rgba(30, 40, 20, 0.7); - --button-selected-bg: rgba(255, 255, 0, 0.85); - --button-hover-bg: rgba(80, 95, 60, 0.7); - --scroll-bg: rgba(70, 85, 55, 1); - --scroll-inverse-bg: rgba(60, 75, 45, 1); - --scroll-active-bg: rgba(100, 115, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 230, 180, 0.9); - --button-selected-text: rgba(40, 50, 30, 1); - --button-hover-text: rgba(240, 250, 200, 1); - --input-text-color: rgba(220, 230, 180, 1.0); - --text-message: rgba(220, 230, 180, 1); - --text-warning: rgba(200, 180, 50, 1); - --text-input-echo: rgba(150, 170, 100, 1); - --text-shell: rgba(120, 130, 90, 1); - --text-error: rgba(200, 0, 0, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 255, 0, 1); - --caret-color: rgba(220, 230, 180, 1.0); +.biohazard-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 50, 30, 0.88); + --button-bg: rgba(60, 75, 45, 1); + --input-field-bg: rgba(30, 40, 20, 0.7); + --button-selected-bg: rgba(255, 255, 0, 0.85); + --button-hover-bg: rgba(80, 95, 60, 0.7); + --scroll-bg: rgba(70, 85, 55, 1); + --scroll-inverse-bg: rgba(60, 75, 45, 1); + --scroll-active-bg: rgba(100, 115, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 230, 180, 0.9); + --button-selected-text: rgba(40, 50, 30, 1); + --button-hover-text: rgba(240, 250, 200, 1); + --input-text-color: rgba(220, 230, 180, 1.0); + --text-message: rgba(220, 230, 180, 1); + --text-warning: rgba(200, 180, 50, 1); + --text-input-echo: rgba(150, 170, 100, 1); + --text-shell: rgba(120, 130, 90, 1); + --text-error: rgba(200, 0, 0, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 255, 0, 1); + --caret-color: rgba(220, 230, 180, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/BlackboardTheme.uss b/Styles/Themes/BlackboardTheme.uss index 8f55884..8394d13 100644 --- a/Styles/Themes/BlackboardTheme.uss +++ b/Styles/Themes/BlackboardTheme.uss @@ -1,26 +1,26 @@ -.blackboard-theme { - /* Backgrounds */ - --terminal-bg: rgba(12, 38, 56, 0.9); - --button-bg: rgba(30, 60, 80, 1); - --input-field-bg: rgba(20, 50, 70, 0.8); - --button-selected-bg: rgba(255, 255, 255, 0.85); - --button-hover-bg: rgba(50, 80, 100, 1); - --scroll-bg: rgba(30, 60, 80, 1); - --scroll-inverse-bg: rgba(5, 20, 40, 1); - --scroll-active-bg: rgba(174, 193, 208, 1); - - /* Text & Foreground */ - --button-text: rgba(240, 240, 240, 0.9); - --button-selected-text: rgba(12, 38, 56, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(240, 240, 240, 1); - --text-message: rgba(240, 240, 240, 1); - --text-warning: rgba(255, 235, 160, 1); - --text-input-echo: rgba(180, 220, 255, 1); - --text-shell: rgba(174, 193, 208, 1); - --text-error: rgba(255, 170, 170, 1); - - /* Other UI Elements */ - --scroll-color: rgba(240, 240, 240, 1); - --caret-color: rgba(240, 240, 240, 1); +.blackboard-theme { + /* Backgrounds */ + --terminal-bg: rgba(12, 38, 56, 0.9); + --button-bg: rgba(30, 60, 80, 1); + --input-field-bg: rgba(20, 50, 70, 0.8); + --button-selected-bg: rgba(255, 255, 255, 0.85); + --button-hover-bg: rgba(50, 80, 100, 1); + --scroll-bg: rgba(30, 60, 80, 1); + --scroll-inverse-bg: rgba(5, 20, 40, 1); + --scroll-active-bg: rgba(174, 193, 208, 1); + + /* Text & Foreground */ + --button-text: rgba(240, 240, 240, 0.9); + --button-selected-text: rgba(12, 38, 56, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(240, 240, 240, 1); + --text-message: rgba(240, 240, 240, 1); + --text-warning: rgba(255, 235, 160, 1); + --text-input-echo: rgba(180, 220, 255, 1); + --text-shell: rgba(174, 193, 208, 1); + --text-error: rgba(255, 170, 170, 1); + + /* Other UI Elements */ + --scroll-color: rgba(240, 240, 240, 1); + --caret-color: rgba(240, 240, 240, 1); } \ No newline at end of file diff --git a/Styles/Themes/BlueprintTheme.uss b/Styles/Themes/BlueprintTheme.uss index be869fe..56ac767 100644 --- a/Styles/Themes/BlueprintTheme.uss +++ b/Styles/Themes/BlueprintTheme.uss @@ -1,26 +1,26 @@ -.blueprint-theme { - /* Backgrounds */ - --terminal-bg: rgba(50, 80, 140, 0.8); - --button-bg: rgba(70, 100, 160, 1); - --input-field-bg: rgba(40, 60, 120, 0.7); - --button-selected-bg: rgba(255, 255, 255, 0.85); - --button-hover-bg: rgba(90, 120, 180, 0.7); - --scroll-bg: rgba(80, 110, 170, 1); - --scroll-inverse-bg: rgba(70, 100, 160, 1); - --scroll-active-bg: rgba(110, 140, 200, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 230, 255, 0.9); - --button-selected-text: rgba(50, 80, 140, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(220, 230, 255, 1.0); - --text-message: rgba(220, 230, 255, 1); - --text-warning: rgba(180, 190, 220, 1); - --text-input-echo: rgba(150, 170, 200, 1); - --text-shell: rgba(120, 140, 180, 1); - --text-error: rgba(255, 180, 180, 1); - - /* Other UI Elements */ - --scroll-color: rgba(200, 210, 240, 1); - --caret-color: rgba(255, 255, 255, 1.0); +.blueprint-theme { + /* Backgrounds */ + --terminal-bg: rgba(50, 80, 140, 0.8); + --button-bg: rgba(70, 100, 160, 1); + --input-field-bg: rgba(40, 60, 120, 0.7); + --button-selected-bg: rgba(255, 255, 255, 0.85); + --button-hover-bg: rgba(90, 120, 180, 0.7); + --scroll-bg: rgba(80, 110, 170, 1); + --scroll-inverse-bg: rgba(70, 100, 160, 1); + --scroll-active-bg: rgba(110, 140, 200, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 230, 255, 0.9); + --button-selected-text: rgba(50, 80, 140, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(220, 230, 255, 1.0); + --text-message: rgba(220, 230, 255, 1); + --text-warning: rgba(180, 190, 220, 1); + --text-input-echo: rgba(150, 170, 200, 1); + --text-shell: rgba(120, 140, 180, 1); + --text-error: rgba(255, 180, 180, 1); + + /* Other UI Elements */ + --scroll-color: rgba(200, 210, 240, 1); + --caret-color: rgba(255, 255, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/BreathOfTheWildTheme.uss b/Styles/Themes/BreathOfTheWildTheme.uss index 7dcb9d2..bd6cb14 100644 --- a/Styles/Themes/BreathOfTheWildTheme.uss +++ b/Styles/Themes/BreathOfTheWildTheme.uss @@ -1,26 +1,26 @@ -.botw-theme { - /* Backgrounds */ - --terminal-bg: rgba(242, 238, 225, 0.9); - --button-bg: rgba(210, 205, 190, 1); - --input-field-bg: rgba(226, 221, 208, 0.8); - --button-selected-bg: rgba(60, 180, 210, 0.85); - --button-hover-bg: rgba(190, 185, 170, 1); - --scroll-bg: rgba(210, 205, 190, 1); - --scroll-inverse-bg: rgba(250, 248, 240, 1); - --scroll-active-bg: rgba(245, 130, 60, 1); - - /* Text & Foreground */ - --button-text: rgba(80, 70, 60, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(60, 50, 40, 1); - --input-text-color: rgba(80, 70, 60, 1); - --text-message: rgba(80, 70, 60, 1); - --text-warning: rgba(245, 130, 60, 1); - --text-input-echo: rgba(60, 180, 210, 1); - --text-shell: rgba(140, 130, 115, 1); - --text-error: rgba(200, 50, 50, 1); - - /* Other UI Elements */ - --scroll-color: rgba(80, 70, 60, 1); - --caret-color: rgba(60, 180, 210, 1); +.botw-theme { + /* Backgrounds */ + --terminal-bg: rgba(242, 238, 225, 0.9); + --button-bg: rgba(210, 205, 190, 1); + --input-field-bg: rgba(226, 221, 208, 0.8); + --button-selected-bg: rgba(60, 180, 210, 0.85); + --button-hover-bg: rgba(190, 185, 170, 1); + --scroll-bg: rgba(210, 205, 190, 1); + --scroll-inverse-bg: rgba(250, 248, 240, 1); + --scroll-active-bg: rgba(245, 130, 60, 1); + + /* Text & Foreground */ + --button-text: rgba(80, 70, 60, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(60, 50, 40, 1); + --input-text-color: rgba(80, 70, 60, 1); + --text-message: rgba(80, 70, 60, 1); + --text-warning: rgba(245, 130, 60, 1); + --text-input-echo: rgba(60, 180, 210, 1); + --text-shell: rgba(140, 130, 115, 1); + --text-error: rgba(200, 50, 50, 1); + + /* Other UI Elements */ + --scroll-color: rgba(80, 70, 60, 1); + --caret-color: rgba(60, 180, 210, 1); } \ No newline at end of file diff --git a/Styles/Themes/BrogrammerTheme.uss b/Styles/Themes/BrogrammerTheme.uss index 45928c5..8e62846 100644 --- a/Styles/Themes/BrogrammerTheme.uss +++ b/Styles/Themes/BrogrammerTheme.uss @@ -1,26 +1,26 @@ -.brogrammer-theme { - /* Backgrounds */ - --terminal-bg: rgba(20, 20, 20, 0.9); - --button-bg: rgba(35, 35, 35, 1); - --input-field-bg: rgba(28, 28, 28, 0.8); - --button-selected-bg: rgba(0, 174, 239, 0.85); - --button-hover-bg: rgba(50, 50, 50, 1); - --scroll-bg: rgba(35, 35, 35, 1); - --scroll-inverse-bg: rgba(10, 10, 10, 1); - --scroll-active-bg: rgba(223, 80, 185, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 220, 220, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(240, 240, 240, 1); - --input-text-color: rgba(220, 220, 220, 1); - --text-message: rgba(220, 220, 220, 1); - --text-warning: rgba(242, 177, 10, 1); - --text-input-echo: rgba(46, 204, 113, 1); - --text-shell: rgba(150, 150, 150, 1); - --text-error: rgba(223, 80, 185, 1); - - /* Other UI Elements */ - --scroll-color: rgba(220, 220, 220, 1); - --caret-color: rgba(220, 220, 220, 1); +.brogrammer-theme { + /* Backgrounds */ + --terminal-bg: rgba(20, 20, 20, 0.9); + --button-bg: rgba(35, 35, 35, 1); + --input-field-bg: rgba(28, 28, 28, 0.8); + --button-selected-bg: rgba(0, 174, 239, 0.85); + --button-hover-bg: rgba(50, 50, 50, 1); + --scroll-bg: rgba(35, 35, 35, 1); + --scroll-inverse-bg: rgba(10, 10, 10, 1); + --scroll-active-bg: rgba(223, 80, 185, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 220, 220, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(240, 240, 240, 1); + --input-text-color: rgba(220, 220, 220, 1); + --text-message: rgba(220, 220, 220, 1); + --text-warning: rgba(242, 177, 10, 1); + --text-input-echo: rgba(46, 204, 113, 1); + --text-shell: rgba(150, 150, 150, 1); + --text-error: rgba(223, 80, 185, 1); + + /* Other UI Elements */ + --scroll-color: rgba(220, 220, 220, 1); + --caret-color: rgba(220, 220, 220, 1); } \ No newline at end of file diff --git a/Styles/Themes/BrushedMetalTheme.uss b/Styles/Themes/BrushedMetalTheme.uss index 8431b48..6d270cd 100644 --- a/Styles/Themes/BrushedMetalTheme.uss +++ b/Styles/Themes/BrushedMetalTheme.uss @@ -1,26 +1,26 @@ -.brushed-metal-theme { - /* Backgrounds */ - --terminal-bg: rgba(140, 150, 160, 0.8); - --button-bg: rgba(160, 170, 180, 1); - --input-field-bg: rgba(120, 130, 140, 0.7); - --button-selected-bg: rgba(80, 90, 100, 0.85); - --button-hover-bg: rgba(180, 190, 200, 0.7); - --scroll-bg: rgba(150, 160, 170, 1); - --scroll-inverse-bg: rgba(160, 170, 180, 1); - --scroll-active-bg: rgba(100, 110, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(40, 50, 60, 0.9); - --button-selected-text: rgba(220, 230, 240, 1); - --button-hover-text: rgba(20, 30, 40, 1); - --input-text-color: rgba(40, 50, 60, 1.0); - --text-message: rgba(40, 50, 60, 1); - --text-warning: rgba(180, 160, 100, 1); - --text-input-echo: rgba(90, 100, 110, 1); - --text-shell: rgba(110, 120, 130, 1); - --text-error: rgba(180, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(80, 90, 100, 1); - --caret-color: rgba(40, 50, 60, 1.0); +.brushed-metal-theme { + /* Backgrounds */ + --terminal-bg: rgba(140, 150, 160, 0.8); + --button-bg: rgba(160, 170, 180, 1); + --input-field-bg: rgba(120, 130, 140, 0.7); + --button-selected-bg: rgba(80, 90, 100, 0.85); + --button-hover-bg: rgba(180, 190, 200, 0.7); + --scroll-bg: rgba(150, 160, 170, 1); + --scroll-inverse-bg: rgba(160, 170, 180, 1); + --scroll-active-bg: rgba(100, 110, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(40, 50, 60, 0.9); + --button-selected-text: rgba(220, 230, 240, 1); + --button-hover-text: rgba(20, 30, 40, 1); + --input-text-color: rgba(40, 50, 60, 1.0); + --text-message: rgba(40, 50, 60, 1); + --text-warning: rgba(180, 160, 100, 1); + --text-input-echo: rgba(90, 100, 110, 1); + --text-shell: rgba(110, 120, 130, 1); + --text-error: rgba(180, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(80, 90, 100, 1); + --caret-color: rgba(40, 50, 60, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/BubbleGumTheme.uss b/Styles/Themes/BubbleGumTheme.uss index afa445e..a88368b 100644 --- a/Styles/Themes/BubbleGumTheme.uss +++ b/Styles/Themes/BubbleGumTheme.uss @@ -1,26 +1,26 @@ -.bubblegum-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 190, 210, 0.75); - --button-bg: rgba(240, 170, 190, 1); - --input-field-bg: rgba(255, 210, 230, 0.7); - --button-selected-bg: rgba(50, 150, 220, 0.85); - --button-hover-bg: rgba(220, 150, 170, 0.7); - --scroll-bg: rgba(230, 160, 180, 1); - --scroll-inverse-bg: rgba(240, 170, 190, 1); - --scroll-active-bg: rgba(180, 120, 140, 1); - - /* Text & Foreground */ - --button-text: rgba(80, 40, 60, 0.9); - --button-selected-text: rgba(240, 250, 255, 1); - --button-hover-text: rgba(60, 20, 40, 1); - --input-text-color: rgba(80, 40, 60, 1.0); - --text-message: rgba(80, 40, 60, 1); - --text-warning: rgba(255, 230, 100, 1); - --text-input-echo: rgba(150, 220, 255, 1); - --text-shell: rgba(150, 90, 110, 1); - --text-error: rgba(180, 30, 50, 1); - - /* Other UI Elements */ - --scroll-color: rgba(50, 150, 220, 1); - --caret-color: rgba(80, 40, 60, 1.0); +.bubblegum-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 190, 210, 0.75); + --button-bg: rgba(240, 170, 190, 1); + --input-field-bg: rgba(255, 210, 230, 0.7); + --button-selected-bg: rgba(50, 150, 220, 0.85); + --button-hover-bg: rgba(220, 150, 170, 0.7); + --scroll-bg: rgba(230, 160, 180, 1); + --scroll-inverse-bg: rgba(240, 170, 190, 1); + --scroll-active-bg: rgba(180, 120, 140, 1); + + /* Text & Foreground */ + --button-text: rgba(80, 40, 60, 0.9); + --button-selected-text: rgba(240, 250, 255, 1); + --button-hover-text: rgba(60, 20, 40, 1); + --input-text-color: rgba(80, 40, 60, 1.0); + --text-message: rgba(80, 40, 60, 1); + --text-warning: rgba(255, 230, 100, 1); + --text-input-echo: rgba(150, 220, 255, 1); + --text-shell: rgba(150, 90, 110, 1); + --text-error: rgba(180, 30, 50, 1); + + /* Other UI Elements */ + --scroll-color: rgba(50, 150, 220, 1); + --caret-color: rgba(80, 40, 60, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/CarbonFiberTheme.uss b/Styles/Themes/CarbonFiberTheme.uss index 50e908b..ef75f18 100644 --- a/Styles/Themes/CarbonFiberTheme.uss +++ b/Styles/Themes/CarbonFiberTheme.uss @@ -1,26 +1,26 @@ -.carbon-fiber-theme { - /* Backgrounds */ - --terminal-bg: rgba(25, 25, 25, 0.92); - --button-bg: rgba(45, 45, 45, 1); - --input-field-bg: rgba(15, 15, 15, 0.7); - --button-selected-bg: rgba(180, 180, 180, 0.85); - --button-hover-bg: rgba(65, 65, 65, 0.7); - --scroll-bg: rgba(55, 55, 55, 1); - --scroll-inverse-bg: rgba(45, 45, 45, 1); - --scroll-active-bg: rgba(90, 90, 90, 1); - - /* Text & Foreground */ - --button-text: rgba(200, 200, 200, 0.9); - --button-selected-text: rgba(25, 25, 25, 1); - --button-hover-text: rgba(230, 230, 230, 1); - --input-text-color: rgba(200, 200, 200, 1.0); - --text-message: rgba(200, 200, 200, 1); - --text-warning: rgba(210, 190, 130, 1); - --text-input-echo: rgba(150, 170, 190, 1); - --text-shell: rgba(140, 140, 140, 1); - --text-error: rgba(220, 100, 100, 1); - - /* Other UI Elements */ - --scroll-color: rgba(180, 180, 180, 1); - --caret-color: rgba(200, 200, 200, 1.0); +.carbon-fiber-theme { + /* Backgrounds */ + --terminal-bg: rgba(25, 25, 25, 0.92); + --button-bg: rgba(45, 45, 45, 1); + --input-field-bg: rgba(15, 15, 15, 0.7); + --button-selected-bg: rgba(180, 180, 180, 0.85); + --button-hover-bg: rgba(65, 65, 65, 0.7); + --scroll-bg: rgba(55, 55, 55, 1); + --scroll-inverse-bg: rgba(45, 45, 45, 1); + --scroll-active-bg: rgba(90, 90, 90, 1); + + /* Text & Foreground */ + --button-text: rgba(200, 200, 200, 0.9); + --button-selected-text: rgba(25, 25, 25, 1); + --button-hover-text: rgba(230, 230, 230, 1); + --input-text-color: rgba(200, 200, 200, 1.0); + --text-message: rgba(200, 200, 200, 1); + --text-warning: rgba(210, 190, 130, 1); + --text-input-echo: rgba(150, 170, 190, 1); + --text-shell: rgba(140, 140, 140, 1); + --text-error: rgba(220, 100, 100, 1); + + /* Other UI Elements */ + --scroll-color: rgba(180, 180, 180, 1); + --caret-color: rgba(200, 200, 200, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/CatppuccinTheme.uss b/Styles/Themes/CatppuccinTheme.uss index 808c157..85fe089 100644 --- a/Styles/Themes/CatppuccinTheme.uss +++ b/Styles/Themes/CatppuccinTheme.uss @@ -1,26 +1,26 @@ -.catppuccin-theme { - /* Backgrounds */ - --terminal-bg: rgba(30, 30, 46, 0.9); - --button-bg: rgba(49, 50, 68, 1); - --input-field-bg: rgba(24, 24, 37, 0.8); - --button-selected-bg: rgba(137, 180, 250, 0.85); - --button-hover-bg: rgba(69, 71, 90, 1); - --scroll-bg: rgba(49, 50, 68, 1); - --scroll-inverse-bg: rgba(88, 91, 112, 1); - --scroll-active-bg: rgba(108, 112, 134, 1); - - /* Text & Foreground */ - --button-text: rgba(205, 214, 244, 0.9); - --button-selected-text: rgba(30, 30, 46, 1); - --button-hover-text: rgba(205, 214, 244, 1); - --input-text-color: rgba(205, 214, 244, 1.0); - --text-message: rgba(205, 214, 244, 1); - --text-warning: rgba(250, 179, 135, 1); - --text-input-echo: rgba(148, 226, 213, 1); - --text-shell: rgba(127, 132, 156, 1); - --text-error: rgba(243, 139, 168, 1); - - /* Other UI Elements */ - --scroll-color: rgba(205, 214, 244, 1); - --caret-color: rgba(245, 224, 220, 1.0); +.catppuccin-theme { + /* Backgrounds */ + --terminal-bg: rgba(30, 30, 46, 0.9); + --button-bg: rgba(49, 50, 68, 1); + --input-field-bg: rgba(24, 24, 37, 0.8); + --button-selected-bg: rgba(137, 180, 250, 0.85); + --button-hover-bg: rgba(69, 71, 90, 1); + --scroll-bg: rgba(49, 50, 68, 1); + --scroll-inverse-bg: rgba(88, 91, 112, 1); + --scroll-active-bg: rgba(108, 112, 134, 1); + + /* Text & Foreground */ + --button-text: rgba(205, 214, 244, 0.9); + --button-selected-text: rgba(30, 30, 46, 1); + --button-hover-text: rgba(205, 214, 244, 1); + --input-text-color: rgba(205, 214, 244, 1.0); + --text-message: rgba(205, 214, 244, 1); + --text-warning: rgba(250, 179, 135, 1); + --text-input-echo: rgba(148, 226, 213, 1); + --text-shell: rgba(127, 132, 156, 1); + --text-error: rgba(243, 139, 168, 1); + + /* Other UI Elements */ + --scroll-color: rgba(205, 214, 244, 1); + --caret-color: rgba(245, 224, 220, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/ClassicGreenTheme.uss b/Styles/Themes/ClassicGreenTheme.uss index 82e4034..23144da 100644 --- a/Styles/Themes/ClassicGreenTheme.uss +++ b/Styles/Themes/ClassicGreenTheme.uss @@ -1,26 +1,26 @@ -.classic-green-theme { - /* Backgrounds */ - --terminal-bg: rgba(15, 15, 15, 0.85); - --button-bg: rgba(30, 30, 30, 1); - --input-field-bg: rgba(10, 10, 10, 0.7); - --button-selected-bg: rgba(0, 215, 0, 0.85); - --button-hover-bg: rgba(50, 50, 50, 0.7); - --scroll-bg: rgba(48, 48, 48, 1); - --scroll-inverse-bg: rgba(39, 39, 39, 1); - --scroll-active-bg: rgba(80, 80, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 255, 0, 0.9); - --button-selected-text: rgba(15, 15, 15, 1); - --button-hover-text: rgba(0, 235, 0, 1); - --input-text-color: rgba(0, 255, 0, 1.0); - --text-message: rgba(0, 255, 0, 1); - --text-warning: rgba(255, 255, 0, 1); - --text-input-echo: rgba(0, 200, 200, 1); - --text-shell: rgba(0, 180, 0, 1); - --text-error: rgba(255, 60, 60, 1); - - /* Other UI Elements */ - --scroll-color: rgba(0, 200, 0, 1); - --caret-color: rgba(0, 255, 0, 1.0); +.classic-green-theme { + /* Backgrounds */ + --terminal-bg: rgba(15, 15, 15, 0.85); + --button-bg: rgba(30, 30, 30, 1); + --input-field-bg: rgba(10, 10, 10, 0.7); + --button-selected-bg: rgba(0, 215, 0, 0.85); + --button-hover-bg: rgba(50, 50, 50, 0.7); + --scroll-bg: rgba(48, 48, 48, 1); + --scroll-inverse-bg: rgba(39, 39, 39, 1); + --scroll-active-bg: rgba(80, 80, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 255, 0, 0.9); + --button-selected-text: rgba(15, 15, 15, 1); + --button-hover-text: rgba(0, 235, 0, 1); + --input-text-color: rgba(0, 255, 0, 1.0); + --text-message: rgba(0, 255, 0, 1); + --text-warning: rgba(255, 255, 0, 1); + --text-input-echo: rgba(0, 200, 200, 1); + --text-shell: rgba(0, 180, 0, 1); + --text-error: rgba(255, 60, 60, 1); + + /* Other UI Elements */ + --scroll-color: rgba(0, 200, 0, 1); + --caret-color: rgba(0, 255, 0, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/Cobalt2Theme.uss b/Styles/Themes/Cobalt2Theme.uss index f040f58..4e96a75 100644 --- a/Styles/Themes/Cobalt2Theme.uss +++ b/Styles/Themes/Cobalt2Theme.uss @@ -1,26 +1,26 @@ -.cobalt2-theme { - /* Backgrounds */ - --terminal-bg: rgba(29, 46, 69, 0.9); - --button-bg: rgba(42, 62, 90, 1); - --input-field-bg: rgba(22, 35, 53, 0.8); - --button-selected-bg: rgba(255, 153, 0, 0.85); - --button-hover-bg: rgba(52, 77, 112, 1); - --scroll-bg: rgba(42, 62, 90, 1); - --scroll-inverse-bg: rgba(65, 94, 138, 1); - --scroll-active-bg: rgba(80, 115, 168, 1); - - /* Text & Foreground */ - --button-text: rgba(231, 231, 231, 0.9); - --button-selected-text: rgba(29, 46, 69, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(231, 231, 231, 1.0); - --text-message: rgba(231, 231, 231, 1); - --text-warning: rgba(255, 204, 0, 1); - --text-input-echo: rgba(150, 220, 255, 1); - --text-shell: rgba(144, 184, 224, 1); - --text-error: rgba(255, 85, 85, 1); - - /* Other UI Elements */ - --scroll-color: rgba(231, 231, 231, 1); - --caret-color: rgba(255, 153, 0, 1.0); +.cobalt2-theme { + /* Backgrounds */ + --terminal-bg: rgba(29, 46, 69, 0.9); + --button-bg: rgba(42, 62, 90, 1); + --input-field-bg: rgba(22, 35, 53, 0.8); + --button-selected-bg: rgba(255, 153, 0, 0.85); + --button-hover-bg: rgba(52, 77, 112, 1); + --scroll-bg: rgba(42, 62, 90, 1); + --scroll-inverse-bg: rgba(65, 94, 138, 1); + --scroll-active-bg: rgba(80, 115, 168, 1); + + /* Text & Foreground */ + --button-text: rgba(231, 231, 231, 0.9); + --button-selected-text: rgba(29, 46, 69, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(231, 231, 231, 1.0); + --text-message: rgba(231, 231, 231, 1); + --text-warning: rgba(255, 204, 0, 1); + --text-input-echo: rgba(150, 220, 255, 1); + --text-shell: rgba(144, 184, 224, 1); + --text-error: rgba(255, 85, 85, 1); + + /* Other UI Elements */ + --scroll-color: rgba(231, 231, 231, 1); + --caret-color: rgba(255, 153, 0, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/CoffeeShopTheme.uss b/Styles/Themes/CoffeeShopTheme.uss index b36cdd3..e8abab7 100644 --- a/Styles/Themes/CoffeeShopTheme.uss +++ b/Styles/Themes/CoffeeShopTheme.uss @@ -1,26 +1,26 @@ -.coffee-shop-theme { - /* Backgrounds */ - --terminal-bg: rgba(60, 45, 35, 0.9); - --button-bg: rgba(85, 65, 50, 1); - --input-field-bg: rgba(50, 35, 25, 0.7); - --button-selected-bg: rgba(230, 210, 180, 0.85); - --button-hover-bg: rgba(105, 80, 65, 0.7); - --scroll-bg: rgba(95, 75, 60, 1); - --scroll-inverse-bg: rgba(85, 65, 50, 1); - --scroll-active-bg: rgba(125, 100, 85, 1); - - /* Text & Foreground */ - --button-text: rgba(230, 210, 180, 0.9); - --button-selected-text: rgba(60, 45, 35, 1); - --button-hover-text: rgba(245, 230, 210, 1); - --input-text-color: rgba(230, 210, 180, 1.0); - --text-message: rgba(230, 210, 180, 1); - --text-warning: rgba(200, 150, 100, 1); - --text-input-echo: rgba(150, 120, 100, 1); - --text-shell: rgba(120, 100, 80, 1); - --text-error: rgba(190, 90, 90, 1); - - /* Other UI Elements */ - --scroll-color: rgba(150, 120, 100, 1); - --caret-color: rgba(230, 210, 180, 1.0); +.coffee-shop-theme { + /* Backgrounds */ + --terminal-bg: rgba(60, 45, 35, 0.9); + --button-bg: rgba(85, 65, 50, 1); + --input-field-bg: rgba(50, 35, 25, 0.7); + --button-selected-bg: rgba(230, 210, 180, 0.85); + --button-hover-bg: rgba(105, 80, 65, 0.7); + --scroll-bg: rgba(95, 75, 60, 1); + --scroll-inverse-bg: rgba(85, 65, 50, 1); + --scroll-active-bg: rgba(125, 100, 85, 1); + + /* Text & Foreground */ + --button-text: rgba(230, 210, 180, 0.9); + --button-selected-text: rgba(60, 45, 35, 1); + --button-hover-text: rgba(245, 230, 210, 1); + --input-text-color: rgba(230, 210, 180, 1.0); + --text-message: rgba(230, 210, 180, 1); + --text-warning: rgba(200, 150, 100, 1); + --text-input-echo: rgba(150, 120, 100, 1); + --text-shell: rgba(120, 100, 80, 1); + --text-error: rgba(190, 90, 90, 1); + + /* Other UI Elements */ + --scroll-color: rgba(150, 120, 100, 1); + --caret-color: rgba(230, 210, 180, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/Comodore64Theme.uss b/Styles/Themes/Comodore64Theme.uss index faae596..a5de578 100644 --- a/Styles/Themes/Comodore64Theme.uss +++ b/Styles/Themes/Comodore64Theme.uss @@ -1,26 +1,26 @@ -.commodore64-theme { - /* Backgrounds */ - --terminal-bg: rgba(60, 50, 160, 0.85); - --button-bg: rgba(80, 70, 180, 1); - --input-field-bg: rgba(50, 40, 140, 0.7); - --button-selected-bg: rgba(170, 160, 255, 0.85); - --button-hover-bg: rgba(100, 90, 200, 0.7); - --scroll-bg: rgba(90, 80, 190, 1); - --scroll-inverse-bg: rgba(80, 70, 180, 1); - --scroll-active-bg: rgba(120, 110, 210, 1); - - /* Text & Foreground */ - --button-text: rgba(170, 160, 255, 0.9); - --button-selected-text: rgba(60, 50, 160, 1); - --button-hover-text: rgba(200, 190, 255, 1); - --input-text-color: rgba(170, 160, 255, 1.0); - --text-message: rgba(170, 160, 255, 1); - --text-warning: rgba(220, 220, 220, 1); - --text-input-echo: rgba(130, 120, 220, 1); - --text-shell: rgba(100, 90, 200, 1); - --text-error: rgba(255, 120, 120, 1); - - /* Other UI Elements */ - --scroll-color: rgba(170, 160, 255, 1); - --caret-color: rgba(170, 160, 255, 1.0); +.commodore64-theme { + /* Backgrounds */ + --terminal-bg: rgba(60, 50, 160, 0.85); + --button-bg: rgba(80, 70, 180, 1); + --input-field-bg: rgba(50, 40, 140, 0.7); + --button-selected-bg: rgba(170, 160, 255, 0.85); + --button-hover-bg: rgba(100, 90, 200, 0.7); + --scroll-bg: rgba(90, 80, 190, 1); + --scroll-inverse-bg: rgba(80, 70, 180, 1); + --scroll-active-bg: rgba(120, 110, 210, 1); + + /* Text & Foreground */ + --button-text: rgba(170, 160, 255, 0.9); + --button-selected-text: rgba(60, 50, 160, 1); + --button-hover-text: rgba(200, 190, 255, 1); + --input-text-color: rgba(170, 160, 255, 1.0); + --text-message: rgba(170, 160, 255, 1); + --text-warning: rgba(220, 220, 220, 1); + --text-input-echo: rgba(130, 120, 220, 1); + --text-shell: rgba(100, 90, 200, 1); + --text-error: rgba(255, 120, 120, 1); + + /* Other UI Elements */ + --scroll-color: rgba(170, 160, 255, 1); + --caret-color: rgba(170, 160, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/CoolBlueTheme.uss b/Styles/Themes/CoolBlueTheme.uss index afe45bf..a84ff90 100644 --- a/Styles/Themes/CoolBlueTheme.uss +++ b/Styles/Themes/CoolBlueTheme.uss @@ -1,26 +1,26 @@ -.cool-blue-theme { - /* Backgrounds */ - --terminal-bg: rgba(10, 20, 35, 0.85); - --button-bg: rgba(20, 35, 55, 1); - --input-field-bg: rgba(5, 15, 30, 0.7); - --button-selected-bg: rgba(100, 180, 255, 0.85); - --button-hover-bg: rgba(40, 60, 80, 0.7); - --scroll-bg: rgba(48, 68, 88, 1); - --scroll-inverse-bg: rgba(39, 59, 79, 1); - --scroll-active-bg: rgba(80, 100, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(180, 220, 255, 0.9); - --button-selected-text: rgba(10, 20, 35, 1); - --button-hover-text: rgba(210, 235, 255, 1); - --input-text-color: rgba(200, 230, 255, 1.0); - --text-message: rgba(200, 230, 255, 1); - --text-warning: rgba(255, 200, 100, 1); - --text-input-echo: rgba(120, 190, 255, 1); - --text-shell: rgba(150, 180, 210, 1); - --text-error: rgba(255, 100, 100, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 180, 255, 1); - --caret-color: rgba(200, 230, 255, 1.0); +.cool-blue-theme { + /* Backgrounds */ + --terminal-bg: rgba(10, 20, 35, 0.85); + --button-bg: rgba(20, 35, 55, 1); + --input-field-bg: rgba(5, 15, 30, 0.7); + --button-selected-bg: rgba(100, 180, 255, 0.85); + --button-hover-bg: rgba(40, 60, 80, 0.7); + --scroll-bg: rgba(48, 68, 88, 1); + --scroll-inverse-bg: rgba(39, 59, 79, 1); + --scroll-active-bg: rgba(80, 100, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(180, 220, 255, 0.9); + --button-selected-text: rgba(10, 20, 35, 1); + --button-hover-text: rgba(210, 235, 255, 1); + --input-text-color: rgba(200, 230, 255, 1.0); + --text-message: rgba(200, 230, 255, 1); + --text-warning: rgba(255, 200, 100, 1); + --text-input-echo: rgba(120, 190, 255, 1); + --text-shell: rgba(150, 180, 210, 1); + --text-error: rgba(255, 100, 100, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 180, 255, 1); + --caret-color: rgba(200, 230, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/CoralReefTheme.uss b/Styles/Themes/CoralReefTheme.uss index 7fb892c..ccc0ce5 100644 --- a/Styles/Themes/CoralReefTheme.uss +++ b/Styles/Themes/CoralReefTheme.uss @@ -1,26 +1,26 @@ -.coral-reef-theme { - /* Backgrounds */ - --terminal-bg: rgba(180, 240, 255, 0.7); - --button-bg: rgba(150, 210, 230, 1); - --input-field-bg: rgba(210, 250, 255, 0.7); - --button-selected-bg: rgba(255, 110, 140, 0.85); - --button-hover-bg: rgba(120, 180, 200, 0.7); - --scroll-bg: rgba(140, 200, 220, 1); - --scroll-inverse-bg: rgba(150, 210, 230, 1); - --scroll-active-bg: rgba(100, 150, 170, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 80, 100, 0.9); - --button-selected-text: rgba(255, 240, 240, 1); - --button-hover-text: rgba(0, 60, 80, 1); - --input-text-color: rgba(0, 80, 100, 1.0); - --text-message: rgba(0, 80, 100, 1); - --text-warning: rgba(255, 200, 50, 1); - --text-input-echo: rgba(80, 150, 200, 1); - --text-shell: rgba(50, 120, 150, 1); - --text-error: rgba(200, 50, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 110, 140, 1); - --caret-color: rgba(0, 80, 100, 1.0); +.coral-reef-theme { + /* Backgrounds */ + --terminal-bg: rgba(180, 240, 255, 0.7); + --button-bg: rgba(150, 210, 230, 1); + --input-field-bg: rgba(210, 250, 255, 0.7); + --button-selected-bg: rgba(255, 110, 140, 0.85); + --button-hover-bg: rgba(120, 180, 200, 0.7); + --scroll-bg: rgba(140, 200, 220, 1); + --scroll-inverse-bg: rgba(150, 210, 230, 1); + --scroll-active-bg: rgba(100, 150, 170, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 80, 100, 0.9); + --button-selected-text: rgba(255, 240, 240, 1); + --button-hover-text: rgba(0, 60, 80, 1); + --input-text-color: rgba(0, 80, 100, 1.0); + --text-message: rgba(0, 80, 100, 1); + --text-warning: rgba(255, 200, 50, 1); + --text-input-echo: rgba(80, 150, 200, 1); + --text-shell: rgba(50, 120, 150, 1); + --text-error: rgba(200, 50, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 110, 140, 1); + --caret-color: rgba(0, 80, 100, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/CreamTheme.uss b/Styles/Themes/CreamTheme.uss index b0aec15..4ea6d1d 100644 --- a/Styles/Themes/CreamTheme.uss +++ b/Styles/Themes/CreamTheme.uss @@ -1,26 +1,26 @@ -.cream-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 250, 240, 0.7); - --button-bg: rgba(240, 235, 220, 1); - --input-field-bg: rgba(255, 255, 250, 0.75); - --button-selected-bg: rgba(120, 90, 70, 0.85); - --button-hover-bg: rgba(220, 215, 200, 0.7); - --scroll-bg: rgba(230, 225, 210, 1); - --scroll-inverse-bg: rgba(240, 235, 220, 1); - --scroll-active-bg: rgba(160, 140, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(90, 70, 50, 0.9); - --button-selected-text: rgba(255, 250, 240, 1); - --button-hover-text: rgba(70, 50, 30, 1); - --input-text-color: rgba(90, 70, 50, 1.0); - --text-message: rgba(90, 70, 50, 1); - --text-warning: rgba(210, 140, 80, 1); - --text-input-echo: rgba(140, 110, 90, 1); - --text-shell: rgba(160, 140, 120, 1); - --text-error: rgba(180, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(120, 90, 70, 1); - --caret-color: rgba(90, 70, 50, 1.0); +.cream-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 250, 240, 0.7); + --button-bg: rgba(240, 235, 220, 1); + --input-field-bg: rgba(255, 255, 250, 0.75); + --button-selected-bg: rgba(120, 90, 70, 0.85); + --button-hover-bg: rgba(220, 215, 200, 0.7); + --scroll-bg: rgba(230, 225, 210, 1); + --scroll-inverse-bg: rgba(240, 235, 220, 1); + --scroll-active-bg: rgba(160, 140, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(90, 70, 50, 0.9); + --button-selected-text: rgba(255, 250, 240, 1); + --button-hover-text: rgba(70, 50, 30, 1); + --input-text-color: rgba(90, 70, 50, 1.0); + --text-message: rgba(90, 70, 50, 1); + --text-warning: rgba(210, 140, 80, 1); + --text-input-echo: rgba(140, 110, 90, 1); + --text-shell: rgba(160, 140, 120, 1); + --text-error: rgba(180, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(120, 90, 70, 1); + --caret-color: rgba(90, 70, 50, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/CyberpunkNeonTheme.uss b/Styles/Themes/CyberpunkNeonTheme.uss index 50bb609..643b190 100644 --- a/Styles/Themes/CyberpunkNeonTheme.uss +++ b/Styles/Themes/CyberpunkNeonTheme.uss @@ -1,26 +1,26 @@ -.cyberpunk-neon-theme { - /* Backgrounds */ - --terminal-bg: rgba(21, 21, 31, 0.9); - --button-bg: rgba(41, 41, 61, 1); - --input-field-bg: rgba(31, 31, 51, 0.8); - --button-selected-bg: rgba(236, 0, 140, 0.85); - --button-hover-bg: rgba(61, 61, 81, 1); - --scroll-bg: rgba(41, 41, 61, 1); - --scroll-inverse-bg: rgba(11, 11, 21, 1); - --scroll-active-bg: rgba(0, 255, 255, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 255, 255, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(80, 255, 255, 1); - --input-text-color: rgba(0, 255, 255, 1); - --text-message: rgba(220, 220, 230, 1); - --text-warning: rgba(255, 232, 31, 1); - --text-input-echo: rgba(80, 255, 255, 1); - --text-shell: rgba(150, 150, 180, 1); - --text-error: rgba(255, 0, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(0, 255, 255, 1); - --caret-color: rgba(236, 0, 140, 1); +.cyberpunk-neon-theme { + /* Backgrounds */ + --terminal-bg: rgba(21, 21, 31, 0.9); + --button-bg: rgba(41, 41, 61, 1); + --input-field-bg: rgba(31, 31, 51, 0.8); + --button-selected-bg: rgba(236, 0, 140, 0.85); + --button-hover-bg: rgba(61, 61, 81, 1); + --scroll-bg: rgba(41, 41, 61, 1); + --scroll-inverse-bg: rgba(11, 11, 21, 1); + --scroll-active-bg: rgba(0, 255, 255, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 255, 255, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(80, 255, 255, 1); + --input-text-color: rgba(0, 255, 255, 1); + --text-message: rgba(220, 220, 230, 1); + --text-warning: rgba(255, 232, 31, 1); + --text-input-echo: rgba(80, 255, 255, 1); + --text-shell: rgba(150, 150, 180, 1); + --text-error: rgba(255, 0, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(0, 255, 255, 1); + --caret-color: rgba(236, 0, 140, 1); } \ No newline at end of file diff --git a/Styles/Themes/DarculaTheme.uss b/Styles/Themes/DarculaTheme.uss index 0d4a51e..24e4f55 100644 --- a/Styles/Themes/DarculaTheme.uss +++ b/Styles/Themes/DarculaTheme.uss @@ -1,26 +1,26 @@ -.darcula-theme { - /* Backgrounds */ - --terminal-bg: rgba(43, 43, 43, 0.9); - --button-bg: rgba(60, 63, 65, 1); - --input-field-bg: rgba(50, 50, 50, 0.8); - --button-selected-bg: rgba(75, 110, 175, 0.85); - --button-hover-bg: rgba(75, 78, 80, 1); - --scroll-bg: rgba(60, 63, 65, 1); - --scroll-inverse-bg: rgba(100, 100, 100, 1); - --scroll-active-bg: rgba(120, 120, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(187, 187, 187, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(210, 210, 210, 1); - --input-text-color: rgba(187, 187, 187, 1.0); - --text-message: rgba(169, 183, 198, 1); - --text-warning: rgba(255, 198, 109, 1); - --text-input-echo: rgba(104, 151, 187, 1); - --text-shell: rgba(128, 128, 128, 1); - --text-error: rgba(255, 107, 104, 1); - - /* Other UI Elements */ - --scroll-color: rgba(187, 187, 187, 1); - --caret-color: rgba(187, 187, 187, 1.0); +.darcula-theme { + /* Backgrounds */ + --terminal-bg: rgba(43, 43, 43, 0.9); + --button-bg: rgba(60, 63, 65, 1); + --input-field-bg: rgba(50, 50, 50, 0.8); + --button-selected-bg: rgba(75, 110, 175, 0.85); + --button-hover-bg: rgba(75, 78, 80, 1); + --scroll-bg: rgba(60, 63, 65, 1); + --scroll-inverse-bg: rgba(100, 100, 100, 1); + --scroll-active-bg: rgba(120, 120, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(187, 187, 187, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(210, 210, 210, 1); + --input-text-color: rgba(187, 187, 187, 1.0); + --text-message: rgba(169, 183, 198, 1); + --text-warning: rgba(255, 198, 109, 1); + --text-input-echo: rgba(104, 151, 187, 1); + --text-shell: rgba(128, 128, 128, 1); + --text-error: rgba(255, 107, 104, 1); + + /* Other UI Elements */ + --scroll-color: rgba(187, 187, 187, 1); + --caret-color: rgba(187, 187, 187, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/DarkTheme.uss b/Styles/Themes/DarkTheme.uss index 2ed6aff..5b8f8bb 100644 --- a/Styles/Themes/DarkTheme.uss +++ b/Styles/Themes/DarkTheme.uss @@ -1,26 +1,26 @@ -.dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(28, 28, 30, 0.9); - --button-bg: rgba(58, 58, 60, 1); - --input-field-bg: rgba(44, 44, 46, 0.8); - --button-selected-bg: rgba(0, 122, 255, 0.85); - --button-hover-bg: rgba(72, 72, 74, 1); - --scroll-bg: rgba(58, 58, 60, 1); - --scroll-inverse-bg: rgba(90, 90, 90, 1); - --scroll-active-bg: rgba(110, 110, 110, 1); - - /* Text & Foreground */ - --button-text: rgba(242, 242, 247, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(242, 242, 247, 1); - --input-text-color: rgba(242, 242, 247, 1.0); - --text-message: rgba(242, 242, 247, 1); - --text-warning: rgba(255, 204, 0, 1); - --text-input-echo: rgba(50, 173, 230, 1); - --text-shell: rgba(142, 142, 147, 1); - --text-error: rgba(255, 69, 58, 1); - - /* Other UI Elements */ - --scroll-color: rgba(242, 242, 247, 1); - --caret-color: rgba(242, 242, 247, 1.0); +.dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(28, 28, 30, 0.9); + --button-bg: rgba(58, 58, 60, 1); + --input-field-bg: rgba(44, 44, 46, 0.8); + --button-selected-bg: rgba(0, 122, 255, 0.85); + --button-hover-bg: rgba(72, 72, 74, 1); + --scroll-bg: rgba(58, 58, 60, 1); + --scroll-inverse-bg: rgba(90, 90, 90, 1); + --scroll-active-bg: rgba(110, 110, 110, 1); + + /* Text & Foreground */ + --button-text: rgba(242, 242, 247, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(242, 242, 247, 1); + --input-text-color: rgba(242, 242, 247, 1.0); + --text-message: rgba(242, 242, 247, 1); + --text-warning: rgba(255, 204, 0, 1); + --text-input-echo: rgba(50, 173, 230, 1); + --text-shell: rgba(142, 142, 147, 1); + --text-error: rgba(255, 69, 58, 1); + + /* Other UI Elements */ + --scroll-color: rgba(242, 242, 247, 1); + --caret-color: rgba(242, 242, 247, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/DeepForestTheme.uss b/Styles/Themes/DeepForestTheme.uss index b7f8de9..17a96bd 100644 --- a/Styles/Themes/DeepForestTheme.uss +++ b/Styles/Themes/DeepForestTheme.uss @@ -1,26 +1,26 @@ -.deep-forest-theme { - /* Backgrounds */ - --terminal-bg: rgba(20, 35, 25, 0.85); - --button-bg: rgba(35, 55, 40, 1); - --input-field-bg: rgba(15, 30, 20, 0.7); - --button-selected-bg: rgba(100, 180, 120, 0.85); - --button-hover-bg: rgba(50, 80, 60, 0.7); - --scroll-bg: rgba(68, 88, 78, 1); - --scroll-inverse-bg: rgba(59, 79, 69, 1); - --scroll-active-bg: rgba(100, 120, 110, 1); - - /* Text & Foreground */ - --button-text: rgba(180, 220, 190, 0.9); - --button-selected-text: rgba(20, 35, 25, 1); - --button-hover-text: rgba(210, 235, 215, 1); - --input-text-color: rgba(200, 230, 205, 1.0); - --text-message: rgba(200, 230, 205, 1); - --text-warning: rgba(210, 160, 80, 1); - --text-input-echo: rgba(120, 190, 140, 1); - --text-shell: rgba(150, 180, 160, 1); - --text-error: rgba(220, 90, 90, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 180, 120, 1); - --caret-color: rgba(200, 230, 205, 1.0); +.deep-forest-theme { + /* Backgrounds */ + --terminal-bg: rgba(20, 35, 25, 0.85); + --button-bg: rgba(35, 55, 40, 1); + --input-field-bg: rgba(15, 30, 20, 0.7); + --button-selected-bg: rgba(100, 180, 120, 0.85); + --button-hover-bg: rgba(50, 80, 60, 0.7); + --scroll-bg: rgba(68, 88, 78, 1); + --scroll-inverse-bg: rgba(59, 79, 69, 1); + --scroll-active-bg: rgba(100, 120, 110, 1); + + /* Text & Foreground */ + --button-text: rgba(180, 220, 190, 0.9); + --button-selected-text: rgba(20, 35, 25, 1); + --button-hover-text: rgba(210, 235, 215, 1); + --input-text-color: rgba(200, 230, 205, 1.0); + --text-message: rgba(200, 230, 205, 1); + --text-warning: rgba(210, 160, 80, 1); + --text-input-echo: rgba(120, 190, 140, 1); + --text-shell: rgba(150, 180, 160, 1); + --text-error: rgba(220, 90, 90, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 180, 120, 1); + --caret-color: rgba(200, 230, 205, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/DesertNightTheme.uss b/Styles/Themes/DesertNightTheme.uss index 8cc16c9..0e70eb0 100644 --- a/Styles/Themes/DesertNightTheme.uss +++ b/Styles/Themes/DesertNightTheme.uss @@ -1,26 +1,26 @@ -.desert-night-theme { - /* Backgrounds */ - --terminal-bg: rgba(45, 35, 30, 0.8); - --button-bg: rgba(70, 55, 45, 1); - --input-field-bg: rgba(35, 25, 20, 0.7); - --button-selected-bg: rgba(217, 136, 60, 0.85); - --button-hover-bg: rgba(90, 75, 65, 0.7); - --scroll-bg: rgba(88, 68, 58, 1); - --scroll-inverse-bg: rgba(79, 59, 49, 1); - --scroll-active-bg: rgba(120, 100, 90, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 200, 180, 0.9); - --button-selected-text: rgba(45, 35, 30, 1); - --button-hover-text: rgba(235, 215, 195, 1); - --input-text-color: rgba(220, 200, 180, 1.0); - --text-message: rgba(220, 200, 180, 1); - --text-warning: rgba(255, 180, 90, 1); - --text-input-echo: rgba(180, 150, 130, 1); - --text-shell: rgba(160, 140, 120, 1); - --text-error: rgba(230, 100, 100, 1); - - /* Other UI Elements */ - --scroll-color: rgba(217, 136, 60, 1); - --caret-color: rgba(220, 200, 180, 1.0); +.desert-night-theme { + /* Backgrounds */ + --terminal-bg: rgba(45, 35, 30, 0.8); + --button-bg: rgba(70, 55, 45, 1); + --input-field-bg: rgba(35, 25, 20, 0.7); + --button-selected-bg: rgba(217, 136, 60, 0.85); + --button-hover-bg: rgba(90, 75, 65, 0.7); + --scroll-bg: rgba(88, 68, 58, 1); + --scroll-inverse-bg: rgba(79, 59, 49, 1); + --scroll-active-bg: rgba(120, 100, 90, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 200, 180, 0.9); + --button-selected-text: rgba(45, 35, 30, 1); + --button-hover-text: rgba(235, 215, 195, 1); + --input-text-color: rgba(220, 200, 180, 1.0); + --text-message: rgba(220, 200, 180, 1); + --text-warning: rgba(255, 180, 90, 1); + --text-input-echo: rgba(180, 150, 130, 1); + --text-shell: rgba(160, 140, 120, 1); + --text-error: rgba(230, 100, 100, 1); + + /* Other UI Elements */ + --scroll-color: rgba(217, 136, 60, 1); + --caret-color: rgba(220, 200, 180, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/DesertOasisTheme.uss b/Styles/Themes/DesertOasisTheme.uss index 1cd48ac..5b26688 100644 --- a/Styles/Themes/DesertOasisTheme.uss +++ b/Styles/Themes/DesertOasisTheme.uss @@ -1,26 +1,26 @@ -.desert-oasis-theme { - /* Backgrounds */ - --terminal-bg: rgba(248, 236, 212, 0.9); - --button-bg: rgba(228, 210, 180, 1); - --input-field-bg: rgba(238, 223, 196, 0.8); - --button-selected-bg: rgba(80, 160, 170, 0.85); - --button-hover-bg: rgba(208, 190, 160, 1); - --scroll-bg: rgba(228, 210, 180, 1); - --scroll-inverse-bg: rgba(255, 248, 232, 1); - --scroll-active-bg: rgba(180, 160, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(88, 70, 50, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(68, 50, 30, 1); - --input-text-color: rgba(88, 70, 50, 1); - --text-message: rgba(88, 70, 50, 1); - --text-warning: rgba(200, 120, 40, 1); - --text-input-echo: rgba(60, 140, 150, 1); - --text-shell: rgba(140, 120, 95, 1); - --text-error: rgba(190, 60, 50, 1); - - /* Other UI Elements */ - --scroll-color: rgba(88, 70, 50, 1); - --caret-color: rgba(80, 160, 170, 1); +.desert-oasis-theme { + /* Backgrounds */ + --terminal-bg: rgba(248, 236, 212, 0.9); + --button-bg: rgba(228, 210, 180, 1); + --input-field-bg: rgba(238, 223, 196, 0.8); + --button-selected-bg: rgba(80, 160, 170, 0.85); + --button-hover-bg: rgba(208, 190, 160, 1); + --scroll-bg: rgba(228, 210, 180, 1); + --scroll-inverse-bg: rgba(255, 248, 232, 1); + --scroll-active-bg: rgba(180, 160, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(88, 70, 50, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(68, 50, 30, 1); + --input-text-color: rgba(88, 70, 50, 1); + --text-message: rgba(88, 70, 50, 1); + --text-warning: rgba(200, 120, 40, 1); + --text-input-echo: rgba(60, 140, 150, 1); + --text-shell: rgba(140, 120, 95, 1); + --text-error: rgba(190, 60, 50, 1); + + /* Other UI Elements */ + --scroll-color: rgba(88, 70, 50, 1); + --caret-color: rgba(80, 160, 170, 1); } \ No newline at end of file diff --git a/Styles/Themes/DimmedTheme.uss b/Styles/Themes/DimmedTheme.uss index b80bbc6..2802dc8 100644 --- a/Styles/Themes/DimmedTheme.uss +++ b/Styles/Themes/DimmedTheme.uss @@ -1,26 +1,26 @@ -.dimmed-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 40, 45, 0.9); - --button-bg: rgba(55, 55, 60, 1); - --input-field-bg: rgba(30, 30, 35, 0.7); - --button-selected-bg: rgba(90, 90, 95, 0.85); - --button-hover-bg: rgba(70, 70, 75, 0.7); - --scroll-bg: rgba(60, 60, 65, 1); - --scroll-inverse-bg: rgba(55, 55, 60, 1); - --scroll-active-bg: rgba(80, 80, 85, 1); - - /* Text & Foreground */ - --button-text: rgba(120, 120, 125, 0.9); - --button-selected-text: rgba(40, 40, 45, 1); - --button-hover-text: rgba(140, 140, 145, 1); - --input-text-color: rgba(120, 120, 125, 1.0); - --text-message: rgba(120, 120, 125, 1); - --text-warning: rgba(110, 110, 100, 1); - --text-input-echo: rgba(100, 100, 115, 1); - --text-shell: rgba(90, 90, 95, 1); - --text-error: rgba(130, 90, 90, 1); - - /* Other UI Elements */ - --scroll-color: rgba(90, 90, 95, 1); - --caret-color: rgba(120, 120, 125, 1.0); +.dimmed-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 40, 45, 0.9); + --button-bg: rgba(55, 55, 60, 1); + --input-field-bg: rgba(30, 30, 35, 0.7); + --button-selected-bg: rgba(90, 90, 95, 0.85); + --button-hover-bg: rgba(70, 70, 75, 0.7); + --scroll-bg: rgba(60, 60, 65, 1); + --scroll-inverse-bg: rgba(55, 55, 60, 1); + --scroll-active-bg: rgba(80, 80, 85, 1); + + /* Text & Foreground */ + --button-text: rgba(120, 120, 125, 0.9); + --button-selected-text: rgba(40, 40, 45, 1); + --button-hover-text: rgba(140, 140, 145, 1); + --input-text-color: rgba(120, 120, 125, 1.0); + --text-message: rgba(120, 120, 125, 1); + --text-warning: rgba(110, 110, 100, 1); + --text-input-echo: rgba(100, 100, 115, 1); + --text-shell: rgba(90, 90, 95, 1); + --text-error: rgba(130, 90, 90, 1); + + /* Other UI Elements */ + --scroll-color: rgba(90, 90, 95, 1); + --caret-color: rgba(120, 120, 125, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/DoomEternalTheme.uss b/Styles/Themes/DoomEternalTheme.uss index 6dcd8c8..365a0a6 100644 --- a/Styles/Themes/DoomEternalTheme.uss +++ b/Styles/Themes/DoomEternalTheme.uss @@ -1,26 +1,26 @@ -.doom-eternal-theme { - /* Backgrounds */ - --terminal-bg: rgba(30, 15, 10, 0.9); - --button-bg: rgba(60, 30, 20, 1); - --input-field-bg: rgba(45, 22, 15, 0.8); - --button-selected-bg: rgba(255, 100, 0, 0.85); - --button-hover-bg: rgba(80, 40, 30, 1); - --scroll-bg: rgba(60, 30, 20, 1); - --scroll-inverse-bg: rgba(15, 7, 5, 1); - --scroll-active-bg: rgba(180, 50, 20, 1); - - /* Text & Foreground */ - --button-text: rgba(200, 180, 170, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(220, 200, 190, 1); - --input-text-color: rgba(200, 180, 170, 1); - --text-message: rgba(200, 180, 170, 1); - --text-warning: rgba(255, 165, 0, 1); - --text-input-echo: rgba(100, 200, 255, 1); - --text-shell: rgba(150, 130, 120, 1); - --text-error: rgba(255, 0, 0, 1); - - /* Other UI Elements */ - --scroll-color: rgba(200, 180, 170, 1); - --caret-color: rgba(255, 100, 0, 1); +.doom-eternal-theme { + /* Backgrounds */ + --terminal-bg: rgba(30, 15, 10, 0.9); + --button-bg: rgba(60, 30, 20, 1); + --input-field-bg: rgba(45, 22, 15, 0.8); + --button-selected-bg: rgba(255, 100, 0, 0.85); + --button-hover-bg: rgba(80, 40, 30, 1); + --scroll-bg: rgba(60, 30, 20, 1); + --scroll-inverse-bg: rgba(15, 7, 5, 1); + --scroll-active-bg: rgba(180, 50, 20, 1); + + /* Text & Foreground */ + --button-text: rgba(200, 180, 170, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(220, 200, 190, 1); + --input-text-color: rgba(200, 180, 170, 1); + --text-message: rgba(200, 180, 170, 1); + --text-warning: rgba(255, 165, 0, 1); + --text-input-echo: rgba(100, 200, 255, 1); + --text-shell: rgba(150, 130, 120, 1); + --text-error: rgba(255, 0, 0, 1); + + /* Other UI Elements */ + --scroll-color: rgba(200, 180, 170, 1); + --caret-color: rgba(255, 100, 0, 1); } \ No newline at end of file diff --git a/Styles/Themes/DraculaTheme.uss b/Styles/Themes/DraculaTheme.uss index eac8b81..468dfce 100644 --- a/Styles/Themes/DraculaTheme.uss +++ b/Styles/Themes/DraculaTheme.uss @@ -1,26 +1,26 @@ -.dracula-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 42, 54, 0.9); - --button-bg: rgba(68, 71, 90, 1); - --input-field-bg: rgba(30, 31, 41, 0.8); - --button-selected-bg: rgba(189, 147, 249, 0.85); - --button-hover-bg: rgba(85, 89, 112, 1); - --scroll-bg: rgba(68, 71, 90, 1); - --scroll-inverse-bg: rgba(98, 114, 164, 1); - --scroll-active-bg: rgba(118, 138, 196, 1); - - /* Text & Foreground */ - --button-text: rgba(248, 248, 242, 0.9); - --button-selected-text: rgba(40, 42, 54, 1); - --button-hover-text: rgba(248, 248, 242, 1); - --input-text-color: rgba(248, 248, 242, 1.0); - --text-message: rgba(248, 248, 242, 1); - --text-warning: rgba(241, 250, 140, 1); - --text-input-echo: rgba(139, 233, 253, 1); - --text-shell: rgba(98, 114, 164, 1); - --text-error: rgba(255, 85, 85, 1); - - /* Other UI Elements */ - --scroll-color: rgba(248, 248, 242, 1); - --caret-color: rgba(248, 248, 242, 1.0); +.dracula-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 42, 54, 0.9); + --button-bg: rgba(68, 71, 90, 1); + --input-field-bg: rgba(30, 31, 41, 0.8); + --button-selected-bg: rgba(189, 147, 249, 0.85); + --button-hover-bg: rgba(85, 89, 112, 1); + --scroll-bg: rgba(68, 71, 90, 1); + --scroll-inverse-bg: rgba(98, 114, 164, 1); + --scroll-active-bg: rgba(118, 138, 196, 1); + + /* Text & Foreground */ + --button-text: rgba(248, 248, 242, 0.9); + --button-selected-text: rgba(40, 42, 54, 1); + --button-hover-text: rgba(248, 248, 242, 1); + --input-text-color: rgba(248, 248, 242, 1.0); + --text-message: rgba(248, 248, 242, 1); + --text-warning: rgba(241, 250, 140, 1); + --text-input-echo: rgba(139, 233, 253, 1); + --text-shell: rgba(98, 114, 164, 1); + --text-error: rgba(255, 85, 85, 1); + + /* Other UI Elements */ + --scroll-color: rgba(248, 248, 242, 1); + --caret-color: rgba(248, 248, 242, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/DuskTheme.uss b/Styles/Themes/DuskTheme.uss index 6d087e0..ff06a3a 100644 --- a/Styles/Themes/DuskTheme.uss +++ b/Styles/Themes/DuskTheme.uss @@ -1,26 +1,26 @@ -.dusk-theme { - /* Backgrounds */ - --terminal-bg: rgba(30, 30, 60, 0.9); - --button-bg: rgba(50, 50, 80, 1); - --input-field-bg: rgba(20, 20, 50, 0.7); - --button-selected-bg: rgba(255, 160, 100, 0.85); - --button-hover-bg: rgba(70, 70, 100, 0.7); - --scroll-bg: rgba(60, 60, 90, 1); - --scroll-inverse-bg: rgba(50, 50, 80, 1); - --scroll-active-bg: rgba(90, 90, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 180, 240, 0.9); - --button-selected-text: rgba(30, 30, 60, 1); - --button-hover-text: rgba(240, 210, 255, 1); - --input-text-color: rgba(220, 180, 240, 1.0); - --text-message: rgba(220, 180, 240, 1); - --text-warning: rgba(255, 200, 140, 1); - --text-input-echo: rgba(180, 150, 210, 1); - --text-shell: rgba(140, 120, 170, 1); - --text-error: rgba(255, 120, 140, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 160, 100, 1); - --caret-color: rgba(220, 180, 240, 1.0); +.dusk-theme { + /* Backgrounds */ + --terminal-bg: rgba(30, 30, 60, 0.9); + --button-bg: rgba(50, 50, 80, 1); + --input-field-bg: rgba(20, 20, 50, 0.7); + --button-selected-bg: rgba(255, 160, 100, 0.85); + --button-hover-bg: rgba(70, 70, 100, 0.7); + --scroll-bg: rgba(60, 60, 90, 1); + --scroll-inverse-bg: rgba(50, 50, 80, 1); + --scroll-active-bg: rgba(90, 90, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 180, 240, 0.9); + --button-selected-text: rgba(30, 30, 60, 1); + --button-hover-text: rgba(240, 210, 255, 1); + --input-text-color: rgba(220, 180, 240, 1.0); + --text-message: rgba(220, 180, 240, 1); + --text-warning: rgba(255, 200, 140, 1); + --text-input-echo: rgba(180, 150, 210, 1); + --text-shell: rgba(140, 120, 170, 1); + --text-error: rgba(255, 120, 140, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 160, 100, 1); + --caret-color: rgba(220, 180, 240, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/EldritchHorrorTheme.uss b/Styles/Themes/EldritchHorrorTheme.uss index 37430d5..094e053 100644 --- a/Styles/Themes/EldritchHorrorTheme.uss +++ b/Styles/Themes/EldritchHorrorTheme.uss @@ -1,26 +1,26 @@ -.eldritch-horror-theme { - /* Backgrounds */ - --terminal-bg: rgba(30, 45, 40, 0.92); - --button-bg: rgba(45, 65, 55, 1); - --input-field-bg: rgba(20, 35, 30, 0.7); - --button-selected-bg: rgba(100, 40, 60, 0.85); - --button-hover-bg: rgba(60, 80, 70, 0.7); - --scroll-bg: rgba(55, 75, 65, 1); - --scroll-inverse-bg: rgba(45, 65, 55, 1); - --scroll-active-bg: rgba(80, 100, 90, 1); - - /* Text & Foreground */ - --button-text: rgba(160, 180, 170, 0.9); - --button-selected-text: rgba(210, 190, 200, 1); - --button-hover-text: rgba(190, 210, 200, 1); - --input-text-color: rgba(160, 180, 170, 1.0); - --text-message: rgba(160, 180, 170, 1); - --text-warning: rgba(140, 120, 160, 1); - --text-input-echo: rgba(110, 130, 120, 1); - --text-shell: rgba(90, 110, 100, 1); - --text-error: rgba(180, 60, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 40, 60, 1); - --caret-color: rgba(140, 120, 160, 1.0); +.eldritch-horror-theme { + /* Backgrounds */ + --terminal-bg: rgba(30, 45, 40, 0.92); + --button-bg: rgba(45, 65, 55, 1); + --input-field-bg: rgba(20, 35, 30, 0.7); + --button-selected-bg: rgba(100, 40, 60, 0.85); + --button-hover-bg: rgba(60, 80, 70, 0.7); + --scroll-bg: rgba(55, 75, 65, 1); + --scroll-inverse-bg: rgba(45, 65, 55, 1); + --scroll-active-bg: rgba(80, 100, 90, 1); + + /* Text & Foreground */ + --button-text: rgba(160, 180, 170, 0.9); + --button-selected-text: rgba(210, 190, 200, 1); + --button-hover-text: rgba(190, 210, 200, 1); + --input-text-color: rgba(160, 180, 170, 1.0); + --text-message: rgba(160, 180, 170, 1); + --text-warning: rgba(140, 120, 160, 1); + --text-input-echo: rgba(110, 130, 120, 1); + --text-shell: rgba(90, 110, 100, 1); + --text-error: rgba(180, 60, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 40, 60, 1); + --caret-color: rgba(140, 120, 160, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/EverforestDarkTheme.uss b/Styles/Themes/EverforestDarkTheme.uss index ea9d4b8..5c75fb4 100644 --- a/Styles/Themes/EverforestDarkTheme.uss +++ b/Styles/Themes/EverforestDarkTheme.uss @@ -1,26 +1,26 @@ -.everforest-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(45, 51, 59, 0.9); - --button-bg: rgba(54, 61, 71, 1); - --input-field-bg: rgba(38, 43, 51, 0.8); - --button-selected-bg: rgba(167, 190, 121, 0.85); - --button-hover-bg: rgba(64, 71, 82, 1); - --scroll-bg: rgba(54, 61, 71, 1); - --scroll-inverse-bg: rgba(84, 91, 102, 1); - --scroll-active-bg: rgba(100, 107, 117, 1); - - /* Text & Foreground */ - --button-text: rgba(211, 208, 191, 0.9); - --button-selected-text: rgba(45, 51, 59, 1); - --button-hover-text: rgba(211, 208, 191, 1); - --input-text-color: rgba(211, 208, 191, 1.0); - --text-message: rgba(211, 208, 191, 1); - --text-warning: rgba(219, 188, 100, 1); - --text-input-echo: rgba(122, 162, 210, 1); - --text-shell: rgba(148, 142, 130, 1); - --text-error: rgba(228, 104, 104, 1); - - /* Other UI Elements */ - --scroll-color: rgba(211, 208, 191, 1); - --caret-color: rgba(211, 208, 191, 1.0); +.everforest-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(45, 51, 59, 0.9); + --button-bg: rgba(54, 61, 71, 1); + --input-field-bg: rgba(38, 43, 51, 0.8); + --button-selected-bg: rgba(167, 190, 121, 0.85); + --button-hover-bg: rgba(64, 71, 82, 1); + --scroll-bg: rgba(54, 61, 71, 1); + --scroll-inverse-bg: rgba(84, 91, 102, 1); + --scroll-active-bg: rgba(100, 107, 117, 1); + + /* Text & Foreground */ + --button-text: rgba(211, 208, 191, 0.9); + --button-selected-text: rgba(45, 51, 59, 1); + --button-hover-text: rgba(211, 208, 191, 1); + --input-text-color: rgba(211, 208, 191, 1.0); + --text-message: rgba(211, 208, 191, 1); + --text-warning: rgba(219, 188, 100, 1); + --text-input-echo: rgba(122, 162, 210, 1); + --text-shell: rgba(148, 142, 130, 1); + --text-error: rgba(228, 104, 104, 1); + + /* Other UI Elements */ + --scroll-color: rgba(211, 208, 191, 1); + --caret-color: rgba(211, 208, 191, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/FlatUITheme.uss b/Styles/Themes/FlatUITheme.uss index e694c9e..4d65294 100644 --- a/Styles/Themes/FlatUITheme.uss +++ b/Styles/Themes/FlatUITheme.uss @@ -1,26 +1,26 @@ -.flat-ui-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(236, 240, 241, 0.9); - --button-bg: rgba(189, 195, 199, 1); - --input-field-bg: rgba(210, 215, 218, 0.8); - --button-selected-bg: rgba(52, 152, 219, 0.85); - --button-hover-bg: rgba(149, 165, 166, 1); - --scroll-bg: rgba(189, 195, 199, 1); - --scroll-inverse-bg: rgba(245, 250, 251, 1); - --scroll-active-bg: rgba(127, 140, 141, 1); - - /* Text & Foreground */ - --button-text: rgba(44, 62, 80, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(44, 62, 80, 1); - --text-message: rgba(44, 62, 80, 1); - --text-warning: rgba(241, 196, 15, 1); - --text-input-echo: rgba(26, 188, 156, 1); - --text-shell: rgba(127, 140, 141, 1); - --text-error: rgba(231, 76, 60, 1); - - /* Other UI Elements */ - --scroll-color: rgba(44, 62, 80, 1); - --caret-color: rgba(44, 62, 80, 1); +.flat-ui-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(236, 240, 241, 0.9); + --button-bg: rgba(189, 195, 199, 1); + --input-field-bg: rgba(210, 215, 218, 0.8); + --button-selected-bg: rgba(52, 152, 219, 0.85); + --button-hover-bg: rgba(149, 165, 166, 1); + --scroll-bg: rgba(189, 195, 199, 1); + --scroll-inverse-bg: rgba(245, 250, 251, 1); + --scroll-active-bg: rgba(127, 140, 141, 1); + + /* Text & Foreground */ + --button-text: rgba(44, 62, 80, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(44, 62, 80, 1); + --text-message: rgba(44, 62, 80, 1); + --text-warning: rgba(241, 196, 15, 1); + --text-input-echo: rgba(26, 188, 156, 1); + --text-shell: rgba(127, 140, 141, 1); + --text-error: rgba(231, 76, 60, 1); + + /* Other UI Elements */ + --scroll-color: rgba(44, 62, 80, 1); + --caret-color: rgba(44, 62, 80, 1); } \ No newline at end of file diff --git a/Styles/Themes/ForestFloorTheme.uss b/Styles/Themes/ForestFloorTheme.uss index fce7508..f0e6e61 100644 --- a/Styles/Themes/ForestFloorTheme.uss +++ b/Styles/Themes/ForestFloorTheme.uss @@ -1,26 +1,26 @@ -.forest-floor-theme { - /* Backgrounds */ - --terminal-bg: rgba(50, 45, 35, 0.88); - --button-bg: rgba(70, 65, 50, 1); - --input-field-bg: rgba(40, 35, 25, 0.7); - --button-selected-bg: rgba(140, 190, 100, 0.85); - --button-hover-bg: rgba(90, 85, 70, 0.7); - --scroll-bg: rgba(80, 75, 60, 1); - --scroll-inverse-bg: rgba(70, 65, 50, 1); - --scroll-active-bg: rgba(110, 105, 90, 1); - - /* Text & Foreground */ - --button-text: rgba(200, 210, 180, 0.9); - --button-selected-text: rgba(50, 45, 35, 1); - --button-hover-text: rgba(220, 230, 200, 1); - --input-text-color: rgba(200, 210, 180, 1.0); - --text-message: rgba(200, 210, 180, 1); - --text-warning: rgba(210, 160, 80, 1); - --text-input-echo: rgba(100, 150, 90, 1); - --text-shell: rgba(150, 150, 130, 1); - --text-error: rgba(200, 90, 90, 1); - - /* Other UI Elements */ - --scroll-color: rgba(140, 190, 100, 1); - --caret-color: rgba(200, 210, 180, 1.0); +.forest-floor-theme { + /* Backgrounds */ + --terminal-bg: rgba(50, 45, 35, 0.88); + --button-bg: rgba(70, 65, 50, 1); + --input-field-bg: rgba(40, 35, 25, 0.7); + --button-selected-bg: rgba(140, 190, 100, 0.85); + --button-hover-bg: rgba(90, 85, 70, 0.7); + --scroll-bg: rgba(80, 75, 60, 1); + --scroll-inverse-bg: rgba(70, 65, 50, 1); + --scroll-active-bg: rgba(110, 105, 90, 1); + + /* Text & Foreground */ + --button-text: rgba(200, 210, 180, 0.9); + --button-selected-text: rgba(50, 45, 35, 1); + --button-hover-text: rgba(220, 230, 200, 1); + --input-text-color: rgba(200, 210, 180, 1.0); + --text-message: rgba(200, 210, 180, 1); + --text-warning: rgba(210, 160, 80, 1); + --text-input-echo: rgba(100, 150, 90, 1); + --text-shell: rgba(150, 150, 130, 1); + --text-error: rgba(200, 90, 90, 1); + + /* Other UI Elements */ + --scroll-color: rgba(140, 190, 100, 1); + --caret-color: rgba(200, 210, 180, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/ForestNightTheme.uss b/Styles/Themes/ForestNightTheme.uss index 152ac8b..0f51eea 100644 --- a/Styles/Themes/ForestNightTheme.uss +++ b/Styles/Themes/ForestNightTheme.uss @@ -1,26 +1,26 @@ -.forest-night-theme { - /* Backgrounds */ - --terminal-bg: rgba(33, 40, 34, 0.9); - --button-bg: rgba(50, 60, 51, 1); - --input-field-bg: rgba(42, 50, 43, 0.8); - --button-selected-bg: rgba(162, 203, 130, 0.85); - --button-hover-bg: rgba(68, 78, 69, 1); - --scroll-bg: rgba(50, 60, 51, 1); - --scroll-inverse-bg: rgba(25, 30, 26, 1); - --scroll-active-bg: rgba(113, 143, 103, 1); - - /* Text & Foreground */ - --button-text: rgba(212, 222, 207, 0.9); - --button-selected-text: rgba(33, 40, 34, 1); - --button-hover-text: rgba(232, 242, 227, 1); - --input-text-color: rgba(212, 222, 207, 1); - --text-message: rgba(212, 222, 207, 1); - --text-warning: rgba(228, 194, 119, 1); - --text-input-echo: rgba(130, 174, 204, 1); - --text-shell: rgba(113, 143, 103, 1); - --text-error: rgba(215, 119, 119, 1); - - /* Other UI Elements */ - --scroll-color: rgba(212, 222, 207, 1); - --caret-color: rgba(212, 222, 207, 1); +.forest-night-theme { + /* Backgrounds */ + --terminal-bg: rgba(33, 40, 34, 0.9); + --button-bg: rgba(50, 60, 51, 1); + --input-field-bg: rgba(42, 50, 43, 0.8); + --button-selected-bg: rgba(162, 203, 130, 0.85); + --button-hover-bg: rgba(68, 78, 69, 1); + --scroll-bg: rgba(50, 60, 51, 1); + --scroll-inverse-bg: rgba(25, 30, 26, 1); + --scroll-active-bg: rgba(113, 143, 103, 1); + + /* Text & Foreground */ + --button-text: rgba(212, 222, 207, 0.9); + --button-selected-text: rgba(33, 40, 34, 1); + --button-hover-text: rgba(232, 242, 227, 1); + --input-text-color: rgba(212, 222, 207, 1); + --text-message: rgba(212, 222, 207, 1); + --text-warning: rgba(228, 194, 119, 1); + --text-input-echo: rgba(130, 174, 204, 1); + --text-shell: rgba(113, 143, 103, 1); + --text-error: rgba(215, 119, 119, 1); + + /* Other UI Elements */ + --scroll-color: rgba(212, 222, 207, 1); + --caret-color: rgba(212, 222, 207, 1); } \ No newline at end of file diff --git a/Styles/Themes/GithubDarkTheme.uss b/Styles/Themes/GithubDarkTheme.uss index 0cf7b54..c7ec32d 100644 --- a/Styles/Themes/GithubDarkTheme.uss +++ b/Styles/Themes/GithubDarkTheme.uss @@ -1,26 +1,26 @@ -.github-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(13, 17, 23, 0.9); - --button-bg: rgba(34, 39, 46, 1); - --input-field-bg: rgba(22, 27, 34, 0.8); - --button-selected-bg: rgba(47, 129, 247, 0.85); - --button-hover-bg: rgba(48, 54, 61, 1); - --scroll-bg: rgba(34, 39, 46, 1); - --scroll-inverse-bg: rgba(56, 62, 70, 1); - --scroll-active-bg: rgba(70, 77, 86, 1); - - /* Text & Foreground */ - --button-text: rgba(201, 209, 217, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(201, 209, 217, 1); - --input-text-color: rgba(201, 209, 217, 1.0); - --text-message: rgba(201, 209, 217, 1); - --text-warning: rgba(210, 153, 34, 1); - --text-input-echo: rgba(165, 198, 255, 1); - --text-shell: rgba(139, 148, 158, 1); - --text-error: rgba(248, 131, 125, 1); - - /* Other UI Elements */ - --scroll-color: rgba(201, 209, 217, 1); - --caret-color: rgba(88, 166, 255, 1.0); +.github-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(13, 17, 23, 0.9); + --button-bg: rgba(34, 39, 46, 1); + --input-field-bg: rgba(22, 27, 34, 0.8); + --button-selected-bg: rgba(47, 129, 247, 0.85); + --button-hover-bg: rgba(48, 54, 61, 1); + --scroll-bg: rgba(34, 39, 46, 1); + --scroll-inverse-bg: rgba(56, 62, 70, 1); + --scroll-active-bg: rgba(70, 77, 86, 1); + + /* Text & Foreground */ + --button-text: rgba(201, 209, 217, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(201, 209, 217, 1); + --input-text-color: rgba(201, 209, 217, 1.0); + --text-message: rgba(201, 209, 217, 1); + --text-warning: rgba(210, 153, 34, 1); + --text-input-echo: rgba(165, 198, 255, 1); + --text-shell: rgba(139, 148, 158, 1); + --text-error: rgba(248, 131, 125, 1); + + /* Other UI Elements */ + --scroll-color: rgba(201, 209, 217, 1); + --caret-color: rgba(88, 166, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/GithubLightTheme.uss b/Styles/Themes/GithubLightTheme.uss index 6e983e0..40aeb8e 100644 --- a/Styles/Themes/GithubLightTheme.uss +++ b/Styles/Themes/GithubLightTheme.uss @@ -1,26 +1,26 @@ -.github-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 255, 255, 0.9); - --button-bg: rgba(246, 248, 250, 1); - --input-field-bg: rgba(246, 248, 250, 0.8); - --button-selected-bg: rgba(3, 102, 214, 0.85); - --button-hover-bg: rgba(231, 235, 240, 1); - --scroll-bg: rgba(246, 248, 250, 1); - --scroll-inverse-bg: rgba(221, 224, 228, 1); - --scroll-active-bg: rgba(174, 182, 190, 1); - - /* Text & Foreground */ - --button-text: rgba(36, 41, 46, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(16, 21, 26, 1); - --input-text-color: rgba(36, 41, 46, 1); - --text-message: rgba(36, 41, 46, 1); - --text-warning: rgba(179, 92, 0, 1); - --text-input-echo: rgba(1, 73, 153, 1); - --text-shell: rgba(106, 115, 125, 1); - --text-error: rgba(207, 34, 46, 1); - - /* Other UI Elements */ - --scroll-color: rgba(36, 41, 46, 1); - --caret-color: rgba(3, 102, 214, 1); +.github-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 255, 255, 0.9); + --button-bg: rgba(246, 248, 250, 1); + --input-field-bg: rgba(246, 248, 250, 0.8); + --button-selected-bg: rgba(3, 102, 214, 0.85); + --button-hover-bg: rgba(231, 235, 240, 1); + --scroll-bg: rgba(246, 248, 250, 1); + --scroll-inverse-bg: rgba(221, 224, 228, 1); + --scroll-active-bg: rgba(174, 182, 190, 1); + + /* Text & Foreground */ + --button-text: rgba(36, 41, 46, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(16, 21, 26, 1); + --input-text-color: rgba(36, 41, 46, 1); + --text-message: rgba(36, 41, 46, 1); + --text-warning: rgba(179, 92, 0, 1); + --text-input-echo: rgba(1, 73, 153, 1); + --text-shell: rgba(106, 115, 125, 1); + --text-error: rgba(207, 34, 46, 1); + + /* Other UI Elements */ + --scroll-color: rgba(36, 41, 46, 1); + --caret-color: rgba(3, 102, 214, 1); } \ No newline at end of file diff --git a/Styles/Themes/GlitchTheme.uss b/Styles/Themes/GlitchTheme.uss index cb977f5..1eeb27e 100644 --- a/Styles/Themes/GlitchTheme.uss +++ b/Styles/Themes/GlitchTheme.uss @@ -1,26 +1,26 @@ -.glitch-theme { - /* Backgrounds */ - --terminal-bg: rgba(15, 10, 25, 0.9); - --button-bg: rgba(30, 20, 45, 1); - --input-field-bg: rgba(10, 5, 20, 0.7); - --button-selected-bg: rgba(0, 255, 255, 0.85); - --button-hover-bg: rgba(255, 0, 255, 0.4); - --scroll-bg: rgba(40, 30, 55, 1); - --scroll-inverse-bg: rgba(30, 20, 45, 1); - --scroll-active-bg: rgba(0, 200, 50, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 255, 100, 0.9); - --button-selected-text: rgba(15, 10, 25, 1); - --button-hover-text: rgba(255, 255, 0, 1); - --input-text-color: rgba(0, 255, 100, 1.0); - --text-message: rgba(0, 255, 100, 1); - --text-warning: rgba(255, 255, 0, 1); - --text-input-echo: rgba(0, 255, 255, 1); - --text-shell: rgba(180, 220, 150, 1); - --text-error: rgba(255, 0, 255, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 0, 255, 1); - --caret-color: rgba(255, 255, 0, 1.0); +.glitch-theme { + /* Backgrounds */ + --terminal-bg: rgba(15, 10, 25, 0.9); + --button-bg: rgba(30, 20, 45, 1); + --input-field-bg: rgba(10, 5, 20, 0.7); + --button-selected-bg: rgba(0, 255, 255, 0.85); + --button-hover-bg: rgba(255, 0, 255, 0.4); + --scroll-bg: rgba(40, 30, 55, 1); + --scroll-inverse-bg: rgba(30, 20, 45, 1); + --scroll-active-bg: rgba(0, 200, 50, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 255, 100, 0.9); + --button-selected-text: rgba(15, 10, 25, 1); + --button-hover-text: rgba(255, 255, 0, 1); + --input-text-color: rgba(0, 255, 100, 1.0); + --text-message: rgba(0, 255, 100, 1); + --text-warning: rgba(255, 255, 0, 1); + --text-input-echo: rgba(0, 255, 255, 1); + --text-shell: rgba(180, 220, 150, 1); + --text-error: rgba(255, 0, 255, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 0, 255, 1); + --caret-color: rgba(255, 255, 0, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/GraphiteTheme.uss b/Styles/Themes/GraphiteTheme.uss index e501815..fe32c8d 100644 --- a/Styles/Themes/GraphiteTheme.uss +++ b/Styles/Themes/GraphiteTheme.uss @@ -1,26 +1,26 @@ -.graphite-theme { - /* Backgrounds */ - --terminal-bg: rgba(45, 45, 45, 0.9); - --button-bg: rgba(65, 65, 65, 1); - --input-field-bg: rgba(35, 35, 35, 0.7); - --button-selected-bg: rgba(150, 150, 150, 0.85); - --button-hover-bg: rgba(85, 85, 85, 0.7); - --scroll-bg: rgba(75, 75, 75, 1); - --scroll-inverse-bg: rgba(65, 65, 65, 1); - --scroll-active-bg: rgba(110, 110, 110, 1); - - /* Text & Foreground */ - --button-text: rgba(200, 200, 200, 0.9); - --button-selected-text: rgba(45, 45, 45, 1); - --button-hover-text: rgba(220, 220, 220, 1); - --input-text-color: rgba(200, 200, 200, 1.0); - --text-message: rgba(200, 200, 200, 1); - --text-warning: rgba(180, 180, 150, 1); - --text-input-echo: rgba(140, 160, 180, 1); - --text-shell: rgba(160, 160, 160, 1); - --text-error: rgba(200, 120, 120, 1); - - /* Other UI Elements */ - --scroll-color: rgba(150, 150, 150, 1); - --caret-color: rgba(200, 200, 200, 1.0); +.graphite-theme { + /* Backgrounds */ + --terminal-bg: rgba(45, 45, 45, 0.9); + --button-bg: rgba(65, 65, 65, 1); + --input-field-bg: rgba(35, 35, 35, 0.7); + --button-selected-bg: rgba(150, 150, 150, 0.85); + --button-hover-bg: rgba(85, 85, 85, 0.7); + --scroll-bg: rgba(75, 75, 75, 1); + --scroll-inverse-bg: rgba(65, 65, 65, 1); + --scroll-active-bg: rgba(110, 110, 110, 1); + + /* Text & Foreground */ + --button-text: rgba(200, 200, 200, 0.9); + --button-selected-text: rgba(45, 45, 45, 1); + --button-hover-text: rgba(220, 220, 220, 1); + --input-text-color: rgba(200, 200, 200, 1.0); + --text-message: rgba(200, 200, 200, 1); + --text-warning: rgba(180, 180, 150, 1); + --text-input-echo: rgba(140, 160, 180, 1); + --text-shell: rgba(160, 160, 160, 1); + --text-error: rgba(200, 120, 120, 1); + + /* Other UI Elements */ + --scroll-color: rgba(150, 150, 150, 1); + --caret-color: rgba(200, 200, 200, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/GruvBoxDarkTheme.uss b/Styles/Themes/GruvBoxDarkTheme.uss index 9a39b5e..3cc8fce 100644 --- a/Styles/Themes/GruvBoxDarkTheme.uss +++ b/Styles/Themes/GruvBoxDarkTheme.uss @@ -1,26 +1,26 @@ -.gruvbox-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 40, 40, 0.9); - --button-bg: rgba(60, 56, 54, 1); - --input-field-bg: rgba(28, 28, 28, 0.8); - --button-selected-bg: rgba(254, 128, 25, 0.85); - --button-hover-bg: rgba(80, 73, 72, 1); - --scroll-bg: rgba(60, 56, 54, 1); - --scroll-inverse-bg: rgba(124, 111, 100, 1); - --scroll-active-bg: rgba(148, 131, 118, 1); - - /* Text & Foreground */ - --button-text: rgba(235, 219, 178, 0.9); - --button-selected-text: rgba(40, 40, 40, 1); - --button-hover-text: rgba(251, 241, 199, 1); - --input-text-color: rgba(235, 219, 178, 1.0); - --text-message: rgba(235, 219, 178, 1); - --text-warning: rgba(250, 189, 47, 1); - --text-input-echo: rgba(131, 165, 152, 1); - --text-shell: rgba(146, 131, 116, 1); - --text-error: rgba(251, 73, 52, 1); - - /* Other UI Elements */ - --scroll-color: rgba(235, 219, 178, 1); - --caret-color: rgba(235, 219, 178, 1.0); +.gruvbox-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 40, 40, 0.9); + --button-bg: rgba(60, 56, 54, 1); + --input-field-bg: rgba(28, 28, 28, 0.8); + --button-selected-bg: rgba(254, 128, 25, 0.85); + --button-hover-bg: rgba(80, 73, 72, 1); + --scroll-bg: rgba(60, 56, 54, 1); + --scroll-inverse-bg: rgba(124, 111, 100, 1); + --scroll-active-bg: rgba(148, 131, 118, 1); + + /* Text & Foreground */ + --button-text: rgba(235, 219, 178, 0.9); + --button-selected-text: rgba(40, 40, 40, 1); + --button-hover-text: rgba(251, 241, 199, 1); + --input-text-color: rgba(235, 219, 178, 1.0); + --text-message: rgba(235, 219, 178, 1); + --text-warning: rgba(250, 189, 47, 1); + --text-input-echo: rgba(131, 165, 152, 1); + --text-shell: rgba(146, 131, 116, 1); + --text-error: rgba(251, 73, 52, 1); + + /* Other UI Elements */ + --scroll-color: rgba(235, 219, 178, 1); + --caret-color: rgba(235, 219, 178, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/GruvBoxLightTheme.uss b/Styles/Themes/GruvBoxLightTheme.uss index dc3fbf5..e1ee35e 100644 --- a/Styles/Themes/GruvBoxLightTheme.uss +++ b/Styles/Themes/GruvBoxLightTheme.uss @@ -1,26 +1,26 @@ -.gruvbox-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(251, 241, 199, 0.9); - --button-bg: rgba(235, 219, 178, 1); - --input-field-bg: rgba(249, 231, 184, 0.8); - --button-selected-bg: rgba(215, 153, 33, 0.85); - --button-hover-bg: rgba(219, 202, 160, 1); - --scroll-bg: rgba(235, 219, 178, 1); - --scroll-inverse-bg: rgba(168, 153, 132, 1); - --scroll-active-bg: rgba(146, 131, 116, 1); - - /* Text & Foreground */ - --button-text: rgba(60, 56, 54, 0.9); - --button-selected-text: rgba(251, 241, 199, 1); - --button-hover-text: rgba(40, 40, 40, 1); - --input-text-color: rgba(60, 56, 54, 1.0); - --text-message: rgba(60, 56, 54, 1); - --text-warning: rgba(215, 153, 33, 1); - --text-input-echo: rgba(69, 133, 136, 1); - --text-shell: rgba(124, 111, 100, 1); - --text-error: rgba(204, 36, 29, 1); - - /* Other UI Elements */ - --scroll-color: rgba(60, 56, 54, 1); - --caret-color: rgba(60, 56, 54, 1.0); +.gruvbox-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(251, 241, 199, 0.9); + --button-bg: rgba(235, 219, 178, 1); + --input-field-bg: rgba(249, 231, 184, 0.8); + --button-selected-bg: rgba(215, 153, 33, 0.85); + --button-hover-bg: rgba(219, 202, 160, 1); + --scroll-bg: rgba(235, 219, 178, 1); + --scroll-inverse-bg: rgba(168, 153, 132, 1); + --scroll-active-bg: rgba(146, 131, 116, 1); + + /* Text & Foreground */ + --button-text: rgba(60, 56, 54, 0.9); + --button-selected-text: rgba(251, 241, 199, 1); + --button-hover-text: rgba(40, 40, 40, 1); + --input-text-color: rgba(60, 56, 54, 1.0); + --text-message: rgba(60, 56, 54, 1); + --text-warning: rgba(215, 153, 33, 1); + --text-input-echo: rgba(69, 133, 136, 1); + --text-shell: rgba(124, 111, 100, 1); + --text-error: rgba(204, 36, 29, 1); + + /* Other UI Elements */ + --scroll-color: rgba(60, 56, 54, 1); + --caret-color: rgba(60, 56, 54, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/HackerMatrixTheme.uss b/Styles/Themes/HackerMatrixTheme.uss index a5c1fb5..0f5dbcd 100644 --- a/Styles/Themes/HackerMatrixTheme.uss +++ b/Styles/Themes/HackerMatrixTheme.uss @@ -1,26 +1,26 @@ -.hacker-matrix-theme { - /* Backgrounds */ - --terminal-bg: rgba(0, 10, 0, 0.85); - --button-bg: rgba(0, 25, 0, 1); - --input-field-bg: rgba(0, 5, 0, 0.7); - --button-selected-bg: rgba(0, 235, 0, 0.85); - --button-hover-bg: rgba(0, 45, 0, 0.7); - --scroll-bg: rgba(0, 38, 0, 1); - --scroll-inverse-bg: rgba(0, 29, 0, 1); - --scroll-active-bg: rgba(0, 70, 0, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 255, 0, 0.9); - --button-selected-text: rgba(0, 10, 0, 1); - --button-hover-text: rgba(100, 255, 100, 1); - --input-text-color: rgba(0, 255, 0, 1.0); - --text-message: rgba(0, 255, 0, 1); - --text-warning: rgba(180, 255, 0, 1); - --text-input-echo: rgba(150, 255, 150, 1); - --text-shell: rgba(0, 180, 0, 1); - --text-error: rgba(255, 0, 0, 1); - - /* Other UI Elements */ - --scroll-color: rgba(0, 200, 0, 1); - --caret-color: rgba(50, 255, 50, 1.0); +.hacker-matrix-theme { + /* Backgrounds */ + --terminal-bg: rgba(0, 10, 0, 0.85); + --button-bg: rgba(0, 25, 0, 1); + --input-field-bg: rgba(0, 5, 0, 0.7); + --button-selected-bg: rgba(0, 235, 0, 0.85); + --button-hover-bg: rgba(0, 45, 0, 0.7); + --scroll-bg: rgba(0, 38, 0, 1); + --scroll-inverse-bg: rgba(0, 29, 0, 1); + --scroll-active-bg: rgba(0, 70, 0, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 255, 0, 0.9); + --button-selected-text: rgba(0, 10, 0, 1); + --button-hover-text: rgba(100, 255, 100, 1); + --input-text-color: rgba(0, 255, 0, 1.0); + --text-message: rgba(0, 255, 0, 1); + --text-warning: rgba(180, 255, 0, 1); + --text-input-echo: rgba(150, 255, 150, 1); + --text-shell: rgba(0, 180, 0, 1); + --text-error: rgba(255, 0, 0, 1); + + /* Other UI Elements */ + --scroll-color: rgba(0, 200, 0, 1); + --caret-color: rgba(50, 255, 50, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/HighContrastBlackTheme.uss b/Styles/Themes/HighContrastBlackTheme.uss index e188a2d..5495666 100644 --- a/Styles/Themes/HighContrastBlackTheme.uss +++ b/Styles/Themes/HighContrastBlackTheme.uss @@ -1,26 +1,26 @@ -.high-contrast-black-theme { - /* Backgrounds */ - --terminal-bg: rgba(0, 0, 0, 0.95); - --button-bg: rgba(20, 20, 20, 1); - --input-field-bg: rgba(10, 10, 10, 0.8); - --button-selected-bg: rgba(0, 120, 215, 0.9); - --button-hover-bg: rgba(40, 40, 40, 1); - --scroll-bg: rgba(20, 20, 20, 1); - --scroll-inverse-bg: rgba(0, 0, 0, 1); - --scroll-active-bg: rgba(60, 60, 60, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 255, 255, 0.95); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(255, 255, 255, 1); - --text-message: rgba(255, 255, 255, 1); - --text-warning: rgba(255, 255, 0, 1); - --text-input-echo: rgba(0, 255, 255, 1); - --text-shell: rgba(200, 200, 200, 1); - --text-error: rgba(255, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 255, 255, 1); - --caret-color: rgba(255, 255, 255, 1); +.high-contrast-black-theme { + /* Backgrounds */ + --terminal-bg: rgba(0, 0, 0, 0.95); + --button-bg: rgba(20, 20, 20, 1); + --input-field-bg: rgba(10, 10, 10, 0.8); + --button-selected-bg: rgba(0, 120, 215, 0.9); + --button-hover-bg: rgba(40, 40, 40, 1); + --scroll-bg: rgba(20, 20, 20, 1); + --scroll-inverse-bg: rgba(0, 0, 0, 1); + --scroll-active-bg: rgba(60, 60, 60, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 255, 255, 0.95); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(255, 255, 255, 1); + --text-message: rgba(255, 255, 255, 1); + --text-warning: rgba(255, 255, 0, 1); + --text-input-echo: rgba(0, 255, 255, 1); + --text-shell: rgba(200, 200, 200, 1); + --text-error: rgba(255, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 255, 255, 1); + --caret-color: rgba(255, 255, 255, 1); } \ No newline at end of file diff --git a/Styles/Themes/HighContrastLightTheme.uss b/Styles/Themes/HighContrastLightTheme.uss index 4e0e61b..7844194 100644 --- a/Styles/Themes/HighContrastLightTheme.uss +++ b/Styles/Themes/HighContrastLightTheme.uss @@ -1,26 +1,26 @@ -.high-contrast-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 255, 255, 0.75); - --button-bg: rgba(230, 230, 230, 1); - --input-field-bg: rgba(255, 255, 255, 0.8); - --button-selected-bg: rgba(0, 0, 0, 0.85); - --button-hover-bg: rgba(200, 200, 200, 0.7); - --scroll-bg: rgba(210, 210, 210, 1); - --scroll-inverse-bg: rgba(220, 220, 220, 1); - --scroll-active-bg: rgba(100, 100, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 0, 0, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(0, 0, 0, 1); - --input-text-color: rgba(0, 0, 0, 1.0); - --text-message: rgba(0, 0, 0, 1); - --text-warning: rgba(50, 50, 50, 1); - --text-input-echo: rgba(30, 30, 30, 1); - --text-shell: rgba(80, 80, 80, 1); - --text-error: rgba(0, 0, 0, 1); - - /* Other UI Elements */ - --scroll-color: rgba(50, 50, 50, 1); - --caret-color: rgba(0, 0, 0, 1.0); +.high-contrast-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 255, 255, 0.75); + --button-bg: rgba(230, 230, 230, 1); + --input-field-bg: rgba(255, 255, 255, 0.8); + --button-selected-bg: rgba(0, 0, 0, 0.85); + --button-hover-bg: rgba(200, 200, 200, 0.7); + --scroll-bg: rgba(210, 210, 210, 1); + --scroll-inverse-bg: rgba(220, 220, 220, 1); + --scroll-active-bg: rgba(100, 100, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 0, 0, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(0, 0, 0, 1); + --input-text-color: rgba(0, 0, 0, 1.0); + --text-message: rgba(0, 0, 0, 1); + --text-warning: rgba(50, 50, 50, 1); + --text-input-echo: rgba(30, 30, 30, 1); + --text-shell: rgba(80, 80, 80, 1); + --text-error: rgba(0, 0, 0, 1); + + /* Other UI Elements */ + --scroll-color: rgba(50, 50, 50, 1); + --caret-color: rgba(0, 0, 0, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/HoloInterfaceTheme.uss b/Styles/Themes/HoloInterfaceTheme.uss index 6648b42..210ec8e 100644 --- a/Styles/Themes/HoloInterfaceTheme.uss +++ b/Styles/Themes/HoloInterfaceTheme.uss @@ -1,26 +1,26 @@ -.holo-interface-theme { - /* Backgrounds */ - --terminal-bg: rgba(0, 20, 40, 0.8); - --button-bg: rgba(10, 50, 80, 0.7); - --input-field-bg: rgba(0, 10, 20, 0.6); - --button-selected-bg: rgba(0, 255, 255, 0.85); - --button-hover-bg: rgba(20, 80, 120, 0.7); - --scroll-bg: rgba(15, 60, 90, 0.8); - --scroll-inverse-bg: rgba(10, 50, 80, 0.7); - --scroll-active-bg: rgba(40, 100, 140, 1); - - /* Text & Foreground */ - --button-text: rgba(150, 230, 255, 0.9); - --button-selected-text: rgba(0, 20, 40, 1); - --button-hover-text: rgba(200, 255, 255, 1); - --input-text-color: rgba(180, 255, 255, 1.0); - --text-message: rgba(180, 255, 255, 1); - --text-warning: rgba(255, 220, 100, 1); - --text-input-echo: rgba(100, 200, 255, 1); - --text-shell: rgba(120, 180, 200, 1); - --text-error: rgba(255, 100, 100, 1); - - /* Other UI Elements */ - --scroll-color: rgba(0, 255, 255, 1); - --caret-color: rgba(180, 255, 255, 1.0); +.holo-interface-theme { + /* Backgrounds */ + --terminal-bg: rgba(0, 20, 40, 0.8); + --button-bg: rgba(10, 50, 80, 0.7); + --input-field-bg: rgba(0, 10, 20, 0.6); + --button-selected-bg: rgba(0, 255, 255, 0.85); + --button-hover-bg: rgba(20, 80, 120, 0.7); + --scroll-bg: rgba(15, 60, 90, 0.8); + --scroll-inverse-bg: rgba(10, 50, 80, 0.7); + --scroll-active-bg: rgba(40, 100, 140, 1); + + /* Text & Foreground */ + --button-text: rgba(150, 230, 255, 0.9); + --button-selected-text: rgba(0, 20, 40, 1); + --button-hover-text: rgba(200, 255, 255, 1); + --input-text-color: rgba(180, 255, 255, 1.0); + --text-message: rgba(180, 255, 255, 1); + --text-warning: rgba(255, 220, 100, 1); + --text-input-echo: rgba(100, 200, 255, 1); + --text-shell: rgba(120, 180, 200, 1); + --text-error: rgba(255, 100, 100, 1); + + /* Other UI Elements */ + --scroll-color: rgba(0, 255, 255, 1); + --caret-color: rgba(180, 255, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/HotPinkNeonTheme.uss b/Styles/Themes/HotPinkNeonTheme.uss index 38614a2..9308114 100644 --- a/Styles/Themes/HotPinkNeonTheme.uss +++ b/Styles/Themes/HotPinkNeonTheme.uss @@ -1,26 +1,26 @@ -.hotpink-neon-theme { - /* Backgrounds */ - --terminal-bg: rgba(30, 0, 20, 0.85); - --button-bg: rgba(50, 10, 35, 1); - --input-field-bg: rgba(20, 0, 10, 0.7); - --button-selected-bg: rgba(255, 105, 180, 0.85); - --button-hover-bg: rgba(70, 30, 55, 0.7); - --scroll-bg: rgba(68, 28, 48, 1); - --scroll-inverse-bg: rgba(59, 19, 39, 1); - --scroll-active-bg: rgba(100, 60, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 180, 220, 0.9); - --button-selected-text: rgba(30, 0, 20, 1); - --button-hover-text: rgba(255, 200, 235, 1); - --input-text-color: rgba(255, 180, 220, 1.0); - --text-message: rgba(255, 180, 220, 1); - --text-warning: rgba(255, 200, 0, 1); - --text-input-echo: rgba(180, 200, 255, 1); - --text-shell: rgba(200, 140, 180, 1); - --text-error: rgba(255, 60, 60, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 105, 180, 1); - --caret-color: rgba(255, 180, 220, 1.0); +.hotpink-neon-theme { + /* Backgrounds */ + --terminal-bg: rgba(30, 0, 20, 0.85); + --button-bg: rgba(50, 10, 35, 1); + --input-field-bg: rgba(20, 0, 10, 0.7); + --button-selected-bg: rgba(255, 105, 180, 0.85); + --button-hover-bg: rgba(70, 30, 55, 0.7); + --scroll-bg: rgba(68, 28, 48, 1); + --scroll-inverse-bg: rgba(59, 19, 39, 1); + --scroll-active-bg: rgba(100, 60, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 180, 220, 0.9); + --button-selected-text: rgba(30, 0, 20, 1); + --button-hover-text: rgba(255, 200, 235, 1); + --input-text-color: rgba(255, 180, 220, 1.0); + --text-message: rgba(255, 180, 220, 1); + --text-warning: rgba(255, 200, 0, 1); + --text-input-echo: rgba(180, 200, 255, 1); + --text-shell: rgba(200, 140, 180, 1); + --text-error: rgba(255, 60, 60, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 105, 180, 1); + --caret-color: rgba(255, 180, 220, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/IcebergDarkTheme.uss b/Styles/Themes/IcebergDarkTheme.uss index 6e329cc..b16fe0d 100644 --- a/Styles/Themes/IcebergDarkTheme.uss +++ b/Styles/Themes/IcebergDarkTheme.uss @@ -1,26 +1,26 @@ -.iceberg-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(21, 25, 30, 0.9); - --button-bg: rgba(31, 36, 42, 1); - --input-field-bg: rgba(26, 30, 36, 0.8); - --button-selected-bg: rgba(96, 170, 226, 0.85); - --button-hover-bg: rgba(41, 46, 52, 1); - --scroll-bg: rgba(31, 36, 42, 1); - --scroll-inverse-bg: rgba(16, 20, 25, 1); - --scroll-active-bg: rgba(89, 98, 108, 1); - - /* Text & Foreground */ - --button-text: rgba(208, 208, 208, 0.9); - --button-selected-text: rgba(21, 25, 30, 1); - --button-hover-text: rgba(230, 230, 230, 1); - --input-text-color: rgba(208, 208, 208, 1); - --text-message: rgba(208, 208, 208, 1); - --text-warning: rgba(184, 151, 101, 1); - --text-input-echo: rgba(130, 170, 157, 1); - --text-shell: rgba(89, 98, 108, 1); - --text-error: rgba(192, 97, 101, 1); - - /* Other UI Elements */ - --scroll-color: rgba(208, 208, 208, 1); - --caret-color: rgba(208, 208, 208, 1); +.iceberg-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(21, 25, 30, 0.9); + --button-bg: rgba(31, 36, 42, 1); + --input-field-bg: rgba(26, 30, 36, 0.8); + --button-selected-bg: rgba(96, 170, 226, 0.85); + --button-hover-bg: rgba(41, 46, 52, 1); + --scroll-bg: rgba(31, 36, 42, 1); + --scroll-inverse-bg: rgba(16, 20, 25, 1); + --scroll-active-bg: rgba(89, 98, 108, 1); + + /* Text & Foreground */ + --button-text: rgba(208, 208, 208, 0.9); + --button-selected-text: rgba(21, 25, 30, 1); + --button-hover-text: rgba(230, 230, 230, 1); + --input-text-color: rgba(208, 208, 208, 1); + --text-message: rgba(208, 208, 208, 1); + --text-warning: rgba(184, 151, 101, 1); + --text-input-echo: rgba(130, 170, 157, 1); + --text-shell: rgba(89, 98, 108, 1); + --text-error: rgba(192, 97, 101, 1); + + /* Other UI Elements */ + --scroll-color: rgba(208, 208, 208, 1); + --caret-color: rgba(208, 208, 208, 1); } \ No newline at end of file diff --git a/Styles/Themes/IcebergLightTheme.uss b/Styles/Themes/IcebergLightTheme.uss index 404c7cc..247ea5d 100644 --- a/Styles/Themes/IcebergLightTheme.uss +++ b/Styles/Themes/IcebergLightTheme.uss @@ -1,26 +1,26 @@ -.iceberg-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(235, 240, 242, 0.9); - --button-bg: rgba(220, 225, 228, 1); - --input-field-bg: rgba(228, 233, 235, 0.8); - --button-selected-bg: rgba(86, 140, 186, 0.85); - --button-hover-bg: rgba(210, 215, 218, 1); - --scroll-bg: rgba(220, 225, 228, 1); - --scroll-inverse-bg: rgba(245, 250, 252, 1); - --scroll-active-bg: rgba(156, 163, 168, 1); - - /* Text & Foreground */ - --button-text: rgba(48, 52, 57, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(28, 32, 37, 1); - --input-text-color: rgba(48, 52, 57, 1); - --text-message: rgba(48, 52, 57, 1); - --text-warning: rgba(164, 121, 81, 1); - --text-input-echo: rgba(100, 140, 127, 1); - --text-shell: rgba(156, 163, 168, 1); - --text-error: rgba(172, 77, 81, 1); - - /* Other UI Elements */ - --scroll-color: rgba(48, 52, 57, 1); - --caret-color: rgba(48, 52, 57, 1); +.iceberg-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(235, 240, 242, 0.9); + --button-bg: rgba(220, 225, 228, 1); + --input-field-bg: rgba(228, 233, 235, 0.8); + --button-selected-bg: rgba(86, 140, 186, 0.85); + --button-hover-bg: rgba(210, 215, 218, 1); + --scroll-bg: rgba(220, 225, 228, 1); + --scroll-inverse-bg: rgba(245, 250, 252, 1); + --scroll-active-bg: rgba(156, 163, 168, 1); + + /* Text & Foreground */ + --button-text: rgba(48, 52, 57, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(28, 32, 37, 1); + --input-text-color: rgba(48, 52, 57, 1); + --text-message: rgba(48, 52, 57, 1); + --text-warning: rgba(164, 121, 81, 1); + --text-input-echo: rgba(100, 140, 127, 1); + --text-shell: rgba(156, 163, 168, 1); + --text-error: rgba(172, 77, 81, 1); + + /* Other UI Elements */ + --scroll-color: rgba(48, 52, 57, 1); + --caret-color: rgba(48, 52, 57, 1); } \ No newline at end of file diff --git a/Styles/Themes/JungleTheme.uss b/Styles/Themes/JungleTheme.uss index a4a11c2..8f32c66 100644 --- a/Styles/Themes/JungleTheme.uss +++ b/Styles/Themes/JungleTheme.uss @@ -1,26 +1,26 @@ -.jungle-theme { - /* Backgrounds */ - --terminal-bg: rgba(25, 45, 30, 0.9); - --button-bg: rgba(40, 70, 50, 1); - --input-field-bg: rgba(15, 35, 20, 0.7); - --button-selected-bg: rgba(180, 150, 70, 0.85); - --button-hover-bg: rgba(55, 90, 70, 0.7); - --scroll-bg: rgba(50, 80, 60, 1); - --scroll-inverse-bg: rgba(40, 70, 50, 1); - --scroll-active-bg: rgba(70, 100, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(160, 200, 170, 0.9); - --button-selected-text: rgba(25, 45, 30, 1); - --button-hover-text: rgba(190, 220, 200, 1); - --input-text-color: rgba(160, 200, 170, 1.0); - --text-message: rgba(160, 200, 170, 1); - --text-warning: rgba(230, 180, 90, 1); - --text-input-echo: rgba(100, 160, 110, 1); - --text-shell: rgba(90, 120, 100, 1); - --text-error: rgba(230, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(180, 150, 70, 1); - --caret-color: rgba(160, 200, 170, 1.0); +.jungle-theme { + /* Backgrounds */ + --terminal-bg: rgba(25, 45, 30, 0.9); + --button-bg: rgba(40, 70, 50, 1); + --input-field-bg: rgba(15, 35, 20, 0.7); + --button-selected-bg: rgba(180, 150, 70, 0.85); + --button-hover-bg: rgba(55, 90, 70, 0.7); + --scroll-bg: rgba(50, 80, 60, 1); + --scroll-inverse-bg: rgba(40, 70, 50, 1); + --scroll-active-bg: rgba(70, 100, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(160, 200, 170, 0.9); + --button-selected-text: rgba(25, 45, 30, 1); + --button-hover-text: rgba(190, 220, 200, 1); + --input-text-color: rgba(160, 200, 170, 1.0); + --text-message: rgba(160, 200, 170, 1); + --text-warning: rgba(230, 180, 90, 1); + --text-input-echo: rgba(100, 160, 110, 1); + --text-shell: rgba(90, 120, 100, 1); + --text-error: rgba(230, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(180, 150, 70, 1); + --caret-color: rgba(160, 200, 170, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/KimbieDarkTheme.uss b/Styles/Themes/KimbieDarkTheme.uss index 1a12ec0..3b5892f 100644 --- a/Styles/Themes/KimbieDarkTheme.uss +++ b/Styles/Themes/KimbieDarkTheme.uss @@ -1,26 +1,26 @@ -.kimbie-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(35, 35, 35, 0.9); - --button-bg: rgba(48, 48, 48, 1); - --input-field-bg: rgba(42, 42, 42, 0.8); - --button-selected-bg: rgba(131, 175, 70, 0.85); - --button-hover-bg: rgba(63, 63, 63, 1); - --scroll-bg: rgba(48, 48, 48, 1); - --scroll-inverse-bg: rgba(25, 25, 25, 1); - --scroll-active-bg: rgba(124, 100, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 216, 200, 0.9); - --button-selected-text: rgba(35, 35, 35, 1); - --button-hover-text: rgba(240, 236, 220, 1); - --input-text-color: rgba(220, 216, 200, 1); - --text-message: rgba(220, 216, 200, 1); - --text-warning: rgba(240, 190, 90, 1); - --text-input-echo: rgba(100, 155, 205, 1); - --text-shell: rgba(124, 100, 80, 1); - --text-error: rgba(215, 85, 70, 1); - - /* Other UI Elements */ - --scroll-color: rgba(220, 216, 200, 1); - --caret-color: rgba(220, 216, 200, 1); +.kimbie-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(35, 35, 35, 0.9); + --button-bg: rgba(48, 48, 48, 1); + --input-field-bg: rgba(42, 42, 42, 0.8); + --button-selected-bg: rgba(131, 175, 70, 0.85); + --button-hover-bg: rgba(63, 63, 63, 1); + --scroll-bg: rgba(48, 48, 48, 1); + --scroll-inverse-bg: rgba(25, 25, 25, 1); + --scroll-active-bg: rgba(124, 100, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 216, 200, 0.9); + --button-selected-text: rgba(35, 35, 35, 1); + --button-hover-text: rgba(240, 236, 220, 1); + --input-text-color: rgba(220, 216, 200, 1); + --text-message: rgba(220, 216, 200, 1); + --text-warning: rgba(240, 190, 90, 1); + --text-input-echo: rgba(100, 155, 205, 1); + --text-shell: rgba(124, 100, 80, 1); + --text-error: rgba(215, 85, 70, 1); + + /* Other UI Elements */ + --scroll-color: rgba(220, 216, 200, 1); + --caret-color: rgba(220, 216, 200, 1); } \ No newline at end of file diff --git a/Styles/Themes/LightTheme.uss b/Styles/Themes/LightTheme.uss index a7a49e9..1224722 100644 --- a/Styles/Themes/LightTheme.uss +++ b/Styles/Themes/LightTheme.uss @@ -1,26 +1,26 @@ -.light-theme { - /* Backgrounds */ - --terminal-bg: rgba(245, 245, 245, 0.7); - --button-bg: rgba(230, 230, 230, 1); - --input-field-bg: rgba(255, 255, 255, 0.7); - --button-selected-bg: rgba(15, 15, 15, 0.85); - --button-hover-bg: rgba(100, 100, 100, 0.7); - --scroll-bg: rgba(188, 188, 188, 1); - --scroll-inverse-bg: rgba(179, 179, 179, 1); - --scroll-active-bg: rgba(80, 80, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 0, 0, 0.9); - --button-selected-text: rgba(235, 235, 235, 1); - --button-hover-text: rgba(60, 60, 60, 1); - --input-text-color: rgba(0, 0, 0, 1.0); - --text-message: rgba(51, 51, 51, 1); - --text-warning: rgba(230, 149, 0, 1); - --text-input-echo: rgba(0, 0, 139, 1); - --text-shell: rgba(119, 119, 119, 1); - --text-error: rgba(220, 20, 60, 1); - - /* Other UI Elements */ - --scroll-color: rgba(24, 24, 24, 1); - --caret-color: rgba(0, 0, 0, 1.0); +.light-theme { + /* Backgrounds */ + --terminal-bg: rgba(245, 245, 245, 0.7); + --button-bg: rgba(230, 230, 230, 1); + --input-field-bg: rgba(255, 255, 255, 0.7); + --button-selected-bg: rgba(15, 15, 15, 0.85); + --button-hover-bg: rgba(100, 100, 100, 0.7); + --scroll-bg: rgba(188, 188, 188, 1); + --scroll-inverse-bg: rgba(179, 179, 179, 1); + --scroll-active-bg: rgba(80, 80, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 0, 0, 0.9); + --button-selected-text: rgba(235, 235, 235, 1); + --button-hover-text: rgba(60, 60, 60, 1); + --input-text-color: rgba(0, 0, 0, 1.0); + --text-message: rgba(51, 51, 51, 1); + --text-warning: rgba(230, 149, 0, 1); + --text-input-echo: rgba(0, 0, 139, 1); + --text-shell: rgba(119, 119, 119, 1); + --text-error: rgba(220, 20, 60, 1); + + /* Other UI Elements */ + --scroll-color: rgba(24, 24, 24, 1); + --caret-color: rgba(0, 0, 0, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/LinenTheme.uss b/Styles/Themes/LinenTheme.uss index ae02eb2..c62ce6d 100644 --- a/Styles/Themes/LinenTheme.uss +++ b/Styles/Themes/LinenTheme.uss @@ -1,26 +1,26 @@ -.linen-theme { - /* Backgrounds */ - --terminal-bg: rgba(250, 245, 235, 0.75); - --button-bg: rgba(230, 225, 215, 1); - --input-field-bg: rgba(255, 250, 240, 0.7); - --button-selected-bg: rgba(100, 90, 80, 0.85); - --button-hover-bg: rgba(210, 205, 195, 0.7); - --scroll-bg: rgba(220, 215, 205, 1); - --scroll-inverse-bg: rgba(230, 225, 215, 1); - --scroll-active-bg: rgba(150, 140, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(80, 70, 60, 0.9); - --button-selected-text: rgba(250, 245, 235, 1); - --button-hover-text: rgba(60, 50, 40, 1); - --input-text-color: rgba(80, 70, 60, 1.0); - --text-message: rgba(80, 70, 60, 1); - --text-warning: rgba(190, 140, 90, 1); - --text-input-echo: rgba(120, 110, 100, 1); - --text-shell: rgba(140, 130, 120, 1); - --text-error: rgba(170, 80, 70, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 90, 80, 1); - --caret-color: rgba(80, 70, 60, 1.0); +.linen-theme { + /* Backgrounds */ + --terminal-bg: rgba(250, 245, 235, 0.75); + --button-bg: rgba(230, 225, 215, 1); + --input-field-bg: rgba(255, 250, 240, 0.7); + --button-selected-bg: rgba(100, 90, 80, 0.85); + --button-hover-bg: rgba(210, 205, 195, 0.7); + --scroll-bg: rgba(220, 215, 205, 1); + --scroll-inverse-bg: rgba(230, 225, 215, 1); + --scroll-active-bg: rgba(150, 140, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(80, 70, 60, 0.9); + --button-selected-text: rgba(250, 245, 235, 1); + --button-hover-text: rgba(60, 50, 40, 1); + --input-text-color: rgba(80, 70, 60, 1.0); + --text-message: rgba(80, 70, 60, 1); + --text-warning: rgba(190, 140, 90, 1); + --text-input-echo: rgba(120, 110, 100, 1); + --text-shell: rgba(140, 130, 120, 1); + --text-error: rgba(170, 80, 70, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 90, 80, 1); + --caret-color: rgba(80, 70, 60, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MatchaTheme.uss b/Styles/Themes/MatchaTheme.uss index 3d937ab..74295d2 100644 --- a/Styles/Themes/MatchaTheme.uss +++ b/Styles/Themes/MatchaTheme.uss @@ -1,26 +1,26 @@ -.matcha-theme { - /* Backgrounds */ - --terminal-bg: rgba(220, 235, 210, 0.75); - --button-bg: rgba(195, 215, 180, 1); - --input-field-bg: rgba(235, 245, 225, 0.7); - --button-selected-bg: rgba(80, 100, 70, 0.85); - --button-hover-bg: rgba(175, 195, 160, 0.7); - --scroll-bg: rgba(185, 205, 170, 1); - --scroll-inverse-bg: rgba(195, 215, 180, 1); - --scroll-active-bg: rgba(130, 150, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(70, 90, 60, 0.9); - --button-selected-text: rgba(220, 235, 210, 1); - --button-hover-text: rgba(50, 70, 40, 1); - --input-text-color: rgba(70, 90, 60, 1.0); - --text-message: rgba(70, 90, 60, 1); - --text-warning: rgba(170, 140, 90, 1); - --text-input-echo: rgba(110, 130, 100, 1); - --text-shell: rgba(130, 150, 120, 1); - --text-error: rgba(160, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(80, 100, 70, 1); - --caret-color: rgba(70, 90, 60, 1.0); +.matcha-theme { + /* Backgrounds */ + --terminal-bg: rgba(220, 235, 210, 0.75); + --button-bg: rgba(195, 215, 180, 1); + --input-field-bg: rgba(235, 245, 225, 0.7); + --button-selected-bg: rgba(80, 100, 70, 0.85); + --button-hover-bg: rgba(175, 195, 160, 0.7); + --scroll-bg: rgba(185, 205, 170, 1); + --scroll-inverse-bg: rgba(195, 215, 180, 1); + --scroll-active-bg: rgba(130, 150, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(70, 90, 60, 0.9); + --button-selected-text: rgba(220, 235, 210, 1); + --button-hover-text: rgba(50, 70, 40, 1); + --input-text-color: rgba(70, 90, 60, 1.0); + --text-message: rgba(70, 90, 60, 1); + --text-warning: rgba(170, 140, 90, 1); + --text-input-echo: rgba(110, 130, 100, 1); + --text-shell: rgba(130, 150, 120, 1); + --text-error: rgba(160, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(80, 100, 70, 1); + --caret-color: rgba(70, 90, 60, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MaterialDarkerTheme.uss b/Styles/Themes/MaterialDarkerTheme.uss index 74db354..c1e8044 100644 --- a/Styles/Themes/MaterialDarkerTheme.uss +++ b/Styles/Themes/MaterialDarkerTheme.uss @@ -1,26 +1,26 @@ -.material-darker-theme { - /* Backgrounds */ - --terminal-bg: rgba(33, 33, 33, 0.9); - --button-bg: rgba(50, 50, 50, 1); - --input-field-bg: rgba(42, 42, 42, 0.8); - --button-selected-bg: rgba(128, 203, 196, 0.85); - --button-hover-bg: rgba(66, 66, 66, 1); - --scroll-bg: rgba(50, 50, 50, 1); - --scroll-inverse-bg: rgba(84, 84, 84, 1); - --scroll-active-bg: rgba(100, 100, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(238, 238, 238, 0.9); - --button-selected-text: rgba(33, 33, 33, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(238, 238, 238, 1.0); - --text-message: rgba(238, 238, 238, 1); - --text-warning: rgba(255, 203, 107, 1); - --text-input-echo: rgba(130, 170, 255, 1); - --text-shell: rgba(117, 117, 117, 1); - --text-error: rgba(255, 134, 141, 1); - - /* Other UI Elements */ - --scroll-color: rgba(238, 238, 238, 1); - --caret-color: rgba(255, 204, 0, 1.0); +.material-darker-theme { + /* Backgrounds */ + --terminal-bg: rgba(33, 33, 33, 0.9); + --button-bg: rgba(50, 50, 50, 1); + --input-field-bg: rgba(42, 42, 42, 0.8); + --button-selected-bg: rgba(128, 203, 196, 0.85); + --button-hover-bg: rgba(66, 66, 66, 1); + --scroll-bg: rgba(50, 50, 50, 1); + --scroll-inverse-bg: rgba(84, 84, 84, 1); + --scroll-active-bg: rgba(100, 100, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(238, 238, 238, 0.9); + --button-selected-text: rgba(33, 33, 33, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(238, 238, 238, 1.0); + --text-message: rgba(238, 238, 238, 1); + --text-warning: rgba(255, 203, 107, 1); + --text-input-echo: rgba(130, 170, 255, 1); + --text-shell: rgba(117, 117, 117, 1); + --text-error: rgba(255, 134, 141, 1); + + /* Other UI Elements */ + --scroll-color: rgba(238, 238, 238, 1); + --caret-color: rgba(255, 204, 0, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MaterialLightTheme.uss b/Styles/Themes/MaterialLightTheme.uss index e86594a..4e878a2 100644 --- a/Styles/Themes/MaterialLightTheme.uss +++ b/Styles/Themes/MaterialLightTheme.uss @@ -1,26 +1,26 @@ -.material-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(250, 250, 250, 0.9); - --button-bg: rgba(230, 230, 230, 1); - --input-field-bg: rgba(245, 245, 245, 0.8); - --button-selected-bg: rgba(33, 150, 243, 0.85); - --button-hover-bg: rgba(215, 215, 215, 1); - --scroll-bg: rgba(224, 224, 224, 1); - --scroll-inverse-bg: rgba(189, 189, 189, 1); - --scroll-active-bg: rgba(158, 158, 158, 1); - - /* Text & Foreground */ - --button-text: rgba(66, 66, 66, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(66, 66, 66, 1); - --input-text-color: rgba(66, 66, 66, 1.0); - --text-message: rgba(66, 66, 66, 1); - --text-warning: rgba(255, 193, 7, 1); - --text-input-echo: rgba(33, 150, 243, 1); - --text-shell: rgba(117, 117, 117, 1); - --text-error: rgba(244, 67, 54, 1); - - /* Other UI Elements */ - --scroll-color: rgba(97, 97, 97, 1); - --caret-color: rgba(33, 150, 243, 1.0); +.material-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(250, 250, 250, 0.9); + --button-bg: rgba(230, 230, 230, 1); + --input-field-bg: rgba(245, 245, 245, 0.8); + --button-selected-bg: rgba(33, 150, 243, 0.85); + --button-hover-bg: rgba(215, 215, 215, 1); + --scroll-bg: rgba(224, 224, 224, 1); + --scroll-inverse-bg: rgba(189, 189, 189, 1); + --scroll-active-bg: rgba(158, 158, 158, 1); + + /* Text & Foreground */ + --button-text: rgba(66, 66, 66, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(66, 66, 66, 1); + --input-text-color: rgba(66, 66, 66, 1.0); + --text-message: rgba(66, 66, 66, 1); + --text-warning: rgba(255, 193, 7, 1); + --text-input-echo: rgba(33, 150, 243, 1); + --text-shell: rgba(117, 117, 117, 1); + --text-error: rgba(244, 67, 54, 1); + + /* Other UI Elements */ + --scroll-color: rgba(97, 97, 97, 1); + --caret-color: rgba(33, 150, 243, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MaterialOceanicTheme.uss b/Styles/Themes/MaterialOceanicTheme.uss index 4d5a4c4..2d1f0ba 100644 --- a/Styles/Themes/MaterialOceanicTheme.uss +++ b/Styles/Themes/MaterialOceanicTheme.uss @@ -1,26 +1,26 @@ -.material-oceanic-theme { - /* Backgrounds */ - --terminal-bg: rgba(38, 50, 56, 0.9); - --button-bg: rgba(54, 81, 94, 1); - --input-field-bg: rgba(30, 40, 46, 0.7); - --button-selected-bg: rgba(130, 170, 255, 0.85); - --button-hover-bg: rgba(69, 90, 100, 0.7); - --scroll-bg: rgba(54, 81, 94, 1); - --scroll-inverse-bg: rgba(38, 50, 56, 1); - --scroll-active-bg: rgba(84, 110, 122, 1); - - /* Text & Foreground */ - --button-text: rgba(176, 190, 197, 0.9); - --button-selected-text: rgba(38, 50, 56, 1); - --button-hover-text: rgba(207, 216, 220, 1); - --input-text-color: rgba(176, 190, 197, 1.0); - --text-message: rgba(176, 190, 197, 1); - --text-warning: rgba(255, 204, 102, 1); - --text-input-echo: rgba(199, 146, 234, 1); - --text-shell: rgba(137, 221, 255, 1); - --text-error: rgba(255, 138, 128, 1); - - /* Other UI Elements */ - --scroll-color: rgba(130, 170, 255, 1); - --caret-color: rgba(176, 190, 197, 1.0); +.material-oceanic-theme { + /* Backgrounds */ + --terminal-bg: rgba(38, 50, 56, 0.9); + --button-bg: rgba(54, 81, 94, 1); + --input-field-bg: rgba(30, 40, 46, 0.7); + --button-selected-bg: rgba(130, 170, 255, 0.85); + --button-hover-bg: rgba(69, 90, 100, 0.7); + --scroll-bg: rgba(54, 81, 94, 1); + --scroll-inverse-bg: rgba(38, 50, 56, 1); + --scroll-active-bg: rgba(84, 110, 122, 1); + + /* Text & Foreground */ + --button-text: rgba(176, 190, 197, 0.9); + --button-selected-text: rgba(38, 50, 56, 1); + --button-hover-text: rgba(207, 216, 220, 1); + --input-text-color: rgba(176, 190, 197, 1.0); + --text-message: rgba(176, 190, 197, 1); + --text-warning: rgba(255, 204, 102, 1); + --text-input-echo: rgba(199, 146, 234, 1); + --text-shell: rgba(137, 221, 255, 1); + --text-error: rgba(255, 138, 128, 1); + + /* Other UI Elements */ + --scroll-color: rgba(130, 170, 255, 1); + --caret-color: rgba(176, 190, 197, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MaterialPaleNightTheme.uss b/Styles/Themes/MaterialPaleNightTheme.uss index 7ddb161..eb1bdff 100644 --- a/Styles/Themes/MaterialPaleNightTheme.uss +++ b/Styles/Themes/MaterialPaleNightTheme.uss @@ -1,26 +1,26 @@ -.material-palenight-theme { - /* Backgrounds */ - --terminal-bg: rgba(41, 45, 62, 0.9); - --button-bg: rgba(55, 61, 82, 1); - --input-field-bg: rgba(35, 38, 53, 0.8); - --button-selected-bg: rgba(128, 203, 196, 0.85); - --button-hover-bg: rgba(67, 74, 98, 1); - --scroll-bg: rgba(55, 61, 82, 1); - --scroll-inverse-bg: rgba(79, 88, 117, 1); - --scroll-active-bg: rgba(94, 105, 140, 1); - - /* Text & Foreground */ - --button-text: rgba(166, 173, 190, 0.9); - --button-selected-text: rgba(41, 45, 62, 1); - --button-hover-text: rgba(190, 198, 214, 1); - --input-text-color: rgba(166, 173, 190, 1.0); - --text-message: rgba(166, 173, 190, 1); - --text-warning: rgba(255, 203, 107, 1); - --text-input-echo: rgba(130, 170, 255, 1); - --text-shell: rgba(103, 112, 140, 1); - --text-error: rgba(255, 134, 141, 1); - - /* Other UI Elements */ - --scroll-color: rgba(166, 173, 190, 1); - --caret-color: rgba(199, 146, 234, 1.0); +.material-palenight-theme { + /* Backgrounds */ + --terminal-bg: rgba(41, 45, 62, 0.9); + --button-bg: rgba(55, 61, 82, 1); + --input-field-bg: rgba(35, 38, 53, 0.8); + --button-selected-bg: rgba(128, 203, 196, 0.85); + --button-hover-bg: rgba(67, 74, 98, 1); + --scroll-bg: rgba(55, 61, 82, 1); + --scroll-inverse-bg: rgba(79, 88, 117, 1); + --scroll-active-bg: rgba(94, 105, 140, 1); + + /* Text & Foreground */ + --button-text: rgba(166, 173, 190, 0.9); + --button-selected-text: rgba(41, 45, 62, 1); + --button-hover-text: rgba(190, 198, 214, 1); + --input-text-color: rgba(166, 173, 190, 1.0); + --text-message: rgba(166, 173, 190, 1); + --text-warning: rgba(255, 203, 107, 1); + --text-input-echo: rgba(130, 170, 255, 1); + --text-shell: rgba(103, 112, 140, 1); + --text-error: rgba(255, 134, 141, 1); + + /* Other UI Elements */ + --scroll-color: rgba(166, 173, 190, 1); + --caret-color: rgba(199, 146, 234, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MignightCityTheme.uss b/Styles/Themes/MignightCityTheme.uss index db27104..bfe9f47 100644 --- a/Styles/Themes/MignightCityTheme.uss +++ b/Styles/Themes/MignightCityTheme.uss @@ -1,26 +1,26 @@ -.midnight-city-theme { - /* Backgrounds */ - --terminal-bg: rgba(10, 10, 30, 0.9); - --button-bg: rgba(25, 25, 50, 1); - --input-field-bg: rgba(5, 5, 20, 0.7); - --button-selected-bg: rgba(255, 215, 0, 0.85); - --button-hover-bg: rgba(45, 45, 70, 0.7); - --scroll-bg: rgba(48, 48, 68, 1); - --scroll-inverse-bg: rgba(39, 39, 59, 1); - --scroll-active-bg: rgba(80, 80, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(180, 180, 220, 0.9); - --button-selected-text: rgba(10, 10, 30, 1); - --button-hover-text: rgba(210, 210, 240, 1); - --input-text-color: rgba(200, 200, 230, 1.0); - --text-message: rgba(200, 200, 230, 1); - --text-warning: rgba(255, 165, 0, 1); - --text-input-echo: rgba(100, 180, 255, 1); - --text-shell: rgba(140, 140, 170, 1); - --text-error: rgba(255, 80, 120, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 215, 0, 1); /* Gold */ - --caret-color: rgba(200, 200, 230, 1.0); +.midnight-city-theme { + /* Backgrounds */ + --terminal-bg: rgba(10, 10, 30, 0.9); + --button-bg: rgba(25, 25, 50, 1); + --input-field-bg: rgba(5, 5, 20, 0.7); + --button-selected-bg: rgba(255, 215, 0, 0.85); + --button-hover-bg: rgba(45, 45, 70, 0.7); + --scroll-bg: rgba(48, 48, 68, 1); + --scroll-inverse-bg: rgba(39, 39, 59, 1); + --scroll-active-bg: rgba(80, 80, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(180, 180, 220, 0.9); + --button-selected-text: rgba(10, 10, 30, 1); + --button-hover-text: rgba(210, 210, 240, 1); + --input-text-color: rgba(200, 200, 230, 1.0); + --text-message: rgba(200, 200, 230, 1); + --text-warning: rgba(255, 165, 0, 1); + --text-input-echo: rgba(100, 180, 255, 1); + --text-shell: rgba(140, 140, 170, 1); + --text-error: rgba(255, 80, 120, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 215, 0, 1); /* Gold */ + --caret-color: rgba(200, 200, 230, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MolokaiTheme.uss b/Styles/Themes/MolokaiTheme.uss index 56fe2ee..f52f490 100644 --- a/Styles/Themes/MolokaiTheme.uss +++ b/Styles/Themes/MolokaiTheme.uss @@ -1,26 +1,26 @@ -.molokai-theme { - /* Backgrounds */ - --terminal-bg: rgba(25, 25, 25, 0.9); - --button-bg: rgba(40, 40, 40, 1); - --input-field-bg: rgba(32, 32, 32, 0.8); - --button-selected-bg: rgba(166, 226, 46, 0.85); - --button-hover-bg: rgba(55, 55, 55, 1); - --scroll-bg: rgba(40, 40, 40, 1); - --scroll-inverse-bg: rgba(15, 15, 15, 1); - --scroll-active-bg: rgba(120, 120, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(248, 248, 242, 0.9); - --button-selected-text: rgba(25, 25, 25, 1); - --button-hover-text: rgba(255, 255, 250, 1); - --input-text-color: rgba(248, 248, 242, 1); - --text-message: rgba(248, 248, 242, 1); - --text-warning: rgba(230, 219, 116, 1); - --text-input-echo: rgba(102, 217, 239, 1); - --text-shell: rgba(120, 120, 120, 1); - --text-error: rgba(249, 38, 114, 1); - - /* Other UI Elements */ - --scroll-color: rgba(248, 248, 242, 1); - --caret-color: rgba(248, 248, 242, 1); +.molokai-theme { + /* Backgrounds */ + --terminal-bg: rgba(25, 25, 25, 0.9); + --button-bg: rgba(40, 40, 40, 1); + --input-field-bg: rgba(32, 32, 32, 0.8); + --button-selected-bg: rgba(166, 226, 46, 0.85); + --button-hover-bg: rgba(55, 55, 55, 1); + --scroll-bg: rgba(40, 40, 40, 1); + --scroll-inverse-bg: rgba(15, 15, 15, 1); + --scroll-active-bg: rgba(120, 120, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(248, 248, 242, 0.9); + --button-selected-text: rgba(25, 25, 25, 1); + --button-hover-text: rgba(255, 255, 250, 1); + --input-text-color: rgba(248, 248, 242, 1); + --text-message: rgba(248, 248, 242, 1); + --text-warning: rgba(230, 219, 116, 1); + --text-input-echo: rgba(102, 217, 239, 1); + --text-shell: rgba(120, 120, 120, 1); + --text-error: rgba(249, 38, 114, 1); + + /* Other UI Elements */ + --scroll-color: rgba(248, 248, 242, 1); + --caret-color: rgba(248, 248, 242, 1); } \ No newline at end of file diff --git a/Styles/Themes/MonokaiTheme.uss b/Styles/Themes/MonokaiTheme.uss index c904b0c..60c292e 100644 --- a/Styles/Themes/MonokaiTheme.uss +++ b/Styles/Themes/MonokaiTheme.uss @@ -1,26 +1,26 @@ -.monokai-theme { - /* Backgrounds */ - --terminal-bg: rgba(39, 40, 34, 0.9); - --button-bg: rgba(55, 56, 50, 1); - --input-field-bg: rgba(30, 31, 27, 0.8); - --button-selected-bg: rgba(166, 226, 46, 0.85); - --button-hover-bg: rgba(75, 77, 69, 1); - --scroll-bg: rgba(55, 56, 50, 1); - --scroll-inverse-bg: rgba(90, 92, 84, 1); - --scroll-active-bg: rgba(110, 112, 102, 1); - - /* Text & Foreground */ - --button-text: rgba(248, 248, 242, 0.9); - --button-selected-text: rgba(39, 40, 34, 1); - --button-hover-text: rgba(248, 248, 242, 1); - --input-text-color: rgba(248, 248, 242, 1.0); - --text-message: rgba(248, 248, 242, 1); - --text-warning: rgba(253, 151, 31, 1); - --text-input-echo: rgba(102, 217, 239, 1); - --text-shell: rgba(117, 113, 94, 1); - --text-error: rgba(249, 38, 114, 1); - - /* Other UI Elements */ - --scroll-color: rgba(248, 248, 242, 1); - --caret-color: rgba(248, 248, 242, 1.0); +.monokai-theme { + /* Backgrounds */ + --terminal-bg: rgba(39, 40, 34, 0.9); + --button-bg: rgba(55, 56, 50, 1); + --input-field-bg: rgba(30, 31, 27, 0.8); + --button-selected-bg: rgba(166, 226, 46, 0.85); + --button-hover-bg: rgba(75, 77, 69, 1); + --scroll-bg: rgba(55, 56, 50, 1); + --scroll-inverse-bg: rgba(90, 92, 84, 1); + --scroll-active-bg: rgba(110, 112, 102, 1); + + /* Text & Foreground */ + --button-text: rgba(248, 248, 242, 0.9); + --button-selected-text: rgba(39, 40, 34, 1); + --button-hover-text: rgba(248, 248, 242, 1); + --input-text-color: rgba(248, 248, 242, 1.0); + --text-message: rgba(248, 248, 242, 1); + --text-warning: rgba(253, 151, 31, 1); + --text-input-echo: rgba(102, 217, 239, 1); + --text-shell: rgba(117, 113, 94, 1); + --text-error: rgba(249, 38, 114, 1); + + /* Other UI Elements */ + --scroll-color: rgba(248, 248, 242, 1); + --caret-color: rgba(248, 248, 242, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/MountainPeakTheme.uss b/Styles/Themes/MountainPeakTheme.uss index 4418b9c..75f9730 100644 --- a/Styles/Themes/MountainPeakTheme.uss +++ b/Styles/Themes/MountainPeakTheme.uss @@ -1,26 +1,26 @@ -.mountain-peak-theme { - /* Backgrounds */ - --terminal-bg: rgba(235, 240, 245, 0.75); - --button-bg: rgba(210, 215, 220, 1); - --input-field-bg: rgba(250, 252, 255, 0.7); - --button-selected-bg: rgba(80, 100, 120, 0.85); - --button-hover-bg: rgba(190, 195, 200, 0.7); - --scroll-bg: rgba(200, 205, 210, 1); - --scroll-inverse-bg: rgba(210, 215, 220, 1); - --scroll-active-bg: rgba(130, 140, 150, 1); - - /* Text & Foreground */ - --button-text: rgba(60, 70, 80, 0.9); - --button-selected-text: rgba(235, 240, 245, 1); - --button-hover-text: rgba(40, 50, 60, 1); - --input-text-color: rgba(60, 70, 80, 1.0); - --text-message: rgba(60, 70, 80, 1); - --text-warning: rgba(100, 150, 180, 1); - --text-input-echo: rgba(120, 130, 140, 1); - --text-shell: rgba(90, 100, 110, 1); - --text-error: rgba(160, 80, 90, 1); - - /* Other UI Elements */ - --scroll-color: rgba(80, 100, 120, 1); - --caret-color: rgba(60, 70, 80, 1.0); +.mountain-peak-theme { + /* Backgrounds */ + --terminal-bg: rgba(235, 240, 245, 0.75); + --button-bg: rgba(210, 215, 220, 1); + --input-field-bg: rgba(250, 252, 255, 0.7); + --button-selected-bg: rgba(80, 100, 120, 0.85); + --button-hover-bg: rgba(190, 195, 200, 0.7); + --scroll-bg: rgba(200, 205, 210, 1); + --scroll-inverse-bg: rgba(210, 215, 220, 1); + --scroll-active-bg: rgba(130, 140, 150, 1); + + /* Text & Foreground */ + --button-text: rgba(60, 70, 80, 0.9); + --button-selected-text: rgba(235, 240, 245, 1); + --button-hover-text: rgba(40, 50, 60, 1); + --input-text-color: rgba(60, 70, 80, 1.0); + --text-message: rgba(60, 70, 80, 1); + --text-warning: rgba(100, 150, 180, 1); + --text-input-echo: rgba(120, 130, 140, 1); + --text-shell: rgba(90, 100, 110, 1); + --text-error: rgba(160, 80, 90, 1); + + /* Other UI Elements */ + --scroll-color: rgba(80, 100, 120, 1); + --caret-color: rgba(60, 70, 80, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/NeonNoirTheme.uss b/Styles/Themes/NeonNoirTheme.uss index cd58f9f..ae606f0 100644 --- a/Styles/Themes/NeonNoirTheme.uss +++ b/Styles/Themes/NeonNoirTheme.uss @@ -1,26 +1,26 @@ -.neon-noir-theme { - /* Backgrounds */ - --terminal-bg: rgba(10, 10, 10, 0.95); - --button-bg: rgba(25, 25, 25, 1); - --input-field-bg: rgba(5, 5, 5, 0.7); - --button-selected-bg: rgba(255, 0, 150, 0.85); - --button-hover-bg: rgba(40, 40, 40, 0.7); - --scroll-bg: rgba(30, 30, 30, 1); - --scroll-inverse-bg: rgba(25, 25, 25, 1); - --scroll-active-bg: rgba(60, 60, 60, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 220, 255, 0.9); - --button-selected-text: rgba(10, 10, 10, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(0, 220, 255, 1.0); - --text-message: rgba(0, 220, 255, 1); - --text-warning: rgba(255, 220, 0, 1); - --text-input-echo: rgba(180, 180, 180, 1); - --text-shell: rgba(100, 100, 100, 1); - --text-error: rgba(255, 0, 150, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 0, 150, 1); - --caret-color: rgba(255, 255, 255, 1.0); +.neon-noir-theme { + /* Backgrounds */ + --terminal-bg: rgba(10, 10, 10, 0.95); + --button-bg: rgba(25, 25, 25, 1); + --input-field-bg: rgba(5, 5, 5, 0.7); + --button-selected-bg: rgba(255, 0, 150, 0.85); + --button-hover-bg: rgba(40, 40, 40, 0.7); + --scroll-bg: rgba(30, 30, 30, 1); + --scroll-inverse-bg: rgba(25, 25, 25, 1); + --scroll-active-bg: rgba(60, 60, 60, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 220, 255, 0.9); + --button-selected-text: rgba(10, 10, 10, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(0, 220, 255, 1.0); + --text-message: rgba(0, 220, 255, 1); + --text-warning: rgba(255, 220, 0, 1); + --text-input-echo: rgba(180, 180, 180, 1); + --text-shell: rgba(100, 100, 100, 1); + --text-error: rgba(255, 0, 150, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 0, 150, 1); + --caret-color: rgba(255, 255, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/NightOwlTheme.uss b/Styles/Themes/NightOwlTheme.uss index c6147e4..6884b26 100644 --- a/Styles/Themes/NightOwlTheme.uss +++ b/Styles/Themes/NightOwlTheme.uss @@ -1,26 +1,26 @@ -.night-owl-theme { - /* Backgrounds */ - --terminal-bg: rgba(1, 22, 39, 0.9); - --button-bg: rgba(13, 38, 61, 1); - --input-field-bg: rgba(0, 16, 29, 0.8); - --button-selected-bg: rgba(127, 219, 202, 0.85); - --button-hover-bg: rgba(21, 52, 82, 1); - --scroll-bg: rgba(13, 38, 61, 1); - --scroll-inverse-bg: rgba(33, 70, 104, 1); - --scroll-active-bg: rgba(46, 98, 145, 1); - - /* Text & Foreground */ - --button-text: rgba(214, 222, 235, 0.9); - --button-selected-text: rgba(1, 22, 39, 1); - --button-hover-text: rgba(230, 237, 250, 1); - --input-text-color: rgba(214, 222, 235, 1.0); - --text-message: rgba(214, 222, 235, 1); - --text-warning: rgba(255, 239, 186, 1); - --text-input-echo: rgba(130, 170, 255, 1); - --text-shell: rgba(100, 112, 134, 1); - --text-error: rgba(255, 134, 141, 1); - - /* Other UI Elements */ - --scroll-color: rgba(214, 222, 235, 1); - --caret-color: rgba(157, 204, 237, 1.0); +.night-owl-theme { + /* Backgrounds */ + --terminal-bg: rgba(1, 22, 39, 0.9); + --button-bg: rgba(13, 38, 61, 1); + --input-field-bg: rgba(0, 16, 29, 0.8); + --button-selected-bg: rgba(127, 219, 202, 0.85); + --button-hover-bg: rgba(21, 52, 82, 1); + --scroll-bg: rgba(13, 38, 61, 1); + --scroll-inverse-bg: rgba(33, 70, 104, 1); + --scroll-active-bg: rgba(46, 98, 145, 1); + + /* Text & Foreground */ + --button-text: rgba(214, 222, 235, 0.9); + --button-selected-text: rgba(1, 22, 39, 1); + --button-hover-text: rgba(230, 237, 250, 1); + --input-text-color: rgba(214, 222, 235, 1.0); + --text-message: rgba(214, 222, 235, 1); + --text-warning: rgba(255, 239, 186, 1); + --text-input-echo: rgba(130, 170, 255, 1); + --text-shell: rgba(100, 112, 134, 1); + --text-error: rgba(255, 134, 141, 1); + + /* Other UI Elements */ + --scroll-color: rgba(214, 222, 235, 1); + --caret-color: rgba(157, 204, 237, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/NoctisTheme.uss b/Styles/Themes/NoctisTheme.uss index c7830c0..cf42082 100644 --- a/Styles/Themes/NoctisTheme.uss +++ b/Styles/Themes/NoctisTheme.uss @@ -1,26 +1,26 @@ -.noctis-theme { - /* Backgrounds */ - --terminal-bg: rgba(12, 12, 12, 0.9); - --button-bg: rgba(44, 44, 44, 1); - --input-field-bg: rgba(30, 30, 30, 0.8); - --button-selected-bg: rgba(110, 180, 255, 0.85); - --button-hover-bg: rgba(55, 55, 55, 1); - --scroll-bg: rgba(44, 44, 44, 1); - --scroll-inverse-bg: rgba(70, 70, 70, 1); - --scroll-active-bg: rgba(90, 90, 90, 1); - - /* Text & Foreground */ - --button-text: rgba(210, 210, 210, 0.9); - --button-selected-text: rgba(12, 12, 12, 1); - --button-hover-text: rgba(220, 220, 220, 1); - --input-text-color: rgba(220, 220, 220, 1.0); - --text-message: rgba(220, 220, 220, 1); - --text-warning: rgba(255, 210, 100, 1); - --text-input-echo: rgba(110, 180, 255, 1); - --text-shell: rgba(130, 130, 130, 1); - --text-error: rgba(255, 110, 110, 1); - - /* Other UI Elements */ - --scroll-color: rgba(210, 210, 210, 1); - --caret-color: rgba(110, 180, 255, 1.0); +.noctis-theme { + /* Backgrounds */ + --terminal-bg: rgba(12, 12, 12, 0.9); + --button-bg: rgba(44, 44, 44, 1); + --input-field-bg: rgba(30, 30, 30, 0.8); + --button-selected-bg: rgba(110, 180, 255, 0.85); + --button-hover-bg: rgba(55, 55, 55, 1); + --scroll-bg: rgba(44, 44, 44, 1); + --scroll-inverse-bg: rgba(70, 70, 70, 1); + --scroll-active-bg: rgba(90, 90, 90, 1); + + /* Text & Foreground */ + --button-text: rgba(210, 210, 210, 0.9); + --button-selected-text: rgba(12, 12, 12, 1); + --button-hover-text: rgba(220, 220, 220, 1); + --input-text-color: rgba(220, 220, 220, 1.0); + --text-message: rgba(220, 220, 220, 1); + --text-warning: rgba(255, 210, 100, 1); + --text-input-echo: rgba(110, 180, 255, 1); + --text-shell: rgba(130, 130, 130, 1); + --text-error: rgba(255, 110, 110, 1); + + /* Other UI Elements */ + --scroll-color: rgba(210, 210, 210, 1); + --caret-color: rgba(110, 180, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/NordTheme.uss b/Styles/Themes/NordTheme.uss index 3e98886..d4404d0 100644 --- a/Styles/Themes/NordTheme.uss +++ b/Styles/Themes/NordTheme.uss @@ -1,26 +1,26 @@ -.nord-theme { - /* Backgrounds */ - --terminal-bg: rgba(46, 52, 64, 0.9); - --button-bg: rgba(59, 66, 82, 1); - --input-field-bg: rgba(38, 43, 53, 0.8); - --button-selected-bg: rgba(136, 192, 208, 0.85); - --button-hover-bg: rgba(67, 76, 94, 1); - --scroll-bg: rgba(59, 66, 82, 1); - --scroll-inverse-bg: rgba(76, 86, 106, 1); - --scroll-active-bg: rgba(90, 102, 125, 1); - - /* Text & Foreground */ - --button-text: rgba(216, 222, 233, 0.9); - --button-selected-text: rgba(46, 52, 64, 1); - --button-hover-text: rgba(229, 233, 240, 1); - --input-text-color: rgba(216, 222, 233, 1.0); - --text-message: rgba(216, 222, 233, 1); - --text-warning: rgba(235, 203, 139, 1); - --text-input-echo: rgba(143, 188, 187, 1); - --text-shell: rgba(100, 110, 130, 1); - --text-error: rgba(191, 97, 106, 1); - - /* Other UI Elements */ - --scroll-color: rgba(216, 222, 233, 1); - --caret-color: rgba(216, 222, 233, 1.0); +.nord-theme { + /* Backgrounds */ + --terminal-bg: rgba(46, 52, 64, 0.9); + --button-bg: rgba(59, 66, 82, 1); + --input-field-bg: rgba(38, 43, 53, 0.8); + --button-selected-bg: rgba(136, 192, 208, 0.85); + --button-hover-bg: rgba(67, 76, 94, 1); + --scroll-bg: rgba(59, 66, 82, 1); + --scroll-inverse-bg: rgba(76, 86, 106, 1); + --scroll-active-bg: rgba(90, 102, 125, 1); + + /* Text & Foreground */ + --button-text: rgba(216, 222, 233, 0.9); + --button-selected-text: rgba(46, 52, 64, 1); + --button-hover-text: rgba(229, 233, 240, 1); + --input-text-color: rgba(216, 222, 233, 1.0); + --text-message: rgba(216, 222, 233, 1); + --text-warning: rgba(235, 203, 139, 1); + --text-input-echo: rgba(143, 188, 187, 1); + --text-shell: rgba(100, 110, 130, 1); + --text-error: rgba(191, 97, 106, 1); + + /* Other UI Elements */ + --scroll-color: rgba(216, 222, 233, 1); + --caret-color: rgba(216, 222, 233, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OblivionTheme.uss b/Styles/Themes/OblivionTheme.uss index b91db68..3128f5a 100644 --- a/Styles/Themes/OblivionTheme.uss +++ b/Styles/Themes/OblivionTheme.uss @@ -1,26 +1,26 @@ -.oblivion-theme { - /* Backgrounds */ - --terminal-bg: rgba(31, 31, 31, 0.9); - --button-bg: rgba(48, 48, 48, 1); - --input-field-bg: rgba(25, 25, 25, 0.8); - --button-selected-bg: rgba(234, 221, 95, 0.85); - --button-hover-bg: rgba(64, 64, 64, 1); - --scroll-bg: rgba(48, 48, 48, 1); - --scroll-inverse-bg: rgba(96, 96, 96, 1); - --scroll-active-bg: rgba(120, 120, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(222, 222, 222, 0.9); - --button-selected-text: rgba(31, 31, 31, 1); - --button-hover-text: rgba(240, 240, 240, 1); - --input-text-color: rgba(222, 222, 222, 1.0); - --text-message: rgba(222, 222, 222, 1); - --text-warning: rgba(255, 176, 44, 1); - --text-input-echo: rgba(115, 181, 214, 1); - --text-shell: rgba(153, 153, 153, 1); - --text-error: rgba(214, 108, 108, 1); - - /* Other UI Elements */ - --scroll-color: rgba(222, 222, 222, 1); - --caret-color: rgba(222, 222, 222, 1.0); +.oblivion-theme { + /* Backgrounds */ + --terminal-bg: rgba(31, 31, 31, 0.9); + --button-bg: rgba(48, 48, 48, 1); + --input-field-bg: rgba(25, 25, 25, 0.8); + --button-selected-bg: rgba(234, 221, 95, 0.85); + --button-hover-bg: rgba(64, 64, 64, 1); + --scroll-bg: rgba(48, 48, 48, 1); + --scroll-inverse-bg: rgba(96, 96, 96, 1); + --scroll-active-bg: rgba(120, 120, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(222, 222, 222, 0.9); + --button-selected-text: rgba(31, 31, 31, 1); + --button-hover-text: rgba(240, 240, 240, 1); + --input-text-color: rgba(222, 222, 222, 1.0); + --text-message: rgba(222, 222, 222, 1); + --text-warning: rgba(255, 176, 44, 1); + --text-input-echo: rgba(115, 181, 214, 1); + --text-shell: rgba(153, 153, 153, 1); + --text-error: rgba(214, 108, 108, 1); + + /* Other UI Elements */ + --scroll-color: rgba(222, 222, 222, 1); + --caret-color: rgba(222, 222, 222, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/ObsidianTheme.uss b/Styles/Themes/ObsidianTheme.uss index a609637..ec877ed 100644 --- a/Styles/Themes/ObsidianTheme.uss +++ b/Styles/Themes/ObsidianTheme.uss @@ -1,26 +1,26 @@ -.obsidian-theme { - /* Backgrounds */ - --terminal-bg: rgba(27, 27, 27, 0.9); - --button-bg: rgba(45, 45, 45, 1); - --input-field-bg: rgba(35, 35, 35, 0.8); - --button-selected-bg: rgba(123, 104, 238, 0.85); - --button-hover-bg: rgba(60, 60, 60, 1); - --scroll-bg: rgba(45, 45, 45, 1); - --scroll-inverse-bg: rgba(85, 85, 85, 1); - --scroll-active-bg: rgba(110, 110, 110, 1); - - /* Text & Foreground */ - --button-text: rgba(221, 221, 221, 0.9); - --button-selected-text: rgba(250, 250, 250, 1); - --button-hover-text: rgba(238, 238, 238, 1); - --input-text-color: rgba(221, 221, 221, 1.0); - --text-message: rgba(221, 221, 221, 1); - --text-warning: rgba(255, 193, 7, 1); - --text-input-echo: rgba(130, 170, 255, 1); - --text-shell: rgba(158, 158, 158, 1); - --text-error: rgba(239, 83, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(221, 221, 221, 1); - --caret-color: rgba(123, 104, 238, 1.0); +.obsidian-theme { + /* Backgrounds */ + --terminal-bg: rgba(27, 27, 27, 0.9); + --button-bg: rgba(45, 45, 45, 1); + --input-field-bg: rgba(35, 35, 35, 0.8); + --button-selected-bg: rgba(123, 104, 238, 0.85); + --button-hover-bg: rgba(60, 60, 60, 1); + --scroll-bg: rgba(45, 45, 45, 1); + --scroll-inverse-bg: rgba(85, 85, 85, 1); + --scroll-active-bg: rgba(110, 110, 110, 1); + + /* Text & Foreground */ + --button-text: rgba(221, 221, 221, 0.9); + --button-selected-text: rgba(250, 250, 250, 1); + --button-hover-text: rgba(238, 238, 238, 1); + --input-text-color: rgba(221, 221, 221, 1.0); + --text-message: rgba(221, 221, 221, 1); + --text-warning: rgba(255, 193, 7, 1); + --text-input-echo: rgba(130, 170, 255, 1); + --text-shell: rgba(158, 158, 158, 1); + --text-error: rgba(239, 83, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(221, 221, 221, 1); + --caret-color: rgba(123, 104, 238, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OceanDeepTheme.uss b/Styles/Themes/OceanDeepTheme.uss index c86062c..99c20e2 100644 --- a/Styles/Themes/OceanDeepTheme.uss +++ b/Styles/Themes/OceanDeepTheme.uss @@ -1,26 +1,26 @@ -.ocean-deep-theme { - /* Backgrounds */ - --terminal-bg: rgba(0, 25, 40, 0.9); - --button-bg: rgba(10, 45, 60, 1); - --input-field-bg: rgba(0, 15, 30, 0.7); - --button-selected-bg: rgba(100, 200, 220, 0.85); - --button-hover-bg: rgba(30, 70, 90, 0.7); - --scroll-bg: rgba(28, 68, 88, 1); - --scroll-inverse-bg: rgba(19, 59, 79, 1); - --scroll-active-bg: rgba(60, 100, 120, 1); - - /* Text & Foreground */ - --button-text: rgba(180, 230, 255, 0.9); - --button-selected-text: rgba(0, 25, 40, 1); - --button-hover-text: rgba(210, 245, 255, 1); - --input-text-color: rgba(180, 230, 255, 1.0); - --text-message: rgba(180, 230, 255, 1); - --text-warning: rgba(255, 220, 100, 1); - --text-input-echo: rgba(60, 180, 210, 1); - --text-shell: rgba(100, 160, 180, 1); - --text-error: rgba(255, 120, 120, 1); - - /* Other UI Elements */ - --scroll-color: rgba(60, 180, 210, 1); - --caret-color: rgba(180, 230, 255, 1.0); +.ocean-deep-theme { + /* Backgrounds */ + --terminal-bg: rgba(0, 25, 40, 0.9); + --button-bg: rgba(10, 45, 60, 1); + --input-field-bg: rgba(0, 15, 30, 0.7); + --button-selected-bg: rgba(100, 200, 220, 0.85); + --button-hover-bg: rgba(30, 70, 90, 0.7); + --scroll-bg: rgba(28, 68, 88, 1); + --scroll-inverse-bg: rgba(19, 59, 79, 1); + --scroll-active-bg: rgba(60, 100, 120, 1); + + /* Text & Foreground */ + --button-text: rgba(180, 230, 255, 0.9); + --button-selected-text: rgba(0, 25, 40, 1); + --button-hover-text: rgba(210, 245, 255, 1); + --input-text-color: rgba(180, 230, 255, 1.0); + --text-message: rgba(180, 230, 255, 1); + --text-warning: rgba(255, 220, 100, 1); + --text-input-echo: rgba(60, 180, 210, 1); + --text-shell: rgba(100, 160, 180, 1); + --text-error: rgba(255, 120, 120, 1); + + /* Other UI Elements */ + --scroll-color: rgba(60, 180, 210, 1); + --caret-color: rgba(180, 230, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OceanicNextTheme.uss b/Styles/Themes/OceanicNextTheme.uss index 37a3b26..77f21f1 100644 --- a/Styles/Themes/OceanicNextTheme.uss +++ b/Styles/Themes/OceanicNextTheme.uss @@ -1,26 +1,26 @@ -.oceanic-next-theme { - /* Backgrounds */ - --terminal-bg: rgba(27, 43, 58, 0.9); - --button-bg: rgba(40, 58, 73, 1); - --input-field-bg: rgba(22, 35, 47, 0.8); - --button-selected-bg: rgba(249, 143, 80, 0.85); - --button-hover-bg: rgba(53, 77, 97, 1); - --scroll-bg: rgba(40, 58, 73, 1); - --scroll-inverse-bg: rgba(66, 95, 124, 1); - --scroll-active-bg: rgba(80, 115, 148, 1); - - /* Text & Foreground */ - --button-text: rgba(192, 207, 221, 0.9); - --button-selected-text: rgba(27, 43, 58, 1); - --button-hover-text: rgba(210, 225, 239, 1); - --input-text-color: rgba(192, 207, 221, 1.0); - --text-message: rgba(192, 207, 221, 1); - --text-warning: rgba(236, 196, 100, 1); - --text-input-echo: rgba(102, 153, 204, 1); - --text-shell: rgba(108, 130, 148, 1); - --text-error: rgba(236, 102, 102, 1); - - /* Other UI Elements */ - --scroll-color: rgba(192, 207, 221, 1); - --caret-color: rgba(192, 207, 221, 1.0); +.oceanic-next-theme { + /* Backgrounds */ + --terminal-bg: rgba(27, 43, 58, 0.9); + --button-bg: rgba(40, 58, 73, 1); + --input-field-bg: rgba(22, 35, 47, 0.8); + --button-selected-bg: rgba(249, 143, 80, 0.85); + --button-hover-bg: rgba(53, 77, 97, 1); + --scroll-bg: rgba(40, 58, 73, 1); + --scroll-inverse-bg: rgba(66, 95, 124, 1); + --scroll-active-bg: rgba(80, 115, 148, 1); + + /* Text & Foreground */ + --button-text: rgba(192, 207, 221, 0.9); + --button-selected-text: rgba(27, 43, 58, 1); + --button-hover-text: rgba(210, 225, 239, 1); + --input-text-color: rgba(192, 207, 221, 1.0); + --text-message: rgba(192, 207, 221, 1); + --text-warning: rgba(236, 196, 100, 1); + --text-input-echo: rgba(102, 153, 204, 1); + --text-shell: rgba(108, 130, 148, 1); + --text-error: rgba(236, 102, 102, 1); + + /* Other UI Elements */ + --scroll-color: rgba(192, 207, 221, 1); + --caret-color: rgba(192, 207, 221, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OldPaperTheme.uss b/Styles/Themes/OldPaperTheme.uss index 221d138..f07ef26 100644 --- a/Styles/Themes/OldPaperTheme.uss +++ b/Styles/Themes/OldPaperTheme.uss @@ -1,26 +1,26 @@ -.old-paper-theme { - /* Backgrounds */ - --terminal-bg: rgba(240, 230, 210, 0.78); - --button-bg: rgba(220, 210, 190, 1); - --input-field-bg: rgba(250, 245, 230, 0.7); - --button-selected-bg: rgba(90, 60, 40, 0.85); - --button-hover-bg: rgba(200, 190, 170, 0.7); - --scroll-bg: rgba(210, 200, 180, 1); - --scroll-inverse-bg: rgba(220, 210, 190, 1); - --scroll-active-bg: rgba(150, 130, 110, 1); - - /* Text & Foreground */ - --button-text: rgba(70, 40, 20, 0.9); - --button-selected-text: rgba(240, 230, 210, 1); - --button-hover-text: rgba(50, 20, 10, 1); - --input-text-color: rgba(70, 40, 20, 1.0); - --text-message: rgba(70, 40, 20, 1); - --text-warning: rgba(160, 100, 60, 1); - --text-input-echo: rgba(110, 80, 60, 1); - --text-shell: rgba(130, 100, 80, 1); - --text-error: rgba(150, 50, 50, 1); - - /* Other UI Elements */ - --scroll-color: rgba(90, 60, 40, 1); - --caret-color: rgba(70, 40, 20, 1.0); +.old-paper-theme { + /* Backgrounds */ + --terminal-bg: rgba(240, 230, 210, 0.78); + --button-bg: rgba(220, 210, 190, 1); + --input-field-bg: rgba(250, 245, 230, 0.7); + --button-selected-bg: rgba(90, 60, 40, 0.85); + --button-hover-bg: rgba(200, 190, 170, 0.7); + --scroll-bg: rgba(210, 200, 180, 1); + --scroll-inverse-bg: rgba(220, 210, 190, 1); + --scroll-active-bg: rgba(150, 130, 110, 1); + + /* Text & Foreground */ + --button-text: rgba(70, 40, 20, 0.9); + --button-selected-text: rgba(240, 230, 210, 1); + --button-hover-text: rgba(50, 20, 10, 1); + --input-text-color: rgba(70, 40, 20, 1.0); + --text-message: rgba(70, 40, 20, 1); + --text-warning: rgba(160, 100, 60, 1); + --text-input-echo: rgba(110, 80, 60, 1); + --text-shell: rgba(130, 100, 80, 1); + --text-error: rgba(150, 50, 50, 1); + + /* Other UI Elements */ + --scroll-color: rgba(90, 60, 40, 1); + --caret-color: rgba(70, 40, 20, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OneDarkProTheme.uss b/Styles/Themes/OneDarkProTheme.uss index eac8b81..468dfce 100644 --- a/Styles/Themes/OneDarkProTheme.uss +++ b/Styles/Themes/OneDarkProTheme.uss @@ -1,26 +1,26 @@ -.dracula-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 42, 54, 0.9); - --button-bg: rgba(68, 71, 90, 1); - --input-field-bg: rgba(30, 31, 41, 0.8); - --button-selected-bg: rgba(189, 147, 249, 0.85); - --button-hover-bg: rgba(85, 89, 112, 1); - --scroll-bg: rgba(68, 71, 90, 1); - --scroll-inverse-bg: rgba(98, 114, 164, 1); - --scroll-active-bg: rgba(118, 138, 196, 1); - - /* Text & Foreground */ - --button-text: rgba(248, 248, 242, 0.9); - --button-selected-text: rgba(40, 42, 54, 1); - --button-hover-text: rgba(248, 248, 242, 1); - --input-text-color: rgba(248, 248, 242, 1.0); - --text-message: rgba(248, 248, 242, 1); - --text-warning: rgba(241, 250, 140, 1); - --text-input-echo: rgba(139, 233, 253, 1); - --text-shell: rgba(98, 114, 164, 1); - --text-error: rgba(255, 85, 85, 1); - - /* Other UI Elements */ - --scroll-color: rgba(248, 248, 242, 1); - --caret-color: rgba(248, 248, 242, 1.0); +.dracula-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 42, 54, 0.9); + --button-bg: rgba(68, 71, 90, 1); + --input-field-bg: rgba(30, 31, 41, 0.8); + --button-selected-bg: rgba(189, 147, 249, 0.85); + --button-hover-bg: rgba(85, 89, 112, 1); + --scroll-bg: rgba(68, 71, 90, 1); + --scroll-inverse-bg: rgba(98, 114, 164, 1); + --scroll-active-bg: rgba(118, 138, 196, 1); + + /* Text & Foreground */ + --button-text: rgba(248, 248, 242, 0.9); + --button-selected-text: rgba(40, 42, 54, 1); + --button-hover-text: rgba(248, 248, 242, 1); + --input-text-color: rgba(248, 248, 242, 1.0); + --text-message: rgba(248, 248, 242, 1); + --text-warning: rgba(241, 250, 140, 1); + --text-input-echo: rgba(139, 233, 253, 1); + --text-shell: rgba(98, 114, 164, 1); + --text-error: rgba(255, 85, 85, 1); + + /* Other UI Elements */ + --scroll-color: rgba(248, 248, 242, 1); + --caret-color: rgba(248, 248, 242, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OneDarkTheme.uss b/Styles/Themes/OneDarkTheme.uss index e8cc8c3..c471c1c 100644 --- a/Styles/Themes/OneDarkTheme.uss +++ b/Styles/Themes/OneDarkTheme.uss @@ -1,26 +1,26 @@ -.one-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 44, 52, 0.9); - --button-bg: rgba(50, 55, 63, 1); - --input-field-bg: rgba(33, 37, 43, 0.7); - --button-selected-bg: rgba(97, 175, 239, 0.85); - --button-hover-bg: rgba(60, 66, 75, 0.7); - --scroll-bg: rgba(50, 55, 63, 1); - --scroll-inverse-bg: rgba(40, 44, 52, 1); - --scroll-active-bg: rgba(82, 89, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(171, 178, 191, 0.9); - --button-selected-text: rgba(40, 44, 52, 1); - --button-hover-text: rgba(198, 204, 214, 1); - --input-text-color: rgba(171, 178, 191, 1.0); - --text-message: rgba(171, 178, 191, 1); - --text-warning: rgba(229, 192, 123, 1); - --text-input-echo: rgba(198, 120, 221, 1); - --text-shell: rgba(152, 195, 121, 1); - --text-error: rgba(224, 108, 117, 1); - - /* Other UI Elements */ - --scroll-color: rgba(97, 175, 239, 1); - --caret-color: rgba(171, 178, 191, 1.0); +.one-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 44, 52, 0.9); + --button-bg: rgba(50, 55, 63, 1); + --input-field-bg: rgba(33, 37, 43, 0.7); + --button-selected-bg: rgba(97, 175, 239, 0.85); + --button-hover-bg: rgba(60, 66, 75, 0.7); + --scroll-bg: rgba(50, 55, 63, 1); + --scroll-inverse-bg: rgba(40, 44, 52, 1); + --scroll-active-bg: rgba(82, 89, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(171, 178, 191, 0.9); + --button-selected-text: rgba(40, 44, 52, 1); + --button-hover-text: rgba(198, 204, 214, 1); + --input-text-color: rgba(171, 178, 191, 1.0); + --text-message: rgba(171, 178, 191, 1); + --text-warning: rgba(229, 192, 123, 1); + --text-input-echo: rgba(198, 120, 221, 1); + --text-shell: rgba(152, 195, 121, 1); + --text-error: rgba(224, 108, 117, 1); + + /* Other UI Elements */ + --scroll-color: rgba(97, 175, 239, 1); + --caret-color: rgba(171, 178, 191, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OneLightTheme.uss b/Styles/Themes/OneLightTheme.uss index ad8968b..01df6b9 100644 --- a/Styles/Themes/OneLightTheme.uss +++ b/Styles/Themes/OneLightTheme.uss @@ -1,26 +1,26 @@ -.one-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(250, 250, 250, 0.9); - --button-bg: rgba(240, 240, 240, 1); - --input-field-bg: rgba(250, 250, 250, 0.8); - --button-selected-bg: rgba(80, 118, 179, 0.85); - --button-hover-bg: rgba(230, 230, 230, 1); - --scroll-bg: rgba(240, 240, 240, 1); - --scroll-inverse-bg: rgba(220, 220, 220, 1); - --scroll-active-bg: rgba(160, 161, 167, 1); - - /* Text & Foreground */ - --button-text: rgba(58, 62, 67, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(38, 42, 47, 1); - --input-text-color: rgba(58, 62, 67, 1); - --text-message: rgba(58, 62, 67, 1); - --text-warning: rgba(197, 142, 22, 1); - --text-input-echo: rgba(6, 126, 150, 1); - --text-shell: rgba(160, 161, 167, 1); - --text-error: rgba(228, 86, 73, 1); - - /* Other UI Elements */ - --scroll-color: rgba(58, 62, 67, 1); - --caret-color: rgba(80, 118, 179, 1); +.one-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(250, 250, 250, 0.9); + --button-bg: rgba(240, 240, 240, 1); + --input-field-bg: rgba(250, 250, 250, 0.8); + --button-selected-bg: rgba(80, 118, 179, 0.85); + --button-hover-bg: rgba(230, 230, 230, 1); + --scroll-bg: rgba(240, 240, 240, 1); + --scroll-inverse-bg: rgba(220, 220, 220, 1); + --scroll-active-bg: rgba(160, 161, 167, 1); + + /* Text & Foreground */ + --button-text: rgba(58, 62, 67, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(38, 42, 47, 1); + --input-text-color: rgba(58, 62, 67, 1); + --text-message: rgba(58, 62, 67, 1); + --text-warning: rgba(197, 142, 22, 1); + --text-input-echo: rgba(6, 126, 150, 1); + --text-shell: rgba(160, 161, 167, 1); + --text-error: rgba(228, 86, 73, 1); + + /* Other UI Elements */ + --scroll-color: rgba(58, 62, 67, 1); + --caret-color: rgba(80, 118, 179, 1); } \ No newline at end of file diff --git a/Styles/Themes/OneMonokaiTheme.uss b/Styles/Themes/OneMonokaiTheme.uss index d3d8214..a67b64b 100644 --- a/Styles/Themes/OneMonokaiTheme.uss +++ b/Styles/Themes/OneMonokaiTheme.uss @@ -1,26 +1,26 @@ -.one-monokai-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 42, 54, 0.9); - --button-bg: rgba(68, 71, 90, 1); - --input-field-bg: rgba(50, 52, 70, 0.8); - --button-selected-bg: rgba(189, 147, 249, 0.85); - --button-hover-bg: rgba(80, 84, 108, 1); - --scroll-bg: rgba(68, 71, 90, 1); - --scroll-inverse-bg: rgba(98, 102, 134, 1); - --scroll-active-bg: rgba(118, 123, 160, 1); - - /* Text & Foreground */ - --button-text: rgba(248, 248, 242, 0.9); - --button-selected-text: rgba(40, 42, 54, 1); - --button-hover-text: rgba(248, 248, 242, 1); - --input-text-color: rgba(248, 248, 242, 1.0); - --text-message: rgba(248, 248, 242, 1); - --text-warning: rgba(241, 250, 140, 1); - --text-input-echo: rgba(139, 233, 253, 1); - --text-shell: rgba(150, 152, 170, 1); - --text-error: rgba(255, 92, 87, 1); - - /* Other UI Elements */ - --scroll-color: rgba(248, 248, 242, 1); - --caret-color: rgba(248, 248, 242, 1.0); +.one-monokai-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 42, 54, 0.9); + --button-bg: rgba(68, 71, 90, 1); + --input-field-bg: rgba(50, 52, 70, 0.8); + --button-selected-bg: rgba(189, 147, 249, 0.85); + --button-hover-bg: rgba(80, 84, 108, 1); + --scroll-bg: rgba(68, 71, 90, 1); + --scroll-inverse-bg: rgba(98, 102, 134, 1); + --scroll-active-bg: rgba(118, 123, 160, 1); + + /* Text & Foreground */ + --button-text: rgba(248, 248, 242, 0.9); + --button-selected-text: rgba(40, 42, 54, 1); + --button-hover-text: rgba(248, 248, 242, 1); + --input-text-color: rgba(248, 248, 242, 1.0); + --text-message: rgba(248, 248, 242, 1); + --text-warning: rgba(241, 250, 140, 1); + --text-input-echo: rgba(139, 233, 253, 1); + --text-shell: rgba(150, 152, 170, 1); + --text-error: rgba(255, 92, 87, 1); + + /* Other UI Elements */ + --scroll-color: rgba(248, 248, 242, 1); + --caret-color: rgba(248, 248, 242, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/OutrunElectricTheme.uss b/Styles/Themes/OutrunElectricTheme.uss index e147747..1ecc5a2 100644 --- a/Styles/Themes/OutrunElectricTheme.uss +++ b/Styles/Themes/OutrunElectricTheme.uss @@ -1,26 +1,26 @@ -.outrun-electric-theme { - /* Backgrounds */ - --terminal-bg: rgba(25, 19, 37, 0.9); - --button-bg: rgba(51, 37, 72, 1); - --input-field-bg: rgba(40, 28, 55, 0.8); - --button-selected-bg: rgba(255, 118, 198, 0.85); - --button-hover-bg: rgba(71, 53, 92, 1); - --scroll-bg: rgba(51, 37, 72, 1); - --scroll-inverse-bg: rgba(15, 9, 27, 1); - --scroll-active-bg: rgba(0, 239, 255, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 239, 255, 0.9); - --button-selected-text: rgba(25, 19, 37, 1); - --button-hover-text: rgba(80, 250, 255, 1); - --input-text-color: rgba(0, 239, 255, 1); - --text-message: rgba(220, 210, 240, 1); - --text-warning: rgba(255, 217, 0, 1); - --text-input-echo: rgba(80, 250, 255, 1); - --text-shell: rgba(150, 130, 170, 1); - --text-error: rgba(255, 37, 105, 1); - - /* Other UI Elements */ - --scroll-color: rgba(0, 239, 255, 1); - --caret-color: rgba(255, 118, 198, 1); +.outrun-electric-theme { + /* Backgrounds */ + --terminal-bg: rgba(25, 19, 37, 0.9); + --button-bg: rgba(51, 37, 72, 1); + --input-field-bg: rgba(40, 28, 55, 0.8); + --button-selected-bg: rgba(255, 118, 198, 0.85); + --button-hover-bg: rgba(71, 53, 92, 1); + --scroll-bg: rgba(51, 37, 72, 1); + --scroll-inverse-bg: rgba(15, 9, 27, 1); + --scroll-active-bg: rgba(0, 239, 255, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 239, 255, 0.9); + --button-selected-text: rgba(25, 19, 37, 1); + --button-hover-text: rgba(80, 250, 255, 1); + --input-text-color: rgba(0, 239, 255, 1); + --text-message: rgba(220, 210, 240, 1); + --text-warning: rgba(255, 217, 0, 1); + --text-input-echo: rgba(80, 250, 255, 1); + --text-shell: rgba(150, 130, 170, 1); + --text-error: rgba(255, 37, 105, 1); + + /* Other UI Elements */ + --scroll-color: rgba(0, 239, 255, 1); + --caret-color: rgba(255, 118, 198, 1); } \ No newline at end of file diff --git a/Styles/Themes/PaleNightTheme.uss b/Styles/Themes/PaleNightTheme.uss index caaee72..dce3f95 100644 --- a/Styles/Themes/PaleNightTheme.uss +++ b/Styles/Themes/PaleNightTheme.uss @@ -1,26 +1,26 @@ -.palenight-theme { - /* Backgrounds */ - --terminal-bg: rgba(41, 45, 62, 0.9); - --button-bg: rgba(63, 69, 93, 1); - --input-field-bg: rgba(52, 57, 78, 0.8); - --button-selected-bg: rgba(195, 121, 241, 0.85); - --button-hover-bg: rgba(83, 89, 113, 1); - --scroll-bg: rgba(63, 69, 93, 1); - --scroll-inverse-bg: rgba(31, 35, 52, 1); - --scroll-active-bg: rgba(104, 111, 140, 1); - - /* Text & Foreground */ - --button-text: rgba(166, 173, 200, 0.9); - --button-selected-text: rgba(41, 45, 62, 1); - --button-hover-text: rgba(190, 198, 225, 1); - --input-text-color: rgba(166, 173, 200, 1); - --text-message: rgba(166, 173, 200, 1); - --text-warning: rgba(255, 203, 107, 1); - --text-input-echo: rgba(130, 170, 255, 1); - --text-shell: rgba(104, 111, 140, 1); - --text-error: rgba(255, 138, 128, 1); - - /* Other UI Elements */ - --scroll-color: rgba(166, 173, 200, 1); - --caret-color: rgba(195, 121, 241, 1); +.palenight-theme { + /* Backgrounds */ + --terminal-bg: rgba(41, 45, 62, 0.9); + --button-bg: rgba(63, 69, 93, 1); + --input-field-bg: rgba(52, 57, 78, 0.8); + --button-selected-bg: rgba(195, 121, 241, 0.85); + --button-hover-bg: rgba(83, 89, 113, 1); + --scroll-bg: rgba(63, 69, 93, 1); + --scroll-inverse-bg: rgba(31, 35, 52, 1); + --scroll-active-bg: rgba(104, 111, 140, 1); + + /* Text & Foreground */ + --button-text: rgba(166, 173, 200, 0.9); + --button-selected-text: rgba(41, 45, 62, 1); + --button-hover-text: rgba(190, 198, 225, 1); + --input-text-color: rgba(166, 173, 200, 1); + --text-message: rgba(166, 173, 200, 1); + --text-warning: rgba(255, 203, 107, 1); + --text-input-echo: rgba(130, 170, 255, 1); + --text-shell: rgba(104, 111, 140, 1); + --text-error: rgba(255, 138, 128, 1); + + /* Other UI Elements */ + --scroll-color: rgba(166, 173, 200, 1); + --caret-color: rgba(195, 121, 241, 1); } \ No newline at end of file diff --git a/Styles/Themes/PandaSyntaxTheme.uss b/Styles/Themes/PandaSyntaxTheme.uss index fa4bf62..9c173ac 100644 --- a/Styles/Themes/PandaSyntaxTheme.uss +++ b/Styles/Themes/PandaSyntaxTheme.uss @@ -1,26 +1,26 @@ -.panda-syntax-theme { - /* Backgrounds */ - --terminal-bg: rgba(46, 47, 56, 0.9); - --button-bg: rgba(59, 60, 70, 1); - --input-field-bg: rgba(38, 39, 47, 0.8); - --button-selected-bg: rgba(255, 149, 236, 0.85); - --button-hover-bg: rgba(72, 73, 85, 1); - --scroll-bg: rgba(59, 60, 70, 1); - --scroll-inverse-bg: rgba(94, 95, 107, 1); - --scroll-active-bg: rgba(110, 111, 125, 1); - - /* Text & Foreground */ - --button-text: rgba(229, 233, 240, 0.9); - --button-selected-text: rgba(46, 47, 56, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(229, 233, 240, 1.0); - --text-message: rgba(229, 233, 240, 1); - --text-warning: rgba(255, 204, 102, 1); - --text-input-echo: rgba(31, 221, 218, 1); - --text-shell: rgba(108, 109, 117, 1); - --text-error: rgba(255, 99, 140, 1); - - /* Other UI Elements */ - --scroll-color: rgba(229, 233, 240, 1); - --caret-color: rgba(255, 184, 108, 1.0); +.panda-syntax-theme { + /* Backgrounds */ + --terminal-bg: rgba(46, 47, 56, 0.9); + --button-bg: rgba(59, 60, 70, 1); + --input-field-bg: rgba(38, 39, 47, 0.8); + --button-selected-bg: rgba(255, 149, 236, 0.85); + --button-hover-bg: rgba(72, 73, 85, 1); + --scroll-bg: rgba(59, 60, 70, 1); + --scroll-inverse-bg: rgba(94, 95, 107, 1); + --scroll-active-bg: rgba(110, 111, 125, 1); + + /* Text & Foreground */ + --button-text: rgba(229, 233, 240, 0.9); + --button-selected-text: rgba(46, 47, 56, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(229, 233, 240, 1.0); + --text-message: rgba(229, 233, 240, 1); + --text-warning: rgba(255, 204, 102, 1); + --text-input-echo: rgba(31, 221, 218, 1); + --text-shell: rgba(108, 109, 117, 1); + --text-error: rgba(255, 99, 140, 1); + + /* Other UI Elements */ + --scroll-color: rgba(229, 233, 240, 1); + --caret-color: rgba(255, 184, 108, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/PaperColorLightTheme.uss b/Styles/Themes/PaperColorLightTheme.uss index e1e2253..cf81548 100644 --- a/Styles/Themes/PaperColorLightTheme.uss +++ b/Styles/Themes/PaperColorLightTheme.uss @@ -1,26 +1,26 @@ -.papercolor-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(238, 238, 238, 0.9); - --button-bg: rgba(210, 210, 210, 1); - --input-field-bg: rgba(224, 224, 224, 0.8); - --button-selected-bg: rgba(0, 95, 135, 0.85); - --button-hover-bg: rgba(190, 190, 190, 1); - --scroll-bg: rgba(210, 210, 210, 1); - --scroll-inverse-bg: rgba(250, 250, 250, 1); - --scroll-active-bg: rgba(135, 135, 135, 1); - - /* Text & Foreground */ - --button-text: rgba(68, 68, 68, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(48, 48, 48, 1); - --input-text-color: rgba(68, 68, 68, 1); - --text-message: rgba(68, 68, 68, 1); - --text-warning: rgba(215, 135, 0, 1); - --text-input-echo: rgba(0, 135, 0, 1); - --text-shell: rgba(135, 135, 135, 1); - --text-error: rgba(215, 0, 0, 1); - - /* Other UI Elements */ - --scroll-color: rgba(68, 68, 68, 1); - --caret-color: rgba(68, 68, 68, 1); +.papercolor-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(238, 238, 238, 0.9); + --button-bg: rgba(210, 210, 210, 1); + --input-field-bg: rgba(224, 224, 224, 0.8); + --button-selected-bg: rgba(0, 95, 135, 0.85); + --button-hover-bg: rgba(190, 190, 190, 1); + --scroll-bg: rgba(210, 210, 210, 1); + --scroll-inverse-bg: rgba(250, 250, 250, 1); + --scroll-active-bg: rgba(135, 135, 135, 1); + + /* Text & Foreground */ + --button-text: rgba(68, 68, 68, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(48, 48, 48, 1); + --input-text-color: rgba(68, 68, 68, 1); + --text-message: rgba(68, 68, 68, 1); + --text-warning: rgba(215, 135, 0, 1); + --text-input-echo: rgba(0, 135, 0, 1); + --text-shell: rgba(135, 135, 135, 1); + --text-error: rgba(215, 0, 0, 1); + + /* Other UI Elements */ + --scroll-color: rgba(68, 68, 68, 1); + --caret-color: rgba(68, 68, 68, 1); } \ No newline at end of file diff --git a/Styles/Themes/PaperWhiteTheme.uss b/Styles/Themes/PaperWhiteTheme.uss index d33b9e1..c779dee 100644 --- a/Styles/Themes/PaperWhiteTheme.uss +++ b/Styles/Themes/PaperWhiteTheme.uss @@ -1,26 +1,26 @@ -.paper-white-theme { - /* Backgrounds */ - --terminal-bg: rgba(250, 250, 245, 0.7); - --button-bg: rgba(230, 230, 225, 1); - --input-field-bg: rgba(255, 255, 255, 0.75); - --button-selected-bg: rgba(60, 60, 60, 0.85); - --button-hover-bg: rgba(200, 200, 195, 0.7); - --scroll-bg: rgba(218, 218, 213, 1); - --scroll-inverse-bg: rgba(229, 229, 224, 1); - --scroll-active-bg: rgba(100, 100, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(40, 40, 40, 0.9); - --button-selected-text: rgba(240, 240, 235, 1); - --button-hover-text: rgba(20, 20, 20, 1); - --input-text-color: rgba(30, 30, 30, 1.0); - --text-message: rgba(30, 30, 30, 1); - --text-warning: rgba(190, 90, 0, 1); - --text-input-echo: rgba(0, 60, 120, 1); - --text-shell: rgba(100, 100, 100, 1); - --text-error: rgba(170, 20, 20, 1); - - /* Other UI Elements */ - --scroll-color: rgba(80, 80, 80, 1); - --caret-color: rgba(30, 30, 30, 1.0); +.paper-white-theme { + /* Backgrounds */ + --terminal-bg: rgba(250, 250, 245, 0.7); + --button-bg: rgba(230, 230, 225, 1); + --input-field-bg: rgba(255, 255, 255, 0.75); + --button-selected-bg: rgba(60, 60, 60, 0.85); + --button-hover-bg: rgba(200, 200, 195, 0.7); + --scroll-bg: rgba(218, 218, 213, 1); + --scroll-inverse-bg: rgba(229, 229, 224, 1); + --scroll-active-bg: rgba(100, 100, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(40, 40, 40, 0.9); + --button-selected-text: rgba(240, 240, 235, 1); + --button-hover-text: rgba(20, 20, 20, 1); + --input-text-color: rgba(30, 30, 30, 1.0); + --text-message: rgba(30, 30, 30, 1); + --text-warning: rgba(190, 90, 0, 1); + --text-input-echo: rgba(0, 60, 120, 1); + --text-shell: rgba(100, 100, 100, 1); + --text-error: rgba(170, 20, 20, 1); + + /* Other UI Elements */ + --scroll-color: rgba(80, 80, 80, 1); + --caret-color: rgba(30, 30, 30, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/PastelDreamsTheme.uss b/Styles/Themes/PastelDreamsTheme.uss index dbc04b8..a85d56d 100644 --- a/Styles/Themes/PastelDreamsTheme.uss +++ b/Styles/Themes/PastelDreamsTheme.uss @@ -1,26 +1,26 @@ -.pastel-dreams-theme { - /* Backgrounds */ - --terminal-bg: rgba(230, 220, 240, 0.7); - --button-bg: rgba(210, 200, 220, 1); - --input-field-bg: rgba(245, 240, 250, 0.7); - --button-selected-bg: rgba(180, 230, 200, 0.85); - --button-hover-bg: rgba(190, 180, 200, 0.7); - --scroll-bg: rgba(200, 190, 210, 1); - --scroll-inverse-bg: rgba(210, 200, 220, 1); - --scroll-active-bg: rgba(150, 140, 160, 1); - - /* Text & Foreground */ - --button-text: rgba(90, 80, 100, 0.9); - --button-selected-text: rgba(70, 100, 80, 1); - --button-hover-text: rgba(70, 60, 80, 1); - --input-text-color: rgba(90, 80, 100, 1.0); - --text-message: rgba(90, 80, 100, 1); - --text-warning: rgba(240, 210, 140, 1); - --text-input-echo: rgba(160, 190, 230, 1); - --text-shell: rgba(140, 130, 150, 1); - --text-error: rgba(220, 130, 150, 1); - - /* Other UI Elements */ - --scroll-color: rgba(140, 130, 150, 1); - --caret-color: rgba(90, 80, 100, 1.0); +.pastel-dreams-theme { + /* Backgrounds */ + --terminal-bg: rgba(230, 220, 240, 0.7); + --button-bg: rgba(210, 200, 220, 1); + --input-field-bg: rgba(245, 240, 250, 0.7); + --button-selected-bg: rgba(180, 230, 200, 0.85); + --button-hover-bg: rgba(190, 180, 200, 0.7); + --scroll-bg: rgba(200, 190, 210, 1); + --scroll-inverse-bg: rgba(210, 200, 220, 1); + --scroll-active-bg: rgba(150, 140, 160, 1); + + /* Text & Foreground */ + --button-text: rgba(90, 80, 100, 0.9); + --button-selected-text: rgba(70, 100, 80, 1); + --button-hover-text: rgba(70, 60, 80, 1); + --input-text-color: rgba(90, 80, 100, 1.0); + --text-message: rgba(90, 80, 100, 1); + --text-warning: rgba(240, 210, 140, 1); + --text-input-echo: rgba(160, 190, 230, 1); + --text-shell: rgba(140, 130, 150, 1); + --text-error: rgba(220, 130, 150, 1); + + /* Other UI Elements */ + --scroll-color: rgba(140, 130, 150, 1); + --caret-color: rgba(90, 80, 100, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/PipBoyTheme.uss b/Styles/Themes/PipBoyTheme.uss index 1d0cb94..fe1520a 100644 --- a/Styles/Themes/PipBoyTheme.uss +++ b/Styles/Themes/PipBoyTheme.uss @@ -1,26 +1,26 @@ -.pip-boy-theme { - /* Backgrounds */ - --terminal-bg: rgba(20, 35, 25, 0.9); - --button-bg: rgba(30, 55, 35, 1); - --input-field-bg: rgba(25, 45, 30, 0.8); - --button-selected-bg: rgba(47, 255, 173, 0.85); - --button-hover-bg: rgba(40, 65, 45, 1); - --scroll-bg: rgba(30, 55, 35, 1); - --scroll-inverse-bg: rgba(15, 25, 20, 1); - --scroll-active-bg: rgba(40, 100, 50, 1); - - /* Text & Foreground */ - --button-text: rgba(47, 255, 173, 0.9); - --button-selected-text: rgba(20, 35, 25, 1); - --button-hover-text: rgba(80, 255, 180, 1); - --input-text-color: rgba(47, 255, 173, 1); - --text-message: rgba(47, 255, 173, 1); - --text-warning: rgba(180, 255, 200, 1); - --text-input-echo: rgba(100, 255, 190, 1); - --text-shell: rgba(35, 180, 120, 1); - --text-error: rgba(150, 255, 180, 1); - - /* Other UI Elements */ - --scroll-color: rgba(47, 255, 173, 1); - --caret-color: rgba(47, 255, 173, 1); +.pip-boy-theme { + /* Backgrounds */ + --terminal-bg: rgba(20, 35, 25, 0.9); + --button-bg: rgba(30, 55, 35, 1); + --input-field-bg: rgba(25, 45, 30, 0.8); + --button-selected-bg: rgba(47, 255, 173, 0.85); + --button-hover-bg: rgba(40, 65, 45, 1); + --scroll-bg: rgba(30, 55, 35, 1); + --scroll-inverse-bg: rgba(15, 25, 20, 1); + --scroll-active-bg: rgba(40, 100, 50, 1); + + /* Text & Foreground */ + --button-text: rgba(47, 255, 173, 0.9); + --button-selected-text: rgba(20, 35, 25, 1); + --button-hover-text: rgba(80, 255, 180, 1); + --input-text-color: rgba(47, 255, 173, 1); + --text-message: rgba(47, 255, 173, 1); + --text-warning: rgba(180, 255, 200, 1); + --text-input-echo: rgba(100, 255, 190, 1); + --text-shell: rgba(35, 180, 120, 1); + --text-error: rgba(150, 255, 180, 1); + + /* Other UI Elements */ + --scroll-color: rgba(47, 255, 173, 1); + --caret-color: rgba(47, 255, 173, 1); } \ No newline at end of file diff --git a/Styles/Themes/PortalTheme.uss b/Styles/Themes/PortalTheme.uss index 887c681..05bf765 100644 --- a/Styles/Themes/PortalTheme.uss +++ b/Styles/Themes/PortalTheme.uss @@ -1,26 +1,26 @@ -.portal-theme { - /* Backgrounds */ - --terminal-bg: rgba(240, 240, 240, 0.9); - --button-bg: rgba(210, 210, 215, 1); - --input-field-bg: rgba(220, 220, 225, 0.8); - --button-selected-bg: rgba(255, 106, 0, 0.85); - --button-hover-bg: rgba(190, 190, 195, 1); - --scroll-bg: rgba(210, 210, 215, 1); - --scroll-inverse-bg: rgba(250, 250, 250, 1); - --scroll-active-bg: rgba(170, 170, 175, 1); - - /* Text & Foreground */ - --button-text: rgba(50, 50, 55, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(30, 30, 35, 1); - --input-text-color: rgba(30, 30, 35, 1); - --text-message: rgba(50, 50, 55, 1); - --text-warning: rgba(200, 80, 0, 1); - --text-input-echo: rgba(0, 111, 255, 1); - --text-shell: rgba(100, 100, 105, 1); - --text-error: rgba(200, 40, 30, 1); - - /* Other UI Elements */ - --scroll-color: rgba(50, 50, 55, 1); - --caret-color: rgba(0, 111, 255, 1); +.portal-theme { + /* Backgrounds */ + --terminal-bg: rgba(240, 240, 240, 0.9); + --button-bg: rgba(210, 210, 215, 1); + --input-field-bg: rgba(220, 220, 225, 0.8); + --button-selected-bg: rgba(255, 106, 0, 0.85); + --button-hover-bg: rgba(190, 190, 195, 1); + --scroll-bg: rgba(210, 210, 215, 1); + --scroll-inverse-bg: rgba(250, 250, 250, 1); + --scroll-active-bg: rgba(170, 170, 175, 1); + + /* Text & Foreground */ + --button-text: rgba(50, 50, 55, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(30, 30, 35, 1); + --input-text-color: rgba(30, 30, 35, 1); + --text-message: rgba(50, 50, 55, 1); + --text-warning: rgba(200, 80, 0, 1); + --text-input-echo: rgba(0, 111, 255, 1); + --text-shell: rgba(100, 100, 105, 1); + --text-error: rgba(200, 40, 30, 1); + + /* Other UI Elements */ + --scroll-color: rgba(50, 50, 55, 1); + --caret-color: rgba(0, 111, 255, 1); } \ No newline at end of file diff --git a/Styles/Themes/PureBlackWhiteTheme.uss b/Styles/Themes/PureBlackWhiteTheme.uss index b9a6400..e47c366 100644 --- a/Styles/Themes/PureBlackWhiteTheme.uss +++ b/Styles/Themes/PureBlackWhiteTheme.uss @@ -1,26 +1,26 @@ -.pure-black-white-theme { - /* Backgrounds */ - --terminal-bg: rgba(0, 0, 0, 0.85); - --button-bg: rgba(30, 30, 30, 1); - --input-field-bg: rgba(0, 0, 0, 0.75); - --button-selected-bg: rgba(255, 255, 255, 0.85); - --button-hover-bg: rgba(60, 60, 60, 0.7); - --scroll-bg: rgba(50, 50, 50, 1); - --scroll-inverse-bg: rgba(40, 40, 40, 1); - --scroll-active-bg: rgba(90, 90, 90, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 255, 255, 0.9); - --button-selected-text: rgba(0, 0, 0, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(255, 255, 255, 1.0); - --text-message: rgba(255, 255, 255, 1); - --text-warning: rgba(200, 200, 200, 1); - --text-input-echo: rgba(220, 220, 220, 1); - --text-shell: rgba(180, 180, 180, 1); - --text-error: rgba(255, 255, 255, 1); - - /* Other UI Elements */ - --scroll-color: rgba(150, 150, 150, 1); - --caret-color: rgba(255, 255, 255, 1.0); +.pure-black-white-theme { + /* Backgrounds */ + --terminal-bg: rgba(0, 0, 0, 0.85); + --button-bg: rgba(30, 30, 30, 1); + --input-field-bg: rgba(0, 0, 0, 0.75); + --button-selected-bg: rgba(255, 255, 255, 0.85); + --button-hover-bg: rgba(60, 60, 60, 0.7); + --scroll-bg: rgba(50, 50, 50, 1); + --scroll-inverse-bg: rgba(40, 40, 40, 1); + --scroll-active-bg: rgba(90, 90, 90, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 255, 255, 0.9); + --button-selected-text: rgba(0, 0, 0, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(255, 255, 255, 1.0); + --text-message: rgba(255, 255, 255, 1); + --text-warning: rgba(200, 200, 200, 1); + --text-input-echo: rgba(220, 220, 220, 1); + --text-shell: rgba(180, 180, 180, 1); + --text-error: rgba(255, 255, 255, 1); + + /* Other UI Elements */ + --scroll-color: rgba(150, 150, 150, 1); + --caret-color: rgba(255, 255, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/PurpleHazeTheme.uss b/Styles/Themes/PurpleHazeTheme.uss index a61e1cb..1b06bb7 100644 --- a/Styles/Themes/PurpleHazeTheme.uss +++ b/Styles/Themes/PurpleHazeTheme.uss @@ -1,26 +1,26 @@ -.purple-haze-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 25, 50, 0.85); - --button-bg: rgba(60, 40, 75, 1); - --input-field-bg: rgba(30, 15, 40, 0.7); - --button-selected-bg: rgba(220, 180, 255, 0.85); - --button-hover-bg: rgba(80, 60, 95, 0.7); - --scroll-bg: rgba(78, 58, 98, 1); - --scroll-inverse-bg: rgba(69, 49, 89, 1); - --scroll-active-bg: rgba(110, 90, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(230, 210, 255, 0.9); - --button-selected-text: rgba(40, 25, 50, 1); - --button-hover-text: rgba(245, 235, 255, 1); - --input-text-color: rgba(230, 210, 255, 1.0); - --text-message: rgba(230, 210, 255, 1); - --text-warning: rgba(255, 210, 120, 1); - --text-input-echo: rgba(180, 140, 220, 1); - --text-shell: rgba(160, 130, 190, 1); - --text-error: rgba(255, 130, 130, 1); - - /* Other UI Elements */ - --scroll-color: rgba(180, 140, 220, 1); - --caret-color: rgba(230, 210, 255, 1.0); +.purple-haze-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 25, 50, 0.85); + --button-bg: rgba(60, 40, 75, 1); + --input-field-bg: rgba(30, 15, 40, 0.7); + --button-selected-bg: rgba(220, 180, 255, 0.85); + --button-hover-bg: rgba(80, 60, 95, 0.7); + --scroll-bg: rgba(78, 58, 98, 1); + --scroll-inverse-bg: rgba(69, 49, 89, 1); + --scroll-active-bg: rgba(110, 90, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(230, 210, 255, 0.9); + --button-selected-text: rgba(40, 25, 50, 1); + --button-hover-text: rgba(245, 235, 255, 1); + --input-text-color: rgba(230, 210, 255, 1.0); + --text-message: rgba(230, 210, 255, 1); + --text-warning: rgba(255, 210, 120, 1); + --text-input-echo: rgba(180, 140, 220, 1); + --text-shell: rgba(160, 130, 190, 1); + --text-error: rgba(255, 130, 130, 1); + + /* Other UI Elements */ + --scroll-color: rgba(180, 140, 220, 1); + --caret-color: rgba(230, 210, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/RedAlertTheme.uss b/Styles/Themes/RedAlertTheme.uss index 16341a5..047a038 100644 --- a/Styles/Themes/RedAlertTheme.uss +++ b/Styles/Themes/RedAlertTheme.uss @@ -1,26 +1,26 @@ -.red-alert-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 10, 10, 0.8); - --button-bg: rgba(60, 20, 20, 1); - --input-field-bg: rgba(30, 5, 5, 0.7); - --button-selected-bg: rgba(255, 50, 50, 0.85); - --button-hover-bg: rgba(80, 40, 40, 0.7); - --scroll-bg: rgba(88, 48, 48, 1); - --scroll-inverse-bg: rgba(79, 39, 39, 1); - --scroll-active-bg: rgba(120, 80, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 180, 180, 0.9); - --button-selected-text: rgba(40, 10, 10, 1); - --button-hover-text: rgba(255, 210, 210, 1); - --input-text-color: rgba(255, 200, 200, 1.0); - --text-message: rgba(255, 200, 200, 1); - --text-warning: rgba(255, 220, 100, 1); - --text-input-echo: rgba(255, 150, 150, 1); - --text-shell: rgba(210, 150, 150, 1); - --text-error: rgba(255, 255, 255, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 50, 50, 1); - --caret-color: rgba(255, 200, 200, 1.0); -} +.red-alert-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 10, 10, 0.8); + --button-bg: rgba(60, 20, 20, 1); + --input-field-bg: rgba(30, 5, 5, 0.7); + --button-selected-bg: rgba(255, 50, 50, 0.85); + --button-hover-bg: rgba(80, 40, 40, 0.7); + --scroll-bg: rgba(88, 48, 48, 1); + --scroll-inverse-bg: rgba(79, 39, 39, 1); + --scroll-active-bg: rgba(120, 80, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 180, 180, 0.9); + --button-selected-text: rgba(40, 10, 10, 1); + --button-hover-text: rgba(255, 210, 210, 1); + --input-text-color: rgba(255, 200, 200, 1.0); + --text-message: rgba(255, 200, 200, 1); + --text-warning: rgba(255, 220, 100, 1); + --text-input-echo: rgba(255, 150, 150, 1); + --text-shell: rgba(210, 150, 150, 1); + --text-error: rgba(255, 255, 255, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 50, 50, 1); + --caret-color: rgba(255, 200, 200, 1.0); +} diff --git a/Styles/Themes/RedmondTheme.uss b/Styles/Themes/RedmondTheme.uss index 083172d..7044956 100644 --- a/Styles/Themes/RedmondTheme.uss +++ b/Styles/Themes/RedmondTheme.uss @@ -1,26 +1,26 @@ -.redmond-theme { - /* Backgrounds */ - --terminal-bg: rgba(240, 240, 240, 0.9); - --button-bg: rgba(225, 225, 225, 1); - --input-field-bg: rgba(255, 255, 255, 0.8); - --button-selected-bg: rgba(0, 120, 215, 0.85); - --button-hover-bg: rgba(200, 220, 240, 1); - --scroll-bg: rgba(225, 225, 225, 1); - --scroll-inverse-bg: rgba(200, 200, 200, 1); - --scroll-active-bg: rgba(160, 160, 160, 1); - - /* Text & Foreground */ - --button-text: rgba(0, 0, 0, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(0, 0, 0, 1); - --input-text-color: rgba(0, 0, 0, 1); - --text-message: rgba(0, 0, 0, 1); - --text-warning: rgba(180, 100, 0, 1); - --text-input-echo: rgba(0, 0, 180, 1); - --text-shell: rgba(80, 80, 80, 1); - --text-error: rgba(180, 0, 0, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 100, 100, 1); - --caret-color: rgba(0, 0, 0, 1); +.redmond-theme { + /* Backgrounds */ + --terminal-bg: rgba(240, 240, 240, 0.9); + --button-bg: rgba(225, 225, 225, 1); + --input-field-bg: rgba(255, 255, 255, 0.8); + --button-selected-bg: rgba(0, 120, 215, 0.85); + --button-hover-bg: rgba(200, 220, 240, 1); + --scroll-bg: rgba(225, 225, 225, 1); + --scroll-inverse-bg: rgba(200, 200, 200, 1); + --scroll-active-bg: rgba(160, 160, 160, 1); + + /* Text & Foreground */ + --button-text: rgba(0, 0, 0, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(0, 0, 0, 1); + --input-text-color: rgba(0, 0, 0, 1); + --text-message: rgba(0, 0, 0, 1); + --text-warning: rgba(180, 100, 0, 1); + --text-input-echo: rgba(0, 0, 180, 1); + --text-shell: rgba(80, 80, 80, 1); + --text-error: rgba(180, 0, 0, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 100, 100, 1); + --caret-color: rgba(0, 0, 0, 1); } \ No newline at end of file diff --git a/Styles/Themes/RedwoodTheme.uss b/Styles/Themes/RedwoodTheme.uss index 809832b..c2dfea6 100644 --- a/Styles/Themes/RedwoodTheme.uss +++ b/Styles/Themes/RedwoodTheme.uss @@ -1,26 +1,26 @@ -.redwood-theme { - /* Backgrounds */ - --terminal-bg: rgba(80, 45, 35, 0.9); - --button-bg: rgba(105, 60, 45, 1); - --input-field-bg: rgba(65, 35, 25, 0.7); - --button-selected-bg: rgba(170, 200, 150, 0.85); - --button-hover-bg: rgba(125, 75, 60, 0.7); - --scroll-bg: rgba(115, 70, 55, 1); - --scroll-inverse-bg: rgba(105, 60, 45, 1); - --scroll-active-bg: rgba(145, 95, 80, 1); - - /* Text & Foreground */ - --button-text: rgba(210, 190, 170, 0.9); - --button-selected-text: rgba(50, 60, 40, 1); - --button-hover-text: rgba(230, 210, 190, 1); - --input-text-color: rgba(210, 190, 170, 1.0); - --text-message: rgba(210, 190, 170, 1); - --text-warning: rgba(200, 140, 80, 1); - --text-input-echo: rgba(100, 130, 100, 1); - --text-shell: rgba(140, 100, 80, 1); - --text-error: rgba(190, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(170, 200, 150, 1); - --caret-color: rgba(210, 190, 170, 1.0); +.redwood-theme { + /* Backgrounds */ + --terminal-bg: rgba(80, 45, 35, 0.9); + --button-bg: rgba(105, 60, 45, 1); + --input-field-bg: rgba(65, 35, 25, 0.7); + --button-selected-bg: rgba(170, 200, 150, 0.85); + --button-hover-bg: rgba(125, 75, 60, 0.7); + --scroll-bg: rgba(115, 70, 55, 1); + --scroll-inverse-bg: rgba(105, 60, 45, 1); + --scroll-active-bg: rgba(145, 95, 80, 1); + + /* Text & Foreground */ + --button-text: rgba(210, 190, 170, 0.9); + --button-selected-text: rgba(50, 60, 40, 1); + --button-hover-text: rgba(230, 210, 190, 1); + --input-text-color: rgba(210, 190, 170, 1.0); + --text-message: rgba(210, 190, 170, 1); + --text-warning: rgba(200, 140, 80, 1); + --text-input-echo: rgba(100, 130, 100, 1); + --text-shell: rgba(140, 100, 80, 1); + --text-error: rgba(190, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(170, 200, 150, 1); + --caret-color: rgba(210, 190, 170, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/RosePineTheme.uss b/Styles/Themes/RosePineTheme.uss index 5ebe2ff..249188b 100644 --- a/Styles/Themes/RosePineTheme.uss +++ b/Styles/Themes/RosePineTheme.uss @@ -1,26 +1,26 @@ -.rose-pine-theme { - /* Backgrounds */ - --terminal-bg: rgba(31, 29, 46, 0.9); - --button-bg: rgba(38, 35, 58, 1); - --input-field-bg: rgba(25, 23, 39, 0.8); - --button-selected-bg: rgba(235, 111, 146, 0.85); - --button-hover-bg: rgba(60, 56, 82, 1); - --scroll-bg: rgba(38, 35, 58, 1); - --scroll-inverse-bg: rgba(86, 80, 110, 1); - --scroll-active-bg: rgba(110, 103, 139, 1); - - /* Text & Foreground */ - --button-text: rgba(224, 220, 240, 0.9); - --button-selected-text: rgba(31, 29, 46, 1); - --button-hover-text: rgba(224, 220, 240, 1); - --input-text-color: rgba(224, 220, 240, 1.0); - --text-message: rgba(224, 220, 240, 1); - --text-warning: rgba(246, 193, 119, 1); - --text-input-echo: rgba(49, 116, 143, 1); - --text-shell: rgba(110, 103, 139, 1); - --text-error: rgba(235, 111, 146, 1); - - /* Other UI Elements */ - --scroll-color: rgba(224, 220, 240, 1); - --caret-color: rgba(224, 220, 240, 1.0); +.rose-pine-theme { + /* Backgrounds */ + --terminal-bg: rgba(31, 29, 46, 0.9); + --button-bg: rgba(38, 35, 58, 1); + --input-field-bg: rgba(25, 23, 39, 0.8); + --button-selected-bg: rgba(235, 111, 146, 0.85); + --button-hover-bg: rgba(60, 56, 82, 1); + --scroll-bg: rgba(38, 35, 58, 1); + --scroll-inverse-bg: rgba(86, 80, 110, 1); + --scroll-active-bg: rgba(110, 103, 139, 1); + + /* Text & Foreground */ + --button-text: rgba(224, 220, 240, 0.9); + --button-selected-text: rgba(31, 29, 46, 1); + --button-hover-text: rgba(224, 220, 240, 1); + --input-text-color: rgba(224, 220, 240, 1.0); + --text-message: rgba(224, 220, 240, 1); + --text-warning: rgba(246, 193, 119, 1); + --text-input-echo: rgba(49, 116, 143, 1); + --text-shell: rgba(110, 103, 139, 1); + --text-error: rgba(235, 111, 146, 1); + + /* Other UI Elements */ + --scroll-color: rgba(224, 220, 240, 1); + --caret-color: rgba(224, 220, 240, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SavannaTheme.uss b/Styles/Themes/SavannaTheme.uss index 56f4fbd..96ef708 100644 --- a/Styles/Themes/SavannaTheme.uss +++ b/Styles/Themes/SavannaTheme.uss @@ -1,26 +1,26 @@ -.savanna-theme { - /* Backgrounds */ - --terminal-bg: rgba(245, 230, 200, 0.75); - --button-bg: rgba(220, 205, 175, 1); - --input-field-bg: rgba(255, 245, 220, 0.7); - --button-selected-bg: rgba(100, 80, 60, 0.85); - --button-hover-bg: rgba(200, 185, 155, 0.7); - --scroll-bg: rgba(210, 195, 165, 1); - --scroll-inverse-bg: rgba(220, 205, 175, 1); - --scroll-active-bg: rgba(140, 120, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(80, 60, 40, 0.9); - --button-selected-text: rgba(245, 230, 200, 1); - --button-hover-text: rgba(60, 40, 20, 1); - --input-text-color: rgba(80, 60, 40, 1.0); - --text-message: rgba(80, 60, 40, 1); - --text-warning: rgba(200, 120, 50, 1); - --text-input-echo: rgba(100, 130, 80, 1); - --text-shell: rgba(130, 100, 80, 1); - --text-error: rgba(180, 60, 60, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 80, 60, 1); - --caret-color: rgba(80, 60, 40, 1.0); +.savanna-theme { + /* Backgrounds */ + --terminal-bg: rgba(245, 230, 200, 0.75); + --button-bg: rgba(220, 205, 175, 1); + --input-field-bg: rgba(255, 245, 220, 0.7); + --button-selected-bg: rgba(100, 80, 60, 0.85); + --button-hover-bg: rgba(200, 185, 155, 0.7); + --scroll-bg: rgba(210, 195, 165, 1); + --scroll-inverse-bg: rgba(220, 205, 175, 1); + --scroll-active-bg: rgba(140, 120, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(80, 60, 40, 0.9); + --button-selected-text: rgba(245, 230, 200, 1); + --button-hover-text: rgba(60, 40, 20, 1); + --input-text-color: rgba(80, 60, 40, 1.0); + --text-message: rgba(80, 60, 40, 1); + --text-warning: rgba(200, 120, 50, 1); + --text-input-echo: rgba(100, 130, 80, 1); + --text-shell: rgba(130, 100, 80, 1); + --text-error: rgba(180, 60, 60, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 80, 60, 1); + --caret-color: rgba(80, 60, 40, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SepiaTheme.uss b/Styles/Themes/SepiaTheme.uss index e643c66..f7dc131 100644 --- a/Styles/Themes/SepiaTheme.uss +++ b/Styles/Themes/SepiaTheme.uss @@ -1,26 +1,26 @@ -.sepia-theme { - /* Backgrounds */ - --terminal-bg: rgba(90, 70, 50, 0.8); - --button-bg: rgba(110, 90, 70, 1); - --input-field-bg: rgba(80, 60, 40, 0.7); - --button-selected-bg: rgba(240, 230, 210, 0.85); - --button-hover-bg: rgba(130, 110, 90, 0.7); - --scroll-bg: rgba(120, 100, 80, 1); - --scroll-inverse-bg: rgba(110, 90, 70, 1); - --scroll-active-bg: rgba(150, 130, 110, 1); - - /* Text & Foreground */ - --button-text: rgba(240, 230, 210, 0.9); - --button-selected-text: rgba(90, 70, 50, 1); - --button-hover-text: rgba(250, 245, 230, 1); - --input-text-color: rgba(240, 230, 210, 1.0); - --text-message: rgba(240, 230, 210, 1); - --text-warning: rgba(200, 150, 100, 1); - --text-input-echo: rgba(180, 160, 140, 1); - --text-shell: rgba(160, 140, 120, 1); - --text-error: rgba(180, 90, 90, 1); - - /* Other UI Elements */ - --scroll-color: rgba(160, 140, 120, 1); - --caret-color: rgba(240, 230, 210, 1.0); +.sepia-theme { + /* Backgrounds */ + --terminal-bg: rgba(90, 70, 50, 0.8); + --button-bg: rgba(110, 90, 70, 1); + --input-field-bg: rgba(80, 60, 40, 0.7); + --button-selected-bg: rgba(240, 230, 210, 0.85); + --button-hover-bg: rgba(130, 110, 90, 0.7); + --scroll-bg: rgba(120, 100, 80, 1); + --scroll-inverse-bg: rgba(110, 90, 70, 1); + --scroll-active-bg: rgba(150, 130, 110, 1); + + /* Text & Foreground */ + --button-text: rgba(240, 230, 210, 0.9); + --button-selected-text: rgba(90, 70, 50, 1); + --button-hover-text: rgba(250, 245, 230, 1); + --input-text-color: rgba(240, 230, 210, 1.0); + --text-message: rgba(240, 230, 210, 1); + --text-warning: rgba(200, 150, 100, 1); + --text-input-echo: rgba(180, 160, 140, 1); + --text-shell: rgba(160, 140, 120, 1); + --text-error: rgba(180, 90, 90, 1); + + /* Other UI Elements */ + --scroll-color: rgba(160, 140, 120, 1); + --caret-color: rgba(240, 230, 210, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/ShadesOfPurpleTheme.uss b/Styles/Themes/ShadesOfPurpleTheme.uss index 94c4a48..107ae43 100644 --- a/Styles/Themes/ShadesOfPurpleTheme.uss +++ b/Styles/Themes/ShadesOfPurpleTheme.uss @@ -1,26 +1,26 @@ -.shades-of-purple-theme { - /* Backgrounds */ - --terminal-bg: rgba(45, 33, 87, 0.9); - --button-bg: rgba(60, 45, 110, 1); - --input-field-bg: rgba(38, 28, 74, 0.8); - --button-selected-bg: rgba(173, 255, 47, 0.85); - --button-hover-bg: rgba(75, 56, 138, 1); - --scroll-bg: rgba(60, 45, 110, 1); - --scroll-inverse-bg: rgba(90, 68, 165, 1); - --scroll-active-bg: rgba(105, 80, 193, 1); - - /* Text & Foreground */ - --button-text: rgba(160, 134, 255, 0.9); - --button-selected-text: rgba(45, 33, 87, 1); - --button-hover-text: rgba(180, 150, 255, 1); - --input-text-color: rgba(160, 134, 255, 1.0); - --text-message: rgba(160, 134, 255, 1); - --text-warning: rgba(255, 216, 102, 1); - --text-input-echo: rgba(46, 196, 255, 1); - --text-shell: rgba(128, 107, 204, 1); - --text-error: rgba(255, 102, 102, 1); - - /* Other UI Elements */ - --scroll-color: rgba(160, 134, 255, 1); - --caret-color: rgba(173, 255, 47, 1.0); +.shades-of-purple-theme { + /* Backgrounds */ + --terminal-bg: rgba(45, 33, 87, 0.9); + --button-bg: rgba(60, 45, 110, 1); + --input-field-bg: rgba(38, 28, 74, 0.8); + --button-selected-bg: rgba(173, 255, 47, 0.85); + --button-hover-bg: rgba(75, 56, 138, 1); + --scroll-bg: rgba(60, 45, 110, 1); + --scroll-inverse-bg: rgba(90, 68, 165, 1); + --scroll-active-bg: rgba(105, 80, 193, 1); + + /* Text & Foreground */ + --button-text: rgba(160, 134, 255, 0.9); + --button-selected-text: rgba(45, 33, 87, 1); + --button-hover-text: rgba(180, 150, 255, 1); + --input-text-color: rgba(160, 134, 255, 1.0); + --text-message: rgba(160, 134, 255, 1); + --text-warning: rgba(255, 216, 102, 1); + --text-input-echo: rgba(46, 196, 255, 1); + --text-shell: rgba(128, 107, 204, 1); + --text-error: rgba(255, 102, 102, 1); + + /* Other UI Elements */ + --scroll-color: rgba(160, 134, 255, 1); + --caret-color: rgba(173, 255, 47, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SlateTheme.uss b/Styles/Themes/SlateTheme.uss index 3abcdbf..bfc7038 100644 --- a/Styles/Themes/SlateTheme.uss +++ b/Styles/Themes/SlateTheme.uss @@ -1,26 +1,26 @@ -.slate-theme { - /* Backgrounds */ - --terminal-bg: rgba(70, 80, 90, 0.88); - --button-bg: rgba(90, 100, 110, 1); - --input-field-bg: rgba(60, 70, 80, 0.7); - --button-selected-bg: rgba(200, 210, 220, 0.85); - --button-hover-bg: rgba(110, 120, 130, 0.7); - --scroll-bg: rgba(100, 110, 120, 1); - --scroll-inverse-bg: rgba(90, 100, 110, 1); - --scroll-active-bg: rgba(130, 140, 150, 1); - - /* Text & Foreground */ - --button-text: rgba(210, 220, 230, 0.9); - --button-selected-text: rgba(70, 80, 90, 1); - --button-hover-text: rgba(230, 240, 250, 1); - --input-text-color: rgba(210, 220, 230, 1.0); - --text-message: rgba(210, 220, 230, 1); - --text-warning: rgba(210, 180, 130, 1); - --text-input-echo: rgba(150, 160, 170, 1); - --text-shell: rgba(120, 130, 140, 1); - --text-error: rgba(200, 130, 140, 1); - - /* Other UI Elements */ - --scroll-color: rgba(150, 160, 170, 1); - --caret-color: rgba(210, 220, 230, 1.0); +.slate-theme { + /* Backgrounds */ + --terminal-bg: rgba(70, 80, 90, 0.88); + --button-bg: rgba(90, 100, 110, 1); + --input-field-bg: rgba(60, 70, 80, 0.7); + --button-selected-bg: rgba(200, 210, 220, 0.85); + --button-hover-bg: rgba(110, 120, 130, 0.7); + --scroll-bg: rgba(100, 110, 120, 1); + --scroll-inverse-bg: rgba(90, 100, 110, 1); + --scroll-active-bg: rgba(130, 140, 150, 1); + + /* Text & Foreground */ + --button-text: rgba(210, 220, 230, 0.9); + --button-selected-text: rgba(70, 80, 90, 1); + --button-hover-text: rgba(230, 240, 250, 1); + --input-text-color: rgba(210, 220, 230, 1.0); + --text-message: rgba(210, 220, 230, 1); + --text-warning: rgba(210, 180, 130, 1); + --text-input-echo: rgba(150, 160, 170, 1); + --text-shell: rgba(120, 130, 140, 1); + --text-error: rgba(200, 130, 140, 1); + + /* Other UI Elements */ + --scroll-color: rgba(150, 160, 170, 1); + --caret-color: rgba(210, 220, 230, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SlimeTheme.uss b/Styles/Themes/SlimeTheme.uss index c10b7a0..51d938d 100644 --- a/Styles/Themes/SlimeTheme.uss +++ b/Styles/Themes/SlimeTheme.uss @@ -1,26 +1,26 @@ -.slime-theme { - /* Backgrounds */ - --terminal-bg: rgba(0, 116, 188, 0.9); - --button-bg: rgba(0, 90, 150, 1); - --input-field-bg: rgba(0, 100, 165, 0.8); - --button-selected-bg: rgba(255, 255, 255, 0.85); - --button-hover-bg: rgba(30, 136, 208, 1); - --scroll-bg: rgba(0, 90, 150, 1); - --scroll-inverse-bg: rgba(0, 70, 130, 1); - --scroll-active-bg: rgba(255, 255, 255, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 255, 255, 0.9); - --button-selected-text: rgba(0, 116, 188, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(255, 255, 255, 1); - --text-message: rgba(240, 240, 240, 1); - --text-warning: rgba(255, 215, 0, 1); - --text-input-echo: rgba(200, 230, 255, 1); - --text-shell: rgba(180, 210, 230, 1); - --text-error: rgba(255, 80, 60, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 255, 255, 1); - --caret-color: rgba(255, 255, 255, 1); +.slime-theme { + /* Backgrounds */ + --terminal-bg: rgba(0, 116, 188, 0.9); + --button-bg: rgba(0, 90, 150, 1); + --input-field-bg: rgba(0, 100, 165, 0.8); + --button-selected-bg: rgba(255, 255, 255, 0.85); + --button-hover-bg: rgba(30, 136, 208, 1); + --scroll-bg: rgba(0, 90, 150, 1); + --scroll-inverse-bg: rgba(0, 70, 130, 1); + --scroll-active-bg: rgba(255, 255, 255, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 255, 255, 0.9); + --button-selected-text: rgba(0, 116, 188, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(255, 255, 255, 1); + --text-message: rgba(240, 240, 240, 1); + --text-warning: rgba(255, 215, 0, 1); + --text-input-echo: rgba(200, 230, 255, 1); + --text-shell: rgba(180, 210, 230, 1); + --text-error: rgba(255, 80, 60, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 255, 255, 1); + --caret-color: rgba(255, 255, 255, 1); } \ No newline at end of file diff --git a/Styles/Themes/SolarizedDarkTheme.uss b/Styles/Themes/SolarizedDarkTheme.uss index 21b0688..bc8a5d5 100644 --- a/Styles/Themes/SolarizedDarkTheme.uss +++ b/Styles/Themes/SolarizedDarkTheme.uss @@ -1,26 +1,26 @@ -.solarized-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(0, 43, 54, 0.9); - --button-bg: rgba(7, 54, 66, 1); - --input-field-bg: rgba(0, 30, 38, 0.8); - --button-selected-bg: rgba(181, 137, 0, 0.85); - --button-hover-bg: rgba(12, 70, 86, 1); - --scroll-bg: rgba(7, 54, 66, 1); - --scroll-inverse-bg: rgba(88, 110, 117, 1); - --scroll-active-bg: rgba(101, 123, 131, 1); - - /* Text & Foreground */ - --button-text: rgba(131, 148, 150, 0.9); - --button-selected-text: rgba(0, 43, 54, 1); - --button-hover-text: rgba(147, 161, 161, 1); - --input-text-color: rgba(131, 148, 150, 1.0); - --text-message: rgba(131, 148, 150, 1); - --text-warning: rgba(203, 75, 22, 1); - --text-input-echo: rgba(38, 139, 210, 1); - --text-shell: rgba(88, 110, 117, 1); - --text-error: rgba(220, 50, 47, 1); - - /* Other UI Elements */ - --scroll-color: rgba(131, 148, 150, 1); - --caret-color: rgba(147, 161, 161, 1.0); +.solarized-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(0, 43, 54, 0.9); + --button-bg: rgba(7, 54, 66, 1); + --input-field-bg: rgba(0, 30, 38, 0.8); + --button-selected-bg: rgba(181, 137, 0, 0.85); + --button-hover-bg: rgba(12, 70, 86, 1); + --scroll-bg: rgba(7, 54, 66, 1); + --scroll-inverse-bg: rgba(88, 110, 117, 1); + --scroll-active-bg: rgba(101, 123, 131, 1); + + /* Text & Foreground */ + --button-text: rgba(131, 148, 150, 0.9); + --button-selected-text: rgba(0, 43, 54, 1); + --button-hover-text: rgba(147, 161, 161, 1); + --input-text-color: rgba(131, 148, 150, 1.0); + --text-message: rgba(131, 148, 150, 1); + --text-warning: rgba(203, 75, 22, 1); + --text-input-echo: rgba(38, 139, 210, 1); + --text-shell: rgba(88, 110, 117, 1); + --text-error: rgba(220, 50, 47, 1); + + /* Other UI Elements */ + --scroll-color: rgba(131, 148, 150, 1); + --caret-color: rgba(147, 161, 161, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SolarizedLightTheme.uss b/Styles/Themes/SolarizedLightTheme.uss index bcfff88..c013082 100644 --- a/Styles/Themes/SolarizedLightTheme.uss +++ b/Styles/Themes/SolarizedLightTheme.uss @@ -1,26 +1,26 @@ -.solarized-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(253, 246, 227, 0.9); - --button-bg: rgba(238, 232, 213, 1); - --input-field-bg: rgba(255, 255, 255, 0.8); - --button-selected-bg: rgba(38, 139, 210, 0.85); - --button-hover-bg: rgba(220, 215, 195, 1); - --scroll-bg: rgba(238, 232, 213, 1); - --scroll-inverse-bg: rgba(147, 161, 161, 1); - --scroll-active-bg: rgba(101, 123, 131, 1); - - /* Text & Foreground */ - --button-text: rgba(101, 123, 131, 0.9); - --button-selected-text: rgba(253, 246, 227, 1); - --button-hover-text: rgba(88, 110, 117, 1); - --input-text-color: rgba(88, 110, 117, 1.0); - --text-message: rgba(101, 123, 131, 1); - --text-warning: rgba(203, 75, 22, 1); - --text-input-echo: rgba(38, 139, 210, 1); - --text-shell: rgba(147, 161, 161, 1); - --text-error: rgba(220, 50, 47, 1); - - /* Other UI Elements */ - --scroll-color: rgba(101, 123, 131, 1); - --caret-color: rgba(88, 110, 117, 1.0); +.solarized-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(253, 246, 227, 0.9); + --button-bg: rgba(238, 232, 213, 1); + --input-field-bg: rgba(255, 255, 255, 0.8); + --button-selected-bg: rgba(38, 139, 210, 0.85); + --button-hover-bg: rgba(220, 215, 195, 1); + --scroll-bg: rgba(238, 232, 213, 1); + --scroll-inverse-bg: rgba(147, 161, 161, 1); + --scroll-active-bg: rgba(101, 123, 131, 1); + + /* Text & Foreground */ + --button-text: rgba(101, 123, 131, 0.9); + --button-selected-text: rgba(253, 246, 227, 1); + --button-hover-text: rgba(88, 110, 117, 1); + --input-text-color: rgba(88, 110, 117, 1.0); + --text-message: rgba(101, 123, 131, 1); + --text-warning: rgba(203, 75, 22, 1); + --text-input-echo: rgba(38, 139, 210, 1); + --text-shell: rgba(147, 161, 161, 1); + --text-error: rgba(220, 50, 47, 1); + + /* Other UI Elements */ + --scroll-color: rgba(101, 123, 131, 1); + --caret-color: rgba(88, 110, 117, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/StardewValleyTheme.uss b/Styles/Themes/StardewValleyTheme.uss index 36fa731..cf651b2 100644 --- a/Styles/Themes/StardewValleyTheme.uss +++ b/Styles/Themes/StardewValleyTheme.uss @@ -1,26 +1,26 @@ -.stardew-valley-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 239, 191, 0.9); - --button-bg: rgba(140, 82, 50, 1); - --input-field-bg: rgba(240, 220, 170, 0.8); - --button-selected-bg: rgba(76, 175, 80, 0.85); - --button-hover-bg: rgba(170, 110, 75, 1); - --scroll-bg: rgba(140, 82, 50, 1); - --scroll-inverse-bg: rgba(255, 245, 210, 1); - --scroll-active-bg: rgba(100, 50, 30, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 239, 191, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(255, 245, 210, 1); - --input-text-color: rgba(80, 40, 20, 1); - --text-message: rgba(80, 40, 20, 1); - --text-warning: rgba(255, 165, 0, 1); - --text-input-echo: rgba(66, 134, 244, 1); - --text-shell: rgba(110, 70, 50, 1); - --text-error: rgba(220, 40, 40, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 239, 191, 1); - --caret-color: rgba(80, 40, 20, 1); +.stardew-valley-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 239, 191, 0.9); + --button-bg: rgba(140, 82, 50, 1); + --input-field-bg: rgba(240, 220, 170, 0.8); + --button-selected-bg: rgba(76, 175, 80, 0.85); + --button-hover-bg: rgba(170, 110, 75, 1); + --scroll-bg: rgba(140, 82, 50, 1); + --scroll-inverse-bg: rgba(255, 245, 210, 1); + --scroll-active-bg: rgba(100, 50, 30, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 239, 191, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(255, 245, 210, 1); + --input-text-color: rgba(80, 40, 20, 1); + --text-message: rgba(80, 40, 20, 1); + --text-warning: rgba(255, 165, 0, 1); + --text-input-echo: rgba(66, 134, 244, 1); + --text-shell: rgba(110, 70, 50, 1); + --text-error: rgba(220, 40, 40, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 239, 191, 1); + --caret-color: rgba(80, 40, 20, 1); } \ No newline at end of file diff --git a/Styles/Themes/StarfieldTheme.uss b/Styles/Themes/StarfieldTheme.uss index 8aa2e01..1bd36cf 100644 --- a/Styles/Themes/StarfieldTheme.uss +++ b/Styles/Themes/StarfieldTheme.uss @@ -1,26 +1,26 @@ -.starfield-theme { - /* Backgrounds */ - --terminal-bg: rgba(10, 10, 20, 0.95); - --button-bg: rgba(20, 20, 35, 1); - --input-field-bg: rgba(5, 5, 15, 0.7); - --button-selected-bg: rgba(200, 180, 255, 0.85); - --button-hover-bg: rgba(30, 30, 50, 0.7); - --scroll-bg: rgba(25, 25, 40, 1); - --scroll-inverse-bg: rgba(20, 20, 35, 1); - --scroll-active-bg: rgba(45, 45, 65, 1); - - /* Text & Foreground */ - --button-text: rgba(230, 230, 255, 0.9); - --button-selected-text: rgba(10, 10, 20, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(230, 230, 255, 1.0); - --text-message: rgba(230, 230, 255, 1); - --text-warning: rgba(255, 255, 180, 1); - --text-input-echo: rgba(150, 220, 255, 1); - --text-shell: rgba(180, 180, 220, 1); - --text-error: rgba(255, 100, 150, 1); - - /* Other UI Elements */ - --scroll-color: rgba(200, 180, 255, 1); - --caret-color: rgba(255, 255, 255, 1.0); +.starfield-theme { + /* Backgrounds */ + --terminal-bg: rgba(10, 10, 20, 0.95); + --button-bg: rgba(20, 20, 35, 1); + --input-field-bg: rgba(5, 5, 15, 0.7); + --button-selected-bg: rgba(200, 180, 255, 0.85); + --button-hover-bg: rgba(30, 30, 50, 0.7); + --scroll-bg: rgba(25, 25, 40, 1); + --scroll-inverse-bg: rgba(20, 20, 35, 1); + --scroll-active-bg: rgba(45, 45, 65, 1); + + /* Text & Foreground */ + --button-text: rgba(230, 230, 255, 0.9); + --button-selected-text: rgba(10, 10, 20, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(230, 230, 255, 1.0); + --text-message: rgba(230, 230, 255, 1); + --text-warning: rgba(255, 255, 180, 1); + --text-input-echo: rgba(150, 220, 255, 1); + --text-shell: rgba(180, 180, 220, 1); + --text-error: rgba(255, 100, 150, 1); + + /* Other UI Elements */ + --scroll-color: rgba(200, 180, 255, 1); + --caret-color: rgba(255, 255, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SteampunkTheme.uss b/Styles/Themes/SteampunkTheme.uss index d4a59f4..b36bb2c 100644 --- a/Styles/Themes/SteampunkTheme.uss +++ b/Styles/Themes/SteampunkTheme.uss @@ -1,26 +1,26 @@ -.steampunk-theme { - /* Backgrounds */ - --terminal-bg: rgba(100, 80, 60, 0.85); - --button-bg: rgba(125, 100, 75, 1); - --input-field-bg: rgba(85, 65, 45, 0.7); - --button-selected-bg: rgba(50, 60, 70, 0.85); - --button-hover-bg: rgba(145, 120, 95, 0.7); - --scroll-bg: rgba(135, 110, 85, 1); - --scroll-inverse-bg: rgba(125, 100, 75, 1); - --scroll-active-bg: rgba(165, 140, 115, 1); - - /* Text & Foreground */ - --button-text: rgba(240, 230, 210, 0.9); - --button-selected-text: rgba(220, 210, 190, 1); - --button-hover-text: rgba(255, 250, 235, 1); - --input-text-color: rgba(240, 230, 210, 1.0); - --text-message: rgba(240, 230, 210, 1); - --text-warning: rgba(180, 120, 70, 1); - --text-input-echo: rgba(100, 140, 130, 1); - --text-shell: rgba(160, 140, 120, 1); - --text-error: rgba(170, 70, 70, 1); - - /* Other UI Elements */ - --scroll-color: rgba(50, 60, 70, 1); - --caret-color: rgba(240, 230, 210, 1.0); +.steampunk-theme { + /* Backgrounds */ + --terminal-bg: rgba(100, 80, 60, 0.85); + --button-bg: rgba(125, 100, 75, 1); + --input-field-bg: rgba(85, 65, 45, 0.7); + --button-selected-bg: rgba(50, 60, 70, 0.85); + --button-hover-bg: rgba(145, 120, 95, 0.7); + --scroll-bg: rgba(135, 110, 85, 1); + --scroll-inverse-bg: rgba(125, 100, 75, 1); + --scroll-active-bg: rgba(165, 140, 115, 1); + + /* Text & Foreground */ + --button-text: rgba(240, 230, 210, 0.9); + --button-selected-text: rgba(220, 210, 190, 1); + --button-hover-text: rgba(255, 250, 235, 1); + --input-text-color: rgba(240, 230, 210, 1.0); + --text-message: rgba(240, 230, 210, 1); + --text-warning: rgba(180, 120, 70, 1); + --text-input-echo: rgba(100, 140, 130, 1); + --text-shell: rgba(160, 140, 120, 1); + --text-error: rgba(170, 70, 70, 1); + + /* Other UI Elements */ + --scroll-color: rgba(50, 60, 70, 1); + --caret-color: rgba(240, 230, 210, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SublimeTheme.uss b/Styles/Themes/SublimeTheme.uss index c010e1a..1c760bb 100644 --- a/Styles/Themes/SublimeTheme.uss +++ b/Styles/Themes/SublimeTheme.uss @@ -1,26 +1,26 @@ -.sublime-theme { - /* Backgrounds */ - --terminal-bg: rgba(41, 42, 36, 0.9); - --button-bg: rgba(58, 59, 52, 1); - --input-field-bg: rgba(35, 36, 31, 0.8); - --button-selected-bg: rgba(253, 151, 31, 0.85); - --button-hover-bg: rgba(75, 76, 69, 1); - --scroll-bg: rgba(58, 59, 52, 1); - --scroll-inverse-bg: rgba(95, 97, 88, 1); - --scroll-active-bg: rgba(115, 117, 106, 1); - - /* Text & Foreground */ - --button-text: rgba(248, 248, 242, 0.9); - --button-selected-text: rgba(41, 42, 36, 1); - --button-hover-text: rgba(248, 248, 242, 1); - --input-text-color: rgba(248, 248, 242, 1.0); - --text-message: rgba(248, 248, 242, 1); - --text-warning: rgba(230, 219, 116, 1); - --text-input-echo: rgba(102, 217, 239, 1); - --text-shell: rgba(117, 113, 94, 1); - --text-error: rgba(249, 38, 114, 1); - - /* Other UI Elements */ - --scroll-color: rgba(248, 248, 242, 1); - --caret-color: rgba(248, 248, 242, 1.0); +.sublime-theme { + /* Backgrounds */ + --terminal-bg: rgba(41, 42, 36, 0.9); + --button-bg: rgba(58, 59, 52, 1); + --input-field-bg: rgba(35, 36, 31, 0.8); + --button-selected-bg: rgba(253, 151, 31, 0.85); + --button-hover-bg: rgba(75, 76, 69, 1); + --scroll-bg: rgba(58, 59, 52, 1); + --scroll-inverse-bg: rgba(95, 97, 88, 1); + --scroll-active-bg: rgba(115, 117, 106, 1); + + /* Text & Foreground */ + --button-text: rgba(248, 248, 242, 0.9); + --button-selected-text: rgba(41, 42, 36, 1); + --button-hover-text: rgba(248, 248, 242, 1); + --input-text-color: rgba(248, 248, 242, 1.0); + --text-message: rgba(248, 248, 242, 1); + --text-warning: rgba(230, 219, 116, 1); + --text-input-echo: rgba(102, 217, 239, 1); + --text-shell: rgba(117, 113, 94, 1); + --text-error: rgba(249, 38, 114, 1); + + /* Other UI Elements */ + --scroll-color: rgba(248, 248, 242, 1); + --caret-color: rgba(248, 248, 242, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SubtleGreyTheme.uss b/Styles/Themes/SubtleGreyTheme.uss index 348833b..c2ef2d3 100644 --- a/Styles/Themes/SubtleGreyTheme.uss +++ b/Styles/Themes/SubtleGreyTheme.uss @@ -1,26 +1,26 @@ -.subtle-gray-theme { - /* Backgrounds */ - --terminal-bg: rgba(55, 55, 55, 0.8); - --button-bg: rgba(75, 75, 75, 1); - --input-field-bg: rgba(45, 45, 45, 0.7); - --button-selected-bg: rgba(180, 180, 180, 0.85); - --button-hover-bg: rgba(95, 95, 95, 0.7); - --scroll-bg: rgba(90, 90, 90, 1); - --scroll-inverse-bg: rgba(80, 80, 80, 1); - --scroll-active-bg: rgba(130, 130, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(210, 210, 210, 0.9); - --button-selected-text: rgba(40, 40, 40, 1); - --button-hover-text: rgba(235, 235, 235, 1); - --input-text-color: rgba(220, 220, 220, 1.0); - --text-message: rgba(220, 220, 220, 1); - --text-warning: rgba(220, 180, 90, 1); - --text-input-echo: rgba(160, 190, 210, 1); - --text-shell: rgba(170, 170, 170, 1); - --text-error: rgba(210, 100, 100, 1); - - /* Other UI Elements */ - --scroll-color: rgba(150, 150, 150, 1); - --caret-color: rgba(220, 220, 220, 1.0); +.subtle-gray-theme { + /* Backgrounds */ + --terminal-bg: rgba(55, 55, 55, 0.8); + --button-bg: rgba(75, 75, 75, 1); + --input-field-bg: rgba(45, 45, 45, 0.7); + --button-selected-bg: rgba(180, 180, 180, 0.85); + --button-hover-bg: rgba(95, 95, 95, 0.7); + --scroll-bg: rgba(90, 90, 90, 1); + --scroll-inverse-bg: rgba(80, 80, 80, 1); + --scroll-active-bg: rgba(130, 130, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(210, 210, 210, 0.9); + --button-selected-text: rgba(40, 40, 40, 1); + --button-hover-text: rgba(235, 235, 235, 1); + --input-text-color: rgba(220, 220, 220, 1.0); + --text-message: rgba(220, 220, 220, 1); + --text-warning: rgba(220, 180, 90, 1); + --text-input-echo: rgba(160, 190, 210, 1); + --text-shell: rgba(170, 170, 170, 1); + --text-error: rgba(210, 100, 100, 1); + + /* Other UI Elements */ + --scroll-color: rgba(150, 150, 150, 1); + --caret-color: rgba(220, 220, 220, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SunsetTheme.uss b/Styles/Themes/SunsetTheme.uss index 67380d2..0b80647 100644 --- a/Styles/Themes/SunsetTheme.uss +++ b/Styles/Themes/SunsetTheme.uss @@ -1,26 +1,26 @@ -.sunset-theme { - /* Backgrounds */ - --terminal-bg: rgba(50, 20, 60, 0.88); - --button-bg: rgba(75, 40, 85, 1); - --input-field-bg: rgba(40, 15, 50, 0.7); - --button-selected-bg: rgba(255, 180, 80, 0.85); - --button-hover-bg: rgba(95, 60, 105, 0.7); - --scroll-bg: rgba(85, 50, 95, 1); - --scroll-inverse-bg: rgba(75, 40, 85, 1); - --scroll-active-bg: rgba(115, 80, 125, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 210, 180, 0.9); - --button-selected-text: rgba(50, 20, 60, 1); - --button-hover-text: rgba(255, 225, 200, 1); - --input-text-color: rgba(255, 210, 180, 1.0); - --text-message: rgba(255, 210, 180, 1); - --text-warning: rgba(255, 230, 100, 1); - --text-input-echo: rgba(255, 150, 150, 1); - --text-shell: rgba(200, 160, 180, 1); - --text-error: rgba(255, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 180, 80, 1); - --caret-color: rgba(255, 210, 180, 1.0); +.sunset-theme { + /* Backgrounds */ + --terminal-bg: rgba(50, 20, 60, 0.88); + --button-bg: rgba(75, 40, 85, 1); + --input-field-bg: rgba(40, 15, 50, 0.7); + --button-selected-bg: rgba(255, 180, 80, 0.85); + --button-hover-bg: rgba(95, 60, 105, 0.7); + --scroll-bg: rgba(85, 50, 95, 1); + --scroll-inverse-bg: rgba(75, 40, 85, 1); + --scroll-active-bg: rgba(115, 80, 125, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 210, 180, 0.9); + --button-selected-text: rgba(50, 20, 60, 1); + --button-hover-text: rgba(255, 225, 200, 1); + --input-text-color: rgba(255, 210, 180, 1.0); + --text-message: rgba(255, 210, 180, 1); + --text-warning: rgba(255, 230, 100, 1); + --text-input-echo: rgba(255, 150, 150, 1); + --text-shell: rgba(200, 160, 180, 1); + --text-error: rgba(255, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 180, 80, 1); + --caret-color: rgba(255, 210, 180, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/SwampTheme.uss b/Styles/Themes/SwampTheme.uss index a55e2d5..d96bd78 100644 --- a/Styles/Themes/SwampTheme.uss +++ b/Styles/Themes/SwampTheme.uss @@ -1,26 +1,26 @@ -.swamp-theme { - /* Backgrounds */ - --terminal-bg: rgba(60, 70, 50, 0.88); - --button-bg: rgba(80, 90, 65, 1); - --input-field-bg: rgba(50, 60, 40, 0.7); - --button-selected-bg: rgba(150, 160, 100, 0.85); - --button-hover-bg: rgba(100, 110, 80, 0.7); - --scroll-bg: rgba(90, 100, 75, 1); - --scroll-inverse-bg: rgba(80, 90, 65, 1); - --scroll-active-bg: rgba(120, 130, 100, 1); - - /* Text & Foreground */ - --button-text: rgba(190, 200, 170, 0.9); - --button-selected-text: rgba(60, 70, 50, 1); - --button-hover-text: rgba(210, 220, 190, 1); - --input-text-color: rgba(190, 200, 170, 1.0); - --text-message: rgba(190, 200, 170, 1); - --text-warning: rgba(180, 170, 90, 1); - --text-input-echo: rgba(100, 110, 80, 1); - --text-shell: rgba(120, 130, 110, 1); - --text-error: rgba(170, 100, 90, 1); - - /* Other UI Elements */ - --scroll-color: rgba(150, 160, 100, 1); - --caret-color: rgba(190, 200, 170, 1.0); +.swamp-theme { + /* Backgrounds */ + --terminal-bg: rgba(60, 70, 50, 0.88); + --button-bg: rgba(80, 90, 65, 1); + --input-field-bg: rgba(50, 60, 40, 0.7); + --button-selected-bg: rgba(150, 160, 100, 0.85); + --button-hover-bg: rgba(100, 110, 80, 0.7); + --scroll-bg: rgba(90, 100, 75, 1); + --scroll-inverse-bg: rgba(80, 90, 65, 1); + --scroll-active-bg: rgba(120, 130, 100, 1); + + /* Text & Foreground */ + --button-text: rgba(190, 200, 170, 0.9); + --button-selected-text: rgba(60, 70, 50, 1); + --button-hover-text: rgba(210, 220, 190, 1); + --input-text-color: rgba(190, 200, 170, 1.0); + --text-message: rgba(190, 200, 170, 1); + --text-warning: rgba(180, 170, 90, 1); + --text-input-echo: rgba(100, 110, 80, 1); + --text-shell: rgba(120, 130, 110, 1); + --text-error: rgba(170, 100, 90, 1); + + /* Other UI Elements */ + --scroll-color: rgba(150, 160, 100, 1); + --caret-color: rgba(190, 200, 170, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/Synthwave84Theme.uss b/Styles/Themes/Synthwave84Theme.uss index 570dbd9..32c44dd 100644 --- a/Styles/Themes/Synthwave84Theme.uss +++ b/Styles/Themes/Synthwave84Theme.uss @@ -1,26 +1,26 @@ -.synthwave84-theme { - /* Backgrounds */ - --terminal-bg: rgba(41, 37, 71, 0.9); - --button-bg: rgba(53, 48, 90, 1); - --input-field-bg: rgba(35, 31, 60, 0.8); - --button-selected-bg: rgba(255, 108, 250, 0.85); - --button-hover-bg: rgba(65, 59, 110, 1); - --scroll-bg: rgba(53, 48, 90, 1); - --scroll-inverse-bg: rgba(80, 73, 130, 1); - --scroll-active-bg: rgba(95, 87, 150, 1); - - /* Text & Foreground */ - --button-text: rgba(218, 218, 218, 0.9); - --button-selected-text: rgba(41, 37, 71, 1); - --button-hover-text: rgba(255, 255, 255, 1); - --input-text-color: rgba(218, 218, 218, 1.0); - --text-message: rgba(218, 218, 218, 1); - --text-warning: rgba(255, 218, 111, 1); - --text-input-echo: rgba(113, 224, 255, 1); - --text-shell: rgba(144, 134, 199, 1); - --text-error: rgba(255, 95, 162, 1); - - /* Other UI Elements */ - --scroll-color: rgba(218, 218, 218, 1); - --caret-color: rgba(255, 108, 250, 1.0); +.synthwave84-theme { + /* Backgrounds */ + --terminal-bg: rgba(41, 37, 71, 0.9); + --button-bg: rgba(53, 48, 90, 1); + --input-field-bg: rgba(35, 31, 60, 0.8); + --button-selected-bg: rgba(255, 108, 250, 0.85); + --button-hover-bg: rgba(65, 59, 110, 1); + --scroll-bg: rgba(53, 48, 90, 1); + --scroll-inverse-bg: rgba(80, 73, 130, 1); + --scroll-active-bg: rgba(95, 87, 150, 1); + + /* Text & Foreground */ + --button-text: rgba(218, 218, 218, 0.9); + --button-selected-text: rgba(41, 37, 71, 1); + --button-hover-text: rgba(255, 255, 255, 1); + --input-text-color: rgba(218, 218, 218, 1.0); + --text-message: rgba(218, 218, 218, 1); + --text-warning: rgba(255, 218, 111, 1); + --text-input-echo: rgba(113, 224, 255, 1); + --text-shell: rgba(144, 134, 199, 1); + --text-error: rgba(255, 95, 162, 1); + + /* Other UI Elements */ + --scroll-color: rgba(218, 218, 218, 1); + --caret-color: rgba(255, 108, 250, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/TokyoNightTheme.uss b/Styles/Themes/TokyoNightTheme.uss index d9738c3..49ecc0a 100644 --- a/Styles/Themes/TokyoNightTheme.uss +++ b/Styles/Themes/TokyoNightTheme.uss @@ -1,26 +1,26 @@ -.tokyo-night-theme { - /* Backgrounds */ - --terminal-bg: rgba(26, 27, 38, 0.9); - --button-bg: rgba(31, 32, 46, 1); - --input-field-bg: rgba(20, 21, 30, 0.8); - --button-selected-bg: rgba(122, 162, 247, 0.85); - --button-hover-bg: rgba(44, 48, 64, 1); - --scroll-bg: rgba(31, 32, 46, 1); - --scroll-inverse-bg: rgba(68, 73, 94, 1); - --scroll-active-bg: rgba(92, 99, 122, 1); - - /* Text & Foreground */ - --button-text: rgba(169, 177, 208, 0.9); - --button-selected-text: rgba(26, 27, 38, 1); - --button-hover-text: rgba(192, 197, 216, 1); - --input-text-color: rgba(169, 177, 208, 1.0); - --text-message: rgba(169, 177, 208, 1); - --text-warning: rgba(224, 160, 108, 1); - --text-input-echo: rgba(158, 206, 246, 1); - --text-shell: rgba(92, 99, 122, 1); - --text-error: rgba(247, 118, 142, 1); - - /* Other UI Elements */ - --scroll-color: rgba(169, 177, 208, 1); - --caret-color: rgba(169, 177, 208, 1.0); +.tokyo-night-theme { + /* Backgrounds */ + --terminal-bg: rgba(26, 27, 38, 0.9); + --button-bg: rgba(31, 32, 46, 1); + --input-field-bg: rgba(20, 21, 30, 0.8); + --button-selected-bg: rgba(122, 162, 247, 0.85); + --button-hover-bg: rgba(44, 48, 64, 1); + --scroll-bg: rgba(31, 32, 46, 1); + --scroll-inverse-bg: rgba(68, 73, 94, 1); + --scroll-active-bg: rgba(92, 99, 122, 1); + + /* Text & Foreground */ + --button-text: rgba(169, 177, 208, 0.9); + --button-selected-text: rgba(26, 27, 38, 1); + --button-hover-text: rgba(192, 197, 216, 1); + --input-text-color: rgba(169, 177, 208, 1.0); + --text-message: rgba(169, 177, 208, 1); + --text-warning: rgba(224, 160, 108, 1); + --text-input-echo: rgba(158, 206, 246, 1); + --text-shell: rgba(92, 99, 122, 1); + --text-error: rgba(247, 118, 142, 1); + + /* Other UI Elements */ + --scroll-color: rgba(169, 177, 208, 1); + --caret-color: rgba(169, 177, 208, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/TomorrowLightTheme.uss b/Styles/Themes/TomorrowLightTheme.uss index 5aac141..bc516ee 100644 --- a/Styles/Themes/TomorrowLightTheme.uss +++ b/Styles/Themes/TomorrowLightTheme.uss @@ -1,26 +1,26 @@ -.tomorrow-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 255, 255, 0.9); - --button-bg: rgba(230, 230, 230, 1); - --input-field-bg: rgba(242, 242, 242, 0.8); - --button-selected-bg: rgba(62, 100, 155, 0.85); - --button-hover-bg: rgba(215, 215, 215, 1); - --scroll-bg: rgba(230, 230, 230, 1); - --scroll-inverse-bg: rgba(245, 245, 245, 1); - --scroll-active-bg: rgba(142, 144, 140, 1); - - /* Text & Foreground */ - --button-text: rgba(77, 77, 77, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(55, 55, 55, 1); - --input-text-color: rgba(77, 77, 77, 1); - --text-message: rgba(77, 77, 77, 1); - --text-warning: rgba(232, 165, 34, 1); - --text-input-echo: rgba(118, 148, 104, 1); - --text-shell: rgba(142, 144, 140, 1); - --text-error: rgba(195, 79, 77, 1); - - /* Other UI Elements */ - --scroll-color: rgba(77, 77, 77, 1); - --caret-color: rgba(77, 77, 77, 1); +.tomorrow-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 255, 255, 0.9); + --button-bg: rgba(230, 230, 230, 1); + --input-field-bg: rgba(242, 242, 242, 0.8); + --button-selected-bg: rgba(62, 100, 155, 0.85); + --button-hover-bg: rgba(215, 215, 215, 1); + --scroll-bg: rgba(230, 230, 230, 1); + --scroll-inverse-bg: rgba(245, 245, 245, 1); + --scroll-active-bg: rgba(142, 144, 140, 1); + + /* Text & Foreground */ + --button-text: rgba(77, 77, 77, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(55, 55, 55, 1); + --input-text-color: rgba(77, 77, 77, 1); + --text-message: rgba(77, 77, 77, 1); + --text-warning: rgba(232, 165, 34, 1); + --text-input-echo: rgba(118, 148, 104, 1); + --text-shell: rgba(142, 144, 140, 1); + --text-error: rgba(195, 79, 77, 1); + + /* Other UI Elements */ + --scroll-color: rgba(77, 77, 77, 1); + --caret-color: rgba(77, 77, 77, 1); } \ No newline at end of file diff --git a/Styles/Themes/TomorrowNightTheme.uss b/Styles/Themes/TomorrowNightTheme.uss index b141453..77fe6ce 100644 --- a/Styles/Themes/TomorrowNightTheme.uss +++ b/Styles/Themes/TomorrowNightTheme.uss @@ -1,26 +1,26 @@ -.tomorrow-night-theme { - /* Backgrounds */ - --terminal-bg: rgba(29, 31, 33, 0.9); - --button-bg: rgba(44, 47, 49, 1); - --input-field-bg: rgba(36, 39, 41, 0.8); - --button-selected-bg: rgba(138, 154, 197, 0.85); - --button-hover-bg: rgba(59, 62, 64, 1); - --scroll-bg: rgba(44, 47, 49, 1); - --scroll-inverse-bg: rgba(20, 22, 24, 1); - --scroll-active-bg: rgba(150, 152, 150, 1); - - /* Text & Foreground */ - --button-text: rgba(197, 199, 197, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(220, 222, 220, 1); - --input-text-color: rgba(197, 199, 197, 1); - --text-message: rgba(197, 199, 197, 1); - --text-warning: rgba(240, 198, 116, 1); - --text-input-echo: rgba(138, 190, 183, 1); - --text-shell: rgba(150, 152, 150, 1); - --text-error: rgba(204, 102, 102, 1); - - /* Other UI Elements */ - --scroll-color: rgba(197, 199, 197, 1); - --caret-color: rgba(197, 199, 197, 1); +.tomorrow-night-theme { + /* Backgrounds */ + --terminal-bg: rgba(29, 31, 33, 0.9); + --button-bg: rgba(44, 47, 49, 1); + --input-field-bg: rgba(36, 39, 41, 0.8); + --button-selected-bg: rgba(138, 154, 197, 0.85); + --button-hover-bg: rgba(59, 62, 64, 1); + --scroll-bg: rgba(44, 47, 49, 1); + --scroll-inverse-bg: rgba(20, 22, 24, 1); + --scroll-active-bg: rgba(150, 152, 150, 1); + + /* Text & Foreground */ + --button-text: rgba(197, 199, 197, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(220, 222, 220, 1); + --input-text-color: rgba(197, 199, 197, 1); + --text-message: rgba(197, 199, 197, 1); + --text-warning: rgba(240, 198, 116, 1); + --text-input-echo: rgba(138, 190, 183, 1); + --text-shell: rgba(150, 152, 150, 1); + --text-error: rgba(204, 102, 102, 1); + + /* Other UI Elements */ + --scroll-color: rgba(197, 199, 197, 1); + --caret-color: rgba(197, 199, 197, 1); } \ No newline at end of file diff --git a/Styles/Themes/UbuntuTheme.uss b/Styles/Themes/UbuntuTheme.uss index 072287e..810c56f 100644 --- a/Styles/Themes/UbuntuTheme.uss +++ b/Styles/Themes/UbuntuTheme.uss @@ -1,26 +1,26 @@ -.ubuntu-theme { - /* Backgrounds */ - --terminal-bg: rgba(48, 10, 36, 0.9); - --button-bg: rgba(70, 25, 55, 1); - --input-field-bg: rgba(60, 18, 45, 0.8); - --button-selected-bg: rgba(233, 84, 32, 0.85); - --button-hover-bg: rgba(90, 40, 75, 1); - --scroll-bg: rgba(70, 25, 55, 1); - --scroll-inverse-bg: rgba(30, 0, 20, 1); - --scroll-active-bg: rgba(150, 140, 145, 1); - - /* Text & Foreground */ - --button-text: rgba(222, 221, 218, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(240, 240, 238, 1); - --input-text-color: rgba(222, 221, 218, 1); - --text-message: rgba(222, 221, 218, 1); - --text-warning: rgba(252, 175, 62, 1); - --text-input-echo: rgba(170, 200, 255, 1); - --text-shell: rgba(150, 140, 145, 1); - --text-error: rgba(239, 41, 41, 1); - - /* Other UI Elements */ - --scroll-color: rgba(222, 221, 218, 1); - --caret-color: rgba(222, 221, 218, 1); +.ubuntu-theme { + /* Backgrounds */ + --terminal-bg: rgba(48, 10, 36, 0.9); + --button-bg: rgba(70, 25, 55, 1); + --input-field-bg: rgba(60, 18, 45, 0.8); + --button-selected-bg: rgba(233, 84, 32, 0.85); + --button-hover-bg: rgba(90, 40, 75, 1); + --scroll-bg: rgba(70, 25, 55, 1); + --scroll-inverse-bg: rgba(30, 0, 20, 1); + --scroll-active-bg: rgba(150, 140, 145, 1); + + /* Text & Foreground */ + --button-text: rgba(222, 221, 218, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(240, 240, 238, 1); + --input-text-color: rgba(222, 221, 218, 1); + --text-message: rgba(222, 221, 218, 1); + --text-warning: rgba(252, 175, 62, 1); + --text-input-echo: rgba(170, 200, 255, 1); + --text-shell: rgba(150, 140, 145, 1); + --text-error: rgba(239, 41, 41, 1); + + /* Other UI Elements */ + --scroll-color: rgba(222, 221, 218, 1); + --caret-color: rgba(222, 221, 218, 1); } \ No newline at end of file diff --git a/Styles/Themes/UnderwaterTheme.uss b/Styles/Themes/UnderwaterTheme.uss index 7876d40..fc40d01 100644 --- a/Styles/Themes/UnderwaterTheme.uss +++ b/Styles/Themes/UnderwaterTheme.uss @@ -1,26 +1,26 @@ -.underwater-theme { - /* Backgrounds */ - --terminal-bg: rgba(10, 40, 55, 0.88); - --button-bg: rgba(20, 60, 80, 1); - --input-field-bg: rgba(5, 30, 45, 0.7); - --button-selected-bg: rgba(100, 220, 220, 0.85); - --button-hover-bg: rgba(30, 80, 100, 0.7); - --scroll-bg: rgba(40, 80, 100, 1); - --scroll-inverse-bg: rgba(30, 70, 90, 1); - --scroll-active-bg: rgba(70, 110, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(180, 240, 255, 0.9); - --button-selected-text: rgba(10, 40, 55, 1); - --button-hover-text: rgba(210, 250, 255, 1); - --input-text-color: rgba(200, 245, 255, 1.0); - --text-message: rgba(200, 245, 255, 1); - --text-warning: rgba(255, 230, 130, 1); - --text-input-echo: rgba(80, 190, 230, 1); - --text-shell: rgba(130, 200, 210, 1); - --text-error: rgba(255, 100, 140, 1); - - /* Other UI Elements */ - --scroll-color: rgba(100, 220, 220, 1); - --caret-color: rgba(200, 245, 255, 1.0); +.underwater-theme { + /* Backgrounds */ + --terminal-bg: rgba(10, 40, 55, 0.88); + --button-bg: rgba(20, 60, 80, 1); + --input-field-bg: rgba(5, 30, 45, 0.7); + --button-selected-bg: rgba(100, 220, 220, 0.85); + --button-hover-bg: rgba(30, 80, 100, 0.7); + --scroll-bg: rgba(40, 80, 100, 1); + --scroll-inverse-bg: rgba(30, 70, 90, 1); + --scroll-active-bg: rgba(70, 110, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(180, 240, 255, 0.9); + --button-selected-text: rgba(10, 40, 55, 1); + --button-hover-text: rgba(210, 250, 255, 1); + --input-text-color: rgba(200, 245, 255, 1.0); + --text-message: rgba(200, 245, 255, 1); + --text-warning: rgba(255, 230, 130, 1); + --text-input-echo: rgba(80, 190, 230, 1); + --text-shell: rgba(130, 200, 210, 1); + --text-error: rgba(255, 100, 140, 1); + + /* Other UI Elements */ + --scroll-color: rgba(100, 220, 220, 1); + --caret-color: rgba(200, 245, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/VS2019DarkTheme.uss b/Styles/Themes/VS2019DarkTheme.uss index 1bc24d0..58049f0 100644 --- a/Styles/Themes/VS2019DarkTheme.uss +++ b/Styles/Themes/VS2019DarkTheme.uss @@ -1,26 +1,26 @@ -.vs2019-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(30, 30, 30, 0.9); - --button-bg: rgba(51, 51, 51, 1); - --input-field-bg: rgba(37, 37, 38, 0.8); - --button-selected-bg: rgba(0, 122, 204, 0.85); - --button-hover-bg: rgba(62, 62, 62, 1); - --scroll-bg: rgba(64, 64, 64, 1); - --scroll-inverse-bg: rgba(104, 104, 104, 1); - --scroll-active-bg: rgba(158, 158, 158, 1); - - /* Text & Foreground */ - --button-text: rgba(241, 241, 241, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(241, 241, 241, 1); - --input-text-color: rgba(212, 212, 212, 1.0); - --text-message: rgba(212, 212, 212, 1); - --text-warning: rgba(255, 204, 0, 1); - --text-input-echo: rgba(78, 201, 176, 1); - --text-shell: rgba(86, 156, 214, 1); - --text-error: rgba(244, 70, 70, 1); - - /* Other UI Elements */ - --scroll-color: rgba(212, 212, 212, 1); - --caret-color: rgba(212, 212, 212, 1.0); +.vs2019-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(30, 30, 30, 0.9); + --button-bg: rgba(51, 51, 51, 1); + --input-field-bg: rgba(37, 37, 38, 0.8); + --button-selected-bg: rgba(0, 122, 204, 0.85); + --button-hover-bg: rgba(62, 62, 62, 1); + --scroll-bg: rgba(64, 64, 64, 1); + --scroll-inverse-bg: rgba(104, 104, 104, 1); + --scroll-active-bg: rgba(158, 158, 158, 1); + + /* Text & Foreground */ + --button-text: rgba(241, 241, 241, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(241, 241, 241, 1); + --input-text-color: rgba(212, 212, 212, 1.0); + --text-message: rgba(212, 212, 212, 1); + --text-warning: rgba(255, 204, 0, 1); + --text-input-echo: rgba(78, 201, 176, 1); + --text-shell: rgba(86, 156, 214, 1); + --text-error: rgba(244, 70, 70, 1); + + /* Other UI Elements */ + --scroll-color: rgba(212, 212, 212, 1); + --caret-color: rgba(212, 212, 212, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/VaporwaveTheme.uss b/Styles/Themes/VaporwaveTheme.uss index ed9ffa8..8800dbe 100644 --- a/Styles/Themes/VaporwaveTheme.uss +++ b/Styles/Themes/VaporwaveTheme.uss @@ -1,26 +1,26 @@ -.vaporwave-theme { - /* Backgrounds */ - --terminal-bg: rgba(40, 15, 50, 0.85); - --button-bg: rgba(60, 30, 70, 1); - --input-field-bg: rgba(30, 10, 40, 0.7); - --button-selected-bg: rgba(0, 255, 255, 0.85); - --button-hover-bg: rgba(255, 105, 180, 0.5); - --scroll-bg: rgba(70, 40, 80, 1); - --scroll-inverse-bg: rgba(60, 30, 70, 1); - --scroll-active-bg: rgba(100, 60, 110, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 105, 180, 0.9); - --button-selected-text: rgba(40, 15, 50, 1); - --button-hover-text: rgba(0, 255, 255, 1); - --input-text-color: rgba(0, 255, 255, 1.0); - --text-message: rgba(0, 255, 255, 1); - --text-warning: rgba(255, 255, 100, 1); - --text-input-echo: rgba(255, 150, 200, 1); - --text-shell: rgba(180, 100, 200, 1); - --text-error: rgba(255, 80, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 105, 180, 1); - --caret-color: rgba(0, 255, 255, 1.0); +.vaporwave-theme { + /* Backgrounds */ + --terminal-bg: rgba(40, 15, 50, 0.85); + --button-bg: rgba(60, 30, 70, 1); + --input-field-bg: rgba(30, 10, 40, 0.7); + --button-selected-bg: rgba(0, 255, 255, 0.85); + --button-hover-bg: rgba(255, 105, 180, 0.5); + --scroll-bg: rgba(70, 40, 80, 1); + --scroll-inverse-bg: rgba(60, 30, 70, 1); + --scroll-active-bg: rgba(100, 60, 110, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 105, 180, 0.9); + --button-selected-text: rgba(40, 15, 50, 1); + --button-hover-text: rgba(0, 255, 255, 1); + --input-text-color: rgba(0, 255, 255, 1.0); + --text-message: rgba(0, 255, 255, 1); + --text-warning: rgba(255, 255, 100, 1); + --text-input-echo: rgba(255, 150, 200, 1); + --text-shell: rgba(180, 100, 200, 1); + --text-error: rgba(255, 80, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 105, 180, 1); + --caret-color: rgba(0, 255, 255, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/VisualStudioDarkTheme.uss b/Styles/Themes/VisualStudioDarkTheme.uss index c089119..1007acc 100644 --- a/Styles/Themes/VisualStudioDarkTheme.uss +++ b/Styles/Themes/VisualStudioDarkTheme.uss @@ -1,26 +1,26 @@ -.vs-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(30, 30, 30, 0.9); - --button-bg: rgba(51, 51, 51, 1); - --input-field-bg: rgba(62, 62, 62, 0.8); - --button-selected-bg: rgba(14, 99, 156, 0.85); - --button-hover-bg: rgba(60, 60, 60, 1); - --scroll-bg: rgba(51, 51, 51, 1); - --scroll-inverse-bg: rgba(30, 30, 30, 1); - --scroll-active-bg: rgba(104, 104, 104, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 220, 220, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(240, 240, 240, 1); - --input-text-color: rgba(220, 220, 220, 1); - --text-message: rgba(212, 212, 212, 1); - --text-warning: rgba(253, 249, 120, 1); - --text-input-echo: rgba(86, 156, 214, 1); - --text-shell: rgba(156, 220, 254, 1); - --text-error: rgba(244, 70, 70, 1); - - /* Other UI Elements */ - --scroll-color: rgba(197, 197, 197, 1); - --caret-color: rgba(174, 174, 174, 1); +.vs-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(30, 30, 30, 0.9); + --button-bg: rgba(51, 51, 51, 1); + --input-field-bg: rgba(62, 62, 62, 0.8); + --button-selected-bg: rgba(14, 99, 156, 0.85); + --button-hover-bg: rgba(60, 60, 60, 1); + --scroll-bg: rgba(51, 51, 51, 1); + --scroll-inverse-bg: rgba(30, 30, 30, 1); + --scroll-active-bg: rgba(104, 104, 104, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 220, 220, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(240, 240, 240, 1); + --input-text-color: rgba(220, 220, 220, 1); + --text-message: rgba(212, 212, 212, 1); + --text-warning: rgba(253, 249, 120, 1); + --text-input-echo: rgba(86, 156, 214, 1); + --text-shell: rgba(156, 220, 254, 1); + --text-error: rgba(244, 70, 70, 1); + + /* Other UI Elements */ + --scroll-color: rgba(197, 197, 197, 1); + --caret-color: rgba(174, 174, 174, 1); } \ No newline at end of file diff --git a/Styles/Themes/VisualStudioLightTheme.uss b/Styles/Themes/VisualStudioLightTheme.uss index 3b2cca2..d9c4570 100644 --- a/Styles/Themes/VisualStudioLightTheme.uss +++ b/Styles/Themes/VisualStudioLightTheme.uss @@ -1,26 +1,26 @@ -.vs-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 255, 255, 0.9); - --button-bg: rgba(239, 239, 242, 1); - --input-field-bg: rgba(246, 246, 246, 0.8); - --button-selected-bg: rgba(0, 122, 204, 0.85); - --button-hover-bg: rgba(220, 220, 225, 1); - --scroll-bg: rgba(239, 239, 242, 1); - --scroll-inverse-bg: rgba(255, 255, 255, 1); - --scroll-active-bg: rgba(180, 180, 180, 1); - - /* Text & Foreground */ - --button-text: rgba(30, 30, 30, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(10, 10, 10, 1); - --input-text-color: rgba(30, 30, 30, 1); - --text-message: rgba(0, 0, 0, 1); - --text-warning: rgba(190, 140, 0, 1); - --text-input-echo: rgba(0, 0, 255, 1); - --text-shell: rgba(43, 145, 175, 1); - --text-error: rgba(255, 0, 0, 1); - - /* Other UI Elements */ - --scroll-color: rgba(104, 104, 104, 1); - --caret-color: rgba(0, 0, 0, 1); +.vs-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 255, 255, 0.9); + --button-bg: rgba(239, 239, 242, 1); + --input-field-bg: rgba(246, 246, 246, 0.8); + --button-selected-bg: rgba(0, 122, 204, 0.85); + --button-hover-bg: rgba(220, 220, 225, 1); + --scroll-bg: rgba(239, 239, 242, 1); + --scroll-inverse-bg: rgba(255, 255, 255, 1); + --scroll-active-bg: rgba(180, 180, 180, 1); + + /* Text & Foreground */ + --button-text: rgba(30, 30, 30, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(10, 10, 10, 1); + --input-text-color: rgba(30, 30, 30, 1); + --text-message: rgba(0, 0, 0, 1); + --text-warning: rgba(190, 140, 0, 1); + --text-input-echo: rgba(0, 0, 255, 1); + --text-shell: rgba(43, 145, 175, 1); + --text-error: rgba(255, 0, 0, 1); + + /* Other UI Elements */ + --scroll-color: rgba(104, 104, 104, 1); + --caret-color: rgba(0, 0, 0, 1); } \ No newline at end of file diff --git a/Styles/Themes/VolcanoTheme.uss b/Styles/Themes/VolcanoTheme.uss index 064c9de..2b98434 100644 --- a/Styles/Themes/VolcanoTheme.uss +++ b/Styles/Themes/VolcanoTheme.uss @@ -1,26 +1,26 @@ -.volcano-theme { - /* Backgrounds */ - --terminal-bg: rgba(35, 20, 15, 0.9); - --button-bg: rgba(60, 35, 25, 1); - --input-field-bg: rgba(25, 15, 10, 0.7); - --button-selected-bg: rgba(255, 100, 0, 0.85); - --button-hover-bg: rgba(80, 50, 40, 0.7); - --scroll-bg: rgba(70, 45, 35, 1); - --scroll-inverse-bg: rgba(60, 35, 25, 1); - --scroll-active-bg: rgba(100, 70, 60, 1); - - /* Text & Foreground */ - --button-text: rgba(255, 180, 130, 0.9); - --button-selected-text: rgba(35, 20, 15, 1); - --button-hover-text: rgba(255, 200, 160, 1); - --input-text-color: rgba(255, 180, 130, 1.0); - --text-message: rgba(255, 180, 130, 1); - --text-warning: rgba(255, 220, 80, 1); - --text-input-echo: rgba(230, 120, 50, 1); - --text-shell: rgba(180, 100, 80, 1); - --text-error: rgba(220, 40, 40, 1); - - /* Other UI Elements */ - --scroll-color: rgba(255, 100, 0, 1); - --caret-color: rgba(255, 180, 130, 1.0); +.volcano-theme { + /* Backgrounds */ + --terminal-bg: rgba(35, 20, 15, 0.9); + --button-bg: rgba(60, 35, 25, 1); + --input-field-bg: rgba(25, 15, 10, 0.7); + --button-selected-bg: rgba(255, 100, 0, 0.85); + --button-hover-bg: rgba(80, 50, 40, 0.7); + --scroll-bg: rgba(70, 45, 35, 1); + --scroll-inverse-bg: rgba(60, 35, 25, 1); + --scroll-active-bg: rgba(100, 70, 60, 1); + + /* Text & Foreground */ + --button-text: rgba(255, 180, 130, 0.9); + --button-selected-text: rgba(35, 20, 15, 1); + --button-hover-text: rgba(255, 200, 160, 1); + --input-text-color: rgba(255, 180, 130, 1.0); + --text-message: rgba(255, 180, 130, 1); + --text-warning: rgba(255, 220, 80, 1); + --text-input-echo: rgba(230, 120, 50, 1); + --text-shell: rgba(180, 100, 80, 1); + --text-error: rgba(220, 40, 40, 1); + + /* Other UI Elements */ + --scroll-color: rgba(255, 100, 0, 1); + --caret-color: rgba(255, 180, 130, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/WinterDarkTheme.uss b/Styles/Themes/WinterDarkTheme.uss index 3d3facb..55a766f 100644 --- a/Styles/Themes/WinterDarkTheme.uss +++ b/Styles/Themes/WinterDarkTheme.uss @@ -1,26 +1,26 @@ -.winter-dark-theme { - /* Backgrounds */ - --terminal-bg: rgba(1, 19, 28, 0.9); - --button-bg: rgba(32, 60, 76, 1); - --input-field-bg: rgba(10, 36, 51, 0.8); - --button-selected-bg: rgba(126, 198, 230, 0.85); - --button-hover-bg: rgba(40, 78, 99, 1); - --scroll-bg: rgba(32, 60, 76, 1); - --scroll-inverse-bg: rgba(50, 90, 110, 1); - --scroll-active-bg: rgba(70, 110, 130, 1); - - /* Text & Foreground */ - --button-text: rgba(214, 222, 235, 0.9); - --button-selected-text: rgba(1, 19, 28, 1); - --button-hover-text: rgba(214, 222, 235, 1); - --input-text-color: rgba(214, 222, 235, 1.0); - --text-message: rgba(214, 222, 235, 1); - --text-warning: rgba(255, 215, 109, 1); - --text-input-echo: rgba(126, 198, 230, 1); - --text-shell: rgba(113, 140, 152, 1); - --text-error: rgba(255, 130, 130, 1); - - /* Other UI Elements */ - --scroll-color: rgba(214, 222, 235, 1); - --caret-color: rgba(126, 198, 230, 1.0); +.winter-dark-theme { + /* Backgrounds */ + --terminal-bg: rgba(1, 19, 28, 0.9); + --button-bg: rgba(32, 60, 76, 1); + --input-field-bg: rgba(10, 36, 51, 0.8); + --button-selected-bg: rgba(126, 198, 230, 0.85); + --button-hover-bg: rgba(40, 78, 99, 1); + --scroll-bg: rgba(32, 60, 76, 1); + --scroll-inverse-bg: rgba(50, 90, 110, 1); + --scroll-active-bg: rgba(70, 110, 130, 1); + + /* Text & Foreground */ + --button-text: rgba(214, 222, 235, 0.9); + --button-selected-text: rgba(1, 19, 28, 1); + --button-hover-text: rgba(214, 222, 235, 1); + --input-text-color: rgba(214, 222, 235, 1.0); + --text-message: rgba(214, 222, 235, 1); + --text-warning: rgba(255, 215, 109, 1); + --text-input-echo: rgba(126, 198, 230, 1); + --text-shell: rgba(113, 140, 152, 1); + --text-error: rgba(255, 130, 130, 1); + + /* Other UI Elements */ + --scroll-color: rgba(214, 222, 235, 1); + --caret-color: rgba(126, 198, 230, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/WinterLightTheme.uss b/Styles/Themes/WinterLightTheme.uss index cf4f7c8..9801c4c 100644 --- a/Styles/Themes/WinterLightTheme.uss +++ b/Styles/Themes/WinterLightTheme.uss @@ -1,26 +1,26 @@ -.winter-light-theme { - /* Backgrounds */ - --terminal-bg: rgba(255, 250, 240, 0.9); - --button-bg: rgba(235, 235, 235, 1); - --input-field-bg: rgba(245, 245, 245, 0.8); - --button-selected-bg: rgba(0, 116, 184, 0.85); - --button-hover-bg: rgba(220, 220, 220, 1); - --scroll-bg: rgba(235, 235, 235, 1); - --scroll-inverse-bg: rgba(200, 200, 200, 1); - --scroll-active-bg: rgba(180, 180, 180, 1); - - /* Text & Foreground */ - --button-text: rgba(68, 68, 68, 0.9); - --button-selected-text: rgba(255, 255, 255, 1); - --button-hover-text: rgba(68, 68, 68, 1); - --input-text-color: rgba(68, 68, 68, 1.0); - --text-message: rgba(68, 68, 68, 1); - --text-warning: rgba(199, 146, 0, 1); - --text-input-echo: rgba(0, 116, 184, 1); - --text-shell: rgba(128, 128, 128, 1); - --text-error: rgba(211, 53, 53, 1); - - /* Other UI Elements */ - --scroll-color: rgba(68, 68, 68, 1); - --caret-color: rgba(0, 116, 184, 1.0); +.winter-light-theme { + /* Backgrounds */ + --terminal-bg: rgba(255, 250, 240, 0.9); + --button-bg: rgba(235, 235, 235, 1); + --input-field-bg: rgba(245, 245, 245, 0.8); + --button-selected-bg: rgba(0, 116, 184, 0.85); + --button-hover-bg: rgba(220, 220, 220, 1); + --scroll-bg: rgba(235, 235, 235, 1); + --scroll-inverse-bg: rgba(200, 200, 200, 1); + --scroll-active-bg: rgba(180, 180, 180, 1); + + /* Text & Foreground */ + --button-text: rgba(68, 68, 68, 0.9); + --button-selected-text: rgba(255, 255, 255, 1); + --button-hover-text: rgba(68, 68, 68, 1); + --input-text-color: rgba(68, 68, 68, 1.0); + --text-message: rgba(68, 68, 68, 1); + --text-warning: rgba(199, 146, 0, 1); + --text-input-echo: rgba(0, 116, 184, 1); + --text-shell: rgba(128, 128, 128, 1); + --text-error: rgba(211, 53, 53, 1); + + /* Other UI Elements */ + --scroll-color: rgba(68, 68, 68, 1); + --caret-color: rgba(0, 116, 184, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/WinterSkyTheme.uss b/Styles/Themes/WinterSkyTheme.uss index 7b87e9a..09b7942 100644 --- a/Styles/Themes/WinterSkyTheme.uss +++ b/Styles/Themes/WinterSkyTheme.uss @@ -1,26 +1,26 @@ -.winter-sky-theme { - /* Backgrounds */ - --terminal-bg: rgba(210, 225, 235, 0.75); - --button-bg: rgba(190, 205, 215, 1); - --input-field-bg: rgba(225, 235, 245, 0.7); - --button-selected-bg: rgba(70, 90, 110, 0.85); - --button-hover-bg: rgba(170, 185, 195, 0.7); - --scroll-bg: rgba(180, 195, 205, 1); - --scroll-inverse-bg: rgba(190, 205, 215, 1); - --scroll-active-bg: rgba(120, 135, 145, 1); - - /* Text & Foreground */ - --button-text: rgba(50, 70, 90, 0.9); - --button-selected-text: rgba(210, 225, 235, 1); - --button-hover-text: rgba(30, 50, 70, 1); - --input-text-color: rgba(50, 70, 90, 1.0); - --text-message: rgba(50, 70, 90, 1); - --text-warning: rgba(180, 120, 50, 1); - --text-input-echo: rgba(100, 140, 180, 1); - --text-shell: rgba(110, 130, 150, 1); - --text-error: rgba(170, 60, 80, 1); - - /* Other UI Elements */ - --scroll-color: rgba(70, 90, 110, 1); - --caret-color: rgba(50, 70, 90, 1.0); +.winter-sky-theme { + /* Backgrounds */ + --terminal-bg: rgba(210, 225, 235, 0.75); + --button-bg: rgba(190, 205, 215, 1); + --input-field-bg: rgba(225, 235, 245, 0.7); + --button-selected-bg: rgba(70, 90, 110, 0.85); + --button-hover-bg: rgba(170, 185, 195, 0.7); + --scroll-bg: rgba(180, 195, 205, 1); + --scroll-inverse-bg: rgba(190, 205, 215, 1); + --scroll-active-bg: rgba(120, 135, 145, 1); + + /* Text & Foreground */ + --button-text: rgba(50, 70, 90, 0.9); + --button-selected-text: rgba(210, 225, 235, 1); + --button-hover-text: rgba(30, 50, 70, 1); + --input-text-color: rgba(50, 70, 90, 1.0); + --text-message: rgba(50, 70, 90, 1); + --text-warning: rgba(180, 120, 50, 1); + --text-input-echo: rgba(100, 140, 180, 1); + --text-shell: rgba(110, 130, 150, 1); + --text-error: rgba(170, 60, 80, 1); + + /* Other UI Elements */ + --scroll-color: rgba(70, 90, 110, 1); + --caret-color: rgba(50, 70, 90, 1.0); } \ No newline at end of file diff --git a/Styles/Themes/ZenBurnTheme.uss b/Styles/Themes/ZenBurnTheme.uss index 7fa228b..747e1b3 100644 --- a/Styles/Themes/ZenBurnTheme.uss +++ b/Styles/Themes/ZenBurnTheme.uss @@ -1,26 +1,26 @@ -.zenburn-theme { - /* Backgrounds */ - --terminal-bg: rgba(63, 63, 63, 0.9); - --button-bg: rgba(79, 79, 79, 1); - --input-field-bg: rgba(71, 71, 71, 0.8); - --button-selected-bg: rgba(128, 159, 118, 0.85); - --button-hover-bg: rgba(95, 95, 95, 1); - --scroll-bg: rgba(79, 79, 79, 1); - --scroll-inverse-bg: rgba(47, 47, 47, 1); - --scroll-active-bg: rgba(127, 127, 127, 1); - - /* Text & Foreground */ - --button-text: rgba(220, 220, 190, 0.9); - --button-selected-text: rgba(63, 63, 63, 1); - --button-hover-text: rgba(240, 240, 210, 1); - --input-text-color: rgba(220, 220, 190, 1); - --text-message: rgba(220, 220, 190, 1); - --text-warning: rgba(223, 175, 143, 1); - --text-input-echo: rgba(143, 191, 171, 1); - --text-shell: rgba(127, 127, 127, 1); - --text-error: rgba(204, 136, 136, 1); - - /* Other UI Elements */ - --scroll-color: rgba(220, 220, 190, 1); - --caret-color: rgba(220, 220, 190, 1); +.zenburn-theme { + /* Backgrounds */ + --terminal-bg: rgba(63, 63, 63, 0.9); + --button-bg: rgba(79, 79, 79, 1); + --input-field-bg: rgba(71, 71, 71, 0.8); + --button-selected-bg: rgba(128, 159, 118, 0.85); + --button-hover-bg: rgba(95, 95, 95, 1); + --scroll-bg: rgba(79, 79, 79, 1); + --scroll-inverse-bg: rgba(47, 47, 47, 1); + --scroll-active-bg: rgba(127, 127, 127, 1); + + /* Text & Foreground */ + --button-text: rgba(220, 220, 190, 0.9); + --button-selected-text: rgba(63, 63, 63, 1); + --button-hover-text: rgba(240, 240, 210, 1); + --input-text-color: rgba(220, 220, 190, 1); + --text-message: rgba(220, 220, 190, 1); + --text-warning: rgba(223, 175, 143, 1); + --text-input-echo: rgba(143, 191, 171, 1); + --text-shell: rgba(127, 127, 127, 1); + --text-error: rgba(204, 136, 136, 1); + + /* Other UI Elements */ + --scroll-color: rgba(220, 220, 190, 1); + --caret-color: rgba(220, 220, 190, 1); } \ No newline at end of file diff --git a/Tests/Runtime/CommandArgTests.cs b/Tests/Runtime/CommandArgTests.cs index dd873d8..a57b0c4 100644 --- a/Tests/Runtime/CommandArgTests.cs +++ b/Tests/Runtime/CommandArgTests.cs @@ -1,2718 +1,2718 @@ -namespace WallstopStudios.DxCommandTerminal.Tests.Runtime -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using Backend; - using JetBrains.Annotations; - using NUnit.Framework; - using UnityEngine; - - public sealed class CommandArgTests - { - private readonly struct TestStruct1 : IEquatable - { - private readonly Guid _id; - - public TestStruct1(Guid id) - { - _id = id; - } - - public override bool Equals(object obj) - { - return obj is TestStruct1 other && Equals(other); - } - - public bool Equals(TestStruct1 other) - { - return _id.Equals(other._id); - } - - public override int GetHashCode() - { - return _id.GetHashCode(); - } - } - - private enum TestEnum1 - { - [UsedImplicitly] - Value1, - - [UsedImplicitly] - Value2, - - [UsedImplicitly] - Value3, - - [UsedImplicitly] - Value4, - - [UsedImplicitly] - Value5, - } - - private enum TestEnum2 - { - [UsedImplicitly] - Value1, - - [UsedImplicitly] - Value2, - - [UsedImplicitly] - Value3, - - [UsedImplicitly] - Value4, - - [UsedImplicitly] - Value5, - - [UsedImplicitly] - Value6, - - [UsedImplicitly] - Value7, - - [UsedImplicitly] - Value8, - } - - private const int NumTries = 5_000; - - private readonly System.Random _random = new(); - - private readonly List _prepend = new() { "(", "[", "<", "{" }; - private readonly List _append = new() { ")", "]", ">", "}" }; - - [SetUp] - [TearDown] - public void CleanUp() - { - int unregistered = CommandArg.UnregisterAllParsers(); - if (0 < unregistered) - { - Debug.Log( - $"Unregistered {unregistered} parser{(unregistered == 1 ? string.Empty : "s")}." - ); - } - } - - [Test] - public void Float() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out float value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0f, value); - arg = new CommandArg("1"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(Approximately(1.0f, value), $"Expected {value} to be equal to {1.0f}"); - arg = new CommandArg("3"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(Approximately(3.0f, value), $"Expected {value} to be equal to {3.0f}"); - arg = new CommandArg("-100"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(-100.0f, value), - $"Expected {value} to be equal to {-100.0f}" - ); - - for (int i = 0; i < NumTries; ++i) - { - float expected = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - arg = new CommandArg(expected.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(Approximately(expected, value), $"{expected} not equal to {value}"); - } - - arg = new CommandArg(nameof(float.PositiveInfinity)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(float.PositiveInfinity, value); - arg = new CommandArg(nameof(float.NegativeInfinity)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(float.NegativeInfinity, value); - arg = new CommandArg(nameof(float.NaN)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(float.NaN, value); - arg = new CommandArg(nameof(float.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(float.MaxValue, value); - - const double tooBig = (double)float.MaxValue * 2; - arg = new CommandArg(tooBig.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - const double tooSmall = (double)float.MinValue * 2; - arg = new CommandArg(tooSmall.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Decimal() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out decimal value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(new decimal(0.0), value); - arg = new CommandArg("1"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(new decimal(1), value), - $"Expected {value} to be equal to {1.0}" - ); - arg = new CommandArg("3"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(new decimal(3), value), - $"Expected {value} to be equal to {3.0}" - ); - arg = new CommandArg("-100"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(new decimal(-100), value), - $"Expected {value} to be equal to {-100.0}" - ); - - for (int i = 0; i < NumTries; ++i) - { - double expectedDouble = - _random.NextDouble() * _random.Next(int.MinValue, int.MaxValue); - decimal expected = new(expectedDouble); - arg = new CommandArg(expected.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(Approximately(expected, value), $"{expected} not equal to {value}"); - } - - arg = new CommandArg(nameof(decimal.Zero)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.Zero, value); - arg = new CommandArg(decimal.Zero.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.Zero, value); - - arg = new CommandArg(nameof(decimal.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.MaxValue, value); - arg = new CommandArg(decimal.MaxValue.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.MaxValue, value); - - arg = new CommandArg(nameof(decimal.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.MinValue, value); - arg = new CommandArg(decimal.MinValue.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.MinValue, value); - - arg = new CommandArg(nameof(decimal.One)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.One, value); - arg = new CommandArg(decimal.One.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.One, value); - - arg = new CommandArg(nameof(decimal.MinusOne)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.MinusOne, value); - arg = new CommandArg(decimal.MinusOne.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(decimal.MinusOne, value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Double() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out double value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0.0, value); - arg = new CommandArg("1"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(Approximately(1.0, value), $"Expected {value} to be equal to {1.0}"); - arg = new CommandArg("3"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(Approximately(3.0, value), $"Expected {value} to be equal to {3.0}"); - arg = new CommandArg("-100"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(-100.0, value), - $"Expected {value} to be equal to {-100.0}" - ); - - for (int i = 0; i < NumTries; ++i) - { - double expected = _random.NextDouble() * _random.Next(int.MinValue, int.MaxValue); - arg = new CommandArg(expected.ToString(CultureInfo.InvariantCulture)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(Approximately(expected, value), $"{expected} not equal to {value}"); - } - - arg = new CommandArg(nameof(double.PositiveInfinity)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(double.PositiveInfinity, value); - arg = new CommandArg(nameof(double.NegativeInfinity)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(double.NegativeInfinity, value); - arg = new CommandArg(nameof(double.NaN)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(double.NaN, value); - arg = new CommandArg(nameof(double.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(double.MaxValue, value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Bool() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out bool value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("True"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(value); - arg = new CommandArg("False"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsFalse(value); - - arg = new CommandArg("TRUE"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(value); - arg = new CommandArg("true"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(value); - - arg = new CommandArg(bool.TrueString); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue(value); - arg = new CommandArg(bool.FalseString); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsFalse(value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg(" "); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void DateTimeOffset() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out DateTimeOffset value), $"Unexpectedly parsed {value}"); - - byte[] longBytes = new byte[sizeof(long)]; - for (int i = 0; i < NumTries; ++i) - { - long ticks; - do - { - _random.NextBytes(longBytes); - ticks = BitConverter.ToInt64(longBytes, 0); - } while ( - ticks < System.DateTime.MinValue.Ticks || System.DateTime.MaxValue.Ticks < ticks - ); - - DateTimeOffset expected = new(ticks, TimeSpan.Zero); - arg = new CommandArg(expected.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - DateTimeOffset now = System.DateTimeOffset.Now; - arg = new CommandArg(now.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(now, value); - - DateTimeOffset utcNow = System.DateTimeOffset.UtcNow; - arg = new CommandArg(utcNow.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(utcNow, value); - - /* - Don't validate these, as they're mutable and might change between time of - generation and time of check, all we need to know is if they're parsable - */ - arg = new CommandArg(nameof(System.DateTimeOffset.Now)); - Assert.IsTrue(arg.TryGet(out value)); - arg = new CommandArg(nameof(System.DateTimeOffset.UtcNow)); - Assert.IsTrue(arg.TryGet(out value)); - - arg = new CommandArg(nameof(System.DateTimeOffset.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTimeOffset.MaxValue, value); - - arg = new CommandArg(System.DateTimeOffset.MaxValue.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTimeOffset.MaxValue, value); - - arg = new CommandArg(nameof(System.DateTimeOffset.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTimeOffset.MinValue, value); - - arg = new CommandArg(System.DateTimeOffset.MinValue.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTimeOffset.MinValue, value); - - arg = new CommandArg(nameof(System.DateTimeOffset.UnixEpoch)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTimeOffset.UnixEpoch, value); - - arg = new CommandArg(System.DateTimeOffset.UnixEpoch.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTimeOffset.UnixEpoch, value); - - arg = new CommandArg("00"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void DateTime() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out DateTime value), $"Unexpectedly parsed {value}"); - - DateTimeKind[] kinds = System - .Enum.GetValues(typeof(DateTimeKind)) - .OfType() - .ToArray(); - - byte[] longBytes = new byte[sizeof(long)]; - for (int i = 0; i < NumTries; ++i) - { - DateTimeKind kind = kinds[_random.Next(0, kinds.Length)]; - long ticks; - do - { - _random.NextBytes(longBytes); - ticks = BitConverter.ToInt64(longBytes, 0); - } while ( - ticks < System.DateTime.MinValue.Ticks || System.DateTime.MaxValue.Ticks < ticks - ); - - DateTime expected = new(ticks, kind); - arg = new CommandArg(expected.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - if (kind == DateTimeKind.Utc) - { - Assert.AreEqual(expected.ToLocalTime(), value); - } - else - { - if (!expected.Equals(value)) - { - Assert.IsTrue( - Math.Abs(expected.Hour - value.Hour) <= 1, - $"Failed to parse - expected: {expected}, parsed: {value}" - ); - } - else - { - Assert.AreEqual( - expected, - value, - $"Failed to parse - expected is using {expected.Kind}, parsed is using {value.Kind}" - ); - } - } - } - - DateTime now = System.DateTime.Now; - arg = new CommandArg(now.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(now, value); - - DateTime utcNow = System.DateTime.UtcNow; - arg = new CommandArg(utcNow.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(utcNow.ToLocalTime(), value); - - /* - Don't validate these, as they're mutable and might change between time of - generation and time of check, all we need to know is if they're parsable - */ - arg = new CommandArg(nameof(System.DateTime.Now)); - Assert.IsTrue(arg.TryGet(out value)); - arg = new CommandArg(nameof(System.DateTime.UtcNow)); - Assert.IsTrue(arg.TryGet(out value)); - arg = new CommandArg(nameof(System.DateTime.Today)); - Assert.IsTrue(arg.TryGet(out value)); - - arg = new CommandArg(nameof(System.DateTime.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTime.MaxValue, value); - - arg = new CommandArg(System.DateTime.MaxValue.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTime.MaxValue, value); - - arg = new CommandArg(nameof(System.DateTime.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTime.MinValue, value); - - arg = new CommandArg(System.DateTime.MinValue.ToString("O")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.DateTime.MinValue, value); - - arg = new CommandArg("00"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Guid() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Guid value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.Empty.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.Guid.Empty, value); - - for (int i = 0; i < NumTries; ++i) - { - Guid expected = System.Guid.NewGuid(); - arg = new CommandArg( - _random.Next() % 2 == 0 - ? expected.ToString().ToLower() - : expected.ToString().ToUpper() - ); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg(nameof(System.Guid.Empty)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(System.Guid.Empty, value); - - arg = new CommandArg("00"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Char() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out char value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual('0', value); - - for (int i = 0; i < NumTries; ++i) - { - char expected = (char)_random.Next(char.MinValue, char.MaxValue); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue( - arg.TryGet(out value), - $"Failed to parse {expected} as char. Cleaned arg: '{arg.CleanedContents}'" - ); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg("00"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("z"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents[0], value); - - arg = new CommandArg(nameof(char.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(char.MaxValue, value); - - arg = new CommandArg(char.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(char.MaxValue, value); - - arg = new CommandArg(nameof(char.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(char.MinValue, value); - - arg = new CommandArg(char.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(char.MinValue, value); - - const long tooBig = char.MaxValue + 1L; - arg = new CommandArg(tooBig.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - const long tooSmall = char.MinValue - 1L; - arg = new CommandArg(tooSmall.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Sbyte() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out sbyte value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0, value); - - for (int i = 0; i < NumTries; ++i) - { - sbyte expected = (sbyte)_random.Next(sbyte.MinValue, sbyte.MaxValue); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg(nameof(sbyte.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(sbyte.MaxValue, value); - - arg = new CommandArg(sbyte.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(sbyte.MaxValue, value); - - arg = new CommandArg(nameof(sbyte.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(sbyte.MinValue, value); - - arg = new CommandArg(sbyte.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(sbyte.MinValue, value); - - long tooBig = sbyte.MaxValue + 1L; - arg = new CommandArg(tooBig.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - long tooSmall = sbyte.MinValue - 1L; - arg = new CommandArg(tooSmall.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Byte() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out byte value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0, value); - - for (int i = 0; i < NumTries; ++i) - { - byte expected = (byte)_random.Next(byte.MinValue, byte.MaxValue); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg(nameof(byte.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(byte.MaxValue, value); - - arg = new CommandArg(byte.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(byte.MaxValue, value); - - arg = new CommandArg(nameof(byte.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(byte.MinValue, value); - - arg = new CommandArg(byte.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(byte.MinValue, value); - - const long tooBig = byte.MaxValue + 1L; - arg = new CommandArg(tooBig.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - const long tooSmall = byte.MinValue - 1L; - arg = new CommandArg(tooSmall.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Short() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out short value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0, value); - - for (int i = 0; i < NumTries; ++i) - { - short expected = (short)_random.Next(short.MinValue, short.MaxValue); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg(nameof(short.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(short.MaxValue, value); - - arg = new CommandArg(short.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(short.MaxValue, value); - - arg = new CommandArg(nameof(short.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(short.MinValue, value); - - arg = new CommandArg(short.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(short.MinValue, value); - - const long tooBig = short.MaxValue + 1L; - arg = new CommandArg(tooBig.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - const long tooSmall = short.MinValue - 1L; - arg = new CommandArg(tooSmall.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Int() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out int value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0, value); - - for (int i = 0; i < NumTries; ++i) - { - int expected = _random.Next(int.MinValue, int.MaxValue); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg(nameof(int.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(int.MaxValue, value); - - arg = new CommandArg(int.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(int.MaxValue, value); - - arg = new CommandArg(nameof(int.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(int.MinValue, value); - - arg = new CommandArg(int.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(int.MinValue, value); - - const long tooBig = int.MaxValue + 1L; - arg = new CommandArg(tooBig.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - const long tooSmall = int.MinValue - 1L; - arg = new CommandArg(tooSmall.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Long() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out long value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0L, value); - - byte[] bytes = new byte[sizeof(long)]; - for (int i = 0; i < NumTries; ++i) - { - _random.NextBytes(bytes); - long expected = BitConverter.ToInt64(bytes, 0); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg(nameof(long.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(long.MaxValue, value); - - arg = new CommandArg(long.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(long.MaxValue, value); - - arg = new CommandArg(nameof(long.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(long.MinValue, value); - - arg = new CommandArg(long.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(long.MinValue, value); - - arg = new CommandArg(long.MinValue + "0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(long.MinValue + "0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Ulong() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out ulong value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0UL, value); - - byte[] bytes = new byte[sizeof(ulong)]; - for (int i = 0; i < NumTries; ++i) - { - _random.NextBytes(bytes); - ulong expected = BitConverter.ToUInt64(bytes, 0); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg(nameof(ulong.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ulong.MaxValue, value); - - arg = new CommandArg(ulong.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ulong.MaxValue, value); - - arg = new CommandArg(nameof(ulong.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ulong.MinValue, value); - - arg = new CommandArg(ulong.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ulong.MinValue, value); - - arg = new CommandArg("-1"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(ulong.MaxValue + "0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Enum() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out TestEnum1 value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual((TestEnum1)0, value); - - TestEnum1[] testEnum1Values = System - .Enum.GetValues(typeof(TestEnum1)) - .OfType() - .ToArray(); - for (int i = 0; i < NumTries; ++i) - { - int index = _random.Next(testEnum1Values.Length); - TestEnum1 expected = testEnum1Values[index]; - arg = new CommandArg(expected.ToString("G")); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - - arg = new CommandArg("Value6"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - Assert.IsTrue(arg.TryGet(out TestEnum2 testEnum2Value)); - Assert.AreEqual(TestEnum2.Value6, testEnum2Value); - - TestEnum2[] testEnum2Values = System - .Enum.GetValues(typeof(TestEnum2)) - .OfType() - .ToArray(); - for (int i = 0; i < NumTries; ++i) - { - int index = _random.Next(testEnum2Values.Length); - TestEnum2 expected = testEnum2Values[index]; - arg = new CommandArg(expected.ToString("G")); - Assert.IsTrue(arg.TryGet(out TestEnum2 value2)); - Assert.AreEqual(expected, value2); - } - - arg = new CommandArg("1"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual((TestEnum1)1, value); - Assert.IsTrue(arg.TryGet(out testEnum2Value)); - Assert.AreEqual((TestEnum2)1, testEnum2Value); - - arg = new CommandArg("100"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("-1"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Vector2Int() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Vector2Int value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Vector2Int expected; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(short.MinValue, short.MaxValue); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - // x,y - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(int.MinValue, int.MaxValue); - int y = _random.Next(int.MinValue, int.MaxValue); - expected = new Vector2Int(x, y); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg($"{expected.x},{expected.y}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - } - - // x,y, z - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(int.MinValue, int.MaxValue); - int y = _random.Next(int.MinValue, int.MaxValue); - int z = _random.Next(int.MinValue, int.MaxValue); - expected = new Vector2Int(x, y); - - arg = new CommandArg($"{expected.x},{expected.y},{z}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - } - - arg = new CommandArg(nameof(UnityEngine.Vector2Int.zero)); - expected = UnityEngine.Vector2Int.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector2Int.zero.ToString()); - expected = UnityEngine.Vector2Int.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(nameof(UnityEngine.Vector2Int.up)); - expected = UnityEngine.Vector2Int.up; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector2Int.up.ToString()); - expected = UnityEngine.Vector2Int.up; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(nameof(UnityEngine.Vector2Int.left)); - expected = UnityEngine.Vector2Int.left; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector2Int.left.ToString()); - expected = UnityEngine.Vector2Int.left; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Vector3Int() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Vector3Int value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Vector3Int expected; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(short.MinValue, short.MaxValue); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - // x,y - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(int.MinValue, int.MaxValue); - int y = _random.Next(int.MinValue, int.MaxValue); - expected = new Vector3Int(x, y); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg($"{expected.x},{expected.y}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - } - - // x,y,z - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(int.MinValue, int.MaxValue); - int y = _random.Next(int.MinValue, int.MaxValue); - int z = _random.Next(int.MinValue, int.MaxValue); - expected = new Vector3Int(x, y, z); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg($"{expected.x},{expected.y},{expected.z}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y},{expected.z}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - } - - arg = new CommandArg(nameof(UnityEngine.Vector3Int.zero)); - expected = UnityEngine.Vector3Int.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector3Int.zero.ToString()); - expected = UnityEngine.Vector3Int.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(nameof(UnityEngine.Vector3Int.up)); - expected = UnityEngine.Vector3Int.up; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector3Int.up.ToString()); - expected = UnityEngine.Vector3Int.up; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(nameof(UnityEngine.Vector3Int.left)); - expected = UnityEngine.Vector3Int.left; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector3Int.left.ToString()); - expected = UnityEngine.Vector3Int.left; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(nameof(UnityEngine.Vector3Int.forward)); - expected = UnityEngine.Vector3Int.forward; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector3Int.forward.ToString()); - expected = UnityEngine.Vector3Int.forward; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(nameof(UnityEngine.Vector3Int.one)); - expected = UnityEngine.Vector3Int.one; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Vector3Int.one.ToString()); - expected = UnityEngine.Vector3Int.one; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Rect() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Rect value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Rect expected; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(short.MinValue, short.MaxValue); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - const float rectTolerance = 0.01f; - - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float width = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float height = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - expected = new Rect(x, y, width, height); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, rectTolerance) - && Approximately(expected.y, value.y, rectTolerance) - && Approximately(expected.width, value.width, rectTolerance) - && Approximately(expected.height, value.height, rectTolerance) - ); - - arg = new CommandArg( - $"{expected.x},{expected.y},{expected.width},{expected.height}" - ); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, rectTolerance) - && Approximately(expected.y, value.y, rectTolerance) - && Approximately(expected.width, value.width, rectTolerance) - && Approximately(expected.height, value.height, rectTolerance) - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg( - $"{pre}{expected.x},{expected.y},{expected.width},{expected.height}{post}" - ); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, rectTolerance) - && Approximately(expected.y, value.y, rectTolerance) - && Approximately(expected.width, value.width, rectTolerance) - && Approximately(expected.height, value.height, rectTolerance) - ); - } - } - - arg = new CommandArg(nameof(UnityEngine.Rect.zero)); - expected = UnityEngine.Rect.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - arg = new CommandArg(UnityEngine.Rect.zero.ToString()); - expected = UnityEngine.Rect.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void RectInt() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out RectInt value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - RectInt expected; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(short.MinValue, short.MaxValue); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - for (int i = 0; i < NumTries; ++i) - { - int x = _random.Next(int.MinValue, int.MaxValue); - int y = _random.Next(int.MinValue, int.MaxValue); - int width = _random.Next(int.MinValue, int.MaxValue); - int height = _random.Next(int.MinValue, int.MaxValue); - expected = new RectInt(x, y, width, height); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg( - $"{expected.x},{expected.y},{expected.width},{expected.height}" - ); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg( - $"{pre}{expected.x},{expected.y},{expected.width},{expected.height}{post}" - ); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - } - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Vector2() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Vector2 value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Vector2 expected; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - const float vector2RoundTolerance = 0.01f; - - // x,y - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - expected = new Vector2(x, y); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, vector2RoundTolerance) - && Approximately(expected.y, value.y, vector2RoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y}). " - + $"Value: ({value.x},{value.y}). " - + $"Expected: ({expected.x},{expected.y})." - ); - - arg = new CommandArg($"{expected.x},{expected.y}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) && Approximately(expected.y, value.y), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) && Approximately(expected.y, value.y), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." - ); - } - } - - // x,y,z (z is ok, but ignored) - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float z = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - expected = new Vector2(x, y); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, vector2RoundTolerance) - && Approximately(expected.y, value.y, vector2RoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y},{z}). " - + $"Value: ({value.x},{value.y}). " - + $"Expected: ({expected.x},{expected.y})." - ); - - arg = new CommandArg($"{expected.x},{expected.y},{z}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) && Approximately(expected.y, value.y), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) && Approximately(expected.y, value.y), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." - ); - } - } - - arg = new CommandArg(nameof(UnityEngine.Vector2.zero)); - expected = UnityEngine.Vector2.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) && Approximately(expected.y, value.y), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector2.up)); - expected = UnityEngine.Vector2.up; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) && Approximately(expected.y, value.y), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector2.left)); - expected = UnityEngine.Vector2.left; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) && Approximately(expected.y, value.y), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Vector3() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Vector3 value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Vector3 expected; - - const float vector3RoundTolerance = 0.01f; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - // x,y - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float z = 0f; - expected = new Vector3(x, y); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, vector3RoundTolerance) - && Approximately(expected.y, value.y, vector3RoundTolerance) - && Approximately(expected.z, value.z, vector3RoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y},{z}). " - + $"Value: ({value.x},{value.y},{value.z}). " - + $"Expected: ({expected.x},{expected.y},{expected.z})." - ); - - arg = new CommandArg($"{expected.x},{expected.y}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z}). Expected: ({x},{y},{z})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." - ); - } - } - - // x,y,z (z is ok, but ignored) - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float z = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - expected = new Vector3(x, y, z); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, vector3RoundTolerance) - && Approximately(expected.y, value.y, vector3RoundTolerance) - && Approximately(expected.z, value.z, vector3RoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y},{z}). " - + $"Value: ({value.x},{value.y},{value.z}). " - + $"Expected: ({expected.x},{expected.y},{expected.z})." - ); - - arg = new CommandArg($"{expected.x},{expected.y},{expected.z}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z}). Expected: ({x},{y},{z})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z}). Expected: ({x},{y},{z})." - ); - } - } - - arg = new CommandArg(nameof(UnityEngine.Vector3.zero)); - expected = UnityEngine.Vector3.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector3.up)); - expected = UnityEngine.Vector3.up; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector3.left)); - expected = UnityEngine.Vector3.left; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector3.back)); - expected = UnityEngine.Vector3.back; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector3.forward)); - expected = UnityEngine.Vector3.forward; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector3.one)); - expected = UnityEngine.Vector3.one; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Vector4() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Vector4 value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Vector4 expected; - - const float vector4RoundTolerance = 0.01f; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - // x,y - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float z = 0f; - float w = 0f; - expected = new Vector4(x, y); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, vector4RoundTolerance) - && Approximately(expected.y, value.y, vector4RoundTolerance) - && Approximately(expected.z, value.z, vector4RoundTolerance) - && Approximately(expected.w, value.w, vector4RoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y},{z},{w}). " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). " - + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." - ); - - arg = new CommandArg($"{expected.x},{expected.y}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." - ); - } - } - - // x,y,z - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float z = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float w = 0f; - expected = new Vector4(x, y, z); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, vector4RoundTolerance) - && Approximately(expected.y, value.y, vector4RoundTolerance) - && Approximately(expected.z, value.z, vector4RoundTolerance) - && Approximately(expected.w, value.w, vector4RoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y},{z},{w}). " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). " - + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." - ); - - arg = new CommandArg($"{expected.x},{expected.y},{expected.z}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." - ); - } - } - - // x,y,z,w - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float z = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float w = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - expected = new Vector4(x, y, z, w); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, vector4RoundTolerance) - && Approximately(expected.y, value.y, vector4RoundTolerance) - && Approximately(expected.z, value.z, vector4RoundTolerance) - && Approximately(expected.w, value.w, vector4RoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y},{z},{w}). " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). " - + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." - ); - - arg = new CommandArg($"{expected.x},{expected.y},{expected.z},{expected.w}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg( - $"{pre}{expected.x},{expected.y},{expected.z},{expected.w}{post}" - ); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." - ); - } - } - - arg = new CommandArg(nameof(UnityEngine.Vector4.zero)); - expected = UnityEngine.Vector4.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}." - ); - arg = new CommandArg(UnityEngine.Vector4.zero.ToString()); - expected = UnityEngine.Vector4.zero; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector4.negativeInfinity)); - expected = UnityEngine.Vector4.negativeInfinity; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}." - ); - arg = new CommandArg(UnityEngine.Vector4.negativeInfinity.ToString()); - expected = UnityEngine.Vector4.negativeInfinity; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(nameof(UnityEngine.Vector4.positiveInfinity)); - expected = UnityEngine.Vector4.positiveInfinity; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}." - ); - arg = new CommandArg(UnityEngine.Vector4.positiveInfinity.ToString()); - expected = UnityEngine.Vector4.positiveInfinity; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}." - ); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Uint() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out uint value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(0U, value); - - arg = new CommandArg("-1"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("1.3"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - unchecked - { - for (int i = 0; i < NumTries; ++i) - { - uint expected = (uint)_random.Next(int.MinValue, int.MaxValue); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - } - - const long tooBig = uint.MaxValue + 1L; - arg = new CommandArg(tooBig.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(nameof(uint.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(uint.MaxValue, value); - - arg = new CommandArg(nameof(uint.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(uint.MinValue, value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Ushort() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out ushort value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("0"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual((ushort)0, value); - - arg = new CommandArg("-1"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg("1.3"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - unchecked - { - for (int i = 0; i < NumTries; ++i) - { - ushort expected = (ushort)_random.Next(short.MinValue, short.MaxValue); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - } - } - - const int tooBig = ushort.MaxValue + 1; - arg = new CommandArg(tooBig.ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - arg = new CommandArg(nameof(ushort.MaxValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ushort.MaxValue, value); - - arg = new CommandArg(ushort.MaxValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ushort.MaxValue, value); - - arg = new CommandArg(nameof(ushort.MinValue)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ushort.MinValue, value); - - arg = new CommandArg(ushort.MinValue.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(ushort.MinValue, value); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void String() - { - string expected = string.Empty; - CommandArg arg = new(expected); - Assert.IsTrue(arg.TryGet(out string value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - - expected = "asdf"; - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - - expected = "1111"; - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - - expected = "1.3333"; - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - - for (int i = 0; i < NumTries; ++i) - { - expected = System.Guid.NewGuid().ToString(); - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - } - - expected = "#$$$__.azxfd87&*_&&&-={'|"; - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - - // Check strings aren't sanitized - expected = " "; - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - - // Make sure string.Empty isn't resolved to "" - expected = "Empty"; - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - - expected = "string.Empty"; - arg = new CommandArg(expected); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(arg.contents, value); - Assert.AreEqual(expected, value); - } - - [Test] - public void Quaternion() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Quaternion value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Quaternion expected; - - // Unexpected input - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x}{post}"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - } - - const float quaternionRoundTolerance = 0.00001f; - - // x,y,z, w (z is ok, but ignored) - for (int i = 0; i < NumTries; ++i) - { - float x = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float y = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float z = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - float w = (float)( - _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) - ); - expected = new Quaternion(x, y, z, w); - arg = new CommandArg(expected.ToString()); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x, quaternionRoundTolerance) - && Approximately(expected.y, value.y, quaternionRoundTolerance) - && Approximately(expected.z, value.z, quaternionRoundTolerance) - && Approximately(expected.w, value.w, quaternionRoundTolerance), - $"Expected {value} to be approximately {expected}. " - + $"Input: ({x},{y},{z},{w}). " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). " - + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." - ); - - arg = new CommandArg($"{x},{y},{z},{w}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). " - + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{x},{y},{z},{w}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). " - + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." - ); - } - } - - arg = new CommandArg(nameof(UnityEngine.Quaternion.identity)); - expected = UnityEngine.Quaternion.identity; - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.x, value.x) - && Approximately(expected.y, value.y) - && Approximately(expected.z, value.z) - && Approximately(expected.w, value.w), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({value.x},{value.y},{value.z},{value.w}). " - + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." - ); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Color() - { - CommandArg arg = new(""); - Assert.IsFalse(arg.TryGet(out Color value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("0"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - Color expected = UnityEngine.Color.white; - arg = new CommandArg(nameof(UnityEngine.Color.white)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - expected = UnityEngine.Color.red; - arg = new CommandArg(nameof(UnityEngine.Color.red)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - expected = UnityEngine.Color.cyan; - arg = new CommandArg(nameof(UnityEngine.Color.cyan)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - expected = UnityEngine.Color.black; - arg = new CommandArg(nameof(UnityEngine.Color.black)); - Assert.IsTrue(arg.TryGet(out value)); - Assert.AreEqual(expected, value); - - arg = new CommandArg("(1.0, 0.5)"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("(0.7)"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - - for (int i = 0; i < NumTries; ++i) - { - float r = (float)_random.NextDouble(); - float g = (float)_random.NextDouble(); - float b = (float)_random.NextDouble(); - expected = new Color(r, g, b); - arg = new CommandArg(expected.ToString()); - - // Colors have a floating point precision of 3 decimal places, otherwise our equality checks will be off - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.r, value.r, 0.001f) - && Approximately(expected.g, value.g, 0.001f) - && Approximately(expected.b, value.b, 0.001f) - && Approximately(expected.a, value.a, 0.001f), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({r},{g},{b},{expected.a}). " - + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." - ); - - arg = new CommandArg($"{r},{g},{b}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.r, value.r, 0.001f) - && Approximately(expected.g, value.g, 0.001f) - && Approximately(expected.b, value.b, 0.001f) - && Approximately(expected.a, value.a, 0.001f), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({r},{g},{b},{expected.a}). " - + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{r},{g},{b}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.r, value.r, 0.001f) - && Approximately(expected.g, value.g, 0.001f) - && Approximately(expected.b, value.b, 0.001f) - && Approximately(expected.a, value.a, 0.001f), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({r},{g},{b},{expected.a}). " - + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." - ); - } - } - - for (int i = 0; i < NumTries; ++i) - { - float r = (float)_random.NextDouble(); - float g = (float)_random.NextDouble(); - float b = (float)_random.NextDouble(); - float a = (float)_random.NextDouble(); - expected = new Color(r, g, b, a); - arg = new CommandArg(expected.ToString()); - - // Colors have a floating point precision of 3 decimal places, otherwise our equality checks will be off - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.r, value.r, 0.001f) - && Approximately(expected.g, value.g, 0.001f) - && Approximately(expected.b, value.b, 0.001f) - && Approximately(expected.a, value.a, 0.001f), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({r},{g},{b},{a}). " - + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." - ); - - arg = new CommandArg($"{r},{g},{b},{a}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.r, value.r, 0.001f) - && Approximately(expected.g, value.g, 0.001f) - && Approximately(expected.b, value.b, 0.001f) - && Approximately(expected.a, value.a, 0.001f), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({r},{g},{b},{a}). " - + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." - ); - - foreach ( - (string pre, string post) in _prepend.Zip( - _append, - (preValue, postValue) => (preValue, postValue) - ) - ) - { - arg = new CommandArg($"{pre}{r},{g},{b},{a}{post}"); - Assert.IsTrue(arg.TryGet(out value)); - Assert.IsTrue( - Approximately(expected.r, value.r, 0.001f) - && Approximately(expected.g, value.g, 0.001f) - && Approximately(expected.b, value.b, 0.001f) - && Approximately(expected.a, value.a, 0.001f), - $"Expected {value} to be approximately {expected}. " - + $"Value: ({r},{g},{b},{a}). " - + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." - ); - } - } - - arg = new CommandArg("invisible"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("false"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); - } - - [Test] - public void Untyped() - { - CommandArg arg = new("1"); - Assert.IsTrue(arg.TryGet(typeof(int), out object value)); - Assert.AreEqual(1, value); - - arg = new CommandArg("2.5"); - Assert.IsTrue(arg.TryGet(typeof(float), out value)); - Assert.IsTrue( - Approximately((float)value, 2.5f), - $"Expected {value} to be approximately {2.5f}" - ); - - arg = new CommandArg("red"); - Assert.IsTrue(arg.TryGet(typeof(Color), out value)); - Assert.AreEqual(UnityEngine.Color.red, (Color)value); - - arg = new CommandArg("invisible"); - Assert.IsFalse(arg.TryGet(typeof(Color), out value)); - - arg = new CommandArg("(1.2564, 3.6)"); - Assert.IsTrue(arg.TryGet(typeof(Vector2), out value)); - Vector2 expected = (Vector2)value; - Assert.IsTrue( - Approximately(expected.x, 1.2564f) && Approximately(expected.y, 3.6f), - $"Expected {expected} to be approximately {arg.contents}" - ); - - arg = new CommandArg("asdf"); - Assert.IsFalse(arg.TryGet(typeof(float), out value)); - Assert.IsFalse(arg.TryGet(typeof(int), out value)); - Assert.IsTrue(arg.TryGet(typeof(string), out value)); - Assert.AreEqual(arg.contents, value); - } - - [Test] - public void CustomParserBuiltInType() - { - for (int i = 0; i < NumTries; ++i) - { - string expectedString = System.Guid.NewGuid().ToString(); - int expected = _random.Next(int.MinValue, int.MaxValue); - CommandArg arg = new(expectedString); - Assert.IsFalse(arg.TryGet(out int value)); - Assert.IsTrue(arg.TryGet(out value, CustomParser)); - Assert.AreEqual(expected, value); - - // Make sure the parser isn't sticky - Assert.IsFalse(arg.TryGet(out value)); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value, CustomParser)); - continue; - - bool CustomParser(string input, out int parsed) - { - if (string.Equals(expectedString, input, StringComparison.OrdinalIgnoreCase)) - { - parsed = expected; - return true; - } - - parsed = 0; - return false; - } - } - } - - [Test] - public void CustomParserCustomType() - { - for (int i = 0; i < NumTries; ++i) - { - string expectedString = System.Guid.NewGuid().ToString(); - TestStruct1 expected = new(System.Guid.NewGuid()); - CommandArg arg = new(expectedString); - Assert.IsFalse(arg.TryGet(out TestStruct1 value)); - Assert.IsTrue(arg.TryGet(out value, CustomParser)); - Assert.AreEqual(expected, value); - - // Make sure the parser isn't sticky - Assert.IsFalse(arg.TryGet(out value)); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - Assert.IsFalse(arg.TryGet(out value, CustomParser)); - - continue; - - bool CustomParser(string input, out TestStruct1 parsed) - { - if (string.Equals(expectedString, input, StringComparison.OrdinalIgnoreCase)) - { - parsed = expected; - return true; - } - - parsed = default; - return false; - } - } - } - - [Test] - public void RegisteredParsersAreUsed() - { - const int constParsed = -23; - bool registered = CommandArg.RegisterParser(CustomIntParser); - Assert.IsTrue(registered); - - CommandArg arg = new("Garbage"); - RunRegisteredParsingLogic(); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - RunRegisteredParsingLogic(); - - arg = new CommandArg(""); - RunRegisteredParsingLogic(); - - arg = new CommandArg("x_YZZZZ$$$"); - RunRegisteredParsingLogic(); - - bool unregistered = CommandArg.UnregisterParser(); - Assert.IsTrue(unregistered); - - arg = new CommandArg("Garbage"); - RunUnregisteredParsingLogic(); - - arg = new CommandArg(System.Guid.NewGuid().ToString()); - RunUnregisteredParsingLogic(); - - arg = new CommandArg(""); - RunUnregisteredParsingLogic(); - - arg = new CommandArg("x_YZZZZ$$$"); - RunUnregisteredParsingLogic(); - - return; - - void RunRegisteredParsingLogic() - { - Assert.IsTrue(arg.TryGet(typeof(int), out object value)); - Assert.AreEqual(constParsed, value); - Assert.IsTrue(arg.TryGet(out int parsed)); - Assert.AreEqual(constParsed, parsed); - Assert.IsFalse(arg.TryGet(out float _)); - } - - void RunUnregisteredParsingLogic() - { - Assert.IsFalse(arg.TryGet(typeof(int), out object _)); - Assert.IsFalse(arg.TryGet(out int _)); - Assert.IsFalse(arg.TryGet(out float _)); - } - - static bool CustomIntParser(string input, out int parsed) - { - parsed = constParsed; - return true; - } - } - - [Test] - public void ParserRegistration() - { - bool registered = CommandArg.RegisterParser(CustomIntParser1); - Assert.IsTrue(registered); - Assert.IsTrue(CommandArg.TryGetParser(out CommandArgParser registeredParser)); - Assert.AreEqual((CommandArgParser)CustomIntParser1, registeredParser); - - registered = CommandArg.RegisterParser(CustomIntParser1); - Assert.IsFalse(registered); - Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); - Assert.AreEqual((CommandArgParser)CustomIntParser1, registeredParser); - - registered = CommandArg.RegisterParser(CustomIntParser2); - Assert.IsFalse(registered); - Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); - Assert.AreEqual((CommandArgParser)CustomIntParser1, registeredParser); - - registered = CommandArg.RegisterParser(CustomIntParser2, force: true); - Assert.IsTrue(registered); - - Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); - Assert.AreEqual((CommandArgParser)CustomIntParser2, registeredParser); - - return; - - static bool CustomIntParser1(string input, out int parsed) - { - parsed = 1; - return false; - } - - static bool CustomIntParser2(string input, out int parsed) - { - parsed = 2; - return false; - } - } - - [Test] - public void NullParserRegistration() - { - bool registered = CommandArg.RegisterParser(null); - Assert.IsFalse(registered); - registered = CommandArg.RegisterParser(null, force: true); - Assert.IsFalse(registered); - } - - [Test] - public void ParserDeregistration() - { - Assert.IsFalse(CommandArg.TryGetParser(out CommandArgParser registeredParser)); - - bool deregistered = CommandArg.UnregisterParser(); - Assert.IsFalse(deregistered); - - bool registered = CommandArg.RegisterParser(CustomIntParser1); - Assert.IsTrue(registered); - Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); - Assert.IsNotNull(registeredParser); - - deregistered = CommandArg.UnregisterParser(); - Assert.IsTrue(deregistered); - Assert.IsFalse(CommandArg.TryGetParser(out registeredParser)); - - deregistered = CommandArg.UnregisterParser(); - Assert.IsFalse(deregistered); - Assert.IsFalse(CommandArg.TryGetParser(out registeredParser)); - - return; - - static bool CustomIntParser1(string input, out int parsed) - { - parsed = 1; - return false; - } - } - - private static bool Approximately(float a, float b, float tolerance = 0.0001f) - { - float delta = Math.Abs(a - b); - // Check ToString representations too, the numbers may be crazy small or crazy big, outside the scope of our tolerance - return delta <= tolerance - || a.ToString(CultureInfo.InvariantCulture) - .Equals(b.ToString(CultureInfo.InvariantCulture)); - } - - private static bool Approximately(decimal a, decimal b, decimal? tolerance = null) - { - if (a == b) - { - return true; - } - - tolerance ??= new decimal(0.0001); - - decimal delta = Math.Abs(a - b); - return delta <= tolerance - || a.ToString(CultureInfo.InvariantCulture) - .Equals(b.ToString(CultureInfo.InvariantCulture)); - } - - private static bool Approximately(double a, double b, double tolerance = 0.0001) - { - double delta = Math.Abs(a - b); - // Check ToString representations too, the numbers may be crazy small or crazy big, outside the scope of our tolerance - return delta <= tolerance - || a.ToString(CultureInfo.InvariantCulture) - .Equals(b.ToString(CultureInfo.InvariantCulture)); - } - } -} +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using Backend; + using JetBrains.Annotations; + using NUnit.Framework; + using UnityEngine; + + public sealed class CommandArgTests + { + private readonly struct TestStruct1 : IEquatable + { + private readonly Guid _id; + + public TestStruct1(Guid id) + { + _id = id; + } + + public override bool Equals(object obj) + { + return obj is TestStruct1 other && Equals(other); + } + + public bool Equals(TestStruct1 other) + { + return _id.Equals(other._id); + } + + public override int GetHashCode() + { + return _id.GetHashCode(); + } + } + + private enum TestEnum1 + { + [UsedImplicitly] + Value1, + + [UsedImplicitly] + Value2, + + [UsedImplicitly] + Value3, + + [UsedImplicitly] + Value4, + + [UsedImplicitly] + Value5, + } + + private enum TestEnum2 + { + [UsedImplicitly] + Value1, + + [UsedImplicitly] + Value2, + + [UsedImplicitly] + Value3, + + [UsedImplicitly] + Value4, + + [UsedImplicitly] + Value5, + + [UsedImplicitly] + Value6, + + [UsedImplicitly] + Value7, + + [UsedImplicitly] + Value8, + } + + private const int NumTries = 5_000; + + private readonly System.Random _random = new(); + + private readonly List _prepend = new() { "(", "[", "<", "{" }; + private readonly List _append = new() { ")", "]", ">", "}" }; + + [SetUp] + [TearDown] + public void CleanUp() + { + int unregistered = CommandArg.UnregisterAllParsers(); + if (0 < unregistered) + { + Debug.Log( + $"Unregistered {unregistered} parser{(unregistered == 1 ? string.Empty : "s")}." + ); + } + } + + [Test] + public void Float() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out float value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0f, value); + arg = new CommandArg("1"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(Approximately(1.0f, value), $"Expected {value} to be equal to {1.0f}"); + arg = new CommandArg("3"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(Approximately(3.0f, value), $"Expected {value} to be equal to {3.0f}"); + arg = new CommandArg("-100"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(-100.0f, value), + $"Expected {value} to be equal to {-100.0f}" + ); + + for (int i = 0; i < NumTries; ++i) + { + float expected = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + arg = new CommandArg(expected.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(Approximately(expected, value), $"{expected} not equal to {value}"); + } + + arg = new CommandArg(nameof(float.PositiveInfinity)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(float.PositiveInfinity, value); + arg = new CommandArg(nameof(float.NegativeInfinity)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(float.NegativeInfinity, value); + arg = new CommandArg(nameof(float.NaN)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(float.NaN, value); + arg = new CommandArg(nameof(float.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(float.MaxValue, value); + + const double tooBig = (double)float.MaxValue * 2; + arg = new CommandArg(tooBig.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + const double tooSmall = (double)float.MinValue * 2; + arg = new CommandArg(tooSmall.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Decimal() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out decimal value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(new decimal(0.0), value); + arg = new CommandArg("1"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(new decimal(1), value), + $"Expected {value} to be equal to {1.0}" + ); + arg = new CommandArg("3"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(new decimal(3), value), + $"Expected {value} to be equal to {3.0}" + ); + arg = new CommandArg("-100"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(new decimal(-100), value), + $"Expected {value} to be equal to {-100.0}" + ); + + for (int i = 0; i < NumTries; ++i) + { + double expectedDouble = + _random.NextDouble() * _random.Next(int.MinValue, int.MaxValue); + decimal expected = new(expectedDouble); + arg = new CommandArg(expected.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(Approximately(expected, value), $"{expected} not equal to {value}"); + } + + arg = new CommandArg(nameof(decimal.Zero)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.Zero, value); + arg = new CommandArg(decimal.Zero.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.Zero, value); + + arg = new CommandArg(nameof(decimal.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.MaxValue, value); + arg = new CommandArg(decimal.MaxValue.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.MaxValue, value); + + arg = new CommandArg(nameof(decimal.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.MinValue, value); + arg = new CommandArg(decimal.MinValue.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.MinValue, value); + + arg = new CommandArg(nameof(decimal.One)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.One, value); + arg = new CommandArg(decimal.One.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.One, value); + + arg = new CommandArg(nameof(decimal.MinusOne)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.MinusOne, value); + arg = new CommandArg(decimal.MinusOne.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(decimal.MinusOne, value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Double() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out double value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0.0, value); + arg = new CommandArg("1"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(Approximately(1.0, value), $"Expected {value} to be equal to {1.0}"); + arg = new CommandArg("3"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(Approximately(3.0, value), $"Expected {value} to be equal to {3.0}"); + arg = new CommandArg("-100"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(-100.0, value), + $"Expected {value} to be equal to {-100.0}" + ); + + for (int i = 0; i < NumTries; ++i) + { + double expected = _random.NextDouble() * _random.Next(int.MinValue, int.MaxValue); + arg = new CommandArg(expected.ToString(CultureInfo.InvariantCulture)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(Approximately(expected, value), $"{expected} not equal to {value}"); + } + + arg = new CommandArg(nameof(double.PositiveInfinity)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(double.PositiveInfinity, value); + arg = new CommandArg(nameof(double.NegativeInfinity)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(double.NegativeInfinity, value); + arg = new CommandArg(nameof(double.NaN)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(double.NaN, value); + arg = new CommandArg(nameof(double.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(double.MaxValue, value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Bool() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out bool value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("True"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(value); + arg = new CommandArg("False"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsFalse(value); + + arg = new CommandArg("TRUE"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(value); + arg = new CommandArg("true"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(value); + + arg = new CommandArg(bool.TrueString); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue(value); + arg = new CommandArg(bool.FalseString); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsFalse(value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg(" "); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void DateTimeOffset() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out DateTimeOffset value), $"Unexpectedly parsed {value}"); + + byte[] longBytes = new byte[sizeof(long)]; + for (int i = 0; i < NumTries; ++i) + { + long ticks; + do + { + _random.NextBytes(longBytes); + ticks = BitConverter.ToInt64(longBytes, 0); + } while ( + ticks < System.DateTime.MinValue.Ticks || System.DateTime.MaxValue.Ticks < ticks + ); + + DateTimeOffset expected = new(ticks, TimeSpan.Zero); + arg = new CommandArg(expected.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + DateTimeOffset now = System.DateTimeOffset.Now; + arg = new CommandArg(now.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(now, value); + + DateTimeOffset utcNow = System.DateTimeOffset.UtcNow; + arg = new CommandArg(utcNow.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(utcNow, value); + + /* + Don't validate these, as they're mutable and might change between time of + generation and time of check, all we need to know is if they're parsable + */ + arg = new CommandArg(nameof(System.DateTimeOffset.Now)); + Assert.IsTrue(arg.TryGet(out value)); + arg = new CommandArg(nameof(System.DateTimeOffset.UtcNow)); + Assert.IsTrue(arg.TryGet(out value)); + + arg = new CommandArg(nameof(System.DateTimeOffset.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTimeOffset.MaxValue, value); + + arg = new CommandArg(System.DateTimeOffset.MaxValue.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTimeOffset.MaxValue, value); + + arg = new CommandArg(nameof(System.DateTimeOffset.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTimeOffset.MinValue, value); + + arg = new CommandArg(System.DateTimeOffset.MinValue.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTimeOffset.MinValue, value); + + arg = new CommandArg(nameof(System.DateTimeOffset.UnixEpoch)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTimeOffset.UnixEpoch, value); + + arg = new CommandArg(System.DateTimeOffset.UnixEpoch.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTimeOffset.UnixEpoch, value); + + arg = new CommandArg("00"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void DateTime() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out DateTime value), $"Unexpectedly parsed {value}"); + + DateTimeKind[] kinds = System + .Enum.GetValues(typeof(DateTimeKind)) + .OfType() + .ToArray(); + + byte[] longBytes = new byte[sizeof(long)]; + for (int i = 0; i < NumTries; ++i) + { + DateTimeKind kind = kinds[_random.Next(0, kinds.Length)]; + long ticks; + do + { + _random.NextBytes(longBytes); + ticks = BitConverter.ToInt64(longBytes, 0); + } while ( + ticks < System.DateTime.MinValue.Ticks || System.DateTime.MaxValue.Ticks < ticks + ); + + DateTime expected = new(ticks, kind); + arg = new CommandArg(expected.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + if (kind == DateTimeKind.Utc) + { + Assert.AreEqual(expected.ToLocalTime(), value); + } + else + { + if (!expected.Equals(value)) + { + Assert.IsTrue( + Math.Abs(expected.Hour - value.Hour) <= 1, + $"Failed to parse - expected: {expected}, parsed: {value}" + ); + } + else + { + Assert.AreEqual( + expected, + value, + $"Failed to parse - expected is using {expected.Kind}, parsed is using {value.Kind}" + ); + } + } + } + + DateTime now = System.DateTime.Now; + arg = new CommandArg(now.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(now, value); + + DateTime utcNow = System.DateTime.UtcNow; + arg = new CommandArg(utcNow.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(utcNow.ToLocalTime(), value); + + /* + Don't validate these, as they're mutable and might change between time of + generation and time of check, all we need to know is if they're parsable + */ + arg = new CommandArg(nameof(System.DateTime.Now)); + Assert.IsTrue(arg.TryGet(out value)); + arg = new CommandArg(nameof(System.DateTime.UtcNow)); + Assert.IsTrue(arg.TryGet(out value)); + arg = new CommandArg(nameof(System.DateTime.Today)); + Assert.IsTrue(arg.TryGet(out value)); + + arg = new CommandArg(nameof(System.DateTime.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTime.MaxValue, value); + + arg = new CommandArg(System.DateTime.MaxValue.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTime.MaxValue, value); + + arg = new CommandArg(nameof(System.DateTime.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTime.MinValue, value); + + arg = new CommandArg(System.DateTime.MinValue.ToString("O")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.DateTime.MinValue, value); + + arg = new CommandArg("00"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Guid() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Guid value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.Empty.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.Guid.Empty, value); + + for (int i = 0; i < NumTries; ++i) + { + Guid expected = System.Guid.NewGuid(); + arg = new CommandArg( + _random.Next() % 2 == 0 + ? expected.ToString().ToLower() + : expected.ToString().ToUpper() + ); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg(nameof(System.Guid.Empty)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(System.Guid.Empty, value); + + arg = new CommandArg("00"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Char() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out char value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual('0', value); + + for (int i = 0; i < NumTries; ++i) + { + char expected = (char)_random.Next(char.MinValue, char.MaxValue); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue( + arg.TryGet(out value), + $"Failed to parse {expected} as char. Cleaned arg: '{arg.CleanedContents}'" + ); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg("00"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("z"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents[0], value); + + arg = new CommandArg(nameof(char.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(char.MaxValue, value); + + arg = new CommandArg(char.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(char.MaxValue, value); + + arg = new CommandArg(nameof(char.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(char.MinValue, value); + + arg = new CommandArg(char.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(char.MinValue, value); + + const long tooBig = char.MaxValue + 1L; + arg = new CommandArg(tooBig.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + const long tooSmall = char.MinValue - 1L; + arg = new CommandArg(tooSmall.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Sbyte() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out sbyte value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0, value); + + for (int i = 0; i < NumTries; ++i) + { + sbyte expected = (sbyte)_random.Next(sbyte.MinValue, sbyte.MaxValue); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg(nameof(sbyte.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(sbyte.MaxValue, value); + + arg = new CommandArg(sbyte.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(sbyte.MaxValue, value); + + arg = new CommandArg(nameof(sbyte.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(sbyte.MinValue, value); + + arg = new CommandArg(sbyte.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(sbyte.MinValue, value); + + long tooBig = sbyte.MaxValue + 1L; + arg = new CommandArg(tooBig.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + long tooSmall = sbyte.MinValue - 1L; + arg = new CommandArg(tooSmall.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Byte() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out byte value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0, value); + + for (int i = 0; i < NumTries; ++i) + { + byte expected = (byte)_random.Next(byte.MinValue, byte.MaxValue); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg(nameof(byte.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(byte.MaxValue, value); + + arg = new CommandArg(byte.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(byte.MaxValue, value); + + arg = new CommandArg(nameof(byte.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(byte.MinValue, value); + + arg = new CommandArg(byte.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(byte.MinValue, value); + + const long tooBig = byte.MaxValue + 1L; + arg = new CommandArg(tooBig.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + const long tooSmall = byte.MinValue - 1L; + arg = new CommandArg(tooSmall.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Short() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out short value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0, value); + + for (int i = 0; i < NumTries; ++i) + { + short expected = (short)_random.Next(short.MinValue, short.MaxValue); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg(nameof(short.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(short.MaxValue, value); + + arg = new CommandArg(short.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(short.MaxValue, value); + + arg = new CommandArg(nameof(short.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(short.MinValue, value); + + arg = new CommandArg(short.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(short.MinValue, value); + + const long tooBig = short.MaxValue + 1L; + arg = new CommandArg(tooBig.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + const long tooSmall = short.MinValue - 1L; + arg = new CommandArg(tooSmall.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Int() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out int value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0, value); + + for (int i = 0; i < NumTries; ++i) + { + int expected = _random.Next(int.MinValue, int.MaxValue); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg(nameof(int.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(int.MaxValue, value); + + arg = new CommandArg(int.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(int.MaxValue, value); + + arg = new CommandArg(nameof(int.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(int.MinValue, value); + + arg = new CommandArg(int.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(int.MinValue, value); + + const long tooBig = int.MaxValue + 1L; + arg = new CommandArg(tooBig.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + const long tooSmall = int.MinValue - 1L; + arg = new CommandArg(tooSmall.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Long() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out long value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0L, value); + + byte[] bytes = new byte[sizeof(long)]; + for (int i = 0; i < NumTries; ++i) + { + _random.NextBytes(bytes); + long expected = BitConverter.ToInt64(bytes, 0); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg(nameof(long.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(long.MaxValue, value); + + arg = new CommandArg(long.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(long.MaxValue, value); + + arg = new CommandArg(nameof(long.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(long.MinValue, value); + + arg = new CommandArg(long.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(long.MinValue, value); + + arg = new CommandArg(long.MinValue + "0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(long.MinValue + "0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Ulong() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out ulong value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0UL, value); + + byte[] bytes = new byte[sizeof(ulong)]; + for (int i = 0; i < NumTries; ++i) + { + _random.NextBytes(bytes); + ulong expected = BitConverter.ToUInt64(bytes, 0); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg(nameof(ulong.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ulong.MaxValue, value); + + arg = new CommandArg(ulong.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ulong.MaxValue, value); + + arg = new CommandArg(nameof(ulong.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ulong.MinValue, value); + + arg = new CommandArg(ulong.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ulong.MinValue, value); + + arg = new CommandArg("-1"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(ulong.MaxValue + "0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Enum() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out TestEnum1 value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual((TestEnum1)0, value); + + TestEnum1[] testEnum1Values = System + .Enum.GetValues(typeof(TestEnum1)) + .OfType() + .ToArray(); + for (int i = 0; i < NumTries; ++i) + { + int index = _random.Next(testEnum1Values.Length); + TestEnum1 expected = testEnum1Values[index]; + arg = new CommandArg(expected.ToString("G")); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + + arg = new CommandArg("Value6"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + Assert.IsTrue(arg.TryGet(out TestEnum2 testEnum2Value)); + Assert.AreEqual(TestEnum2.Value6, testEnum2Value); + + TestEnum2[] testEnum2Values = System + .Enum.GetValues(typeof(TestEnum2)) + .OfType() + .ToArray(); + for (int i = 0; i < NumTries; ++i) + { + int index = _random.Next(testEnum2Values.Length); + TestEnum2 expected = testEnum2Values[index]; + arg = new CommandArg(expected.ToString("G")); + Assert.IsTrue(arg.TryGet(out TestEnum2 value2)); + Assert.AreEqual(expected, value2); + } + + arg = new CommandArg("1"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual((TestEnum1)1, value); + Assert.IsTrue(arg.TryGet(out testEnum2Value)); + Assert.AreEqual((TestEnum2)1, testEnum2Value); + + arg = new CommandArg("100"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("-1"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Vector2Int() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Vector2Int value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Vector2Int expected; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(short.MinValue, short.MaxValue); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + // x,y + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(int.MinValue, int.MaxValue); + int y = _random.Next(int.MinValue, int.MaxValue); + expected = new Vector2Int(x, y); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg($"{expected.x},{expected.y}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + } + + // x,y, z + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(int.MinValue, int.MaxValue); + int y = _random.Next(int.MinValue, int.MaxValue); + int z = _random.Next(int.MinValue, int.MaxValue); + expected = new Vector2Int(x, y); + + arg = new CommandArg($"{expected.x},{expected.y},{z}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + } + + arg = new CommandArg(nameof(UnityEngine.Vector2Int.zero)); + expected = UnityEngine.Vector2Int.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector2Int.zero.ToString()); + expected = UnityEngine.Vector2Int.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(nameof(UnityEngine.Vector2Int.up)); + expected = UnityEngine.Vector2Int.up; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector2Int.up.ToString()); + expected = UnityEngine.Vector2Int.up; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(nameof(UnityEngine.Vector2Int.left)); + expected = UnityEngine.Vector2Int.left; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector2Int.left.ToString()); + expected = UnityEngine.Vector2Int.left; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Vector3Int() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Vector3Int value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Vector3Int expected; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(short.MinValue, short.MaxValue); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + // x,y + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(int.MinValue, int.MaxValue); + int y = _random.Next(int.MinValue, int.MaxValue); + expected = new Vector3Int(x, y); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg($"{expected.x},{expected.y}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + } + + // x,y,z + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(int.MinValue, int.MaxValue); + int y = _random.Next(int.MinValue, int.MaxValue); + int z = _random.Next(int.MinValue, int.MaxValue); + expected = new Vector3Int(x, y, z); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg($"{expected.x},{expected.y},{expected.z}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y},{expected.z}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + } + + arg = new CommandArg(nameof(UnityEngine.Vector3Int.zero)); + expected = UnityEngine.Vector3Int.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector3Int.zero.ToString()); + expected = UnityEngine.Vector3Int.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(nameof(UnityEngine.Vector3Int.up)); + expected = UnityEngine.Vector3Int.up; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector3Int.up.ToString()); + expected = UnityEngine.Vector3Int.up; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(nameof(UnityEngine.Vector3Int.left)); + expected = UnityEngine.Vector3Int.left; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector3Int.left.ToString()); + expected = UnityEngine.Vector3Int.left; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(nameof(UnityEngine.Vector3Int.forward)); + expected = UnityEngine.Vector3Int.forward; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector3Int.forward.ToString()); + expected = UnityEngine.Vector3Int.forward; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(nameof(UnityEngine.Vector3Int.one)); + expected = UnityEngine.Vector3Int.one; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Vector3Int.one.ToString()); + expected = UnityEngine.Vector3Int.one; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Rect() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Rect value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Rect expected; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(short.MinValue, short.MaxValue); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + const float rectTolerance = 0.01f; + + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float width = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float height = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + expected = new Rect(x, y, width, height); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, rectTolerance) + && Approximately(expected.y, value.y, rectTolerance) + && Approximately(expected.width, value.width, rectTolerance) + && Approximately(expected.height, value.height, rectTolerance) + ); + + arg = new CommandArg( + $"{expected.x},{expected.y},{expected.width},{expected.height}" + ); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, rectTolerance) + && Approximately(expected.y, value.y, rectTolerance) + && Approximately(expected.width, value.width, rectTolerance) + && Approximately(expected.height, value.height, rectTolerance) + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg( + $"{pre}{expected.x},{expected.y},{expected.width},{expected.height}{post}" + ); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, rectTolerance) + && Approximately(expected.y, value.y, rectTolerance) + && Approximately(expected.width, value.width, rectTolerance) + && Approximately(expected.height, value.height, rectTolerance) + ); + } + } + + arg = new CommandArg(nameof(UnityEngine.Rect.zero)); + expected = UnityEngine.Rect.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + arg = new CommandArg(UnityEngine.Rect.zero.ToString()); + expected = UnityEngine.Rect.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void RectInt() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out RectInt value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + RectInt expected; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(short.MinValue, short.MaxValue); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + for (int i = 0; i < NumTries; ++i) + { + int x = _random.Next(int.MinValue, int.MaxValue); + int y = _random.Next(int.MinValue, int.MaxValue); + int width = _random.Next(int.MinValue, int.MaxValue); + int height = _random.Next(int.MinValue, int.MaxValue); + expected = new RectInt(x, y, width, height); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg( + $"{expected.x},{expected.y},{expected.width},{expected.height}" + ); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg( + $"{pre}{expected.x},{expected.y},{expected.width},{expected.height}{post}" + ); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + } + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Vector2() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Vector2 value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Vector2 expected; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + const float vector2RoundTolerance = 0.01f; + + // x,y + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + expected = new Vector2(x, y); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, vector2RoundTolerance) + && Approximately(expected.y, value.y, vector2RoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y}). " + + $"Value: ({value.x},{value.y}). " + + $"Expected: ({expected.x},{expected.y})." + ); + + arg = new CommandArg($"{expected.x},{expected.y}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) && Approximately(expected.y, value.y), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) && Approximately(expected.y, value.y), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." + ); + } + } + + // x,y,z (z is ok, but ignored) + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float z = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + expected = new Vector2(x, y); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, vector2RoundTolerance) + && Approximately(expected.y, value.y, vector2RoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y},{z}). " + + $"Value: ({value.x},{value.y}). " + + $"Expected: ({expected.x},{expected.y})." + ); + + arg = new CommandArg($"{expected.x},{expected.y},{z}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) && Approximately(expected.y, value.y), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) && Approximately(expected.y, value.y), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." + ); + } + } + + arg = new CommandArg(nameof(UnityEngine.Vector2.zero)); + expected = UnityEngine.Vector2.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) && Approximately(expected.y, value.y), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector2.up)); + expected = UnityEngine.Vector2.up; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) && Approximately(expected.y, value.y), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector2.left)); + expected = UnityEngine.Vector2.left; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) && Approximately(expected.y, value.y), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Vector3() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Vector3 value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Vector3 expected; + + const float vector3RoundTolerance = 0.01f; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + // x,y + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float z = 0f; + expected = new Vector3(x, y); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, vector3RoundTolerance) + && Approximately(expected.y, value.y, vector3RoundTolerance) + && Approximately(expected.z, value.z, vector3RoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y},{z}). " + + $"Value: ({value.x},{value.y},{value.z}). " + + $"Expected: ({expected.x},{expected.y},{expected.z})." + ); + + arg = new CommandArg($"{expected.x},{expected.y}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z}). Expected: ({x},{y},{z})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y}). Expected: ({x},{y})." + ); + } + } + + // x,y,z (z is ok, but ignored) + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float z = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + expected = new Vector3(x, y, z); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, vector3RoundTolerance) + && Approximately(expected.y, value.y, vector3RoundTolerance) + && Approximately(expected.z, value.z, vector3RoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y},{z}). " + + $"Value: ({value.x},{value.y},{value.z}). " + + $"Expected: ({expected.x},{expected.y},{expected.z})." + ); + + arg = new CommandArg($"{expected.x},{expected.y},{expected.z}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z}). Expected: ({x},{y},{z})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z}). Expected: ({x},{y},{z})." + ); + } + } + + arg = new CommandArg(nameof(UnityEngine.Vector3.zero)); + expected = UnityEngine.Vector3.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector3.up)); + expected = UnityEngine.Vector3.up; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector3.left)); + expected = UnityEngine.Vector3.left; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector3.back)); + expected = UnityEngine.Vector3.back; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector3.forward)); + expected = UnityEngine.Vector3.forward; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector3.one)); + expected = UnityEngine.Vector3.one; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Vector4() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Vector4 value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Vector4 expected; + + const float vector4RoundTolerance = 0.01f; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + // x,y + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float z = 0f; + float w = 0f; + expected = new Vector4(x, y); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, vector4RoundTolerance) + && Approximately(expected.y, value.y, vector4RoundTolerance) + && Approximately(expected.z, value.z, vector4RoundTolerance) + && Approximately(expected.w, value.w, vector4RoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y},{z},{w}). " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). " + + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." + ); + + arg = new CommandArg($"{expected.x},{expected.y}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." + ); + } + } + + // x,y,z + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float z = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float w = 0f; + expected = new Vector4(x, y, z); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, vector4RoundTolerance) + && Approximately(expected.y, value.y, vector4RoundTolerance) + && Approximately(expected.z, value.z, vector4RoundTolerance) + && Approximately(expected.w, value.w, vector4RoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y},{z},{w}). " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). " + + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." + ); + + arg = new CommandArg($"{expected.x},{expected.y},{expected.z}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{expected.x},{expected.y},{z}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." + ); + } + } + + // x,y,z,w + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float z = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float w = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + expected = new Vector4(x, y, z, w); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, vector4RoundTolerance) + && Approximately(expected.y, value.y, vector4RoundTolerance) + && Approximately(expected.z, value.z, vector4RoundTolerance) + && Approximately(expected.w, value.w, vector4RoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y},{z},{w}). " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). " + + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." + ); + + arg = new CommandArg($"{expected.x},{expected.y},{expected.z},{expected.w}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg( + $"{pre}{expected.x},{expected.y},{expected.z},{expected.w}{post}" + ); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). Expected: ({x},{y},{z},{w})." + ); + } + } + + arg = new CommandArg(nameof(UnityEngine.Vector4.zero)); + expected = UnityEngine.Vector4.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}." + ); + arg = new CommandArg(UnityEngine.Vector4.zero.ToString()); + expected = UnityEngine.Vector4.zero; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector4.negativeInfinity)); + expected = UnityEngine.Vector4.negativeInfinity; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}." + ); + arg = new CommandArg(UnityEngine.Vector4.negativeInfinity.ToString()); + expected = UnityEngine.Vector4.negativeInfinity; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(nameof(UnityEngine.Vector4.positiveInfinity)); + expected = UnityEngine.Vector4.positiveInfinity; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}." + ); + arg = new CommandArg(UnityEngine.Vector4.positiveInfinity.ToString()); + expected = UnityEngine.Vector4.positiveInfinity; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}." + ); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Uint() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out uint value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(0U, value); + + arg = new CommandArg("-1"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("1.3"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + unchecked + { + for (int i = 0; i < NumTries; ++i) + { + uint expected = (uint)_random.Next(int.MinValue, int.MaxValue); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + } + + const long tooBig = uint.MaxValue + 1L; + arg = new CommandArg(tooBig.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(nameof(uint.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(uint.MaxValue, value); + + arg = new CommandArg(nameof(uint.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(uint.MinValue, value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Ushort() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out ushort value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("0"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual((ushort)0, value); + + arg = new CommandArg("-1"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg("1.3"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + unchecked + { + for (int i = 0; i < NumTries; ++i) + { + ushort expected = (ushort)_random.Next(short.MinValue, short.MaxValue); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + } + } + + const int tooBig = ushort.MaxValue + 1; + arg = new CommandArg(tooBig.ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + arg = new CommandArg(nameof(ushort.MaxValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ushort.MaxValue, value); + + arg = new CommandArg(ushort.MaxValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ushort.MaxValue, value); + + arg = new CommandArg(nameof(ushort.MinValue)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ushort.MinValue, value); + + arg = new CommandArg(ushort.MinValue.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(ushort.MinValue, value); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void String() + { + string expected = string.Empty; + CommandArg arg = new(expected); + Assert.IsTrue(arg.TryGet(out string value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + + expected = "asdf"; + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + + expected = "1111"; + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + + expected = "1.3333"; + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + + for (int i = 0; i < NumTries; ++i) + { + expected = System.Guid.NewGuid().ToString(); + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + } + + expected = "#$$$__.azxfd87&*_&&&-={'|"; + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + + // Check strings aren't sanitized + expected = " "; + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + + // Make sure string.Empty isn't resolved to "" + expected = "Empty"; + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + + expected = "string.Empty"; + arg = new CommandArg(expected); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(arg.contents, value); + Assert.AreEqual(expected, value); + } + + [Test] + public void Quaternion() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Quaternion value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Quaternion expected; + + // Unexpected input + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + arg = new CommandArg(x.ToString(CultureInfo.InvariantCulture)); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x}{post}"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + } + + const float quaternionRoundTolerance = 0.00001f; + + // x,y,z, w (z is ok, but ignored) + for (int i = 0; i < NumTries; ++i) + { + float x = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float y = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float z = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + float w = (float)( + _random.NextDouble() * _random.Next(short.MinValue, short.MaxValue) + ); + expected = new Quaternion(x, y, z, w); + arg = new CommandArg(expected.ToString()); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x, quaternionRoundTolerance) + && Approximately(expected.y, value.y, quaternionRoundTolerance) + && Approximately(expected.z, value.z, quaternionRoundTolerance) + && Approximately(expected.w, value.w, quaternionRoundTolerance), + $"Expected {value} to be approximately {expected}. " + + $"Input: ({x},{y},{z},{w}). " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). " + + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." + ); + + arg = new CommandArg($"{x},{y},{z},{w}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). " + + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{x},{y},{z},{w}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). " + + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." + ); + } + } + + arg = new CommandArg(nameof(UnityEngine.Quaternion.identity)); + expected = UnityEngine.Quaternion.identity; + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.x, value.x) + && Approximately(expected.y, value.y) + && Approximately(expected.z, value.z) + && Approximately(expected.w, value.w), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({value.x},{value.y},{value.z},{value.w}). " + + $"Expected: ({expected.x},{expected.y},{expected.z},{expected.w})." + ); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Color() + { + CommandArg arg = new(""); + Assert.IsFalse(arg.TryGet(out Color value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("0"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + Color expected = UnityEngine.Color.white; + arg = new CommandArg(nameof(UnityEngine.Color.white)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + expected = UnityEngine.Color.red; + arg = new CommandArg(nameof(UnityEngine.Color.red)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + expected = UnityEngine.Color.cyan; + arg = new CommandArg(nameof(UnityEngine.Color.cyan)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + expected = UnityEngine.Color.black; + arg = new CommandArg(nameof(UnityEngine.Color.black)); + Assert.IsTrue(arg.TryGet(out value)); + Assert.AreEqual(expected, value); + + arg = new CommandArg("(1.0, 0.5)"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("(0.7)"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + + for (int i = 0; i < NumTries; ++i) + { + float r = (float)_random.NextDouble(); + float g = (float)_random.NextDouble(); + float b = (float)_random.NextDouble(); + expected = new Color(r, g, b); + arg = new CommandArg(expected.ToString()); + + // Colors have a floating point precision of 3 decimal places, otherwise our equality checks will be off + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.r, value.r, 0.001f) + && Approximately(expected.g, value.g, 0.001f) + && Approximately(expected.b, value.b, 0.001f) + && Approximately(expected.a, value.a, 0.001f), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({r},{g},{b},{expected.a}). " + + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." + ); + + arg = new CommandArg($"{r},{g},{b}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.r, value.r, 0.001f) + && Approximately(expected.g, value.g, 0.001f) + && Approximately(expected.b, value.b, 0.001f) + && Approximately(expected.a, value.a, 0.001f), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({r},{g},{b},{expected.a}). " + + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{r},{g},{b}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.r, value.r, 0.001f) + && Approximately(expected.g, value.g, 0.001f) + && Approximately(expected.b, value.b, 0.001f) + && Approximately(expected.a, value.a, 0.001f), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({r},{g},{b},{expected.a}). " + + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." + ); + } + } + + for (int i = 0; i < NumTries; ++i) + { + float r = (float)_random.NextDouble(); + float g = (float)_random.NextDouble(); + float b = (float)_random.NextDouble(); + float a = (float)_random.NextDouble(); + expected = new Color(r, g, b, a); + arg = new CommandArg(expected.ToString()); + + // Colors have a floating point precision of 3 decimal places, otherwise our equality checks will be off + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.r, value.r, 0.001f) + && Approximately(expected.g, value.g, 0.001f) + && Approximately(expected.b, value.b, 0.001f) + && Approximately(expected.a, value.a, 0.001f), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({r},{g},{b},{a}). " + + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." + ); + + arg = new CommandArg($"{r},{g},{b},{a}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.r, value.r, 0.001f) + && Approximately(expected.g, value.g, 0.001f) + && Approximately(expected.b, value.b, 0.001f) + && Approximately(expected.a, value.a, 0.001f), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({r},{g},{b},{a}). " + + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." + ); + + foreach ( + (string pre, string post) in _prepend.Zip( + _append, + (preValue, postValue) => (preValue, postValue) + ) + ) + { + arg = new CommandArg($"{pre}{r},{g},{b},{a}{post}"); + Assert.IsTrue(arg.TryGet(out value)); + Assert.IsTrue( + Approximately(expected.r, value.r, 0.001f) + && Approximately(expected.g, value.g, 0.001f) + && Approximately(expected.b, value.b, 0.001f) + && Approximately(expected.a, value.a, 0.001f), + $"Expected {value} to be approximately {expected}. " + + $"Value: ({r},{g},{b},{a}). " + + $"Expected: ({expected.r},{expected.g},{expected.b},{expected.a})." + ); + } + } + + arg = new CommandArg("invisible"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("false"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(out value), $"Unexpectedly parsed {value}"); + } + + [Test] + public void Untyped() + { + CommandArg arg = new("1"); + Assert.IsTrue(arg.TryGet(typeof(int), out object value)); + Assert.AreEqual(1, value); + + arg = new CommandArg("2.5"); + Assert.IsTrue(arg.TryGet(typeof(float), out value)); + Assert.IsTrue( + Approximately((float)value, 2.5f), + $"Expected {value} to be approximately {2.5f}" + ); + + arg = new CommandArg("red"); + Assert.IsTrue(arg.TryGet(typeof(Color), out value)); + Assert.AreEqual(UnityEngine.Color.red, (Color)value); + + arg = new CommandArg("invisible"); + Assert.IsFalse(arg.TryGet(typeof(Color), out value)); + + arg = new CommandArg("(1.2564, 3.6)"); + Assert.IsTrue(arg.TryGet(typeof(Vector2), out value)); + Vector2 expected = (Vector2)value; + Assert.IsTrue( + Approximately(expected.x, 1.2564f) && Approximately(expected.y, 3.6f), + $"Expected {expected} to be approximately {arg.contents}" + ); + + arg = new CommandArg("asdf"); + Assert.IsFalse(arg.TryGet(typeof(float), out value)); + Assert.IsFalse(arg.TryGet(typeof(int), out value)); + Assert.IsTrue(arg.TryGet(typeof(string), out value)); + Assert.AreEqual(arg.contents, value); + } + + [Test] + public void CustomParserBuiltInType() + { + for (int i = 0; i < NumTries; ++i) + { + string expectedString = System.Guid.NewGuid().ToString(); + int expected = _random.Next(int.MinValue, int.MaxValue); + CommandArg arg = new(expectedString); + Assert.IsFalse(arg.TryGet(out int value)); + Assert.IsTrue(arg.TryGet(out value, CustomParser)); + Assert.AreEqual(expected, value); + + // Make sure the parser isn't sticky + Assert.IsFalse(arg.TryGet(out value)); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value, CustomParser)); + continue; + + bool CustomParser(string input, out int parsed) + { + if (string.Equals(expectedString, input, StringComparison.OrdinalIgnoreCase)) + { + parsed = expected; + return true; + } + + parsed = 0; + return false; + } + } + } + + [Test] + public void CustomParserCustomType() + { + for (int i = 0; i < NumTries; ++i) + { + string expectedString = System.Guid.NewGuid().ToString(); + TestStruct1 expected = new(System.Guid.NewGuid()); + CommandArg arg = new(expectedString); + Assert.IsFalse(arg.TryGet(out TestStruct1 value)); + Assert.IsTrue(arg.TryGet(out value, CustomParser)); + Assert.AreEqual(expected, value); + + // Make sure the parser isn't sticky + Assert.IsFalse(arg.TryGet(out value)); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + Assert.IsFalse(arg.TryGet(out value, CustomParser)); + + continue; + + bool CustomParser(string input, out TestStruct1 parsed) + { + if (string.Equals(expectedString, input, StringComparison.OrdinalIgnoreCase)) + { + parsed = expected; + return true; + } + + parsed = default; + return false; + } + } + } + + [Test] + public void RegisteredParsersAreUsed() + { + const int constParsed = -23; + bool registered = CommandArg.RegisterParser(CustomIntParser); + Assert.IsTrue(registered); + + CommandArg arg = new("Garbage"); + RunRegisteredParsingLogic(); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + RunRegisteredParsingLogic(); + + arg = new CommandArg(""); + RunRegisteredParsingLogic(); + + arg = new CommandArg("x_YZZZZ$$$"); + RunRegisteredParsingLogic(); + + bool unregistered = CommandArg.UnregisterParser(); + Assert.IsTrue(unregistered); + + arg = new CommandArg("Garbage"); + RunUnregisteredParsingLogic(); + + arg = new CommandArg(System.Guid.NewGuid().ToString()); + RunUnregisteredParsingLogic(); + + arg = new CommandArg(""); + RunUnregisteredParsingLogic(); + + arg = new CommandArg("x_YZZZZ$$$"); + RunUnregisteredParsingLogic(); + + return; + + void RunRegisteredParsingLogic() + { + Assert.IsTrue(arg.TryGet(typeof(int), out object value)); + Assert.AreEqual(constParsed, value); + Assert.IsTrue(arg.TryGet(out int parsed)); + Assert.AreEqual(constParsed, parsed); + Assert.IsFalse(arg.TryGet(out float _)); + } + + void RunUnregisteredParsingLogic() + { + Assert.IsFalse(arg.TryGet(typeof(int), out object _)); + Assert.IsFalse(arg.TryGet(out int _)); + Assert.IsFalse(arg.TryGet(out float _)); + } + + static bool CustomIntParser(string input, out int parsed) + { + parsed = constParsed; + return true; + } + } + + [Test] + public void ParserRegistration() + { + bool registered = CommandArg.RegisterParser(CustomIntParser1); + Assert.IsTrue(registered); + Assert.IsTrue(CommandArg.TryGetParser(out CommandArgParser registeredParser)); + Assert.AreEqual((CommandArgParser)CustomIntParser1, registeredParser); + + registered = CommandArg.RegisterParser(CustomIntParser1); + Assert.IsFalse(registered); + Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); + Assert.AreEqual((CommandArgParser)CustomIntParser1, registeredParser); + + registered = CommandArg.RegisterParser(CustomIntParser2); + Assert.IsFalse(registered); + Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); + Assert.AreEqual((CommandArgParser)CustomIntParser1, registeredParser); + + registered = CommandArg.RegisterParser(CustomIntParser2, force: true); + Assert.IsTrue(registered); + + Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); + Assert.AreEqual((CommandArgParser)CustomIntParser2, registeredParser); + + return; + + static bool CustomIntParser1(string input, out int parsed) + { + parsed = 1; + return false; + } + + static bool CustomIntParser2(string input, out int parsed) + { + parsed = 2; + return false; + } + } + + [Test] + public void NullParserRegistration() + { + bool registered = CommandArg.RegisterParser(null); + Assert.IsFalse(registered); + registered = CommandArg.RegisterParser(null, force: true); + Assert.IsFalse(registered); + } + + [Test] + public void ParserDeregistration() + { + Assert.IsFalse(CommandArg.TryGetParser(out CommandArgParser registeredParser)); + + bool deregistered = CommandArg.UnregisterParser(); + Assert.IsFalse(deregistered); + + bool registered = CommandArg.RegisterParser(CustomIntParser1); + Assert.IsTrue(registered); + Assert.IsTrue(CommandArg.TryGetParser(out registeredParser)); + Assert.IsNotNull(registeredParser); + + deregistered = CommandArg.UnregisterParser(); + Assert.IsTrue(deregistered); + Assert.IsFalse(CommandArg.TryGetParser(out registeredParser)); + + deregistered = CommandArg.UnregisterParser(); + Assert.IsFalse(deregistered); + Assert.IsFalse(CommandArg.TryGetParser(out registeredParser)); + + return; + + static bool CustomIntParser1(string input, out int parsed) + { + parsed = 1; + return false; + } + } + + private static bool Approximately(float a, float b, float tolerance = 0.0001f) + { + float delta = Math.Abs(a - b); + // Check ToString representations too, the numbers may be crazy small or crazy big, outside the scope of our tolerance + return delta <= tolerance + || a.ToString(CultureInfo.InvariantCulture) + .Equals(b.ToString(CultureInfo.InvariantCulture)); + } + + private static bool Approximately(decimal a, decimal b, decimal? tolerance = null) + { + if (a == b) + { + return true; + } + + tolerance ??= new decimal(0.0001); + + decimal delta = Math.Abs(a - b); + return delta <= tolerance + || a.ToString(CultureInfo.InvariantCulture) + .Equals(b.ToString(CultureInfo.InvariantCulture)); + } + + private static bool Approximately(double a, double b, double tolerance = 0.0001) + { + double delta = Math.Abs(a - b); + // Check ToString representations too, the numbers may be crazy small or crazy big, outside the scope of our tolerance + return delta <= tolerance + || a.ToString(CultureInfo.InvariantCulture) + .Equals(b.ToString(CultureInfo.InvariantCulture)); + } + } +} diff --git a/Tests/Runtime/CommandShellTests.cs b/Tests/Runtime/CommandShellTests.cs index 1cba45a..2ccc02b 100644 --- a/Tests/Runtime/CommandShellTests.cs +++ b/Tests/Runtime/CommandShellTests.cs @@ -1,353 +1,353 @@ -namespace WallstopStudios.DxCommandTerminal.Tests.Runtime -{ - using System; - using System.Collections; - using System.Linq; - using System.Text; - using Backend; - using Components; - using NUnit.Framework; - using UI; - using UnityEngine; - using UnityEngine.TestTools; - using Application = UnityEngine.Device.Application; - - public sealed class CommandShellTests - { - [TearDown] - public void TearDown() - { - if (TerminalUI.Instance != null) - { - UnityEngine.Object.Destroy(TerminalUI.Instance.gameObject); - } - } - - [UnityTest] - public IEnumerator UnescapedQuotes() - { - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - - int logCount = 0; - Exception exception = null; - Action assertion = null; - - Application.logMessageReceived += HandleMessageReceived; - try - { - CommandShell shell = Terminal.Shell; - Assert.IsNotNull(shell); - CommandHistory history = Terminal.History; - Assert.IsNotNull(history); - - int expectedLogCount = 0; - assertion = message => Assert.AreEqual(string.Empty, message); - string command = "log ' "; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - string[] logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - string expected = "' abd \" "; - assertion = message => Assert.AreEqual(expected.Substring(1), message); - command = "log " + expected; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log " + expected), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - } - finally - { - Application.logMessageReceived -= HandleMessageReceived; - } - - yield break; - - void HandleMessageReceived(string message, string stackTrace, LogType type) - { - ++logCount; - try - { - assertion?.Invoke(message); - } - catch (Exception e) - { - exception = e; - throw; - } - } - } - - [UnityTest] - public IEnumerator RunCommandLineNominal() - { - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - - int logCount = 0; - Exception exception = null; - Action assertion = null; - - Application.logMessageReceived += HandleMessageReceived; - try - { - CommandShell shell = Terminal.Shell; - Assert.IsNotNull(shell); - CommandHistory history = Terminal.History; - Assert.IsNotNull(history); - - int expectedLogCount = 0; - assertion = message => Assert.AreEqual(string.Empty, message); - string command = "log"; - shell.RunCommand(command); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - Assert.AreEqual(++expectedLogCount, logCount); - string[] logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - assertion = message => Assert.AreEqual("test", message); - command = "log test"; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log test"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - assertion = message => Assert.AreEqual("quoted argument", message); - command = "log \"quoted argument\""; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log test"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log \"quoted argument\""), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - assertion = message => Assert.AreEqual("multi argument", message); - command = "log multi argument"; - shell.RunCommand("log multi argument"); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log test"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log \"quoted argument\""), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log multi argument"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - assertion = message => Assert.AreEqual("a a a a a a aaaa aa aaa d", message); - command = "log a a a a a a aaaa aa aaa d"; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log test"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log \"quoted argument\""), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains("log a a a a a a aaaa aa aaa d"), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - char[] quotes = CommandArg.Quotes.ToArray(); - CommandArg.Quotes.Clear(); - CommandArg.Quotes.Add('"'); - CommandArg.Quotes.Add('\''); - try - { - int simpleCommandCount = 1; - foreach (char quote in CommandArg.Quotes) - { - string expected = - $"{quote} {quote} {quote} aa bbb ccc {quote} {quote}{Environment.NewLine}abd {Environment.NewLine} \r \t \n__{quote}"; - foreach (char otherQuote in CommandArg.Quotes.Except(new[] { quote })) - { - expected += $" {quote} {otherQuote}ab {quote}"; - } - - expected += $" {quote}{quote} final string"; - assertion = message => - Assert.AreEqual( - expected.Replace(quote.ToString(), string.Empty), - message - ); - command = "log " + expected; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains(command), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - expected = $"{quote}{quote}"; - assertion = message => - Assert.AreEqual( - expected.Replace(quote.ToString(), string.Empty), - message - ); - command = "log " + expected; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.IsTrue( - logs.Contains(command), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - - expected = $"{quote}"; - assertion = message => Assert.AreEqual(string.Empty, message); - command = "log " + expected; - shell.RunCommand(command); - Assert.AreEqual(++expectedLogCount, logCount); - Assert.IsNull(exception, $"Error running {command}: {exception}"); - logs = history.GetHistory(true, true).ToArray(); - Assert.AreEqual( - expectedLogCount, - logs.Length, - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - Assert.AreEqual( - ++simpleCommandCount, - logs.Count(message => string.Equals(message, "log")), - $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" - ); - } - } - finally - { - CommandArg.Quotes.Clear(); - CommandArg.Quotes.AddRange(quotes); - } - - StringBuilder errorBuilder = new(); - bool anyError = false; - bool initialHadError = Terminal.Shell.HasErrors; - while (Terminal.Shell.TryConsumeErrorMessage(out string errorMessage)) - { - anyError = true; - errorBuilder.AppendLine(errorMessage); - } - - Assert.IsFalse(anyError, errorBuilder.ToString()); - Assert.IsFalse( - initialHadError, - "Shell reported errors, but was unable to consume error messages!" - ); - } - finally - { - Application.logMessageReceived -= HandleMessageReceived; - } - - yield break; - - void HandleMessageReceived(string message, string stackTrace, LogType type) - { - ++logCount; - try - { - assertion?.Invoke(message); - } - catch (Exception e) - { - exception = e; - throw; - } - } - } - } -} +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections; + using System.Linq; + using System.Text; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + using Application = UnityEngine.Device.Application; + + public sealed class CommandShellTests + { + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + UnityEngine.Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + [UnityTest] + public IEnumerator UnescapedQuotes() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + int logCount = 0; + Exception exception = null; + Action assertion = null; + + Application.logMessageReceived += HandleMessageReceived; + try + { + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + CommandHistory history = Terminal.History; + Assert.IsNotNull(history); + + int expectedLogCount = 0; + assertion = message => Assert.AreEqual(string.Empty, message); + string command = "log ' "; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + string[] logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + string expected = "' abd \" "; + assertion = message => Assert.AreEqual(expected.Substring(1), message); + command = "log " + expected; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log " + expected), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + } + finally + { + Application.logMessageReceived -= HandleMessageReceived; + } + + yield break; + + void HandleMessageReceived(string message, string stackTrace, LogType type) + { + ++logCount; + try + { + assertion?.Invoke(message); + } + catch (Exception e) + { + exception = e; + throw; + } + } + } + + [UnityTest] + public IEnumerator RunCommandLineNominal() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + int logCount = 0; + Exception exception = null; + Action assertion = null; + + Application.logMessageReceived += HandleMessageReceived; + try + { + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + CommandHistory history = Terminal.History; + Assert.IsNotNull(history); + + int expectedLogCount = 0; + assertion = message => Assert.AreEqual(string.Empty, message); + string command = "log"; + shell.RunCommand(command); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + Assert.AreEqual(++expectedLogCount, logCount); + string[] logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + assertion = message => Assert.AreEqual("test", message); + command = "log test"; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log test"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + assertion = message => Assert.AreEqual("quoted argument", message); + command = "log \"quoted argument\""; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log test"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log \"quoted argument\""), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + assertion = message => Assert.AreEqual("multi argument", message); + command = "log multi argument"; + shell.RunCommand("log multi argument"); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log test"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log \"quoted argument\""), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log multi argument"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + assertion = message => Assert.AreEqual("a a a a a a aaaa aa aaa d", message); + command = "log a a a a a a aaaa aa aaa d"; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log test"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log \"quoted argument\""), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains("log a a a a a a aaaa aa aaa d"), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + char[] quotes = CommandArg.Quotes.ToArray(); + CommandArg.Quotes.Clear(); + CommandArg.Quotes.Add('"'); + CommandArg.Quotes.Add('\''); + try + { + int simpleCommandCount = 1; + foreach (char quote in CommandArg.Quotes) + { + string expected = + $"{quote} {quote} {quote} aa bbb ccc {quote} {quote}{Environment.NewLine}abd {Environment.NewLine} \r \t \n__{quote}"; + foreach (char otherQuote in CommandArg.Quotes.Except(new[] { quote })) + { + expected += $" {quote} {otherQuote}ab {quote}"; + } + + expected += $" {quote}{quote} final string"; + assertion = message => + Assert.AreEqual( + expected.Replace(quote.ToString(), string.Empty), + message + ); + command = "log " + expected; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains(command), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + expected = $"{quote}{quote}"; + assertion = message => + Assert.AreEqual( + expected.Replace(quote.ToString(), string.Empty), + message + ); + command = "log " + expected; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.IsTrue( + logs.Contains(command), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + + expected = $"{quote}"; + assertion = message => Assert.AreEqual(string.Empty, message); + command = "log " + expected; + shell.RunCommand(command); + Assert.AreEqual(++expectedLogCount, logCount); + Assert.IsNull(exception, $"Error running {command}: {exception}"); + logs = history.GetHistory(true, true).ToArray(); + Assert.AreEqual( + expectedLogCount, + logs.Length, + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + Assert.AreEqual( + ++simpleCommandCount, + logs.Count(message => string.Equals(message, "log")), + $"Unexpected logs:{Environment.NewLine}{string.Join(Environment.NewLine, logs)}" + ); + } + } + finally + { + CommandArg.Quotes.Clear(); + CommandArg.Quotes.AddRange(quotes); + } + + StringBuilder errorBuilder = new(); + bool anyError = false; + bool initialHadError = Terminal.Shell.HasErrors; + while (Terminal.Shell.TryConsumeErrorMessage(out string errorMessage)) + { + anyError = true; + errorBuilder.AppendLine(errorMessage); + } + + Assert.IsFalse(anyError, errorBuilder.ToString()); + Assert.IsFalse( + initialHadError, + "Shell reported errors, but was unable to consume error messages!" + ); + } + finally + { + Application.logMessageReceived -= HandleMessageReceived; + } + + yield break; + + void HandleMessageReceived(string message, string stackTrace, LogType type) + { + ++logCount; + try + { + assertion?.Invoke(message); + } + catch (Exception e) + { + exception = e; + throw; + } + } + } + } +} diff --git a/Tests/Runtime/Components/StartTracker.cs b/Tests/Runtime/Components/StartTracker.cs index 96130d0..2e16d67 100644 --- a/Tests/Runtime/Components/StartTracker.cs +++ b/Tests/Runtime/Components/StartTracker.cs @@ -1,15 +1,15 @@ -namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components -{ - using UnityEngine; - - [DisallowMultipleComponent] - public sealed class StartTracker : MonoBehaviour - { - public bool Started { get; private set; } - - private void Start() - { - Started = true; - } - } -} +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components +{ + using UnityEngine; + + [DisallowMultipleComponent] + public sealed class StartTracker : MonoBehaviour + { + public bool Started { get; private set; } + + private void Start() + { + Started = true; + } + } +} diff --git a/Tests/Runtime/Components/TerminalInputHandler.cs b/Tests/Runtime/Components/TerminalInputHandler.cs index 68fc29d..b6d643e 100644 --- a/Tests/Runtime/Components/TerminalInputHandler.cs +++ b/Tests/Runtime/Components/TerminalInputHandler.cs @@ -1,9 +1,9 @@ -namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components -{ - using UnityEngine; - - public sealed class TerminalInputHandler : MonoBehaviour - { - // TODO - } -} +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components +{ + using UnityEngine; + + public sealed class TerminalInputHandler : MonoBehaviour + { + // TODO + } +} diff --git a/Tests/Runtime/Components/TestCommands.cs b/Tests/Runtime/Components/TestCommands.cs index 9b3f174..7ce4984 100644 --- a/Tests/Runtime/Components/TestCommands.cs +++ b/Tests/Runtime/Components/TestCommands.cs @@ -1,76 +1,76 @@ -namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components -{ - using System; - using System.Diagnostics; - using System.Linq; - using Backend; - - public static class TestCommands - { - //[RegisterCommand] - public static void TestCommand(CommandArg[] args) { } - - //[RegisterCommand] - public static void InvalidTestCommand1() { } - - //[RegisterCommand] - public static void InvalidTestCommand2(string args) { } - - //[RegisterCommand] - public static void InvalidTestCommand3(string args, string[] args2) { } - - //[RegisterCommand(MinArgCount = 0, MaxArgCount = 1, Name = "generate-test-data")] - public static void GenerateTestData(CommandArg[] args) - { - if (args.Length != 1 || !args.Single().TryGet(out int count)) - { - count = 50; - } - - Random random = new(); - foreach (int i in Enumerable.Range(0, count).OrderBy(_ => random.Next())) - { - Terminal.Shell?.RunCommand( - "log " + string.Join("a", Enumerable.Range(0, i).Select(_ => string.Empty)) - ); - } - - Terminal.Shell?.RunCommand( - "log " + string.Join("a", Enumerable.Range(0, 1_000).Select(_ => string.Empty)) - ); - } - - //[RegisterCommand(MinArgCount = 0, MaxArgCount = 1, Name = "perf-test")] - public static void RunPerformanceTest(CommandArg[] args) - { - if (args.Length != 1 || !args.Single().TryGet(out int count)) - { - count = 1_000_000; - } - - CommandShell shell = Terminal.Shell; - if (shell == null) - { - return; - } - - // Construct log messages outside of timer - string[] logMessages = Enumerable.Range(0, count).Select(i => $"no-op {i}").ToArray(); - - Stopwatch timer = Stopwatch.StartNew(); - try - { - for (int i = 0; i < count; ++i) - { - shell.RunCommand("no-op"); - Terminal.Log(TerminalLogType.Input, logMessages[i]); - } - } - finally - { - timer.Stop(); - shell.RunCommand($"log Performance test took {timer.ElapsedMilliseconds}ms"); - } - } - } -} +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components +{ + using System; + using System.Diagnostics; + using System.Linq; + using Backend; + + public static class TestCommands + { + //[RegisterCommand] + public static void TestCommand(CommandArg[] args) { } + + //[RegisterCommand] + public static void InvalidTestCommand1() { } + + //[RegisterCommand] + public static void InvalidTestCommand2(string args) { } + + //[RegisterCommand] + public static void InvalidTestCommand3(string args, string[] args2) { } + + //[RegisterCommand(MinArgCount = 0, MaxArgCount = 1, Name = "generate-test-data")] + public static void GenerateTestData(CommandArg[] args) + { + if (args.Length != 1 || !args.Single().TryGet(out int count)) + { + count = 50; + } + + Random random = new(); + foreach (int i in Enumerable.Range(0, count).OrderBy(_ => random.Next())) + { + Terminal.Shell?.RunCommand( + "log " + string.Join("a", Enumerable.Range(0, i).Select(_ => string.Empty)) + ); + } + + Terminal.Shell?.RunCommand( + "log " + string.Join("a", Enumerable.Range(0, 1_000).Select(_ => string.Empty)) + ); + } + + //[RegisterCommand(MinArgCount = 0, MaxArgCount = 1, Name = "perf-test")] + public static void RunPerformanceTest(CommandArg[] args) + { + if (args.Length != 1 || !args.Single().TryGet(out int count)) + { + count = 1_000_000; + } + + CommandShell shell = Terminal.Shell; + if (shell == null) + { + return; + } + + // Construct log messages outside of timer + string[] logMessages = Enumerable.Range(0, count).Select(i => $"no-op {i}").ToArray(); + + Stopwatch timer = Stopwatch.StartNew(); + try + { + for (int i = 0; i < count; ++i) + { + shell.RunCommand("no-op"); + Terminal.Log(TerminalLogType.Input, logMessages[i]); + } + } + finally + { + timer.Stop(); + shell.RunCommand($"log Performance test took {timer.ElapsedMilliseconds}ms"); + } + } + } +} diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index 8378472..ac4f5b6 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -1,168 +1,168 @@ -namespace WallstopStudios.DxCommandTerminal.Tests.Runtime -{ - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using Backend; - using Components; - using NUnit.Framework; - using UI; - using UnityEngine; - using UnityEngine.TestTools; - using UnityEngine.UIElements; - - public sealed class TerminalTests - { - [TearDown] - public void TearDown() - { - if (TerminalUI.Instance != null) - { - Object.Destroy(TerminalUI.Instance.gameObject); - } - } - - [UnityTest] - public IEnumerator ToggleResetsState() - { - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - - TerminalUI terminal = TerminalUI.Instance; - CommandShell shell = Terminal.Shell; - Dictionary shellCommands = shell.Commands.ToDictionary( - kvp => kvp.Key, - kvp => kvp.Value - ); - CommandHistory history = Terminal.History; - CommandLog buffer = Terminal.Buffer; - CommandAutoComplete autoComplete = Terminal.AutoComplete; - - shell.RunCommand("log"); - - string[] events = history.GetHistory(onlySuccess: true, onlyErrorFree: true).ToArray(); - Assert.AreNotEqual(0, events.Length); - - terminal.enabled = false; - terminal.resetStateOnInit = false; - terminal.ignoreDefaultCommands = !terminal.ignoreDefaultCommands; - terminal.enabled = true; - Assert.AreSame(shell, Terminal.Shell); - Assert.AreNotEqual(shellCommands.Count, shell.Commands.Count); - Assert.AreSame(history, Terminal.History); - string[] currentEvents = history - .GetHistory(onlySuccess: true, onlyErrorFree: true) - .ToArray(); - Assert.AreEqual(events.Length, currentEvents.Length); - for (int i = 0; i < events.Length; ++i) - { - Assert.AreEqual(events[i], currentEvents[i], $"History event {i} wasn't the same!"); - } - Assert.AreSame(buffer, Terminal.Buffer); - Assert.AreSame(autoComplete, Terminal.AutoComplete); - } - - [UnityTest] - public IEnumerator CleanConstruction() - { - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - - TerminalUI terminal1 = TerminalUI.Instance; - Assert.IsNotNull(terminal1); - CommandShell shell = Terminal.Shell; - Assert.IsNotNull(shell); - CommandHistory history = Terminal.History; - Assert.IsNotNull(history); - CommandLog buffer = Terminal.Buffer; - Assert.IsNotNull(buffer); - CommandAutoComplete autoComplete = Terminal.AutoComplete; - Assert.IsNotNull(autoComplete); - - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: false); - - TerminalUI terminal2 = TerminalUI.Instance; - Assert.IsNotNull(TerminalUI.Instance); - Assert.AreNotSame(terminal1, TerminalUI.Instance); - Assert.AreSame(shell, Terminal.Shell); - Assert.AreSame(history, Terminal.History); - Assert.AreSame(buffer, Terminal.Buffer); - Assert.AreSame(autoComplete, Terminal.AutoComplete); - - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - - Assert.IsNotNull(TerminalUI.Instance); - Assert.AreNotSame(terminal2, TerminalUI.Instance); - Assert.AreNotSame(terminal1, TerminalUI.Instance); - Assert.AreNotSame(shell, Terminal.Shell); - Assert.AreNotSame(shell, Terminal.Shell); - Assert.IsNotNull(Terminal.Shell); - Assert.AreNotSame(history, Terminal.History); - Assert.IsNotNull(Terminal.History); - Assert.AreNotSame(buffer, Terminal.Buffer); - Assert.IsNotNull(Terminal.Buffer); - Assert.AreNotSame(autoComplete, Terminal.AutoComplete); - Assert.IsNotNull(Terminal.AutoComplete); - } - - internal static IEnumerator SpawnTerminal(bool resetStateOnInit) - { - GameObject go = new("Terminal"); - go.SetActive(false); - - // In tests we skip building UI entirely to avoid engine panel updates - - // Create lightweight test packs to avoid warnings - var themePack = ScriptableObject.CreateInstance(); - var style = ScriptableObject.CreateInstance(); - themePack.Add(style, "test-theme"); - - var fontPack = ScriptableObject.CreateInstance(); - // UI is disabled during tests; no need to add a real font asset - - StartTracker startTracker = go.AddComponent(); - - TerminalUI terminal = go.AddComponent(); - terminal.disableUIForTests = true; - terminal.InjectPacks(themePack, fontPack); - terminal.resetStateOnInit = resetStateOnInit; - - go.SetActive(true); - yield return new WaitUntil(() => startTracker.Started); - // Ensure the buffer is large enough for concurrency tests - if (Terminal.Buffer != null) - { - Terminal.Buffer.Resize(4096); - } - } - - [UnityTest] - public IEnumerator CleanRestartHelperWorks() - { - // Start with reset and capture instances - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - - var shell1 = Terminal.Shell; - var history1 = Terminal.History; - var buffer1 = Terminal.Buffer; - var auto1 = Terminal.AutoComplete; - Assert.IsNotNull(shell1); - Assert.IsNotNull(history1); - - // Clean restart without reset should keep instances - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: false); - Assert.AreSame(shell1, Terminal.Shell); - Assert.AreSame(history1, Terminal.History); - Assert.AreSame(buffer1, Terminal.Buffer); - Assert.AreSame(auto1, Terminal.AutoComplete); - - // Clean restart with reset should replace instances - yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - Assert.AreNotSame(shell1, Terminal.Shell); - Assert.AreNotSame(history1, Terminal.History); - Assert.AreNotSame(buffer1, Terminal.Buffer); - Assert.AreNotSame(auto1, Terminal.AutoComplete); - } - - // Test-only pack types moved to Components/TestPacks.cs - } -} +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + using UnityEngine.UIElements; + + public sealed class TerminalTests + { + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + [UnityTest] + public IEnumerator ToggleResetsState() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + TerminalUI terminal = TerminalUI.Instance; + CommandShell shell = Terminal.Shell; + Dictionary shellCommands = shell.Commands.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value + ); + CommandHistory history = Terminal.History; + CommandLog buffer = Terminal.Buffer; + CommandAutoComplete autoComplete = Terminal.AutoComplete; + + shell.RunCommand("log"); + + string[] events = history.GetHistory(onlySuccess: true, onlyErrorFree: true).ToArray(); + Assert.AreNotEqual(0, events.Length); + + terminal.enabled = false; + terminal.resetStateOnInit = false; + terminal.ignoreDefaultCommands = !terminal.ignoreDefaultCommands; + terminal.enabled = true; + Assert.AreSame(shell, Terminal.Shell); + Assert.AreNotEqual(shellCommands.Count, shell.Commands.Count); + Assert.AreSame(history, Terminal.History); + string[] currentEvents = history + .GetHistory(onlySuccess: true, onlyErrorFree: true) + .ToArray(); + Assert.AreEqual(events.Length, currentEvents.Length); + for (int i = 0; i < events.Length; ++i) + { + Assert.AreEqual(events[i], currentEvents[i], $"History event {i} wasn't the same!"); + } + Assert.AreSame(buffer, Terminal.Buffer); + Assert.AreSame(autoComplete, Terminal.AutoComplete); + } + + [UnityTest] + public IEnumerator CleanConstruction() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + TerminalUI terminal1 = TerminalUI.Instance; + Assert.IsNotNull(terminal1); + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + CommandHistory history = Terminal.History; + Assert.IsNotNull(history); + CommandLog buffer = Terminal.Buffer; + Assert.IsNotNull(buffer); + CommandAutoComplete autoComplete = Terminal.AutoComplete; + Assert.IsNotNull(autoComplete); + + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: false); + + TerminalUI terminal2 = TerminalUI.Instance; + Assert.IsNotNull(TerminalUI.Instance); + Assert.AreNotSame(terminal1, TerminalUI.Instance); + Assert.AreSame(shell, Terminal.Shell); + Assert.AreSame(history, Terminal.History); + Assert.AreSame(buffer, Terminal.Buffer); + Assert.AreSame(autoComplete, Terminal.AutoComplete); + + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + Assert.IsNotNull(TerminalUI.Instance); + Assert.AreNotSame(terminal2, TerminalUI.Instance); + Assert.AreNotSame(terminal1, TerminalUI.Instance); + Assert.AreNotSame(shell, Terminal.Shell); + Assert.AreNotSame(shell, Terminal.Shell); + Assert.IsNotNull(Terminal.Shell); + Assert.AreNotSame(history, Terminal.History); + Assert.IsNotNull(Terminal.History); + Assert.AreNotSame(buffer, Terminal.Buffer); + Assert.IsNotNull(Terminal.Buffer); + Assert.AreNotSame(autoComplete, Terminal.AutoComplete); + Assert.IsNotNull(Terminal.AutoComplete); + } + + internal static IEnumerator SpawnTerminal(bool resetStateOnInit) + { + GameObject go = new("Terminal"); + go.SetActive(false); + + // In tests we skip building UI entirely to avoid engine panel updates + + // Create lightweight test packs to avoid warnings + var themePack = ScriptableObject.CreateInstance(); + var style = ScriptableObject.CreateInstance(); + themePack.Add(style, "test-theme"); + + var fontPack = ScriptableObject.CreateInstance(); + // UI is disabled during tests; no need to add a real font asset + + StartTracker startTracker = go.AddComponent(); + + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + terminal.InjectPacks(themePack, fontPack); + terminal.resetStateOnInit = resetStateOnInit; + + go.SetActive(true); + yield return new WaitUntil(() => startTracker.Started); + // Ensure the buffer is large enough for concurrency tests + if (Terminal.Buffer != null) + { + Terminal.Buffer.Resize(4096); + } + } + + [UnityTest] + public IEnumerator CleanRestartHelperWorks() + { + // Start with reset and capture instances + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + var shell1 = Terminal.Shell; + var history1 = Terminal.History; + var buffer1 = Terminal.Buffer; + var auto1 = Terminal.AutoComplete; + Assert.IsNotNull(shell1); + Assert.IsNotNull(history1); + + // Clean restart without reset should keep instances + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: false); + Assert.AreSame(shell1, Terminal.Shell); + Assert.AreSame(history1, Terminal.History); + Assert.AreSame(buffer1, Terminal.Buffer); + Assert.AreSame(auto1, Terminal.AutoComplete); + + // Clean restart with reset should replace instances + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + Assert.AreNotSame(shell1, Terminal.Shell); + Assert.AreNotSame(history1, Terminal.History); + Assert.AreNotSame(buffer1, Terminal.Buffer); + Assert.AreNotSame(auto1, Terminal.AutoComplete); + } + + // Test-only pack types moved to Components/TestPacks.cs + } +} diff --git a/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef b/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef index 9139c16..8841c45 100644 --- a/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef +++ b/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef @@ -1,21 +1,14 @@ { - "name": "WallstopStudios.DxCommandTerminal.Tests.Runtime", - "rootNamespace": "WallstopStudios.DxCommandTerminal.Tests", - "references": [ - "WallstopStudios.DxCommandTerminal", - "UnityEngine.TestRunner" - ], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": true, - "precompiledReferences": [ - "nunit.framework.dll" - ], - "autoReferenced": false, - "defineConstraints": [ - "UNITY_INCLUDE_TESTS" - ], - "versionDefines": [], - "noEngineReferences": false -} \ No newline at end of file + "name": "WallstopStudios.DxCommandTerminal.Tests.Runtime", + "rootNamespace": "WallstopStudios.DxCommandTerminal.Tests", + "references": ["WallstopStudios.DxCommandTerminal", "UnityEngine.TestRunner"], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": ["nunit.framework.dll"], + "autoReferenced": false, + "defineConstraints": ["UNITY_INCLUDE_TESTS"], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/package.json b/package.json index 2d28009..568e99d 100644 --- a/package.json +++ b/package.json @@ -1,43 +1,43 @@ -{ - "name": "com.wallstop-studios.dxcommandterminal", - "version": "1.0.0-rc24.7", - "displayName": "DxCommandTerminal", - "description": "Wallstop Studios fork of Command Terminal for Unity", - "dependencies": {}, - "unity": "2021.3", - "keywords": [ - "console", - "command", - "commands", - "terminal", - "command terminal", - "library", - "utility", - "cheats" - ], - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/wallstop/DxCommandTerminal.git" - }, - "bugs": { - "url": "https://github.com/wallstop/DxCommandTerminal/issues" - }, - "author": "wallstop studios (https://wallstopstudios.com)", - "homepage": "https://github.com/wallstop/DxCommandTerminal/blob/master/README.md", - "main": "README.md", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "format:md": "prettier --write \"**/*.{md,markdown}\"", - "format:md:check": "prettier --check \"**/*.{md,markdown}\"", - "format:json": "prettier --write \"**/*.{json,asmdef,asmref}\"", - "format:json:check": "prettier --check \"**/*.{json,asmdef,asmref}\"", - "format:yaml": "prettier --write \"**/*.{yml,yaml}\"", - "format:yaml:check": "prettier --check \"**/*.{yml,yaml}\"", - "lint:markdown": "markdownlint \"**/*.md\" \"**/*.markdown\" --config .markdownlint.json --ignore-path .markdownlintignore" - }, - "devDependencies": { - "markdownlint-cli": "0.40.0", - "prettier": "3.3.3" - } -} +{ + "name": "com.wallstop-studios.dxcommandterminal", + "version": "1.0.0-rc24.7", + "displayName": "DxCommandTerminal", + "description": "Wallstop Studios fork of Command Terminal for Unity", + "dependencies": {}, + "unity": "2021.3", + "keywords": [ + "console", + "command", + "commands", + "terminal", + "command terminal", + "library", + "utility", + "cheats" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/wallstop/DxCommandTerminal.git" + }, + "bugs": { + "url": "https://github.com/wallstop/DxCommandTerminal/issues" + }, + "author": "wallstop studios (https://wallstopstudios.com)", + "homepage": "https://github.com/wallstop/DxCommandTerminal/blob/master/README.md", + "main": "README.md", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "format:md": "prettier --write \"**/*.{md,markdown}\"", + "format:md:check": "prettier --check \"**/*.{md,markdown}\"", + "format:json": "prettier --write \"**/*.{json,asmdef,asmref}\"", + "format:json:check": "prettier --check \"**/*.{json,asmdef,asmref}\"", + "format:yaml": "prettier --write \"**/*.{yml,yaml}\"", + "format:yaml:check": "prettier --check \"**/*.{yml,yaml}\"", + "lint:markdown": "markdownlint \"**/*.md\" \"**/*.markdown\" --config .markdownlint.json --ignore-path .markdownlintignore" + }, + "devDependencies": { + "markdownlint-cli": "0.40.0", + "prettier": "3.3.3" + } +} From 61d3a3172be71ace418f3dff3469b0c0bc7dc54d Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 10:48:15 -0700 Subject: [PATCH 08/69] Linter updates --- .github/workflows/prettier-autofix.yml | 1 - .pre-commit-config.yaml | 16 ++++++ package.json | 7 ++- scripts/check-eol.ps1 | 42 +++++++++++++--- scripts/lint-doc-links.ps1 | 70 ++++++++++++++++++++++++++ scripts/lint-doc-links.ps1.meta | 7 +++ 6 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 scripts/lint-doc-links.ps1 create mode 100644 scripts/lint-doc-links.ps1.meta diff --git a/.github/workflows/prettier-autofix.yml b/.github/workflows/prettier-autofix.yml index ee89e07..6737cfb 100644 --- a/.github/workflows/prettier-autofix.yml +++ b/.github/workflows/prettier-autofix.yml @@ -28,7 +28,6 @@ jobs: cache-dependency-path: package.json - name: Install dependencies - if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} run: | if [ -f package-lock.json ]; then npm ci diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 415414f..da513a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,3 +49,19 @@ repos: entry: bash -c 'if command -v yamllint >/dev/null 2>&1; then yamllint -c .yamllint.yaml "$@"; else echo "yamllint not installed; skipping"; fi' -- language: system files: '(?i)\.(ya?ml)$' + + - repo: local + hooks: + - id: check-eol-bom + name: Enforce CRLF + no BOM (auto-fix) + entry: pwsh -NoProfile -File scripts/check-eol.ps1 -Fix + language: system + pass_filenames: false + always_run: true + description: Normalizes line endings to CRLF and strips UTF-8 BOM. + - id: lint-doc-links + name: Lint relative Markdown links + entry: pwsh -NoProfile -File scripts/lint-doc-links.ps1 + language: system + pass_filenames: false + files: '(?i)\.(md|markdown)$' diff --git a/package.json b/package.json index 568e99d..d164bda 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,12 @@ "format:json:check": "prettier --check \"**/*.{json,asmdef,asmref}\"", "format:yaml": "prettier --write \"**/*.{yml,yaml}\"", "format:yaml:check": "prettier --check \"**/*.{yml,yaml}\"", - "lint:markdown": "markdownlint \"**/*.md\" \"**/*.markdown\" --config .markdownlint.json --ignore-path .markdownlintignore" + "format:csharp": "dotnet tool run csharpier format .", + "format:eol": "pwsh -NoProfile -File ./scripts/check-eol.ps1 -Fix", + "format:all": "npm run format:md && npm run format:json && npm run format:yaml && npm run format:csharp && npm run format:eol", + "lint:markdown": "markdownlint \"**/*.md\" \"**/*.markdown\" --config .markdownlint.json --ignore-path .markdownlintignore", + "lint:doc-links": "pwsh -NoProfile -File ./scripts/lint-doc-links.ps1 -VerboseOutput", + "lint:all": "npm run format:md:check && npm run format:json:check && npm run format:yaml:check && npm run lint:markdown && npm run lint:doc-links" }, "devDependencies": { "markdownlint-cli": "0.40.0", diff --git a/scripts/check-eol.ps1 b/scripts/check-eol.ps1 index 37eb17d..d1775f7 100644 --- a/scripts/check-eol.ps1 +++ b/scripts/check-eol.ps1 @@ -1,10 +1,11 @@ -$ErrorActionPreference = 'Stop' - param( [string] $Root = '.', - [switch] $VerboseOutput + [switch] $VerboseOutput, + [switch] $Fix ) +$ErrorActionPreference = 'Stop' + function Write-VerboseLine($msg) { if ($VerboseOutput) { Write-Host $msg } } @@ -14,6 +15,7 @@ $extensions = @('md','markdown','json','asmdef','asmref','yml','yaml') $badBom = New-Object System.Collections.Generic.List[string] $badEol = New-Object System.Collections.Generic.List[string] +$fixedFiles = New-Object System.Collections.Generic.List[string] $files = Get-ChildItem -Path $Root -Recurse -File | Where-Object { @@ -29,9 +31,9 @@ foreach ($f in $files) { Write-VerboseLine "Checking: $($f.FullName)" $bytes = [System.IO.File]::ReadAllBytes($f.FullName) + $hasBom = $false if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { - $badBom.Add($f.FullName) - continue + $hasBom = $true } # Verify all LF (0x0A) are preceded by CR (0x0D) @@ -45,12 +47,32 @@ foreach ($f in $files) { } } - if ($hasLfWithoutCr) { - $badEol.Add($f.FullName) + if ($Fix -and ($hasBom -or $hasLfWithoutCr)) { + # Decode as UTF-8, skip BOM bytes if present + $startIndex = if ($hasBom) { 3 } else { 0 } + $len = $bytes.Length - $startIndex + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIndex, $len) + # Normalize line endings to CRLF + $text = [System.Text.RegularExpressions.Regex]::Replace($text, "\r?\n", "`r`n") + # Write back without BOM + $enc = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($f.FullName, $text, $enc) + $fixedFiles.Add($f.FullName) + # Recompute to reflect post-fix status + $bytes = [System.IO.File]::ReadAllBytes($f.FullName) + $hasBom = $false + $hasLfWithoutCr = $false } + + if ($hasBom) { $badBom.Add($f.FullName) } + if ($hasLfWithoutCr) { $badEol.Add($f.FullName) } } if ($badBom.Count -eq 0 -and $badEol.Count -eq 0) { + if ($fixedFiles.Count -gt 0) { + Write-Host "EOL/BOM issues were fixed in the following files:" + $fixedFiles | ForEach-Object { Write-Host " - $_" } + } Write-Host "EOL/BOM check passed: All checked files use CRLF and no BOM." exit 0 } @@ -65,6 +87,10 @@ if ($badEol.Count -gt 0) { $badEol | ForEach-Object { Write-Host " - $_" } } +if ($fixedFiles.Count -gt 0) { + Write-Host "Fixed files:" + $fixedFiles | ForEach-Object { Write-Host " - $_" } +} + Write-Error "EOL/BOM validation failed. See lists above." exit 1 - diff --git a/scripts/lint-doc-links.ps1 b/scripts/lint-doc-links.ps1 new file mode 100644 index 0000000..3bf4a58 --- /dev/null +++ b/scripts/lint-doc-links.ps1 @@ -0,0 +1,70 @@ +param( + [string] $Root = '.', + [switch] $VerboseOutput +) + +$ErrorActionPreference = 'Stop' + +function Write-VerboseLine($msg) { + if ($VerboseOutput) { Write-Host $msg } +} + +# Lints relative Markdown links. External links are validated by lychee in CI. +# - Flags relative links that point to non-existent files or directories. +# - Ignores mailto:, http(s):, absolute paths, and pure fragment links (#anchor). + +$mdFiles = Get-ChildItem -Path $Root -Recurse -File -Include *.md, *.markdown | + Where-Object { $_.FullName -notmatch '(?:\\|/)(node_modules|.git)(?:\\|/)' } + +$broken = New-Object System.Collections.Generic.List[object] + +$linkPattern = '\[(?:[^\]]+)\]\((?[^)\s]+)(?:\s+"[^"]*")?\)' + +foreach ($file in $mdFiles) { + $lines = [System.IO.File]::ReadAllLines($file.FullName) + for ($i = 0; $i -lt $lines.Length; $i++) { + $line = $lines[$i] + foreach ($m in [System.Text.RegularExpressions.Regex]::Matches($line, $linkPattern)) { + $target = $m.Groups['target'].Value + + # Skip external and pure anchors + if ($target -match '^(?:https?:|mailto:|tel:)' -or $target -match '^#') { continue } + + # Normalize and strip optional fragment + $basePath = $target.Split('#')[0] + if ([string]::IsNullOrWhiteSpace($basePath)) { continue } + + # Resolve relative to file directory + # Only validate file-like links (with a dot in the last segment). Directory links are allowed in docs. + $lastSeg = [System.IO.Path]::GetFileName($basePath.TrimEnd([char[]]@('/','\'))) + if ($lastSeg -notmatch '\.') { continue } + + $candidate = Join-Path -Path $file.DirectoryName -ChildPath $basePath + + # On case-sensitive CI we also want to catch case-only mismatches + $exists = Test-Path -LiteralPath $candidate + if (-not $exists) { + $broken.Add([pscustomobject]@{ + File = $file.FullName + Line = $i + 1 + Target = $target + }) + } else { + Write-VerboseLine "OK: ${($file.Name)}:$($i + 1) -> $target" + } + } + } +} + +if ($broken.Count -eq 0) { + Write-Host 'Markdown link lint passed: all relative links resolve.' + exit 0 +} + +Write-Host 'Broken relative Markdown links found:' +foreach ($b in $broken) { + Write-Host " - $($b.File):$($b.Line) -> $($b.Target)" +} + +Write-Error 'Relative Markdown link validation failed.' +exit 1 diff --git a/scripts/lint-doc-links.ps1.meta b/scripts/lint-doc-links.ps1.meta new file mode 100644 index 0000000..1eb4ab7 --- /dev/null +++ b/scripts/lint-doc-links.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 54c6490203c952349bd1e53020a1e44d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 30264ba4f80ec4fd7637a1a346a16237608dec91 Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 14:14:00 -0700 Subject: [PATCH 09/69] Progress --- AGENTS.md | 2 + .../CustomEditors/TerminalThemePackEditor.cs | 37 ++- Editor/CustomEditors/TerminalUIEditor.cs | 148 ++++++----- .../Helper/TerminalThemeStyleSheetHelper.cs | 9 +- Editor/Parsers/ParserAutoDiscovery.cs | 4 +- Editor/TerminalUI.RuntimeMode.Editor.cs | 2 +- .../Utils/ScriptableObjectSingletonCreator.cs | 56 ++++- .../Backend/BuiltinCommands.cs | 56 +++-- Runtime/CommandTerminal/Backend/CommandArg.cs | 66 +++-- .../Backend/CommandAutoComplete.cs | 79 ++++-- .../CommandTerminal/Backend/CommandHistory.cs | 40 ++- Runtime/CommandTerminal/Backend/CommandLog.cs | 14 +- .../CommandTerminal/Backend/CommandShell.cs | 170 +++++++++---- .../Completers/FontArgumentCompleter.cs | 44 +++- .../Completers/ThemeArgumentCompleter.cs | 42 +++- .../Backend/Parsers/EnumArgParser.cs | 14 +- .../Backend/Parsers/StaticMemberParser.cs | 27 +- .../Input/TerminalKeyboardController.cs | 159 ++++++++---- Runtime/CommandTerminal/UI/TerminalUI.cs | 232 ++++++++++++------ Tests/Runtime/AutocompleteTests.cs | 228 +++++++++++++++++ Tests/Runtime/AutocompleteTests.cs.meta | 11 + Tests/Runtime/CommandHistoryTests.cs | 41 ++++ Tests/Runtime/CommandHistoryTests.cs.meta | 11 + Tests/Runtime/CompletersTests.cs | 86 +++++++ Tests/Runtime/CompletersTests.cs.meta | 11 + Tests/Runtime/ParserTests.cs | 51 ++++ Tests/Runtime/ParserTests.cs.meta | 11 + Tests/Runtime/TerminalTests.cs | 1 - linq_hits.txt | 122 +++++++++ linq_hits.txt.meta | 7 + 30 files changed, 1420 insertions(+), 361 deletions(-) create mode 100644 Tests/Runtime/AutocompleteTests.cs create mode 100644 Tests/Runtime/AutocompleteTests.cs.meta create mode 100644 Tests/Runtime/CommandHistoryTests.cs create mode 100644 Tests/Runtime/CommandHistoryTests.cs.meta create mode 100644 Tests/Runtime/CompletersTests.cs create mode 100644 Tests/Runtime/CompletersTests.cs.meta create mode 100644 Tests/Runtime/ParserTests.cs create mode 100644 Tests/Runtime/ParserTests.cs.meta create mode 100644 linq_hits.txt create mode 100644 linq_hits.txt.meta diff --git a/AGENTS.md b/AGENTS.md index 9503aa9..dfe75dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,7 @@ - Use CSharpier for formatting before committing. - Do not use underscores in function names, especially test function names. - Do not use regions, anywhere, ever. +- Avoid `var` wherever possible, use expressive types. ## Testing Guidelines @@ -39,6 +40,7 @@ - Do not use Description annotations for tests. - Do not create `async Task` test methods - the Unity test runner does not support this. Make do with `IEnumerator` based UnityTestMethods. - Do not use `Assert.ThrowsAsync`, it does not exist. +- When asserting that UnityEngine.Objects are null or not null, please check for null directly (thing != null, thing == null), to properly adhere to Unity Object existence checks. ## Commit & Pull Request Guidelines diff --git a/Editor/CustomEditors/TerminalThemePackEditor.cs b/Editor/CustomEditors/TerminalThemePackEditor.cs index b5af8a7..022531b 100644 --- a/Editor/CustomEditors/TerminalThemePackEditor.cs +++ b/Editor/CustomEditors/TerminalThemePackEditor.cs @@ -4,7 +4,6 @@ namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors using System; using System.Collections.Generic; using System.IO; - using System.Linq; using DxCommandTerminal.Helper; using Extensions; using Helper; @@ -67,7 +66,8 @@ public override void OnInspectorGUI() continue; } _styleCache.Add(theme); - if (!TerminalThemeStyleSheetHelper.GetAvailableThemes(theme).Any()) + string[] available = TerminalThemeStyleSheetHelper.GetAvailableThemes(theme); + if (available == null || available.Length == 0) { _invalidStyles.Add(theme); } @@ -79,7 +79,7 @@ public override void OnInspectorGUI() if ( anyInvalidTheme || _styleCache.Count != themePack._themes.Count - || _invalidStyles.Any() + || _invalidStyles.Count != 0 ) { if (GUILayout.Button("Fix Invalid Themes", _impactButtonStyle)) @@ -159,9 +159,18 @@ void SortThemes() themePack._themes.SortByName(); themePack._themeNames ??= new List(); themePack._themeNames.Clear(); - themePack._themeNames.AddRange( - themePack._themes.SelectMany(TerminalThemeStyleSheetHelper.GetAvailableThemes) - ); + foreach (StyleSheet style in themePack._themes) + { + string[] themes = TerminalThemeStyleSheetHelper.GetAvailableThemes(style); + if (themes == null) + { + continue; + } + for (int i = 0; i < themes.Length; ++i) + { + themePack._themeNames.Add(themes[i]); + } + } } void UpdateFromDirectory(string directory) @@ -192,14 +201,16 @@ void UpdateFromDirectory(string directory) StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath( styleAssetPath ); - if ( - styleSheet != null - && TerminalThemeStyleSheetHelper.GetAvailableThemes(styleSheet).Any() - && _styleCache.Add(styleSheet) - ) + if (styleSheet != null) { - anyChanged = true; - themePack._themes.Add(styleSheet); + string[] themes = TerminalThemeStyleSheetHelper.GetAvailableThemes( + styleSheet + ); + if ((themes != null && themes.Length > 0) && _styleCache.Add(styleSheet)) + { + anyChanged = true; + themePack._themes.Add(styleSheet); + } } } } diff --git a/Editor/CustomEditors/TerminalUIEditor.cs b/Editor/CustomEditors/TerminalUIEditor.cs index f0e49cc..bd6cf89 100644 --- a/Editor/CustomEditors/TerminalUIEditor.cs +++ b/Editor/CustomEditors/TerminalUIEditor.cs @@ -205,25 +205,26 @@ out TerminalKeyboardController keyboardController private void OnEnable() { _allCommands.Clear(); - _allCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Select(attribute => attribute.Name) - ); _defaultCommands.Clear(); - _defaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => tuple.Default) - .Select(attribute => attribute.Name) - ); _nonDefaultCommands.Clear(); - _nonDefaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => !tuple.Default) - .Select(attribute => attribute.Name) - ); + var reg = CommandShell.RegisteredCommands.Value; + for (int i = 0; i < reg.Length; ++i) + { + var attr = reg[i].attribute; + if (attr == null || string.IsNullOrWhiteSpace(attr.Name)) + { + continue; + } + _allCommands.Add(attr.Name); + if (attr.Default) + { + _defaultCommands.Add(attr.Name); + } + else + { + _nonDefaultCommands.Add(attr.Name); + } + } _fontsByPrefix.Clear(); ResetStateIdempotent(force: true); @@ -310,13 +311,13 @@ private void ResetStateIdempotent(bool force) } TerminalAssetPackPostProcessor.NewFontPacks.Clear(); - if (!_fontPacks.Any()) + if (_fontPacks.Count == 0) { _fontPacks.Clear(); _fontPacks.AddRange(LoadAll()); } - if (!_themePacks.Any()) + if (_themePacks.Count == 0) { _themePacks.Clear(); _themePacks.AddRange(LoadAll()); @@ -397,7 +398,26 @@ private Font GetCurrentlySelectedFont(TerminalUI terminal) try { - return _fontsByPrefix.ToArray()[_fontKey].Value.ToArray()[_secondFontKey].Value; + int i = 0; + foreach ( + KeyValuePair> outer in _fontsByPrefix + ) + { + if (i++ != _fontKey) + { + continue; + } + int j = 0; + foreach (KeyValuePair inner in outer.Value) + { + if (j++ == _secondFontKey) + { + return inner.Value; + } + } + break; + } + return terminal.CurrentFont; } catch { @@ -472,25 +492,26 @@ public override void OnInspectorGUI() private void HydrateCommandCaches() { _allCommands.Clear(); - _allCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Select(attribute => attribute.Name) - ); _defaultCommands.Clear(); - _defaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => tuple.Default) - .Select(attribute => attribute.Name) - ); _nonDefaultCommands.Clear(); - _nonDefaultCommands.UnionWith( - CommandShell - .RegisteredCommands.Value.Select(tuple => tuple.attribute) - .Where(tuple => !tuple.Default) - .Select(attribute => attribute.Name) - ); + var reg = CommandShell.RegisteredCommands.Value; + for (int i = 0; i < reg.Length; ++i) + { + var attr = reg[i].attribute; + if (attr == null || string.IsNullOrWhiteSpace(attr.Name)) + { + continue; + } + _allCommands.Add(attr.Name); + if (attr.Default) + { + _defaultCommands.Add(attr.Name); + } + else + { + _nonDefaultCommands.Add(attr.Name); + } + } } private void RenderCyclingPreviews() @@ -733,7 +754,7 @@ private bool CheckForThemingAndFontChanges(TerminalUI terminal) EditorGUILayout.BeginHorizontal(); try { - if (!_themePacks.Any()) + if (_themePacks.Count == 0) { GUILayout.Label("NO THEME PACKS", _impactLabelStyle); } @@ -749,10 +770,13 @@ private bool CheckForThemingAndFontChanges(TerminalUI terminal) GUILayout.Label("Select Theme Pack:"); } - _themePackIndex = EditorGUILayout.Popup( - _themePackIndex, - _themePacks.Select(themePack => themePack.name).ToArray() - ); + string[] themePackNames = new string[_themePacks.Count]; + for (int i = 0; i < _themePacks.Count; ++i) + { + themePackNames[i] = + _themePacks[i] != null ? _themePacks[i].name : string.Empty; + } + _themePackIndex = EditorGUILayout.Popup(_themePackIndex, themePackNames); if (0 <= _themePackIndex && _themePackIndex < _themePacks.Count) { TerminalThemePack themePack = _themePacks[_themePackIndex]; @@ -785,7 +809,7 @@ private bool CheckForThemingAndFontChanges(TerminalUI terminal) EditorGUILayout.BeginHorizontal(); try { - if (!_fontPacks.Any()) + if (_fontPacks.Count == 0) { GUILayout.Label("NO FONT PACKS", _impactLabelStyle); } @@ -801,10 +825,13 @@ private bool CheckForThemingAndFontChanges(TerminalUI terminal) GUILayout.Label("Select Font Pack:"); } - _fontPackIndex = EditorGUILayout.Popup( - _fontPackIndex, - _fontPacks.Select(fontPack => fontPack.name).ToArray() - ); + string[] fontPackNames = new string[_fontPacks.Count]; + for (int i = 0; i < _fontPacks.Count; ++i) + { + fontPackNames[i] = + _fontPacks[i] != null ? _fontPacks[i].name : string.Empty; + } + _fontPackIndex = EditorGUILayout.Popup(_fontPackIndex, fontPackNames); if (0 <= _fontPackIndex && _fontPackIndex < _fontPacks.Count) { TerminalFontPack fontPack = _fontPacks[_fontPackIndex]; @@ -856,24 +883,15 @@ private bool CheckForThemingAndFontChanges(TerminalUI terminal) GUILayout.Label("Select Theme:"); } - _themeIndex = EditorGUILayout.Popup( - _themeIndex, - terminal - ._themePack._themeNames.Select(theme => - theme - .Replace( - "-theme", - string.Empty, - StringComparison.OrdinalIgnoreCase - ) - .Replace( - "theme-", - string.Empty, - StringComparison.OrdinalIgnoreCase - ) - ) - .ToArray() - ); + string[] themeOptions = new string[terminal._themePack._themeNames.Count]; + for (int i = 0; i < themeOptions.Length; ++i) + { + string t = terminal._themePack._themeNames[i] ?? string.Empty; + t = t.Replace("-theme", string.Empty, StringComparison.OrdinalIgnoreCase); + t = t.Replace("theme-", string.Empty, StringComparison.OrdinalIgnoreCase); + themeOptions[i] = t; + } + _themeIndex = EditorGUILayout.Popup(_themeIndex, themeOptions); if (0 <= _themeIndex && _themeIndex < terminal._themePack._themeNames.Count) { diff --git a/Editor/Helper/TerminalThemeStyleSheetHelper.cs b/Editor/Helper/TerminalThemeStyleSheetHelper.cs index 2915de8..d14e425 100644 --- a/Editor/Helper/TerminalThemeStyleSheetHelper.cs +++ b/Editor/Helper/TerminalThemeStyleSheetHelper.cs @@ -4,7 +4,6 @@ namespace WallstopStudios.DxCommandTerminal.Editor.Helper using System; using System.Collections.Generic; using System.IO; - using System.Linq; using System.Text.RegularExpressions; using Themes; using UnityEditor; @@ -143,7 +142,13 @@ public static string[] GetAvailableThemes(StyleSheet styleSheetAsset) lastIndex = nextBraceIndex < 0 ? ussContent.Length : nextBraceIndex + 1; } - return selectors.ToArray(); + if (selectors.Count == 0) + { + return Array.Empty(); + } + string[] arr = new string[selectors.Count]; + selectors.CopyTo(arr); + return arr; } catch (Exception e) { diff --git a/Editor/Parsers/ParserAutoDiscovery.cs b/Editor/Parsers/ParserAutoDiscovery.cs index f8c3b63..a1b3a94 100644 --- a/Editor/Parsers/ParserAutoDiscovery.cs +++ b/Editor/Parsers/ParserAutoDiscovery.cs @@ -11,8 +11,8 @@ static ParserAutoDiscovery() { // Editor convenience: allow auto-discovery via config flags if ( - Backend.TerminalRuntimeConfig.ShouldEnableEditorFeatures() - && Backend.TerminalRuntimeConfig.EditorAutoDiscover + TerminalRuntimeConfig.ShouldEnableEditorFeatures() + && TerminalRuntimeConfig.EditorAutoDiscover ) { CommandArg.DiscoverAndRegisterParsers(replaceExisting: false); diff --git a/Editor/TerminalUI.RuntimeMode.Editor.cs b/Editor/TerminalUI.RuntimeMode.Editor.cs index 172f4e4..4a150d2 100644 --- a/Editor/TerminalUI.RuntimeMode.Editor.cs +++ b/Editor/TerminalUI.RuntimeMode.Editor.cs @@ -8,7 +8,7 @@ namespace WallstopStudios.DxCommandTerminal.Editor internal static class TerminalUIRuntimeModeMenu { - private const string MenuRoot = "Tools/DxCommandTerminal/Runtime Mode/"; + private const string MenuRoot = "Tools/Wallstop Studios/DxCommandTerminal/Runtime Mode/"; [MenuItem(MenuRoot + "Editor", false, 0)] private static void SetEditorMode() diff --git a/Editor/Utils/ScriptableObjectSingletonCreator.cs b/Editor/Utils/ScriptableObjectSingletonCreator.cs index c8c77fd..8729f00 100644 --- a/Editor/Utils/ScriptableObjectSingletonCreator.cs +++ b/Editor/Utils/ScriptableObjectSingletonCreator.cs @@ -4,7 +4,7 @@ namespace WallstopStudios.DxCommandTerminal.Editor.Utils using System; using System.Collections.Generic; using System.IO; - using System.Linq; + using System.Text; using System.Reflection; using UnityEditor; using UnityEngine; @@ -66,7 +66,19 @@ public static void EnsureSingletonAssets() } catch (ReflectionTypeLoadException ex) { - types = ex.Types.Where(x => x != null).ToArray(); + List tmp = new List(); + if (ex.Types != null) + { + for (int i = 0; i < ex.Types.Length; ++i) + { + Type t = ex.Types[i]; + if (t != null) + { + tmp.Add(t); + } + } + } + types = tmp.ToArray(); } foreach (Type t in types) @@ -79,17 +91,47 @@ public static void EnsureSingletonAssets() } // Simple collision detection by simple name - var collisions = candidates - .GroupBy(t => t.Name, StringComparer.OrdinalIgnoreCase) - .Where(g => g.Count() > 1) - .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase); + Dictionary> nameGroups = new Dictionary>( + StringComparer.OrdinalIgnoreCase + ); + for (int i = 0; i < candidates.Count; ++i) + { + Type t = candidates[i]; + string key = t.Name ?? string.Empty; + if (!nameGroups.TryGetValue(key, out List list)) + { + list = new List(); + nameGroups[key] = list; + } + list.Add(t); + } + Dictionary> collisions = new Dictionary>( + StringComparer.OrdinalIgnoreCase + ); + foreach (KeyValuePair> kv in nameGroups) + { + if (kv.Value != null && kv.Value.Count > 1) + { + collisions[kv.Key] = kv.Value; + } + } foreach (Type type in candidates) { if (collisions.ContainsKey(type.Name)) { + List coll = collisions[type.Name]; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < coll.Count; ++i) + { + if (i > 0) + { + sb.Append(", "); + } + sb.Append(coll[i]?.FullName); + } Debug.LogWarning( - $"ScriptableObjectSingletonCreator: Multiple types share the name '{type.Name}'. Skipping auto-creation. Add [ScriptableSingletonPath] to disambiguate or rename. Types: {string.Join(", ", collisions[type.Name].Select(x => x.FullName))}" + $"ScriptableObjectSingletonCreator: Multiple types share the name '{type.Name}'. Skipping auto-creation. Add [ScriptableSingletonPath] to disambiguate or rename. Types: {sb.ToString()}" ); continue; } diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index cd7c2dd..2f66bbd 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -3,7 +3,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend using System; using System.Collections.Generic; using System.Diagnostics; - using System.Linq; using System.Text; using Attributes; using Themes; @@ -38,10 +37,17 @@ public static void CommandListThemes(CommandArg[] args) return; } - string themes = string.Join( - BulkSeparator, - terminal._themePack._themeNames.Select(ThemeNameHelper.GetFriendlyThemeName) - ); + StringBuilder.Clear(); + List names = terminal._themePack._themeNames; + for (int i = 0; i < names.Count; ++i) + { + if (i > 0) + { + StringBuilder.Append(BulkSeparator); + } + StringBuilder.Append(ThemeNameHelper.GetFriendlyThemeName(names[i])); + } + string themes = StringBuilder.ToString(); Terminal.Log(TerminalLogType.Message, themes); } @@ -66,10 +72,18 @@ public static void CommandListFonts(CommandArg[] args) return; } - string themes = string.Join( - BulkSeparator, - terminal._fontPack._fonts.Select(font => font.name) - ); + StringBuilder.Clear(); + List fonts = terminal._fontPack._fonts; + for (int i = 0; i < fonts.Count; ++i) + { + if (i > 0) + { + StringBuilder.Append(BulkSeparator); + } + Font f = fonts[i]; + StringBuilder.Append(f != null ? f.name : string.Empty); + } + string themes = StringBuilder.ToString(); Terminal.Log(TerminalLogType.Message, themes); } @@ -159,9 +173,22 @@ public static void CommandSetFont(CommandArg[] args) return; } - UnityEngine.Font font = terminal._fontPack._fonts.FirstOrDefault(f => - f != null && string.Equals(f.name, fontName, StringComparison.OrdinalIgnoreCase) - ); + Font font = null; + foreach (Font existingFont in terminal._fontPack._fonts) + { + if ( + existingFont != null + && string.Equals( + existingFont.name, + fontName, + StringComparison.OrdinalIgnoreCase + ) + ) + { + font = existingFont; + break; + } + } if (font == null) { @@ -566,7 +593,8 @@ public static void CommandClearAllVariable(CommandArg[] args) } int variableCount = shell.Variables.Count; - foreach (string variable in shell.Variables.Keys.ToArray()) + List variableNames = new List(shell.Variables.Keys); + foreach (string variable in variableNames) { shell.ClearVariable(variable); } @@ -667,7 +695,7 @@ public static void CommandGetAllVariables(CommandArg[] args) return; } - if (!shell.Variables.Any()) + if (shell.Variables.Count == 0) { Terminal.Log(TerminalLogType.Warning, "No variables found."); return; diff --git a/Runtime/CommandTerminal/Backend/CommandArg.cs b/Runtime/CommandTerminal/Backend/CommandArg.cs index c2e9134..81723f0 100644 --- a/Runtime/CommandTerminal/Backend/CommandArg.cs +++ b/Runtime/CommandTerminal/Backend/CommandArg.cs @@ -2,7 +2,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend { using System; using System.Collections.Generic; - using System.Linq; using System.Reflection; using WallstopStudios.DxCommandTerminal.Backend.Parsers; @@ -17,11 +16,25 @@ static CommandArg() } private static readonly Lazy TryGetMethod = new(() => - typeof(CommandArg) - .GetMethods(BindingFlags.Instance | BindingFlags.Public) - .Where(method => method.Name == nameof(TryGet)) - .FirstOrDefault(method => method.GetParameters().Length == 1) - ); + { + MethodInfo[] methods = typeof(CommandArg).GetMethods( + BindingFlags.Instance | BindingFlags.Public + ); + for (int i = 0; i < methods.Length; ++i) + { + MethodInfo m = methods[i]; + if (m.Name != nameof(TryGet)) + { + continue; + } + ParameterInfo[] p = m.GetParameters(); + if (p != null && p.Length == 1) + { + return m; + } + } + return null; + }); private static readonly Dictionary RegisteredParsers = new(); private static readonly Dictionary RegisteredObjectParsers = new(); @@ -62,15 +75,14 @@ public string CleanedContents get { string cleanedString = contents; - cleanedString = IgnoredValuesForCleanedTypes.Aggregate( - cleanedString, - (current, ignoredValue) => - current.Replace( - ignoredValue, - string.Empty, - StringComparison.OrdinalIgnoreCase - ) - ); + foreach (string ignoredValue in IgnoredValuesForCleanedTypes) + { + cleanedString = cleanedString.Replace( + ignoredValue, + string.Empty, + StringComparison.OrdinalIgnoreCase + ); + } return cleanedString; } } @@ -239,13 +251,13 @@ public static int UnregisterAllObjectParsers() public static IReadOnlyCollection GetRegisteredObjectParserTypes() { // Snapshot for thread-safety and immutability to callers - return RegisteredObjectParsers.Keys.ToArray(); + return new List(RegisteredObjectParsers.Keys); } public static int DiscoverAndRegisterParsers(bool replaceExisting = false) { int added = 0; - foreach (System.Reflection.Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) + foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) { Type[] types; try @@ -254,7 +266,19 @@ public static int DiscoverAndRegisterParsers(bool replaceExisting = false) } catch (ReflectionTypeLoadException e) { - types = e.Types.Where(t => t != null).ToArray(); + List tmp = new(); + if (e.Types != null) + { + for (int i = 0; i < e.Types.Length; ++i) + { + Type t = e.Types[i]; + if (t != null) + { + tmp.Add(t); + } + } + } + types = tmp.ToArray(); } foreach (Type t in types) @@ -270,7 +294,7 @@ public static int DiscoverAndRegisterParsers(bool replaceExisting = false) IArgParser instance = null; // Prefer public static Instance singleton if available - var instProp = t.GetProperty( + PropertyInfo instProp = t.GetProperty( "Instance", BindingFlags.Public | BindingFlags.Static ); @@ -283,7 +307,7 @@ public static int DiscoverAndRegisterParsers(bool replaceExisting = false) } else { - var instField = t.GetField( + FieldInfo instField = t.GetField( "Instance", BindingFlags.Public | BindingFlags.Static ); @@ -299,7 +323,7 @@ public static int DiscoverAndRegisterParsers(bool replaceExisting = false) if (instance == null) { // Fall back to parameterless constructor - var ctor = t.GetConstructor(Type.EmptyTypes); + ConstructorInfo ctor = t.GetConstructor(Type.EmptyTypes); if (ctor != null) { instance = (IArgParser)Activator.CreateInstance(t); diff --git a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs index 79baabb..10dff22 100644 --- a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs +++ b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs @@ -2,7 +2,7 @@ namespace WallstopStudios.DxCommandTerminal.Backend { using System; using System.Collections.Generic; - using System.Linq; + using System.Text; using Extensions; public sealed class CommandAutoComplete @@ -10,6 +10,8 @@ public sealed class CommandAutoComplete private readonly SortedSet _knownWords = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _duplicateBuffer = new(StringComparer.OrdinalIgnoreCase); private readonly List _buffer = new(); + private readonly List _historyScratch = new(); + private readonly StringBuilder _sb = new(); private readonly CommandHistory _history; private readonly CommandShell _shell; @@ -22,7 +24,10 @@ public CommandAutoComplete( { _history = history ?? throw new ArgumentNullException(nameof(history)); _shell = shell ?? throw new ArgumentNullException(nameof(shell)); - _knownWords.UnionWith(commands ?? Enumerable.Empty()); + if (commands != null) + { + _knownWords.UnionWith(commands); + } } public string[] Complete(string text) @@ -91,10 +96,9 @@ public List Complete(string text, int caretIndex, List buffer) } // Special case: caret is immediately after the command name with no space. - // Treat this as requesting suggestions for the first argument. + // Treat this as requesting suggestions for the first argument, but preserve any partial text. if (!trailingWhitespace && args.Count == 0) { - partialArg = string.Empty; argIndex = 0; } @@ -111,29 +115,49 @@ public List Complete(string text, int caretIndex, List buffer) _shell ); - foreach ( - string suggestion in cmdInfo.completer.Complete(ctx) ?? Array.Empty() - ) + IEnumerable suggestions = + cmdInfo.completer.Complete(ctx) ?? Array.Empty(); + + _sb.Clear(); + _sb.Append(commandName); + if (0 < args.Count) { - if (string.IsNullOrWhiteSpace(suggestion)) + _sb.Append(' '); + for (int i = 0; i < args.Count; ++i) { - continue; + if (i > 0) + { + _sb.Append(' '); + } + _sb.Append(args[i].contents); } + } + if (argIndex >= 0) + { + _sb.Append(' '); + } + string prefixBase = _sb.ToString(); - string prefix = commandName; - if (0 < args.Count) + foreach (string suggestion in suggestions) + { + if (string.IsNullOrWhiteSpace(suggestion)) { - prefix += " " + string.Join(" ", args.Select(a => a.contents)); + continue; } - if (argIndex >= 0) + string insertion = suggestion; + bool needsQuoting = false; + if (!string.IsNullOrEmpty(insertion)) { - prefix += " "; + for (int i = 0; i < insertion.Length; ++i) + { + if (char.IsWhiteSpace(insertion[i])) + { + needsQuoting = true; + break; + } + } } - - string insertion = suggestion; - bool needsQuoting = - !string.IsNullOrEmpty(insertion) && insertion.Any(char.IsWhiteSpace); if (needsQuoting) { // Basic quoting to keep single argument with whitespace @@ -141,7 +165,10 @@ string suggestion in cmdInfo.completer.Complete(ctx) ?? Array.Empty() insertion = "\"" + insertion.Replace("\"", "\\\"") + "\""; } - string full = prefix + insertion; + _sb.Clear(); + _sb.Append(prefixBase); + _sb.Append(insertion); + string full = _sb.ToString(); string key = full.NeedsLowerInvariantConversion() ? full.ToLowerInvariant() : full; @@ -185,8 +212,11 @@ List buffer buffer.Clear(); // Commands - foreach (string command in _shell.Commands.Keys) + _historyScratch.Clear(); + _shell.CopyCommandNamesTo(_historyScratch); + for (int ci = 0; ci < _historyScratch.Count; ++ci) { + string command = _historyScratch[ci]; string known = command.NeedsLowerInvariantConversion() ? command.ToLowerInvariant() : command; @@ -214,13 +244,10 @@ List buffer } // History - foreach ( - string known in _history.GetHistory( - onlySuccess: onlySuccess, - onlyErrorFree: onlyErrorFree - ) - ) + _history.CopyHistoryTo(_historyScratch, onlySuccess, onlyErrorFree); + for (int hi = 0; hi < _historyScratch.Count; ++hi) { + string known = _historyScratch[hi]; if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) { continue; diff --git a/Runtime/CommandTerminal/Backend/CommandHistory.cs b/Runtime/CommandTerminal/Backend/CommandHistory.cs index 4471efd..89726c8 100644 --- a/Runtime/CommandTerminal/Backend/CommandHistory.cs +++ b/Runtime/CommandTerminal/Backend/CommandHistory.cs @@ -2,7 +2,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend { using System; using System.Collections.Generic; - using System.Linq; using DataStructures; public sealed class CommandHistory @@ -20,10 +19,41 @@ public CommandHistory(int capacity) public IEnumerable GetHistory(bool onlySuccess, bool onlyErrorFree) { - return _history - .Where(value => !onlySuccess || value.success == true) - .Where(value => !onlyErrorFree || value.errorFree == true) - .Select(value => value.text); + foreach ((string text, bool? success, bool? errorFree) entry in _history) + { + if (onlySuccess && entry.success != true) + { + continue; + } + if (onlyErrorFree && entry.errorFree != true) + { + continue; + } + yield return entry.text; + } + } + + public void CopyHistoryTo(List buffer, bool onlySuccess, bool onlyErrorFree) + { + if (buffer == null) + { + return; + } + buffer.Clear(); + int count = _history.Count; + for (int i = 0; i < count; ++i) + { + (string text, bool? success, bool? errorFree) entry = _history[i]; + if (onlySuccess && entry.success != true) + { + continue; + } + if (onlyErrorFree && entry.errorFree != true) + { + continue; + } + buffer.Add(entry.text); + } } public void Resize(int newCapacity) diff --git a/Runtime/CommandTerminal/Backend/CommandLog.cs b/Runtime/CommandTerminal/Backend/CommandLog.cs index 7eb11bb..e1a4917 100644 --- a/Runtime/CommandTerminal/Backend/CommandLog.cs +++ b/Runtime/CommandTerminal/Backend/CommandLog.cs @@ -3,7 +3,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend using System; using System.Collections.Concurrent; using System.Collections.Generic; - using System.Linq; using DataStructures; using UnityEngine; @@ -57,7 +56,7 @@ public CommandLog(int maxItems, IEnumerable ignoredLogTypes = n { _logs = new CyclicBuffer(maxItems); this.ignoredLogTypes = new HashSet( - ignoredLogTypes ?? Enumerable.Empty() + ignoredLogTypes ?? Array.Empty() ); } @@ -163,7 +162,16 @@ public void EnqueueUnityLog(string message, string stackTrace, TerminalLogType t public int DrainPending() { int added = 0; - while (_pending.TryDequeue(out var item)) + while ( + _pending.TryDequeue( + out ( + string message, + string stackTrace, + TerminalLogType type, + bool includeStackTrace + ) item + ) + ) { string stack = item.includeStackTrace ? GetAccurateStackTrace() : item.stackTrace; if (ignoredLogTypes.Contains(item.type)) diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index 96f8bf6..305dfa6 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -3,7 +3,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend using System; using System.Collections.Generic; using System.Collections.Immutable; - using System.Linq; using System.Reflection; using System.Text; using Attributes; @@ -22,66 +21,93 @@ RegisterCommandAttribute attribute const BindingFlags methodFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; - Assembly[] ourAssembly = { typeof(BuiltInCommands).Assembly }; - foreach ( - Type type in AppDomain - .CurrentDomain.GetAssemblies() - /* - Force our assembly to be processed last so user commands, - if they conflict with in-built ones, are always registered first. - */ - .Except(ourAssembly) - .Concat(ourAssembly) - .SelectMany(assembly => assembly.GetTypes()) + Assembly ourAsm = typeof(BuiltInCommands).Assembly; + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + // First process all but our assembly + for (int ai = 0; ai < assemblies.Length; ++ai) + { + Assembly asm = assemblies[ai]; + if (asm == ourAsm) + { + continue; + } + ProcessAssembly(asm, methodFlags, commands); + } + + // Then process our assembly last + ProcessAssembly(ourAsm, methodFlags, commands); + + return commands.ToArray(); + + static void ProcessAssembly( + Assembly assembly, + BindingFlags methodFlags, + List<(MethodInfo, RegisterCommandAttribute)> commands ) { + Type[] types; try { - foreach (MethodInfo method in type.GetMethods(methodFlags)) + types = assembly.GetTypes(); + } + catch + { + return; + } + + for (int ti = 0; ti < types.Length; ++ti) + { + Type type = types[ti]; + if (type == null) { - try + continue; + } + try + { + MethodInfo[] methods = type.GetMethods(methodFlags); + for (int mi = 0; mi < methods.Length; ++mi) { - if ( - Attribute.GetCustomAttribute( - method, - typeof(RegisterCommandAttribute) - ) - is not RegisterCommandAttribute attribute - ) + MethodInfo method = methods[mi]; + try { - continue; + if ( + Attribute.GetCustomAttribute( + method, + typeof(RegisterCommandAttribute) + ) + is not RegisterCommandAttribute attribute + ) + { + continue; + } + attribute.NormalizeName(method); + commands.Add((method, attribute)); } - - attribute.NormalizeName(method); - commands.Add((method, attribute)); - } - catch (Exception e) - { - if (ShouldIgnoreExceptionForType(type)) + catch (Exception e) { - continue; + if (ShouldIgnoreExceptionForType(type)) + { + continue; + } + Debug.LogError( + $"Failed to resolve method {method.Name} of type {type.FullName} with exception {e}" + ); } - - Debug.LogError( - $"Failed to resolve method {method.Name} of type {type.FullName} with exception {e}" - ); } } - } - catch (Exception e) - { - if (ShouldIgnoreExceptionForType(type)) + catch (Exception e) { - continue; + if (ShouldIgnoreExceptionForType(type)) + { + continue; + } + Debug.LogError( + $"Failed to resolve methods for type {type.FullName} with exception {e}" + ); } - - Debug.LogError( - $"Failed to resolve methods for type {type.FullName} with exception {e}" - ); } } - - return commands.ToArray(); }); private readonly List _arguments = new(); // Cache for performance @@ -180,7 +206,11 @@ public void InitializeAutoRegisteredCommands( IgnoringDefaultCommands = ignoreDefaultCommands; ClearAutoRegisteredCommands(); _ignoredCommands.Clear(); - _ignoredCommands.UnionWith(ignoredCommands ?? Enumerable.Empty()); + if (ignoreDefaultCommands != null) + { + _ignoredCommands.UnionWith(ignoredCommands); + } + foreach (string ignoredCommand in _ignoredCommands) { _commands.Remove(ignoredCommand); @@ -255,7 +285,9 @@ public void InitializeAutoRegisteredCommands( IssueErrorMessage( $"{command.Key} has an invalid signature. " + $"Expected: {command.Value.Name}(CommandArg[]). " - + $"Found: {command.Value.Name}({string.Join(",", command.Value.GetParameters().Select(p => p.ParameterType.Name))})" + + $"Found: {command.Value.Name}(" + + GetParameterTypeNames(command.Value) + + ")" ); } } @@ -484,6 +516,19 @@ public bool SetVariable(string name, CommandArg value) return true; } + internal void CopyCommandNamesTo(List buffer) + { + if (buffer == null) + { + return; + } + buffer.Clear(); + foreach (string key in _commands.Keys) + { + buffer.Add(key); + } + } + // ReSharper disable once UnusedMember.Global public bool TryGetVariable(string name, out CommandArg variable) { @@ -511,6 +556,33 @@ public void IssueErrorMessage(string format, params object[] parameters) _errorMessages.Enqueue(formattedMessage); } + private static string GetParameterTypeNames(MethodInfo method) + { + if (method == null) + { + return string.Empty; + } + + ParameterInfo[] parameters = method.GetParameters(); + if (parameters == null || parameters.Length == 0) + { + return string.Empty; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < parameters.Length; ++i) + { + if (i > 0) + { + sb.Append(','); + } + ParameterInfo p = parameters[i]; + Type pt = p != null ? p.ParameterType : null; + sb.Append(pt != null ? pt.Name : string.Empty); + } + return sb.ToString(); + } + public static bool TryEatArgument(ref string stringValue, out CommandArg arg) { stringValue = stringValue.TrimStart(); @@ -605,9 +677,9 @@ private static IArgumentCompleter ResolveCompleter(MethodInfo method) { object attr = Attribute.GetCustomAttribute( method, - typeof(Attributes.CommandCompleterAttribute) + typeof(CommandCompleterAttribute) ); - if (attr is not Attributes.CommandCompleterAttribute cca) + if (attr is not CommandCompleterAttribute cca) { return null; } diff --git a/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs b/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs index 53ad17b..f3df79f 100644 --- a/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs +++ b/Runtime/CommandTerminal/Backend/Completers/FontArgumentCompleter.cs @@ -2,7 +2,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend.Completers { using System; using System.Collections.Generic; - using System.Linq; using UI; public sealed class FontArgumentCompleter : IArgumentCompleter @@ -21,19 +20,48 @@ public IEnumerable Complete(CommandCompletionContext context) return Array.Empty(); } - IEnumerable names = terminal - ._fontPack._fonts.Where(f => f != null && !string.IsNullOrWhiteSpace(f.name)) - .Select(f => f.name) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(n => n, StringComparer.OrdinalIgnoreCase); + HashSet set = new(StringComparer.OrdinalIgnoreCase); + List namesList = new(); + List result = new(); + + // Collect unique names (case-insensitive) + foreach (UnityEngine.Font font in terminal._fontPack._fonts) + { + if (font == null) + { + continue; + } + + string name = font.name; + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + if (set.Add(name)) + { + namesList.Add(name); + } + } + + // Sort deterministically + namesList.Sort(StringComparer.OrdinalIgnoreCase); string partial = context.PartialArg ?? string.Empty; if (string.IsNullOrWhiteSpace(partial)) { - return names; + return namesList; + } + + for (int i = 0; i < namesList.Count; ++i) + { + string n = namesList[i]; + if (n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)) + { + result.Add(n); + } } - return names.Where(n => n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)); + return result; } } } diff --git a/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs b/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs index 74bf033..1a57adf 100644 --- a/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs +++ b/Runtime/CommandTerminal/Backend/Completers/ThemeArgumentCompleter.cs @@ -2,7 +2,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend.Completers { using System; using System.Collections.Generic; - using System.Linq; using Themes; using UI; @@ -22,20 +21,45 @@ public IEnumerable Complete(CommandCompletionContext context) return Array.Empty(); } - IEnumerable friendly = terminal - ._themePack._themeNames.Where(n => !string.IsNullOrWhiteSpace(n)) - .Select(ThemeNameHelper.GetFriendlyThemeName) - .Where(n => !string.IsNullOrWhiteSpace(n)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(n => n, StringComparer.OrdinalIgnoreCase); + HashSet set = new(StringComparer.OrdinalIgnoreCase); + List friendlyList = new(); + List result = new(); + + foreach (string raw in terminal._themePack._themeNames) + { + if (string.IsNullOrWhiteSpace(raw)) + { + continue; + } + string friendly = ThemeNameHelper.GetFriendlyThemeName(raw); + if (string.IsNullOrWhiteSpace(friendly)) + { + continue; + } + if (set.Add(friendly)) + { + friendlyList.Add(friendly); + } + } + + friendlyList.Sort(StringComparer.OrdinalIgnoreCase); string partial = context.PartialArg ?? string.Empty; if (string.IsNullOrWhiteSpace(partial)) { - return friendly; + return friendlyList; + } + + for (int i = 0; i < friendlyList.Count; ++i) + { + string n = friendlyList[i]; + if (n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)) + { + result.Add(n); + } } - return friendly.Where(n => n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)); + return result; } } } diff --git a/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs b/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs index ded08a4..d7da602 100644 --- a/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs +++ b/Runtime/CommandTerminal/Backend/Parsers/EnumArgParser.cs @@ -2,7 +2,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend.Parsers { using System; using System.Collections.Generic; - using System.Linq; public static class EnumArgParser { @@ -20,12 +19,13 @@ public static bool TryParse(Type enumType, string input, out object value) // Fast name map (case-insensitive) if (!CachedNames.TryGetValue(enumType, out Dictionary nameMap)) { - nameMap = Enum.GetNames(enumType) - .ToDictionary( - n => n, - n => (object)Enum.Parse(enumType, n), - StringComparer.OrdinalIgnoreCase - ); + nameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + string[] names = Enum.GetNames(enumType); + for (int i = 0; i < names.Length; ++i) + { + string n = names[i]; + nameMap[n] = Enum.Parse(enumType, n); + } CachedNames[enumType] = nameMap; } diff --git a/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs b/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs index 565a004..1de1430 100644 --- a/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs +++ b/Runtime/CommandTerminal/Backend/Parsers/StaticMemberParser.cs @@ -2,7 +2,6 @@ namespace WallstopStudios.DxCommandTerminal.Backend.Parsers { using System; using System.Collections.Generic; - using System.Linq; using System.Reflection; public static class StaticMemberParser @@ -19,13 +18,27 @@ private static void EnsureInitialized() } Type type = typeof(T); - Properties = type.GetProperties(BindingFlags.Static | BindingFlags.Public) - .Where(p => p.PropertyType == type) - .ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase); + Properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + PropertyInfo[] props = type.GetProperties(BindingFlags.Static | BindingFlags.Public); + for (int i = 0; i < props.Length; ++i) + { + PropertyInfo p = props[i]; + if (p != null && p.PropertyType == type) + { + Properties[p.Name] = p; + } + } - Fields = type.GetFields(BindingFlags.Static | BindingFlags.Public) - .Where(f => f.FieldType == type) - .ToDictionary(f => f.Name, f => f, StringComparer.OrdinalIgnoreCase); + Fields = new Dictionary(StringComparer.OrdinalIgnoreCase); + FieldInfo[] fields = type.GetFields(BindingFlags.Static | BindingFlags.Public); + for (int i = 0; i < fields.Length; ++i) + { + FieldInfo f = fields[i]; + if (f != null && f.FieldType == type) + { + Fields[f.Name] = f; + } + } initialized = true; } diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index 5d2e910..cbe9a3e 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -2,21 +2,34 @@ namespace WallstopStudios.DxCommandTerminal.Input { using System; using System.Collections.Generic; - using System.Linq; using UI; using UnityEngine; [DisallowMultipleComponent] public class TerminalKeyboardController : MonoBehaviour, IInputHandler { - protected static readonly TerminalControlTypes[] ControlTypes = Enum.GetValues( - typeof(TerminalControlTypes) - ) - .OfType() + protected static readonly TerminalControlTypes[] ControlTypes = BuildControlTypes(); + + private static TerminalControlTypes[] BuildControlTypes() + { + Array values = Enum.GetValues(typeof(TerminalControlTypes)); + List list = new(); + for (int i = 0; i < values.Length; ++i) + { + object v = values.GetValue(i); + if (v is TerminalControlTypes t) + { #pragma warning disable CS0612 // Type or member is obsolete - .Except(new[] { TerminalControlTypes.None }) + if (t == TerminalControlTypes.None) + { + continue; + } #pragma warning restore CS0612 // Type or member is obsolete - .ToArray(); + list.Add(t); + } + } + return list.ToArray(); + } public bool ShouldHandleInputThisFrame { @@ -24,11 +37,7 @@ public bool ShouldHandleInputThisFrame { foreach (TerminalControlTypes controlType in _controlOrder) { - if (!_inputChecks.TryGetValue(controlType, out Func inputCheck)) - { - continue; - } - if (inputCheck()) + if (IsControlPressed(controlType)) { return true; } @@ -86,31 +95,7 @@ public bool ShouldHandleInputThisFrame TerminalControlTypes.CompleteForward, }; - protected readonly Dictionary> _inputChecks = new(); - protected readonly Dictionary _controlHandlerActions = new(); - - public TerminalKeyboardController() - { - _inputChecks.Clear(); - _inputChecks[TerminalControlTypes.Close] = IsClosePressed; - _inputChecks[TerminalControlTypes.EnterCommand] = IsEnterCommandPressed; - _inputChecks[TerminalControlTypes.Previous] = IsPreviousPressed; - _inputChecks[TerminalControlTypes.Next] = IsNextPressed; - _inputChecks[TerminalControlTypes.ToggleFull] = IsToggleFullPressed; - _inputChecks[TerminalControlTypes.ToggleSmall] = IsToggleSmallPressed; - _inputChecks[TerminalControlTypes.CompleteBackward] = IsCompleteBackwardPressed; - _inputChecks[TerminalControlTypes.CompleteForward] = IsCompletePressed; - - _controlHandlerActions.Clear(); - _controlHandlerActions[TerminalControlTypes.Close] = Close; - _controlHandlerActions[TerminalControlTypes.EnterCommand] = EnterCommand; - _controlHandlerActions[TerminalControlTypes.Previous] = Previous; - _controlHandlerActions[TerminalControlTypes.Next] = Next; - _controlHandlerActions[TerminalControlTypes.ToggleFull] = ToggleFull; - _controlHandlerActions[TerminalControlTypes.ToggleSmall] = ToggleSmall; - _controlHandlerActions[TerminalControlTypes.CompleteBackward] = CompleteBackward; - _controlHandlerActions[TerminalControlTypes.CompleteForward] = Complete; - } + public TerminalKeyboardController() { } protected virtual void Awake() { @@ -144,10 +129,36 @@ protected virtual void OnValidate() private void VerifyControlOrderIntegrity() { - if (!_controlOrder.ToHashSet().SetEquals(ControlTypes)) + // Verify set equality without LINQ + HashSet set = new HashSet(_controlOrder); + bool equal = set.Count == ControlTypes.Length; + if (equal) + { + for (int i = 0; i < ControlTypes.Length; ++i) + { + if (!set.Contains(ControlTypes[i])) + { + equal = false; + break; + } + } + } + + if (!equal) { + // Build missing list for message + List missing = new List(); + for (int i = 0; i < ControlTypes.Length; ++i) + { + TerminalControlTypes t = ControlTypes[i]; + if (!set.Contains(t)) + { + missing.Add(t.ToString()); + } + } + Debug.LogWarning( - $"Control Order is missing the following controls: [{string.Join(", ", ControlTypes.Except(_controlOrder))}]. " + $"Control Order is missing the following controls: [{string.Join(", ", missing)}]. " + "Input for these will not be handled. Is this intentional?", this ); @@ -163,22 +174,12 @@ protected virtual void Update() foreach (TerminalControlTypes controlType in _controlOrder) { - if (!_inputChecks.TryGetValue(controlType, out Func inputCheck)) - { - continue; - } - - if (!inputCheck()) + if (!IsControlPressed(controlType)) { continue; } - if (!_controlHandlerActions.TryGetValue(controlType, out Action action)) - { - continue; - } - - action(); + ExecuteControl(controlType); break; } } @@ -318,5 +319,61 @@ protected virtual bool IsEnterCommandPressed() } #endregion + + private bool IsControlPressed(TerminalControlTypes controlType) + { + switch (controlType) + { + case TerminalControlTypes.Close: + return IsClosePressed(); + case TerminalControlTypes.EnterCommand: + return IsEnterCommandPressed(); + case TerminalControlTypes.Previous: + return IsPreviousPressed(); + case TerminalControlTypes.Next: + return IsNextPressed(); + case TerminalControlTypes.ToggleFull: + return IsToggleFullPressed(); + case TerminalControlTypes.ToggleSmall: + return IsToggleSmallPressed(); + case TerminalControlTypes.CompleteBackward: + return IsCompleteBackwardPressed(); + case TerminalControlTypes.CompleteForward: + return IsCompletePressed(); + default: + return false; + } + } + + private void ExecuteControl(TerminalControlTypes controlType) + { + switch (controlType) + { + case TerminalControlTypes.Close: + Close(); + break; + case TerminalControlTypes.EnterCommand: + EnterCommand(); + break; + case TerminalControlTypes.Previous: + Previous(); + break; + case TerminalControlTypes.Next: + Next(); + break; + case TerminalControlTypes.ToggleFull: + ToggleFull(); + break; + case TerminalControlTypes.ToggleSmall: + ToggleSmall(); + break; + case TerminalControlTypes.CompleteBackward: + CompleteBackward(); + break; + case TerminalControlTypes.CompleteForward: + Complete(); + break; + } + } } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index dcf2d8b..c58ca3b 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -7,7 +7,6 @@ namespace WallstopStudios.DxCommandTerminal.UI using System; using System.Collections.Generic; using System.ComponentModel; - using System.Linq; using Attributes; using Backend; using Extensions; @@ -182,9 +181,7 @@ private enum ScrollBarCaptureState )] [SerializeField] #pragma warning disable CS0618 // Type or member is obsolete - private Backend.TerminalRuntimeModeFlags _runtimeModes = Backend - .TerminalRuntimeModeFlags - .None; + private TerminalRuntimeModeFlags _runtimeModes = TerminalRuntimeModeFlags.None; #pragma warning restore CS0618 // Type or member is obsolete // Test helper to skip building UI entirely (prevents UI Toolkit panel updates) @@ -249,11 +246,11 @@ public TerminalUI() private void Awake() { - Backend.TerminalRuntimeConfig.SetMode(_runtimeModes); + TerminalRuntimeConfig.SetMode(_runtimeModes); #if UNITY_EDITOR - Backend.TerminalRuntimeConfig.EditorAutoDiscover = _autoDiscoverParsersInEditor; + TerminalRuntimeConfig.EditorAutoDiscover = _autoDiscoverParsersInEditor; #endif - Backend.TerminalRuntimeConfig.TryAutoDiscoverParsers(); + TerminalRuntimeConfig.TryAutoDiscoverParsers(); switch (_logBufferSize) { case <= 0: @@ -347,13 +344,13 @@ void TrackProperties(string[] properties, List storage) switch (value) { case List stringList: - value = stringList.ToList(); + value = new List(stringList); break; case List logTypeList: - value = logTypeList.ToList(); + value = new List(logTypeList); break; case List fontList: - value = fontList.ToList(); + value = new List(fontList); break; } _propertyValues[property.name] = value; @@ -455,16 +452,10 @@ private void RefreshStaticState(bool force) { Terminal.Buffer.Resize(logBufferSize); } - if ( - !Terminal.Buffer.ignoredLogTypes.SetEquals( - _ignoredLogTypes ?? Enumerable.Empty() - ) - ) + if (!Terminal.Buffer.ignoredLogTypes.SetEquals(_ignoredLogTypes)) { Terminal.Buffer.ignoredLogTypes.Clear(); - Terminal.Buffer.ignoredLogTypes.UnionWith( - _ignoredLogTypes ?? Enumerable.Empty() - ); + Terminal.Buffer.ignoredLogTypes.UnionWith(_ignoredLogTypes); } } @@ -491,9 +482,7 @@ private void RefreshStaticState(bool force) if ( Terminal.Shell.IgnoringDefaultCommands != ignoreDefaultCommands || Terminal.Shell.Commands.Count <= 0 - || !Terminal.Shell.IgnoredCommands.SetEquals( - _disabledCommands ?? Enumerable.Empty() - ) + || !Terminal.Shell.IgnoredCommands.SetEquals(_disabledCommands) ) { Terminal.Shell.ClearAutoRegisteredCommands(); @@ -597,10 +586,10 @@ propertyValue is List currentStringList && previousValue is List previousStringList ) { - if (!currentStringList.SequenceEqual(previousStringList)) + if (!ListsEqual(currentStringList, previousStringList)) { needRefresh = true; - _propertyValues[property.name] = currentStringList.ToList(); + _propertyValues[property.name] = new List(currentStringList); } continue; @@ -610,10 +599,12 @@ propertyValue is List currentLogTypeList && previousValue is List previousLogTypeList ) { - if (!currentLogTypeList.SequenceEqual(previousLogTypeList)) + if (!ListsEqual(currentLogTypeList, previousLogTypeList)) { needRefresh = true; - _propertyValues[property.name] = currentLogTypeList.ToList(); + _propertyValues[property.name] = new List( + currentLogTypeList + ); } continue; @@ -654,6 +645,32 @@ public void SetState(TerminalState newState) } } + private static bool ListsEqual(List a, List b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + if (a is null || b is null) + { + return false; + } + int count = a.Count; + if (count != b.Count) + { + return false; + } + EqualityComparer cmp = EqualityComparer.Default; + for (int i = 0; i < count; ++i) + { + if (!cmp.Equals(a[i], b[i])) + { + return false; + } + } + return true; + } + private static void ConsumeAndLogErrors() { while (Terminal.Shell?.TryConsumeErrorMessage(out string error) == true) @@ -880,7 +897,7 @@ private void SetupUI() string prev = evt.previousValue ?? string.Empty; string curr = evt.newValue ?? string.Empty; bool justTypedSpace = curr.EndsWith(" ") && curr.Length == prev.Length + 1; - if (justTypedSpace && Backend.Terminal.Shell != null) + if (justTypedSpace && Terminal.Shell != null) { string check = curr; // Remove trailing space(s) to isolate the command token @@ -889,14 +906,9 @@ private void SetupUI() check = check.TrimEnd(); } - if ( - Backend.CommandShell.TryEatArgument( - ref check, - out Backend.CommandArg cmd - ) - ) + if (CommandShell.TryEatArgument(ref check, out CommandArg cmd)) { - if (Backend.Terminal.Shell.Commands.ContainsKey(cmd.contents)) + if (Terminal.Shell.Commands.ContainsKey(cmd.contents)) { // Clear existing suggestions immediately context._lastCompletionIndex = null; @@ -986,18 +998,46 @@ private void InitializeTheme(VisualElement root) if (themeNames is { Count: > 0 }) { - _runtimeTheme = themeNames.FirstOrDefault(theme => - theme.Contains("dark", StringComparison.OrdinalIgnoreCase) - ); - if (_runtimeTheme == null) + string runtimeTheme = null; + foreach (string themeName in themeNames) { - _runtimeTheme = themeNames.FirstOrDefault(theme => - theme.Contains("light", StringComparison.OrdinalIgnoreCase) - ); + if ( + themeName != null + && themeName.Contains("dark", StringComparison.OrdinalIgnoreCase) + ) + { + runtimeTheme = themeName; + break; + } } + + _runtimeTheme = runtimeTheme; if (_runtimeTheme == null) { - _runtimeTheme = themeNames.FirstOrDefault(); + foreach (string themeName in themeNames) + { + if ( + themeName != null + && themeName.Contains("light", StringComparison.OrdinalIgnoreCase) + ) + { + runtimeTheme = themeName; + break; + } + } + + _runtimeTheme = runtimeTheme; + } + if (_runtimeTheme == null && themeNames.Count > 0) + { + foreach (string themeName in themeNames) + { + if (themeName != null) + { + _runtimeTheme = themeName; + break; + } + } } Debug.LogWarning($"Persisted theme not found, defaulting to '{_runtimeTheme}'."); } @@ -1008,10 +1048,7 @@ private void InitializeTheme(VisualElement root) } // Support method for tests and tooling to inject theme/font packs before enabling - public void InjectPacks( - Themes.TerminalThemePack themePack, - Themes.TerminalFontPack fontPack - ) + public void InjectPacks(TerminalThemePack themePack, TerminalFontPack fontPack) { _themePack = themePack; _fontPack = fontPack; @@ -1034,25 +1071,65 @@ private void InitializeFont() List loadedFonts = _fontPack._fonts; if (loadedFonts is { Count: > 0 }) { - _runtimeFont = loadedFonts.FirstOrDefault(font => - font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) - && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) - ); - if (_runtimeFont == null) + Font runtimeFont = null; + foreach (Font font in loadedFonts) { - _runtimeFont = loadedFonts.FirstOrDefault(font => - font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) - ); + if ( + font != null + && font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) + && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) + ) + { + runtimeFont = font; + break; + } } + + _runtimeFont = runtimeFont; if (_runtimeFont == null) { - _runtimeFont = loadedFonts.FirstOrDefault(font => - font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) - ); + foreach (Font font in loadedFonts) + { + if ( + font != null + && font.name.Contains("Mono", StringComparison.OrdinalIgnoreCase) + ) + { + runtimeFont = font; + break; + } + } + + _runtimeFont = runtimeFont; } if (_runtimeFont == null) { - _runtimeFont = loadedFonts.FirstOrDefault(); + foreach (Font font in loadedFonts) + { + if ( + font != null + && font.name.Contains("Regular", StringComparison.OrdinalIgnoreCase) + ) + { + runtimeFont = font; + break; + } + } + + _runtimeFont = runtimeFont; + } + if (_runtimeFont == null && loadedFonts.Count > 0) + { + foreach (Font font in loadedFonts) + { + if (font != null) + { + runtimeFont = font; + break; + } + } + + _runtimeFont = runtimeFont; } } @@ -1919,18 +1996,30 @@ bool IsValidTheme(out string validTheme) } List themeNames = _themePack._themeNames; - if (themeNames.Contains(theme, StringComparer.OrdinalIgnoreCase)) + foreach (string themeName in themeNames) { - validTheme = theme; - return true; + if (string.Equals(themeName, theme, StringComparison.OrdinalIgnoreCase)) + { + validTheme = themeName; + return true; + } } foreach (string themeName in ThemeNameHelper.GetPossibleThemeNames(theme)) { - if (themeNames.Contains(themeName, StringComparer.OrdinalIgnoreCase)) + foreach (string existingThemeName in themeNames) { - validTheme = themeName; - return true; + if ( + string.Equals( + existingThemeName, + themeName, + StringComparison.OrdinalIgnoreCase + ) + ) + { + validTheme = existingThemeName; + return true; + } } } @@ -1963,14 +2052,17 @@ void SetRuntimeTheme() return; } - string[] loadedThemes = terminalRoot - .GetClasses() - .Where(ThemeNameHelper.IsThemeName) - .ToArray(); - - foreach (string loadedTheme in loadedThemes) + List loadedThemes = new List(); + foreach (string cls in terminalRoot.GetClasses()) + { + if (ThemeNameHelper.IsThemeName(cls)) + { + loadedThemes.Add(cls); + } + } + for (int i = 0; i < loadedThemes.Count; ++i) { - terminalRoot.RemoveFromClassList(loadedTheme); + terminalRoot.RemoveFromClassList(loadedThemes[i]); } terminalRoot.AddToClassList(validatedTheme); diff --git a/Tests/Runtime/AutocompleteTests.cs b/Tests/Runtime/AutocompleteTests.cs new file mode 100644 index 0000000..71a1634 --- /dev/null +++ b/Tests/Runtime/AutocompleteTests.cs @@ -0,0 +1,228 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections.Generic; + using Backend; + using NUnit.Framework; + using WallstopStudios.DxCommandTerminal.Attributes; + + internal sealed class DummyCompleter : IArgumentCompleter + { + public IEnumerable Complete(CommandCompletionContext context) + { + // Return suggestions with and without whitespace + yield return "foo bar"; + yield return "baz"; + } + } + + internal sealed class RecordingCompleter : IArgumentCompleter + { + private readonly Func> _handler; + + public RecordingCompleter(Func> handler) + { + _handler = handler; + } + + public List Calls { get; } = new List(); + + public IEnumerable Complete(CommandCompletionContext context) + { + Calls.Add(context); + if (_handler != null) + { + IEnumerable results = _handler(context); + if (results != null) + { + return results; + } + } + + return Array.Empty(); + } + } + + internal sealed class ChainedCompleter : IArgumentCompleter + { + public List Calls { get; } = new List(); + + public IEnumerable Complete(CommandCompletionContext context) + { + Calls.Add(context); + if (context.ArgIndex == 0) + { + return new[] { "alpha", "beta" }; + } + + if (context.ArgIndex == 1) + { + if ( + context.ArgsBeforeCursor != null + && 0 < context.ArgsBeforeCursor.Count + && context.ArgsBeforeCursor[0].contents == "alpha" + ) + { + return new[] { "gamma" }; + } + + return new[] { "delta" }; + } + + return Array.Empty(); + } + } + + internal sealed class AutoRegisteredCompleter : IArgumentCompleter + { + public static AutoRegisteredCompleter Instance { get; } = new AutoRegisteredCompleter(); + + private AutoRegisteredCompleter() { } + + public IEnumerable Complete(CommandCompletionContext context) + { + return new[] { "auto" }; + } + } + + internal static class AutoRegistrationFixture + { + public const string CommandName = "autoFixture"; + + [RegisterCommand(Name = CommandName)] + [CommandCompleter(typeof(AutoRegisteredCompleter))] + public static void AutoFixture(CommandArg[] args) { } + } + + public sealed class AutocompleteTests + { + [Test] + public void CompleterProducesQuotedSuggestions() + { + CommandHistory history = new CommandHistory(8); + CommandShell shell = new CommandShell(history); + shell.AddCommand("testcmd", _ => { }, 0, -1, string.Empty, null, new DummyCompleter()); + + CommandAutoComplete ac = new CommandAutoComplete(history, shell); + string[] results = ac.Complete("testcmd "); + + // Expect both suggestions formatted for insertion + Assert.IsNotNull(results); + CollectionAssert.Contains(results, "testcmd \"foo bar\""); + CollectionAssert.Contains(results, "testcmd baz"); + } + + [Test] + public void ManualAddCommandExposesCompleter() + { + CommandHistory history = new CommandHistory(8); + CommandShell shell = new CommandShell(history); + RecordingCompleter recordingCompleter = new RecordingCompleter(_ => + Array.Empty() + ); + + bool added = shell.AddCommand( + "manual", + _ => { }, + 0, + -1, + string.Empty, + null, + recordingCompleter + ); + + Assert.IsTrue(added); + Assert.IsTrue(shell.Commands.TryGetValue("manual", out CommandInfo info)); + Assert.AreSame(recordingCompleter, info.completer); + } + + [Test] + public void AddCommandWithCommandInfoPreservesCompleter() + { + CommandHistory history = new CommandHistory(8); + CommandShell shell = new CommandShell(history); + RecordingCompleter recordingCompleter = new RecordingCompleter(_ => + Array.Empty() + ); + + CommandInfo info = new CommandInfo(_ => { }, 0, 1, "help", "hint", recordingCompleter); + + bool added = shell.AddCommand("infoCommand", info); + + Assert.IsTrue(added); + Assert.IsTrue(shell.Commands.TryGetValue("infoCommand", out CommandInfo stored)); + Assert.AreSame(recordingCompleter, stored.completer); + } + + [Test] + public void AutoRegisteredCommandReceivesCompleter() + { + CommandHistory history = new CommandHistory(8); + CommandShell shell = new CommandShell(history); + + shell.InitializeAutoRegisteredCommands(); + + Assert.IsTrue( + shell.Commands.TryGetValue( + AutoRegistrationFixture.CommandName, + out CommandInfo info + ) + ); + Assert.IsInstanceOf(info.completer); + Assert.AreSame(AutoRegisteredCompleter.Instance, info.completer); + } + + [Test] + public void AutoCompleteChainsArguments() + { + CommandHistory history = new CommandHistory(16); + CommandShell shell = new CommandShell(history); + ChainedCompleter chainedCompleter = new ChainedCompleter(); + shell.AddCommand("chain", _ => { }, 0, -1, string.Empty, null, chainedCompleter); + + CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell); + + string[] commandSuggestions = autoComplete.Complete("cha"); + CollectionAssert.Contains(commandSuggestions, "chain"); + + string[] firstArgumentSuggestions = autoComplete.Complete("chain "); + CollectionAssert.AreEquivalent( + new[] { "chain alpha", "chain beta" }, + firstArgumentSuggestions + ); + Assert.AreEqual(1, chainedCompleter.Calls.Count); + CommandCompletionContext firstContext = chainedCompleter.Calls[0]; + Assert.AreEqual(0, firstContext.ArgIndex); + Assert.AreEqual(string.Empty, firstContext.PartialArg); + Assert.AreEqual(0, firstContext.ArgsBeforeCursor.Count); + + string[] secondArgumentSuggestions = autoComplete.Complete("chain alpha "); + CollectionAssert.Contains(secondArgumentSuggestions, "chain alpha gamma"); + Assert.AreEqual(2, chainedCompleter.Calls.Count); + CommandCompletionContext secondContext = chainedCompleter.Calls[1]; + Assert.AreEqual(1, secondContext.ArgIndex); + Assert.AreEqual(string.Empty, secondContext.PartialArg); + Assert.AreEqual(1, secondContext.ArgsBeforeCursor.Count); + Assert.AreEqual("alpha", secondContext.ArgsBeforeCursor[0].contents); + + chainedCompleter.Calls.Clear(); + string[] partialFirstArgument = autoComplete.Complete("chain a"); + CollectionAssert.Contains(partialFirstArgument, "chain alpha"); + Assert.AreEqual(1, chainedCompleter.Calls.Count); + CommandCompletionContext partialFirstContext = chainedCompleter.Calls[0]; + Assert.AreEqual(0, partialFirstContext.ArgIndex); + Assert.AreEqual("a", partialFirstContext.PartialArg); + Assert.AreEqual(0, partialFirstContext.ArgsBeforeCursor.Count); + + chainedCompleter.Calls.Clear(); + string[] partialSecondArgument = autoComplete.Complete("chain alpha g"); + CollectionAssert.Contains(partialSecondArgument, "chain alpha gamma"); + Assert.AreEqual(1, chainedCompleter.Calls.Count); + CommandCompletionContext partialSecondContext = chainedCompleter.Calls[0]; + Assert.AreEqual(1, partialSecondContext.ArgIndex); + Assert.AreEqual("g", partialSecondContext.PartialArg); + Assert.AreEqual(1, partialSecondContext.ArgsBeforeCursor.Count); + Assert.AreEqual("alpha", partialSecondContext.ArgsBeforeCursor[0].contents); + } + } +} diff --git a/Tests/Runtime/AutocompleteTests.cs.meta b/Tests/Runtime/AutocompleteTests.cs.meta new file mode 100644 index 0000000..1521c81 --- /dev/null +++ b/Tests/Runtime/AutocompleteTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ee7e007476a7bc4d9a4da3b4a50b64a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/CommandHistoryTests.cs b/Tests/Runtime/CommandHistoryTests.cs new file mode 100644 index 0000000..29dcec3 --- /dev/null +++ b/Tests/Runtime/CommandHistoryTests.cs @@ -0,0 +1,41 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + + public sealed class CommandHistoryTests + { + [Test] + public void FiltersAndOrderWork() + { + CommandHistory history = new CommandHistory(10); + + history.Push("a ok", true, true); + history.Push("b fail", false, false); + history.Push("c ok but error", true, false); + history.Push("d ok", true, true); + + // onlySuccess + System.Collections.Generic.List onlySuccess = + new System.Collections.Generic.List(history.GetHistory(true, false)); + Assert.AreEqual(3, onlySuccess.Count); + Assert.AreEqual("a ok", onlySuccess[0]); + Assert.AreEqual("c ok but error", onlySuccess[1]); + Assert.AreEqual("d ok", onlySuccess[2]); + + // onlyErrorFree + System.Collections.Generic.List onlyErrorFree = + new System.Collections.Generic.List(history.GetHistory(false, true)); + Assert.AreEqual(2, onlyErrorFree.Count); + Assert.AreEqual("a ok", onlyErrorFree[0]); + Assert.AreEqual("d ok", onlyErrorFree[1]); + + // both filters + System.Collections.Generic.List both = + new System.Collections.Generic.List(history.GetHistory(true, true)); + Assert.AreEqual(2, both.Count); + Assert.AreEqual("a ok", both[0]); + Assert.AreEqual("d ok", both[1]); + } + } +} diff --git a/Tests/Runtime/CommandHistoryTests.cs.meta b/Tests/Runtime/CommandHistoryTests.cs.meta new file mode 100644 index 0000000..f89816a --- /dev/null +++ b/Tests/Runtime/CommandHistoryTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18d97c42d25daf54689ff5fcbfda5337 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/CompletersTests.cs b/Tests/Runtime/CompletersTests.cs new file mode 100644 index 0000000..43d019a --- /dev/null +++ b/Tests/Runtime/CompletersTests.cs @@ -0,0 +1,86 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using System.Collections.Generic; + using Backend; + using Backend.Completers; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + using UnityEngine.UIElements; + + public sealed class CompletersTests + { + [UnityTest] + public IEnumerator ThemeCompleterReturnsDistinctSortedAndFiltered() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + // Replace packs with custom contents + var themePack = ScriptableObject.CreateInstance(); + var style = ScriptableObject.CreateInstance(); + themePack.Add(style, "theme-Alpha"); + themePack.Add(style, "beta-theme"); + themePack.Add(style, "Gamma"); + TerminalUI.Instance.InjectPacks( + themePack, + ScriptableObject.CreateInstance() + ); + + ThemeArgumentCompleter completer = new ThemeArgumentCompleter(); + var ctx = new CommandCompletionContext( + "set-theme ", + "set-theme", + new List(), + "b", + 0, + Terminal.Shell + ); + + List results = new List(completer.Complete(ctx)); + // Friendly names should be alpha/beta/gamma, filtering by 'b' -> beta only + Assert.AreEqual(1, results.Count); + Assert.AreEqual("beta", results[0]); + } + + [UnityTest] + public IEnumerator FontCompleterHandlesDuplicatesAndFiltering() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + var fontPack = ScriptableObject.CreateInstance(); + // Create test fonts with names (Font is a UnityEngine.Object, not a ScriptableObject) + var f1 = new Font(); + f1.name = "Consolas"; + var f2 = new Font(); + f2.name = "Cousine"; + var f3 = new Font(); + f3.name = "consolas"; // duplicate name differing by case + + fontPack.Add(f1); + fontPack.Add(f2); + fontPack.Add(f3); + + TerminalUI.Instance.InjectPacks( + ScriptableObject.CreateInstance(), + fontPack + ); + + FontArgumentCompleter completer = new FontArgumentCompleter(); + var ctx = new CommandCompletionContext( + "set-font ", + "set-font", + new List(), + "co", + 0, + Terminal.Shell + ); + + List results = new List(completer.Complete(ctx)); + // Distinct should collapse 'Consolas' duplicates; filter 'co' -> Consolas and Cousine + CollectionAssert.AreEquivalent(new[] { "Consolas", "Cousine" }, results); + } + } +} diff --git a/Tests/Runtime/CompletersTests.cs.meta b/Tests/Runtime/CompletersTests.cs.meta new file mode 100644 index 0000000..b17712f --- /dev/null +++ b/Tests/Runtime/CompletersTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aff028a804a7cf14eaaf5d5403aaeaef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/ParserTests.cs b/Tests/Runtime/ParserTests.cs new file mode 100644 index 0000000..321fe7b --- /dev/null +++ b/Tests/Runtime/ParserTests.cs @@ -0,0 +1,51 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using Backend.Parsers; + using NUnit.Framework; + + public sealed class ParserTests + { + private sealed class StaticLike + { + public static StaticLike Alpha = new StaticLike(1); + public static StaticLike Beta = new StaticLike(2); + + public int Value { get; } + + private StaticLike(int value) + { + Value = value; + } + } + + private enum SampleEnum + { + Zero, + One, + Two, + } + + [Test] + public void StaticMemberParserFindsFields() + { + Assert.IsTrue(StaticMemberParser.TryParse("Alpha", out var a)); + Assert.IsNotNull(a); + Assert.AreEqual(1, a.Value); + + Assert.IsTrue(StaticMemberParser.TryParse("Beta", out var b)); + Assert.IsNotNull(b); + Assert.AreEqual(2, b.Value); + } + + [Test] + public void EnumArgParserParsesNamesAndOrdinals() + { + Assert.IsTrue(EnumArgParser.TryParse(typeof(SampleEnum), "One", out object nameVal)); + Assert.AreEqual(SampleEnum.One, nameVal); + + Assert.IsTrue(EnumArgParser.TryParse(typeof(SampleEnum), "2", out object ordVal)); + Assert.AreEqual(SampleEnum.Two, ordVal); + } + } +} diff --git a/Tests/Runtime/ParserTests.cs.meta b/Tests/Runtime/ParserTests.cs.meta new file mode 100644 index 0000000..3640ff7 --- /dev/null +++ b/Tests/Runtime/ParserTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c29d33e9aa7bf440957df65b3b9638b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index ac4f5b6..bdee64a 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -3,7 +3,6 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime using System.Collections; using System.Collections.Generic; using System.Linq; - using System.Reflection; using Backend; using Components; using NUnit.Framework; diff --git a/linq_hits.txt b/linq_hits.txt new file mode 100644 index 0000000..24e143a --- /dev/null +++ b/linq_hits.txt @@ -0,0 +1,122 @@ +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalThemePackEditor.cs:7:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalThemePackEditor.cs:70:if (!TerminalThemeStyleSheetHelper.GetAvailableThemes(theme).Any()) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalThemePackEditor.cs:82:|| _invalidStyles.Any() +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalThemePackEditor.cs:197:&& TerminalThemeStyleSheetHelper.GetAvailableThemes(styleSheet).Any() +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:8:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:210:.RegisteredCommands.Value.Select(tuple => tuple.attribute) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:211:.Select(attribute => attribute.Name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:216:.RegisteredCommands.Value.Select(tuple => tuple.attribute) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:217:.Where(tuple => tuple.Default) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:218:.Select(attribute => attribute.Name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:223:.RegisteredCommands.Value.Select(tuple => tuple.attribute) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:224:.Where(tuple => !tuple.Default) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:225:.Select(attribute => attribute.Name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:271:directories.ToArray() +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:288:return ordered.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:313:if (!_fontPacks.Any()) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:319:if (!_themePacks.Any()) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:400:return _fontsByPrefix.ToArray()[_fontKey].Value.ToArray()[_secondFontKey].Value; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:477:.RegisteredCommands.Value.Select(tuple => tuple.attribute) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:478:.Select(attribute => attribute.Name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:483:.RegisteredCommands.Value.Select(tuple => tuple.attribute) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:484:.Where(tuple => tuple.Default) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:485:.Select(attribute => attribute.Name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:490:.RegisteredCommands.Value.Select(tuple => tuple.attribute) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:491:.Where(tuple => !tuple.Default) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:492:.Select(attribute => attribute.Name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:736:if (!_themePacks.Any()) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:754:_themePacks.Select(themePack => themePack.name).ToArray() +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:788:if (!_fontPacks.Any()) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:806:_fontPacks.Select(fontPack => fontPack.name).ToArray() +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:862:._themePack._themeNames.Select(theme => +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:875:.ToArray() +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:991:panelSettingGuids = AssetDatabase.FindAssets("t:PanelSettings", directories.ToArray()); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:1046:string[] ignorableCommands = _intermediateResults.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:1292:string[] fontKeys = _fontsByPrefix.Keys.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\CustomEditors\TerminalUIEditor.cs:1306:string[] secondFontKeys = availableFonts.Keys.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Helper\TerminalThemeStyleSheetHelper.cs:7:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Helper\TerminalThemeStyleSheetHelper.cs:146:return selectors.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Utils\ScriptableObjectSingletonCreator.cs:7:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Utils\ScriptableObjectSingletonCreator.cs:69:types = ex.Types.Where(x => x != null).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Utils\ScriptableObjectSingletonCreator.cs:83:.GroupBy(t => t.Name, StringComparer.OrdinalIgnoreCase) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Utils\ScriptableObjectSingletonCreator.cs:84:.Where(g => g.Count() > 1) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Utils\ScriptableObjectSingletonCreator.cs:85:.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Editor\Utils\ScriptableObjectSingletonCreator.cs:92:$"ScriptableObjectSingletonCreator: Multiple types share the name '{type.Name}'. Skipping auto-creation. Add [ScriptableSingletonPath] to disambiguate or rename. Types: {string.Join(", ", collisions[type.Name].Select(x => x.FullName))}" +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\FontArgumentCompleter.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\FontArgumentCompleter.cs:25:._fontPack._fonts.Where(f => f != null && !string.IsNullOrWhiteSpace(f.name)) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\FontArgumentCompleter.cs:26:.Select(f => f.name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\FontArgumentCompleter.cs:27:.Distinct(StringComparer.OrdinalIgnoreCase) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\FontArgumentCompleter.cs:28:.OrderBy(n => n, StringComparer.OrdinalIgnoreCase); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\FontArgumentCompleter.cs:36:return names.Where(n => n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\ThemeArgumentCompleter.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\ThemeArgumentCompleter.cs:26:._themePack._themeNames.Where(n => !string.IsNullOrWhiteSpace(n)) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\ThemeArgumentCompleter.cs:27:.Select(ThemeNameHelper.GetFriendlyThemeName) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\ThemeArgumentCompleter.cs:28:.Where(n => !string.IsNullOrWhiteSpace(n)) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\ThemeArgumentCompleter.cs:29:.Distinct(StringComparer.OrdinalIgnoreCase) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\ThemeArgumentCompleter.cs:30:.OrderBy(n => n, StringComparer.OrdinalIgnoreCase); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Completers\ThemeArgumentCompleter.cs:38:return friendly.Where(n => n.StartsWith(partial, StringComparison.OrdinalIgnoreCase)); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Parsers\EnumArgParser.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Parsers\StaticMemberParser.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Parsers\StaticMemberParser.cs:23:.Where(p => p.PropertyType == type) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\Parsers\StaticMemberParser.cs:27:.Where(f => f.FieldType == type) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\BuiltinCommands.cs:6:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\BuiltinCommands.cs:43:terminal._themePack._themeNames.Select(ThemeNameHelper.GetFriendlyThemeName) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\BuiltinCommands.cs:71:terminal._fontPack._fonts.Select(font => font.name) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\BuiltinCommands.cs:569:foreach (string variable in shell.Variables.Keys.ToArray()) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\BuiltinCommands.cs:670:if (!shell.Variables.Any()) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandArg.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandArg.cs:22:.Where(method => method.Name == nameof(TryGet)) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandArg.cs:65:cleanedString = IgnoredValuesForCleanedTypes.Aggregate( +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandArg.cs:242:return RegisteredObjectParsers.Keys.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandArg.cs:257:types = e.Types.Where(t => t != null).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandAutoComplete.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandAutoComplete.cs:30:return Complete(text: text, buffer: _buffer).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandAutoComplete.cs:126:prefix += " " + string.Join(" ", args.Select(a => a.contents)); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandAutoComplete.cs:136:!string.IsNullOrEmpty(insertion) && insertion.Any(char.IsWhiteSpace); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandHistory.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandHistory.cs:24:.Where(value => !onlySuccess || value.success == true) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandHistory.cs:25:.Where(value => !onlyErrorFree || value.errorFree == true) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandHistory.cs:26:.Select(value => value.text); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandLog.cs:6:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandShell.cs:6:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandShell.cs:84:return commands.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandShell.cs:258:+ $"Found: {command.Value.Name}({string.Join(",", command.Value.GetParameters().Select(p => p.ParameterType.Name))})" +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Backend\CommandShell.cs:313:_arguments.Count == 0 ? Array.Empty() : _arguments.ToArray() +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Input\TerminalKeyboardController.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\Input\TerminalKeyboardController.cs:19:.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:10:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:350:value = stringList.ToList(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:353:value = logTypeList.ToList(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:356:value = fontList.ToList(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:603:_propertyValues[property.name] = currentStringList.ToList(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:616:_propertyValues[property.name] = currentLogTypeList.ToList(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:1968:.Where(ThemeNameHelper.IsThemeName) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Runtime\CommandTerminal\UI\TerminalUI.cs:1969:.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\Components\TestCommands.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\Components\TestCommands.cs:31:foreach (int i in Enumerable.Range(0, count).OrderBy(_ => random.Next())) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\Components\TestCommands.cs:34:"log " + string.Join("a", Enumerable.Range(0, i).Select(_ => string.Empty)) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\Components\TestCommands.cs:39:"log " + string.Join("a", Enumerable.Range(0, 1_000).Select(_ => string.Empty)) +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\Components\TestCommands.cs:58:string[] logMessages = Enumerable.Range(0, count).Select(i => $"no-op {i}").ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\ArrayPoolTests.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\ArrayPoolTests.cs:56:Assert.IsTrue(second.All(v => v == 0)); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\ArrayPoolTests.cs:72:Assert.IsTrue(second.All(v => v == 0x7F)); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandArgTests.cs:6:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandArgTests.cs:412:.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandArgTests.cs:905:.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandArgTests.cs:923:.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:49:string[] logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:66:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:122:string[] logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:138:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:158:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:182:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:210:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:233:char[] quotes = CommandArg.Quotes.ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:259:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:280:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:297:logs = history.GetHistory(true, true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\CommandShellTests.cs:305:logs.Count(message => string.Equals(message, "log")), +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\TerminalTests.cs:5:using System.Linq; +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\TerminalTests.cs:43:string[] events = history.GetHistory(onlySuccess: true, onlyErrorFree: true).ToArray(); +D:\Code\Packages\Packages\com.wallstop-studios.dxcommandterminal\Tests\Runtime\TerminalTests.cs:55:.ToArray(); diff --git a/linq_hits.txt.meta b/linq_hits.txt.meta new file mode 100644 index 0000000..1b708fa --- /dev/null +++ b/linq_hits.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a4a2cb608462911439e106a8553c5f4c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 02ab4fd4910920a70dc6971dd8346d897c8f2ab4 Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 14:29:42 -0700 Subject: [PATCH 10/69] Test fixes --- Runtime/DataStructures/CyclicBuffer.cs | 26 ++++++++-- Tests/Runtime/AutocompleteTests.cs | 66 ++++++++++++++++++++++++- Tests/Runtime/CyclicBufferTests.cs | 60 ++++++++++++++++++++++ Tests/Runtime/CyclicBufferTests.cs.meta | 3 ++ 4 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 Tests/Runtime/CyclicBufferTests.cs create mode 100644 Tests/Runtime/CyclicBufferTests.cs.meta diff --git a/Runtime/DataStructures/CyclicBuffer.cs b/Runtime/DataStructures/CyclicBuffer.cs index 7e10291..fe9c5d8 100644 --- a/Runtime/DataStructures/CyclicBuffer.cs +++ b/Runtime/DataStructures/CyclicBuffer.cs @@ -135,22 +135,40 @@ public void Clear() public void Resize(int newCapacity) { + if (newCapacity == Capacity) + { + return; + } + if (newCapacity < 0) { throw new ArgumentException(nameof(newCapacity)); } - int oldCapacity = Capacity; Capacity = newCapacity; + + // Normalize underlying storage so the oldest element is at index 0. _buffer.Shift(-_position); + if (newCapacity < _buffer.Count) { - _buffer.RemoveRange(newCapacity, _buffer.Count - newCapacity); + // When shrinking, drop the oldest elements to retain the most recent window. + int removeCount = _buffer.Count - newCapacity; + _buffer.RemoveRange(0, removeCount); + } + + // Update next-write position: if full, wrap to 0 to overwrite oldest; otherwise append at end. + if (Capacity <= 0) + { + _position = 0; + Count = 0; + _buffer.Clear(); + return; } - _position = - newCapacity < oldCapacity && newCapacity <= _buffer.Count ? 0 : _buffer.Count; + // Count cannot exceed new capacity Count = Math.Min(newCapacity, Count); + _position = _buffer.Count >= Capacity ? 0 : _buffer.Count; } public bool Contains(T item) diff --git a/Tests/Runtime/AutocompleteTests.cs b/Tests/Runtime/AutocompleteTests.cs index 71a1634..03d5ba2 100644 --- a/Tests/Runtime/AutocompleteTests.cs +++ b/Tests/Runtime/AutocompleteTests.cs @@ -160,7 +160,7 @@ public void AutoRegisteredCommandReceivesCompleter() CommandHistory history = new CommandHistory(8); CommandShell shell = new CommandShell(history); - shell.InitializeAutoRegisteredCommands(); + shell.InitializeAutoRegisteredCommands(Array.Empty()); Assert.IsTrue( shell.Commands.TryGetValue( @@ -224,5 +224,69 @@ public void AutoCompleteChainsArguments() Assert.AreEqual(1, partialSecondContext.ArgsBeforeCursor.Count); Assert.AreEqual("alpha", partialSecondContext.ArgsBeforeCursor[0].contents); } + + [Test] + public void AutoCompleteHonorsCaretIndexWithinInput() + { + CommandHistory history = new CommandHistory(16); + CommandShell shell = new CommandShell(history); + ChainedCompleter chainedCompleter = new ChainedCompleter(); + shell.AddCommand("chain", _ => { }, 0, -1, string.Empty, null, chainedCompleter); + + CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell); + List buffer = new List(); + int caretIndex = "chain alpha ".Length; + autoComplete.Complete("chain alpha gamma", caretIndex, buffer); + + Assert.AreEqual(1, buffer.Count); + Assert.AreEqual("chain alpha gamma", buffer[0]); + Assert.AreEqual(1, chainedCompleter.Calls.Count); + CommandCompletionContext context = chainedCompleter.Calls[0]; + Assert.AreEqual(1, context.ArgIndex); + Assert.AreEqual(string.Empty, context.PartialArg); + Assert.AreEqual(1, context.ArgsBeforeCursor.Count); + Assert.AreEqual("alpha", context.ArgsBeforeCursor[0].contents); + } + + [Test] + public void AutoCompleteFallsBackToHistoryWhenCommandUnknown() + { + CommandHistory history = new CommandHistory(16); + history.Push("login", true, true); + history.Push("logout", true, true); + CommandShell shell = new CommandShell(history); + + CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell); + string[] suggestions = autoComplete.Complete("lo"); + + Assert.IsNotNull(suggestions); + CollectionAssert.Contains(suggestions, "login"); + CollectionAssert.Contains(suggestions, "logout"); + } + + [Test] + public void AutoCompleteDeduplicatesValuesAcrossSources() + { + CommandHistory history = new CommandHistory(16); + history.Push("list", true, true); + CommandShell shell = new CommandShell(history); + shell.AddCommand("list", _ => { }); + List knownWords = new List { "list" }; + CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell, knownWords); + + string[] suggestions = autoComplete.Complete("li"); + Assert.IsNotNull(suggestions); + + int matches = 0; + for (int i = 0; i < suggestions.Length; ++i) + { + if (string.Equals(suggestions[i], "list", StringComparison.Ordinal)) + { + matches++; + } + } + + Assert.AreEqual(1, matches, "Expected deduplicated suggestion list."); + } } } diff --git a/Tests/Runtime/CyclicBufferTests.cs b/Tests/Runtime/CyclicBufferTests.cs new file mode 100644 index 0000000..dcd2b43 --- /dev/null +++ b/Tests/Runtime/CyclicBufferTests.cs @@ -0,0 +1,60 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using NUnit.Framework; + using UnityEngine.TestTools; + using WallstopStudios.DxCommandTerminal.DataStructures; + + public sealed class CyclicBufferTests + { + [UnityTest] + public IEnumerator AddAndOverwritePreservesChronology() + { + CyclicBuffer buf = new(3) { 0, 1, 2 }; + + Assert.AreEqual( + 3, + buf.Count, + "Count should reflect number of elements added up to capacity." + ); + Assert.AreEqual(0, buf[0], "Oldest element should be at index 0 before wrap."); + Assert.AreEqual(1, buf[1], "Next element should be index 1."); + Assert.AreEqual(2, buf[2], "Newest element should be index 2 before wrap."); + + // Overwrite oldest + buf.Add(3); + Assert.AreEqual(3, buf.Count, "Count should not grow beyond capacity."); + Assert.AreEqual(1, buf[0], "After overwrite, oldest is dropped."); + Assert.AreEqual(2, buf[1], "Element order should advance by one."); + Assert.AreEqual(3, buf[2], "Newest written value should be last."); + + yield break; + } + + [UnityTest] + public IEnumerator ResizeTruncatesOrExtends() + { + CyclicBuffer buf = new(5); + for (int i = 0; i < 5; ++i) + { + buf.Add(i); + } + Assert.AreEqual(5, buf.Count, "Filled buffer should have full count."); + + // Shrink: oldest entries should be truncated + buf.Resize(3); + Assert.AreEqual(3, buf.Count, "Count should reflect new capacity after shrink."); + Assert.AreEqual(2, buf[0], "Shrink should retain most recent entries and drop oldest."); + Assert.AreEqual(3, buf[1], "Remaining order should be preserved (middle)."); + Assert.AreEqual(4, buf[2], "Remaining order should be preserved (newest)."); + + // Grow: capacity increases, order stays + buf.Resize(6); + Assert.AreEqual(3, buf.Count, "Growing capacity should not change current count."); + Assert.AreEqual(2, buf[0], "Growing capacity should not alter order (first)."); + Assert.AreEqual(3, buf[1], "Growing capacity should not alter order (second)."); + Assert.AreEqual(4, buf[2], "Growing capacity should not alter order (third)."); + yield break; + } + } +} diff --git a/Tests/Runtime/CyclicBufferTests.cs.meta b/Tests/Runtime/CyclicBufferTests.cs.meta new file mode 100644 index 0000000..9133961 --- /dev/null +++ b/Tests/Runtime/CyclicBufferTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cc5f7590ac134018ad5c48129e86ce06 +timeCreated: 1760390235 \ No newline at end of file From 30fce839afa08a5671f8c95c9a63522cbe61aeb0 Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 15:26:52 -0700 Subject: [PATCH 11/69] Add launcher --- .../CommandTerminal/Backend/CommandShell.cs | 2 +- .../Input/TerminalControlTypes.cs | 1 + .../Input/TerminalKeyboardController.cs | 23 + .../Input/TerminalPlayerInputController.cs | 13 + .../UI/TerminalLauncherSettings.cs | 216 ++++++ .../UI/TerminalLauncherSettings.cs.meta | 4 + Runtime/CommandTerminal/UI/TerminalState.cs | 1 + Runtime/CommandTerminal/UI/TerminalUI.cs | 706 ++++++++++++++---- Styles/BaseStyles.uss | 22 +- Tests/Runtime/LauncherModeTests.cs | 130 ++++ Tests/Runtime/LauncherModeTests.cs.meta | 4 + 11 files changed, 980 insertions(+), 142 deletions(-) create mode 100644 Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs create mode 100644 Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs.meta create mode 100644 Tests/Runtime/LauncherModeTests.cs create mode 100644 Tests/Runtime/LauncherModeTests.cs.meta diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index 305dfa6..3161ed7 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -206,7 +206,7 @@ public void InitializeAutoRegisteredCommands( IgnoringDefaultCommands = ignoreDefaultCommands; ClearAutoRegisteredCommands(); _ignoredCommands.Clear(); - if (ignoreDefaultCommands != null) + if (ignoredCommands != null) { _ignoredCommands.UnionWith(ignoredCommands); } diff --git a/Runtime/CommandTerminal/Input/TerminalControlTypes.cs b/Runtime/CommandTerminal/Input/TerminalControlTypes.cs index 2b96ee2..4fdabd2 100644 --- a/Runtime/CommandTerminal/Input/TerminalControlTypes.cs +++ b/Runtime/CommandTerminal/Input/TerminalControlTypes.cs @@ -14,5 +14,6 @@ public enum TerminalControlTypes ToggleSmall = 6, CompleteForward = 7, CompleteBackward = 8, + ToggleLauncher = 9, } } diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index cbe9a3e..be2408c 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -63,6 +63,9 @@ public bool ShouldHandleInputThisFrame [SerializeField] public string toggleFullHotkey = "#`"; + [SerializeField] + public string toggleLauncherHotkey = "%space"; + [SerializeField] public string completeHotkey = "tab"; @@ -89,6 +92,7 @@ public bool ShouldHandleInputThisFrame TerminalControlTypes.EnterCommand, TerminalControlTypes.Previous, TerminalControlTypes.Next, + TerminalControlTypes.ToggleLauncher, TerminalControlTypes.ToggleFull, TerminalControlTypes.ToggleSmall, TerminalControlTypes.CompleteBackward, @@ -232,6 +236,15 @@ protected virtual void ToggleFull() terminal.ToggleFull(); } + protected virtual void ToggleLauncher() + { + if (terminal == null) + { + return; + } + terminal.ToggleLauncher(); + } + protected virtual void ToggleSmall() { if (terminal == null) @@ -285,6 +298,11 @@ protected virtual bool IsToggleFullPressed() return InputHelpers.IsKeyPressed(toggleFullHotkey, inputMode); } + protected virtual bool IsToggleLauncherPressed() + { + return InputHelpers.IsKeyPressed(toggleLauncherHotkey, inputMode); + } + protected virtual bool IsToggleSmallPressed() { return InputHelpers.IsKeyPressed(toggleHotkey, inputMode); @@ -336,6 +354,8 @@ private bool IsControlPressed(TerminalControlTypes controlType) return IsToggleFullPressed(); case TerminalControlTypes.ToggleSmall: return IsToggleSmallPressed(); + case TerminalControlTypes.ToggleLauncher: + return IsToggleLauncherPressed(); case TerminalControlTypes.CompleteBackward: return IsCompleteBackwardPressed(); case TerminalControlTypes.CompleteForward: @@ -367,6 +387,9 @@ private void ExecuteControl(TerminalControlTypes controlType) case TerminalControlTypes.ToggleSmall: ToggleSmall(); break; + case TerminalControlTypes.ToggleLauncher: + ToggleLauncher(); + break; case TerminalControlTypes.CompleteBackward: CompleteBackward(); break; diff --git a/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs b/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs index a527154..44b355c 100644 --- a/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs +++ b/Runtime/CommandTerminal/Input/TerminalPlayerInputController.cs @@ -120,6 +120,19 @@ public virtual void OnToggleFull(InputValue inputValue) terminal.ToggleFull(); } + public virtual void OnToggleLauncher(InputValue inputValue) + { + if (!_enabled) + { + return; + } + if (terminal == null) + { + return; + } + terminal.ToggleLauncher(); + } + public virtual void OnCompleteCommand(InputValue input) { if (!_enabled) diff --git a/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs b/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs new file mode 100644 index 0000000..35b5521 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs @@ -0,0 +1,216 @@ +namespace WallstopStudios.DxCommandTerminal.UI +{ + using System; + using UnityEngine; + + public enum LauncherSizeMode + { + Pixels = 0, + RelativeToScreen = 1, + RelativeToLauncher = 2, + } + + [Serializable] + public struct LauncherDimension + { + public LauncherSizeMode mode; + + [Min(0f)] + public float value; + + public static LauncherDimension RelativeToScreen(float ratio) + { + return new LauncherDimension + { + mode = LauncherSizeMode.RelativeToScreen, + value = ratio, + }; + } + + public static LauncherDimension RelativeToLauncher(float ratio) + { + return new LauncherDimension + { + mode = LauncherSizeMode.RelativeToLauncher, + value = ratio, + }; + } + + public static LauncherDimension Pixels(float pixels) + { + return new LauncherDimension { mode = LauncherSizeMode.Pixels, value = pixels }; + } + + public float ResolvePixels(int screenLength, float launcherLength = 0f) + { + switch (mode) + { + case LauncherSizeMode.Pixels: + return Mathf.Max(0f, value); + case LauncherSizeMode.RelativeToScreen: + return Mathf.Clamp01(value) * screenLength; + case LauncherSizeMode.RelativeToLauncher: + return Mathf.Clamp01(value) * Mathf.Max(launcherLength, 0f); + default: + return 0f; + } + } + } + + [Serializable] + public sealed class TerminalLauncherSettings + { + private const float MinimumPadding = 0f; + private const float MinimumCorner = 0f; + + [Header("Dimensions")] + public LauncherDimension width = LauncherDimension.RelativeToScreen(0.55f); + + public LauncherDimension height = LauncherDimension.RelativeToScreen(0.18f); + + public LauncherDimension historyHeight = LauncherDimension.RelativeToLauncher(0.45f); + + [Min(220f)] + public float minimumWidth = 380f; + + [Min(72f)] + public float minimumHeight = 110f; + + [Header("Position")] + [Range(0f, 1f)] + public float verticalAnchor = 0.5f; + + [Range(0f, 1f)] + public float horizontalAnchor = 0.5f; + + [Min(MinimumPadding)] + public float screenPadding = 32f; + + [Header("Visuals")] + [Min(MinimumCorner)] + public float cornerRadius = 16f; + + [Min(MinimumPadding)] + public float insetPadding = 14f; + + [Header("History")] + [Min(1)] + public int historyVisibleEntryCount = 6; + + [Range(0.1f, 8f)] + public float historyFadeExponent = 2.3f; + + [Tooltip("Pixels reserved for the input row and autocomplete when sizing history.")] + [Min(48f)] + public float inputReservePixels = 96f; + + [Header("Behaviour")] + public bool snapOpen = true; + + [Min(0f)] + public float animationDuration = 0.14f; + + public LauncherLayoutMetrics ComputeMetrics(int screenWidth, int screenHeight) + { + float safeWidth = Mathf.Max(minimumWidth, width.ResolvePixels(screenWidth)); + float safeHeight = Mathf.Max(minimumHeight, height.ResolvePixels(screenHeight)); + + float horizontalPadding = Mathf.Max(screenPadding, MinimumPadding); + float verticalPadding = Mathf.Max(screenPadding, MinimumPadding); + + float maxWidth = Mathf.Max(minimumWidth, screenWidth - (horizontalPadding * 2f)); + float maxHeight = Mathf.Max(minimumHeight, screenHeight - (verticalPadding * 2f)); + + safeWidth = Mathf.Min(safeWidth, maxWidth); + safeHeight = Mathf.Min(safeHeight, maxHeight); + + float left = Mathf.Clamp( + (screenWidth * horizontalAnchor) - (safeWidth * 0.5f), + horizontalPadding, + Mathf.Max(horizontalPadding, screenWidth - safeWidth - horizontalPadding) + ); + float top = Mathf.Clamp( + (screenHeight * verticalAnchor) - (safeHeight * 0.5f), + verticalPadding, + Mathf.Max(verticalPadding, screenHeight - safeHeight - verticalPadding) + ); + + float maxHistoryHeight = Mathf.Max(0f, safeHeight - inputReservePixels); + float historyPixels = Mathf.Min( + maxHistoryHeight, + historyHeight.ResolvePixels(screenHeight, safeHeight) + ); + + if (historyPixels < 0f) + { + historyPixels = 0f; + } + + return new LauncherLayoutMetrics( + width: safeWidth, + height: safeHeight, + left: left, + top: top, + historyHeight: historyPixels, + cornerRadius: Mathf.Max(cornerRadius, MinimumCorner), + insetPadding: Mathf.Max(insetPadding, MinimumPadding), + historyVisibleEntryCount: Mathf.Max(0, historyVisibleEntryCount), + historyFadeExponent: historyFadeExponent, + snapOpen: snapOpen, + animationDuration: Mathf.Max(0f, animationDuration) + ); + } + } + + public readonly struct LauncherLayoutMetrics + { + public LauncherLayoutMetrics( + float width, + float height, + float left, + float top, + float historyHeight, + float cornerRadius, + float insetPadding, + int historyVisibleEntryCount, + float historyFadeExponent, + bool snapOpen, + float animationDuration + ) + { + Width = width; + Height = height; + Left = left; + Top = top; + HistoryHeight = historyHeight; + CornerRadius = cornerRadius; + InsetPadding = insetPadding; + HistoryVisibleEntryCount = historyVisibleEntryCount; + HistoryFadeExponent = historyFadeExponent; + SnapOpen = snapOpen; + AnimationDuration = animationDuration; + } + + public float Width { get; } + + public float Height { get; } + + public float Left { get; } + + public float Top { get; } + + public float HistoryHeight { get; } + + public float CornerRadius { get; } + + public float InsetPadding { get; } + + public int HistoryVisibleEntryCount { get; } + + public float HistoryFadeExponent { get; } + + public bool SnapOpen { get; } + + public float AnimationDuration { get; } + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs.meta b/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs.meta new file mode 100644 index 0000000..0c937bb --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs.meta @@ -0,0 +1,4 @@ +fileFormatVersion: 2 +guid: 2c0e72455786478e8e54b7c74035102a +timeCreated: 1752787200 + diff --git a/Runtime/CommandTerminal/UI/TerminalState.cs b/Runtime/CommandTerminal/UI/TerminalState.cs index bb79cfc..cc5657a 100644 --- a/Runtime/CommandTerminal/UI/TerminalState.cs +++ b/Runtime/CommandTerminal/UI/TerminalState.cs @@ -9,5 +9,6 @@ public enum TerminalState Closed = 1, OpenSmall = 2, OpenFull = 3, + OpenLauncher = 4, } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index c58ca3b..61c3d4a 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -38,8 +38,11 @@ private enum ScrollBarCaptureState public bool IsClosed => _state != TerminalState.OpenFull && _state != TerminalState.OpenSmall + && _state != TerminalState.OpenLauncher && Mathf.Approximately(_currentWindowHeight, _targetWindowHeight); + private bool IsLauncherActive => _state == TerminalState.OpenLauncher; + public string CurrentTheme => !string.IsNullOrWhiteSpace(_runtimeTheme) ? _runtimeTheme : _persistedTheme; @@ -83,6 +86,10 @@ private enum ScrollBarCaptureState [Tooltip("Duration for the ease-in animation in seconds")] public float easeInTime = 0.5f; + [Header("Launcher")] + [SerializeField] + private TerminalLauncherSettings _launcherSettings = new(); + [Header("System")] [SerializeField] private int _logBufferSize = 256; @@ -113,6 +120,9 @@ private enum ScrollBarCaptureState [DxShowIf(nameof(showGUIButtons))] public string fullButtonText = "full"; + [DxShowIf(nameof(showGUIButtons))] + public string launcherButtonText = "launcher"; + [Header("Hints")] public HintDisplayMode hintDisplayMode = HintDisplayMode.AutoCompleteOnly; @@ -188,6 +198,7 @@ private enum ScrollBarCaptureState internal bool disableUIForTests; private TerminalState _state = TerminalState.Closed; + private TerminalState _previousState = TerminalState.Closed; private float _currentWindowHeight; private float _targetWindowHeight; private float _realWindowHeight; @@ -209,6 +220,8 @@ private enum ScrollBarCaptureState private float _initialWindowHeight; private float _animationTimer; private bool _isAnimating; + private LauncherLayoutMetrics _launcherMetrics; + private bool _launcherMetricsInitialized; private VisualElement _terminalContainer; private ScrollView _logScrollView; @@ -632,6 +645,7 @@ public void ToggleState(TerminalState newState) public void SetState(TerminalState newState) { _commandIssuedThisFrame = true; + _previousState = _state; _state = newState; ResetWindowIdempotent(); if (_state != TerminalState.Closed) @@ -736,25 +750,38 @@ private void ResetAutoComplete() private void ResetWindowIdempotent() { int height = Screen.height; + int width = Screen.width; float oldTargetHeight = _targetWindowHeight; + bool wasLauncher = _launcherMetricsInitialized; try { switch (_state) { case TerminalState.OpenSmall: { + _launcherMetricsInitialized = false; _realWindowHeight = height * maxHeight * smallTerminalRatio; _targetWindowHeight = _realWindowHeight; break; } case TerminalState.OpenFull: { + _launcherMetricsInitialized = false; _realWindowHeight = height * maxHeight; _targetWindowHeight = _realWindowHeight; break; } + case TerminalState.OpenLauncher: + { + _launcherMetrics = _launcherSettings.ComputeMetrics(width, height); + _launcherMetricsInitialized = true; + _realWindowHeight = _launcherMetrics.Height; + _targetWindowHeight = _realWindowHeight; + break; + } default: { + _launcherMetricsInitialized = false; _realWindowHeight = height * maxHeight * smallTerminalRatio; _targetWindowHeight = 0; break; @@ -763,10 +790,19 @@ private void ResetWindowIdempotent() } finally { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (oldTargetHeight != _targetWindowHeight) + if (!Mathf.Approximately(oldTargetHeight, _targetWindowHeight)) { - StartHeightAnimation(); + bool snapHeight = + (_launcherMetricsInitialized || wasLauncher) && _launcherMetrics.SnapOpen; + if (snapHeight) + { + _currentWindowHeight = _targetWindowHeight; + _isAnimating = false; + } + else + { + StartHeightAnimation(); + } } } } @@ -1269,6 +1305,108 @@ void OnDraggerMouseLeave(MouseLeaveEvent evt) } } + private void ApplyStandardLayout(float screenWidth) + { + VisualElement rootElement = _uiDocument.rootVisualElement; + rootElement.style.width = screenWidth; + rootElement.style.height = _currentWindowHeight; + + _terminalContainer.EnableInClassList("terminal-container--launcher", false); + _terminalContainer.style.width = screenWidth; + _terminalContainer.style.height = _currentWindowHeight; + _terminalContainer.style.left = 0; + _terminalContainer.style.top = 0; + _terminalContainer.style.paddingLeft = 0; + _terminalContainer.style.paddingRight = 0; + _terminalContainer.style.paddingTop = 0; + _terminalContainer.style.paddingBottom = 0; + _terminalContainer.style.marginLeft = 0; + _terminalContainer.style.marginRight = 0; + _terminalContainer.style.marginTop = 0; + _terminalContainer.style.marginBottom = 0; + _terminalContainer.style.borderTopLeftRadius = 0; + _terminalContainer.style.borderTopRightRadius = 0; + _terminalContainer.style.borderBottomLeftRadius = 0; + _terminalContainer.style.borderBottomRightRadius = 0; + _terminalContainer.style.justifyContent = Justify.FlexStart; + _terminalContainer.style.alignItems = Align.Stretch; + + _logScrollView.style.marginTop = 0; + _logScrollView.style.height = new StyleLength(StyleKeyword.Null); + _logScrollView.style.maxHeight = new StyleLength(StyleKeyword.Null); + _logScrollView.style.minHeight = new StyleLength(StyleKeyword.Null); + _logScrollView.style.display = DisplayStyle.Flex; + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + _autoCompleteContainer.style.marginBottom = 0; + _inputContainer.style.marginBottom = 0; + + EnsureChildOrder( + _terminalContainer, + _logScrollView, + _autoCompleteContainer, + _inputContainer + ); + } + + private void ApplyLauncherLayout(float screenWidth, float screenHeight) + { + VisualElement rootElement = _uiDocument.rootVisualElement; + rootElement.style.width = screenWidth; + rootElement.style.height = screenHeight; + + _terminalContainer.EnableInClassList("terminal-container--launcher", true); + _terminalContainer.style.width = _launcherMetrics.Width; + _terminalContainer.style.height = _currentWindowHeight; + _terminalContainer.style.left = _launcherMetrics.Left; + _terminalContainer.style.top = _launcherMetrics.Top; + _terminalContainer.style.justifyContent = Justify.FlexStart; + _terminalContainer.style.alignItems = Align.Stretch; + + float horizontalPadding = _launcherMetrics.InsetPadding; + float verticalPadding = Mathf.Max(4f, _launcherMetrics.InsetPadding * 0.5f); + _terminalContainer.style.paddingLeft = horizontalPadding; + _terminalContainer.style.paddingRight = horizontalPadding; + _terminalContainer.style.paddingTop = verticalPadding; + _terminalContainer.style.paddingBottom = verticalPadding; + _terminalContainer.style.marginLeft = 0; + _terminalContainer.style.marginRight = 0; + _terminalContainer.style.marginTop = 0; + _terminalContainer.style.marginBottom = 0; + + float cornerRadius = _launcherMetrics.CornerRadius; + _terminalContainer.style.borderTopLeftRadius = cornerRadius; + _terminalContainer.style.borderTopRightRadius = cornerRadius; + _terminalContainer.style.borderBottomLeftRadius = cornerRadius; + _terminalContainer.style.borderBottomRightRadius = cornerRadius; + + _autoCompleteContainer.style.marginBottom = Mathf.Max(2f, verticalPadding * 0.25f); + _inputContainer.style.marginBottom = Mathf.Max(4f, verticalPadding * 0.5f); + + if (_launcherMetrics.HistoryHeight > 0f) + { + _logScrollView.style.display = DisplayStyle.Flex; + _logScrollView.style.height = _launcherMetrics.HistoryHeight; + _logScrollView.style.maxHeight = _launcherMetrics.HistoryHeight; + _logScrollView.style.minHeight = 0; + _logScrollView.style.marginTop = Mathf.Max(6f, verticalPadding * 0.35f); + } + else + { + _logScrollView.style.display = DisplayStyle.None; + _logScrollView.style.height = 0; + _logScrollView.style.maxHeight = 0; + _logScrollView.style.marginTop = 0; + } + + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden; + EnsureChildOrder( + _terminalContainer, + _autoCompleteContainer, + _inputContainer, + _logScrollView + ); + } + private void RefreshUI() { if (_terminalContainer == null) @@ -1281,11 +1419,24 @@ private void RefreshUI() return; } - _uiDocument.rootVisualElement.style.height = _currentWindowHeight; + float screenWidth = Screen.width; + float screenHeight = Screen.height; + + if (IsLauncherActive && _launcherMetricsInitialized) + { + ApplyLauncherLayout(screenWidth, screenHeight); + } + else + { + ApplyStandardLayout(screenWidth); + } + _terminalContainer.style.height = _currentWindowHeight; - _terminalContainer.style.width = Screen.width; + DisplayStyle commandInputStyle = - _currentWindowHeight <= 30 ? DisplayStyle.None : DisplayStyle.Flex; + !IsLauncherActive && _currentWindowHeight <= 30 + ? DisplayStyle.None + : DisplayStyle.Flex; _needsFocus |= _inputContainer.resolvedStyle.display != commandInputStyle @@ -1294,6 +1445,7 @@ private void RefreshUI() RefreshLogs(); RefreshAutoCompleteHints(); + string commandInput = _input.CommandText; if (!string.Equals(_commandInput.value, commandInput)) { @@ -1319,11 +1471,13 @@ private void RefreshUI() _needsScrollToEnd && _logScrollView != null && _logScrollView.style.display != DisplayStyle.None + && !IsLauncherActive ) { ScrollToEnd(); _needsScrollToEnd = false; } + RefreshStateButtons(); } @@ -1353,7 +1507,14 @@ private void RefreshLogs() return; } + if (IsLauncherActive && _launcherMetricsInitialized) + { + RefreshLauncherHistory(logs); + return; + } + VisualElement content = _logScrollView.contentContainer; + _logScrollView.style.display = DisplayStyle.Flex; bool dirty = _lastSeenBufferVersion != Terminal.Buffer.Version; if (content.childCount != logs.Count) { @@ -1383,30 +1544,31 @@ private void RefreshLogs() for (int i = 0; i < logs.Count && i < content.childCount; ++i) { VisualElement item = content[i]; + LogItem logItem = logs[i]; switch (item) { case TextField logText: { - LogItem logItem = logs[i]; - SetupLogText(logText, logItem); + ApplyLogStyling(logText, logItem); logText.value = logItem.message; break; } case Label logLabel: { - LogItem logItem = logs[i]; - SetupLogText(logLabel, logItem); + ApplyLogStyling(logLabel, logItem); logLabel.text = logItem.message; break; } case Button button: { - LogItem logItem = logs[i]; - SetupLogText(button, logItem); + ApplyLogStyling(button, logItem); button.text = logItem.message; break; } } + + item.style.opacity = 1f; + item.style.display = DisplayStyle.Flex; } if (logs.Count == content.childCount) @@ -1414,34 +1576,108 @@ private void RefreshLogs() _lastSeenBufferVersion = Terminal.Buffer.Version; } } - return; + } - static void SetupLogText(VisualElement logText, LogItem log) + private void RefreshLauncherHistory(IReadOnlyList logs) + { + VisualElement content = _logScrollView.contentContainer; + int visibleCount = Mathf.Min(_launcherMetrics.HistoryVisibleEntryCount, logs.Count); + + if (_launcherMetrics.HistoryHeight <= 0f || visibleCount <= 0) { - logText.EnableInClassList( - "terminal-output-label--shell", - log.type == TerminalLogType.ShellMessage - ); - logText.EnableInClassList( - "terminal-output-label--error", - log.type - is TerminalLogType.Exception - or TerminalLogType.Error - or TerminalLogType.Assert - ); - logText.EnableInClassList( - "terminal-output-label--warning", - log.type == TerminalLogType.Warning - ); - logText.EnableInClassList( - "terminal-output-label--message", - log.type == TerminalLogType.Message - ); - logText.EnableInClassList( - "terminal-output-label--input", - log.type == TerminalLogType.Input - ); + _logScrollView.style.display = DisplayStyle.None; + for (int i = 0; i < content.childCount; ++i) + { + content[i].style.display = DisplayStyle.None; + } + _lastSeenBufferVersion = Terminal.Buffer?.Version; + _needsScrollToEnd = false; + return; + } + + _logScrollView.style.display = DisplayStyle.Flex; + + if (content.childCount < visibleCount) + { + for (int i = content.childCount; i < visibleCount; ++i) + { + Label logText = new(); + logText.AddToClassList("terminal-output-label"); + content.Add(logText); + } + } + + for (int i = visibleCount; i < content.childCount; ++i) + { + content[i].style.display = DisplayStyle.None; + } + + float denominator = Mathf.Max(1f, visibleCount - 1f); + for (int i = 0; i < visibleCount; ++i) + { + int logIndex = logs.Count - 1 - i; + VisualElement element = content[i]; + LogItem logItem = logs[logIndex]; + + switch (element) + { + case TextField logText: + { + ApplyLogStyling(logText, logItem); + logText.value = logItem.message; + break; + } + case Label logLabel: + { + ApplyLogStyling(logLabel, logItem); + logLabel.text = logItem.message; + break; + } + case Button button: + { + ApplyLogStyling(button, logItem); + button.text = logItem.message; + break; + } + } + + element.style.display = DisplayStyle.Flex; + float fade = + visibleCount == 1 + ? 1f + : Mathf.Pow(1f - (i / denominator), _launcherMetrics.HistoryFadeExponent); + element.style.opacity = Mathf.Clamp01(fade); } + + _lastSeenBufferVersion = Terminal.Buffer.Version; + _needsScrollToEnd = false; + } + + private static void ApplyLogStyling(VisualElement logText, LogItem log) + { + logText.EnableInClassList( + "terminal-output-label--shell", + log.type == TerminalLogType.ShellMessage + ); + logText.EnableInClassList( + "terminal-output-label--error", + log.type + is TerminalLogType.Exception + or TerminalLogType.Error + or TerminalLogType.Assert + ); + logText.EnableInClassList( + "terminal-output-label--warning", + log.type == TerminalLogType.Warning + ); + logText.EnableInClassList( + "terminal-output-label--message", + log.type == TerminalLogType.Message + ); + logText.EnableInClassList( + "terminal-output-label--input", + log.type == TerminalLogType.Input + ); } private void ScrollToEnd() @@ -1687,123 +1923,114 @@ private void UpdateAutoCompleteView() { _autoCompleteContainer.Add(element); } - } - private void RefreshStateButtons() - { - if (_stateButtonContainer == null) + float desiredTop = _currentWindowHeight; + float desiredLeft = 2f; + float desiredWidth = Screen.width; + if (IsLauncherActive && _launcherMetricsInitialized) { - return; + desiredTop = _launcherMetrics.Top + _currentWindowHeight + 12f; + desiredLeft = _launcherMetrics.Left; + desiredWidth = _launcherMetrics.Width; } - _stateButtonContainer.style.top = _currentWindowHeight; - DisplayStyle displayStyle = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; + _stateButtonContainer.style.top = desiredTop; + _stateButtonContainer.style.left = desiredLeft; + _stateButtonContainer.style.width = desiredWidth; + _stateButtonContainer.style.display = showGUIButtons + ? DisplayStyle.Flex + : DisplayStyle.None; + _stateButtonContainer.style.justifyContent = + IsLauncherActive && _launcherMetricsInitialized + ? Justify.Center + : Justify.FlexStart; + + Button primaryButton; + Button secondaryButton; + Button launcherButton; + EnsureButtons(out primaryButton, out secondaryButton, out launcherButton); + + DisplayStyle buttonDisplay = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; + + UpdateButton(primaryButton, GetPrimaryLabel(), _state == TerminalState.OpenSmall); + UpdateButton(secondaryButton, GetSecondaryLabel(), _state == TerminalState.OpenFull); + UpdateButton(launcherButton, launcherButtonText, IsLauncherActive); + + return; - for (int i = 0; i < _stateButtonContainer.childCount; ++i) + void EnsureButtons(out Button primary, out Button secondary, out Button launcher) { - VisualElement child = _stateButtonContainer[i]; - child.style.display = displayStyle; + while (_stateButtonContainer.childCount < 3) + { + int index = _stateButtonContainer.childCount; + Button button = index switch + { + 0 => new Button(FirstClicked) { name = "StateButton1" }, + 1 => new Button(SecondClicked) { name = "StateButton2" }, + _ => new Button(LauncherClicked) { name = "StateButton3" }, + }; + button.AddToClassList("terminal-button"); + _stateButtonContainer.Add(button); + } + + primary = _stateButtonContainer[0] as Button; + secondary = _stateButtonContainer[1] as Button; + launcher = _stateButtonContainer[2] as Button; } - if (!showGUIButtons) + string GetPrimaryLabel() { - return; + return _state switch + { + TerminalState.Closed => smallButtonText, + TerminalState.OpenSmall => closeButtonText, + TerminalState.OpenFull => closeButtonText, + TerminalState.OpenLauncher => closeButtonText, + _ => string.Empty, + }; } - Button firstButton; - Button secondButton; - if (_stateButtonContainer.childCount == 0) + string GetSecondaryLabel() { - firstButton = new Button(FirstClicked) { name = "StateButton1" }; - firstButton.AddToClassList("terminal-button"); - firstButton.style.display = displayStyle; - _stateButtonContainer.Add(firstButton); - - secondButton = new Button(SecondClicked) { name = "StateButton2" }; - secondButton.AddToClassList("terminal-button"); - secondButton.style.display = displayStyle; - _stateButtonContainer.Add(secondButton); + return _state switch + { + TerminalState.Closed => fullButtonText, + TerminalState.OpenSmall => fullButtonText, + TerminalState.OpenFull => smallButtonText, + TerminalState.OpenLauncher => fullButtonText, + _ => string.Empty, + }; } - else + + void UpdateButton(Button button, string text, bool isActive) { - firstButton = _stateButtonContainer[0] as Button; - if (firstButton == null) + if (button == null) { return; } - secondButton = _stateButtonContainer[1] as Button; - if (secondButton == null) + + bool shouldShow = + buttonDisplay == DisplayStyle.Flex && !string.IsNullOrWhiteSpace(text); + button.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None; + if (shouldShow) { - return; + button.text = text; } + button.EnableInClassList("terminal-button-active", shouldShow && isActive); } - _inputCaretLabel.text = _inputCaret; - - switch (_state) - { - case TerminalState.Closed: - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - firstButton.text = smallButtonText; - } - if (!string.IsNullOrWhiteSpace(fullButtonText)) - { - secondButton.text = fullButtonText; - } - break; - case TerminalState.OpenSmall: - if (!string.IsNullOrWhiteSpace(closeButtonText)) - { - firstButton.text = closeButtonText; - } - if (!string.IsNullOrWhiteSpace(fullButtonText)) - { - secondButton.text = fullButtonText; - } - break; - case TerminalState.OpenFull: - if (!string.IsNullOrWhiteSpace(closeButtonText)) - { - firstButton.text = closeButtonText; - } - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - secondButton.text = smallButtonText; - } - break; - default: - throw new InvalidEnumArgumentException( - nameof(_state), - (int)_state, - typeof(TerminalState) - ); - } - return; - void FirstClicked() { switch (_state) { case TerminalState.Closed: - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - SetState(TerminalState.OpenSmall); - } + ToggleSmall(); break; case TerminalState.OpenSmall: case TerminalState.OpenFull: - if (!string.IsNullOrWhiteSpace(closeButtonText)) - { - SetState(TerminalState.Closed); - } + case TerminalState.OpenLauncher: + Close(); break; - default: - throw new InvalidEnumArgumentException( - nameof(_state), - (int)_state, - typeof(TerminalState) - ); } } @@ -1813,25 +2040,48 @@ void SecondClicked() { case TerminalState.Closed: case TerminalState.OpenSmall: - if (!string.IsNullOrWhiteSpace(fullButtonText)) - { - SetState(TerminalState.OpenFull); - } + case TerminalState.OpenLauncher: + ToggleFull(); break; case TerminalState.OpenFull: - if (!string.IsNullOrWhiteSpace(smallButtonText)) - { - SetState(TerminalState.OpenSmall); - } + ToggleSmall(); break; - default: - throw new InvalidEnumArgumentException( - nameof(_state), - (int)_state, - typeof(TerminalState) - ); } } + + void LauncherClicked() + { + ToggleLauncher(); + } + } + + private static void EnsureChildOrder( + VisualElement parent, + params VisualElement[] orderedChildren + ) + { + if (parent == null) + { + return; + } + + int insertIndex = 0; + foreach (VisualElement child in orderedChildren) + { + if (child == null || child.parent != parent) + { + continue; + } + + int currentIndex = parent.IndexOf(child); + if (currentIndex != insertIndex) + { + parent.Remove(child); + parent.Insert(insertIndex, child); + } + + insertIndex++; + } } public Font SetRandomFont(bool persist = false) @@ -2109,6 +2359,37 @@ public void ToggleFull() ToggleState(TerminalState.OpenFull); } + public void ToggleLauncher() + { + ToggleState(TerminalState.OpenLauncher); + } + + // Internal test hooks + internal TerminalState CurrentStateForTests => _state; + + internal bool LauncherMetricsInitializedForTests => _launcherMetricsInitialized; + + internal ScrollView LogScrollViewForTests => _logScrollView; + + internal void SetLauncherMetricsForTests( + LauncherLayoutMetrics metrics, + bool initialized = true + ) + { + _launcherMetrics = metrics; + _launcherMetricsInitialized = initialized; + } + + internal void SetLogScrollViewForTests(ScrollView scrollView) + { + _logScrollView = scrollView; + } + + internal void RefreshLauncherHistoryForTests(IReadOnlyList logs) + { + RefreshLauncherHistory(logs); + } + public void EnterCommand() { if (_state == TerminalState.Closed) @@ -2251,6 +2532,144 @@ public void CompleteCommand(bool searchForward = true) } } + private void RefreshStateButtons() + { + if (_stateButtonContainer == null) + { + return; + } + + float desiredTop = _currentWindowHeight; + float desiredLeft = 2f; + float desiredWidth = Screen.width; + if (IsLauncherActive && _launcherMetricsInitialized) + { + desiredTop = _launcherMetrics.Top + _currentWindowHeight + 12f; + desiredLeft = _launcherMetrics.Left; + desiredWidth = _launcherMetrics.Width; + } + + _stateButtonContainer.style.top = desiredTop; + _stateButtonContainer.style.left = desiredLeft; + _stateButtonContainer.style.width = desiredWidth; + _stateButtonContainer.style.display = showGUIButtons + ? DisplayStyle.Flex + : DisplayStyle.None; + _stateButtonContainer.style.justifyContent = + IsLauncherActive && _launcherMetricsInitialized + ? Justify.Center + : Justify.FlexStart; + + Button primaryButton; + Button secondaryButton; + Button launcherButton; + EnsureButtons(out primaryButton, out secondaryButton, out launcherButton); + + DisplayStyle buttonDisplay = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; + + UpdateButton(primaryButton, GetPrimaryLabel(), _state == TerminalState.OpenSmall); + UpdateButton(secondaryButton, GetSecondaryLabel(), _state == TerminalState.OpenFull); + UpdateButton(launcherButton, launcherButtonText, IsLauncherActive); + + return; + + void EnsureButtons(out Button primary, out Button secondary, out Button launcher) + { + while (_stateButtonContainer.childCount < 3) + { + int index = _stateButtonContainer.childCount; + Button button = index switch + { + 0 => new Button(FirstClicked) { name = "StateButton1" }, + 1 => new Button(SecondClicked) { name = "StateButton2" }, + _ => new Button(LauncherClicked) { name = "StateButton3" }, + }; + button.AddToClassList("terminal-button"); + _stateButtonContainer.Add(button); + } + + primary = _stateButtonContainer[0] as Button; + secondary = _stateButtonContainer[1] as Button; + launcher = _stateButtonContainer[2] as Button; + } + + string GetPrimaryLabel() + { + return _state switch + { + TerminalState.Closed => smallButtonText, + TerminalState.OpenSmall => closeButtonText, + TerminalState.OpenFull => closeButtonText, + TerminalState.OpenLauncher => closeButtonText, + _ => string.Empty, + }; + } + + string GetSecondaryLabel() + { + return _state switch + { + TerminalState.Closed => fullButtonText, + TerminalState.OpenSmall => fullButtonText, + TerminalState.OpenFull => smallButtonText, + TerminalState.OpenLauncher => fullButtonText, + _ => string.Empty, + }; + } + + void UpdateButton(Button button, string text, bool isActive) + { + if (button == null) + { + return; + } + + bool shouldShow = + buttonDisplay == DisplayStyle.Flex && !string.IsNullOrWhiteSpace(text); + button.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None; + if (shouldShow) + { + button.text = text; + } + button.EnableInClassList("terminal-button-active", shouldShow && isActive); + } + + void FirstClicked() + { + switch (_state) + { + case TerminalState.Closed: + ToggleSmall(); + break; + case TerminalState.OpenSmall: + case TerminalState.OpenFull: + case TerminalState.OpenLauncher: + Close(); + break; + } + } + + void SecondClicked() + { + switch (_state) + { + case TerminalState.Closed: + case TerminalState.OpenSmall: + case TerminalState.OpenLauncher: + ToggleFull(); + break; + case TerminalState.OpenFull: + ToggleSmall(); + break; + } + } + + void LauncherClicked() + { + ToggleLauncher(); + } + } + private void StartHeightAnimation() { if (Mathf.Approximately(_currentWindowHeight, _targetWindowHeight)) @@ -2277,15 +2696,22 @@ private void HandleHeightAnimation() float animationDuration; bool isExpanding = _targetWindowHeight > _initialWindowHeight; + bool useLauncherTiming = + IsLauncherActive || _previousState == TerminalState.OpenLauncher; + if (isExpanding) { selectedCurve = easeOutCurve; - animationDuration = easeOutTime; + animationDuration = useLauncherTiming + ? Mathf.Max(_launcherMetrics.AnimationDuration, 0.0001f) + : easeOutTime; } else { selectedCurve = easeInCurve; - animationDuration = easeInTime; + animationDuration = useLauncherTiming + ? Mathf.Max(_launcherMetrics.AnimationDuration, 0.0001f) + : easeInTime; } if (animationDuration <= 0f) diff --git a/Styles/BaseStyles.uss b/Styles/BaseStyles.uss index afee1f7..79a6e78 100644 --- a/Styles/BaseStyles.uss +++ b/Styles/BaseStyles.uss @@ -101,6 +101,8 @@ position: absolute; flex-direction: row; left: 2px; + gap: 6px; + align-items: center; } .terminal-output-label { @@ -166,6 +168,24 @@ background-color: var(--terminal-bg); } +.terminal-container--launcher { + background-color: var(--terminal-bg); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: 0 32px 60px rgba(0, 0, 0, 0.45); + transition: transform 0.12s ease, opacity 0.12s ease; +} + +.terminal-button-active { + background-color: var(--button-selected-bg); + color: var(--button-selected-text); +} + +.terminal-button-active:hover { + background-color: var(--button-selected-bg); + color: var(--button-selected-text); +} + .log-scroll-view .unity-scroller .unity-base-slider__dragger { background-color: var(--scroll-color); transition: background-color 0.1s ease; @@ -248,4 +268,4 @@ .terminal-output-label--error { color: var(--text-error); -} \ No newline at end of file +} diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs new file mode 100644 index 0000000..da0f680 --- /dev/null +++ b/Tests/Runtime/LauncherModeTests.cs @@ -0,0 +1,130 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + using UnityEngine.UIElements; + + public sealed class LauncherModeTests + { + [Test] + public void LauncherMetricsRespectSizingModes() + { + var settings = new TerminalLauncherSettings + { + width = LauncherDimension.RelativeToScreen(0.5f), + height = LauncherDimension.RelativeToScreen(0.18f), + historyHeight = LauncherDimension.RelativeToLauncher(0.5f), + minimumWidth = 300f, + minimumHeight = 120f, + screenPadding = 40f, + historyVisibleEntryCount = 4, + historyFadeExponent = 2f, + }; + + LauncherLayoutMetrics metrics = settings.ComputeMetrics(1920, 1080); + + Assert.That(metrics.Width, Is.EqualTo(960f).Within(0.001f)); + Assert.That(metrics.Height, Is.EqualTo(194.4f).Within(0.001f)); + Assert.That(metrics.Left, Is.EqualTo(480f).Within(0.001f)); + Assert.That(metrics.Top, Is.EqualTo(442.8f).Within(0.5f)); + Assert.That(metrics.HistoryHeight, Is.EqualTo(metrics.Height * 0.5f).Within(0.001f)); + Assert.That(metrics.HistoryVisibleEntryCount, Is.EqualTo(4)); + Assert.That(metrics.HistoryFadeExponent, Is.EqualTo(2f).Within(0.001f)); + } + + [UnityTest] + public IEnumerator ToggleLauncherTogglesState() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + Assert.That(terminal.IsClosed, Is.True); + + terminal.ToggleLauncher(); + yield return TestSceneHelpers.WaitFrames(2); + + Assert.That(terminal.CurrentStateForTests, Is.EqualTo(TerminalState.OpenLauncher)); + Assert.That(terminal.IsClosed, Is.False); + + terminal.ToggleLauncher(); + yield return TestSceneHelpers.WaitFrames(2); + + Assert.That(terminal.CurrentStateForTests, Is.EqualTo(TerminalState.Closed)); + Assert.That(terminal.IsClosed, Is.True); + } + + [UnityTest] + public IEnumerator RefreshLauncherHistoryProducesFadedEntries() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + CommandLog previousLog = Terminal.Buffer; + var log = new CommandLog(16); + Terminal.Buffer = log; + + try + { + log.EnqueueMessage("first", TerminalLogType.Message, includeStackTrace: false); + log.EnqueueMessage("second", TerminalLogType.Message, includeStackTrace: false); + log.EnqueueMessage("third", TerminalLogType.Message, includeStackTrace: false); + log.DrainPending(); + + var metrics = new LauncherLayoutMetrics( + width: 640f, + height: 160f, + left: 100f, + top: 200f, + historyHeight: 120f, + cornerRadius: 14f, + insetPadding: 12f, + historyVisibleEntryCount: 3, + historyFadeExponent: 2f, + snapOpen: true, + animationDuration: 0.1f + ); + + var scroll = new ScrollView(); + terminal.SetLogScrollViewForTests(scroll); + terminal.SetLauncherMetricsForTests(metrics); + terminal.SetState(TerminalState.OpenLauncher); + terminal.RefreshLauncherHistoryForTests(Terminal.Buffer.Logs); + + VisualElement content = terminal.LogScrollViewForTests.contentContainer; + Assert.That(content.childCount, Is.EqualTo(3)); + + // Verify newest entry is first and fully opaque + var newest = content[0] as Label; + Assert.IsNotNull(newest); + Assert.That(newest!.text, Is.EqualTo("third")); + Assert.That(newest.style.opacity.value, Is.EqualTo(1f).Within(0.001f)); + + // Middle entry has partial opacity + var middle = content[1] as Label; + Assert.IsNotNull(middle); + Assert.That(middle!.text, Is.EqualTo("second")); + Assert.That(middle.style.opacity.value, Is.LessThan(1f).And.GreaterThan(0f)); + + // Oldest entry is faded out + var oldest = content[2] as Label; + Assert.IsNotNull(oldest); + Assert.That(oldest!.text, Is.EqualTo("first")); + Assert.That(oldest.style.opacity.value, Is.EqualTo(0f).Within(0.001f)); + } + finally + { + Terminal.Buffer = previousLog; + } + + yield return TestSceneHelpers.DestroyTerminalAndWait(); + } + } +} diff --git a/Tests/Runtime/LauncherModeTests.cs.meta b/Tests/Runtime/LauncherModeTests.cs.meta new file mode 100644 index 0000000..940f2ed --- /dev/null +++ b/Tests/Runtime/LauncherModeTests.cs.meta @@ -0,0 +1,4 @@ +fileFormatVersion: 2 +guid: c073d08856b042e6a37ba9210edc558f +timeCreated: 1752787200 + From 67f15084d5d465e57ee071d7b9be05729a9a9e9a Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 16:32:16 -0700 Subject: [PATCH 12/69] Update terminal input --- .../Input/TerminalKeyboardController.cs | 46 ++-- Runtime/CommandTerminal/UI/TerminalUI.cs | 122 +++++++++- Tests/Runtime/CommandHistoryTests.cs | 120 ++++++++- Tests/Runtime/CommandLogTests.cs | 72 ++++++ Tests/Runtime/CommandLogTests.cs.meta | 11 + Tests/Runtime/CommandShellBehaviorTests.cs | 228 ++++++++++++++++++ .../Runtime/CommandShellBehaviorTests.cs.meta | 11 + ...ios.DxCommandTerminal.Tests.Runtime.asmdef | 2 +- 8 files changed, 567 insertions(+), 45 deletions(-) create mode 100644 Tests/Runtime/CommandLogTests.cs create mode 100644 Tests/Runtime/CommandLogTests.cs.meta create mode 100644 Tests/Runtime/CommandShellBehaviorTests.cs create mode 100644 Tests/Runtime/CommandShellBehaviorTests.cs.meta diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index be2408c..39b6951 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -10,6 +10,9 @@ public class TerminalKeyboardController : MonoBehaviour, IInputHandler { protected static readonly TerminalControlTypes[] ControlTypes = BuildControlTypes(); + protected readonly HashSet _missing = new(); + protected readonly HashSet _terminalControlTypes = new(); + private static TerminalControlTypes[] BuildControlTypes() { Array values = Enum.GetValues(typeof(TerminalControlTypes)); @@ -57,31 +60,14 @@ public bool ShouldHandleInputThisFrame public TerminalUI terminal; [Header("Hotkeys")] - [SerializeField] public string toggleHotkey = "`"; - - [SerializeField] public string toggleFullHotkey = "#`"; - - [SerializeField] - public string toggleLauncherHotkey = "%space"; - - [SerializeField] + public string toggleLauncherHotkey = "#space"; public string completeHotkey = "tab"; - - [SerializeField] public string reverseCompleteHotkey = "#tab"; - - [SerializeField] public string previousHotkey = "up"; - - [SerializeField] public List _completeCommandHotkeys = new() { "enter", "return" }; - - [SerializeField] public string closeHotkey = "escape"; - - [SerializeField] public string nextHotkey = "down"; [SerializeField] @@ -134,13 +120,17 @@ protected virtual void OnValidate() private void VerifyControlOrderIntegrity() { // Verify set equality without LINQ - HashSet set = new HashSet(_controlOrder); - bool equal = set.Count == ControlTypes.Length; + _terminalControlTypes.Clear(); + foreach (TerminalControlTypes controlType in _controlOrder) + { + _terminalControlTypes.Add(controlType); + } + bool equal = _terminalControlTypes.Count == ControlTypes.Length; if (equal) { for (int i = 0; i < ControlTypes.Length; ++i) { - if (!set.Contains(ControlTypes[i])) + if (!_terminalControlTypes.Contains(ControlTypes[i])) { equal = false; break; @@ -151,19 +141,21 @@ private void VerifyControlOrderIntegrity() if (!equal) { // Build missing list for message - List missing = new List(); + _missing.Clear(); for (int i = 0; i < ControlTypes.Length; ++i) { TerminalControlTypes t = ControlTypes[i]; - if (!set.Contains(t)) + if (!_terminalControlTypes.Contains(t)) { - missing.Add(t.ToString()); + _missing.Add(t.ToString()); } } - Debug.LogWarning( - $"Control Order is missing the following controls: [{string.Join(", ", missing)}]. " - + "Input for these will not be handled. Is this intentional?", + Debug.LogError( + $"Control Order is missing the following controls: [{string.Join(", ", _missing)}]. " + + "Input for these will not be handled. Is this intentional?" + + $"\nTerminal Control Types: [{string.Join(", ", ControlTypes)}]" + + $"\nExisting Control Types: [{string.Join(", ", _terminalControlTypes)}]", this ); } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 61c3d4a..9ca503a 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -21,6 +21,7 @@ namespace WallstopStudios.DxCommandTerminal.UI public sealed class TerminalUI : MonoBehaviour { private const string TerminalRootName = "TerminalRoot"; + private const float LauncherAutoCompleteSpacing = 6f; private enum ScrollBarCaptureState { @@ -87,6 +88,7 @@ private enum ScrollBarCaptureState public float easeInTime = 0.5f; [Header("Launcher")] + [ContextMenuItem("Reset Launcher Layout (Danger!)", nameof(ResetLauncherSettings))] [SerializeField] private TerminalLauncherSettings _launcherSettings = new(); @@ -1316,6 +1318,7 @@ private void ApplyStandardLayout(float screenWidth) _terminalContainer.style.height = _currentWindowHeight; _terminalContainer.style.left = 0; _terminalContainer.style.top = 0; + _terminalContainer.style.position = Position.Relative; _terminalContainer.style.paddingLeft = 0; _terminalContainer.style.paddingRight = 0; _terminalContainer.style.paddingTop = 0; @@ -1337,6 +1340,12 @@ private void ApplyStandardLayout(float screenWidth) _logScrollView.style.minHeight = new StyleLength(StyleKeyword.Null); _logScrollView.style.display = DisplayStyle.Flex; _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + + _autoCompleteContainer.style.position = Position.Relative; + _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.top = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.width = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.maxHeight = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.marginBottom = 0; _inputContainer.style.marginBottom = 0; @@ -1359,8 +1368,10 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _terminalContainer.style.height = _currentWindowHeight; _terminalContainer.style.left = _launcherMetrics.Left; _terminalContainer.style.top = _launcherMetrics.Top; + _terminalContainer.style.position = Position.Absolute; _terminalContainer.style.justifyContent = Justify.FlexStart; _terminalContainer.style.alignItems = Align.Stretch; + _terminalContainer.style.flexDirection = FlexDirection.Column; float horizontalPadding = _launcherMetrics.InsetPadding; float verticalPadding = Mathf.Max(4f, _launcherMetrics.InsetPadding * 0.5f); @@ -1378,9 +1389,10 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _terminalContainer.style.borderTopRightRadius = cornerRadius; _terminalContainer.style.borderBottomLeftRadius = cornerRadius; _terminalContainer.style.borderBottomRightRadius = cornerRadius; + _terminalContainer.style.overflow = Overflow.Visible; - _autoCompleteContainer.style.marginBottom = Mathf.Max(2f, verticalPadding * 0.25f); - _inputContainer.style.marginBottom = Mathf.Max(4f, verticalPadding * 0.5f); + _inputContainer.style.marginBottom = 0; + _autoCompleteContainer.style.marginBottom = 0; if (_launcherMetrics.HistoryHeight > 0f) { @@ -1399,12 +1411,12 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) } _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden; - EnsureChildOrder( - _terminalContainer, - _autoCompleteContainer, - _inputContainer, - _logScrollView - ); + + _autoCompleteContainer.style.position = Position.Absolute; + _autoCompleteContainer.style.maxHeight = _launcherMetrics.HistoryHeight; + _autoCompleteContainer.style.display = DisplayStyle.None; + + EnsureChildOrder(_terminalContainer, _inputContainer, _logScrollView); } private void RefreshUI() @@ -1425,6 +1437,7 @@ private void RefreshUI() if (IsLauncherActive && _launcherMetricsInitialized) { ApplyLauncherLayout(screenWidth, screenHeight); + UpdateLauncherLayoutMetrics(); } else { @@ -2390,6 +2403,16 @@ internal void RefreshLauncherHistoryForTests(IReadOnlyList logs) RefreshLauncherHistory(logs); } + private void ResetLauncherSettings() + { + Debug.LogWarning( + "Launcher settings reset to defaults. This action is destructive.", + this + ); + _launcherSettings = new TerminalLauncherSettings(); + ResetWindowIdempotent(); + } + public void EnterCommand() { if (_state == TerminalState.Closed) @@ -2670,6 +2693,89 @@ void LauncherClicked() } } + private void UpdateLauncherLayoutMetrics() + { + if (!IsLauncherActive || !_launcherMetricsInitialized) + { + return; + } + + float padding = _launcherMetrics.InsetPadding; + float inputHeight = Mathf.Max(_inputContainer.resolvedStyle.height, 0f); + float availableWidth = _launcherMetrics.Width - (padding * 2f); + if (availableWidth < 0f) + { + availableWidth = 0f; + } + + float suggestionTop = padding + inputHeight + LauncherAutoCompleteSpacing; + bool hasSuggestions = _autoCompleteContainer.childCount > 0; + _autoCompleteContainer.style.left = padding; + _autoCompleteContainer.style.width = availableWidth; + if (hasSuggestions) + { + _autoCompleteContainer.style.display = DisplayStyle.Flex; + _autoCompleteContainer.style.top = suggestionTop; + } + else + { + _autoCompleteContainer.style.display = DisplayStyle.None; + } + + float suggestionsHeight = hasSuggestions + ? Mathf.Max( + _autoCompleteContainer.contentContainer.layout.height, + _autoCompleteContainer.resolvedStyle.height + ) + : 0f; + + if (hasSuggestions && suggestionsHeight <= 0f) + { + float computedHeight = 0f; + int childCount = _autoCompleteContainer.contentContainer.childCount; + for (int i = 0; i < childCount; ++i) + { + VisualElement child = _autoCompleteContainer.contentContainer[i]; + if (child == null) + { + continue; + } + + computedHeight += child.resolvedStyle.height; + computedHeight += + child.resolvedStyle.marginTop + child.resolvedStyle.marginBottom; + } + + suggestionsHeight = Mathf.Max(computedHeight, suggestionsHeight); + } + + float reservedForSuggestions = ( + hasSuggestions + ? suggestionsHeight + LauncherAutoCompleteSpacing + : LauncherAutoCompleteSpacing + ); + + float availableForHistory = + _currentWindowHeight - (padding * 2f) - inputHeight - reservedForSuggestions; + availableForHistory = Mathf.Min(availableForHistory, _launcherMetrics.HistoryHeight); + availableForHistory = Mathf.Max(0f, availableForHistory); + + if (availableForHistory <= 0.01f || _logScrollView.contentContainer.childCount == 0) + { + _logScrollView.style.display = DisplayStyle.None; + _logScrollView.style.height = 0; + _logScrollView.style.maxHeight = 0; + } + else + { + _logScrollView.style.display = DisplayStyle.Flex; + _logScrollView.style.height = availableForHistory; + _logScrollView.style.maxHeight = availableForHistory; + } + + _logScrollView.style.marginTop = reservedForSuggestions; + } + private void StartHeightAnimation() { if (Mathf.Approximately(_currentWindowHeight, _targetWindowHeight)) diff --git a/Tests/Runtime/CommandHistoryTests.cs b/Tests/Runtime/CommandHistoryTests.cs index 29dcec3..a994945 100644 --- a/Tests/Runtime/CommandHistoryTests.cs +++ b/Tests/Runtime/CommandHistoryTests.cs @@ -1,5 +1,6 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime { + using System.Collections.Generic; using Backend; using NUnit.Framework; @@ -15,27 +16,128 @@ public void FiltersAndOrderWork() history.Push("c ok but error", true, false); history.Push("d ok", true, true); - // onlySuccess - System.Collections.Generic.List onlySuccess = - new System.Collections.Generic.List(history.GetHistory(true, false)); + List onlySuccess = new List(history.GetHistory(true, false)); Assert.AreEqual(3, onlySuccess.Count); Assert.AreEqual("a ok", onlySuccess[0]); Assert.AreEqual("c ok but error", onlySuccess[1]); Assert.AreEqual("d ok", onlySuccess[2]); - // onlyErrorFree - System.Collections.Generic.List onlyErrorFree = - new System.Collections.Generic.List(history.GetHistory(false, true)); + List onlyErrorFree = new List(history.GetHistory(false, true)); Assert.AreEqual(2, onlyErrorFree.Count); Assert.AreEqual("a ok", onlyErrorFree[0]); Assert.AreEqual("d ok", onlyErrorFree[1]); - // both filters - System.Collections.Generic.List both = - new System.Collections.Generic.List(history.GetHistory(true, true)); + List both = new List(history.GetHistory(true, true)); Assert.AreEqual(2, both.Count); Assert.AreEqual("a ok", both[0]); Assert.AreEqual("d ok", both[1]); } + + [Test] + public void PreviousAndNextSkipSameCommandsWhenEnabled() + { + CommandHistory history = new CommandHistory(8); + + history.Push("alpha", true, true); + history.Push("beta", true, true); + history.Push("beta", true, true); + history.Push("gamma", true, true); + + List traversed = new List(); + string command; + while (!string.IsNullOrEmpty(command = history.Previous(true))) + { + traversed.Add(command); + } + + Assert.AreEqual(3, traversed.Count); + CollectionAssert.AreEquivalent(new[] { "alpha", "beta", "gamma" }, traversed); + for (int i = 1; i < traversed.Count; ++i) + { + Assert.AreNotEqual(traversed[i - 1], traversed[i]); + } + + List forward = new List(); + while (!string.IsNullOrEmpty(command = history.Next(true))) + { + forward.Add(command); + } + + CollectionAssert.AreEquivalent(traversed, forward); + for (int i = 1; i < forward.Count; ++i) + { + Assert.AreNotEqual(forward[i - 1], forward[i]); + } + } + + [Test] + public void PreviousAndNextReturnDuplicatesWhenSkippingDisabled() + { + CommandHistory history = new CommandHistory(6); + + history.Push("alpha", true, true); + history.Push("beta", true, true); + history.Push("beta", true, true); + + List backward = new List(); + string command; + while (!string.IsNullOrEmpty(command = history.Previous(false))) + { + backward.Add(command); + } + + CollectionAssert.AreEquivalent(new[] { "alpha", "beta", "beta" }, backward); + Assert.IsTrue(backward.Contains("alpha")); + Assert.AreEqual(2, backward.FindAll(entry => entry == "beta").Count); + + List forward = new List(); + while (!string.IsNullOrEmpty(command = history.Next(false))) + { + forward.Add(command); + } + + CollectionAssert.AreEquivalent(new[] { "alpha", "beta", "beta" }, forward); + Assert.AreEqual(3, forward.Count); + Assert.AreEqual("alpha", forward[0]); + Assert.AreEqual(2, forward.FindAll(entry => entry == "beta").Count); + } + + [Test] + public void ResizeRetainsMostRecentEntries() + { + CommandHistory history = new CommandHistory(10); + + for (int i = 0; i < 10; ++i) + { + history.Push($"command {i}", true, true); + } + + history.Resize(4); + + List remaining = new List(history.GetHistory(false, false)); + + Assert.AreEqual(4, remaining.Count); + Assert.AreEqual("command 6", remaining[0]); + Assert.AreEqual("command 7", remaining[1]); + Assert.AreEqual("command 8", remaining[2]); + Assert.AreEqual("command 9", remaining[3]); + } + + [Test] + public void ClearResetsHistoryState() + { + CommandHistory history = new CommandHistory(4); + + history.Push("alpha", true, true); + history.Push("beta", true, false); + + int removed = history.Clear(); + + Assert.AreEqual(2, removed); + List entries = new List(history.GetHistory(false, false)); + Assert.AreEqual(0, entries.Count); + Assert.AreEqual(string.Empty, history.Previous(false)); + Assert.AreEqual(string.Empty, history.Next(false)); + } } } diff --git a/Tests/Runtime/CommandLogTests.cs b/Tests/Runtime/CommandLogTests.cs new file mode 100644 index 0000000..137e7e3 --- /dev/null +++ b/Tests/Runtime/CommandLogTests.cs @@ -0,0 +1,72 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections.Generic; + using System.Linq; + using Backend; + using NUnit.Framework; + + public sealed class CommandLogTests + { + [Test] + public void HandleLogRespectsIgnoredTypes() + { + CommandLog log = new CommandLog(4, new[] { TerminalLogType.Warning }); + + bool handled = log.HandleLog("ignored", TerminalLogType.Warning); + + Assert.IsFalse(handled); + Assert.AreEqual(0, log.Logs.Count); + } + + [Test] + public void DrainPendingProcessesQueuedMessagesInOrder() + { + CommandLog log = new CommandLog(8); + + log.EnqueueMessage("first", TerminalLogType.Message, includeStackTrace: false); + log.EnqueueUnityLog("second", "stack", TerminalLogType.Error); + + int added = log.DrainPending(); + + Assert.AreEqual(2, added); + Assert.AreEqual(2, log.Logs.Count); + Assert.AreEqual("first", log.Logs[0].message); + Assert.AreEqual(string.Empty, log.Logs[0].stackTrace); + Assert.AreEqual("second", log.Logs[1].message); + Assert.AreEqual("stack", log.Logs[1].stackTrace); + Assert.AreEqual(TerminalLogType.Error, log.Logs[1].type); + } + + [Test] + public void ResizeTrimsOldestEntriesAndKeepsOrder() + { + CommandLog log = new CommandLog(6); + + for (int i = 0; i < 6; ++i) + { + log.HandleLog($"log {i}", TerminalLogType.Message); + } + + log.Resize(3); + + Assert.AreEqual(3, log.Logs.Count); + List messages = log.Logs.Select(item => item.message).ToList(); + CollectionAssert.AreEqual(new[] { "log 3", "log 4", "log 5" }, messages); + } + + [Test] + public void ClearEmptiesBufferAndReturnsRemovedCount() + { + CommandLog log = new CommandLog(5); + + log.HandleLog("a", TerminalLogType.Message); + log.HandleLog("b", TerminalLogType.Warning); + + int removed = log.Clear(); + + Assert.AreEqual(2, removed); + Assert.AreEqual(0, log.Logs.Count); + Assert.AreEqual(0, log.DrainPending()); + } + } +} diff --git a/Tests/Runtime/CommandLogTests.cs.meta b/Tests/Runtime/CommandLogTests.cs.meta new file mode 100644 index 0000000..5fc0d3c --- /dev/null +++ b/Tests/Runtime/CommandLogTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cddedfd76cdb4676a09d8c7932ec9052 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/CommandShellBehaviorTests.cs b/Tests/Runtime/CommandShellBehaviorTests.cs new file mode 100644 index 0000000..827d933 --- /dev/null +++ b/Tests/Runtime/CommandShellBehaviorTests.cs @@ -0,0 +1,228 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections.Generic; + using Backend; + using NUnit.Framework; + + public sealed class CommandShellBehaviorTests + { + [Test] + public void InitializeAutoRegisteredCommandsRespectsIgnoredNames() + { + CommandHistory history = new CommandHistory(8); + CommandShell shell = new CommandShell(history); + + shell.InitializeAutoRegisteredCommands(new[] { "help" }, ignoreDefaultCommands: false); + + Assert.IsFalse(shell.Commands.ContainsKey("help")); + Assert.IsTrue(shell.Commands.ContainsKey("log")); + Assert.IsTrue(shell.IgnoredCommands.Contains("help")); + } + + [Test] + public void InitializeAutoRegisteredCommandsCanSkipDefaultsEntirely() + { + CommandHistory history = new CommandHistory(8); + CommandShell shell = new CommandShell(history); + + shell.InitializeAutoRegisteredCommands( + Array.Empty(), + ignoreDefaultCommands: true + ); + + Assert.IsFalse(shell.Commands.ContainsKey("help")); + Assert.IsFalse(shell.Commands.ContainsKey("log")); + Assert.IsFalse(shell.Commands.ContainsKey("clear-console")); + Assert.IsTrue(shell.AutoRegisteredCommands.SetEquals(shell.Commands.Keys)); + } + + [Test] + public void InitializeAutoRegisteredCommandsRefreshesIgnoredCommands() + { + CommandHistory history = new CommandHistory(8); + CommandShell shell = new CommandShell(history); + + shell.InitializeAutoRegisteredCommands( + Array.Empty(), + ignoreDefaultCommands: false + ); + Assert.IsTrue(shell.Commands.ContainsKey("help")); + + shell.InitializeAutoRegisteredCommands(new[] { "help" }, ignoreDefaultCommands: false); + + Assert.IsFalse(shell.Commands.ContainsKey("help")); + Assert.IsTrue(shell.IgnoredCommands.Contains("help")); + } + + [Test] + public void RunCommandQueuesErrorForMissingCommand() + { + CommandHistory history = new CommandHistory(4); + CommandShell shell = new CommandShell(history); + + bool result = shell.RunCommand("missing"); + + Assert.IsFalse(result); + Assert.IsTrue(shell.TryConsumeErrorMessage(out string message)); + StringAssert.Contains("missing", message); + Assert.IsFalse(shell.TryConsumeErrorMessage(out _)); + + List all = new List(history.GetHistory(false, false)); + Assert.AreEqual(1, all.Count); + Assert.AreEqual("missing", all[0]); + + List successes = new List(history.GetHistory(true, false)); + Assert.AreEqual(0, successes.Count); + } + + [Test] + public void RunCommandInvokesHandlerAndRecordsSuccess() + { + CommandHistory history = new CommandHistory(4); + CommandShell shell = new CommandShell(history); + + CommandArg[] captured = null; + shell.AddCommand("echo", args => captured = args, 0, -1); + + bool executed = shell.RunCommand("echo hello world"); + + Assert.IsTrue(executed); + Assert.IsNotNull(captured); + Assert.AreEqual(2, captured.Length); + Assert.AreEqual("hello", captured[0].contents); + Assert.AreEqual("world", captured[1].contents); + Assert.IsFalse(shell.TryConsumeErrorMessage(out _)); + + List successes = new List(history.GetHistory(true, true)); + Assert.AreEqual(1, successes.Count); + Assert.AreEqual("echo hello world", successes[0]); + } + + [Test] + public void RunCommandValidatesArgumentCount() + { + CommandHistory history = new CommandHistory(4); + CommandShell shell = new CommandShell(history); + shell.AddCommand("require-two", _ => { }, 2, 2); + + bool executed = shell.RunCommand("require-two only-one"); + + Assert.IsFalse(executed); + Assert.IsTrue(shell.TryConsumeErrorMessage(out string message)); + StringAssert.Contains("requires", message); + + List successes = new List(history.GetHistory(true, false)); + Assert.AreEqual(0, successes.Count); + + List all = new List(history.GetHistory(false, false)); + Assert.AreEqual(1, all.Count); + Assert.AreEqual("require-two only-one", all[0]); + } + + [Test] + public void VariableSubstitutionInjectsStoredValues() + { + CommandHistory history = new CommandHistory(4); + CommandShell shell = new CommandShell(history); + + CommandArg[] captured = null; + shell.AddCommand("say", args => captured = args); + + Assert.IsTrue(shell.SetVariable("target", new CommandArg("world"))); + + bool executed = shell.RunCommand("say $target"); + + Assert.IsTrue(executed); + Assert.IsNotNull(captured); + Assert.AreEqual(1, captured.Length); + Assert.AreEqual("world", captured[0].contents); + } + + [Test] + public void UnknownVariableReferencesRemainLiteral() + { + CommandHistory history = new CommandHistory(4); + CommandShell shell = new CommandShell(history); + + CommandArg[] captured = null; + shell.AddCommand("say", args => captured = args); + + bool executed = shell.RunCommand("say $missing"); + + Assert.IsTrue(executed); + Assert.IsNotNull(captured); + Assert.AreEqual(1, captured.Length); + Assert.AreEqual("$missing", captured[0].contents); + } + + [Test] + public void VariableSubstitutionPreservesStoredQuotes() + { + CommandHistory history = new CommandHistory(4); + CommandShell shell = new CommandShell(history); + + CommandArg[] captured = null; + shell.AddCommand("say", args => captured = args); + + shell.SetVariable("phrase", new CommandArg("hello world", '"', '"')); + bool executed = shell.RunCommand("say $phrase"); + + Assert.IsTrue(executed); + Assert.IsNotNull(captured); + Assert.AreEqual("hello world", captured[0].contents); + + List entries = new List(history.GetHistory(false, false)); + Assert.AreEqual(1, entries.Count); + Assert.AreEqual("say \"hello world\"", entries[0]); + } + + [Test] + public void ClearVariableRemovesStoredValue() + { + CommandShell shell = new CommandShell(new CommandHistory(2)); + + Assert.IsTrue(shell.SetVariable("temp", new CommandArg("value"))); + Assert.IsTrue(shell.ClearVariable("temp")); + Assert.IsFalse(shell.TryGetVariable("temp", out _)); + Assert.IsFalse(shell.ClearVariable("temp")); + } + + [Test] + public void TryConsumeErrorMessageReturnsFalseWhenEmpty() + { + CommandShell shell = new CommandShell(new CommandHistory(2)); + + Assert.IsFalse(shell.TryConsumeErrorMessage(out _)); + + shell.IssueErrorMessage("sample error"); + Assert.IsTrue(shell.TryConsumeErrorMessage(out string message)); + Assert.AreEqual("sample error", message); + Assert.IsFalse(shell.TryConsumeErrorMessage(out _)); + } + + [Test] + public void AddCommandPreventsDuplicateNames() + { + CommandShell shell = new CommandShell(new CommandHistory(2)); + + bool first = shell.AddCommand("duplicate", _ => { }); + bool second = shell.AddCommand("DUPLICATE", _ => { }); + + Assert.IsTrue(first); + Assert.IsFalse(second); + Assert.IsTrue(shell.TryConsumeErrorMessage(out string message)); + StringAssert.Contains("duplicate", message.ToLowerInvariant()); + } + + [Test] + public void ClearVariableRejectsInvalidNames() + { + CommandShell shell = new CommandShell(new CommandHistory(2)); + + Assert.IsFalse(shell.ClearVariable(string.Empty)); + Assert.IsTrue(shell.TryConsumeErrorMessage(out string message)); + StringAssert.Contains("invalid", message.ToLowerInvariant()); + } + } +} diff --git a/Tests/Runtime/CommandShellBehaviorTests.cs.meta b/Tests/Runtime/CommandShellBehaviorTests.cs.meta new file mode 100644 index 0000000..d5371e6 --- /dev/null +++ b/Tests/Runtime/CommandShellBehaviorTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d25771b80c04e91a0da2043e4c86fb1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef b/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef index 8841c45..8662f65 100644 --- a/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef +++ b/Tests/Runtime/WallstopStudios.DxCommandTerminal.Tests.Runtime.asmdef @@ -6,7 +6,7 @@ "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": true, - "precompiledReferences": ["nunit.framework.dll"], + "precompiledReferences": ["nunit.framework.dll", "System.Collections.Immutable.dll"], "autoReferenced": false, "defineConstraints": ["UNITY_INCLUDE_TESTS"], "versionDefines": [], From fd604e319bf52826f830944f238ab14f934e1af8 Mon Sep 17 00:00:00 2001 From: wallstop Date: Mon, 13 Oct 2025 19:48:34 -0700 Subject: [PATCH 13/69] More tests --- Runtime/CommandTerminal/UI/TerminalUI.cs | 37 +++++++++++ Tests/Runtime/ArrayPoolTests.cs | 2 +- Tests/Runtime/AutocompleteTests.cs | 66 +++++++++---------- Tests/Runtime/CommandHistoryTests.cs | 28 ++++---- Tests/Runtime/CommandLogTests.cs | 8 +-- Tests/Runtime/CommandShellBehaviorTests.cs | 56 ++++++++-------- Tests/Runtime/CompletersTests.cs | 8 +-- Tests/Runtime/CultureParsingTests.cs | 8 +-- Tests/Runtime/ParserTests.cs | 4 +- .../Parsers/ObjectParserRegistryTests.cs | 2 +- Tests/Runtime/Parsers/ParserDiscoveryTests.cs | 2 +- Tests/Runtime/Parsers/RuntimeConfigTests.cs | 2 +- Tests/Runtime/UnityBoundaryMalformedTests.cs | 14 ++-- Tests/Runtime/UnityDelimiterParsingTests.cs | 24 +++---- Tests/Runtime/UnityExtremeParsingTests.cs | 16 ++--- .../UnityLabelPermutationSuccessTests.cs | 10 +-- Tests/Runtime/UnityLabeledParsingTests.cs | 8 +-- Tests/Runtime/UnityMalformedParsingTests.cs | 44 ++++++------- .../Runtime/UnityQuotedWrapperParsingTests.cs | 4 +- .../UntypedUnityParsingFailureTests.cs | 14 ++-- Tests/Runtime/UntypedUnityParsingTests.cs | 16 ++--- Tests/Runtime/VectorParsingTests.cs | 4 +- 22 files changed, 205 insertions(+), 172 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 9ca503a..9f820c6 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -2755,6 +2755,43 @@ private void UpdateLauncherLayoutMetrics() : LauncherAutoCompleteSpacing ); + float historyContentHeight = Mathf.Max( + _logScrollView.contentContainer.layout.height, + _logScrollView.resolvedStyle.height + ); + float desiredHistoryHeight = Mathf.Min( + historyContentHeight, + _launcherMetrics.HistoryHeight + ); + if (desiredHistoryHeight < 0f) + { + desiredHistoryHeight = 0f; + } + + if (_logScrollView.contentContainer.childCount > 0) + { + desiredHistoryHeight = Mathf.Max( + desiredHistoryHeight, + Mathf.Min(48f, _launcherMetrics.HistoryHeight) + ); + } + + float minimumHeight = padding * 2f + inputHeight + reservedForSuggestions; + float desiredHeight = minimumHeight + desiredHistoryHeight; + float clampedHeight = Mathf.Clamp( + desiredHeight, + minimumHeight, + _launcherMetrics.Height + ); + + if (!Mathf.Approximately(clampedHeight, _targetWindowHeight)) + { + _initialWindowHeight = _currentWindowHeight; + _targetWindowHeight = clampedHeight; + _animationTimer = 0f; + _isAnimating = true; + } + float availableForHistory = _currentWindowHeight - (padding * 2f) - inputHeight - reservedForSuggestions; availableForHistory = Mathf.Min(availableForHistory, _launcherMetrics.HistoryHeight); diff --git a/Tests/Runtime/ArrayPoolTests.cs b/Tests/Runtime/ArrayPoolTests.cs index 21bdaba..f4ce1e6 100644 --- a/Tests/Runtime/ArrayPoolTests.cs +++ b/Tests/Runtime/ArrayPoolTests.cs @@ -85,7 +85,7 @@ public IEnumerator ConcurrencySanity() { workers[t] = Task.Run(() => { - Random rand = new Random(Environment.TickCount + t); + Random rand = new(Environment.TickCount + t); for (int i = 0; i < iterations; ++i) { int size = rand.Next(1, 128); diff --git a/Tests/Runtime/AutocompleteTests.cs b/Tests/Runtime/AutocompleteTests.cs index 03d5ba2..0b549b2 100644 --- a/Tests/Runtime/AutocompleteTests.cs +++ b/Tests/Runtime/AutocompleteTests.cs @@ -25,7 +25,7 @@ public RecordingCompleter(Func> ha _handler = handler; } - public List Calls { get; } = new List(); + public List Calls { get; } = new(); public IEnumerable Complete(CommandCompletionContext context) { @@ -45,7 +45,7 @@ public IEnumerable Complete(CommandCompletionContext context) internal sealed class ChainedCompleter : IArgumentCompleter { - public List Calls { get; } = new List(); + public List Calls { get; } = new(); public IEnumerable Complete(CommandCompletionContext context) { @@ -75,7 +75,7 @@ public IEnumerable Complete(CommandCompletionContext context) internal sealed class AutoRegisteredCompleter : IArgumentCompleter { - public static AutoRegisteredCompleter Instance { get; } = new AutoRegisteredCompleter(); + public static AutoRegisteredCompleter Instance { get; } = new(); private AutoRegisteredCompleter() { } @@ -99,11 +99,11 @@ public sealed class AutocompleteTests [Test] public void CompleterProducesQuotedSuggestions() { - CommandHistory history = new CommandHistory(8); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(8); + CommandShell shell = new(history); shell.AddCommand("testcmd", _ => { }, 0, -1, string.Empty, null, new DummyCompleter()); - CommandAutoComplete ac = new CommandAutoComplete(history, shell); + CommandAutoComplete ac = new(history, shell); string[] results = ac.Complete("testcmd "); // Expect both suggestions formatted for insertion @@ -115,11 +115,9 @@ public void CompleterProducesQuotedSuggestions() [Test] public void ManualAddCommandExposesCompleter() { - CommandHistory history = new CommandHistory(8); - CommandShell shell = new CommandShell(history); - RecordingCompleter recordingCompleter = new RecordingCompleter(_ => - Array.Empty() - ); + CommandHistory history = new(8); + CommandShell shell = new(history); + RecordingCompleter recordingCompleter = new(_ => Array.Empty()); bool added = shell.AddCommand( "manual", @@ -139,13 +137,11 @@ public void ManualAddCommandExposesCompleter() [Test] public void AddCommandWithCommandInfoPreservesCompleter() { - CommandHistory history = new CommandHistory(8); - CommandShell shell = new CommandShell(history); - RecordingCompleter recordingCompleter = new RecordingCompleter(_ => - Array.Empty() - ); + CommandHistory history = new(8); + CommandShell shell = new(history); + RecordingCompleter recordingCompleter = new(_ => Array.Empty()); - CommandInfo info = new CommandInfo(_ => { }, 0, 1, "help", "hint", recordingCompleter); + CommandInfo info = new(_ => { }, 0, 1, "help", "hint", recordingCompleter); bool added = shell.AddCommand("infoCommand", info); @@ -157,8 +153,8 @@ public void AddCommandWithCommandInfoPreservesCompleter() [Test] public void AutoRegisteredCommandReceivesCompleter() { - CommandHistory history = new CommandHistory(8); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(8); + CommandShell shell = new(history); shell.InitializeAutoRegisteredCommands(Array.Empty()); @@ -175,12 +171,12 @@ out CommandInfo info [Test] public void AutoCompleteChainsArguments() { - CommandHistory history = new CommandHistory(16); - CommandShell shell = new CommandShell(history); - ChainedCompleter chainedCompleter = new ChainedCompleter(); + CommandHistory history = new(16); + CommandShell shell = new(history); + ChainedCompleter chainedCompleter = new(); shell.AddCommand("chain", _ => { }, 0, -1, string.Empty, null, chainedCompleter); - CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell); + CommandAutoComplete autoComplete = new(history, shell); string[] commandSuggestions = autoComplete.Complete("cha"); CollectionAssert.Contains(commandSuggestions, "chain"); @@ -228,13 +224,13 @@ public void AutoCompleteChainsArguments() [Test] public void AutoCompleteHonorsCaretIndexWithinInput() { - CommandHistory history = new CommandHistory(16); - CommandShell shell = new CommandShell(history); - ChainedCompleter chainedCompleter = new ChainedCompleter(); + CommandHistory history = new(16); + CommandShell shell = new(history); + ChainedCompleter chainedCompleter = new(); shell.AddCommand("chain", _ => { }, 0, -1, string.Empty, null, chainedCompleter); - CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell); - List buffer = new List(); + CommandAutoComplete autoComplete = new(history, shell); + List buffer = new(); int caretIndex = "chain alpha ".Length; autoComplete.Complete("chain alpha gamma", caretIndex, buffer); @@ -251,12 +247,12 @@ public void AutoCompleteHonorsCaretIndexWithinInput() [Test] public void AutoCompleteFallsBackToHistoryWhenCommandUnknown() { - CommandHistory history = new CommandHistory(16); + CommandHistory history = new(16); history.Push("login", true, true); history.Push("logout", true, true); - CommandShell shell = new CommandShell(history); + CommandShell shell = new(history); - CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell); + CommandAutoComplete autoComplete = new(history, shell); string[] suggestions = autoComplete.Complete("lo"); Assert.IsNotNull(suggestions); @@ -267,12 +263,12 @@ public void AutoCompleteFallsBackToHistoryWhenCommandUnknown() [Test] public void AutoCompleteDeduplicatesValuesAcrossSources() { - CommandHistory history = new CommandHistory(16); + CommandHistory history = new(16); history.Push("list", true, true); - CommandShell shell = new CommandShell(history); + CommandShell shell = new(history); shell.AddCommand("list", _ => { }); - List knownWords = new List { "list" }; - CommandAutoComplete autoComplete = new CommandAutoComplete(history, shell, knownWords); + List knownWords = new() { "list" }; + CommandAutoComplete autoComplete = new(history, shell, knownWords); string[] suggestions = autoComplete.Complete("li"); Assert.IsNotNull(suggestions); diff --git a/Tests/Runtime/CommandHistoryTests.cs b/Tests/Runtime/CommandHistoryTests.cs index a994945..1cd0f92 100644 --- a/Tests/Runtime/CommandHistoryTests.cs +++ b/Tests/Runtime/CommandHistoryTests.cs @@ -9,25 +9,25 @@ public sealed class CommandHistoryTests [Test] public void FiltersAndOrderWork() { - CommandHistory history = new CommandHistory(10); + CommandHistory history = new(10); history.Push("a ok", true, true); history.Push("b fail", false, false); history.Push("c ok but error", true, false); history.Push("d ok", true, true); - List onlySuccess = new List(history.GetHistory(true, false)); + List onlySuccess = new(history.GetHistory(true, false)); Assert.AreEqual(3, onlySuccess.Count); Assert.AreEqual("a ok", onlySuccess[0]); Assert.AreEqual("c ok but error", onlySuccess[1]); Assert.AreEqual("d ok", onlySuccess[2]); - List onlyErrorFree = new List(history.GetHistory(false, true)); + List onlyErrorFree = new(history.GetHistory(false, true)); Assert.AreEqual(2, onlyErrorFree.Count); Assert.AreEqual("a ok", onlyErrorFree[0]); Assert.AreEqual("d ok", onlyErrorFree[1]); - List both = new List(history.GetHistory(true, true)); + List both = new(history.GetHistory(true, true)); Assert.AreEqual(2, both.Count); Assert.AreEqual("a ok", both[0]); Assert.AreEqual("d ok", both[1]); @@ -36,14 +36,14 @@ public void FiltersAndOrderWork() [Test] public void PreviousAndNextSkipSameCommandsWhenEnabled() { - CommandHistory history = new CommandHistory(8); + CommandHistory history = new(8); history.Push("alpha", true, true); history.Push("beta", true, true); history.Push("beta", true, true); history.Push("gamma", true, true); - List traversed = new List(); + List traversed = new(); string command; while (!string.IsNullOrEmpty(command = history.Previous(true))) { @@ -57,7 +57,7 @@ public void PreviousAndNextSkipSameCommandsWhenEnabled() Assert.AreNotEqual(traversed[i - 1], traversed[i]); } - List forward = new List(); + List forward = new(); while (!string.IsNullOrEmpty(command = history.Next(true))) { forward.Add(command); @@ -73,13 +73,13 @@ public void PreviousAndNextSkipSameCommandsWhenEnabled() [Test] public void PreviousAndNextReturnDuplicatesWhenSkippingDisabled() { - CommandHistory history = new CommandHistory(6); + CommandHistory history = new(6); history.Push("alpha", true, true); history.Push("beta", true, true); history.Push("beta", true, true); - List backward = new List(); + List backward = new(); string command; while (!string.IsNullOrEmpty(command = history.Previous(false))) { @@ -90,7 +90,7 @@ public void PreviousAndNextReturnDuplicatesWhenSkippingDisabled() Assert.IsTrue(backward.Contains("alpha")); Assert.AreEqual(2, backward.FindAll(entry => entry == "beta").Count); - List forward = new List(); + List forward = new(); while (!string.IsNullOrEmpty(command = history.Next(false))) { forward.Add(command); @@ -105,7 +105,7 @@ public void PreviousAndNextReturnDuplicatesWhenSkippingDisabled() [Test] public void ResizeRetainsMostRecentEntries() { - CommandHistory history = new CommandHistory(10); + CommandHistory history = new(10); for (int i = 0; i < 10; ++i) { @@ -114,7 +114,7 @@ public void ResizeRetainsMostRecentEntries() history.Resize(4); - List remaining = new List(history.GetHistory(false, false)); + List remaining = new(history.GetHistory(false, false)); Assert.AreEqual(4, remaining.Count); Assert.AreEqual("command 6", remaining[0]); @@ -126,7 +126,7 @@ public void ResizeRetainsMostRecentEntries() [Test] public void ClearResetsHistoryState() { - CommandHistory history = new CommandHistory(4); + CommandHistory history = new(4); history.Push("alpha", true, true); history.Push("beta", true, false); @@ -134,7 +134,7 @@ public void ClearResetsHistoryState() int removed = history.Clear(); Assert.AreEqual(2, removed); - List entries = new List(history.GetHistory(false, false)); + List entries = new(history.GetHistory(false, false)); Assert.AreEqual(0, entries.Count); Assert.AreEqual(string.Empty, history.Previous(false)); Assert.AreEqual(string.Empty, history.Next(false)); diff --git a/Tests/Runtime/CommandLogTests.cs b/Tests/Runtime/CommandLogTests.cs index 137e7e3..489b63a 100644 --- a/Tests/Runtime/CommandLogTests.cs +++ b/Tests/Runtime/CommandLogTests.cs @@ -10,7 +10,7 @@ public sealed class CommandLogTests [Test] public void HandleLogRespectsIgnoredTypes() { - CommandLog log = new CommandLog(4, new[] { TerminalLogType.Warning }); + CommandLog log = new(4, new[] { TerminalLogType.Warning }); bool handled = log.HandleLog("ignored", TerminalLogType.Warning); @@ -21,7 +21,7 @@ public void HandleLogRespectsIgnoredTypes() [Test] public void DrainPendingProcessesQueuedMessagesInOrder() { - CommandLog log = new CommandLog(8); + CommandLog log = new(8); log.EnqueueMessage("first", TerminalLogType.Message, includeStackTrace: false); log.EnqueueUnityLog("second", "stack", TerminalLogType.Error); @@ -40,7 +40,7 @@ public void DrainPendingProcessesQueuedMessagesInOrder() [Test] public void ResizeTrimsOldestEntriesAndKeepsOrder() { - CommandLog log = new CommandLog(6); + CommandLog log = new(6); for (int i = 0; i < 6; ++i) { @@ -57,7 +57,7 @@ public void ResizeTrimsOldestEntriesAndKeepsOrder() [Test] public void ClearEmptiesBufferAndReturnsRemovedCount() { - CommandLog log = new CommandLog(5); + CommandLog log = new(5); log.HandleLog("a", TerminalLogType.Message); log.HandleLog("b", TerminalLogType.Warning); diff --git a/Tests/Runtime/CommandShellBehaviorTests.cs b/Tests/Runtime/CommandShellBehaviorTests.cs index 827d933..6f8b0a6 100644 --- a/Tests/Runtime/CommandShellBehaviorTests.cs +++ b/Tests/Runtime/CommandShellBehaviorTests.cs @@ -10,8 +10,8 @@ public sealed class CommandShellBehaviorTests [Test] public void InitializeAutoRegisteredCommandsRespectsIgnoredNames() { - CommandHistory history = new CommandHistory(8); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(8); + CommandShell shell = new(history); shell.InitializeAutoRegisteredCommands(new[] { "help" }, ignoreDefaultCommands: false); @@ -23,8 +23,8 @@ public void InitializeAutoRegisteredCommandsRespectsIgnoredNames() [Test] public void InitializeAutoRegisteredCommandsCanSkipDefaultsEntirely() { - CommandHistory history = new CommandHistory(8); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(8); + CommandShell shell = new(history); shell.InitializeAutoRegisteredCommands( Array.Empty(), @@ -40,8 +40,8 @@ public void InitializeAutoRegisteredCommandsCanSkipDefaultsEntirely() [Test] public void InitializeAutoRegisteredCommandsRefreshesIgnoredCommands() { - CommandHistory history = new CommandHistory(8); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(8); + CommandShell shell = new(history); shell.InitializeAutoRegisteredCommands( Array.Empty(), @@ -58,8 +58,8 @@ public void InitializeAutoRegisteredCommandsRefreshesIgnoredCommands() [Test] public void RunCommandQueuesErrorForMissingCommand() { - CommandHistory history = new CommandHistory(4); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(4); + CommandShell shell = new(history); bool result = shell.RunCommand("missing"); @@ -68,19 +68,19 @@ public void RunCommandQueuesErrorForMissingCommand() StringAssert.Contains("missing", message); Assert.IsFalse(shell.TryConsumeErrorMessage(out _)); - List all = new List(history.GetHistory(false, false)); + List all = new(history.GetHistory(false, false)); Assert.AreEqual(1, all.Count); Assert.AreEqual("missing", all[0]); - List successes = new List(history.GetHistory(true, false)); + List successes = new(history.GetHistory(true, false)); Assert.AreEqual(0, successes.Count); } [Test] public void RunCommandInvokesHandlerAndRecordsSuccess() { - CommandHistory history = new CommandHistory(4); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(4); + CommandShell shell = new(history); CommandArg[] captured = null; shell.AddCommand("echo", args => captured = args, 0, -1); @@ -94,7 +94,7 @@ public void RunCommandInvokesHandlerAndRecordsSuccess() Assert.AreEqual("world", captured[1].contents); Assert.IsFalse(shell.TryConsumeErrorMessage(out _)); - List successes = new List(history.GetHistory(true, true)); + List successes = new(history.GetHistory(true, true)); Assert.AreEqual(1, successes.Count); Assert.AreEqual("echo hello world", successes[0]); } @@ -102,8 +102,8 @@ public void RunCommandInvokesHandlerAndRecordsSuccess() [Test] public void RunCommandValidatesArgumentCount() { - CommandHistory history = new CommandHistory(4); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(4); + CommandShell shell = new(history); shell.AddCommand("require-two", _ => { }, 2, 2); bool executed = shell.RunCommand("require-two only-one"); @@ -112,10 +112,10 @@ public void RunCommandValidatesArgumentCount() Assert.IsTrue(shell.TryConsumeErrorMessage(out string message)); StringAssert.Contains("requires", message); - List successes = new List(history.GetHistory(true, false)); + List successes = new(history.GetHistory(true, false)); Assert.AreEqual(0, successes.Count); - List all = new List(history.GetHistory(false, false)); + List all = new(history.GetHistory(false, false)); Assert.AreEqual(1, all.Count); Assert.AreEqual("require-two only-one", all[0]); } @@ -123,8 +123,8 @@ public void RunCommandValidatesArgumentCount() [Test] public void VariableSubstitutionInjectsStoredValues() { - CommandHistory history = new CommandHistory(4); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(4); + CommandShell shell = new(history); CommandArg[] captured = null; shell.AddCommand("say", args => captured = args); @@ -142,8 +142,8 @@ public void VariableSubstitutionInjectsStoredValues() [Test] public void UnknownVariableReferencesRemainLiteral() { - CommandHistory history = new CommandHistory(4); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(4); + CommandShell shell = new(history); CommandArg[] captured = null; shell.AddCommand("say", args => captured = args); @@ -159,8 +159,8 @@ public void UnknownVariableReferencesRemainLiteral() [Test] public void VariableSubstitutionPreservesStoredQuotes() { - CommandHistory history = new CommandHistory(4); - CommandShell shell = new CommandShell(history); + CommandHistory history = new(4); + CommandShell shell = new(history); CommandArg[] captured = null; shell.AddCommand("say", args => captured = args); @@ -172,7 +172,7 @@ public void VariableSubstitutionPreservesStoredQuotes() Assert.IsNotNull(captured); Assert.AreEqual("hello world", captured[0].contents); - List entries = new List(history.GetHistory(false, false)); + List entries = new(history.GetHistory(false, false)); Assert.AreEqual(1, entries.Count); Assert.AreEqual("say \"hello world\"", entries[0]); } @@ -180,7 +180,7 @@ public void VariableSubstitutionPreservesStoredQuotes() [Test] public void ClearVariableRemovesStoredValue() { - CommandShell shell = new CommandShell(new CommandHistory(2)); + CommandShell shell = new(new CommandHistory(2)); Assert.IsTrue(shell.SetVariable("temp", new CommandArg("value"))); Assert.IsTrue(shell.ClearVariable("temp")); @@ -191,7 +191,7 @@ public void ClearVariableRemovesStoredValue() [Test] public void TryConsumeErrorMessageReturnsFalseWhenEmpty() { - CommandShell shell = new CommandShell(new CommandHistory(2)); + CommandShell shell = new(new CommandHistory(2)); Assert.IsFalse(shell.TryConsumeErrorMessage(out _)); @@ -204,7 +204,7 @@ public void TryConsumeErrorMessageReturnsFalseWhenEmpty() [Test] public void AddCommandPreventsDuplicateNames() { - CommandShell shell = new CommandShell(new CommandHistory(2)); + CommandShell shell = new(new CommandHistory(2)); bool first = shell.AddCommand("duplicate", _ => { }); bool second = shell.AddCommand("DUPLICATE", _ => { }); @@ -218,7 +218,7 @@ public void AddCommandPreventsDuplicateNames() [Test] public void ClearVariableRejectsInvalidNames() { - CommandShell shell = new CommandShell(new CommandHistory(2)); + CommandShell shell = new(new CommandHistory(2)); Assert.IsFalse(shell.ClearVariable(string.Empty)); Assert.IsTrue(shell.TryConsumeErrorMessage(out string message)); diff --git a/Tests/Runtime/CompletersTests.cs b/Tests/Runtime/CompletersTests.cs index 43d019a..b4e33e1 100644 --- a/Tests/Runtime/CompletersTests.cs +++ b/Tests/Runtime/CompletersTests.cs @@ -29,7 +29,7 @@ public IEnumerator ThemeCompleterReturnsDistinctSortedAndFiltered() ScriptableObject.CreateInstance() ); - ThemeArgumentCompleter completer = new ThemeArgumentCompleter(); + ThemeArgumentCompleter completer = new(); var ctx = new CommandCompletionContext( "set-theme ", "set-theme", @@ -39,7 +39,7 @@ public IEnumerator ThemeCompleterReturnsDistinctSortedAndFiltered() Terminal.Shell ); - List results = new List(completer.Complete(ctx)); + List results = new(completer.Complete(ctx)); // Friendly names should be alpha/beta/gamma, filtering by 'b' -> beta only Assert.AreEqual(1, results.Count); Assert.AreEqual("beta", results[0]); @@ -68,7 +68,7 @@ public IEnumerator FontCompleterHandlesDuplicatesAndFiltering() fontPack ); - FontArgumentCompleter completer = new FontArgumentCompleter(); + FontArgumentCompleter completer = new(); var ctx = new CommandCompletionContext( "set-font ", "set-font", @@ -78,7 +78,7 @@ public IEnumerator FontCompleterHandlesDuplicatesAndFiltering() Terminal.Shell ); - List results = new List(completer.Complete(ctx)); + List results = new(completer.Complete(ctx)); // Distinct should collapse 'Consolas' duplicates; filter 'co' -> Consolas and Cousine CollectionAssert.AreEquivalent(new[] { "Consolas", "Cousine" }, results); } diff --git a/Tests/Runtime/CultureParsingTests.cs b/Tests/Runtime/CultureParsingTests.cs index 4be8658..7741cb2 100644 --- a/Tests/Runtime/CultureParsingTests.cs +++ b/Tests/Runtime/CultureParsingTests.cs @@ -16,7 +16,7 @@ public void ParsesInvariantFloatsAndVectorsUnderNonUsCulture() { Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); - CommandArg arg = new CommandArg("3.14159"); + CommandArg arg = new("3.14159"); Assert.IsTrue(arg.TryGet(out float f)); Assert.AreEqual(3.14159f, f, 1e-5f); @@ -40,21 +40,21 @@ public void ParsesRectQuaternionAndColorUnderNonUsCulture() { Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); - CommandArg rectArg = new CommandArg("1.5, 2.5, 3.5, 4.5"); + CommandArg rectArg = new("1.5, 2.5, 3.5, 4.5"); Assert.IsTrue(rectArg.TryGet(out Rect r)); Assert.AreEqual(1.5f, r.x, 1e-5f); Assert.AreEqual(2.5f, r.y, 1e-5f); Assert.AreEqual(3.5f, r.width, 1e-5f); Assert.AreEqual(4.5f, r.height, 1e-5f); - CommandArg quatArg = new CommandArg("0.1, 0.2, 0.3, 0.4"); + CommandArg quatArg = new("0.1, 0.2, 0.3, 0.4"); Assert.IsTrue(quatArg.TryGet(out Quaternion q)); Assert.AreEqual(0.1f, q.x, 1e-5f); Assert.AreEqual(0.2f, q.y, 1e-5f); Assert.AreEqual(0.3f, q.z, 1e-5f); Assert.AreEqual(0.4f, q.w, 1e-5f); - CommandArg colorArg = new CommandArg("RGBA(0.1,0.2,0.3,0.4)"); + CommandArg colorArg = new("RGBA(0.1,0.2,0.3,0.4)"); Assert.IsTrue(colorArg.TryGet(out Color c)); Assert.AreEqual(0.1f, c.r, 1e-5f); Assert.AreEqual(0.2f, c.g, 1e-5f); diff --git a/Tests/Runtime/ParserTests.cs b/Tests/Runtime/ParserTests.cs index 321fe7b..e7a8acd 100644 --- a/Tests/Runtime/ParserTests.cs +++ b/Tests/Runtime/ParserTests.cs @@ -8,8 +8,8 @@ public sealed class ParserTests { private sealed class StaticLike { - public static StaticLike Alpha = new StaticLike(1); - public static StaticLike Beta = new StaticLike(2); + public static StaticLike Alpha = new(1); + public static StaticLike Beta = new(2); public int Value { get; } diff --git a/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs index 140cbf5..7726677 100644 --- a/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs +++ b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs @@ -23,7 +23,7 @@ private sealed class CustomType { } private sealed class CustomTypeParser : ArgParser { - public static readonly CustomTypeParser Instance = new CustomTypeParser(); + public static readonly CustomTypeParser Instance = new(); protected override bool TryParseTyped(string input, out CustomType value) { diff --git a/Tests/Runtime/Parsers/ParserDiscoveryTests.cs b/Tests/Runtime/Parsers/ParserDiscoveryTests.cs index c9a8c87..5133487 100644 --- a/Tests/Runtime/Parsers/ParserDiscoveryTests.cs +++ b/Tests/Runtime/Parsers/ParserDiscoveryTests.cs @@ -12,7 +12,7 @@ public void DiscoversAndRegistersBuiltInParsers() Assert.GreaterOrEqual(removed, 0); // Without object parsers, numeric parsing should fail - CommandArg arg = new CommandArg("42"); + CommandArg arg = new("42"); Assert.IsFalse(arg.TryGet(out int _)); // Discover and register all IArgParser implementations in loaded assemblies diff --git a/Tests/Runtime/Parsers/RuntimeConfigTests.cs b/Tests/Runtime/Parsers/RuntimeConfigTests.cs index c512c0a..853a01f 100644 --- a/Tests/Runtime/Parsers/RuntimeConfigTests.cs +++ b/Tests/Runtime/Parsers/RuntimeConfigTests.cs @@ -35,7 +35,7 @@ public void AutoDiscovery_GatedByConfig() Assert.Greater(added, 0); // Validate a simple parse now succeeds via discovered parsers - CommandArg arg = new CommandArg("123"); + CommandArg arg = new("123"); Assert.IsTrue(arg.TryGet(out int value)); Assert.AreEqual(123, value); } diff --git a/Tests/Runtime/UnityBoundaryMalformedTests.cs b/Tests/Runtime/UnityBoundaryMalformedTests.cs index a6fa3a3..a2e1881 100644 --- a/Tests/Runtime/UnityBoundaryMalformedTests.cs +++ b/Tests/Runtime/UnityBoundaryMalformedTests.cs @@ -9,49 +9,49 @@ public sealed class UnityBoundaryMalformedTests [Test] public void Vector2IntOutOfRangePositive() { - CommandArg arg = new CommandArg("2147483648,0"); // int.MaxValue + 1 + CommandArg arg = new("2147483648,0"); // int.MaxValue + 1 Assert.IsFalse(arg.TryGet(out Vector2Int _)); } [Test] public void Vector2IntOutOfRangeNegative() { - CommandArg arg = new CommandArg("-2147483649,0"); // int.MinValue - 1 + CommandArg arg = new("-2147483649,0"); // int.MinValue - 1 Assert.IsFalse(arg.TryGet(out Vector2Int _)); } [Test] public void Vector3IntOutOfRangeComponent() { - CommandArg arg = new CommandArg("0,999999999999999999999,0"); + CommandArg arg = new("0,999999999999999999999,0"); Assert.IsFalse(arg.TryGet(out Vector3Int _)); } [Test] public void RectIntOutOfRangeWidth() { - CommandArg arg = new CommandArg("0,0,2147483648,10"); + CommandArg arg = new("0,0,2147483648,10"); Assert.IsFalse(arg.TryGet(out RectInt _)); } [Test] public void RectIntOutOfRangeHeight() { - CommandArg arg = new CommandArg("0,0,10,2147483648"); + CommandArg arg = new("0,0,10,2147483648"); Assert.IsFalse(arg.TryGet(out RectInt _)); } [Test] public void QuaternionTooManyComponents() { - CommandArg arg = new CommandArg("0.1,0.2,0.3,0.4,0.5"); + CommandArg arg = new("0.1,0.2,0.3,0.4,0.5"); Assert.IsFalse(arg.TryGet(out Quaternion _)); } [Test] public void ColorRgbaTrailingCommaParsesRgb() { - CommandArg arg = new CommandArg("RGBA(0.1,0.2,0.3,)"); + CommandArg arg = new("RGBA(0.1,0.2,0.3,)"); Assert.IsTrue(arg.TryGet(out Color c)); Assert.AreEqual(0.1f, c.r, 1e-4f); Assert.AreEqual(0.2f, c.g, 1e-4f); diff --git a/Tests/Runtime/UnityDelimiterParsingTests.cs b/Tests/Runtime/UnityDelimiterParsingTests.cs index ebd3bcb..6219e62 100644 --- a/Tests/Runtime/UnityDelimiterParsingTests.cs +++ b/Tests/Runtime/UnityDelimiterParsingTests.cs @@ -9,7 +9,7 @@ public sealed class UnityDelimiterParsingTests [Test] public void ParsesVector2WithUnderscores() { - CommandArg arg = new CommandArg("1.5_2.5"); + CommandArg arg = new("1.5_2.5"); Assert.IsTrue(arg.TryGet(out Vector2 v)); Assert.AreEqual(1.5f, v.x, 1e-4f); Assert.AreEqual(2.5f, v.y, 1e-4f); @@ -18,7 +18,7 @@ public void ParsesVector2WithUnderscores() [Test] public void ParsesVector3WithForwardSlashes() { - CommandArg arg = new CommandArg("1/2/3"); + CommandArg arg = new("1/2/3"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -28,7 +28,7 @@ public void ParsesVector3WithForwardSlashes() [Test] public void ParsesVector3WithBackslashes() { - CommandArg arg = new CommandArg("1\\2\\3"); + CommandArg arg = new("1\\2\\3"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -38,7 +38,7 @@ public void ParsesVector3WithBackslashes() [Test] public void ParsesVector4WithIgnoredCharacters() { - CommandArg arg = new CommandArg("`{(1|2|3|4)}`"); + CommandArg arg = new("`{(1|2|3|4)}`"); Assert.IsTrue(arg.TryGet(out Vector4 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -49,7 +49,7 @@ public void ParsesVector4WithIgnoredCharacters() [Test] public void ParsesVector3WithLabeledAndMixedWrappers() { - CommandArg arg = new CommandArg("[(x:1;y:2;z:3)]"); + CommandArg arg = new("[(x:1;y:2;z:3)]"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -59,7 +59,7 @@ public void ParsesVector3WithLabeledAndMixedWrappers() [Test] public void ParsesRectWithLabeledAndMixedDelimiters() { - CommandArg arg = new CommandArg("{x:10;y:20|width:100,height:50}"); + CommandArg arg = new("{x:10;y:20|width:100,height:50}"); Assert.IsTrue(arg.TryGet(out Rect r)); Assert.AreEqual(10f, r.x, 1e-4f); Assert.AreEqual(20f, r.y, 1e-4f); @@ -70,7 +70,7 @@ public void ParsesRectWithLabeledAndMixedDelimiters() [Test] public void ParsesQuaternionWithLabelsAndWrappers() { - CommandArg arg = new CommandArg("<(x:0.1;y:0.2|z:0.3,w:0.4)>"); + CommandArg arg = new("<(x:0.1;y:0.2|z:0.3,w:0.4)>"); Assert.IsTrue(arg.TryGet(out Quaternion q)); Assert.AreEqual(0.1f, q.x, 1e-4f); Assert.AreEqual(0.2f, q.y, 1e-4f); @@ -81,7 +81,7 @@ public void ParsesQuaternionWithLabelsAndWrappers() [Test] public void ParsesVector2IntWithWrappersAndUnderscore() { - CommandArg arg = new CommandArg(""); + CommandArg arg = new(""); Assert.IsTrue(arg.TryGet(out Vector2Int v)); Assert.AreEqual(-1, v.x); Assert.AreEqual(2, v.y); @@ -90,7 +90,7 @@ public void ParsesVector2IntWithWrappersAndUnderscore() [Test] public void ParsesVector3WithSemicolons() { - CommandArg arg = new CommandArg("1;2;3"); + CommandArg arg = new("1;2;3"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -100,7 +100,7 @@ public void ParsesVector3WithSemicolons() [Test] public void ParsesVector2WithColons() { - CommandArg arg = new CommandArg("1:2"); + CommandArg arg = new("1:2"); Assert.IsTrue(arg.TryGet(out Vector2 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -109,7 +109,7 @@ public void ParsesVector2WithColons() [Test] public void ParsesRectWithSemicolons() { - CommandArg arg = new CommandArg("1;2;3;4"); + CommandArg arg = new("1;2;3;4"); Assert.IsTrue(arg.TryGet(out Rect r)); Assert.AreEqual(1f, r.x, 1e-4f); Assert.AreEqual(2f, r.y, 1e-4f); @@ -120,7 +120,7 @@ public void ParsesRectWithSemicolons() [Test] public void ParsesVector3WithMixedDelimiters() { - CommandArg arg = new CommandArg("1;2,3"); + CommandArg arg = new("1;2,3"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); diff --git a/Tests/Runtime/UnityExtremeParsingTests.cs b/Tests/Runtime/UnityExtremeParsingTests.cs index e51df5c..0578cb1 100644 --- a/Tests/Runtime/UnityExtremeParsingTests.cs +++ b/Tests/Runtime/UnityExtremeParsingTests.cs @@ -9,7 +9,7 @@ public sealed class UnityExtremeParsingTests [Test] public void ExtremeVector3LargeMagnitudes() { - CommandArg arg = new CommandArg("1e20,-2e20,3.4e10"); + CommandArg arg = new("1e20,-2e20,3.4e10"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.IsFalse(float.IsNaN(v.x) || float.IsInfinity(v.x)); Assert.IsFalse(float.IsNaN(v.y) || float.IsInfinity(v.y)); @@ -22,7 +22,7 @@ public void ExtremeVector3LargeMagnitudes() [Test] public void ExtremeVector4LargeMagnitudes() { - CommandArg arg = new CommandArg("-5e15,6e15,-7e15,8e15"); + CommandArg arg = new("-5e15,6e15,-7e15,8e15"); Assert.IsTrue(arg.TryGet(out Vector4 v)); Assert.IsFalse(float.IsNaN(v.x) || float.IsInfinity(v.x)); Assert.IsFalse(float.IsNaN(v.y) || float.IsInfinity(v.y)); @@ -37,7 +37,7 @@ public void ExtremeVector4LargeMagnitudes() [Test] public void ExtremeRectLargeMagnitudes() { - CommandArg arg = new CommandArg("1e10,-1e10,2e10,3e10"); + CommandArg arg = new("1e10,-1e10,2e10,3e10"); Assert.IsTrue(arg.TryGet(out Rect r)); Assert.IsFalse(float.IsNaN(r.x) || float.IsInfinity(r.x)); Assert.IsFalse(float.IsNaN(r.y) || float.IsInfinity(r.y)); @@ -52,7 +52,7 @@ public void ExtremeRectLargeMagnitudes() [Test] public void RectAcceptsNegativeDimensions() { - CommandArg arg = new CommandArg("10,20,-5,-7"); + CommandArg arg = new("10,20,-5,-7"); Assert.IsTrue(arg.TryGet(out Rect r)); Assert.AreEqual(10f, r.x, 1e-4f); Assert.AreEqual(20f, r.y, 1e-4f); @@ -63,7 +63,7 @@ public void RectAcceptsNegativeDimensions() [Test] public void ExtremeQuaternionLargeMagnitudes() { - CommandArg arg = new CommandArg("1e10,2e10,3e10,4e10"); + CommandArg arg = new("1e10,2e10,3e10,4e10"); Assert.IsTrue(arg.TryGet(out Quaternion q)); Assert.IsFalse(float.IsNaN(q.x) || float.IsInfinity(q.x)); Assert.IsFalse(float.IsNaN(q.y) || float.IsInfinity(q.y)); @@ -78,7 +78,7 @@ public void ExtremeQuaternionLargeMagnitudes() [Test] public void ReorderedLabelsVector3() { - CommandArg arg = new CommandArg("z:3 y:2 x:1"); + CommandArg arg = new("z:3 y:2 x:1"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -88,7 +88,7 @@ public void ReorderedLabelsVector3() [Test] public void ReorderedLabelsRect() { - CommandArg arg = new CommandArg("width:100 height:50 y:20 x:10"); + CommandArg arg = new("width:100 height:50 y:20 x:10"); Assert.IsTrue(arg.TryGet(out Rect r)); Assert.AreEqual(10f, r.x, 1e-4f); Assert.AreEqual(20f, r.y, 1e-4f); @@ -99,7 +99,7 @@ public void ReorderedLabelsRect() [Test] public void ReorderedLabelsQuaternion() { - CommandArg arg = new CommandArg("w:0.4 z:0.3 y:0.2 x:0.1"); + CommandArg arg = new("w:0.4 z:0.3 y:0.2 x:0.1"); Assert.IsTrue(arg.TryGet(out Quaternion q)); Assert.AreEqual(0.1f, q.x, 1e-4f); Assert.AreEqual(0.2f, q.y, 1e-4f); diff --git a/Tests/Runtime/UnityLabelPermutationSuccessTests.cs b/Tests/Runtime/UnityLabelPermutationSuccessTests.cs index 962d306..4e31a74 100644 --- a/Tests/Runtime/UnityLabelPermutationSuccessTests.cs +++ b/Tests/Runtime/UnityLabelPermutationSuccessTests.cs @@ -9,7 +9,7 @@ public sealed class UnityLabelPermutationSuccessTests [Test] public void Vector2ReorderedLabels() { - CommandArg arg = new CommandArg("y:2 x:1"); + CommandArg arg = new("y:2 x:1"); Assert.IsTrue(arg.TryGet(out Vector2 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -18,7 +18,7 @@ public void Vector2ReorderedLabels() [Test] public void Vector4ReorderedLabels() { - CommandArg arg = new CommandArg("w:4 z:3 y:2 x:1"); + CommandArg arg = new("w:4 z:3 y:2 x:1"); Assert.IsTrue(arg.TryGet(out Vector4 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -29,7 +29,7 @@ public void Vector4ReorderedLabels() [Test] public void ColorWithLabeledComponentsAndWrappers() { - CommandArg arg = new CommandArg("{r:0.1 g:0.2 b:0.3 a:0.4}"); + CommandArg arg = new("{r:0.1 g:0.2 b:0.3 a:0.4}"); Assert.IsTrue(arg.TryGet(out Color c)); Assert.AreEqual(0.1f, c.r, 1e-4f); Assert.AreEqual(0.2f, c.g, 1e-4f); @@ -40,7 +40,7 @@ public void ColorWithLabeledComponentsAndWrappers() [Test] public void Vector2IntReorderedLabels() { - CommandArg arg = new CommandArg("y:5 x:-3"); + CommandArg arg = new("y:5 x:-3"); Assert.IsTrue(arg.TryGet(out Vector2Int v)); Assert.AreEqual(-3, v.x); Assert.AreEqual(5, v.y); @@ -49,7 +49,7 @@ public void Vector2IntReorderedLabels() [Test] public void Vector3IntReorderedLabels() { - CommandArg arg = new CommandArg("z:9 y:8 x:7"); + CommandArg arg = new("z:9 y:8 x:7"); Assert.IsTrue(arg.TryGet(out Vector3Int v)); Assert.AreEqual(7, v.x); Assert.AreEqual(8, v.y); diff --git a/Tests/Runtime/UnityLabeledParsingTests.cs b/Tests/Runtime/UnityLabeledParsingTests.cs index 42fabed..a320726 100644 --- a/Tests/Runtime/UnityLabeledParsingTests.cs +++ b/Tests/Runtime/UnityLabeledParsingTests.cs @@ -9,7 +9,7 @@ public sealed class UnityLabeledParsingTests [Test] public void ParsesVector3WithLabeledComponents() { - CommandArg arg = new CommandArg("x:1.1 y:2.2 z:3.3"); + CommandArg arg = new("x:1.1 y:2.2 z:3.3"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1.1f, v.x, 1e-4f); Assert.AreEqual(2.2f, v.y, 1e-4f); @@ -19,7 +19,7 @@ public void ParsesVector3WithLabeledComponents() [Test] public void ParsesRectWithLabeledComponents() { - CommandArg arg = new CommandArg("x:10 y:20 width:100 height:50"); + CommandArg arg = new("x:10 y:20 width:100 height:50"); Assert.IsTrue(arg.TryGet(out Rect r)); Assert.AreEqual(10f, r.x, 1e-4f); Assert.AreEqual(20f, r.y, 1e-4f); @@ -30,7 +30,7 @@ public void ParsesRectWithLabeledComponents() [Test] public void ParsesQuaternionWithLabeledComponents() { - CommandArg arg = new CommandArg("x:0.1,y:0.2,z:0.3,w:0.4"); + CommandArg arg = new("x:0.1,y:0.2,z:0.3,w:0.4"); Assert.IsTrue(arg.TryGet(out Quaternion q)); Assert.AreEqual(0.1f, q.x, 1e-4f); Assert.AreEqual(0.2f, q.y, 1e-4f); @@ -41,7 +41,7 @@ public void ParsesQuaternionWithLabeledComponents() [Test] public void ParsesVector2IntWithLabeledComponents() { - CommandArg arg = new CommandArg("x:-3 y:5"); + CommandArg arg = new("x:-3 y:5"); Assert.IsTrue(arg.TryGet(out Vector2Int v)); Assert.AreEqual(-3, v.x); Assert.AreEqual(5, v.y); diff --git a/Tests/Runtime/UnityMalformedParsingTests.cs b/Tests/Runtime/UnityMalformedParsingTests.cs index fb0b965..7bc1078 100644 --- a/Tests/Runtime/UnityMalformedParsingTests.cs +++ b/Tests/Runtime/UnityMalformedParsingTests.cs @@ -9,105 +9,105 @@ public sealed class UnityMalformedParsingTests [Test] public void Vector3MalformedTooFewComponents() { - CommandArg arg = new CommandArg("x: y:2 z:"); + CommandArg arg = new("x: y:2 z:"); Assert.IsFalse(arg.TryGet(out Vector3 _)); } [Test] public void Vector2IntMalformedMissingNumeric() { - CommandArg arg = new CommandArg("x: y:5"); + CommandArg arg = new("x: y:5"); Assert.IsFalse(arg.TryGet(out Vector2Int _)); } [Test] public void QuaternionMalformedTooFewComponents() { - CommandArg arg = new CommandArg("x:0.1 y:0.2 z:0.3"); + CommandArg arg = new("x:0.1 y:0.2 z:0.3"); Assert.IsFalse(arg.TryGet(out Quaternion _)); } [Test] public void RectMalformedMissingWidth() { - CommandArg arg = new CommandArg("x:10 y:20 width: height:50"); + CommandArg arg = new("x:10 y:20 width: height:50"); Assert.IsFalse(arg.TryGet(out Rect _)); } [Test] public void RectIntMalformedTooFewNumbers() { - CommandArg arg = new CommandArg("x:10 y:20"); + CommandArg arg = new("x:10 y:20"); Assert.IsFalse(arg.TryGet(out RectInt _)); } [Test] public void ColorMalformedNonNumericRgba() { - CommandArg arg = new CommandArg("RGBA(0.1, nope, 0.3, 0.4)"); + CommandArg arg = new("RGBA(0.1, nope, 0.3, 0.4)"); Assert.IsFalse(arg.TryGet(out Color _)); } [Test] public void Vector2MixedInvalidToken() { - CommandArg arg = new CommandArg("1,foo"); + CommandArg arg = new("1,foo"); Assert.IsFalse(arg.TryGet(out Vector2 _)); } [Test] public void Vector3TooManyComponents() { - CommandArg arg = new CommandArg("1,2,3,4"); + CommandArg arg = new("1,2,3,4"); Assert.IsFalse(arg.TryGet(out Vector3 _)); } [Test] public void Vector4SingleComponentOnly() { - CommandArg arg = new CommandArg("1"); + CommandArg arg = new("1"); Assert.IsFalse(arg.TryGet(out Vector4 _)); } [Test] public void Vector2IntNonIntegerComponent() { - CommandArg arg = new CommandArg("1,2.5"); + CommandArg arg = new("1,2.5"); Assert.IsFalse(arg.TryGet(out Vector2Int _)); } [Test] public void Vector3IntTooManyComponents() { - CommandArg arg = new CommandArg("1,2,3,4"); + CommandArg arg = new("1,2,3,4"); Assert.IsFalse(arg.TryGet(out Vector3Int _)); } [Test] public void RectNonNumericComponent() { - CommandArg arg = new CommandArg("1,2,three,4"); + CommandArg arg = new("1,2,three,4"); Assert.IsFalse(arg.TryGet(out Rect _)); } [Test] public void RectIntNonIntegerComponent() { - CommandArg arg = new CommandArg("1,2,3.5,4"); + CommandArg arg = new("1,2,3.5,4"); Assert.IsFalse(arg.TryGet(out RectInt _)); } [Test] public void QuaternionNonNumericComponent() { - CommandArg arg = new CommandArg("0.1, nope, 0.3, 0.4"); + CommandArg arg = new("0.1, nope, 0.3, 0.4"); Assert.IsFalse(arg.TryGet(out Quaternion _)); } [Test] public void Vector3OnlyWrappers() { - CommandArg arg = new CommandArg("[]"); + CommandArg arg = new("[]"); Assert.IsFalse(arg.TryGet(out Vector3 _)); arg = new CommandArg("<>"); Assert.IsFalse(arg.TryGet(out Vector3 _)); @@ -120,7 +120,7 @@ public void Vector3OnlyWrappers() [Test] public void RectOnlyDelimiters() { - CommandArg arg = new CommandArg(",,,"); + CommandArg arg = new(",,,"); Assert.IsFalse(arg.TryGet(out Rect _)); arg = new CommandArg("___"); Assert.IsFalse(arg.TryGet(out Rect _)); @@ -133,42 +133,42 @@ public void RectOnlyDelimiters() [Test] public void Vector2OnlyDelimiter() { - CommandArg arg = new CommandArg(":"); + CommandArg arg = new(":"); Assert.IsFalse(arg.TryGet(out Vector2 _)); } [Test] public void ColorInvalidNamedComponents() { - CommandArg arg = new CommandArg("red, green, blue"); + CommandArg arg = new("red, green, blue"); Assert.IsFalse(arg.TryGet(out Color _)); } [Test] public void Vector3DuplicateLabelMissingValue() { - CommandArg arg = new CommandArg("x:1 x: y:3"); + CommandArg arg = new("x:1 x: y:3"); Assert.IsFalse(arg.TryGet(out Vector3 _)); } [Test] public void RectDuplicateLabelMissingValue() { - CommandArg arg = new CommandArg("x:10 y:20 width:100 width: height:50"); + CommandArg arg = new("x:10 y:20 width:100 width: height:50"); Assert.IsFalse(arg.TryGet(out Rect _)); } [Test] public void QuaternionDuplicateLabelMissingValue() { - CommandArg arg = new CommandArg("x:0.1 y:0.2 z:0.3 w:"); + CommandArg arg = new("x:0.1 y:0.2 z:0.3 w:"); Assert.IsFalse(arg.TryGet(out Quaternion _)); } [Test] public void Vector2IntDuplicateLabelMissingValue() { - CommandArg arg = new CommandArg("x:1 x: y:2"); + CommandArg arg = new("x:1 x: y:2"); Assert.IsFalse(arg.TryGet(out Vector2Int _)); } } diff --git a/Tests/Runtime/UnityQuotedWrapperParsingTests.cs b/Tests/Runtime/UnityQuotedWrapperParsingTests.cs index ce3d708..2e6fed6 100644 --- a/Tests/Runtime/UnityQuotedWrapperParsingTests.cs +++ b/Tests/Runtime/UnityQuotedWrapperParsingTests.cs @@ -9,7 +9,7 @@ public sealed class UnityQuotedWrapperParsingTests [Test] public void ParsesVector3WrappedInSingleQuotes() { - CommandArg arg = new CommandArg("'[(1,2,3)]'"); + CommandArg arg = new("'[(1,2,3)]'"); Assert.IsTrue(arg.TryGet(out Vector3 v)); Assert.AreEqual(1f, v.x, 1e-4f); Assert.AreEqual(2f, v.y, 1e-4f); @@ -19,7 +19,7 @@ public void ParsesVector3WrappedInSingleQuotes() [Test] public void ParsesRectWrappedInSingleQuotes() { - CommandArg arg = new CommandArg("'{1;2;3;4}'"); + CommandArg arg = new("'{1;2;3;4}'"); Assert.IsTrue(arg.TryGet(out Rect r)); Assert.AreEqual(1f, r.x, 1e-4f); Assert.AreEqual(2f, r.y, 1e-4f); diff --git a/Tests/Runtime/UntypedUnityParsingFailureTests.cs b/Tests/Runtime/UntypedUnityParsingFailureTests.cs index 5e62b1c..6acd221 100644 --- a/Tests/Runtime/UntypedUnityParsingFailureTests.cs +++ b/Tests/Runtime/UntypedUnityParsingFailureTests.cs @@ -9,7 +9,7 @@ public sealed class UntypedUnityParsingFailureTests [Test] public void Vector3UntypedTwoComponentsParses() { - CommandArg arg = new CommandArg("1,2"); + CommandArg arg = new("1,2"); Assert.IsTrue(arg.TryGet(typeof(Vector3), out object obj)); Vector3 v = (Vector3)obj; Assert.AreEqual(1f, v.x, 1e-4f); @@ -20,42 +20,42 @@ public void Vector3UntypedTwoComponentsParses() [Test] public void RectUntypedNonNumeric() { - CommandArg arg = new CommandArg("1,2,three,4"); + CommandArg arg = new("1,2,three,4"); Assert.IsFalse(arg.TryGet(typeof(Rect), out object _)); } [Test] public void Vector2IntUntypedNonInteger() { - CommandArg arg = new CommandArg("1,2.5"); + CommandArg arg = new("1,2.5"); Assert.IsFalse(arg.TryGet(typeof(Vector2Int), out object _)); } [Test] public void QuaternionUntypedTooManyComponents() { - CommandArg arg = new CommandArg("0.1,0.2,0.3,0.4,0.5"); + CommandArg arg = new("0.1,0.2,0.3,0.4,0.5"); Assert.IsFalse(arg.TryGet(typeof(Quaternion), out object _)); } [Test] public void ColorUntypedNonNumericRgba() { - CommandArg arg = new CommandArg("RGBA(0.1, nope, 0.3, 0.4)"); + CommandArg arg = new("RGBA(0.1, nope, 0.3, 0.4)"); Assert.IsFalse(arg.TryGet(typeof(Color), out object _)); } [Test] public void Vector4UntypedTooManyComponents() { - CommandArg arg = new CommandArg("1,2,3,4,5"); + CommandArg arg = new("1,2,3,4,5"); Assert.IsFalse(arg.TryGet(typeof(Vector4), out object _)); } [Test] public void ColorUntypedTooFewComponents() { - CommandArg arg = new CommandArg("0.1,0.2"); + CommandArg arg = new("0.1,0.2"); Assert.IsFalse(arg.TryGet(typeof(Color), out object _)); } } diff --git a/Tests/Runtime/UntypedUnityParsingTests.cs b/Tests/Runtime/UntypedUnityParsingTests.cs index d39c492..0410b82 100644 --- a/Tests/Runtime/UntypedUnityParsingTests.cs +++ b/Tests/Runtime/UntypedUnityParsingTests.cs @@ -9,14 +9,14 @@ public sealed class UntypedUnityParsingTests [Test] public void ParsesUntypedUnityTypes() { - CommandArg v3Arg = new CommandArg("1,2,3"); + CommandArg v3Arg = new("1,2,3"); Assert.IsTrue(v3Arg.TryGet(typeof(Vector3), out object v3Obj)); Vector3 v3 = (Vector3)v3Obj; Assert.AreEqual(1f, v3.x, 1e-4f); Assert.AreEqual(2f, v3.y, 1e-4f); Assert.AreEqual(3f, v3.z, 1e-4f); - CommandArg rectArg = new CommandArg("1,2,3,4"); + CommandArg rectArg = new("1,2,3,4"); Assert.IsTrue(rectArg.TryGet(typeof(Rect), out object rectObj)); Rect r = (Rect)rectObj; Assert.AreEqual(1f, r.x, 1e-4f); @@ -24,7 +24,7 @@ public void ParsesUntypedUnityTypes() Assert.AreEqual(3f, r.width, 1e-4f); Assert.AreEqual(4f, r.height, 1e-4f); - CommandArg qArg = new CommandArg("0.1,0.2,0.3,0.4"); + CommandArg qArg = new("0.1,0.2,0.3,0.4"); Assert.IsTrue(qArg.TryGet(typeof(Quaternion), out object qObj)); Quaternion q = (Quaternion)qObj; Assert.AreEqual(0.1f, q.x, 1e-4f); @@ -32,7 +32,7 @@ public void ParsesUntypedUnityTypes() Assert.AreEqual(0.3f, q.z, 1e-4f); Assert.AreEqual(0.4f, q.w, 1e-4f); - CommandArg riArg = new CommandArg("1,2,3,4"); + CommandArg riArg = new("1,2,3,4"); Assert.IsTrue(riArg.TryGet(typeof(RectInt), out object riObj)); RectInt ri = (RectInt)riObj; Assert.AreEqual(1, ri.x); @@ -44,7 +44,7 @@ public void ParsesUntypedUnityTypes() [Test] public void ParsesMoreUntypedUnityTypes() { - CommandArg v4Arg = new CommandArg("1,2,3,4"); + CommandArg v4Arg = new("1,2,3,4"); Assert.IsTrue(v4Arg.TryGet(typeof(Vector4), out object v4Obj)); Vector4 v4 = (Vector4)v4Obj; Assert.AreEqual(1f, v4.x, 1e-4f); @@ -52,20 +52,20 @@ public void ParsesMoreUntypedUnityTypes() Assert.AreEqual(3f, v4.z, 1e-4f); Assert.AreEqual(4f, v4.w, 1e-4f); - CommandArg v2iArg = new CommandArg("-1,2"); + CommandArg v2iArg = new("-1,2"); Assert.IsTrue(v2iArg.TryGet(typeof(Vector2Int), out object v2iObj)); Vector2Int v2i = (Vector2Int)v2iObj; Assert.AreEqual(-1, v2i.x); Assert.AreEqual(2, v2i.y); - CommandArg v3iArg = new CommandArg("7,8,9"); + CommandArg v3iArg = new("7,8,9"); Assert.IsTrue(v3iArg.TryGet(typeof(Vector3Int), out object v3iObj)); Vector3Int v3i = (Vector3Int)v3iObj; Assert.AreEqual(7, v3i.x); Assert.AreEqual(8, v3i.y); Assert.AreEqual(9, v3i.z); - CommandArg colorArg = new CommandArg("RGBA(0.1,0.2,0.3,0.4)"); + CommandArg colorArg = new("RGBA(0.1,0.2,0.3,0.4)"); Assert.IsTrue(colorArg.TryGet(typeof(Color), out object cObj)); Color c = (Color)cObj; Assert.AreEqual(0.1f, c.r, 1e-4f); diff --git a/Tests/Runtime/VectorParsingTests.cs b/Tests/Runtime/VectorParsingTests.cs index 75bd7c9..f78c12d 100644 --- a/Tests/Runtime/VectorParsingTests.cs +++ b/Tests/Runtime/VectorParsingTests.cs @@ -9,7 +9,7 @@ public sealed class VectorParsingTests [Test] public void Vector3ParsesVariousDelimiters() { - CommandArg arg = new CommandArg("1.1,2.2,3.3"); + CommandArg arg = new("1.1,2.2,3.3"); Assert.IsTrue(arg.TryGet(out Vector3 v1)); Assert.AreEqual(1.1f, v1.x, 1e-4f); Assert.AreEqual(2.2f, v1.y, 1e-4f); @@ -25,7 +25,7 @@ public void Vector3ParsesVariousDelimiters() [Test] public void ColorParsesRgba() { - CommandArg arg = new CommandArg("RGBA(0.1,0.2,0.3,0.4)"); + CommandArg arg = new("RGBA(0.1,0.2,0.3,0.4)"); Assert.IsTrue(arg.TryGet(out Color c)); Assert.AreEqual(0.1f, c.r, 1e-4f); Assert.AreEqual(0.2f, c.g, 1e-4f); From 2ad47a3011a9e44fb26432f29a5202750ed124f4 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 10:31:00 -0700 Subject: [PATCH 14/69] Slightly better launcher --- Runtime/CommandTerminal/UI/TerminalUI.cs | 80 ++++++++++++++++++++++-- Tests/Runtime/LauncherModeTests.cs | 45 +++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 9f820c6..1b3ff3e 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -775,10 +775,35 @@ private void ResetWindowIdempotent() } case TerminalState.OpenLauncher: { - _launcherMetrics = _launcherSettings.ComputeMetrics(width, height); + LauncherLayoutMetrics computedMetrics = _launcherSettings.ComputeMetrics( + width, + height + ); + float reservedEstimate = Mathf.Max( + _launcherSettings.inputReservePixels, + 48f + ); + float estimatedMinimumHeight = + (computedMetrics.InsetPadding * 2f) + reservedEstimate; + _launcherMetrics = computedMetrics; _launcherMetricsInitialized = true; _realWindowHeight = _launcherMetrics.Height; - _targetWindowHeight = _realWindowHeight; + if (!wasLauncher) + { + _targetWindowHeight = Mathf.Clamp( + estimatedMinimumHeight, + 0f, + _launcherMetrics.Height + ); + } + else + { + _targetWindowHeight = Mathf.Clamp( + _targetWindowHeight, + 0f, + _launcherMetrics.Height + ); + } break; } default: @@ -1410,7 +1435,7 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _logScrollView.style.marginTop = 0; } - _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden; + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; _autoCompleteContainer.style.position = Position.Absolute; _autoCompleteContainer.style.maxHeight = _launcherMetrics.HistoryHeight; @@ -2393,6 +2418,23 @@ internal void SetLauncherMetricsForTests( _launcherMetricsInitialized = initialized; } + internal LauncherLayoutMetrics LauncherMetricsForTests => _launcherMetrics; + + internal float TargetWindowHeightForTests => _targetWindowHeight; + + internal float CurrentWindowHeightForTests => _currentWindowHeight; + + internal void SetWindowHeightsForTests( + float currentHeight, + float targetHeight, + bool isAnimating = false + ) + { + _currentWindowHeight = currentHeight; + _targetWindowHeight = targetHeight; + _isAnimating = isAnimating; + } + internal void SetLogScrollViewForTests(ScrollView scrollView) { _logScrollView = scrollView; @@ -2403,6 +2445,11 @@ internal void RefreshLauncherHistoryForTests(IReadOnlyList logs) RefreshLauncherHistory(logs); } + internal void ResetWindowForTests() + { + ResetWindowIdempotent(); + } + private void ResetLauncherSettings() { Debug.LogWarning( @@ -2720,6 +2767,7 @@ private void UpdateLauncherLayoutMetrics() else { _autoCompleteContainer.style.display = DisplayStyle.None; + _autoCompleteContainer.style.height = 0; } float suggestionsHeight = hasSuggestions @@ -2749,6 +2797,21 @@ private void UpdateLauncherLayoutMetrics() suggestionsHeight = Mathf.Max(computedHeight, suggestionsHeight); } + if (hasSuggestions && suggestionsHeight <= 0f) + { + int childCount = _autoCompleteContainer.contentContainer.childCount; + suggestionsHeight = Mathf.Min( + _launcherMetrics.HistoryHeight, + Mathf.Max(0f, childCount * 28f) + ); + } + + suggestionsHeight = Mathf.Min(suggestionsHeight, _launcherMetrics.HistoryHeight); + if (hasSuggestions) + { + _autoCompleteContainer.style.height = Mathf.Max(0f, suggestionsHeight); + } + float reservedForSuggestions = ( hasSuggestions ? suggestionsHeight + LauncherAutoCompleteSpacing @@ -2786,10 +2849,19 @@ private void UpdateLauncherLayoutMetrics() if (!Mathf.Approximately(clampedHeight, _targetWindowHeight)) { - _initialWindowHeight = _currentWindowHeight; + _initialWindowHeight = Mathf.Clamp( + _currentWindowHeight, + minimumHeight, + _launcherMetrics.Height + ); _targetWindowHeight = clampedHeight; _animationTimer = 0f; _isAnimating = true; + if (Mathf.Approximately(_initialWindowHeight, clampedHeight)) + { + _currentWindowHeight = clampedHeight; + _isAnimating = false; + } } float availableForHistory = diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index da0f680..9ce77ba 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -126,5 +126,50 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() yield return TestSceneHelpers.DestroyTerminalAndWait(); } + + [UnityTest] + public IEnumerator LauncherResetMaintainsDynamicTargetHeight() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + terminal.ToggleLauncher(); + yield return TestSceneHelpers.WaitFrames(2); + + Assert.That(terminal.CurrentStateForTests, Is.EqualTo(TerminalState.OpenLauncher)); + Assert.That(terminal.LauncherMetricsInitializedForTests, Is.True); + + float launcherMaxHeight = terminal.LauncherMetricsForTests.Height; + Assert.That(launcherMaxHeight, Is.GreaterThan(0f)); + + float reducedTarget = Mathf.Max(60f, launcherMaxHeight * 0.25f); + terminal.SetWindowHeightsForTests(reducedTarget, reducedTarget); + + Assert.That( + terminal.TargetWindowHeightForTests, + Is.EqualTo(reducedTarget).Within(0.001f) + ); + + terminal.ResetWindowForTests(); + + Assert.That( + terminal.TargetWindowHeightForTests, + Is.EqualTo(reducedTarget).Within(0.001f) + ); + + float excessiveTarget = launcherMaxHeight * 1.5f; + terminal.SetWindowHeightsForTests(excessiveTarget, excessiveTarget); + + terminal.ResetWindowForTests(); + + Assert.That( + terminal.TargetWindowHeightForTests, + Is.EqualTo(terminal.LauncherMetricsForTests.Height).Within(0.001f) + ); + + yield return TestSceneHelpers.DestroyTerminalAndWait(); + } } } From 4c660f8837141d2915e6320df4c00a9a42cd2332 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 11:19:32 -0700 Subject: [PATCH 15/69] Command Terminal updates --- Runtime/CommandTerminal/UI/TerminalUI.cs | 140 ++++++++++++++--------- Styles/BaseStyles.uss | 45 +++++++- 2 files changed, 127 insertions(+), 58 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 1b3ff3e..4f863e3 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -22,6 +22,8 @@ public sealed class TerminalUI : MonoBehaviour { private const string TerminalRootName = "TerminalRoot"; private const float LauncherAutoCompleteSpacing = 6f; + private const float LauncherEstimatedSuggestionRowHeight = 28f; + private const float LauncherEstimatedHistoryRowHeight = 24f; private enum ScrollBarCaptureState { @@ -1371,7 +1373,15 @@ private void ApplyStandardLayout(float screenWidth) _autoCompleteContainer.style.top = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.width = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.maxHeight = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.maxWidth = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.height = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.marginBottom = 0; + _autoCompleteContainer.style.marginTop = 0; + _autoCompleteContainer.style.marginLeft = 0; + _autoCompleteContainer.style.marginRight = 0; + _autoCompleteContainer.style.flexGrow = StyleKeyword.Null; + _autoCompleteContainer.style.flexShrink = StyleKeyword.Null; + _autoCompleteContainer.style.alignSelf = StyleKeyword.Null; _inputContainer.style.marginBottom = 0; EnsureChildOrder( @@ -1437,11 +1447,25 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; - _autoCompleteContainer.style.position = Position.Absolute; + _autoCompleteContainer.style.position = Position.Relative; + _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.top = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.width = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.maxHeight = _launcherMetrics.HistoryHeight; _autoCompleteContainer.style.display = DisplayStyle.None; + _autoCompleteContainer.style.marginTop = 0; + _autoCompleteContainer.style.marginBottom = 0; + _autoCompleteContainer.style.marginLeft = 0; + _autoCompleteContainer.style.marginRight = 0; + _autoCompleteContainer.style.flexGrow = 0; + _autoCompleteContainer.style.flexShrink = 0; - EnsureChildOrder(_terminalContainer, _inputContainer, _logScrollView); + EnsureChildOrder( + _terminalContainer, + _inputContainer, + _autoCompleteContainer, + _logScrollView + ); } private void RefreshUI() @@ -2749,68 +2773,54 @@ private void UpdateLauncherLayoutMetrics() float padding = _launcherMetrics.InsetPadding; float inputHeight = Mathf.Max(_inputContainer.resolvedStyle.height, 0f); - float availableWidth = _launcherMetrics.Width - (padding * 2f); - if (availableWidth < 0f) + float availableWidth = Mathf.Max(0f, _launcherMetrics.Width - (padding * 2f)); + _autoCompleteContainer.style.width = availableWidth; + _autoCompleteContainer.style.maxWidth = availableWidth; + _autoCompleteContainer.style.alignSelf = Align.Stretch; + _autoCompleteContainer.style.flexGrow = 0; + _autoCompleteContainer.style.flexShrink = 0; + + bool hasSuggestions = false; + int suggestionChildCount = _autoCompleteContainer.contentContainer.childCount; + for (int i = 0; i < suggestionChildCount; ++i) { - availableWidth = 0f; - } + VisualElement suggestion = _autoCompleteContainer.contentContainer[i]; + if (suggestion == null) + { + continue; + } - float suggestionTop = padding + inputHeight + LauncherAutoCompleteSpacing; - bool hasSuggestions = _autoCompleteContainer.childCount > 0; - _autoCompleteContainer.style.left = padding; - _autoCompleteContainer.style.width = availableWidth; + if (suggestion.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + hasSuggestions = true; + } + if (!hasSuggestions && suggestionChildCount > 0) + { + hasSuggestions = true; + } if (hasSuggestions) { _autoCompleteContainer.style.display = DisplayStyle.Flex; - _autoCompleteContainer.style.top = suggestionTop; + _autoCompleteContainer.style.marginTop = LauncherAutoCompleteSpacing; } else { _autoCompleteContainer.style.display = DisplayStyle.None; _autoCompleteContainer.style.height = 0; + _autoCompleteContainer.style.marginTop = 0; } float suggestionsHeight = hasSuggestions - ? Mathf.Max( - _autoCompleteContainer.contentContainer.layout.height, - _autoCompleteContainer.resolvedStyle.height - ) + ? Mathf.Min(LauncherEstimatedSuggestionRowHeight, _launcherMetrics.HistoryHeight) : 0f; - - if (hasSuggestions && suggestionsHeight <= 0f) - { - float computedHeight = 0f; - int childCount = _autoCompleteContainer.contentContainer.childCount; - for (int i = 0; i < childCount; ++i) - { - VisualElement child = _autoCompleteContainer.contentContainer[i]; - if (child == null) - { - continue; - } - - computedHeight += child.resolvedStyle.height; - computedHeight += - child.resolvedStyle.marginTop + child.resolvedStyle.marginBottom; - } - - suggestionsHeight = Mathf.Max(computedHeight, suggestionsHeight); - } - - if (hasSuggestions && suggestionsHeight <= 0f) - { - int childCount = _autoCompleteContainer.contentContainer.childCount; - suggestionsHeight = Mathf.Min( - _launcherMetrics.HistoryHeight, - Mathf.Max(0f, childCount * 28f) - ); - } - - suggestionsHeight = Mathf.Min(suggestionsHeight, _launcherMetrics.HistoryHeight); if (hasSuggestions) { _autoCompleteContainer.style.height = Mathf.Max(0f, suggestionsHeight); } + _autoCompleteContainer.style.marginBottom = 0; float reservedForSuggestions = ( hasSuggestions @@ -2818,12 +2828,30 @@ private void UpdateLauncherLayoutMetrics() : LauncherAutoCompleteSpacing ); - float historyContentHeight = Mathf.Max( - _logScrollView.contentContainer.layout.height, - _logScrollView.resolvedStyle.height - ); + VisualElement historyContent = _logScrollView.contentContainer; + int visibleHistoryCount = 0; + int historyChildCount = historyContent.childCount; + for (int i = 0; i < historyChildCount; ++i) + { + VisualElement entry = historyContent[i]; + if (entry == null || entry.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + visibleHistoryCount++; + } + + if (visibleHistoryCount == 0) + { + int pendingLogs = Terminal.Buffer?.Logs != null ? Terminal.Buffer.Logs.Count : 0; + visibleHistoryCount = Mathf.Min( + pendingLogs, + _launcherMetrics.HistoryVisibleEntryCount + ); + } + float desiredHistoryHeight = Mathf.Min( - historyContentHeight, + visibleHistoryCount * LauncherEstimatedHistoryRowHeight, _launcherMetrics.HistoryHeight ); if (desiredHistoryHeight < 0f) @@ -2831,11 +2859,14 @@ private void UpdateLauncherLayoutMetrics() desiredHistoryHeight = 0f; } - if (_logScrollView.contentContainer.childCount > 0) + if (visibleHistoryCount > 0) { desiredHistoryHeight = Mathf.Max( desiredHistoryHeight, - Mathf.Min(48f, _launcherMetrics.HistoryHeight) + Mathf.Min( + visibleHistoryCount * LauncherEstimatedHistoryRowHeight, + _launcherMetrics.HistoryHeight + ) ); } @@ -2882,7 +2913,8 @@ private void UpdateLauncherLayoutMetrics() _logScrollView.style.maxHeight = availableForHistory; } - _logScrollView.style.marginTop = reservedForSuggestions; + float logTopMargin = LauncherAutoCompleteSpacing; + _logScrollView.style.marginTop = logTopMargin; } private void StartHeightAnimation() diff --git a/Styles/BaseStyles.uss b/Styles/BaseStyles.uss index 79a6e78..28c6c4d 100644 --- a/Styles/BaseStyles.uss +++ b/Styles/BaseStyles.uss @@ -79,7 +79,7 @@ background-color: transparent; flex-direction: row; flex-shrink: 0; - left: 2px; + overflow: visible; } .autocomplete-popup #unity-low-button, @@ -88,6 +88,38 @@ display: none; } +.autocomplete-popup .unity-scroll-view__content-viewport, +.log-scroll-view .unity-scroll-view__content-viewport { + background-color: transparent; + height: 100%; + min-height: 0; + flex-grow: 1; + flex-shrink: 1; +} + +.autocomplete-popup .unity-scroll-view__content-viewport { + overflow: visible; +} + +.log-scroll-view .unity-scroll-view__content-viewport { + overflow: hidden; +} + +.autocomplete-popup .unity-scroll-view__content-container { + padding: 0; + min-height: 0; + flex-direction: row; + align-items: center; + flex-grow: 1; +} + +.log-scroll-view .unity-scroll-view__content-container { + padding: 0; + min-height: 0; + flex-direction: column; + flex-grow: 1; +} + .input-container { flex-direction: row; flex-shrink: 0; @@ -101,7 +133,6 @@ position: absolute; flex-direction: row; left: 2px; - gap: 6px; align-items: center; } @@ -171,8 +202,14 @@ .terminal-container--launcher { background-color: var(--terminal-bg); border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.05); - box-shadow: 0 32px 60px rgba(0, 0, 0, 0.45); + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + border-top-color: rgba(255, 255, 255, 0.05); + border-right-color: rgba(255, 255, 255, 0.05); + border-bottom-color: rgba(255, 255, 255, 0.05); + border-left-color: rgba(255, 255, 255, 0.05); transition: transform 0.12s ease, opacity 0.12s ease; } From 8d5af2dfba1af75657a8b3a5ea45440a2aed65b3 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 13:49:52 -0700 Subject: [PATCH 16/69] Progress --- Runtime/CommandTerminal/UI/TerminalUI.cs | 163 +++++++++++++++++++---- 1 file changed, 140 insertions(+), 23 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 4f863e3..d0a18c5 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -22,8 +22,8 @@ public sealed class TerminalUI : MonoBehaviour { private const string TerminalRootName = "TerminalRoot"; private const float LauncherAutoCompleteSpacing = 6f; - private const float LauncherEstimatedSuggestionRowHeight = 28f; - private const float LauncherEstimatedHistoryRowHeight = 24f; + private const float LauncherEstimatedSuggestionRowHeight = 32f; + private const float LauncherEstimatedHistoryRowHeight = 28f; private enum ScrollBarCaptureState { @@ -230,6 +230,8 @@ private enum ScrollBarCaptureState private VisualElement _terminalContainer; private ScrollView _logScrollView; private ScrollView _autoCompleteContainer; + private VisualElement _autoCompleteViewport; + private VisualElement _logViewport; private VisualElement _inputContainer; private TextField _commandInput; private Button _runButton; @@ -246,6 +248,9 @@ private enum ScrollBarCaptureState ); private readonly List _autoCompleteChildren = new(); + private float _launcherSuggestionContentHeight; + private float _launcherHistoryContentHeight; + // Cached for performance (avoids allocations) private readonly Action _focusInput; #if UNITY_EDITOR @@ -757,6 +762,11 @@ private void ResetWindowIdempotent() int width = Screen.width; float oldTargetHeight = _targetWindowHeight; bool wasLauncher = _launcherMetricsInitialized; + if (_state != TerminalState.OpenLauncher) + { + _launcherSuggestionContentHeight = 0f; + _launcherHistoryContentHeight = 0f; + } try { switch (_state) @@ -789,6 +799,8 @@ private void ResetWindowIdempotent() (computedMetrics.InsetPadding * 2f) + reservedEstimate; _launcherMetrics = computedMetrics; _launcherMetricsInitialized = true; + _launcherSuggestionContentHeight = 0f; + _launcherHistoryContentHeight = 0f; _realWindowHeight = _launcherMetrics.Height; if (!wasLauncher) { @@ -893,6 +905,19 @@ private void SetupUI() _logScrollView.name = "LogScrollView"; _logScrollView.AddToClassList("log-scroll-view"); _terminalContainer.Add(_logScrollView); + _logViewport = _logScrollView.contentViewport; + if (_logViewport != null) + { + _logViewport.style.flexGrow = 1f; + _logViewport.style.flexShrink = 1f; + _logViewport.style.minHeight = 0f; + _logViewport.style.overflow = Overflow.Hidden; + } + VisualElement logContent = _logScrollView.contentContainer; + logContent.style.flexDirection = FlexDirection.Column; + logContent.style.alignItems = Align.Stretch; + logContent.style.minHeight = 0f; + logContent.RegisterCallback(OnLogContentGeometryChanged); _autoCompleteContainer = new ScrollView(ScrollViewMode.Horizontal) { @@ -900,6 +925,25 @@ private void SetupUI() }; _autoCompleteContainer.AddToClassList("autocomplete-popup"); _terminalContainer.Add(_autoCompleteContainer); + _autoCompleteViewport = _autoCompleteContainer.contentViewport; + if (_autoCompleteViewport != null) + { + _autoCompleteViewport.style.flexDirection = FlexDirection.Row; + _autoCompleteViewport.style.flexGrow = 0f; + _autoCompleteViewport.style.flexShrink = 0f; + _autoCompleteViewport.style.minHeight = 0f; + _autoCompleteViewport.style.overflow = Overflow.Visible; + _autoCompleteViewport.RegisterCallback( + OnAutoCompleteGeometryChanged + ); + } + VisualElement autoContent = _autoCompleteContainer.contentContainer; + autoContent.style.flexDirection = FlexDirection.Row; + autoContent.style.alignItems = Align.Center; + autoContent.style.minHeight = 0f; + autoContent.style.justifyContent = Justify.FlexStart; + autoContent.style.flexWrap = Wrap.NoWrap; + autoContent.RegisterCallback(OnAutoCompleteGeometryChanged); _inputContainer = new VisualElement { name = "InputContainer" }; _inputContainer.AddToClassList("input-container"); @@ -1375,6 +1419,7 @@ private void ApplyStandardLayout(float screenWidth) _autoCompleteContainer.style.maxHeight = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.maxWidth = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.height = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.minHeight = new StyleLength(StyleKeyword.Null); _autoCompleteContainer.style.marginBottom = 0; _autoCompleteContainer.style.marginTop = 0; _autoCompleteContainer.style.marginLeft = 0; @@ -1442,6 +1487,7 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _logScrollView.style.display = DisplayStyle.None; _logScrollView.style.height = 0; _logScrollView.style.maxHeight = 0; + _launcherHistoryContentHeight = 0f; _logScrollView.style.marginTop = 0; } @@ -2779,6 +2825,7 @@ private void UpdateLauncherLayoutMetrics() _autoCompleteContainer.style.alignSelf = Align.Stretch; _autoCompleteContainer.style.flexGrow = 0; _autoCompleteContainer.style.flexShrink = 0; + _autoCompleteContainer.style.minHeight = 0f; bool hasSuggestions = false; int suggestionChildCount = _autoCompleteContainer.contentContainer.childCount; @@ -2810,23 +2857,40 @@ private void UpdateLauncherLayoutMetrics() { _autoCompleteContainer.style.display = DisplayStyle.None; _autoCompleteContainer.style.height = 0; + if (_autoCompleteViewport != null) + { + _autoCompleteViewport.style.height = 0; + } _autoCompleteContainer.style.marginTop = 0; + _launcherSuggestionContentHeight = 0f; + } + + float effectiveSuggestionHeight = _launcherSuggestionContentHeight; + if (effectiveSuggestionHeight <= 0f && hasSuggestions) + { + effectiveSuggestionHeight = LauncherEstimatedSuggestionRowHeight; } float suggestionsHeight = hasSuggestions - ? Mathf.Min(LauncherEstimatedSuggestionRowHeight, _launcherMetrics.HistoryHeight) + ? Mathf.Clamp(effectiveSuggestionHeight, 0f, _launcherMetrics.HistoryHeight) : 0f; if (hasSuggestions) { _autoCompleteContainer.style.height = Mathf.Max(0f, suggestionsHeight); + if (_autoCompleteViewport != null) + { + _autoCompleteViewport.style.height = Mathf.Max(0f, suggestionsHeight); + } } _autoCompleteContainer.style.marginBottom = 0; - float reservedForSuggestions = ( - hasSuggestions - ? suggestionsHeight + LauncherAutoCompleteSpacing - : LauncherAutoCompleteSpacing - ); + float spacingAboveLog = hasSuggestions + ? LauncherAutoCompleteSpacing + : Mathf.Max(LauncherAutoCompleteSpacing, padding * 0.25f); + + float reservedForSuggestions = hasSuggestions + ? suggestionsHeight + spacingAboveLog + : spacingAboveLog; VisualElement historyContent = _logScrollView.contentContainer; int visibleHistoryCount = 0; @@ -2848,26 +2912,34 @@ private void UpdateLauncherLayoutMetrics() pendingLogs, _launcherMetrics.HistoryVisibleEntryCount ); + if (visibleHistoryCount == 0) + { + _launcherHistoryContentHeight = 0f; + } } - float desiredHistoryHeight = Mathf.Min( - visibleHistoryCount * LauncherEstimatedHistoryRowHeight, - _launcherMetrics.HistoryHeight - ); - if (desiredHistoryHeight < 0f) + float historyHeightFromContent = + visibleHistoryCount > 0 ? _launcherHistoryContentHeight : 0f; + if (float.IsNaN(historyHeightFromContent) || historyHeightFromContent < 0f) { - desiredHistoryHeight = 0f; + historyHeightFromContent = 0f; } - if (visibleHistoryCount > 0) - { - desiredHistoryHeight = Mathf.Max( - desiredHistoryHeight, - Mathf.Min( - visibleHistoryCount * LauncherEstimatedHistoryRowHeight, + float estimatedHistoryHeight = + visibleHistoryCount > 0 + ? visibleHistoryCount * LauncherEstimatedHistoryRowHeight + : 0f; + + float desiredHistoryHeight = + visibleHistoryCount > 0 + ? Mathf.Min( + Mathf.Max(historyHeightFromContent, estimatedHistoryHeight), _launcherMetrics.HistoryHeight ) - ); + : 0f; + if (desiredHistoryHeight < 0f) + { + desiredHistoryHeight = 0f; } float minimumHeight = padding * 2f + inputHeight + reservedForSuggestions; @@ -2905,6 +2977,7 @@ private void UpdateLauncherLayoutMetrics() _logScrollView.style.display = DisplayStyle.None; _logScrollView.style.height = 0; _logScrollView.style.maxHeight = 0; + _launcherHistoryContentHeight = 0f; } else { @@ -2913,8 +2986,52 @@ private void UpdateLauncherLayoutMetrics() _logScrollView.style.maxHeight = availableForHistory; } - float logTopMargin = LauncherAutoCompleteSpacing; - _logScrollView.style.marginTop = logTopMargin; + _logScrollView.style.marginTop = spacingAboveLog; + } + + private void OnAutoCompleteGeometryChanged(GeometryChangedEvent evt) + { + if (evt == null) + { + return; + } + + float newHeight = Mathf.Max(evt.newRect.height, 0f); + if (float.IsNaN(newHeight)) + { + newHeight = 0f; + } + + bool isViewport = evt.target == _autoCompleteViewport; + bool hasChildren = _autoCompleteContainer?.contentContainer?.childCount > 0; + if (isViewport && newHeight <= 0f && hasChildren) + { + return; + } + + if (!Mathf.Approximately(newHeight, _launcherSuggestionContentHeight)) + { + _launcherSuggestionContentHeight = newHeight; + } + } + + private void OnLogContentGeometryChanged(GeometryChangedEvent evt) + { + if (evt == null) + { + return; + } + + float newHeight = Mathf.Max(evt.newRect.height, 0f); + if (float.IsNaN(newHeight)) + { + newHeight = 0f; + } + + if (!Mathf.Approximately(newHeight, _launcherHistoryContentHeight)) + { + _launcherHistoryContentHeight = newHeight; + } } private void StartHeightAnimation() From 765da6475a8bec7b35309676a722653cdcbc3c76 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 14:25:04 -0700 Subject: [PATCH 17/69] Tons of progress --- Editor/CustomEditors/TerminalUIEditor.cs | 36 ++++++++++++------ .../UI/TerminalLauncherSettings.cs | 2 +- Runtime/Helper/DirectoryHelper.cs | 37 +++++++++++-------- Tests/Runtime/LauncherModeTests.cs | 2 +- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/Editor/CustomEditors/TerminalUIEditor.cs b/Editor/CustomEditors/TerminalUIEditor.cs index bd6cf89..3c0ece1 100644 --- a/Editor/CustomEditors/TerminalUIEditor.cs +++ b/Editor/CustomEditors/TerminalUIEditor.cs @@ -253,14 +253,20 @@ private static T[] LoadAll() } } - if (Directory.Exists(Path.Combine(Application.dataPath, "Packages"))) + string projectRoot = Path.GetDirectoryName(Application.dataPath); + if (!string.IsNullOrWhiteSpace(projectRoot)) { - directories.Add("Packages"); - } + string packagesAbsolute = Path.Combine(projectRoot, "Packages"); + if (Directory.Exists(packagesAbsolute)) + { + directories.Add("Packages"); + } - if (Directory.Exists(Path.Combine(Application.dataPath, "Library"))) - { - directories.Add("Library"); + string libraryAbsolute = Path.Combine(projectRoot, "Library"); + if (Directory.Exists(libraryAbsolute)) + { + directories.Add("Library"); + } } directories.Add("Assets"); @@ -995,14 +1001,20 @@ private static bool CheckForUIDocumentProblems(TerminalUI terminal) } List directories = new(); - if (Directory.Exists(Path.Combine(Application.dataPath, "Library"))) + string projectRoot = Path.GetDirectoryName(Application.dataPath); + if (!string.IsNullOrWhiteSpace(projectRoot)) { - directories.Add("Library"); - } + string libraryAbsolute = Path.Combine(projectRoot, "Library"); + if (Directory.Exists(libraryAbsolute)) + { + directories.Add("Library"); + } - if (Directory.Exists(Path.Combine(Application.dataPath, "Packages"))) - { - directories.Add("Packages"); + string packagesAbsolute = Path.Combine(projectRoot, "Packages"); + if (Directory.Exists(packagesAbsolute)) + { + directories.Add("Packages"); + } } directories.Add("Assets"); diff --git a/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs b/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs index 35b5521..c4aeaa4 100644 --- a/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs +++ b/Runtime/CommandTerminal/UI/TerminalLauncherSettings.cs @@ -66,7 +66,7 @@ public sealed class TerminalLauncherSettings [Header("Dimensions")] public LauncherDimension width = LauncherDimension.RelativeToScreen(0.55f); - public LauncherDimension height = LauncherDimension.RelativeToScreen(0.18f); + public LauncherDimension height = LauncherDimension.RelativeToScreen(0.33f); public LauncherDimension historyHeight = LauncherDimension.RelativeToLauncher(0.45f); diff --git a/Runtime/Helper/DirectoryHelper.cs b/Runtime/Helper/DirectoryHelper.cs index 731ec00..78d89dc 100644 --- a/Runtime/Helper/DirectoryHelper.cs +++ b/Runtime/Helper/DirectoryHelper.cs @@ -105,24 +105,31 @@ internal static string AbsoluteToUnityRelativePath(string absolutePath) return string.Empty; } - if (absolutePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) + if (!absolutePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) { - int startIndex = projectRoot.EndsWith("/", StringComparison.OrdinalIgnoreCase) - ? projectRoot.Length - : projectRoot.Length + 1; - string tail = - absolutePath.Length > startIndex ? absolutePath[startIndex..] : string.Empty; - if (string.IsNullOrEmpty(tail)) - { - return string.Empty; - } - // Ensure path starts with Assets/ - return tail.StartsWith("Assets", StringComparison.OrdinalIgnoreCase) - ? tail - : ($"Assets/{tail}"); + return string.Empty; } - return string.Empty; + int startIndex = projectRoot.EndsWith("/", StringComparison.OrdinalIgnoreCase) + ? projectRoot.Length + : projectRoot.Length + 1; + string tail = absolutePath.Length > startIndex ? absolutePath[startIndex..] : string.Empty; + if (string.IsNullOrEmpty(tail)) + { + return string.Empty; + } + + tail = tail.TrimStart('/'); + if ( + tail.StartsWith("Assets", StringComparison.OrdinalIgnoreCase) + || tail.StartsWith("Packages", StringComparison.OrdinalIgnoreCase) + || tail.StartsWith("Library", StringComparison.OrdinalIgnoreCase) + ) + { + return tail; + } + + return tail; } } } diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index 9ce77ba..79d33be 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -17,7 +17,7 @@ public void LauncherMetricsRespectSizingModes() var settings = new TerminalLauncherSettings { width = LauncherDimension.RelativeToScreen(0.5f), - height = LauncherDimension.RelativeToScreen(0.18f), + height = LauncherDimension.RelativeToScreen(0.33f), historyHeight = LauncherDimension.RelativeToLauncher(0.5f), minimumWidth = 300f, minimumHeight = 120f, From 150d8e05e70d15ec773f8026ac66e89d5f8c3c6f Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 14:49:41 -0700 Subject: [PATCH 18/69] More command goodies --- .../Attributes/RegisterCommandAttribute.cs | 2 + .../Backend/BuiltinCommands.cs | 3 +- .../CommandTerminal/Backend/CommandHistory.cs | 67 ++++++++ .../CommandTerminal/Backend/CommandInfo.cs | 5 +- .../CommandTerminal/Backend/CommandShell.cs | 26 ++- Runtime/CommandTerminal/UI/TerminalUI.cs | 153 ++++++++++++++++-- Tests/Runtime/CommandShellTests.cs | 25 +++ Tests/Runtime/LauncherModeTests.cs | 17 +- 8 files changed, 269 insertions(+), 29 deletions(-) diff --git a/Runtime/Attributes/RegisterCommandAttribute.cs b/Runtime/Attributes/RegisterCommandAttribute.cs index 1e778b6..388e353 100644 --- a/Runtime/Attributes/RegisterCommandAttribute.cs +++ b/Runtime/Attributes/RegisterCommandAttribute.cs @@ -19,6 +19,8 @@ public sealed class RegisterCommandAttribute : Attribute public bool DevelopmentOnly { get; set; } + public bool IncludeInHistory { get; set; } = true; + public RegisterCommandAttribute(string commandName = null) { commandName = commandName?.Replace(" ", string.Empty).Trim(); diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index 2f66bbd..bd24d53 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -341,7 +341,8 @@ public static void CommandClearConsole(CommandArg[] args) isDefault: true, Name = "clear-history", Help = "Clear the command console's history", - MaxArgCount = 0 + MaxArgCount = 0, + IncludeInHistory = false )] public static void CommandClearHistory(CommandArg[] args) { diff --git a/Runtime/CommandTerminal/Backend/CommandHistory.cs b/Runtime/CommandTerminal/Backend/CommandHistory.cs index 89726c8..0edeade 100644 --- a/Runtime/CommandTerminal/Backend/CommandHistory.cs +++ b/Runtime/CommandTerminal/Backend/CommandHistory.cs @@ -4,13 +4,30 @@ namespace WallstopStudios.DxCommandTerminal.Backend using System.Collections.Generic; using DataStructures; + public readonly struct CommandHistoryEntry + { + public CommandHistoryEntry(string text, bool? success, bool? errorFree) + { + Text = text ?? string.Empty; + Success = success; + ErrorFree = errorFree; + } + + public string Text { get; } + public bool? Success { get; } + public bool? ErrorFree { get; } + } + public sealed class CommandHistory { public int Capacity => _history.Capacity; + public int Count => _history.Count; + public long Version => _version; private readonly CyclicBuffer<(string text, bool? success, bool? errorFree)> _history; private int _position; + private long _version; public CommandHistory(int capacity) { @@ -56,9 +73,49 @@ public void CopyHistoryTo(List buffer, bool onlySuccess, bool onlyErrorF } } + public void CopyEntriesTo( + List buffer, + bool onlySuccess, + bool onlyErrorFree + ) + { + if (buffer == null) + { + return; + } + + buffer.Clear(); + int count = _history.Count; + for (int i = 0; i < count; ++i) + { + (string text, bool? success, bool? errorFree) entry = _history[i]; + if (onlySuccess && entry.success != true) + { + continue; + } + if (onlyErrorFree && entry.errorFree != true) + { + continue; + } + + buffer.Add(new CommandHistoryEntry(entry.text, entry.success, entry.errorFree)); + } + } + + public void CopyEntriesTo(List buffer) + { + CopyEntriesTo(buffer, onlySuccess: false, onlyErrorFree: false); + } + public void Resize(int newCapacity) { + int previousCount = _history.Count; _history.Resize(newCapacity); + if (_history.Count != previousCount) + { + _version++; + _position = Math.Min(_position, _history.Count); + } } public bool Push(string commandString, bool? success, bool? errorFree) @@ -68,8 +125,14 @@ public bool Push(string commandString, bool? success, bool? errorFree) return false; } + if (_history.Capacity <= 0) + { + return false; + } + _history.Add((commandString, success, errorFree)); _position = _history.Count; + _version++; return true; } @@ -154,6 +217,10 @@ public int Clear() int count = _history.Count; _history.Clear(); _position = 0; + if (0 < count) + { + _version++; + } return count; } } diff --git a/Runtime/CommandTerminal/Backend/CommandInfo.cs b/Runtime/CommandTerminal/Backend/CommandInfo.cs index c1b0f2a..0b8f776 100644 --- a/Runtime/CommandTerminal/Backend/CommandInfo.cs +++ b/Runtime/CommandTerminal/Backend/CommandInfo.cs @@ -10,6 +10,7 @@ public readonly struct CommandInfo public readonly string help; public readonly string hint; public readonly IArgumentCompleter completer; + public readonly bool includeInHistory; public CommandInfo( Action proc, @@ -17,7 +18,8 @@ public CommandInfo( int maxArgCount, string help, string hint, - IArgumentCompleter completer = null + IArgumentCompleter completer = null, + bool includeInHistory = true ) { this.proc = proc; @@ -26,6 +28,7 @@ public CommandInfo( this.help = help; this.hint = hint; this.completer = completer; + this.includeInHistory = includeInHistory; } } } diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index 3161ed7..9ae2556 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -268,7 +268,8 @@ public void InitializeAutoRegisteredCommands( attribute.MaxArgCount, attribute.Help, attribute.Hint, - completer + completer, + attribute.IncludeInHistory ); if (success) { @@ -428,13 +429,19 @@ public bool RunCommand(string commandName, CommandArg[] arguments) } _errorMessages.Enqueue(invalidMessage); - _history.Push(line, false, false); + if (command.includeInHistory) + { + _history.Push(line, false, false); + } return false; } int errorCount = _errorMessages.Count; command.proc?.Invoke(arguments); - _history.Push(line, true, errorCount == _errorMessages.Count); + if (command.includeInHistory) + { + _history.Push(line, true, errorCount == _errorMessages.Count); + } return true; } @@ -469,10 +476,19 @@ public bool AddCommand( int maxArgs = -1, string help = "", string hint = null, - IArgumentCompleter completer = null + IArgumentCompleter completer = null, + bool includeInHistory = true ) { - CommandInfo info = new(proc, minArgs, maxArgs, help, hint, completer); + CommandInfo info = new( + proc, + minArgs, + maxArgs, + help, + hint, + completer, + includeInHistory + ); return AddCommand(name, info); } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index d0a18c5..b859ee3 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -247,9 +247,14 @@ private enum ScrollBarCaptureState StringComparer.OrdinalIgnoreCase ); private readonly List _autoCompleteChildren = new(); + private readonly List _launcherHistoryEntries = new(); private float _launcherSuggestionContentHeight; private float _launcherHistoryContentHeight; + private long _lastRenderedLauncherHistoryVersion = -1; + private long _cachedLauncherScrollVersion = -1; + private float _cachedLauncherScrollValue; + private bool _restoreLauncherScrollPending; // Cached for performance (avoids allocations) private readonly Action _focusInput; @@ -653,9 +658,18 @@ public void ToggleState(TerminalState newState) public void SetState(TerminalState newState) { + if (_state == TerminalState.OpenLauncher && newState != TerminalState.OpenLauncher) + { + CacheLauncherScrollPosition(); + } + _commandIssuedThisFrame = true; _previousState = _state; _state = newState; + if (_state == TerminalState.OpenLauncher) + { + _restoreLauncherScrollPending = true; + } ResetWindowIdempotent(); if (_state != TerminalState.Closed) { @@ -1617,7 +1631,7 @@ private void RefreshLogs() if (IsLauncherActive && _launcherMetricsInitialized) { - RefreshLauncherHistory(logs); + RefreshLauncherHistory(); return; } @@ -1686,10 +1700,39 @@ private void RefreshLogs() } } - private void RefreshLauncherHistory(IReadOnlyList logs) + private void RefreshLauncherHistory() { + if (_logScrollView == null) + { + return; + } + VisualElement content = _logScrollView.contentContainer; - int visibleCount = Mathf.Min(_launcherMetrics.HistoryVisibleEntryCount, logs.Count); + CommandHistory history = Terminal.History; + + if (history == null) + { + _launcherHistoryEntries.Clear(); + _logScrollView.style.display = DisplayStyle.None; + for (int i = 0; i < content.childCount; ++i) + { + content[i].style.display = DisplayStyle.None; + } + + _lastRenderedLauncherHistoryVersion = -1; + _cachedLauncherScrollVersion = -1; + _cachedLauncherScrollValue = 0f; + _restoreLauncherScrollPending = false; + _launcherHistoryContentHeight = 0f; + _needsScrollToEnd = false; + return; + } + + history.CopyEntriesTo(_launcherHistoryEntries); + long historyVersion = history.Version; + + int entryCount = _launcherHistoryEntries.Count; + int visibleCount = Mathf.Min(_launcherMetrics.HistoryVisibleEntryCount, entryCount); if (_launcherMetrics.HistoryHeight <= 0f || visibleCount <= 0) { @@ -1698,7 +1741,12 @@ private void RefreshLauncherHistory(IReadOnlyList logs) { content[i].style.display = DisplayStyle.None; } - _lastSeenBufferVersion = Terminal.Buffer?.Version; + + _lastRenderedLauncherHistoryVersion = historyVersion; + _cachedLauncherScrollVersion = historyVersion; + _cachedLauncherScrollValue = 0f; + _restoreLauncherScrollPending = false; + _launcherHistoryContentHeight = 0f; _needsScrollToEnd = false; return; } @@ -1723,28 +1771,29 @@ private void RefreshLauncherHistory(IReadOnlyList logs) float denominator = Mathf.Max(1f, visibleCount - 1f); for (int i = 0; i < visibleCount; ++i) { - int logIndex = logs.Count - 1 - i; + int historyIndex = entryCount - 1 - i; + CommandHistoryEntry entry = _launcherHistoryEntries[historyIndex]; VisualElement element = content[i]; - LogItem logItem = logs[logIndex]; + LogItem logItem = new(TerminalLogType.Input, entry.Text, string.Empty); switch (element) { case TextField logText: { ApplyLogStyling(logText, logItem); - logText.value = logItem.message; + logText.value = entry.Text; break; } case Label logLabel: { ApplyLogStyling(logLabel, logItem); - logLabel.text = logItem.message; + logLabel.text = entry.Text; break; } case Button button: { ApplyLogStyling(button, logItem); - button.text = logItem.message; + button.text = entry.Text; break; } } @@ -1757,7 +1806,36 @@ private void RefreshLauncherHistory(IReadOnlyList logs) element.style.opacity = Mathf.Clamp01(fade); } - _lastSeenBufferVersion = Terminal.Buffer.Version; + bool historyChanged = historyVersion != _lastRenderedLauncherHistoryVersion; + bool restoreRequested = _restoreLauncherScrollPending; + float? targetScroll = null; + + if (restoreRequested) + { + float targetValue = _cachedLauncherScrollValue; + if (_cachedLauncherScrollVersion != historyVersion) + { + targetValue = 0f; + } + + _cachedLauncherScrollVersion = historyVersion; + _cachedLauncherScrollValue = targetValue; + targetScroll = targetValue; + _restoreLauncherScrollPending = false; + } + else if (historyChanged) + { + _cachedLauncherScrollVersion = historyVersion; + _cachedLauncherScrollValue = 0f; + targetScroll = 0f; + } + + if (targetScroll.HasValue) + { + ScheduleLauncherScroll(targetScroll.Value); + } + + _lastRenderedLauncherHistoryVersion = historyVersion; _needsScrollToEnd = false; } @@ -1796,6 +1874,55 @@ private void ScrollToEnd() } } + private void CacheLauncherScrollPosition() + { + if (_logScrollView?.verticalScroller == null) + { + return; + } + + float highValue = _logScrollView.verticalScroller.highValue; + float currentValue = Mathf.Clamp( + _logScrollView.verticalScroller.value, + 0f, + highValue + ); + _cachedLauncherScrollValue = currentValue; + _cachedLauncherScrollVersion = Terminal.History?.Version ?? -1; + } + + private void ScheduleLauncherScroll(float targetValue) + { + if (_logScrollView?.verticalScroller == null) + { + return; + } + + float clampedTarget = Mathf.Clamp( + targetValue, + 0f, + _logScrollView.verticalScroller.highValue + ); + + _logScrollView + .schedule + .Execute(() => + { + if (_logScrollView?.verticalScroller == null) + { + return; + } + + float highValue = _logScrollView.verticalScroller.highValue; + _logScrollView.verticalScroller.value = Mathf.Clamp( + clampedTarget, + 0f, + highValue + ); + }) + .ExecuteLater(0); + } + private void RefreshAutoCompleteHints() { bool shouldDisplay = @@ -2510,9 +2637,9 @@ internal void SetLogScrollViewForTests(ScrollView scrollView) _logScrollView = scrollView; } - internal void RefreshLauncherHistoryForTests(IReadOnlyList logs) + internal void RefreshLauncherHistoryForTests() { - RefreshLauncherHistory(logs); + RefreshLauncherHistory(); } internal void ResetWindowForTests() @@ -2907,7 +3034,7 @@ private void UpdateLauncherLayoutMetrics() if (visibleHistoryCount == 0) { - int pendingLogs = Terminal.Buffer?.Logs != null ? Terminal.Buffer.Logs.Count : 0; + int pendingLogs = Terminal.History?.Count ?? 0; visibleHistoryCount = Mathf.Min( pendingLogs, _launcherMetrics.HistoryVisibleEntryCount diff --git a/Tests/Runtime/CommandShellTests.cs b/Tests/Runtime/CommandShellTests.cs index 2ccc02b..88d6dcc 100644 --- a/Tests/Runtime/CommandShellTests.cs +++ b/Tests/Runtime/CommandShellTests.cs @@ -349,5 +349,30 @@ void HandleMessageReceived(string message, string stackTrace, LogType type) } } } + + [UnityTest] + public IEnumerator ClearHistoryCommandClearsWithoutPersistingCommand() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + CommandHistory history = Terminal.History; + Assert.IsNotNull(history); + + bool executed = shell.RunCommand("log test-history"); + Assert.IsTrue(executed, "Expected log command to execute successfully"); + + string[] entries = + history.GetHistory(onlySuccess: false, onlyErrorFree: false).ToArray(); + CollectionAssert.Contains(entries, "log test-history"); + + shell.RunCommand("clear-history"); + + entries = history.GetHistory(onlySuccess: false, onlyErrorFree: false).ToArray(); + Assert.IsEmpty(entries, "History should be empty after invoking clear-history"); + + yield break; + } } } diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index 79d33be..6f80da7 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -67,16 +67,15 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() TerminalUI terminal = TerminalUI.Instance; Assert.IsNotNull(terminal); - CommandLog previousLog = Terminal.Buffer; - var log = new CommandLog(16); - Terminal.Buffer = log; + CommandHistory previousHistory = Terminal.History; + var history = new CommandHistory(16); + Terminal.History = history; try { - log.EnqueueMessage("first", TerminalLogType.Message, includeStackTrace: false); - log.EnqueueMessage("second", TerminalLogType.Message, includeStackTrace: false); - log.EnqueueMessage("third", TerminalLogType.Message, includeStackTrace: false); - log.DrainPending(); + history.Push("first", true, true); + history.Push("second", true, true); + history.Push("third", true, true); var metrics = new LauncherLayoutMetrics( width: 640f, @@ -96,7 +95,7 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() terminal.SetLogScrollViewForTests(scroll); terminal.SetLauncherMetricsForTests(metrics); terminal.SetState(TerminalState.OpenLauncher); - terminal.RefreshLauncherHistoryForTests(Terminal.Buffer.Logs); + terminal.RefreshLauncherHistoryForTests(); VisualElement content = terminal.LogScrollViewForTests.contentContainer; Assert.That(content.childCount, Is.EqualTo(3)); @@ -121,7 +120,7 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() } finally { - Terminal.Buffer = previousLog; + Terminal.History = previousHistory; } yield return TestSceneHelpers.DestroyTerminalAndWait(); From fe8f326504c717de18d59bc43b6535b8210df7c8 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 15:50:58 -0700 Subject: [PATCH 19/69] Progress --- .../UI/TerminalHistoryFadeTargets.cs | 31 +++ .../UI/TerminalHistoryFadeTargets.cs.meta | 11 + Runtime/CommandTerminal/UI/TerminalUI.cs | 191 +++++++++++++++++- Tests/Runtime/LauncherModeTests.cs | 4 +- 4 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs create mode 100644 Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs.meta diff --git a/Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs b/Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs new file mode 100644 index 0000000..36833a5 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs @@ -0,0 +1,31 @@ +namespace WallstopStudios.DxCommandTerminal.UI +{ + using System; + using System.Runtime.CompilerServices; + + [Flags] + public enum TerminalHistoryFadeTargets + { + None = 0, + SmallTerminal = 1 << 0, + FullTerminal = 1 << 1, + Launcher = 1 << 2, + } + + internal static class TerminalHistoryFadeTargetsExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasFlagNoAlloc( + this TerminalHistoryFadeTargets value, + TerminalHistoryFadeTargets flag + ) + { + if (flag == TerminalHistoryFadeTargets.None) + { + return value == TerminalHistoryFadeTargets.None; + } + + return (value & flag) == flag; + } + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs.meta b/Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs.meta new file mode 100644 index 0000000..d03dea9 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalHistoryFadeTargets.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5f9d5a43f57480c8e1dc2f985ddc169 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index b859ee3..910f693 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -24,6 +24,7 @@ public sealed class TerminalUI : MonoBehaviour private const float LauncherAutoCompleteSpacing = 6f; private const float LauncherEstimatedSuggestionRowHeight = 32f; private const float LauncherEstimatedHistoryRowHeight = 28f; + private const float StandardEstimatedHistoryRowHeight = 24f; private enum ScrollBarCaptureState { @@ -127,6 +128,13 @@ private enum ScrollBarCaptureState [DxShowIf(nameof(showGUIButtons))] public string launcherButtonText = "launcher"; + [Header("Appearance")] + [SerializeField] + private TerminalHistoryFadeTargets _historyFadeTargets = + TerminalHistoryFadeTargets.SmallTerminal + | TerminalHistoryFadeTargets.FullTerminal + | TerminalHistoryFadeTargets.Launcher; + [Header("Hints")] public HintDisplayMode hintDisplayMode = HintDisplayMode.AutoCompleteOnly; @@ -1698,6 +1706,15 @@ private void RefreshLogs() _lastSeenBufferVersion = Terminal.Buffer.Version; } } + + if (ShouldApplyHistoryFade()) + { + ApplyHistoryFade(content, fadeFromTop: false); + } + else + { + ResetHistoryFade(content); + } } private void RefreshLauncherHistory() @@ -1768,7 +1785,6 @@ private void RefreshLauncherHistory() content[i].style.display = DisplayStyle.None; } - float denominator = Mathf.Max(1f, visibleCount - 1f); for (int i = 0; i < visibleCount; ++i) { int historyIndex = entryCount - 1 - i; @@ -1799,11 +1815,15 @@ private void RefreshLauncherHistory() } element.style.display = DisplayStyle.Flex; - float fade = - visibleCount == 1 - ? 1f - : Mathf.Pow(1f - (i / denominator), _launcherMetrics.HistoryFadeExponent); - element.style.opacity = Mathf.Clamp01(fade); + } + + if (ShouldApplyHistoryFade()) + { + ApplyHistoryFade(content, fadeFromTop: true); + } + else + { + ResetHistoryFade(content); } bool historyChanged = historyVersion != _lastRenderedLauncherHistoryVersion; @@ -1874,6 +1894,165 @@ private void ScrollToEnd() } } + private bool ShouldApplyHistoryFade() + { + return _state switch + { + TerminalState.OpenLauncher + => _historyFadeTargets.HasFlagNoAlloc(TerminalHistoryFadeTargets.Launcher), + TerminalState.OpenSmall + => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.SmallTerminal + ), + TerminalState.OpenFull + => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.FullTerminal + ), + _ => false, + }; + } + + private float GetHistoryFadeRangeFactor() + { + return _state switch + { + TerminalState.OpenLauncher => 0.6f, + TerminalState.OpenFull => 1.0f, + TerminalState.OpenSmall => 0.85f, + _ => 0.85f, + }; + } + + private float GetHistoryFadeMinimumOpacity() + { + return _state == TerminalState.OpenLauncher ? 0.35f : 0.45f; + } + + private float GetHistoryFallbackRowHeight() + { + return _state == TerminalState.OpenLauncher + ? LauncherEstimatedHistoryRowHeight + : StandardEstimatedHistoryRowHeight; + } + + private float GetHistoryFadeExponent() + { + if (_state == TerminalState.OpenLauncher && _launcherMetricsInitialized) + { + return Mathf.Max(0.01f, _launcherMetrics.HistoryFadeExponent); + } + + return 1f; + } + + private void ApplyHistoryFade(VisualElement container, bool fadeFromTop) + { + if (container == null) + { + return; + } + + Rect viewportBounds = _logViewport?.worldBound ?? Rect.zero; + bool viewportIsValid = viewportBounds.height > 0.01f; + float fallbackRowHeight = GetHistoryFallbackRowHeight(); + + int childCount = container.childCount; + if (childCount == 0) + { + return; + } + + int visibleCount = 0; + for (int i = 0; i < childCount; ++i) + { + VisualElement element = container[i]; + if (element == null || element.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + visibleCount++; + } + + if (visibleCount == 0) + { + return; + } + + if (!viewportIsValid) + { + float fallbackHeight = Mathf.Max(1f, fallbackRowHeight * visibleCount); + viewportBounds.height = fallbackHeight; + } + + float fadeRange = Mathf.Max(1f, viewportBounds.height * GetHistoryFadeRangeFactor()); + float minimumOpacity = Mathf.Clamp01(GetHistoryFadeMinimumOpacity()); + + int visibleIndex = 0; + for (int i = 0; i < childCount; ++i) + { + VisualElement element = container[i]; + if (element == null || element.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + float distance; + if (viewportIsValid) + { + Rect childBounds = element.worldBound; + bool boundsValid = childBounds.height > 0.01f; + if (fadeFromTop) + { + distance = boundsValid + ? Mathf.Max(0f, childBounds.yMin - viewportBounds.yMin) + : fallbackRowHeight * visibleIndex; + } + else + { + int inverseIndex = Math.Max(0, visibleCount - visibleIndex - 1); + distance = boundsValid + ? Mathf.Max(0f, viewportBounds.yMax - childBounds.yMax) + : fallbackRowHeight * inverseIndex; + } + } + else + { + int indexFromEdge = fadeFromTop + ? visibleIndex + : Math.Max(0, visibleCount - visibleIndex - 1); + distance = fallbackRowHeight * indexFromEdge; + } + + float normalized = Mathf.Clamp01(distance / fadeRange); + float adjusted = Mathf.Pow(normalized, GetHistoryFadeExponent()); + float opacity = Mathf.Lerp(1f, minimumOpacity, adjusted); + element.style.opacity = opacity; + + visibleIndex++; + } + } + + private static void ResetHistoryFade(VisualElement container) + { + if (container == null) + { + return; + } + + int childCount = container.childCount; + for (int i = 0; i < childCount; ++i) + { + VisualElement element = container[i]; + if (element == null || element.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + element.style.opacity = 1f; + } + } + private void CacheLauncherScrollPosition() { if (_logScrollView?.verticalScroller == null) diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index 6f80da7..3c8fe57 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -110,13 +110,13 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() var middle = content[1] as Label; Assert.IsNotNull(middle); Assert.That(middle!.text, Is.EqualTo("second")); - Assert.That(middle.style.opacity.value, Is.LessThan(1f).And.GreaterThan(0f)); + Assert.That(middle.style.opacity.value, Is.LessThan(1f).And.GreaterThan(0.35f)); // Oldest entry is faded out var oldest = content[2] as Label; Assert.IsNotNull(oldest); Assert.That(oldest!.text, Is.EqualTo("first")); - Assert.That(oldest.style.opacity.value, Is.EqualTo(0f).Within(0.001f)); + Assert.That(oldest.style.opacity.value, Is.EqualTo(0.35f).Within(0.001f)); } finally { From bb60de6e0446d6ffe1752ee36c9c938c51e08941 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 17:21:13 -0700 Subject: [PATCH 20/69] Command Terminal progress --- .../Backend/BuiltinCommands.cs | 5 + .../Backend/CommandAutoComplete.cs | 45 ++- .../CommandTerminal/Backend/CommandShell.cs | 189 +++++++--- Tests/Runtime/BuiltinCommandTests.cs | 355 ++++++++++++++++++ Tests/Runtime/BuiltinCommandTests.cs.meta | 3 + 5 files changed, 542 insertions(+), 55 deletions(-) create mode 100644 Tests/Runtime/BuiltinCommandTests.cs create mode 100644 Tests/Runtime/BuiltinCommandTests.cs.meta diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index bd24d53..56b4e99 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -320,6 +320,11 @@ public static void CommandSetRandomFont(CommandArg[] args) } Font font = terminal.SetRandomFont(); + if (font == null) + { + Terminal.Log(TerminalLogType.Warning, "No fonts available to select."); + return; + } Terminal.Log( TerminalLogType.Message, $"Randomly selected and set font to '{font.name}'." diff --git a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs index 10dff22..8cd68eb 100644 --- a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs +++ b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs @@ -208,6 +208,8 @@ List buffer { input = input.Trim(); } + string normalizedInput = CommandShell.NormalizeCommandKey(input); + bool useNormalizedMatch = !string.IsNullOrEmpty(normalizedInput); _duplicateBuffer.Clear(); buffer.Clear(); @@ -217,27 +219,43 @@ List buffer for (int ci = 0; ci < _historyScratch.Count; ++ci) { string command = _historyScratch[ci]; - string known = command.NeedsLowerInvariantConversion() - ? command.ToLowerInvariant() - : command; - if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + string normalizedCommand = CommandShell.NormalizeCommandKey(command); + bool matches = useNormalizedMatch + ? normalizedCommand.StartsWith(normalizedInput, StringComparison.Ordinal) + : command.StartsWith(input, StringComparison.OrdinalIgnoreCase); + if (!matches) { continue; } - if (_duplicateBuffer.Add(known)) + string duplicateKey = !string.IsNullOrEmpty(normalizedCommand) + ? normalizedCommand + : command.NeedsLowerInvariantConversion() + ? command.ToLowerInvariant() + : command; + string display = command.NeedsLowerInvariantConversion() + ? command.ToLowerInvariant() + : command; + if (_duplicateBuffer.Add(duplicateKey)) { - buffer.Add(known); + buffer.Add(display); } } // Known words foreach (string known in _knownWords) { - if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + string normalizedKnown = CommandShell.NormalizeCommandKey(known); + bool matches = useNormalizedMatch + ? normalizedKnown.StartsWith(normalizedInput, StringComparison.Ordinal) + : known.StartsWith(input, StringComparison.OrdinalIgnoreCase); + if (!matches) { continue; } - if (_duplicateBuffer.Add(known)) + string duplicateKey = !string.IsNullOrEmpty(normalizedKnown) + ? normalizedKnown + : known; + if (_duplicateBuffer.Add(duplicateKey)) { buffer.Add(known); } @@ -248,11 +266,18 @@ List buffer for (int hi = 0; hi < _historyScratch.Count; ++hi) { string known = _historyScratch[hi]; - if (!known.StartsWith(input, StringComparison.OrdinalIgnoreCase)) + string normalizedKnown = CommandShell.NormalizeCommandKey(known); + bool matches = useNormalizedMatch + ? normalizedKnown.StartsWith(normalizedInput, StringComparison.Ordinal) + : known.StartsWith(input, StringComparison.OrdinalIgnoreCase); + if (!matches) { continue; } - if (_duplicateBuffer.Add(known)) + string duplicateKey = !string.IsNullOrEmpty(normalizedKnown) + ? normalizedKnown + : known; + if (_duplicateBuffer.Add(duplicateKey)) { buffer.Add(known); } diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index 9ae2556..e96ff78 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -349,58 +349,23 @@ public bool RunCommand(string line) public bool RunCommand(string commandName, CommandArg[] arguments) { - _commandBuilder.Clear(); - _commandBuilder.Append(commandName); - if (arguments.Length != 0) - { - _commandBuilder.Append(' '); - } - - for (int i = 0; i < arguments.Length; ++i) - { - CommandArg argument = arguments[i]; - if (argument.startQuote != null) - { - _commandBuilder.Append(argument.startQuote.Value); - } - - _commandBuilder.Append(argument.contents); - if (argument.endQuote != null) - { - _commandBuilder.Append(argument.endQuote.Value); - } - - if (i != arguments.Length - 1) - { - _commandBuilder.Append(' '); - } - } - - string line = _commandBuilder.ToString(); - - if (string.IsNullOrWhiteSpace(commandName)) + string originalCommandName = commandName ?? string.Empty; + if (string.IsNullOrWhiteSpace(originalCommandName)) { - IssueErrorMessage($"Invalid command name '{commandName}'"); - // Don't log empty commands + IssueErrorMessage($"Invalid command name '{originalCommandName}'"); return false; } - if (commandName.Contains(' ')) + if (!TryResolveCommand(originalCommandName, out string canonicalName, out CommandInfo command)) { - commandName = commandName.Replace( - " ", - string.Empty, - StringComparison.OrdinalIgnoreCase - ); - } - - if (!_commands.TryGetValue(commandName, out CommandInfo command)) - { - IssueErrorMessage($"Command {commandName} not found"); - _history.Push(line, false, false); + string originalLine = BuildCommandLine(originalCommandName, arguments); + IssueErrorMessage($"Command {originalCommandName} not found"); + _history.Push(originalLine, false, false); return false; } + string line = BuildCommandLine(canonicalName, arguments); + int argCount = arguments.Length; string errorMessage = null; int requiredArg = 0; @@ -422,7 +387,7 @@ public bool RunCommand(string commandName, CommandArg[] arguments) string pluralFix = requiredArg == 1 ? "" : "s"; string invalidMessage = - $"{commandName} requires {errorMessage} {requiredArg} argument{pluralFix}"; + $"{canonicalName} requires {errorMessage} {requiredArg} argument{pluralFix}"; if (!string.IsNullOrWhiteSpace(command.hint)) { invalidMessage += $"\n -> Usage: {command.hint}"; @@ -459,6 +424,35 @@ public bool AddCommand(string name, CommandInfo info) name = name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); } + string normalizedCandidate = NormalizeCommandKey(name); + if (string.IsNullOrEmpty(normalizedCandidate)) + { + IssueErrorMessage($"Invalid Command Name: {name}"); + return false; + } + + foreach (string existingName in _commands.Keys) + { + if (string.Equals(existingName, name, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if ( + string.Equals( + NormalizeCommandKey(existingName), + normalizedCandidate, + StringComparison.Ordinal + ) + ) + { + IssueErrorMessage( + $"Command {name} conflicts with existing command {existingName}." + ); + return false; + } + } + if (!_commands.TryAdd(name, info)) { IssueErrorMessage($"Command {name} is already defined."); @@ -468,6 +462,111 @@ public bool AddCommand(string name, CommandInfo info) return true; } + private bool TryResolveCommand( + string inputName, + out string canonicalName, + out CommandInfo command + ) + { + canonicalName = string.Empty; + command = default; + + if (string.IsNullOrWhiteSpace(inputName)) + { + return false; + } + + string candidate = inputName; + if (candidate.Contains(' ')) + { + candidate = candidate.Replace( + " ", + string.Empty, + StringComparison.OrdinalIgnoreCase + ); + } + + string normalizedInput = NormalizeCommandKey(candidate); + + foreach (KeyValuePair entry in _commands) + { + if (string.Equals(entry.Key, candidate, StringComparison.OrdinalIgnoreCase)) + { + canonicalName = entry.Key; + command = entry.Value; + return true; + } + + if ( + string.Equals( + NormalizeCommandKey(entry.Key), + normalizedInput, + StringComparison.Ordinal + ) + ) + { + canonicalName = entry.Key; + command = entry.Value; + return true; + } + } + + return false; + } + + private string BuildCommandLine(string commandName, CommandArg[] arguments) + { + _commandBuilder.Clear(); + _commandBuilder.Append(commandName); + if (arguments.Length != 0) + { + _commandBuilder.Append(' '); + } + + for (int i = 0; i < arguments.Length; ++i) + { + CommandArg argument = arguments[i]; + if (argument.startQuote != null) + { + _commandBuilder.Append(argument.startQuote.Value); + } + + _commandBuilder.Append(argument.contents); + if (argument.endQuote != null) + { + _commandBuilder.Append(argument.endQuote.Value); + } + + if (i != arguments.Length - 1) + { + _commandBuilder.Append(' '); + } + } + + return _commandBuilder.ToString(); + } + + internal static string NormalizeCommandKey(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + StringBuilder builder = null; + for (int i = 0; i < name.Length; ++i) + { + char ch = name[i]; + if (char.IsLetterOrDigit(ch)) + { + builder ??= new StringBuilder(name.Length); + builder.Append(char.ToLowerInvariant(ch)); + } + } + + return builder == null ? string.Empty : builder.ToString(); + } + // ReSharper disable once MemberCanBePrivate.Global public bool AddCommand( string name, diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs new file mode 100644 index 0000000..4ad807f --- /dev/null +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -0,0 +1,355 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + using LogType = UnityEngine.LogType; + + public sealed class BuiltinCommandTests + { + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + UnityEngine.Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + private static IEnumerator RestartTerminal() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + } + + [UnityTest] + public IEnumerator BuiltInCommandsAreRegistered() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + + HashSet expected = new( + new[] + { + "list-themes", + "list-fonts", + "set-theme", + "set-font", + "get-theme", + "get-font", + "set-random-theme", + "set-random-font", + "clear-console", + "clear-history", + "help", + "time", + "time-scale", + "log-terminal", + "log", + "trace", + "clear-variable", + "clear-all-variables", + "set-variable", + "get-variable", + "list-variables", + "no-op", + "quit", + }, + StringComparer.OrdinalIgnoreCase + ); + + CollectionAssert.IsSubsetOf(expected, shell.Commands.Keys); + yield break; + } + + [UnityTest] + public IEnumerator ClearHistoryCommandAcceptsSpaceSeparatedInput() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + CommandHistory history = Terminal.History; + Assert.IsNotNull(shell); + Assert.IsNotNull(history); + + Assert.IsTrue(shell.RunCommand("log test-history")); + Terminal.Buffer?.DrainPending(); + + string[] suggestions = Terminal.AutoComplete.Complete("clear history"); + CollectionAssert.Contains(suggestions, "clear-history"); + + bool executed = shell.RunCommand("clear history"); + Assert.IsTrue(executed); + + string[] entries = history.GetHistory(false, false).ToArray(); + Assert.IsEmpty(entries); + yield break; + } + + [UnityTest] + public IEnumerator ClearConsoleCommandClearsBuffer() + { + yield return RestartTerminal(); + + Terminal.Log(TerminalLogType.Message, "one"); + Terminal.Buffer?.DrainPending(); + Assert.Greater(Terminal.Buffer.Logs.Count, 0); + + bool executed = Terminal.Shell.RunCommand("clear-console"); + Assert.IsTrue(executed); + + Terminal.Buffer?.DrainPending(); + Assert.AreEqual(0, Terminal.Buffer.Logs.Count); + yield break; + } + + [UnityTest] + public IEnumerator TimeCommandMeasuresNestedExecution() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + int initialCount = Terminal.Buffer.Logs.Count; + + bool executed = shell.RunCommand("time log-terminal timed-message"); + Assert.IsTrue(executed); + + Terminal.Buffer?.DrainPending(); + LogItem timeLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.ShellMessage && item.message.StartsWith("Time:") + ); + StringAssert.StartsWith("Time:", timeLog.message); + Assert.Greater(Terminal.Buffer.Logs.Count, initialCount); + yield break; + } + + [UnityTest] + public IEnumerator TimeScaleCommandUpdatesTimeScale() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + float originalScale = Time.timeScale; + + try + { + Assert.IsTrue(shell.RunCommand("time-scale 0.42")); + Assert.AreEqual(0.42f, Time.timeScale, 0.0001f); + } + finally + { + Time.timeScale = originalScale; + } + + yield break; + } + + [UnityTest] + public IEnumerator LogCommandsEmitOutput() + { + yield return RestartTerminal(); + + List<(string message, LogType type)> captured = new(); + Application.logMessageReceived += HandleLog; + try + { + Assert.IsTrue(Terminal.Shell.RunCommand("log-terminal captured")); + Terminal.Buffer?.DrainPending(); + LogItem bufferLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.ShellMessage && item.message == "captured" + ); + Assert.AreEqual("captured", bufferLog.message); + + Assert.IsTrue(Terminal.Shell.RunCommand("log unity-log")); + Assert.IsTrue(captured.Any(entry => entry.message == "unity-log")); + } + finally + { + Application.logMessageReceived -= HandleLog; + } + + yield break; + + void HandleLog(string message, string stackTrace, LogType type) + { + captured.Add((message, type)); + } + } + + [UnityTest] + public IEnumerator TraceCommandProducesStackTraceWhenAvailable() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + + Assert.IsTrue(shell.RunCommand("trace")); + Terminal.Buffer?.DrainPending(); + LogItem warningLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + StringAssert.Contains("Nothing to trace", warningLog.message); + + Terminal.Log(TerminalLogType.Message, "trace-target"); + Terminal.Buffer?.DrainPending(); + LogItem previousLog = Terminal.Buffer.Logs.Last(); + Assert.IsFalse(string.IsNullOrWhiteSpace(previousLog.stackTrace)); + + Assert.IsTrue(shell.RunCommand("trace")); + Terminal.Buffer?.DrainPending(); + LogItem traceLog = Terminal.Buffer.Logs.Last(); + Assert.AreEqual(previousLog.stackTrace, traceLog.message); + yield break; + } + + [UnityTest] + public IEnumerator VariableCommandsManageLifecycle() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + + Assert.IsTrue(shell.RunCommand("set-variable foo \"bar baz\"")); + Assert.IsTrue(shell.Variables.ContainsKey("foo")); + + Assert.IsTrue(shell.RunCommand("get-variable foo")); + Terminal.Buffer?.DrainPending(); + LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + StringAssert.Contains("bar baz", getLog.message); + + Assert.IsTrue(shell.RunCommand("list-variables")); + Terminal.Buffer?.DrainPending(); + LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + StringAssert.Contains("foo", listLog.message); + + Assert.IsTrue(shell.RunCommand("clear-variable foo")); + Assert.IsFalse(shell.Variables.ContainsKey("foo")); + + Assert.IsTrue(shell.RunCommand("set-variable alpha one")); + Assert.IsTrue(shell.RunCommand("set-variable beta two")); + Assert.AreEqual(2, shell.Variables.Count); + + Assert.IsTrue(shell.RunCommand("clear-all-variables")); + Assert.AreEqual(0, shell.Variables.Count); + + Assert.IsTrue(shell.RunCommand("list-variables")); + Terminal.Buffer?.DrainPending(); + LogItem emptyLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + StringAssert.Contains("No variables found", emptyLog.message); + yield break; + } + + [UnityTest] + public IEnumerator ThemeCommandsOperateOnAvailableTheme() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + + Assert.IsTrue(shell.RunCommand("list-themes")); + Terminal.Buffer?.DrainPending(); + LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + StringAssert.Contains("test-theme", listLog.message); + + Assert.IsTrue(shell.RunCommand("get-theme")); + Terminal.Buffer?.DrainPending(); + LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + StringAssert.Contains("Current terminal theme", getLog.message); + + Assert.IsTrue(shell.RunCommand("set-theme test-theme")); + Terminal.Buffer?.DrainPending(); + LogItem setLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + StringAssert.Contains("test-theme", setLog.message); + + Assert.IsTrue(shell.RunCommand("set-random-theme")); + Terminal.Buffer?.DrainPending(); + LogItem randomLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + StringAssert.Contains("set theme", randomLog.message); + yield break; + } + + [UnityTest] + public IEnumerator FontCommandsHandleMissingFonts() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + + Assert.IsTrue(shell.RunCommand("list-fonts")); + Terminal.Buffer?.DrainPending(); + LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + Assert.IsNotNull(listLog); + + Assert.IsTrue(shell.RunCommand("get-font")); + Terminal.Buffer?.DrainPending(); + LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + StringAssert.Contains("null", getLog.message); + + Assert.IsTrue(shell.RunCommand("set-font missing-font")); + Terminal.Buffer?.DrainPending(); + LogItem warningLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + StringAssert.Contains("not found", warningLog.message); + + Assert.IsTrue(shell.RunCommand("set-random-font")); + Terminal.Buffer?.DrainPending(); + LogItem randomLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + StringAssert.Contains("No fonts available", randomLog.message); + yield break; + } + + [UnityTest] + public IEnumerator HelpCommandProvidesCommandDetails() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + + int initialCount = Terminal.Buffer.Logs.Count; + Assert.IsTrue(shell.RunCommand("help")); + Terminal.Buffer?.DrainPending(); + bool hasUsage = Terminal.Buffer.Logs + .Skip(initialCount) + .Any(item => item.type == TerminalLogType.Message && item.message.Contains("Usage:")); + Assert.IsTrue(hasUsage); + + initialCount = Terminal.Buffer.Logs.Count; + Assert.IsTrue(shell.RunCommand("help clear-history")); + Terminal.Buffer?.DrainPending(); + LogItem specificLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Message && item.message.Contains("clear-history") + ); + StringAssert.Contains("clear-history", specificLog.message); + yield break; + } + + [UnityTest] + public IEnumerator NoOpCommandPersistsInHistory() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + CommandHistory history = Terminal.History; + + Assert.IsTrue(shell.RunCommand("no-op")); + string[] entries = history.GetHistory(false, false).ToArray(); + CollectionAssert.Contains(entries, "no-op"); + yield break; + } + + [UnityTest] + public IEnumerator QuitCommandIsDiscoverable() + { + yield return RestartTerminal(); + + CommandShell shell = Terminal.Shell; + Assert.IsTrue(shell.Commands.ContainsKey("quit")); + yield break; + } + } +} diff --git a/Tests/Runtime/BuiltinCommandTests.cs.meta b/Tests/Runtime/BuiltinCommandTests.cs.meta new file mode 100644 index 0000000..ce980c6 --- /dev/null +++ b/Tests/Runtime/BuiltinCommandTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2cfde9593ef54551bec60aadf6a76eb5 +timeCreated: 1760485358 From 00a8c0cbbde461fa361b731356e78365d4c1639d Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 17:33:00 -0700 Subject: [PATCH 21/69] Clear-History working --- AGENTS.md | 3 ++ .../Backend/BuiltinCommands.cs | 7 ++++ Runtime/CommandTerminal/Backend/CommandLog.cs | 37 +++++++++++++++++++ Tests/Runtime/BuiltinCommandTests.cs | 22 +++++++++++ 4 files changed, 69 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index dfe75dc..1fc30ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,9 @@ - Do not create `async Task` test methods - the Unity test runner does not support this. Make do with `IEnumerator` based UnityTestMethods. - Do not use `Assert.ThrowsAsync`, it does not exist. - When asserting that UnityEngine.Objects are null or not null, please check for null directly (thing != null, thing == null), to properly adhere to Unity Object existence checks. +- Do not use underscores in function names, especially test function names. +- Do not use regions, anywhere, ever. +- Avoid `var` wherever possible, use expressive types. ## Commit & Pull Request Guidelines diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index 56b4e99..5b279f0 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -352,6 +352,13 @@ public static void CommandClearConsole(CommandArg[] args) public static void CommandClearHistory(CommandArg[] args) { Terminal.History?.Clear(); + + CommandLog buffer = Terminal.Buffer; + if (buffer != null) + { + buffer.DrainPending(); + buffer.RemoveWhere(log => log.type == TerminalLogType.Input); + } } [RegisterCommand( diff --git a/Runtime/CommandTerminal/Backend/CommandLog.cs b/Runtime/CommandTerminal/Backend/CommandLog.cs index e1a4917..fec9bae 100644 --- a/Runtime/CommandTerminal/Backend/CommandLog.cs +++ b/Runtime/CommandTerminal/Backend/CommandLog.cs @@ -194,6 +194,43 @@ public int Clear() return logCount; } + public int RemoveWhere(Func predicate) + { + if (predicate == null) + { + return 0; + } + + int count = _logs.Count; + if (count == 0) + { + return 0; + } + + List retained = new(count); + for (int i = 0; i < count; ++i) + { + LogItem entry = _logs[i]; + if (!predicate(entry)) + { + retained.Add(entry); + } + } + + if (retained.Count == count) + { + return 0; + } + + _logs.Clear(); + for (int i = 0; i < retained.Count; ++i) + { + _logs.Add(retained[i]); + } + _version++; + return count - retained.Count; + } + public void Resize(int newCapacity) { if (newCapacity < _logs.Count) diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index 4ad807f..671de99 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -111,6 +111,28 @@ public IEnumerator ClearConsoleCommandClearsBuffer() yield break; } + [UnityTest] + public IEnumerator ClearHistoryCommandPurgesInputLogs() + { + yield return RestartTerminal(); + + CommandLog buffer = Terminal.Buffer; + Assert.IsNotNull(buffer); + + buffer.Clear(); + Terminal.Log(TerminalLogType.Input, "alpha"); + buffer.DrainPending(); + Assert.IsTrue(buffer.Logs.Any(log => log.type == TerminalLogType.Input)); + + CommandShell shell = Terminal.Shell; + Assert.IsNotNull(shell); + Assert.IsTrue(shell.RunCommand("clear-history")); + + buffer.DrainPending(); + Assert.IsFalse(buffer.Logs.Any(log => log.type == TerminalLogType.Input)); + yield break; + } + [UnityTest] public IEnumerator TimeCommandMeasuresNestedExecution() { From 2446ec6ade76a6c18f8f85d50142214fb66b45a6 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 18:13:17 -0700 Subject: [PATCH 22/69] More better command terminal --- .../Backend/CommandAutoComplete.cs | 23 +++- Runtime/CommandTerminal/UI/TerminalUI.cs | 93 +++++++++---- Tests/Runtime/AutocompleteTests.cs | 31 +++-- Tests/Runtime/Components/TestTerminalInput.cs | 11 ++ .../Components/TestTerminalInput.cs.meta | 11 ++ Tests/Runtime/TerminalAutocompleteTests.cs | 123 ++++++++++++++++++ .../Runtime/TerminalAutocompleteTests.cs.meta | 11 ++ 7 files changed, 258 insertions(+), 45 deletions(-) create mode 100644 Tests/Runtime/Components/TestTerminalInput.cs create mode 100644 Tests/Runtime/Components/TestTerminalInput.cs.meta create mode 100644 Tests/Runtime/TerminalAutocompleteTests.cs create mode 100644 Tests/Runtime/TerminalAutocompleteTests.cs.meta diff --git a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs index 8cd68eb..9dea0e1 100644 --- a/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs +++ b/Runtime/CommandTerminal/Backend/CommandAutoComplete.cs @@ -16,6 +16,10 @@ public sealed class CommandAutoComplete private readonly CommandHistory _history; private readonly CommandShell _shell; + public string LastCompleterPrefix { get; private set; } + + public bool LastCompletionUsedCompleter { get; private set; } + public CommandAutoComplete( CommandHistory history, CommandShell shell, @@ -47,6 +51,8 @@ public List Complete(string text, int caretIndex, List buffer) string input = text ?? string.Empty; buffer.Clear(); _duplicateBuffer.Clear(); + LastCompleterPrefix = null; + LastCompletionUsedCompleter = false; if (string.IsNullOrWhiteSpace(input)) { @@ -138,6 +144,7 @@ public List Complete(string text, int caretIndex, List buffer) } string prefixBase = _sb.ToString(); + bool addedSuggestions = false; foreach (string suggestion in suggestions) { if (string.IsNullOrWhiteSpace(suggestion)) @@ -174,13 +181,16 @@ public List Complete(string text, int caretIndex, List buffer) : full; if (_duplicateBuffer.Add(key)) { - buffer.Add(full); + buffer.Add(insertion); + addedSuggestions = true; } } // If we got any results from the completer, return them. - if (0 < buffer.Count) + if (addedSuggestions) { + LastCompleterPrefix = prefixBase; + LastCompletionUsedCompleter = true; return buffer; } @@ -227,11 +237,10 @@ List buffer { continue; } - string duplicateKey = !string.IsNullOrEmpty(normalizedCommand) - ? normalizedCommand - : command.NeedsLowerInvariantConversion() - ? command.ToLowerInvariant() - : command; + string duplicateKey = + !string.IsNullOrEmpty(normalizedCommand) ? normalizedCommand + : command.NeedsLowerInvariantConversion() ? command.ToLowerInvariant() + : command; string display = command.NeedsLowerInvariantConversion() ? command.ToLowerInvariant() : command; diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 910f693..1c46c88 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -255,6 +255,8 @@ private enum ScrollBarCaptureState StringComparer.OrdinalIgnoreCase ); private readonly List _autoCompleteChildren = new(); + private string _lastCompletionAnchorText; + private int? _lastCompletionAnchorCaretIndex; private readonly List _launcherHistoryEntries = new(); private float _launcherSuggestionContentHeight; @@ -727,6 +729,8 @@ private static void ConsumeAndLogErrors() private void ResetAutoComplete() { _lastKnownCommandText = _input.CommandText ?? string.Empty; + _lastCompletionAnchorText = null; + _lastCompletionAnchorCaretIndex = null; if (hintDisplayMode == HintDisplayMode.Always) { _lastCompletionBufferTempCache.Clear(); @@ -1898,16 +1902,15 @@ private bool ShouldApplyHistoryFade() { return _state switch { - TerminalState.OpenLauncher - => _historyFadeTargets.HasFlagNoAlloc(TerminalHistoryFadeTargets.Launcher), - TerminalState.OpenSmall - => _historyFadeTargets.HasFlagNoAlloc( - TerminalHistoryFadeTargets.SmallTerminal - ), - TerminalState.OpenFull - => _historyFadeTargets.HasFlagNoAlloc( - TerminalHistoryFadeTargets.FullTerminal - ), + TerminalState.OpenLauncher => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.Launcher + ), + TerminalState.OpenSmall => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.SmallTerminal + ), + TerminalState.OpenFull => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.FullTerminal + ), _ => false, }; } @@ -2061,11 +2064,7 @@ private void CacheLauncherScrollPosition() } float highValue = _logScrollView.verticalScroller.highValue; - float currentValue = Mathf.Clamp( - _logScrollView.verticalScroller.value, - 0f, - highValue - ); + float currentValue = Mathf.Clamp(_logScrollView.verticalScroller.value, 0f, highValue); _cachedLauncherScrollValue = currentValue; _cachedLauncherScrollVersion = Terminal.History?.Version ?? -1; } @@ -2084,8 +2083,7 @@ private void ScheduleLauncherScroll(float targetValue) ); _logScrollView - .schedule - .Execute(() => + .schedule.Execute(() => { if (_logScrollView?.verticalScroller == null) { @@ -2147,7 +2145,7 @@ private void RefreshAutoCompleteHints() string currentHint = hint; Button hintButton = new(() => { - _input.CommandText = currentHint; + _input.CommandText = BuildCompletionText(currentHint); _lastCompletionIndex = currentIndex; _needsFocus = true; }) @@ -2874,6 +2872,23 @@ public void EnterCommand() } } + private string BuildCompletionText(string suggestion) + { + if (string.IsNullOrEmpty(suggestion)) + { + return suggestion ?? string.Empty; + } + + CommandAutoComplete autoComplete = Terminal.AutoComplete; + if (autoComplete == null || !autoComplete.LastCompletionUsedCompleter) + { + return suggestion; + } + + string prefix = autoComplete.LastCompleterPrefix ?? string.Empty; + return string.Concat(prefix, suggestion); + } + public void CompleteCommand(bool searchForward = true) { if (_state == TerminalState.Closed) @@ -2883,23 +2898,33 @@ public void CompleteCommand(bool searchForward = true) try { + CommandAutoComplete autoComplete = Terminal.AutoComplete; + if (autoComplete == null) + { + return; + } + _lastKnownCommandText = _input.CommandText ?? string.Empty; _lastCompletionBufferTempCache.Clear(); int caret = _commandInput != null ? _commandInput.cursorIndex : (_lastKnownCommandText?.Length ?? 0); - Terminal.AutoComplete?.Complete( - _lastKnownCommandText, - caret, + + string completionSource = _lastCompletionAnchorText ?? _lastKnownCommandText; + int completionCaret = _lastCompletionAnchorCaretIndex ?? caret; + + autoComplete.Complete( + completionSource, + completionCaret, _lastCompletionBufferTempCache ); + bool equivalentBuffers = true; try { int completionLength = _lastCompletionBufferTempCache.Count; - equivalentBuffers = - _lastCompletionBuffer.Count == _lastCompletionBufferTempCache.Count; + equivalentBuffers = _lastCompletionBuffer.Count == completionLength; if (equivalentBuffers) { _lastCompletionBufferTempSet.Clear(); @@ -2917,9 +2942,10 @@ public void CompleteCommand(bool searchForward = true) } } } + if (equivalentBuffers) { - if (0 < completionLength) + if (completionLength > 0) { if (_lastCompletionIndex == null) { @@ -2937,23 +2963,36 @@ public void CompleteCommand(bool searchForward = true) % completionLength; } - _input.CommandText = _lastCompletionBuffer[_lastCompletionIndex.Value]; + string selection = _lastCompletionBuffer[_lastCompletionIndex.Value]; + _input.CommandText = BuildCompletionText(selection); + if (_lastCompletionAnchorText == null) + { + _lastCompletionAnchorText = completionSource; + _lastCompletionAnchorCaretIndex = completionCaret; + } } else { _lastCompletionIndex = null; + _lastCompletionAnchorText = null; + _lastCompletionAnchorCaretIndex = null; } } else { - if (0 < completionLength) + if (completionLength > 0) { _lastCompletionIndex = 0; - _input.CommandText = _lastCompletionBufferTempCache[0]; + string selection = _lastCompletionBufferTempCache[0]; + _input.CommandText = BuildCompletionText(selection); + _lastCompletionAnchorText = completionSource; + _lastCompletionAnchorCaretIndex = completionCaret; } else { _lastCompletionIndex = null; + _lastCompletionAnchorText = null; + _lastCompletionAnchorCaretIndex = null; } } } diff --git a/Tests/Runtime/AutocompleteTests.cs b/Tests/Runtime/AutocompleteTests.cs index 0b549b2..fbfbddf 100644 --- a/Tests/Runtime/AutocompleteTests.cs +++ b/Tests/Runtime/AutocompleteTests.cs @@ -106,10 +106,12 @@ public void CompleterProducesQuotedSuggestions() CommandAutoComplete ac = new(history, shell); string[] results = ac.Complete("testcmd "); - // Expect both suggestions formatted for insertion + // Expect suggestions returned without the command prefix but still quoted when needed Assert.IsNotNull(results); - CollectionAssert.Contains(results, "testcmd \"foo bar\""); - CollectionAssert.Contains(results, "testcmd baz"); + CollectionAssert.Contains(results, "\"foo bar\""); + CollectionAssert.Contains(results, "baz"); + Assert.IsTrue(ac.LastCompletionUsedCompleter); + Assert.AreEqual("testcmd ", ac.LastCompleterPrefix); } [Test] @@ -182,10 +184,9 @@ public void AutoCompleteChainsArguments() CollectionAssert.Contains(commandSuggestions, "chain"); string[] firstArgumentSuggestions = autoComplete.Complete("chain "); - CollectionAssert.AreEquivalent( - new[] { "chain alpha", "chain beta" }, - firstArgumentSuggestions - ); + CollectionAssert.AreEquivalent(new[] { "alpha", "beta" }, firstArgumentSuggestions); + Assert.IsTrue(autoComplete.LastCompletionUsedCompleter); + Assert.AreEqual("chain ", autoComplete.LastCompleterPrefix); Assert.AreEqual(1, chainedCompleter.Calls.Count); CommandCompletionContext firstContext = chainedCompleter.Calls[0]; Assert.AreEqual(0, firstContext.ArgIndex); @@ -193,7 +194,9 @@ public void AutoCompleteChainsArguments() Assert.AreEqual(0, firstContext.ArgsBeforeCursor.Count); string[] secondArgumentSuggestions = autoComplete.Complete("chain alpha "); - CollectionAssert.Contains(secondArgumentSuggestions, "chain alpha gamma"); + CollectionAssert.Contains(secondArgumentSuggestions, "gamma"); + Assert.IsTrue(autoComplete.LastCompletionUsedCompleter); + Assert.AreEqual("chain alpha ", autoComplete.LastCompleterPrefix); Assert.AreEqual(2, chainedCompleter.Calls.Count); CommandCompletionContext secondContext = chainedCompleter.Calls[1]; Assert.AreEqual(1, secondContext.ArgIndex); @@ -203,7 +206,9 @@ public void AutoCompleteChainsArguments() chainedCompleter.Calls.Clear(); string[] partialFirstArgument = autoComplete.Complete("chain a"); - CollectionAssert.Contains(partialFirstArgument, "chain alpha"); + CollectionAssert.Contains(partialFirstArgument, "alpha"); + Assert.IsTrue(autoComplete.LastCompletionUsedCompleter); + Assert.AreEqual("chain ", autoComplete.LastCompleterPrefix); Assert.AreEqual(1, chainedCompleter.Calls.Count); CommandCompletionContext partialFirstContext = chainedCompleter.Calls[0]; Assert.AreEqual(0, partialFirstContext.ArgIndex); @@ -212,7 +217,9 @@ public void AutoCompleteChainsArguments() chainedCompleter.Calls.Clear(); string[] partialSecondArgument = autoComplete.Complete("chain alpha g"); - CollectionAssert.Contains(partialSecondArgument, "chain alpha gamma"); + CollectionAssert.Contains(partialSecondArgument, "gamma"); + Assert.IsTrue(autoComplete.LastCompletionUsedCompleter); + Assert.AreEqual("chain alpha ", autoComplete.LastCompleterPrefix); Assert.AreEqual(1, chainedCompleter.Calls.Count); CommandCompletionContext partialSecondContext = chainedCompleter.Calls[0]; Assert.AreEqual(1, partialSecondContext.ArgIndex); @@ -235,7 +242,9 @@ public void AutoCompleteHonorsCaretIndexWithinInput() autoComplete.Complete("chain alpha gamma", caretIndex, buffer); Assert.AreEqual(1, buffer.Count); - Assert.AreEqual("chain alpha gamma", buffer[0]); + Assert.AreEqual("gamma", buffer[0]); + Assert.IsTrue(autoComplete.LastCompletionUsedCompleter); + Assert.AreEqual("chain alpha ", autoComplete.LastCompleterPrefix); Assert.AreEqual(1, chainedCompleter.Calls.Count); CommandCompletionContext context = chainedCompleter.Calls[0]; Assert.AreEqual(1, context.ArgIndex); diff --git a/Tests/Runtime/Components/TestTerminalInput.cs b/Tests/Runtime/Components/TestTerminalInput.cs new file mode 100644 index 0000000..8fd78b9 --- /dev/null +++ b/Tests/Runtime/Components/TestTerminalInput.cs @@ -0,0 +1,11 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components +{ + using WallstopStudios.DxCommandTerminal.Input; + using UnityEngine; + + [DisallowMultipleComponent] + public sealed class TestTerminalInput : MonoBehaviour, ITerminalInput + { + public string CommandText { get; set; } = string.Empty; + } +} diff --git a/Tests/Runtime/Components/TestTerminalInput.cs.meta b/Tests/Runtime/Components/TestTerminalInput.cs.meta new file mode 100644 index 0000000..a3dc4d6 --- /dev/null +++ b/Tests/Runtime/Components/TestTerminalInput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 611a425b7ca647f0900f83a83b0adc2e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/TerminalAutocompleteTests.cs b/Tests/Runtime/TerminalAutocompleteTests.cs new file mode 100644 index 0000000..7672ca0 --- /dev/null +++ b/Tests/Runtime/TerminalAutocompleteTests.cs @@ -0,0 +1,123 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using System.Collections.Generic; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + using UnityEngine.UIElements; + + public sealed class TerminalAutocompleteTests + { + private sealed class ThemeCompleter : IArgumentCompleter + { + public IEnumerable Complete(CommandCompletionContext context) + { + return new[] { "dark", "light" }; + } + } + + [TearDown] + public void TearDown() + { + if (TerminalUI.Instance != null) + { + Object.Destroy(TerminalUI.Instance.gameObject); + } + } + + [UnityTest] + public IEnumerator TabCyclesThroughMultipleSuggestions() + { + yield return SetupTerminalWithCustomInput(resetStateOnInit: true); + + TerminalUI terminal = TerminalUI.Instance; + TestTerminalInput input = terminal.GetComponent(); + Assert.IsNotNull(input); + + Terminal.Shell.AddCommand("cycle-first", _ => { }); + Terminal.Shell.AddCommand("cycle-second", _ => { }); + + terminal.SetState(TerminalState.OpenFull); + yield return null; + + input.CommandText = "cyc"; + + terminal.CompleteCommand(); + Assert.AreEqual("cycle-first", input.CommandText); + + terminal.CompleteCommand(); + Assert.AreEqual("cycle-second", input.CommandText); + + terminal.CompleteCommand(); + Assert.AreEqual("cycle-first", input.CommandText); + } + + [UnityTest] + public IEnumerator CompleterSuggestionsCycleWithPrefix() + { + yield return SetupTerminalWithCustomInput(resetStateOnInit: true); + + TerminalUI terminal = TerminalUI.Instance; + TestTerminalInput input = terminal.GetComponent(); + Assert.IsNotNull(input); + + CommandShell shell = Terminal.Shell; + shell.AddCommand( + "set-theme", + _ => { }, + 0, + -1, + string.Empty, + null, + new ThemeCompleter() + ); + + terminal.SetState(TerminalState.OpenFull); + yield return null; + + input.CommandText = "set-theme"; + + terminal.CompleteCommand(); + Assert.AreEqual("set-theme dark", input.CommandText); + Assert.IsTrue(Terminal.AutoComplete.LastCompletionUsedCompleter); + Assert.AreEqual("set-theme ", Terminal.AutoComplete.LastCompleterPrefix); + + terminal.CompleteCommand(); + Assert.AreEqual("set-theme light", input.CommandText); + Assert.AreEqual("set-theme ", Terminal.AutoComplete.LastCompleterPrefix); + + terminal.CompleteCommand(searchForward: false); + Assert.AreEqual("set-theme dark", input.CommandText); + Assert.AreEqual("set-theme ", Terminal.AutoComplete.LastCompleterPrefix); + } + + private static IEnumerator SetupTerminalWithCustomInput(bool resetStateOnInit) + { + yield return TestSceneHelpers.DestroyTerminalAndWait(); + + GameObject go = new("Terminal"); + go.SetActive(false); + + TestThemePack themePack = ScriptableObject.CreateInstance(); + StyleSheet style = ScriptableObject.CreateInstance(); + themePack.Add(style, "test-theme"); + + TestFontPack fontPack = ScriptableObject.CreateInstance(); + + go.AddComponent(); + StartTracker startTracker = go.AddComponent(); + + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + terminal.InjectPacks(themePack, fontPack); + terminal.resetStateOnInit = resetStateOnInit; + + go.SetActive(true); + yield return new WaitUntil(() => startTracker.Started); + } + } +} diff --git a/Tests/Runtime/TerminalAutocompleteTests.cs.meta b/Tests/Runtime/TerminalAutocompleteTests.cs.meta new file mode 100644 index 0000000..881afe3 --- /dev/null +++ b/Tests/Runtime/TerminalAutocompleteTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e789a70e50854bc5a9bb2a943ec914aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 506fc92f5bcba0b42fcf7db2531f440d6217ccfd Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 18:34:00 -0700 Subject: [PATCH 23/69] Progress on launcher --- Runtime/CommandTerminal/UI/TerminalUI.cs | 91 +++++++++++++++--------- Tests/Runtime/LauncherModeTests.cs | 84 ++++++++++++++++++++++ 2 files changed, 141 insertions(+), 34 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 1c46c88..868c781 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -24,6 +24,7 @@ public sealed class TerminalUI : MonoBehaviour private const float LauncherAutoCompleteSpacing = 6f; private const float LauncherEstimatedSuggestionRowHeight = 32f; private const float LauncherEstimatedHistoryRowHeight = 28f; + private const float LauncherInputFallbackHeight = 30f; private const float StandardEstimatedHistoryRowHeight = 24f; private enum ScrollBarCaptureState @@ -3162,9 +3163,14 @@ private void UpdateLauncherLayoutMetrics() return; } - float padding = _launcherMetrics.InsetPadding; + float horizontalPadding = _launcherMetrics.InsetPadding; + float verticalPadding = Mathf.Max(4f, horizontalPadding * 0.5f); float inputHeight = Mathf.Max(_inputContainer.resolvedStyle.height, 0f); - float availableWidth = Mathf.Max(0f, _launcherMetrics.Width - (padding * 2f)); + if (inputHeight <= 0f) + { + inputHeight = LauncherInputFallbackHeight; + } + float availableWidth = Mathf.Max(0f, _launcherMetrics.Width - (horizontalPadding * 2f)); _autoCompleteContainer.style.width = availableWidth; _autoCompleteContainer.style.maxWidth = availableWidth; _autoCompleteContainer.style.alignSelf = Align.Stretch; @@ -3229,14 +3235,6 @@ private void UpdateLauncherLayoutMetrics() } _autoCompleteContainer.style.marginBottom = 0; - float spacingAboveLog = hasSuggestions - ? LauncherAutoCompleteSpacing - : Mathf.Max(LauncherAutoCompleteSpacing, padding * 0.25f); - - float reservedForSuggestions = hasSuggestions - ? suggestionsHeight + spacingAboveLog - : spacingAboveLog; - VisualElement historyContent = _logScrollView.contentContainer; int visibleHistoryCount = 0; int historyChildCount = historyContent.childCount; @@ -3263,31 +3261,46 @@ private void UpdateLauncherLayoutMetrics() } } - float historyHeightFromContent = - visibleHistoryCount > 0 ? _launcherHistoryContentHeight : 0f; + bool hasHistory = visibleHistoryCount > 0; + + float spacingAboveLog = 0f; + if (hasHistory) + { + spacingAboveLog = hasSuggestions + ? LauncherAutoCompleteSpacing + : Mathf.Max(LauncherAutoCompleteSpacing, verticalPadding * 0.25f); + } + else if (hasSuggestions) + { + spacingAboveLog = LauncherAutoCompleteSpacing; + } + + float reservedForSuggestions = hasSuggestions + ? suggestionsHeight + spacingAboveLog + : spacingAboveLog; + + float historyHeightFromContent = hasHistory ? _launcherHistoryContentHeight : 0f; if (float.IsNaN(historyHeightFromContent) || historyHeightFromContent < 0f) { historyHeightFromContent = 0f; } - float estimatedHistoryHeight = - visibleHistoryCount > 0 - ? visibleHistoryCount * LauncherEstimatedHistoryRowHeight - : 0f; + float estimatedHistoryHeight = hasHistory + ? visibleHistoryCount * LauncherEstimatedHistoryRowHeight + : 0f; - float desiredHistoryHeight = - visibleHistoryCount > 0 - ? Mathf.Min( - Mathf.Max(historyHeightFromContent, estimatedHistoryHeight), - _launcherMetrics.HistoryHeight - ) - : 0f; + float desiredHistoryHeight = hasHistory + ? Mathf.Min( + Mathf.Max(historyHeightFromContent, estimatedHistoryHeight), + _launcherMetrics.HistoryHeight + ) + : 0f; if (desiredHistoryHeight < 0f) { desiredHistoryHeight = 0f; } - float minimumHeight = padding * 2f + inputHeight + reservedForSuggestions; + float minimumHeight = (verticalPadding * 2f) + inputHeight + reservedForSuggestions; float desiredHeight = minimumHeight + desiredHistoryHeight; float clampedHeight = Mathf.Clamp( desiredHeight, @@ -3297,23 +3310,33 @@ private void UpdateLauncherLayoutMetrics() if (!Mathf.Approximately(clampedHeight, _targetWindowHeight)) { - _initialWindowHeight = Mathf.Clamp( - _currentWindowHeight, - minimumHeight, - _launcherMetrics.Height - ); + float previousTarget = _targetWindowHeight; _targetWindowHeight = clampedHeight; - _animationTimer = 0f; - _isAnimating = true; - if (Mathf.Approximately(_initialWindowHeight, clampedHeight)) + + if (_launcherMetrics.SnapOpen) { - _currentWindowHeight = clampedHeight; + _currentWindowHeight = _targetWindowHeight; _isAnimating = false; } + else + { + _initialWindowHeight = Mathf.Clamp( + _currentWindowHeight, + minimumHeight, + _launcherMetrics.Height + ); + if (!Mathf.Approximately(previousTarget, _targetWindowHeight)) + { + StartHeightAnimation(); + } + } } float availableForHistory = - _currentWindowHeight - (padding * 2f) - inputHeight - reservedForSuggestions; + _currentWindowHeight + - (verticalPadding * 2f) + - inputHeight + - reservedForSuggestions; availableForHistory = Mathf.Min(availableForHistory, _launcherMetrics.HistoryHeight); availableForHistory = Mathf.Max(0f, availableForHistory); diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index 3c8fe57..1469ec4 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -1,6 +1,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime { using System.Collections; + using System.Reflection; using Backend; using Components; using NUnit.Framework; @@ -170,5 +171,88 @@ public IEnumerator LauncherResetMaintainsDynamicTargetHeight() yield return TestSceneHelpers.DestroyTerminalAndWait(); } + + [Test] + public void LauncherWithoutContentCollapsesToInputPadding() + { + GameObject go = new("LauncherTest"); + TerminalUI terminal = go.AddComponent(); + + try + { + LauncherLayoutMetrics metrics = new( + width: 640f, + height: 200f, + left: 100f, + top: 200f, + historyHeight: 0f, + cornerRadius: 16f, + insetPadding: 14f, + historyVisibleEntryCount: 5, + historyFadeExponent: 2f, + snapOpen: true, + animationDuration: 0.1f + ); + + ScrollView autoComplete = new(ScrollViewMode.Horizontal); + ScrollView log = new(); + VisualElement input = new(); + + SetPrivateField(terminal, "_state", TerminalState.OpenLauncher); + SetPrivateField(terminal, "_launcherMetricsInitialized", true); + SetPrivateField(terminal, "_launcherMetrics", metrics); + SetPrivateField(terminal, "_autoCompleteContainer", autoComplete); + SetPrivateField(terminal, "_autoCompleteViewport", autoComplete.contentViewport); + SetPrivateField(terminal, "_logScrollView", log); + SetPrivateField(terminal, "_inputContainer", input); + SetPrivateField(terminal, "_currentWindowHeight", metrics.Height); + SetPrivateField(terminal, "_targetWindowHeight", metrics.Height); + SetPrivateField(terminal, "_launcherSuggestionContentHeight", 0f); + SetPrivateField(terminal, "_launcherHistoryContentHeight", 0f); + + MethodInfo updateMetrics = typeof(TerminalUI).GetMethod( + "UpdateLauncherLayoutMetrics", + BindingFlags.Instance | BindingFlags.NonPublic + ); + Assert.IsNotNull(updateMetrics); + + updateMetrics!.Invoke(terminal, null); + + float fallbackHeight = (float) + typeof(TerminalUI) + .GetField( + "LauncherInputFallbackHeight", + BindingFlags.Static | BindingFlags.NonPublic + ) + ?.GetValue(null); + Assert.Greater(fallbackHeight, 0f); + + float expectedPadding = Mathf.Max(4f, metrics.InsetPadding * 0.5f); + float expectedHeight = (expectedPadding * 2f) + fallbackHeight; + + Assert.That( + terminal.TargetWindowHeightForTests, + Is.EqualTo(expectedHeight).Within(0.001f) + ); + Assert.That( + terminal.CurrentWindowHeightForTests, + Is.EqualTo(expectedHeight).Within(0.001f) + ); + } + finally + { + Object.DestroyImmediate(go); + } + } + + private static void SetPrivateField(TerminalUI terminal, string fieldName, T value) + { + FieldInfo field = typeof(TerminalUI).GetField( + fieldName, + BindingFlags.Instance | BindingFlags.NonPublic + ); + Assert.IsNotNull(field, $"Expected field '{fieldName}' to exist."); + field!.SetValue(terminal, value); + } } } From ff71f00c45b055b4960dee8b975906bdd09e13b5 Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 19:43:15 -0700 Subject: [PATCH 24/69] Progress --- Editor/CustomEditors/TerminalUIEditor.cs | 14 +++++++---- Editor/Parsers/ParserAutoDiscovery.cs | 2 +- Editor/TerminalUI.RuntimeMode.Editor.cs | 16 ++++++------- .../Utils/ScriptableObjectSingletonCreator.cs | 2 +- Runtime/CommandTerminal/Backend/CommandArg.cs | 2 +- .../CommandTerminal/Backend/CommandShell.cs | 24 +++++++++++++++++-- .../Backend/TerminalRuntimeConfig.cs | 2 +- Tests/Runtime/AutocompleteTests.cs | 2 +- Tests/Runtime/BuiltinCommandTests.cs | 11 +++++---- Tests/Runtime/CompletersTests.cs | 22 ++++++++--------- Tests/Runtime/Components/TestTerminalInput.cs | 2 +- Tests/Runtime/CyclicBufferTests.cs | 2 +- Tests/Runtime/LauncherModeTests.cs | 14 +++++------ Tests/Runtime/ParserTests.cs | 4 ++-- .../Parsers/ObjectParserRegistryTests.cs | 6 +++-- Tests/Runtime/TerminalTests.cs | 14 +++++------ 16 files changed, 85 insertions(+), 54 deletions(-) diff --git a/Editor/CustomEditors/TerminalUIEditor.cs b/Editor/CustomEditors/TerminalUIEditor.cs index 3c0ece1..093546f 100644 --- a/Editor/CustomEditors/TerminalUIEditor.cs +++ b/Editor/CustomEditors/TerminalUIEditor.cs @@ -6,6 +6,8 @@ namespace WallstopStudios.DxCommandTerminal.Editor.CustomEditors using System.Diagnostics; using System.IO; using System.Linq; + using System.Reflection; + using Attributes; using Backend; using DxCommandTerminal.Helper; using UnityEditor; @@ -207,10 +209,12 @@ private void OnEnable() _allCommands.Clear(); _defaultCommands.Clear(); _nonDefaultCommands.Clear(); - var reg = CommandShell.RegisteredCommands.Value; + (MethodInfo method, RegisterCommandAttribute attribute)[] reg = CommandShell + .RegisteredCommands + .Value; for (int i = 0; i < reg.Length; ++i) { - var attr = reg[i].attribute; + RegisterCommandAttribute attr = reg[i].attribute; if (attr == null || string.IsNullOrWhiteSpace(attr.Name)) { continue; @@ -500,10 +504,12 @@ private void HydrateCommandCaches() _allCommands.Clear(); _defaultCommands.Clear(); _nonDefaultCommands.Clear(); - var reg = CommandShell.RegisteredCommands.Value; + (MethodInfo method, RegisterCommandAttribute attribute)[] reg = CommandShell + .RegisteredCommands + .Value; for (int i = 0; i < reg.Length; ++i) { - var attr = reg[i].attribute; + RegisterCommandAttribute attr = reg[i].attribute; if (attr == null || string.IsNullOrWhiteSpace(attr.Name)) { continue; diff --git a/Editor/Parsers/ParserAutoDiscovery.cs b/Editor/Parsers/ParserAutoDiscovery.cs index a1b3a94..2ce2372 100644 --- a/Editor/Parsers/ParserAutoDiscovery.cs +++ b/Editor/Parsers/ParserAutoDiscovery.cs @@ -2,7 +2,7 @@ namespace WallstopStudios.DxCommandTerminal.Editor { #if UNITY_EDITOR using UnityEditor; - using WallstopStudios.DxCommandTerminal.Backend; + using Backend; [InitializeOnLoad] internal static class ParserAutoDiscovery diff --git a/Editor/TerminalUI.RuntimeMode.Editor.cs b/Editor/TerminalUI.RuntimeMode.Editor.cs index 4a150d2..21796ae 100644 --- a/Editor/TerminalUI.RuntimeMode.Editor.cs +++ b/Editor/TerminalUI.RuntimeMode.Editor.cs @@ -45,12 +45,12 @@ private static void SetAllMode() [MenuItem(MenuRoot + "Toggle Auto-Discover Parsers", false, 50)] private static void ToggleAutoDiscover() { - foreach (var obj in Selection.objects) + foreach (Object obj in Selection.objects) { - if (obj is GameObject go && go.TryGetComponent(out var ui)) + if (obj is GameObject go && go.TryGetComponent(out TerminalUI ui)) { - var so = new SerializedObject(ui); - var prop = so.FindProperty("_autoDiscoverParsersInEditor"); + SerializedObject so = new SerializedObject(ui); + SerializedProperty prop = so.FindProperty("_autoDiscoverParsersInEditor"); if (prop != null) { prop.boolValue = !prop.boolValue; @@ -63,12 +63,12 @@ private static void ToggleAutoDiscover() private static void SetSelectedRuntimeMode(TerminalRuntimeModeFlags mode) { - foreach (var obj in Selection.objects) + foreach (Object obj in Selection.objects) { - if (obj is GameObject go && go.TryGetComponent(out var ui)) + if (obj is GameObject go && go.TryGetComponent(out TerminalUI ui)) { - var so = new SerializedObject(ui); - var prop = so.FindProperty("_runtimeModes"); + SerializedObject so = new SerializedObject(ui); + SerializedProperty prop = so.FindProperty("_runtimeModes"); if (prop != null) { prop.intValue = (int)mode; diff --git a/Editor/Utils/ScriptableObjectSingletonCreator.cs b/Editor/Utils/ScriptableObjectSingletonCreator.cs index 8729f00..9179521 100644 --- a/Editor/Utils/ScriptableObjectSingletonCreator.cs +++ b/Editor/Utils/ScriptableObjectSingletonCreator.cs @@ -8,7 +8,7 @@ namespace WallstopStudios.DxCommandTerminal.Editor.Utils using System.Reflection; using UnityEditor; using UnityEngine; - using WallstopStudios.DxCommandTerminal.Internal; + using Internal; [InitializeOnLoad] public static class ScriptableObjectSingletonCreator diff --git a/Runtime/CommandTerminal/Backend/CommandArg.cs b/Runtime/CommandTerminal/Backend/CommandArg.cs index 81723f0..99b8aae 100644 --- a/Runtime/CommandTerminal/Backend/CommandArg.cs +++ b/Runtime/CommandTerminal/Backend/CommandArg.cs @@ -3,7 +3,7 @@ namespace WallstopStudios.DxCommandTerminal.Backend using System; using System.Collections.Generic; using System.Reflection; - using WallstopStudios.DxCommandTerminal.Backend.Parsers; + using Parsers; public delegate bool CommandArgParser(string input, out T parsed); diff --git a/Runtime/CommandTerminal/Backend/CommandShell.cs b/Runtime/CommandTerminal/Backend/CommandShell.cs index e96ff78..5b735e6 100644 --- a/Runtime/CommandTerminal/Backend/CommandShell.cs +++ b/Runtime/CommandTerminal/Backend/CommandShell.cs @@ -338,8 +338,28 @@ public bool RunCommand(string line) } string commandName = _arguments[0].contents ?? string.Empty; - // Remove command name from arguments - _arguments.RemoveAt(0); + int commandSegments = 1; + + if (!TryResolveCommand(commandName, out _, out _)) + { + StringBuilder spacedBuilder = null; + for (int i = 1; i < _arguments.Count; ++i) + { + spacedBuilder ??= new StringBuilder(commandName); + spacedBuilder.Append(' '); + spacedBuilder.Append(_arguments[i].contents); + string candidate = spacedBuilder.ToString(); + + if (TryResolveCommand(candidate, out _, out _)) + { + commandName = candidate; + commandSegments = i + 1; + break; + } + } + } + + _arguments.RemoveRange(0, commandSegments); return RunCommand( commandName, diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs index a57dce2..b3d8eec 100644 --- a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs @@ -1,8 +1,8 @@ namespace WallstopStudios.DxCommandTerminal.Backend { using System; + using Internal; using UnityEngine; - using WallstopStudios.DxCommandTerminal.Internal; [Flags] public enum TerminalRuntimeModeFlags diff --git a/Tests/Runtime/AutocompleteTests.cs b/Tests/Runtime/AutocompleteTests.cs index fbfbddf..3f77a11 100644 --- a/Tests/Runtime/AutocompleteTests.cs +++ b/Tests/Runtime/AutocompleteTests.cs @@ -2,9 +2,9 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime { using System; using System.Collections.Generic; + using Attributes; using Backend; using NUnit.Framework; - using WallstopStudios.DxCommandTerminal.Attributes; internal sealed class DummyCompleter : IArgumentCompleter { diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index 671de99..9be33f7 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -242,12 +242,12 @@ public IEnumerator VariableCommandsManageLifecycle() Assert.IsTrue(shell.RunCommand("get-variable foo")); Terminal.Buffer?.DrainPending(); - LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.ShellMessage); StringAssert.Contains("bar baz", getLog.message); Assert.IsTrue(shell.RunCommand("list-variables")); Terminal.Buffer?.DrainPending(); - LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.ShellMessage); StringAssert.Contains("foo", listLog.message); Assert.IsTrue(shell.RunCommand("clear-variable foo")); @@ -337,14 +337,17 @@ public IEnumerator HelpCommandProvidesCommandDetails() Terminal.Buffer?.DrainPending(); bool hasUsage = Terminal.Buffer.Logs .Skip(initialCount) - .Any(item => item.type == TerminalLogType.Message && item.message.Contains("Usage:")); + .Any(item => + item.type == TerminalLogType.ShellMessage + && item.message.Contains("Usage:") + ); Assert.IsTrue(hasUsage); initialCount = Terminal.Buffer.Logs.Count; Assert.IsTrue(shell.RunCommand("help clear-history")); Terminal.Buffer?.DrainPending(); LogItem specificLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Message && item.message.Contains("clear-history") + item.type == TerminalLogType.ShellMessage && item.message.Contains("clear-history") ); StringAssert.Contains("clear-history", specificLog.message); yield break; diff --git a/Tests/Runtime/CompletersTests.cs b/Tests/Runtime/CompletersTests.cs index b4e33e1..4fbf25f 100644 --- a/Tests/Runtime/CompletersTests.cs +++ b/Tests/Runtime/CompletersTests.cs @@ -19,8 +19,8 @@ public IEnumerator ThemeCompleterReturnsDistinctSortedAndFiltered() yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); // Replace packs with custom contents - var themePack = ScriptableObject.CreateInstance(); - var style = ScriptableObject.CreateInstance(); + TestThemePack themePack = ScriptableObject.CreateInstance(); + StyleSheet style = ScriptableObject.CreateInstance(); themePack.Add(style, "theme-Alpha"); themePack.Add(style, "beta-theme"); themePack.Add(style, "Gamma"); @@ -30,7 +30,7 @@ public IEnumerator ThemeCompleterReturnsDistinctSortedAndFiltered() ); ThemeArgumentCompleter completer = new(); - var ctx = new CommandCompletionContext( + CommandCompletionContext ctx = new CommandCompletionContext( "set-theme ", "set-theme", new List(), @@ -50,14 +50,14 @@ public IEnumerator FontCompleterHandlesDuplicatesAndFiltering() { yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - var fontPack = ScriptableObject.CreateInstance(); + TestFontPack fontPack = ScriptableObject.CreateInstance(); // Create test fonts with names (Font is a UnityEngine.Object, not a ScriptableObject) - var f1 = new Font(); - f1.name = "Consolas"; - var f2 = new Font(); - f2.name = "Cousine"; - var f3 = new Font(); - f3.name = "consolas"; // duplicate name differing by case + Font f1 = new Font { name = "Consolas" }; + Font f2 = new Font { name = "Cousine" }; + Font f3 = new Font + { + name = "consolas", // duplicate name differing by case + }; fontPack.Add(f1); fontPack.Add(f2); @@ -69,7 +69,7 @@ public IEnumerator FontCompleterHandlesDuplicatesAndFiltering() ); FontArgumentCompleter completer = new(); - var ctx = new CommandCompletionContext( + CommandCompletionContext ctx = new CommandCompletionContext( "set-font ", "set-font", new List(), diff --git a/Tests/Runtime/Components/TestTerminalInput.cs b/Tests/Runtime/Components/TestTerminalInput.cs index 8fd78b9..4952d69 100644 --- a/Tests/Runtime/Components/TestTerminalInput.cs +++ b/Tests/Runtime/Components/TestTerminalInput.cs @@ -1,6 +1,6 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Components { - using WallstopStudios.DxCommandTerminal.Input; + using Input; using UnityEngine; [DisallowMultipleComponent] diff --git a/Tests/Runtime/CyclicBufferTests.cs b/Tests/Runtime/CyclicBufferTests.cs index dcd2b43..1b4ca28 100644 --- a/Tests/Runtime/CyclicBufferTests.cs +++ b/Tests/Runtime/CyclicBufferTests.cs @@ -1,9 +1,9 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime { using System.Collections; + using DataStructures; using NUnit.Framework; using UnityEngine.TestTools; - using WallstopStudios.DxCommandTerminal.DataStructures; public sealed class CyclicBufferTests { diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index 1469ec4..4e48376 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -15,7 +15,7 @@ public sealed class LauncherModeTests [Test] public void LauncherMetricsRespectSizingModes() { - var settings = new TerminalLauncherSettings + TerminalLauncherSettings settings = new TerminalLauncherSettings { width = LauncherDimension.RelativeToScreen(0.5f), height = LauncherDimension.RelativeToScreen(0.33f), @@ -69,7 +69,7 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() Assert.IsNotNull(terminal); CommandHistory previousHistory = Terminal.History; - var history = new CommandHistory(16); + CommandHistory history = new CommandHistory(16); Terminal.History = history; try @@ -78,7 +78,7 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() history.Push("second", true, true); history.Push("third", true, true); - var metrics = new LauncherLayoutMetrics( + LauncherLayoutMetrics metrics = new LauncherLayoutMetrics( width: 640f, height: 160f, left: 100f, @@ -92,7 +92,7 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() animationDuration: 0.1f ); - var scroll = new ScrollView(); + ScrollView scroll = new ScrollView(); terminal.SetLogScrollViewForTests(scroll); terminal.SetLauncherMetricsForTests(metrics); terminal.SetState(TerminalState.OpenLauncher); @@ -102,19 +102,19 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() Assert.That(content.childCount, Is.EqualTo(3)); // Verify newest entry is first and fully opaque - var newest = content[0] as Label; + Label newest = content[0] as Label; Assert.IsNotNull(newest); Assert.That(newest!.text, Is.EqualTo("third")); Assert.That(newest.style.opacity.value, Is.EqualTo(1f).Within(0.001f)); // Middle entry has partial opacity - var middle = content[1] as Label; + Label middle = content[1] as Label; Assert.IsNotNull(middle); Assert.That(middle!.text, Is.EqualTo("second")); Assert.That(middle.style.opacity.value, Is.LessThan(1f).And.GreaterThan(0.35f)); // Oldest entry is faded out - var oldest = content[2] as Label; + Label oldest = content[2] as Label; Assert.IsNotNull(oldest); Assert.That(oldest!.text, Is.EqualTo("first")); Assert.That(oldest.style.opacity.value, Is.EqualTo(0.35f).Within(0.001f)); diff --git a/Tests/Runtime/ParserTests.cs b/Tests/Runtime/ParserTests.cs index e7a8acd..d0b949e 100644 --- a/Tests/Runtime/ParserTests.cs +++ b/Tests/Runtime/ParserTests.cs @@ -29,11 +29,11 @@ private enum SampleEnum [Test] public void StaticMemberParserFindsFields() { - Assert.IsTrue(StaticMemberParser.TryParse("Alpha", out var a)); + Assert.IsTrue(StaticMemberParser.TryParse("Alpha", out StaticLike a)); Assert.IsNotNull(a); Assert.AreEqual(1, a.Value); - Assert.IsTrue(StaticMemberParser.TryParse("Beta", out var b)); + Assert.IsTrue(StaticMemberParser.TryParse("Beta", out StaticLike b)); Assert.IsNotNull(b); Assert.AreEqual(2, b.Value); } diff --git a/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs index 7726677..68e69a5 100644 --- a/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs +++ b/Tests/Runtime/Parsers/ObjectParserRegistryTests.cs @@ -1,5 +1,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime.Parsers { + using System; + using System.Collections.Generic; using Backend; using Backend.Parsers; using NUnit.Framework; @@ -14,7 +16,7 @@ public void ListsRegisteredTypes() CommandArg.RegisterObjectParser(IntArgParser.Instance, true); CommandArg.RegisterObjectParser(FloatArgParser.Instance, true); - var types = CommandArg.GetRegisteredObjectParserTypes(); + IReadOnlyCollection types = CommandArg.GetRegisteredObjectParserTypes(); CollectionAssert.Contains(types, typeof(int)); CollectionAssert.Contains(types, typeof(float)); } @@ -37,7 +39,7 @@ public void RegisterAndUnregisterObjectParser() { CommandArg.UnregisterObjectParser(typeof(CustomType)); Assert.IsTrue(CommandArg.RegisterObjectParser(CustomTypeParser.Instance, false)); - var types = CommandArg.GetRegisteredObjectParserTypes(); + IReadOnlyCollection types = CommandArg.GetRegisteredObjectParserTypes(); CollectionAssert.Contains(types, typeof(CustomType)); Assert.IsTrue(CommandArg.UnregisterObjectParser(typeof(CustomType))); diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index bdee64a..7df712d 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -111,11 +111,11 @@ internal static IEnumerator SpawnTerminal(bool resetStateOnInit) // In tests we skip building UI entirely to avoid engine panel updates // Create lightweight test packs to avoid warnings - var themePack = ScriptableObject.CreateInstance(); - var style = ScriptableObject.CreateInstance(); + TestThemePack themePack = ScriptableObject.CreateInstance(); + StyleSheet style = ScriptableObject.CreateInstance(); themePack.Add(style, "test-theme"); - var fontPack = ScriptableObject.CreateInstance(); + TestFontPack fontPack = ScriptableObject.CreateInstance(); // UI is disabled during tests; no need to add a real font asset StartTracker startTracker = go.AddComponent(); @@ -140,10 +140,10 @@ public IEnumerator CleanRestartHelperWorks() // Start with reset and capture instances yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); - var shell1 = Terminal.Shell; - var history1 = Terminal.History; - var buffer1 = Terminal.Buffer; - var auto1 = Terminal.AutoComplete; + CommandShell shell1 = Terminal.Shell; + CommandHistory history1 = Terminal.History; + CommandLog buffer1 = Terminal.Buffer; + CommandAutoComplete auto1 = Terminal.AutoComplete; Assert.IsNotNull(shell1); Assert.IsNotNull(history1); From fbae2947214fccb630300b93c14716e4b31f8bbe Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 19:57:10 -0700 Subject: [PATCH 25/69] Updated tests --- Tests/Runtime/BuiltinCommandTests.cs | 4 +++- Tests/Runtime/LauncherModeTests.cs | 7 +++++-- Tests/Runtime/TerminalAutocompleteTests.cs | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index 9be33f7..393c966 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -7,6 +7,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime using Backend; using Components; using NUnit.Framework; + using Themes; using UI; using UnityEngine; using UnityEngine.TestTools; @@ -277,7 +278,8 @@ public IEnumerator ThemeCommandsOperateOnAvailableTheme() Assert.IsTrue(shell.RunCommand("list-themes")); Terminal.Buffer?.DrainPending(); LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); - StringAssert.Contains("test-theme", listLog.message); + string expectedThemeName = ThemeNameHelper.GetFriendlyThemeName("test-theme"); + StringAssert.Contains(expectedThemeName, listLog.message); Assert.IsTrue(shell.RunCommand("get-theme")); Terminal.Buffer?.DrainPending(); diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index 4e48376..8fa72f9 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -30,9 +30,9 @@ public void LauncherMetricsRespectSizingModes() LauncherLayoutMetrics metrics = settings.ComputeMetrics(1920, 1080); Assert.That(metrics.Width, Is.EqualTo(960f).Within(0.001f)); - Assert.That(metrics.Height, Is.EqualTo(194.4f).Within(0.001f)); + Assert.That(metrics.Height, Is.EqualTo(356.4f).Within(0.001f)); Assert.That(metrics.Left, Is.EqualTo(480f).Within(0.001f)); - Assert.That(metrics.Top, Is.EqualTo(442.8f).Within(0.5f)); + Assert.That(metrics.Top, Is.EqualTo(361.8f).Within(0.5f)); Assert.That(metrics.HistoryHeight, Is.EqualTo(metrics.Height * 0.5f).Within(0.001f)); Assert.That(metrics.HistoryVisibleEntryCount, Is.EqualTo(4)); Assert.That(metrics.HistoryFadeExponent, Is.EqualTo(2f).Within(0.001f)); @@ -176,7 +176,10 @@ public IEnumerator LauncherResetMaintainsDynamicTargetHeight() public void LauncherWithoutContentCollapsesToInputPadding() { GameObject go = new("LauncherTest"); + go.SetActive(false); TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + go.SetActive(true); try { diff --git a/Tests/Runtime/TerminalAutocompleteTests.cs b/Tests/Runtime/TerminalAutocompleteTests.cs index 7672ca0..8cd7438 100644 --- a/Tests/Runtime/TerminalAutocompleteTests.cs +++ b/Tests/Runtime/TerminalAutocompleteTests.cs @@ -66,7 +66,7 @@ public IEnumerator CompleterSuggestionsCycleWithPrefix() Assert.IsNotNull(input); CommandShell shell = Terminal.Shell; - shell.AddCommand( + bool added = shell.AddCommand( "set-theme", _ => { }, 0, @@ -75,6 +75,7 @@ public IEnumerator CompleterSuggestionsCycleWithPrefix() null, new ThemeCompleter() ); + Assert.IsTrue(added); terminal.SetState(TerminalState.OpenFull); yield return null; @@ -113,6 +114,7 @@ private static IEnumerator SetupTerminalWithCustomInput(bool resetStateOnInit) TerminalUI terminal = go.AddComponent(); terminal.disableUIForTests = true; + terminal.ignoreDefaultCommands = true; terminal.InjectPacks(themePack, fontPack); terminal.resetStateOnInit = resetStateOnInit; From 373944a0e81b7c8dfbb460ae077ab2a376a78d1a Mon Sep 17 00:00:00 2001 From: wallstop Date: Tue, 14 Oct 2025 20:23:01 -0700 Subject: [PATCH 26/69] Much better backends --- Editor/TerminalUI.RuntimeMode.Editor.cs | 83 ---------- Editor/TerminalUI.RuntimeMode.Editor.cs.meta | 11 -- README.md | 14 +- .../Backend/BuiltinCommands.cs | 2 +- .../Backend/TerminalRuntimeConfig.cs | 11 ++ Runtime/CommandTerminal/UI/TerminalUI.cs | 147 +++++++++++++++++- Tests/Runtime/BuiltinCommandTests.cs | 61 +++++--- Tests/Runtime/TerminalTests.cs | 117 +++++++++++++- 8 files changed, 318 insertions(+), 128 deletions(-) delete mode 100644 Editor/TerminalUI.RuntimeMode.Editor.cs delete mode 100644 Editor/TerminalUI.RuntimeMode.Editor.cs.meta diff --git a/Editor/TerminalUI.RuntimeMode.Editor.cs b/Editor/TerminalUI.RuntimeMode.Editor.cs deleted file mode 100644 index 21796ae..0000000 --- a/Editor/TerminalUI.RuntimeMode.Editor.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace WallstopStudios.DxCommandTerminal.Editor -{ -#if UNITY_EDITOR - using Backend; - using UI; - using UnityEditor; - using UnityEngine; - - internal static class TerminalUIRuntimeModeMenu - { - private const string MenuRoot = "Tools/Wallstop Studios/DxCommandTerminal/Runtime Mode/"; - - [MenuItem(MenuRoot + "Editor", false, 0)] - private static void SetEditorMode() - { - SetSelectedRuntimeMode(TerminalRuntimeModeFlags.Editor); - } - - [MenuItem(MenuRoot + "Development", false, 1)] - private static void SetDevelopmentMode() - { - SetSelectedRuntimeMode(TerminalRuntimeModeFlags.Development); - } - - [MenuItem(MenuRoot + "Production", false, 2)] - private static void SetProductionMode() - { - SetSelectedRuntimeMode(TerminalRuntimeModeFlags.Production); - } - - [MenuItem(MenuRoot + "Editor+Development", false, 10)] - private static void SetEditorDevMode() - { - SetSelectedRuntimeMode( - TerminalRuntimeModeFlags.Editor | TerminalRuntimeModeFlags.Development - ); - } - - [MenuItem(MenuRoot + "All", false, 11)] - private static void SetAllMode() - { - SetSelectedRuntimeMode(TerminalRuntimeModeFlags.All); - } - - [MenuItem(MenuRoot + "Toggle Auto-Discover Parsers", false, 50)] - private static void ToggleAutoDiscover() - { - foreach (Object obj in Selection.objects) - { - if (obj is GameObject go && go.TryGetComponent(out TerminalUI ui)) - { - SerializedObject so = new SerializedObject(ui); - SerializedProperty prop = so.FindProperty("_autoDiscoverParsersInEditor"); - if (prop != null) - { - prop.boolValue = !prop.boolValue; - so.ApplyModifiedProperties(); - EditorUtility.SetDirty(ui); - } - } - } - } - - private static void SetSelectedRuntimeMode(TerminalRuntimeModeFlags mode) - { - foreach (Object obj in Selection.objects) - { - if (obj is GameObject go && go.TryGetComponent(out TerminalUI ui)) - { - SerializedObject so = new SerializedObject(ui); - SerializedProperty prop = so.FindProperty("_runtimeModes"); - if (prop != null) - { - prop.intValue = (int)mode; - so.ApplyModifiedProperties(); - EditorUtility.SetDirty(ui); - } - } - } - } - } -#endif -} diff --git a/Editor/TerminalUI.RuntimeMode.Editor.cs.meta b/Editor/TerminalUI.RuntimeMode.Editor.cs.meta deleted file mode 100644 index 2d59f1b..0000000 --- a/Editor/TerminalUI.RuntimeMode.Editor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6d3326e2b0b6d5e468717da0295e2692 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/README.md b/README.md index b5a8de1..7ff6adc 100644 --- a/README.md +++ b/README.md @@ -366,16 +366,12 @@ DxCommandTerminal exposes a runtime mode enum to control environment-specific be - `Production` (4) — Enable features only for non-development builds. - `All` (7) — Enable all. -- Set mode on `TerminalUI` (serialized): +- Configure modes on `TerminalUI` via the `Runtime Mode Options` list: - - `Runtime Mode` controls active modes. - - `Editor > Auto-Discover Parsers` toggles automatic parser discovery in Editor when `Editor` mode is active. - -- Editor Menu: - - - Tools > DxCommandTerminal > Runtime Mode > [Editor | Development | Production | Editor+Development | All] - - Tools > DxCommandTerminal > Runtime Mode > Toggle Auto-Discover Parsers - - Acts on selected `TerminalUI` components (in the Hierarchy). + - Define one or more options (identifier, label, flags) to describe your environments. + - `Selected Runtime Mode Id` chooses which option applies when the terminal initializes. + - The legacy `Runtime Mode` enum field remains as a fallback for existing assets. + - `Editor > Auto-Discover Parsers` still toggles automatic parser discovery when the `Editor` flag is active. - Programmatic checks (no allocations): - `TerminalRuntimeConfig.HasFlagNoAlloc(value, flag)` bit-tests without boxing. diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index 5b279f0..7e8c24c 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -357,7 +357,7 @@ public static void CommandClearHistory(CommandArg[] args) if (buffer != null) { buffer.DrainPending(); - buffer.RemoveWhere(log => log.type == TerminalLogType.Input); + buffer.Clear(); } } diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs index b3d8eec..9c586d1 100644 --- a/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeConfig.cs @@ -122,5 +122,16 @@ public static int TryAutoDiscoverParsers() } return 0; } + + internal static TerminalRuntimeModeFlags GetModeForTests() + { +#pragma warning disable CS0618 // Type or member is obsolete + if (Instance != null) + { + return Instance._mode; + } + return _fallbackMode; +#pragma warning restore CS0618 // Type or member is obsolete + } } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 868c781..138d9b6 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -34,6 +34,21 @@ private enum ScrollBarCaptureState TrackerActive = 2, } + [Serializable] + public sealed class RuntimeModeOption + { + [Tooltip("Unique identifier for this runtime mode option.")] + public string id = "default"; + + [Tooltip("Friendly label for inspector tooling.")] + public string displayName = "Default"; + +#pragma warning disable CS0618 // Type or member is obsolete + [Tooltip("Runtime mode flags applied when this option is active.")] + public TerminalRuntimeModeFlags modes = TerminalRuntimeModeFlags.All; +#pragma warning restore CS0618 // Type or member is obsolete + } + // Cache log callback to reduce allocations private static readonly Application.LogCallback UnityLogCallback = HandleUnityLog; @@ -199,9 +214,15 @@ private enum ScrollBarCaptureState #endif [Header("Runtime Mode")] - [Tooltip( - "Controls which environment-specific features are enabled. Choose explicit modes. None is obsolete." - )] + [Tooltip("Available runtime mode options used to configure environment-specific features.")] + [SerializeField] + private List _runtimeModeOptions = new(); + + [Tooltip("Identifier of the runtime mode option applied on startup.")] + [SerializeField] + private string _selectedRuntimeModeId = string.Empty; + + [Tooltip("Legacy fallback for existing data. Prefer runtime mode options.")] [SerializeField] #pragma warning disable CS0618 // Type or member is obsolete private TerminalRuntimeModeFlags _runtimeModes = TerminalRuntimeModeFlags.None; @@ -282,13 +303,129 @@ public TerminalUI() #endif } - private void Awake() + private TerminalRuntimeModeFlags ResolveRuntimeModeFlags() + { + TerminalRuntimeModeFlags resolved; + bool resolvedFromOptions = TryResolveRuntimeModeFromOptions(out resolved); + if (resolvedFromOptions) + { + return resolved; + } + +#pragma warning disable CS0618 // Type or member is obsolete + return _runtimeModes; +#pragma warning restore CS0618 // Type or member is obsolete + } + + private bool TryResolveRuntimeModeFromOptions(out TerminalRuntimeModeFlags resolved) { - TerminalRuntimeConfig.SetMode(_runtimeModes); + resolved = TerminalRuntimeModeFlags.None; + if (_runtimeModeOptions == null || _runtimeModeOptions.Count == 0) + { + return false; + } + + int matchIndex = ResolveRuntimeModeIndex(_selectedRuntimeModeId); + if (matchIndex < 0) + { + matchIndex = 0; + } + + RuntimeModeOption option = _runtimeModeOptions[matchIndex]; + if (option == null) + { + return false; + } + + resolved = option.modes; + if (!string.IsNullOrWhiteSpace(option.id)) + { + _selectedRuntimeModeId = option.id; + } + + return true; + } + + private int ResolveRuntimeModeIndex(string key) + { + if (string.IsNullOrWhiteSpace(key) || _runtimeModeOptions == null) + { + return -1; + } + + for (int i = 0; i < _runtimeModeOptions.Count; ++i) + { + RuntimeModeOption option = _runtimeModeOptions[i]; + if (option == null || string.IsNullOrWhiteSpace(option.id)) + { + continue; + } + + if (string.Equals(option.id, key, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return -1; + } + + internal bool TryApplyRuntimeMode(string runtimeModeId) + { + if (_runtimeModeOptions == null || _runtimeModeOptions.Count == 0) + { + return false; + } + + int index = ResolveRuntimeModeIndex(runtimeModeId); + if (index < 0) + { + return false; + } + + RuntimeModeOption option = _runtimeModeOptions[index]; + if (option == null) + { + return false; + } + + _selectedRuntimeModeId = option.id; + ApplyRuntimeMode(option.modes); + return true; + } + + internal void SetRuntimeModeOptions( + IEnumerable options, + string selectedId + ) + { + if (options == null) + { + _runtimeModeOptions = new List(); + } + else + { + _runtimeModeOptions = new List(options); + } + + _selectedRuntimeModeId = string.IsNullOrWhiteSpace(selectedId) + ? string.Empty + : selectedId; + } + + private void ApplyRuntimeMode(TerminalRuntimeModeFlags modes) + { + TerminalRuntimeConfig.SetMode(modes); #if UNITY_EDITOR TerminalRuntimeConfig.EditorAutoDiscover = _autoDiscoverParsersInEditor; #endif TerminalRuntimeConfig.TryAutoDiscoverParsers(); + } + + private void Awake() + { + TerminalRuntimeModeFlags resolvedRuntimeModes = ResolveRuntimeModeFlags(); + ApplyRuntimeMode(resolvedRuntimeModes); switch (_logBufferSize) { case <= 0: diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index 393c966..7879b1d 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -113,7 +113,7 @@ public IEnumerator ClearConsoleCommandClearsBuffer() } [UnityTest] - public IEnumerator ClearHistoryCommandPurgesInputLogs() + public IEnumerator ClearHistoryCommandPurgesAllLogs() { yield return RestartTerminal(); @@ -122,15 +122,17 @@ public IEnumerator ClearHistoryCommandPurgesInputLogs() buffer.Clear(); Terminal.Log(TerminalLogType.Input, "alpha"); + Terminal.Log(TerminalLogType.Message, "visible"); buffer.DrainPending(); Assert.IsTrue(buffer.Logs.Any(log => log.type == TerminalLogType.Input)); + Assert.IsTrue(buffer.Logs.Any(log => log.type == TerminalLogType.Message)); CommandShell shell = Terminal.Shell; Assert.IsNotNull(shell); Assert.IsTrue(shell.RunCommand("clear-history")); buffer.DrainPending(); - Assert.IsFalse(buffer.Logs.Any(log => log.type == TerminalLogType.Input)); + Assert.AreEqual(0, buffer.Logs.Count); yield break; } @@ -216,7 +218,9 @@ public IEnumerator TraceCommandProducesStackTraceWhenAvailable() Assert.IsTrue(shell.RunCommand("trace")); Terminal.Buffer?.DrainPending(); - LogItem warningLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + LogItem warningLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Warning + ); StringAssert.Contains("Nothing to trace", warningLog.message); Terminal.Log(TerminalLogType.Message, "trace-target"); @@ -243,12 +247,16 @@ public IEnumerator VariableCommandsManageLifecycle() Assert.IsTrue(shell.RunCommand("get-variable foo")); Terminal.Buffer?.DrainPending(); - LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.ShellMessage); + LogItem getLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.ShellMessage + ); StringAssert.Contains("bar baz", getLog.message); Assert.IsTrue(shell.RunCommand("list-variables")); Terminal.Buffer?.DrainPending(); - LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.ShellMessage); + LogItem listLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.ShellMessage + ); StringAssert.Contains("foo", listLog.message); Assert.IsTrue(shell.RunCommand("clear-variable foo")); @@ -263,7 +271,9 @@ public IEnumerator VariableCommandsManageLifecycle() Assert.IsTrue(shell.RunCommand("list-variables")); Terminal.Buffer?.DrainPending(); - LogItem emptyLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + LogItem emptyLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Warning + ); StringAssert.Contains("No variables found", emptyLog.message); yield break; } @@ -277,23 +287,31 @@ public IEnumerator ThemeCommandsOperateOnAvailableTheme() Assert.IsTrue(shell.RunCommand("list-themes")); Terminal.Buffer?.DrainPending(); - LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem listLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Message + ); string expectedThemeName = ThemeNameHelper.GetFriendlyThemeName("test-theme"); StringAssert.Contains(expectedThemeName, listLog.message); Assert.IsTrue(shell.RunCommand("get-theme")); Terminal.Buffer?.DrainPending(); - LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem getLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Message + ); StringAssert.Contains("Current terminal theme", getLog.message); Assert.IsTrue(shell.RunCommand("set-theme test-theme")); Terminal.Buffer?.DrainPending(); - LogItem setLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem setLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Message + ); StringAssert.Contains("test-theme", setLog.message); Assert.IsTrue(shell.RunCommand("set-random-theme")); Terminal.Buffer?.DrainPending(); - LogItem randomLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem randomLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Message + ); StringAssert.Contains("set theme", randomLog.message); yield break; } @@ -307,22 +325,30 @@ public IEnumerator FontCommandsHandleMissingFonts() Assert.IsTrue(shell.RunCommand("list-fonts")); Terminal.Buffer?.DrainPending(); - LogItem listLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem listLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Message + ); Assert.IsNotNull(listLog); Assert.IsTrue(shell.RunCommand("get-font")); Terminal.Buffer?.DrainPending(); - LogItem getLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Message); + LogItem getLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Message + ); StringAssert.Contains("null", getLog.message); Assert.IsTrue(shell.RunCommand("set-font missing-font")); Terminal.Buffer?.DrainPending(); - LogItem warningLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + LogItem warningLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Warning + ); StringAssert.Contains("not found", warningLog.message); Assert.IsTrue(shell.RunCommand("set-random-font")); Terminal.Buffer?.DrainPending(); - LogItem randomLog = Terminal.Buffer.Logs.Last(item => item.type == TerminalLogType.Warning); + LogItem randomLog = Terminal.Buffer.Logs.Last(item => + item.type == TerminalLogType.Warning + ); StringAssert.Contains("No fonts available", randomLog.message); yield break; } @@ -337,11 +363,10 @@ public IEnumerator HelpCommandProvidesCommandDetails() int initialCount = Terminal.Buffer.Logs.Count; Assert.IsTrue(shell.RunCommand("help")); Terminal.Buffer?.DrainPending(); - bool hasUsage = Terminal.Buffer.Logs - .Skip(initialCount) + bool hasUsage = Terminal + .Buffer.Logs.Skip(initialCount) .Any(item => - item.type == TerminalLogType.ShellMessage - && item.message.Contains("Usage:") + item.type == TerminalLogType.ShellMessage && item.message.Contains("Usage:") ); Assert.IsTrue(hasUsage); diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index 7df712d..2b8493f 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -1,5 +1,6 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime { + using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -10,6 +11,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime using UnityEngine; using UnityEngine.TestTools; using UnityEngine.UIElements; + using Object = UnityEngine.Object; public sealed class TerminalTests { @@ -103,7 +105,10 @@ public IEnumerator CleanConstruction() Assert.IsNotNull(Terminal.AutoComplete); } - internal static IEnumerator SpawnTerminal(bool resetStateOnInit) + internal static IEnumerator SpawnTerminal( + bool resetStateOnInit, + Action configure = null + ) { GameObject go = new("Terminal"); go.SetActive(false); @@ -125,6 +130,11 @@ internal static IEnumerator SpawnTerminal(bool resetStateOnInit) terminal.InjectPacks(themePack, fontPack); terminal.resetStateOnInit = resetStateOnInit; + if (configure != null) + { + configure(terminal); + } + go.SetActive(true); yield return new WaitUntil(() => startTracker.Started); // Ensure the buffer is large enough for concurrency tests @@ -162,6 +172,111 @@ public IEnumerator CleanRestartHelperWorks() Assert.AreNotSame(auto1, Terminal.AutoComplete); } + [UnityTest] + public IEnumerator RuntimeModeOptionsApplySelectedConfiguration() + { + yield return TestSceneHelpers.DestroyTerminalAndWait(); + + TerminalRuntimeConfig.SetMode(TerminalRuntimeModeFlags.None); + + TerminalUI.RuntimeModeOption[] options = new TerminalUI.RuntimeModeOption[] + { + new TerminalUI.RuntimeModeOption + { + id = "editor", + displayName = "Editor Only", + modes = TerminalRuntimeModeFlags.Editor, + }, + new TerminalUI.RuntimeModeOption + { + id = "production", + displayName = "Production Only", + modes = TerminalRuntimeModeFlags.Production, + }, + }; + + yield return SpawnTerminal( + resetStateOnInit: true, + configure: terminal => terminal.SetRuntimeModeOptions(options, "production") + ); + + TerminalRuntimeModeFlags configuredMode = TerminalRuntimeConfig.GetModeForTests(); + Assert.AreEqual(TerminalRuntimeModeFlags.Production, configuredMode); + } + + [UnityTest] + public IEnumerator RuntimeModeOptionsFallbackToFirstWhenSelectionMissing() + { + yield return TestSceneHelpers.DestroyTerminalAndWait(); + + TerminalRuntimeConfig.SetMode(TerminalRuntimeModeFlags.None); + + TerminalUI.RuntimeModeOption[] options = new TerminalUI.RuntimeModeOption[] + { + new TerminalUI.RuntimeModeOption + { + id = "development", + displayName = "Development", + modes = TerminalRuntimeModeFlags.Development, + }, + new TerminalUI.RuntimeModeOption + { + id = "production", + displayName = "Production", + modes = TerminalRuntimeModeFlags.Production, + }, + }; + + yield return SpawnTerminal( + resetStateOnInit: true, + configure: terminal => terminal.SetRuntimeModeOptions(options, "missing") + ); + + TerminalRuntimeModeFlags configuredMode = TerminalRuntimeConfig.GetModeForTests(); + Assert.AreEqual(TerminalRuntimeModeFlags.Development, configuredMode); + } + + [UnityTest] + public IEnumerator TryApplyRuntimeModeChangesActiveSelection() + { + yield return TestSceneHelpers.DestroyTerminalAndWait(); + + TerminalRuntimeConfig.SetMode(TerminalRuntimeModeFlags.None); + + TerminalUI.RuntimeModeOption[] options = new TerminalUI.RuntimeModeOption[] + { + new TerminalUI.RuntimeModeOption + { + id = "editor", + displayName = "Editor", + modes = TerminalRuntimeModeFlags.Editor, + }, + new TerminalUI.RuntimeModeOption + { + id = "production", + displayName = "Production", + modes = TerminalRuntimeModeFlags.Production, + }, + }; + + yield return SpawnTerminal( + resetStateOnInit: true, + configure: terminal => terminal.SetRuntimeModeOptions(options, "editor") + ); + + TerminalRuntimeModeFlags initialMode = TerminalRuntimeConfig.GetModeForTests(); + Assert.AreEqual(TerminalRuntimeModeFlags.Editor, initialMode); + + TerminalUI terminalInstance = TerminalUI.Instance; + Assert.IsNotNull(terminalInstance); + + bool switched = terminalInstance.TryApplyRuntimeMode("production"); + Assert.IsTrue(switched); + + TerminalRuntimeModeFlags updatedMode = TerminalRuntimeConfig.GetModeForTests(); + Assert.AreEqual(TerminalRuntimeModeFlags.Production, updatedMode); + } + // Test-only pack types moved to Components/TestPacks.cs } } From 3d9e39026a39b15322e008cc5069a732e1bdff50 Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 08:57:09 -0700 Subject: [PATCH 27/69] Progress --- PLAN.md | 88 +++++++ PLAN.md.meta | 7 + .../Backend/ITerminalRuntime.cs | 21 ++ .../Backend/ITerminalRuntime.cs.meta | 11 + Runtime/CommandTerminal/Backend/Profiles.meta | 8 + .../Profiles/TerminalRuntimeProfile.cs | 84 +++++++ .../Profiles/TerminalRuntimeProfile.cs.meta | 11 + Runtime/CommandTerminal/Backend/Terminal.cs | 38 ++- .../Backend/TerminalRuntime.cs | 232 ++++++++++++++++++ .../Backend/TerminalRuntime.cs.meta | 11 + .../Backend/TerminalRuntimeCache.cs | 29 +++ .../Backend/TerminalRuntimeCache.cs.meta | 3 + .../Backend/TerminalRuntimeSettings.cs | 32 +++ .../Backend/TerminalRuntimeSettings.cs.meta | 11 + .../Backend/TerminalRuntimeUpdateResult.cs | 32 +++ .../TerminalRuntimeUpdateResult.cs.meta | 11 + Runtime/CommandTerminal/UI/TerminalUI.cs | 209 +++++++++++----- Tests/Runtime/BuiltinCommandTests.cs | 107 +++++--- Tests/Runtime/Components/TestSceneHelpers.cs | 6 +- Tests/Runtime/LauncherModeTests.cs | 101 ++++---- Tests/Runtime/TerminalRuntimeTests.cs | 130 ++++++++++ Tests/Runtime/TerminalRuntimeTests.cs.meta | 11 + Tests/Runtime/TerminalTests.cs | 57 ++++- 23 files changed, 1083 insertions(+), 167 deletions(-) create mode 100644 PLAN.md create mode 100644 PLAN.md.meta create mode 100644 Runtime/CommandTerminal/Backend/ITerminalRuntime.cs create mode 100644 Runtime/CommandTerminal/Backend/ITerminalRuntime.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/Profiles.meta create mode 100644 Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs create mode 100644 Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntime.cs create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntime.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs create mode 100644 Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs.meta create mode 100644 Tests/Runtime/TerminalRuntimeTests.cs create mode 100644 Tests/Runtime/TerminalRuntimeTests.cs.meta diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..1640f20 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,88 @@ +# DXCommandTerminal Modernization Plan + +## Objectives +- Eliminate ambient static state and move toward instance-based, SOLID-compliant architecture without sacrificing discoverability or UX ergonomics. +- Achieve zero-allocation hot paths for terminal rendering, history traversal, and input handling while preserving runtime performance. +- Improve maintainability by decomposing the 3.6k line `TerminalUI` monolith and enforcing clearer module boundaries between runtime model, view, persistence, and tooling. +- Tighten correctness through better validation, deterministic behaviour, and comprehensive automated testing (runtime + UI smoke). +- Preserve or improve usability by keeping configuration approachable (consider ScriptableObject assets, inspectors, and presets) and providing guard rails for integrators and designers. + +## Prioritized Initiatives + +### P0 — Runtime Instance Core & Dependency Inversion +- Replace `WallstopStudios.DxCommandTerminal.Backend.Terminal` static with an injectable `TerminalRuntime` aggregate (shell, history, buffer, autocomplete) created per-terminal instance (`Runtime/CommandTerminal/Backend/Terminal.cs`). +- Introduce an interface (`ITerminalRuntimeAccessor`) that `TerminalUI` and input controllers can depend on; provide a ScriptableObject-derived factory (`TerminalRuntimeProfile`) with serialized capacities, ignored log types, default command packs, etc. to maintain discoverability. +- Provide a migration shim that keeps `Terminal` static available as a thin proxy during transition but mark it `[Obsolete]`, delegating to the active runtime and throwing if none are registered (breaking change acceptable now). +- Benefits: Single Responsibility (runtime logic disentangled from UI), Open/Closed (future runtimes), Dependency Inversion (consumers target abstraction), easier testing/mocking. Enables multi-terminal scenes and editor preview workflows. + +### P0 — TerminalUI Decomposition & Presenter Layer +- Split `Runtime/CommandTerminal/UI/TerminalUI.cs` (~3.6k LOC) into focused components: + - `TerminalUIPresenter` (MonoBehaviour) orchestrating runtime ↔ viewmodel sync and command dispatch. + - `TerminalUIView`/`LogView`, `HistoryView`, `InputView`, `LauncherView` for UIToolkit manipulation (each under 300 LOC) using pure view logic. + - `TerminalThemeController` handling font/theme switching and coordinating with persistence. + - `TerminalAnimationController` dedicated to height easing, scroll positioning, fade logic. +- Apply Interface Segregation: each controller exposes minimal update contracts (`ILogView.UpdateLog(LogSlice slice)` etc.). Use composition over inheritance to keep testability high. +- Break editor-only tooling into partial classes or dedicated editor scripts (`Editor/`) to remove `#if UNITY_EDITOR` clutter from runtime components. +- Benefits: maintainability, easier reasoning, smaller diff surface, simpler UI testing. + +### P0 — Zero-Allocation Hot Path Audit +- Profile `LateUpdate` (`Runtime/CommandTerminal/UI/TerminalUI.cs:586`), history fade (`ApplyHistoryFade`), autocomplete refresh, and log drain to identify per-frame allocations; replace `new` operations with reusable buffers (`NativeList`, pooled `List`, struct enumerators). +- Introduce `struct`-based lightweight view models (`LogSlice`, `CompletionBufferView`) that carry spans/indices into pooled storage inside `TerminalRuntime` to avoid copying strings each frame. +- Centralize pooling via `DxArrayPool` (already exists) or custom `ITerminalBufferPool`; ensure `CommandLog.DrainPending` and `CommandShell` reuse `StringBuilder` without ToString allocations on hot paths. +- Add allocation regression tests using Unity's `GC.AllocRecorder` in playmode tests that toggle terminal while issuing commands. + +### P0 — Validation & State Management Contracts +- Formalize state transitions in a dedicated `TerminalStateMachine` with explicit events (`OpenFull`, `Close`, `ToggleLauncher`). `TerminalKeyboardController` then depends on that contract rather than manipulating `TerminalUI` internals. +- Replace ad-hoc boolean flags (`_needsScrollToEnd`, `_commandIssuedThisFrame`) with explicit commands/events queued into the state machine; process deterministically during update. +- Provide defensive checks and central error logging (reduce `Debug.LogError` scatter) to improve correctness and diagnosability. + +### P1 — Configurability via ScriptableObjects & Presets +- Create `TerminalAppearanceProfile`, `TerminalInputProfile`, `TerminalCommandProfile` ScriptableObjects living under `Resources/Wallstop Studios/DxCommandTerminal/` to encapsulate current serialized fields (hotkeys, history fade, button labels, etc.). +- Allow multiple profiles per project and expose assignment in inspector with sensible defaults. `TerminalLauncherSettings` becomes a serializable asset reused across scenes. +- Move persisted theme/font selection into a `TerminalThemePersistenceProfile` (ScriptableObject + runtime adapter) to trim IO concerns from `TerminalThemePersister` MonoBehaviour; supports injection/mocking in tests. + +### P1 — UI Rendering & Virtualization Improvements +- Swap manual `VisualElement` management with UIToolkit `ListView` virtualization for history/log to avoid re-creating labels each refresh; ensure zero allocation by providing custom `MakeItem`/`BindItem` that reuse pooled entries. +- Extract USS selectors into modular style sheets under `Styles/` to reduce runtime code toggling class lists; `LogView` can simply set classes based on pre-defined style variants. +- Provide layout data caches and lightweight diffing to avoid clearing/rebuilding containers when nothing changed (`ListsEqual` currently compares single lists but still calls `Clear`/`Add`). + +### P1 — Input System Stratification +- Introduce an `ITerminalInputSource` abstraction handing parsed commands / navigation intents; implement `LegacyInputSource`, `NewInputSystemSource`, and `EditorShortcutSource`. `TerminalKeyboardController` becomes an adapter composed with an input source chosen via profile. +- Normalize hotkey parsing via dedicated service that pre-resolves key codes at initialization (avoid dictionary lookups each frame) and supports rebinding UI. +- Provide guard rails for conflicting hotkeys (validate at profile load rather than runtime `Debug.LogError`). + +### P1 — Persistence & Extensibility +- Redesign persistence to use async-less, job-friendly APIs (no `Task` inside coroutines). Provide `ITerminalPersistenceProvider` interface; default implementation writes JSON via `Unity.Collections.LowLevel.Unsafe.UnsafeUtility` safe wrappers when possible. +- Support scene-level overrides (ScriptableObject) and user-level persistence channels to help multi-terminal scenarios. + +### P2 — Observability & Tooling +- Add structured diagnostics (allocation counters, command execution timing) behind development flag to help maintain zero allocation guarantee. +- Provide editor window to inspect active terminal runtimes, registered commands, pending logs (replaces reliance on static global state for debugging). +- Publish developer documentation updates (README + API docs) reflecting new architecture and usage patterns. + +## Test Coverage Gaps & Strategy +- **Runtime composition:** Add playmode tests covering multiple terminals instantiated simultaneously with distinct profiles to ensure isolation (new `TerminalRuntime` works). +- **State machine:** Unit tests for `TerminalStateMachine` verifying transitions, animations triggers, and zero allocation command queue behaviour. +- **UI binding:** Introduce UIToolkit integration tests (edit mode with `UIElementsTestUtilities`) validating virtualization binds, theme switching, and launcher metrics (currently untested). +- **Persistence:** Mocked persistence provider tests verifying hydration/save cycles without touching disk; existing `TerminalThemePersister` path can be retired. +- **Input:** Tests per input profile ensuring hotkey translation and conflict detection (no coverage right now for `InputHelpers`). +- **Performance:** Automated allocation guard using `GC.AllocRecorder` around command spam + log scrolling scenario; fail test if allocations exceed threshold. + +## Implementation Notes & Sequencing +1. Land `TerminalRuntime` core + proxy static API (P0). Update tests to inject runtime explicitly. +2. Extract presenter/view/controller slices from `TerminalUI`, wiring new runtime (P0). Ensure incremental commits keep behaviour parity. +3. Integrate zero-allocation audit outcomes (P0); add instrumentation and tests. +4. Move configuration/persistence into ScriptableObject profiles (P1) and update inspector tooling. +5. Roll out input abstraction and UI virtualization (P1), followed by persistence improvements (P1). +6. Add observability/tooling features and documentation (P2). + +## Risks & Mitigations +- **Backwards compatibility break:** Provide migration guide and temporary proxy static for legacy API. Communicate via `CHANGELOG.md`. +- **Test fragility:** Introduce helper factories for runtimes in tests to keep fixtures concise. +- **Performance regressions:** Run allocation/performance tests per PR; add editor validation to flag accidental LINQ usage (see `linq_hits.txt`). + +## Deliverables +- Updated runtime architecture diagrams and README sections describing new profiles and runtime injection. +- Comprehensive `TerminalUI` refactor with modular components, zero-regression tests, and allocation guardrails. +- ScriptableObject-based configuration assets and editor tooling for easier customization. + diff --git a/PLAN.md.meta b/PLAN.md.meta new file mode 100644 index 0000000..09aa529 --- /dev/null +++ b/PLAN.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b215f2943aa6f434ea64ce6d734e5d64 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/ITerminalRuntime.cs b/Runtime/CommandTerminal/Backend/ITerminalRuntime.cs new file mode 100644 index 0000000..ddeed83 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/ITerminalRuntime.cs @@ -0,0 +1,21 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + /// + /// Represents an isolated runtime backing a terminal instance. Provides access to the + /// command buffer, history, shell, and autocomplete services without relying on static state. + /// + internal interface ITerminalRuntime + { + CommandLog Log { get; } + + CommandHistory History { get; } + + CommandShell Shell { get; } + + CommandAutoComplete AutoComplete { get; } + + TerminalRuntimeUpdateResult Configure(in TerminalRuntimeSettings settings, bool forceReset); + + bool LogMessage(TerminalLogType type, string format, params object[] parameters); + } +} diff --git a/Runtime/CommandTerminal/Backend/ITerminalRuntime.cs.meta b/Runtime/CommandTerminal/Backend/ITerminalRuntime.cs.meta new file mode 100644 index 0000000..f06a227 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/ITerminalRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: feee890667bfd7affb0cae0515944dd0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Profiles.meta b/Runtime/CommandTerminal/Backend/Profiles.meta new file mode 100644 index 0000000..b060882 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Profiles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4fd26bd667daf3205a56f14f306d1666 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs b/Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs new file mode 100644 index 0000000..354ed20 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs @@ -0,0 +1,84 @@ +namespace WallstopStudios.DxCommandTerminal.Backend.Profiles +{ + using System.Collections.Generic; + using UnityEngine; + + [CreateAssetMenu( + fileName = "TerminalRuntimeProfile", + menuName = "DXCommandTerminal/Terminal Runtime Profile", + order = 450 + )] + public sealed class TerminalRuntimeProfile : ScriptableObject + { + [SerializeField] + [Min(0)] + private int _logBufferSize = 256; + + [SerializeField] + [Min(0)] + private int _historyBufferSize = 512; + + [SerializeField] + private bool _ignoreDefaultCommands; + + [SerializeField] + private List _ignoredLogTypes = new(); + + [SerializeField] + private List _disabledCommands = new(); + + public int LogBufferSize => Mathf.Max(0, _logBufferSize); + + public int HistoryBufferSize => Mathf.Max(0, _historyBufferSize); + + public bool IgnoreDefaultCommands => _ignoreDefaultCommands; + + public IReadOnlyList IgnoredLogTypes => _ignoredLogTypes; + + public IReadOnlyList DisabledCommands => _disabledCommands; + + internal TerminalRuntimeSettings BuildSettings() + { + return new TerminalRuntimeSettings( + LogBufferSize, + HistoryBufferSize, + _ignoredLogTypes, + _disabledCommands, + _ignoreDefaultCommands + ); + } + +#if UNITY_EDITOR + internal void ConfigureForTests( + int logBufferSize, + int historyBufferSize, + bool ignoreDefaults, + IReadOnlyList ignoredLogTypes, + IReadOnlyList disabledCommands + ) + { + _logBufferSize = logBufferSize; + _historyBufferSize = historyBufferSize; + _ignoreDefaultCommands = ignoreDefaults; + + _ignoredLogTypes.Clear(); + if (ignoredLogTypes != null) + { + for (int i = 0; i < ignoredLogTypes.Count; ++i) + { + _ignoredLogTypes.Add(ignoredLogTypes[i]); + } + } + + _disabledCommands.Clear(); + if (disabledCommands != null) + { + for (int i = 0; i < disabledCommands.Count; ++i) + { + _disabledCommands.Add(disabledCommands[i]); + } + } + } +#endif + } +} diff --git a/Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs.meta b/Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs.meta new file mode 100644 index 0000000..effd482 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/Profiles/TerminalRuntimeProfile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6be846ab190dfc54081a4fa4397bd422 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/Terminal.cs b/Runtime/CommandTerminal/Backend/Terminal.cs index a6b132d..3baae7f 100644 --- a/Runtime/CommandTerminal/Backend/Terminal.cs +++ b/Runtime/CommandTerminal/Backend/Terminal.cs @@ -4,10 +4,30 @@ namespace WallstopStudios.DxCommandTerminal.Backend public static class Terminal { - public static CommandLog Buffer { get; internal set; } - public static CommandShell Shell { get; internal set; } - public static CommandHistory History { get; internal set; } - public static CommandAutoComplete AutoComplete { get; internal set; } + private static ITerminalRuntime _activeRuntime; + + public static CommandLog Buffer => _activeRuntime?.Log; + + public static CommandShell Shell => _activeRuntime?.Shell; + + public static CommandHistory History => _activeRuntime?.History; + + public static CommandAutoComplete AutoComplete => _activeRuntime?.AutoComplete; + + internal static ITerminalRuntime ActiveRuntime => _activeRuntime; + + internal static void RegisterRuntime(ITerminalRuntime runtime) + { + _activeRuntime = runtime; + } + + internal static void UnregisterRuntime(ITerminalRuntime runtime) + { + if (_activeRuntime == runtime) + { + _activeRuntime = null; + } + } [StringFormatMethod("format")] public static bool Log(string format, params object[] parameters) @@ -18,17 +38,13 @@ public static bool Log(string format, params object[] parameters) [StringFormatMethod("format")] public static bool Log(TerminalLogType type, string format, params object[] parameters) { - CommandLog buffer = Buffer; - if (buffer == null) + ITerminalRuntime runtime = _activeRuntime; + if (runtime == null) { return false; } - string formattedMessage = parameters is { Length: > 0 } - ? string.Format(format, parameters) - : format; - buffer.EnqueueMessage(formattedMessage, type, includeStackTrace: true); - return true; + return runtime.LogMessage(type, format, parameters); } } } diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntime.cs b/Runtime/CommandTerminal/Backend/TerminalRuntime.cs new file mode 100644 index 0000000..e25f5bc --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntime.cs @@ -0,0 +1,232 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System; + using System.Collections.Generic; + + internal sealed class TerminalRuntime : ITerminalRuntime + { + private readonly HashSet _ignoredLogTypesScratch = new(); + private readonly HashSet _ignoredCommandScratch = new( + StringComparer.OrdinalIgnoreCase + ); + + private CommandLog _log; + private CommandHistory _history; + private CommandShell _shell; + private CommandAutoComplete _autoComplete; + + private bool _appliedIgnoreDefaultCommands; + + public CommandLog Log => _log; + + public CommandHistory History => _history; + + public CommandShell Shell => _shell; + + public CommandAutoComplete AutoComplete => _autoComplete; + + public TerminalRuntimeUpdateResult Configure( + in TerminalRuntimeSettings settings, + bool forceReset + ) + { + bool logRecreated = EnsureLog(settings, forceReset); + bool historyRecreated = EnsureHistory(settings, forceReset); + bool shellRecreated = EnsureShell(settings, forceReset, historyRecreated); + bool autoCompleteRecreated = EnsureAutoComplete( + forceReset || historyRecreated || shellRecreated + ); + bool commandsRefreshed = EnsureShellConfiguration( + settings, + forceReset, + shellRecreated + ); + + return new TerminalRuntimeUpdateResult( + logRecreated, + historyRecreated, + shellRecreated, + autoCompleteRecreated, + commandsRefreshed + ); + } + + public bool LogMessage(TerminalLogType type, string format, params object[] parameters) + { + CommandLog log = _log; + if (log == null || string.IsNullOrEmpty(format)) + { + return false; + } + + string formattedMessage = parameters is { Length: > 0 } + ? string.Format(format, parameters) + : format; + log.EnqueueMessage(formattedMessage, type, includeStackTrace: true); + return true; + } + + private bool EnsureLog(in TerminalRuntimeSettings settings, bool forceReset) + { + int desiredCapacity = Math.Max(0, settings.LogCapacity); + if (forceReset || _log == null) + { + _log = new CommandLog(desiredCapacity, settings.IgnoredLogTypes); + ApplyIgnoredLogTypes(settings.IgnoredLogTypes); + return true; + } + + if (_log.Capacity != desiredCapacity) + { + _log.Resize(desiredCapacity); + } + + ApplyIgnoredLogTypes(settings.IgnoredLogTypes); + return false; + } + + private bool EnsureHistory(in TerminalRuntimeSettings settings, bool forceReset) + { + int desiredCapacity = Math.Max(0, settings.HistoryCapacity); + if (forceReset || _history == null) + { + _history = new CommandHistory(desiredCapacity); + return true; + } + + if (_history.Capacity != desiredCapacity) + { + _history.Resize(desiredCapacity); + } + + return false; + } + + private bool EnsureShell( + in TerminalRuntimeSettings settings, + bool forceReset, + bool historyRecreated + ) + { + if (forceReset || _shell == null || historyRecreated) + { + _shell = new CommandShell(_history); + return true; + } + + return false; + } + + private bool EnsureAutoComplete(bool recreate) + { + if (recreate || _autoComplete == null) + { + _autoComplete = new CommandAutoComplete(_history, _shell, _shell.Commands.Keys); + return true; + } + + return false; + } + + private bool EnsureShellConfiguration( + in TerminalRuntimeSettings settings, + bool forceReset, + bool shellRecreated + ) + { + if (_shell == null) + { + return false; + } + + bool shouldRefreshCommands = shellRecreated; + if (!shouldRefreshCommands) + { + if (_shell.Commands.Count <= 0) + { + shouldRefreshCommands = true; + } + else + { + bool ignoreFlagChanged = _appliedIgnoreDefaultCommands + != settings.IgnoreDefaultCommands; + if (ignoreFlagChanged) + { + shouldRefreshCommands = true; + } + else + { + _ignoredCommandScratch.Clear(); + for (int i = 0; i < settings.DisabledCommands.Count; ++i) + { + string command = settings.DisabledCommands[i]; + if (!string.IsNullOrWhiteSpace(command)) + { + _ignoredCommandScratch.Add(command); + } + } + + if (!_shell.IgnoredCommands.SetEquals(_ignoredCommandScratch)) + { + shouldRefreshCommands = true; + } + } + } + } + + if (forceReset) + { + shouldRefreshCommands = true; + } + + if (!shouldRefreshCommands) + { + return false; + } + + _shell.ClearAutoRegisteredCommands(); + _ignoredCommandScratch.Clear(); + for (int i = 0; i < settings.DisabledCommands.Count; ++i) + { + string command = settings.DisabledCommands[i]; + if (!string.IsNullOrWhiteSpace(command)) + { + _ignoredCommandScratch.Add(command); + } + } + + _shell.InitializeAutoRegisteredCommands( + _ignoredCommandScratch, + ignoreDefaultCommands: settings.IgnoreDefaultCommands + ); + + _appliedIgnoreDefaultCommands = settings.IgnoreDefaultCommands; + return true; + } + + private void ApplyIgnoredLogTypes(IReadOnlyList ignoredLogTypes) + { + if (_log == null) + { + return; + } + + _ignoredLogTypesScratch.Clear(); + if (ignoredLogTypes != null) + { + for (int i = 0; i < ignoredLogTypes.Count; ++i) + { + _ignoredLogTypesScratch.Add(ignoredLogTypes[i]); + } + } + + if (_log.ignoredLogTypes.SetEquals(_ignoredLogTypesScratch)) + { + return; + } + + _log.ignoredLogTypes.Clear(); + _log.ignoredLogTypes.UnionWith(_ignoredLogTypesScratch); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntime.cs.meta b/Runtime/CommandTerminal/Backend/TerminalRuntime.cs.meta new file mode 100644 index 0000000..cc348d1 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1eb6af7b9f0516acc9539d41a5074ca8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs b/Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs new file mode 100644 index 0000000..fc9f053 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs @@ -0,0 +1,29 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + internal static class TerminalRuntimeCache + { + private static TerminalRuntime _cachedRuntime; + + public static bool TryAcquire(out TerminalRuntime runtime) + { + runtime = _cachedRuntime; + _cachedRuntime = null; + return runtime != null; + } + + public static void Store(TerminalRuntime runtime) + { + if (runtime == null) + { + return; + } + + _cachedRuntime = runtime; + } + + public static void Clear() + { + _cachedRuntime = null; + } + } +} diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs.meta b/Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs.meta new file mode 100644 index 0000000..664669c --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeCache.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 682d23c38b534ce6a8d5c9a5a71c3892 +timeCreated: 0 diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs b/Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs new file mode 100644 index 0000000..8105e86 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs @@ -0,0 +1,32 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System.Collections.Generic; + + internal readonly struct TerminalRuntimeSettings + { + public TerminalRuntimeSettings( + int logCapacity, + int historyCapacity, + IReadOnlyList ignoredLogTypes, + IReadOnlyList disabledCommands, + bool ignoreDefaultCommands + ) + { + LogCapacity = logCapacity; + HistoryCapacity = historyCapacity; + IgnoredLogTypes = ignoredLogTypes ?? System.Array.Empty(); + DisabledCommands = disabledCommands ?? System.Array.Empty(); + IgnoreDefaultCommands = ignoreDefaultCommands; + } + + public int LogCapacity { get; } + + public int HistoryCapacity { get; } + + public IReadOnlyList IgnoredLogTypes { get; } + + public IReadOnlyList DisabledCommands { get; } + + public bool IgnoreDefaultCommands { get; } + } +} diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs.meta b/Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs.meta new file mode 100644 index 0000000..639be18 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6498ca131cde2e1e087c401f3d2e7113 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs b/Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs new file mode 100644 index 0000000..49b3473 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs @@ -0,0 +1,32 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + internal readonly struct TerminalRuntimeUpdateResult + { + public TerminalRuntimeUpdateResult( + bool logRecreated, + bool historyRecreated, + bool shellRecreated, + bool autoCompleteRecreated, + bool commandsRefreshed + ) + { + LogRecreated = logRecreated; + HistoryRecreated = historyRecreated; + ShellRecreated = shellRecreated; + AutoCompleteRecreated = autoCompleteRecreated; + CommandsRefreshed = commandsRefreshed; + } + + public bool LogRecreated { get; } + + public bool HistoryRecreated { get; } + + public bool ShellRecreated { get; } + + public bool AutoCompleteRecreated { get; } + + public bool CommandsRefreshed { get; } + + public bool RuntimeReset => LogRecreated || HistoryRecreated || ShellRecreated || AutoCompleteRecreated; + } +} diff --git a/Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs.meta b/Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs.meta new file mode 100644 index 0000000..393fe80 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalRuntimeUpdateResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02b74b6820582b80bb4aa10e5b47feee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 138d9b6..be539ac 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Editor")] +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests.Runtime")] namespace WallstopStudios.DxCommandTerminal.UI { @@ -9,6 +10,7 @@ namespace WallstopStudios.DxCommandTerminal.UI using System.ComponentModel; using Attributes; using Backend; + using Backend.Profiles; using Extensions; using Helper; using Input; @@ -112,6 +114,10 @@ public sealed class RuntimeModeOption private TerminalLauncherSettings _launcherSettings = new(); [Header("System")] + [SerializeField] + [Tooltip("Optional configuration asset applied on Awake to seed runtime settings.")] + private TerminalRuntimeProfile _runtimeProfile; + [SerializeField] private int _logBufferSize = 256; @@ -190,6 +196,18 @@ public sealed class RuntimeModeOption private IInputHandler[] _inputHandlers; + private TerminalRuntime _runtime; + + internal ITerminalRuntime Runtime => _runtime; + + private CommandLog ActiveLog => _runtime?.Log; + + private CommandShell ActiveShell => _runtime?.Shell; + + private CommandHistory ActiveHistory => _runtime?.History; + + private CommandAutoComplete ActiveAutoComplete => _runtime?.AutoComplete; + #if UNITY_EDITOR private readonly Dictionary _propertyValues = new(); private readonly List _uiProperties = new(); @@ -424,6 +442,8 @@ private void ApplyRuntimeMode(TerminalRuntimeModeFlags modes) private void Awake() { + ApplyRuntimeProfile(); + TerminalRuntimeModeFlags resolvedRuntimeModes = ResolveRuntimeModeFlags(); ApplyRuntimeMode(resolvedRuntimeModes); switch (_logBufferSize) @@ -484,6 +504,7 @@ private void Awake() string[] staticStaticPropertiesTracked = { + nameof(_runtimeProfile), nameof(_logBufferSize), nameof(_historyBufferSize), nameof(_ignoredLogTypes), @@ -542,8 +563,39 @@ void TrackProperties(string[] properties, List storage) #endif } +#if UNITY_EDITOR + private void OnValidate() + { + if (Application.isPlaying) + { + return; + } + + ApplyRuntimeProfile(); + } +#endif + private void OnEnable() { + if (resetStateOnInit) + { + TerminalRuntimeCache.Clear(); + } + + if (_runtime == null) + { + if (!resetStateOnInit && TerminalRuntimeCache.TryAcquire(out TerminalRuntime cachedRuntime)) + { + _runtime = cachedRuntime; + } + else + { + _runtime = new TerminalRuntime(); + } + } + + Terminal.RegisterRuntime(_runtime); + RefreshStaticState(force: resetStateOnInit); ConsumeAndLogErrors(); @@ -577,10 +629,14 @@ private void OnDisable() } SetState(TerminalState.Closed); + + Terminal.UnregisterRuntime(_runtime); } private void OnDestroy() { + TerminalRuntimeCache.Store(_runtime); + if (Instance != this) { return; @@ -608,7 +664,7 @@ private void LateUpdate() { ResetWindowIdempotent(); // Drain any cross-thread logs into the main-thread buffer before refreshing UI - Terminal.Buffer?.DrainPending(); + ActiveLog?.DrainPending(); HandleHeightAnimation(); RefreshUI(); _commandIssuedThisFrame = false; @@ -616,60 +672,68 @@ private void LateUpdate() private void RefreshStaticState(bool force) { - int logBufferSize = Mathf.Max(0, _logBufferSize); - if (force || Terminal.Buffer == null) + if (_runtime == null) { - Terminal.Buffer = new CommandLog(logBufferSize, _ignoredLogTypes); + _runtime = new TerminalRuntime(); } - else + + TerminalRuntimeSettings settings = BuildRuntimeSettings(); + TerminalRuntimeUpdateResult updateResult = _runtime.Configure(settings, force); + + if (_started && (updateResult.CommandsRefreshed || updateResult.RuntimeReset)) { - if (Terminal.Buffer.Capacity != logBufferSize) - { - Terminal.Buffer.Resize(logBufferSize); - } - if (!Terminal.Buffer.ignoredLogTypes.SetEquals(_ignoredLogTypes)) - { - Terminal.Buffer.ignoredLogTypes.Clear(); - Terminal.Buffer.ignoredLogTypes.UnionWith(_ignoredLogTypes); - } + ResetAutoComplete(); } + } + + private TerminalRuntimeSettings BuildRuntimeSettings() + { + int logCapacity = Mathf.Max(0, _logBufferSize); + int historyCapacity = Mathf.Max(0, _historyBufferSize); + return new TerminalRuntimeSettings( + logCapacity, + historyCapacity, + _ignoredLogTypes, + _disabledCommands, + ignoreDefaultCommands + ); + } - int historyBufferSize = Mathf.Max(0, _historyBufferSize); - if (force || Terminal.History == null) + private void ApplyRuntimeProfile() + { + if (_runtimeProfile == null) { - Terminal.History = new CommandHistory(historyBufferSize); + return; } - else if (Terminal.History.Capacity != historyBufferSize) + + _logBufferSize = Mathf.Max(0, _runtimeProfile.LogBufferSize); + _historyBufferSize = Mathf.Max(0, _runtimeProfile.HistoryBufferSize); + ignoreDefaultCommands = _runtimeProfile.IgnoreDefaultCommands; + CopyList(_runtimeProfile.IgnoredLogTypes, _ignoredLogTypes); + CopyList(_runtimeProfile.DisabledCommands, _disabledCommands); + } + + private static void CopyList(IReadOnlyList source, List destination) + { + if (destination == null) { - Terminal.History.Resize(historyBufferSize); + return; } - if (force || Terminal.Shell == null) + if (ReferenceEquals(source, destination)) { - Terminal.Shell = new CommandShell(Terminal.History); + return; } - if (force || Terminal.AutoComplete == null) + destination.Clear(); + if (source == null) { - Terminal.AutoComplete = new CommandAutoComplete(Terminal.History, Terminal.Shell); + return; } - if ( - Terminal.Shell.IgnoringDefaultCommands != ignoreDefaultCommands - || Terminal.Shell.Commands.Count <= 0 - || !Terminal.Shell.IgnoredCommands.SetEquals(_disabledCommands) - ) + for (int i = 0; i < source.Count; ++i) { - Terminal.Shell.ClearAutoRegisteredCommands(); - Terminal.Shell.InitializeAutoRegisteredCommands( - ignoredCommands: _disabledCommands, - ignoreDefaultCommands: ignoreDefaultCommands - ); - - if (_started) - { - ResetAutoComplete(); - } + destination.Add(source[i]); } } @@ -719,6 +783,7 @@ private void CheckForChanges() if (CheckForRefresh(_staticStateProperties)) { + ApplyRuntimeProfile(); RefreshStaticState(force: false); } @@ -856,9 +921,9 @@ private static bool ListsEqual(List a, List b) return true; } - private static void ConsumeAndLogErrors() + private void ConsumeAndLogErrors() { - while (Terminal.Shell?.TryConsumeErrorMessage(out string error) == true) + while (ActiveShell?.TryConsumeErrorMessage(out string error) == true) { Terminal.Log(TerminalLogType.Error, $"Error: {error}"); } @@ -869,6 +934,17 @@ private void ResetAutoComplete() _lastKnownCommandText = _input.CommandText ?? string.Empty; _lastCompletionAnchorText = null; _lastCompletionAnchorCaretIndex = null; + CommandAutoComplete autoComplete = ActiveAutoComplete; + if (autoComplete == null) + { + _lastCompletionIndex = null; + _previousLastCompletionIndex = null; + _lastCompletionBuffer.Clear(); + _lastCompletionBufferTempCache.Clear(); + _lastCompletionBufferTempSet.Clear(); + return; + } + if (hintDisplayMode == HintDisplayMode.Always) { _lastCompletionBufferTempCache.Clear(); @@ -876,7 +952,7 @@ private void ResetAutoComplete() _commandInput != null ? _commandInput.cursorIndex : (_lastKnownCommandText?.Length ?? 0); - Terminal.AutoComplete?.Complete( + autoComplete.Complete( _lastKnownCommandText, caret, _lastCompletionBufferTempCache @@ -1170,7 +1246,7 @@ private void SetupUI() string prev = evt.previousValue ?? string.Empty; string curr = evt.newValue ?? string.Empty; bool justTypedSpace = curr.EndsWith(" ") && curr.Length == prev.Length + 1; - if (justTypedSpace && Terminal.Shell != null) + if (justTypedSpace && context.ActiveShell != null) { string check = curr; // Remove trailing space(s) to isolate the command token @@ -1181,7 +1257,7 @@ private void SetupUI() if (CommandShell.TryEatArgument(ref check, out CommandArg cmd)) { - if (Terminal.Shell.Commands.ContainsKey(cmd.contents)) + if (context.ActiveShell.Commands.ContainsKey(cmd.contents)) { // Clear existing suggestions immediately context._lastCompletionIndex = null; @@ -1768,12 +1844,14 @@ private void FocusInput() private void RefreshLogs() { - IReadOnlyList logs = Terminal.Buffer?.Logs; - if (logs == null) + CommandLog log = ActiveLog; + if (log == null) { return; } + IReadOnlyList logs = log.Logs; + if (_logScrollView == null) { return; @@ -1787,7 +1865,7 @@ private void RefreshLogs() VisualElement content = _logScrollView.contentContainer; _logScrollView.style.display = DisplayStyle.Flex; - bool dirty = _lastSeenBufferVersion != Terminal.Buffer.Version; + bool dirty = _lastSeenBufferVersion != log.Version; if (content.childCount != logs.Count) { dirty = true; @@ -1845,7 +1923,7 @@ private void RefreshLogs() if (logs.Count == content.childCount) { - _lastSeenBufferVersion = Terminal.Buffer.Version; + _lastSeenBufferVersion = log.Version; } } @@ -1867,7 +1945,7 @@ private void RefreshLauncherHistory() } VisualElement content = _logScrollView.contentContainer; - CommandHistory history = Terminal.History; + CommandHistory history = ActiveHistory; if (history == null) { @@ -2204,7 +2282,7 @@ private void CacheLauncherScrollPosition() float highValue = _logScrollView.verticalScroller.highValue; float currentValue = Mathf.Clamp(_logScrollView.verticalScroller.value, 0f, highValue); _cachedLauncherScrollValue = currentValue; - _cachedLauncherScrollVersion = Terminal.History?.Version ?? -1; + _cachedLauncherScrollVersion = ActiveHistory?.Version ?? -1; } private void ScheduleLauncherScroll(float targetValue) @@ -2877,7 +2955,7 @@ public void HandlePrevious() } _input.CommandText = - Terminal.History?.Previous(skipSameCommandsInHistory) ?? string.Empty; + ActiveHistory?.Previous(skipSameCommandsInHistory) ?? string.Empty; ResetAutoComplete(); _needsFocus = true; } @@ -2889,7 +2967,7 @@ public void HandleNext() return; } - _input.CommandText = Terminal.History?.Next(skipSameCommandsInHistory) ?? string.Empty; + _input.CommandText = ActiveHistory?.Next(skipSameCommandsInHistory) ?? string.Empty; ResetAutoComplete(); _needsFocus = true; } @@ -2936,6 +3014,16 @@ internal void SetLauncherMetricsForTests( internal float CurrentWindowHeightForTests => _currentWindowHeight; + internal void SetRuntimeProfileForTests(TerminalRuntimeProfile profile) + { + _runtimeProfile = profile; + ApplyRuntimeProfile(); + if (_runtime != null) + { + RefreshStaticState(force: true); + } + } + internal void SetWindowHeightsForTests( float currentHeight, float targetHeight, @@ -2994,8 +3082,8 @@ public void EnterCommand() } Terminal.Log(TerminalLogType.Input, commandText); - Terminal.Shell?.RunCommand(commandText); - while (Terminal.Shell?.TryConsumeErrorMessage(out string error) == true) + ActiveShell?.RunCommand(commandText); + while (ActiveShell?.TryConsumeErrorMessage(out string error) == true) { Terminal.Log(TerminalLogType.Error, $"Error: {error}"); } @@ -3017,7 +3105,7 @@ private string BuildCompletionText(string suggestion) return suggestion ?? string.Empty; } - CommandAutoComplete autoComplete = Terminal.AutoComplete; + CommandAutoComplete autoComplete = ActiveAutoComplete; if (autoComplete == null || !autoComplete.LastCompletionUsedCompleter) { return suggestion; @@ -3036,7 +3124,7 @@ public void CompleteCommand(bool searchForward = true) try { - CommandAutoComplete autoComplete = Terminal.AutoComplete; + CommandAutoComplete autoComplete = ActiveAutoComplete; if (autoComplete == null) { return; @@ -3387,7 +3475,7 @@ private void UpdateLauncherLayoutMetrics() if (visibleHistoryCount == 0) { - int pendingLogs = Terminal.History?.Count ?? 0; + int pendingLogs = ActiveHistory?.Count ?? 0; visibleHistoryCount = Mathf.Min( pendingLogs, _launcherMetrics.HistoryVisibleEntryCount @@ -3629,7 +3717,14 @@ private void HandleHeightAnimation() private static void HandleUnityLog(string message, string stackTrace, LogType type) { - Terminal.Buffer?.EnqueueUnityLog(message, stackTrace, (TerminalLogType)type); + ITerminalRuntime runtime = Terminal.ActiveRuntime; + if (runtime == null) + { + return; + } + + CommandLog log = runtime.Log; + log?.EnqueueUnityLog(message, stackTrace, (TerminalLogType)type); } } } diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index 7879b1d..6272e97 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -15,13 +15,31 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime public sealed class BuiltinCommandTests { - [TearDown] - public void TearDown() + [UnityTearDown] + public IEnumerator UnityTearDown() { - if (TerminalUI.Instance != null) + yield return TestSceneHelpers.DestroyTerminalAndWait(); + } + + private static LogItem GetLastLog(CommandLog buffer, Func predicate = null) + { + Assert.IsNotNull(buffer, "Command log is not initialized."); + IReadOnlyList logs = buffer.Logs; + for (int i = logs.Count - 1; i >= 0; --i) { - UnityEngine.Object.Destroy(TerminalUI.Instance.gameObject); + LogItem entry = logs[i]; + if (predicate == null || predicate(entry)) + { + return entry; + } } + + Assert.Fail( + predicate == null + ? "Expected at least one log entry, but none were recorded." + : "Expected matching log entry but none were recorded." + ); + return default; } private static IEnumerator RestartTerminal() @@ -148,8 +166,9 @@ public IEnumerator TimeCommandMeasuresNestedExecution() Assert.IsTrue(executed); Terminal.Buffer?.DrainPending(); - LogItem timeLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.ShellMessage && item.message.StartsWith("Time:") + LogItem timeLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.ShellMessage && item.message.StartsWith("Time:") ); StringAssert.StartsWith("Time:", timeLog.message); Assert.Greater(Terminal.Buffer.Logs.Count, initialCount); @@ -188,8 +207,9 @@ public IEnumerator LogCommandsEmitOutput() { Assert.IsTrue(Terminal.Shell.RunCommand("log-terminal captured")); Terminal.Buffer?.DrainPending(); - LogItem bufferLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.ShellMessage && item.message == "captured" + LogItem bufferLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.ShellMessage && item.message == "captured" ); Assert.AreEqual("captured", bufferLog.message); @@ -218,19 +238,20 @@ public IEnumerator TraceCommandProducesStackTraceWhenAvailable() Assert.IsTrue(shell.RunCommand("trace")); Terminal.Buffer?.DrainPending(); - LogItem warningLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Warning + LogItem warningLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Warning ); StringAssert.Contains("Nothing to trace", warningLog.message); Terminal.Log(TerminalLogType.Message, "trace-target"); Terminal.Buffer?.DrainPending(); - LogItem previousLog = Terminal.Buffer.Logs.Last(); + LogItem previousLog = GetLastLog(Terminal.Buffer); Assert.IsFalse(string.IsNullOrWhiteSpace(previousLog.stackTrace)); Assert.IsTrue(shell.RunCommand("trace")); Terminal.Buffer?.DrainPending(); - LogItem traceLog = Terminal.Buffer.Logs.Last(); + LogItem traceLog = GetLastLog(Terminal.Buffer); Assert.AreEqual(previousLog.stackTrace, traceLog.message); yield break; } @@ -247,15 +268,17 @@ public IEnumerator VariableCommandsManageLifecycle() Assert.IsTrue(shell.RunCommand("get-variable foo")); Terminal.Buffer?.DrainPending(); - LogItem getLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.ShellMessage + LogItem getLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.ShellMessage ); StringAssert.Contains("bar baz", getLog.message); Assert.IsTrue(shell.RunCommand("list-variables")); Terminal.Buffer?.DrainPending(); - LogItem listLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.ShellMessage + LogItem listLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.ShellMessage ); StringAssert.Contains("foo", listLog.message); @@ -271,8 +294,9 @@ public IEnumerator VariableCommandsManageLifecycle() Assert.IsTrue(shell.RunCommand("list-variables")); Terminal.Buffer?.DrainPending(); - LogItem emptyLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Warning + LogItem emptyLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Warning ); StringAssert.Contains("No variables found", emptyLog.message); yield break; @@ -287,30 +311,34 @@ public IEnumerator ThemeCommandsOperateOnAvailableTheme() Assert.IsTrue(shell.RunCommand("list-themes")); Terminal.Buffer?.DrainPending(); - LogItem listLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Message + LogItem listLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Message ); string expectedThemeName = ThemeNameHelper.GetFriendlyThemeName("test-theme"); StringAssert.Contains(expectedThemeName, listLog.message); Assert.IsTrue(shell.RunCommand("get-theme")); Terminal.Buffer?.DrainPending(); - LogItem getLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Message + LogItem getLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Message ); StringAssert.Contains("Current terminal theme", getLog.message); Assert.IsTrue(shell.RunCommand("set-theme test-theme")); Terminal.Buffer?.DrainPending(); - LogItem setLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Message + LogItem setLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Message ); StringAssert.Contains("test-theme", setLog.message); Assert.IsTrue(shell.RunCommand("set-random-theme")); Terminal.Buffer?.DrainPending(); - LogItem randomLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Message + LogItem randomLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Message ); StringAssert.Contains("set theme", randomLog.message); yield break; @@ -325,29 +353,33 @@ public IEnumerator FontCommandsHandleMissingFonts() Assert.IsTrue(shell.RunCommand("list-fonts")); Terminal.Buffer?.DrainPending(); - LogItem listLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Message + LogItem listLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Message ); Assert.IsNotNull(listLog); Assert.IsTrue(shell.RunCommand("get-font")); Terminal.Buffer?.DrainPending(); - LogItem getLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Message + LogItem getLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Message ); StringAssert.Contains("null", getLog.message); Assert.IsTrue(shell.RunCommand("set-font missing-font")); Terminal.Buffer?.DrainPending(); - LogItem warningLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Warning + LogItem warningLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Warning ); StringAssert.Contains("not found", warningLog.message); Assert.IsTrue(shell.RunCommand("set-random-font")); Terminal.Buffer?.DrainPending(); - LogItem randomLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.Warning + LogItem randomLog = GetLastLog( + Terminal.Buffer, + item => item.type == TerminalLogType.Warning ); StringAssert.Contains("No fonts available", randomLog.message); yield break; @@ -373,8 +405,11 @@ public IEnumerator HelpCommandProvidesCommandDetails() initialCount = Terminal.Buffer.Logs.Count; Assert.IsTrue(shell.RunCommand("help clear-history")); Terminal.Buffer?.DrainPending(); - LogItem specificLog = Terminal.Buffer.Logs.Last(item => - item.type == TerminalLogType.ShellMessage && item.message.Contains("clear-history") + LogItem specificLog = GetLastLog( + Terminal.Buffer, + item => + item.type == TerminalLogType.ShellMessage + && item.message.Contains("clear-history") ); StringAssert.Contains("clear-history", specificLog.message); yield break; diff --git a/Tests/Runtime/Components/TestSceneHelpers.cs b/Tests/Runtime/Components/TestSceneHelpers.cs index ac3dfb5..2e587dd 100644 --- a/Tests/Runtime/Components/TestSceneHelpers.cs +++ b/Tests/Runtime/Components/TestSceneHelpers.cs @@ -21,7 +21,11 @@ public static IEnumerator DestroyTerminalAndWait(int frames = 2) public static IEnumerator CleanRestart(bool resetStateOnInit, int settleFrames = 2) { yield return DestroyTerminalAndWait(settleFrames); - yield return TerminalTests.SpawnTerminal(resetStateOnInit); + yield return TerminalTests.SpawnTerminal( + resetStateOnInit, + configure: null, + ensureLargeLogBuffer: true + ); for (int i = 0; i < settleFrames; ++i) { yield return null; diff --git a/Tests/Runtime/LauncherModeTests.cs b/Tests/Runtime/LauncherModeTests.cs index 8fa72f9..07bc2e1 100644 --- a/Tests/Runtime/LauncherModeTests.cs +++ b/Tests/Runtime/LauncherModeTests.cs @@ -68,61 +68,54 @@ public IEnumerator RefreshLauncherHistoryProducesFadedEntries() TerminalUI terminal = TerminalUI.Instance; Assert.IsNotNull(terminal); - CommandHistory previousHistory = Terminal.History; - CommandHistory history = new CommandHistory(16); - Terminal.History = history; - - try - { - history.Push("first", true, true); - history.Push("second", true, true); - history.Push("third", true, true); - - LauncherLayoutMetrics metrics = new LauncherLayoutMetrics( - width: 640f, - height: 160f, - left: 100f, - top: 200f, - historyHeight: 120f, - cornerRadius: 14f, - insetPadding: 12f, - historyVisibleEntryCount: 3, - historyFadeExponent: 2f, - snapOpen: true, - animationDuration: 0.1f - ); + CommandHistory history = terminal.Runtime.History; + Assert.IsNotNull(history); + history.Clear(); + + history.Push("first", true, true); + history.Push("second", true, true); + history.Push("third", true, true); + + LauncherLayoutMetrics metrics = new LauncherLayoutMetrics( + width: 640f, + height: 160f, + left: 100f, + top: 200f, + historyHeight: 120f, + cornerRadius: 14f, + insetPadding: 12f, + historyVisibleEntryCount: 3, + historyFadeExponent: 2f, + snapOpen: true, + animationDuration: 0.1f + ); - ScrollView scroll = new ScrollView(); - terminal.SetLogScrollViewForTests(scroll); - terminal.SetLauncherMetricsForTests(metrics); - terminal.SetState(TerminalState.OpenLauncher); - terminal.RefreshLauncherHistoryForTests(); - - VisualElement content = terminal.LogScrollViewForTests.contentContainer; - Assert.That(content.childCount, Is.EqualTo(3)); - - // Verify newest entry is first and fully opaque - Label newest = content[0] as Label; - Assert.IsNotNull(newest); - Assert.That(newest!.text, Is.EqualTo("third")); - Assert.That(newest.style.opacity.value, Is.EqualTo(1f).Within(0.001f)); - - // Middle entry has partial opacity - Label middle = content[1] as Label; - Assert.IsNotNull(middle); - Assert.That(middle!.text, Is.EqualTo("second")); - Assert.That(middle.style.opacity.value, Is.LessThan(1f).And.GreaterThan(0.35f)); - - // Oldest entry is faded out - Label oldest = content[2] as Label; - Assert.IsNotNull(oldest); - Assert.That(oldest!.text, Is.EqualTo("first")); - Assert.That(oldest.style.opacity.value, Is.EqualTo(0.35f).Within(0.001f)); - } - finally - { - Terminal.History = previousHistory; - } + ScrollView scroll = new ScrollView(); + terminal.SetLogScrollViewForTests(scroll); + terminal.SetLauncherMetricsForTests(metrics); + terminal.SetState(TerminalState.OpenLauncher); + terminal.RefreshLauncherHistoryForTests(); + + VisualElement content = terminal.LogScrollViewForTests.contentContainer; + Assert.That(content.childCount, Is.EqualTo(3)); + + // Verify newest entry is first and fully opaque + Label newest = content[0] as Label; + Assert.IsNotNull(newest); + Assert.That(newest!.text, Is.EqualTo("third")); + Assert.That(newest.style.opacity.value, Is.EqualTo(1f).Within(0.001f)); + + // Middle entry has partial opacity + Label middle = content[1] as Label; + Assert.IsNotNull(middle); + Assert.That(middle!.text, Is.EqualTo("second")); + Assert.That(middle.style.opacity.value, Is.LessThan(1f).And.GreaterThan(0.35f)); + + // Oldest entry is faded out + Label oldest = content[2] as Label; + Assert.IsNotNull(oldest); + Assert.That(oldest!.text, Is.EqualTo("first")); + Assert.That(oldest.style.opacity.value, Is.EqualTo(0.35f).Within(0.001f)); yield return TestSceneHelpers.DestroyTerminalAndWait(); } diff --git a/Tests/Runtime/TerminalRuntimeTests.cs b/Tests/Runtime/TerminalRuntimeTests.cs new file mode 100644 index 0000000..1f377d6 --- /dev/null +++ b/Tests/Runtime/TerminalRuntimeTests.cs @@ -0,0 +1,130 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Backend; + using NUnit.Framework; + + public sealed class TerminalRuntimeTests + { + [Test] + public void ConfigureCreatesRuntimeComponents() + { + TerminalRuntime runtime = new TerminalRuntime(); + TerminalRuntimeUpdateResult result = runtime.Configure( + CreateSettings(), + forceReset: true + ); + + Assert.IsNotNull(runtime.Log); + Assert.IsNotNull(runtime.History); + Assert.IsNotNull(runtime.Shell); + Assert.IsNotNull(runtime.AutoComplete); + + Assert.IsTrue(result.LogRecreated); + Assert.IsTrue(result.HistoryRecreated); + Assert.IsTrue(result.ShellRecreated); + Assert.IsTrue(result.AutoCompleteRecreated); + } + + [Test] + public void ConfigureWithoutForceReusesExistingInstances() + { + TerminalRuntime runtime = new TerminalRuntime(); + runtime.Configure(CreateSettings(), forceReset: true); + + CommandLog initialLog = runtime.Log; + CommandHistory initialHistory = runtime.History; + CommandShell initialShell = runtime.Shell; + CommandAutoComplete initialAutoComplete = runtime.AutoComplete; + + TerminalRuntimeUpdateResult result = runtime.Configure( + CreateSettings(), + forceReset: false + ); + + Assert.AreSame(initialLog, runtime.Log); + Assert.AreSame(initialHistory, runtime.History); + Assert.AreSame(initialShell, runtime.Shell); + Assert.AreSame(initialAutoComplete, runtime.AutoComplete); + + Assert.IsFalse(result.LogRecreated); + Assert.IsFalse(result.HistoryRecreated); + Assert.IsFalse(result.ShellRecreated); + Assert.IsFalse(result.AutoCompleteRecreated); + } + + [Test] + public void ConfigureDetectsCommandConfigurationChanges() + { + TerminalRuntime runtime = new TerminalRuntime(); + runtime.Configure(CreateSettings(), forceReset: true); + + TerminalRuntimeUpdateResult updated = runtime.Configure( + CreateSettings(disabledCommands: new[] { "help" }), + forceReset: false + ); + + Assert.IsTrue(updated.CommandsRefreshed); + Assert.IsTrue(runtime.Shell.IgnoredCommands.Contains("help")); + } + + [Test] + public void LogMessageAppendsToCommandLog() + { + TerminalRuntime runtime = new TerminalRuntime(); + runtime.Configure(CreateSettings(), forceReset: true); + + bool logged = runtime.LogMessage( + TerminalLogType.Message, + "hello {0}", + "world" + ); + + Assert.IsTrue(logged); + + CommandLog log = runtime.Log; + Assert.IsNotNull(log); + log.DrainPending(); + + Assert.AreEqual("hello world", log.Logs.Last().message); + } + + [Test] + public void ConfigureWithoutChangesAvoidsAllocations() + { + TerminalRuntime runtime = new TerminalRuntime(); + TerminalRuntimeSettings settings = CreateSettings(); + runtime.Configure(settings, forceReset: true); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + long before = GC.GetAllocatedBytesForCurrentThread(); + TerminalRuntimeUpdateResult result = runtime.Configure(settings, forceReset: false); + long after = GC.GetAllocatedBytesForCurrentThread(); + + Assert.IsFalse(result.RuntimeReset); + Assert.AreEqual(before, after, "Configure should not allocate after warm up."); + } + + private static TerminalRuntimeSettings CreateSettings( + int logCapacity = 32, + int historyCapacity = 16, + IReadOnlyList ignoredLogTypes = null, + IReadOnlyList disabledCommands = null, + bool ignoreDefaultCommands = false + ) + { + return new TerminalRuntimeSettings( + logCapacity, + historyCapacity, + ignoredLogTypes ?? Array.Empty(), + disabledCommands ?? Array.Empty(), + ignoreDefaultCommands + ); + } + } +} diff --git a/Tests/Runtime/TerminalRuntimeTests.cs.meta b/Tests/Runtime/TerminalRuntimeTests.cs.meta new file mode 100644 index 0000000..3f49d90 --- /dev/null +++ b/Tests/Runtime/TerminalRuntimeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42ea6acfb149c2057a177e3a9f52c998 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index 2b8493f..41450c2 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -1,3 +1,4 @@ +#pragma warning disable CS0618 // Type or member is obsolete namespace WallstopStudios.DxCommandTerminal.Tests.Runtime { using System; @@ -5,22 +6,26 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime using System.Collections.Generic; using System.Linq; using Backend; + using Backend.Profiles; using Components; using NUnit.Framework; using UI; using UnityEngine; using UnityEngine.TestTools; using UnityEngine.UIElements; - using Object = UnityEngine.Object; public sealed class TerminalTests { - [TearDown] - public void TearDown() + private TerminalRuntimeProfile _runtimeProfileUnderTest; + + [UnityTearDown] + public IEnumerator UnityTearDown() { - if (TerminalUI.Instance != null) + yield return TestSceneHelpers.DestroyTerminalAndWait(); + if (_runtimeProfileUnderTest != null) { - Object.Destroy(TerminalUI.Instance.gameObject); + ScriptableObject.DestroyImmediate(_runtimeProfileUnderTest); + _runtimeProfileUnderTest = null; } } @@ -95,7 +100,6 @@ public IEnumerator CleanConstruction() Assert.AreNotSame(terminal2, TerminalUI.Instance); Assert.AreNotSame(terminal1, TerminalUI.Instance); Assert.AreNotSame(shell, Terminal.Shell); - Assert.AreNotSame(shell, Terminal.Shell); Assert.IsNotNull(Terminal.Shell); Assert.AreNotSame(history, Terminal.History); Assert.IsNotNull(Terminal.History); @@ -107,7 +111,8 @@ public IEnumerator CleanConstruction() internal static IEnumerator SpawnTerminal( bool resetStateOnInit, - Action configure = null + Action configure = null, + bool ensureLargeLogBuffer = true ) { GameObject go = new("Terminal"); @@ -138,7 +143,7 @@ internal static IEnumerator SpawnTerminal( go.SetActive(true); yield return new WaitUntil(() => startTracker.Started); // Ensure the buffer is large enough for concurrency tests - if (Terminal.Buffer != null) + if (ensureLargeLogBuffer && Terminal.Buffer != null) { Terminal.Buffer.Resize(4096); } @@ -204,6 +209,42 @@ public IEnumerator RuntimeModeOptionsApplySelectedConfiguration() Assert.AreEqual(TerminalRuntimeModeFlags.Production, configuredMode); } +#if UNITY_EDITOR + [UnityTest] + public IEnumerator RuntimeProfileOverridesEmbeddedSettings() + { + TerminalRuntimeProfile profile = ScriptableObject.CreateInstance(); + _runtimeProfileUnderTest = profile; + + profile.ConfigureForTests( + logBufferSize: 10, + historyBufferSize: 5, + ignoreDefaults: true, + ignoredLogTypes: new[] { TerminalLogType.Warning }, + disabledCommands: new[] { "clear" } + ); + + yield return SpawnTerminal( + resetStateOnInit: true, + configure: terminal => terminal.SetRuntimeProfileForTests(profile), + ensureLargeLogBuffer: false + ); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + CommandLog log = terminal.Runtime.Log; + CommandHistory history = terminal.Runtime.History; + CommandShell shell = terminal.Runtime.Shell; + + Assert.AreEqual(10, log.Capacity); + Assert.AreEqual(5, history.Capacity); + Assert.IsTrue(shell.IgnoringDefaultCommands); + Assert.IsTrue(shell.IgnoredCommands.Contains("clear")); + Assert.IsTrue(log.ignoredLogTypes.Contains(TerminalLogType.Warning)); + } +#endif + [UnityTest] public IEnumerator RuntimeModeOptionsFallbackToFirstWhenSelectionMissing() { From 80f81df95c20f7d24db70ec3660ef2a081fb71c8 Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 09:22:35 -0700 Subject: [PATCH 28/69] Terminal decomposition --- .../UI/TerminalUI.AutoCompleteView.cs | 12 + .../CommandTerminal/UI/TerminalUI.LogView.cs | 877 ++++++++++++++++++ .../UI/TerminalUI.LogView.cs.meta | 11 + Runtime/CommandTerminal/UI/TerminalUI.cs | 864 +---------------- Tests/Runtime/BuiltinCommandTests.cs | 19 +- 5 files changed, 916 insertions(+), 867 deletions(-) create mode 100644 Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs create mode 100644 Runtime/CommandTerminal/UI/TerminalUI.LogView.cs create mode 100644 Runtime/CommandTerminal/UI/TerminalUI.LogView.cs.meta diff --git a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs new file mode 100644 index 0000000..46343f0 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs @@ -0,0 +1,12 @@ +namespace WallstopStudios.DxCommandTerminal.UI +{ + using System; + using System.Collections.Generic; + using Backend; + using UnityEngine; + using UnityEngine.UIElements; + + public sealed partial class TerminalUI + { + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs new file mode 100644 index 0000000..be99f0c --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs @@ -0,0 +1,877 @@ +namespace WallstopStudios.DxCommandTerminal.UI +{ + using System; + using System.Collections.Generic; + using Backend; + using Extensions; + using UnityEngine; + using UnityEngine.UIElements; + + public sealed partial class TerminalUI + { + private void RefreshLogs() + { + CommandLog log = ActiveLog; + if (log == null) + { + return; + } + + IReadOnlyList logs = log.Logs; + + if (_logScrollView == null) + { + return; + } + + if (IsLauncherActive && _launcherMetricsInitialized) + { + RefreshLauncherHistory(); + return; + } + + VisualElement content = _logScrollView.contentContainer; + _logScrollView.style.display = DisplayStyle.Flex; + bool dirty = _lastSeenBufferVersion != log.Version; + if (content.childCount != logs.Count) + { + dirty = true; + if (content.childCount < logs.Count) + { + for (int i = 0; i < logs.Count - content.childCount; ++i) + { + Label logText = new(); + logText.AddToClassList("terminal-output-label"); + content.Add(logText); + } + } + else if (logs.Count < content.childCount) + { + for (int i = content.childCount - 1; logs.Count <= i; --i) + { + content.RemoveAt(i); + } + } + + _needsScrollToEnd = true; + } + + if (dirty) + { + for (int i = 0; i < logs.Count && i < content.childCount; ++i) + { + VisualElement item = content[i]; + LogItem logItem = logs[i]; + switch (item) + { + case TextField logText: + { + ApplyLogStyling(logText, logItem); + logText.value = logItem.message; + break; + } + case Label logLabel: + { + ApplyLogStyling(logLabel, logItem); + logLabel.text = logItem.message; + break; + } + case Button button: + { + ApplyLogStyling(button, logItem); + button.text = logItem.message; + break; + } + } + + item.style.opacity = 1f; + item.style.display = DisplayStyle.Flex; + } + + if (logs.Count == content.childCount) + { + _lastSeenBufferVersion = log.Version; + } + } + + if (ShouldApplyHistoryFade()) + { + ApplyHistoryFade(content, fadeFromTop: false); + } + else + { + ResetHistoryFade(content); + } + } + + private void RefreshLauncherHistory() + { + if (_logScrollView == null) + { + return; + } + + VisualElement content = _logScrollView.contentContainer; + CommandHistory history = ActiveHistory; + + if (history == null) + { + _launcherHistoryEntries.Clear(); + _logScrollView.style.display = DisplayStyle.None; + for (int i = 0; i < content.childCount; ++i) + { + content[i].style.display = DisplayStyle.None; + } + + _lastRenderedLauncherHistoryVersion = -1; + _cachedLauncherScrollVersion = -1; + _cachedLauncherScrollValue = 0f; + _restoreLauncherScrollPending = false; + _launcherHistoryContentHeight = 0f; + _needsScrollToEnd = false; + return; + } + + history.CopyEntriesTo(_launcherHistoryEntries); + long historyVersion = history.Version; + + int entryCount = _launcherHistoryEntries.Count; + int visibleCount = Mathf.Min(_launcherMetrics.HistoryVisibleEntryCount, entryCount); + + if (_launcherMetrics.HistoryHeight <= 0f || visibleCount <= 0) + { + _logScrollView.style.display = DisplayStyle.None; + for (int i = 0; i < content.childCount; ++i) + { + content[i].style.display = DisplayStyle.None; + } + + _lastRenderedLauncherHistoryVersion = historyVersion; + _cachedLauncherScrollVersion = historyVersion; + _cachedLauncherScrollValue = 0f; + _restoreLauncherScrollPending = false; + _launcherHistoryContentHeight = 0f; + _needsScrollToEnd = false; + return; + } + + _logScrollView.style.display = DisplayStyle.Flex; + + if (content.childCount < visibleCount) + { + for (int i = content.childCount; i < visibleCount; ++i) + { + Label logText = new(); + logText.AddToClassList("terminal-output-label"); + content.Add(logText); + } + } + + for (int i = visibleCount; i < content.childCount; ++i) + { + content[i].style.display = DisplayStyle.None; + } + + for (int i = 0; i < visibleCount; ++i) + { + int historyIndex = entryCount - 1 - i; + CommandHistoryEntry entry = _launcherHistoryEntries[historyIndex]; + VisualElement element = content[i]; + LogItem logItem = new(TerminalLogType.Input, entry.Text, string.Empty); + + switch (element) + { + case TextField logText: + { + ApplyLogStyling(logText, logItem); + logText.value = entry.Text; + break; + } + case Label logLabel: + { + ApplyLogStyling(logLabel, logItem); + logLabel.text = entry.Text; + break; + } + case Button button: + { + ApplyLogStyling(button, logItem); + button.text = entry.Text; + break; + } + } + + element.style.display = DisplayStyle.Flex; + } + + if (ShouldApplyHistoryFade()) + { + ApplyHistoryFade(content, fadeFromTop: true); + } + else + { + ResetHistoryFade(content); + } + + bool historyChanged = historyVersion != _lastRenderedLauncherHistoryVersion; + bool restoreRequested = _restoreLauncherScrollPending; + float? targetScroll = null; + + if (restoreRequested) + { + float targetValue = _cachedLauncherScrollValue; + if (_cachedLauncherScrollVersion != historyVersion) + { + targetValue = 0f; + } + + _cachedLauncherScrollVersion = historyVersion; + _cachedLauncherScrollValue = targetValue; + targetScroll = targetValue; + _restoreLauncherScrollPending = false; + } + else if (historyChanged) + { + _cachedLauncherScrollVersion = historyVersion; + _cachedLauncherScrollValue = 0f; + targetScroll = 0f; + } + + if (targetScroll.HasValue) + { + ScheduleLauncherScroll(targetScroll.Value); + } + + _lastRenderedLauncherHistoryVersion = historyVersion; + _needsScrollToEnd = false; + } + + private static void ApplyLogStyling(VisualElement logText, LogItem log) + { + logText.EnableInClassList( + "terminal-output-label--shell", + log.type == TerminalLogType.ShellMessage + ); + logText.EnableInClassList( + "terminal-output-label--error", + log.type + is TerminalLogType.Exception + or TerminalLogType.Error + or TerminalLogType.Assert + ); + logText.EnableInClassList( + "terminal-output-label--warning", + log.type == TerminalLogType.Warning + ); + logText.EnableInClassList( + "terminal-output-label--message", + log.type == TerminalLogType.Message + ); + logText.EnableInClassList( + "terminal-output-label--input", + log.type == TerminalLogType.Input + ); + } + + + private bool ShouldApplyHistoryFade() + { + return _state switch + { + TerminalState.OpenLauncher => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.Launcher + ), + TerminalState.OpenSmall => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.SmallTerminal + ), + TerminalState.OpenFull => _historyFadeTargets.HasFlagNoAlloc( + TerminalHistoryFadeTargets.FullTerminal + ), + _ => false, + }; + } + + private float GetHistoryFadeRangeFactor() + { + return _state switch + { + TerminalState.OpenLauncher => 0.6f, + TerminalState.OpenFull => 1.0f, + TerminalState.OpenSmall => 0.85f, + _ => 0.85f, + }; + } + + private float GetHistoryFadeMinimumOpacity() + { + return _state == TerminalState.OpenLauncher ? 0.35f : 0.45f; + } + + private float GetHistoryFallbackRowHeight() + { + return _state == TerminalState.OpenLauncher + ? LauncherEstimatedHistoryRowHeight + : StandardEstimatedHistoryRowHeight; + } + + private float GetHistoryFadeExponent() + { + if (_state == TerminalState.OpenLauncher && _launcherMetricsInitialized) + { + return Mathf.Max(0.01f, _launcherMetrics.HistoryFadeExponent); + } + + return 1f; + } + + private void ApplyHistoryFade(VisualElement container, bool fadeFromTop) + { + if (container == null) + { + return; + } + + Rect viewportBounds = _logViewport?.worldBound ?? Rect.zero; + bool viewportIsValid = viewportBounds.height > 0.01f; + float fallbackRowHeight = GetHistoryFallbackRowHeight(); + + int childCount = container.childCount; + if (childCount == 0) + { + return; + } + + int visibleCount = 0; + for (int i = 0; i < childCount; ++i) + { + VisualElement element = container[i]; + if (element == null || element.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + visibleCount++; + } + + if (visibleCount == 0) + { + return; + } + + if (!viewportIsValid) + { + float fallbackHeight = Mathf.Max(1f, fallbackRowHeight * visibleCount); + viewportBounds.height = fallbackHeight; + } + + float fadeRange = Mathf.Max(1f, viewportBounds.height * GetHistoryFadeRangeFactor()); + float minimumOpacity = Mathf.Clamp01(GetHistoryFadeMinimumOpacity()); + + int visibleIndex = 0; + for (int i = 0; i < childCount; ++i) + { + VisualElement element = container[i]; + if (element == null || element.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + float distance; + if (viewportIsValid) + { + Rect childBounds = element.worldBound; + bool boundsValid = childBounds.height > 0.01f; + if (fadeFromTop) + { + distance = boundsValid + ? Mathf.Max(0f, childBounds.yMin - viewportBounds.yMin) + : fallbackRowHeight * visibleIndex; + } + else + { + int inverseIndex = Math.Max(0, visibleCount - visibleIndex - 1); + distance = boundsValid + ? Mathf.Max(0f, viewportBounds.yMax - childBounds.yMax) + : fallbackRowHeight * inverseIndex; + } + } + else + { + int indexFromEdge = fadeFromTop + ? visibleIndex + : Math.Max(0, visibleCount - visibleIndex - 1); + distance = fallbackRowHeight * indexFromEdge; + } + + float normalized = Mathf.Clamp01(distance / fadeRange); + float adjusted = Mathf.Pow(normalized, GetHistoryFadeExponent()); + float opacity = Mathf.Lerp(1f, minimumOpacity, adjusted); + element.style.opacity = opacity; + + visibleIndex++; + } + } + + private static void ResetHistoryFade(VisualElement container) + { + if (container == null) + { + return; + } + + int childCount = container.childCount; + for (int i = 0; i < childCount; ++i) + { + VisualElement element = container[i]; + if (element == null || element.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + element.style.opacity = 1f; + } + } + + private void CacheLauncherScrollPosition() + { + if (_logScrollView?.verticalScroller == null) + { + return; + } + + float highValue = _logScrollView.verticalScroller.highValue; + float currentValue = Mathf.Clamp(_logScrollView.verticalScroller.value, 0f, highValue); + _cachedLauncherScrollValue = currentValue; + _cachedLauncherScrollVersion = ActiveHistory?.Version ?? -1; + } + + private void ScheduleLauncherScroll(float targetValue) + { + if (_logScrollView?.verticalScroller == null) + { + return; + } + + float clampedTarget = Mathf.Clamp( + targetValue, + 0f, + _logScrollView.verticalScroller.highValue + ); + + _logScrollView + .schedule.Execute(() => + { + if (_logScrollView?.verticalScroller == null) + { + return; + } + + float highValue = _logScrollView.verticalScroller.highValue; + _logScrollView.verticalScroller.value = Mathf.Clamp( + clampedTarget, + 0f, + highValue + ); + }) + .ExecuteLater(0); + } + + private void RefreshAutoCompleteHints() + { + bool shouldDisplay = + 0 < _lastCompletionBuffer.Count + && hintDisplayMode is HintDisplayMode.Always or HintDisplayMode.AutoCompleteOnly + && _autoCompleteContainer != null; + + if (!shouldDisplay) + { + if (0 < _autoCompleteContainer?.childCount) + { + _autoCompleteContainer.Clear(); + } + + _previousLastCompletionIndex = null; + return; + } + + int bufferLength = _lastCompletionBuffer.Count; + if (_lastKnownHintsClickable != makeHintsClickable) + { + _autoCompleteContainer.Clear(); + _lastKnownHintsClickable = makeHintsClickable; + } + + int currentChildCount = _autoCompleteContainer.childCount; + + bool dirty = _lastCompletionIndex != _previousLastCompletionIndex; + bool contentsChanged = currentChildCount != bufferLength; + if (contentsChanged) + { + dirty = true; + if (currentChildCount < bufferLength) + { + for (int i = currentChildCount; i < bufferLength; ++i) + { + string hint = _lastCompletionBuffer[i]; + VisualElement hintElement; + + if (makeHintsClickable) + { + int currentIndex = i; + string currentHint = hint; + Button hintButton = new(() => + { + _input.CommandText = BuildCompletionText(currentHint); + _lastCompletionIndex = currentIndex; + _needsFocus = true; + }) + { + text = hint, + }; + hintElement = hintButton; + } + else + { + Label hintText = new(hint); + hintElement = hintText; + } + + hintElement.name = $"SuggestionText{i}"; + _autoCompleteContainer.Add(hintElement); + + bool isSelected = i == _lastCompletionIndex; + hintElement.AddToClassList("terminal-button"); + hintElement.EnableInClassList("autocomplete-item-selected", isSelected); + hintElement.EnableInClassList("autocomplete-item", !isSelected); + } + } + else if (bufferLength < currentChildCount) + { + for (int i = currentChildCount - 1; bufferLength <= i; --i) + { + _autoCompleteContainer.RemoveAt(i); + } + } + } + + bool shouldUpdateCompletionIndex = false; + try + { + shouldUpdateCompletionIndex = _autoCompleteContainer.childCount == bufferLength; + if (shouldUpdateCompletionIndex) + { + UpdateAutoCompleteView(); + } + + if (dirty) + { + for (int i = 0; i < _autoCompleteContainer.childCount && i < bufferLength; ++i) + { + VisualElement hintElement = _autoCompleteContainer[i]; + switch (hintElement) + { + case Button button: + button.text = _lastCompletionBuffer[i]; + break; + case Label label: + label.text = _lastCompletionBuffer[i]; + break; + case TextField textField: + textField.value = _lastCompletionBuffer[i]; + break; + } + + bool isSelected = i == _lastCompletionIndex; + + hintElement.EnableInClassList("autocomplete-item-selected", isSelected); + hintElement.EnableInClassList("autocomplete-item", !isSelected); + } + } + } + finally + { + if (shouldUpdateCompletionIndex) + { + _previousLastCompletionIndex = _lastCompletionIndex; + } + } + } + + private void UpdateAutoCompleteView() + { + if (_lastCompletionIndex == null) + { + return; + } + + if (_autoCompleteContainer?.contentContainer == null) + { + return; + } + + int childCount = _autoCompleteContainer.childCount; + if (childCount == 0) + { + return; + } + + if (childCount <= _lastCompletionIndex) + { + _lastCompletionIndex = + (_lastCompletionIndex % childCount + childCount) % childCount; + } + + if (_previousLastCompletionIndex == _lastCompletionIndex) + { + return; + } + + VisualElement current = _autoCompleteContainer[_lastCompletionIndex.Value]; + float viewportWidth = _autoCompleteContainer.contentViewport.resolvedStyle.width; + + // Use layout properties relative to the content container + float targetElementLeft = current.layout.x; + float targetElementWidth = current.layout.width; + float targetElementRight = targetElementLeft + targetElementWidth; + + const float epsilon = 0.01f; + + bool isFullyVisible = + epsilon <= targetElementLeft && targetElementRight <= viewportWidth + epsilon; + + if (isFullyVisible) + { + return; + } + + bool isIncrementing; + if (_previousLastCompletionIndex == childCount - 1 && _lastCompletionIndex == 0) + { + isIncrementing = true; + } + else if (_previousLastCompletionIndex == 0 && _lastCompletionIndex == childCount - 1) + { + isIncrementing = false; + } + else + { + isIncrementing = _previousLastCompletionIndex < _lastCompletionIndex; + } + + _autoCompleteChildren.Clear(); + for (int i = 0; i < childCount; ++i) + { + _autoCompleteChildren.Add(_autoCompleteContainer[i]); + } + + int shiftAmount; + if (isIncrementing) + { + shiftAmount = -1 * _lastCompletionIndex.Value; + _lastCompletionIndex = 0; + } + else + { + shiftAmount = 0; + float accumulatedWidth = 0; + for (int i = 1; i <= childCount; ++i) + { + shiftAmount++; + int index = -i % childCount; + index = (index + childCount) % childCount; + VisualElement element = _autoCompleteChildren[index]; + accumulatedWidth += + element.resolvedStyle.width + + element.resolvedStyle.marginLeft + + element.resolvedStyle.marginRight + + element.resolvedStyle.borderLeftWidth + + element.resolvedStyle.borderRightWidth; + + if (accumulatedWidth <= viewportWidth) + { + continue; + } + + if (element != current) + { + --shiftAmount; + } + + break; + } + + _lastCompletionIndex = (shiftAmount - 1 + childCount) % childCount; + } + + _autoCompleteChildren.Shift(shiftAmount); + _lastCompletionBuffer.Shift(shiftAmount); + + _autoCompleteContainer.Clear(); + foreach (VisualElement element in _autoCompleteChildren) + { + _autoCompleteContainer.Add(element); + } + + float desiredTop = _currentWindowHeight; + float desiredLeft = 2f; + float desiredWidth = Screen.width; + if (IsLauncherActive && _launcherMetricsInitialized) + { + desiredTop = _launcherMetrics.Top + _currentWindowHeight + 12f; + desiredLeft = _launcherMetrics.Left; + desiredWidth = _launcherMetrics.Width; + } + + _stateButtonContainer.style.top = desiredTop; + _stateButtonContainer.style.left = desiredLeft; + _stateButtonContainer.style.width = desiredWidth; + _stateButtonContainer.style.display = showGUIButtons + ? DisplayStyle.Flex + : DisplayStyle.None; + _stateButtonContainer.style.justifyContent = + IsLauncherActive && _launcherMetricsInitialized + ? Justify.Center + : Justify.FlexStart; + + Button primaryButton; + Button secondaryButton; + Button launcherButton; + EnsureButtons(out primaryButton, out secondaryButton, out launcherButton); + + DisplayStyle buttonDisplay = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; + + UpdateButton(primaryButton, GetPrimaryLabel(), _state == TerminalState.OpenSmall); + UpdateButton(secondaryButton, GetSecondaryLabel(), _state == TerminalState.OpenFull); + UpdateButton(launcherButton, launcherButtonText, IsLauncherActive); + + return; + + void EnsureButtons(out Button primary, out Button secondary, out Button launcher) + { + while (_stateButtonContainer.childCount < 3) + { + int index = _stateButtonContainer.childCount; + Button button = index switch + { + 0 => new Button(FirstClicked) { name = "StateButton1" }, + 1 => new Button(SecondClicked) { name = "StateButton2" }, + _ => new Button(LauncherClicked) { name = "StateButton3" }, + }; + button.AddToClassList("terminal-button"); + _stateButtonContainer.Add(button); + } + + primary = _stateButtonContainer[0] as Button; + secondary = _stateButtonContainer[1] as Button; + launcher = _stateButtonContainer[2] as Button; + } + + string GetPrimaryLabel() + { + return _state switch + { + TerminalState.Closed => smallButtonText, + TerminalState.OpenSmall => closeButtonText, + TerminalState.OpenFull => closeButtonText, + TerminalState.OpenLauncher => closeButtonText, + _ => string.Empty, + }; + } + + string GetSecondaryLabel() + { + return _state switch + { + TerminalState.Closed => fullButtonText, + TerminalState.OpenSmall => fullButtonText, + TerminalState.OpenFull => smallButtonText, + TerminalState.OpenLauncher => fullButtonText, + _ => string.Empty, + }; + } + + void UpdateButton(Button button, string text, bool isActive) + { + if (button == null) + { + return; + } + + bool shouldShow = + buttonDisplay == DisplayStyle.Flex && !string.IsNullOrWhiteSpace(text); + button.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None; + if (shouldShow) + { + button.text = text; + } + button.EnableInClassList("terminal-button-active", shouldShow && isActive); + } + + void FirstClicked() + { + switch (_state) + { + case TerminalState.Closed: + ToggleSmall(); + break; + case TerminalState.OpenSmall: + case TerminalState.OpenFull: + case TerminalState.OpenLauncher: + Close(); + break; + } + } + + void SecondClicked() + { + switch (_state) + { + case TerminalState.Closed: + case TerminalState.OpenSmall: + case TerminalState.OpenLauncher: + ToggleFull(); + break; + case TerminalState.OpenFull: + ToggleSmall(); + break; + } + } + + void LauncherClicked() + { + ToggleLauncher(); + } + } + + private static void EnsureChildOrder( + VisualElement parent, + params VisualElement[] orderedChildren + ) + { + if (parent == null) + { + return; + } + + int insertIndex = 0; + foreach (VisualElement child in orderedChildren) + { + if (child == null || child.parent != parent) + { + continue; + } + + int currentIndex = parent.IndexOf(child); + if (currentIndex != insertIndex) + { + parent.Remove(child); + parent.Insert(insertIndex, child); + } + + insertIndex++; + } + } + + + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs.meta b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs.meta new file mode 100644 index 0000000..bafc48d --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f2e64399c6da22387bcc918df3bc19d3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index be539ac..aece390 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -20,7 +20,7 @@ namespace WallstopStudios.DxCommandTerminal.UI using UnityEngine.UIElements; [DisallowMultipleComponent] - public sealed class TerminalUI : MonoBehaviour + public sealed partial class TerminalUI : MonoBehaviour { private const string TerminalRootName = "TerminalRoot"; private const float LauncherAutoCompleteSpacing = 6f; @@ -1842,270 +1842,6 @@ private void FocusInput() _commandInput.selectIndex = textEndPosition; } - private void RefreshLogs() - { - CommandLog log = ActiveLog; - if (log == null) - { - return; - } - - IReadOnlyList logs = log.Logs; - - if (_logScrollView == null) - { - return; - } - - if (IsLauncherActive && _launcherMetricsInitialized) - { - RefreshLauncherHistory(); - return; - } - - VisualElement content = _logScrollView.contentContainer; - _logScrollView.style.display = DisplayStyle.Flex; - bool dirty = _lastSeenBufferVersion != log.Version; - if (content.childCount != logs.Count) - { - dirty = true; - if (content.childCount < logs.Count) - { - for (int i = 0; i < logs.Count - content.childCount; ++i) - { - Label logText = new(); - logText.AddToClassList("terminal-output-label"); - content.Add(logText); - } - } - else if (logs.Count < content.childCount) - { - for (int i = content.childCount - 1; logs.Count <= i; --i) - { - content.RemoveAt(i); - } - } - - _needsScrollToEnd = true; - } - - if (dirty) - { - for (int i = 0; i < logs.Count && i < content.childCount; ++i) - { - VisualElement item = content[i]; - LogItem logItem = logs[i]; - switch (item) - { - case TextField logText: - { - ApplyLogStyling(logText, logItem); - logText.value = logItem.message; - break; - } - case Label logLabel: - { - ApplyLogStyling(logLabel, logItem); - logLabel.text = logItem.message; - break; - } - case Button button: - { - ApplyLogStyling(button, logItem); - button.text = logItem.message; - break; - } - } - - item.style.opacity = 1f; - item.style.display = DisplayStyle.Flex; - } - - if (logs.Count == content.childCount) - { - _lastSeenBufferVersion = log.Version; - } - } - - if (ShouldApplyHistoryFade()) - { - ApplyHistoryFade(content, fadeFromTop: false); - } - else - { - ResetHistoryFade(content); - } - } - - private void RefreshLauncherHistory() - { - if (_logScrollView == null) - { - return; - } - - VisualElement content = _logScrollView.contentContainer; - CommandHistory history = ActiveHistory; - - if (history == null) - { - _launcherHistoryEntries.Clear(); - _logScrollView.style.display = DisplayStyle.None; - for (int i = 0; i < content.childCount; ++i) - { - content[i].style.display = DisplayStyle.None; - } - - _lastRenderedLauncherHistoryVersion = -1; - _cachedLauncherScrollVersion = -1; - _cachedLauncherScrollValue = 0f; - _restoreLauncherScrollPending = false; - _launcherHistoryContentHeight = 0f; - _needsScrollToEnd = false; - return; - } - - history.CopyEntriesTo(_launcherHistoryEntries); - long historyVersion = history.Version; - - int entryCount = _launcherHistoryEntries.Count; - int visibleCount = Mathf.Min(_launcherMetrics.HistoryVisibleEntryCount, entryCount); - - if (_launcherMetrics.HistoryHeight <= 0f || visibleCount <= 0) - { - _logScrollView.style.display = DisplayStyle.None; - for (int i = 0; i < content.childCount; ++i) - { - content[i].style.display = DisplayStyle.None; - } - - _lastRenderedLauncherHistoryVersion = historyVersion; - _cachedLauncherScrollVersion = historyVersion; - _cachedLauncherScrollValue = 0f; - _restoreLauncherScrollPending = false; - _launcherHistoryContentHeight = 0f; - _needsScrollToEnd = false; - return; - } - - _logScrollView.style.display = DisplayStyle.Flex; - - if (content.childCount < visibleCount) - { - for (int i = content.childCount; i < visibleCount; ++i) - { - Label logText = new(); - logText.AddToClassList("terminal-output-label"); - content.Add(logText); - } - } - - for (int i = visibleCount; i < content.childCount; ++i) - { - content[i].style.display = DisplayStyle.None; - } - - for (int i = 0; i < visibleCount; ++i) - { - int historyIndex = entryCount - 1 - i; - CommandHistoryEntry entry = _launcherHistoryEntries[historyIndex]; - VisualElement element = content[i]; - LogItem logItem = new(TerminalLogType.Input, entry.Text, string.Empty); - - switch (element) - { - case TextField logText: - { - ApplyLogStyling(logText, logItem); - logText.value = entry.Text; - break; - } - case Label logLabel: - { - ApplyLogStyling(logLabel, logItem); - logLabel.text = entry.Text; - break; - } - case Button button: - { - ApplyLogStyling(button, logItem); - button.text = entry.Text; - break; - } - } - - element.style.display = DisplayStyle.Flex; - } - - if (ShouldApplyHistoryFade()) - { - ApplyHistoryFade(content, fadeFromTop: true); - } - else - { - ResetHistoryFade(content); - } - - bool historyChanged = historyVersion != _lastRenderedLauncherHistoryVersion; - bool restoreRequested = _restoreLauncherScrollPending; - float? targetScroll = null; - - if (restoreRequested) - { - float targetValue = _cachedLauncherScrollValue; - if (_cachedLauncherScrollVersion != historyVersion) - { - targetValue = 0f; - } - - _cachedLauncherScrollVersion = historyVersion; - _cachedLauncherScrollValue = targetValue; - targetScroll = targetValue; - _restoreLauncherScrollPending = false; - } - else if (historyChanged) - { - _cachedLauncherScrollVersion = historyVersion; - _cachedLauncherScrollValue = 0f; - targetScroll = 0f; - } - - if (targetScroll.HasValue) - { - ScheduleLauncherScroll(targetScroll.Value); - } - - _lastRenderedLauncherHistoryVersion = historyVersion; - _needsScrollToEnd = false; - } - - private static void ApplyLogStyling(VisualElement logText, LogItem log) - { - logText.EnableInClassList( - "terminal-output-label--shell", - log.type == TerminalLogType.ShellMessage - ); - logText.EnableInClassList( - "terminal-output-label--error", - log.type - is TerminalLogType.Exception - or TerminalLogType.Error - or TerminalLogType.Assert - ); - logText.EnableInClassList( - "terminal-output-label--warning", - log.type == TerminalLogType.Warning - ); - logText.EnableInClassList( - "terminal-output-label--message", - log.type == TerminalLogType.Message - ); - logText.EnableInClassList( - "terminal-output-label--input", - log.type == TerminalLogType.Input - ); - } - private void ScrollToEnd() { if (0 < _logScrollView?.verticalScroller.highValue) @@ -2114,604 +1850,6 @@ private void ScrollToEnd() } } - private bool ShouldApplyHistoryFade() - { - return _state switch - { - TerminalState.OpenLauncher => _historyFadeTargets.HasFlagNoAlloc( - TerminalHistoryFadeTargets.Launcher - ), - TerminalState.OpenSmall => _historyFadeTargets.HasFlagNoAlloc( - TerminalHistoryFadeTargets.SmallTerminal - ), - TerminalState.OpenFull => _historyFadeTargets.HasFlagNoAlloc( - TerminalHistoryFadeTargets.FullTerminal - ), - _ => false, - }; - } - - private float GetHistoryFadeRangeFactor() - { - return _state switch - { - TerminalState.OpenLauncher => 0.6f, - TerminalState.OpenFull => 1.0f, - TerminalState.OpenSmall => 0.85f, - _ => 0.85f, - }; - } - - private float GetHistoryFadeMinimumOpacity() - { - return _state == TerminalState.OpenLauncher ? 0.35f : 0.45f; - } - - private float GetHistoryFallbackRowHeight() - { - return _state == TerminalState.OpenLauncher - ? LauncherEstimatedHistoryRowHeight - : StandardEstimatedHistoryRowHeight; - } - - private float GetHistoryFadeExponent() - { - if (_state == TerminalState.OpenLauncher && _launcherMetricsInitialized) - { - return Mathf.Max(0.01f, _launcherMetrics.HistoryFadeExponent); - } - - return 1f; - } - - private void ApplyHistoryFade(VisualElement container, bool fadeFromTop) - { - if (container == null) - { - return; - } - - Rect viewportBounds = _logViewport?.worldBound ?? Rect.zero; - bool viewportIsValid = viewportBounds.height > 0.01f; - float fallbackRowHeight = GetHistoryFallbackRowHeight(); - - int childCount = container.childCount; - if (childCount == 0) - { - return; - } - - int visibleCount = 0; - for (int i = 0; i < childCount; ++i) - { - VisualElement element = container[i]; - if (element == null || element.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - - visibleCount++; - } - - if (visibleCount == 0) - { - return; - } - - if (!viewportIsValid) - { - float fallbackHeight = Mathf.Max(1f, fallbackRowHeight * visibleCount); - viewportBounds.height = fallbackHeight; - } - - float fadeRange = Mathf.Max(1f, viewportBounds.height * GetHistoryFadeRangeFactor()); - float minimumOpacity = Mathf.Clamp01(GetHistoryFadeMinimumOpacity()); - - int visibleIndex = 0; - for (int i = 0; i < childCount; ++i) - { - VisualElement element = container[i]; - if (element == null || element.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - - float distance; - if (viewportIsValid) - { - Rect childBounds = element.worldBound; - bool boundsValid = childBounds.height > 0.01f; - if (fadeFromTop) - { - distance = boundsValid - ? Mathf.Max(0f, childBounds.yMin - viewportBounds.yMin) - : fallbackRowHeight * visibleIndex; - } - else - { - int inverseIndex = Math.Max(0, visibleCount - visibleIndex - 1); - distance = boundsValid - ? Mathf.Max(0f, viewportBounds.yMax - childBounds.yMax) - : fallbackRowHeight * inverseIndex; - } - } - else - { - int indexFromEdge = fadeFromTop - ? visibleIndex - : Math.Max(0, visibleCount - visibleIndex - 1); - distance = fallbackRowHeight * indexFromEdge; - } - - float normalized = Mathf.Clamp01(distance / fadeRange); - float adjusted = Mathf.Pow(normalized, GetHistoryFadeExponent()); - float opacity = Mathf.Lerp(1f, minimumOpacity, adjusted); - element.style.opacity = opacity; - - visibleIndex++; - } - } - - private static void ResetHistoryFade(VisualElement container) - { - if (container == null) - { - return; - } - - int childCount = container.childCount; - for (int i = 0; i < childCount; ++i) - { - VisualElement element = container[i]; - if (element == null || element.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - - element.style.opacity = 1f; - } - } - - private void CacheLauncherScrollPosition() - { - if (_logScrollView?.verticalScroller == null) - { - return; - } - - float highValue = _logScrollView.verticalScroller.highValue; - float currentValue = Mathf.Clamp(_logScrollView.verticalScroller.value, 0f, highValue); - _cachedLauncherScrollValue = currentValue; - _cachedLauncherScrollVersion = ActiveHistory?.Version ?? -1; - } - - private void ScheduleLauncherScroll(float targetValue) - { - if (_logScrollView?.verticalScroller == null) - { - return; - } - - float clampedTarget = Mathf.Clamp( - targetValue, - 0f, - _logScrollView.verticalScroller.highValue - ); - - _logScrollView - .schedule.Execute(() => - { - if (_logScrollView?.verticalScroller == null) - { - return; - } - - float highValue = _logScrollView.verticalScroller.highValue; - _logScrollView.verticalScroller.value = Mathf.Clamp( - clampedTarget, - 0f, - highValue - ); - }) - .ExecuteLater(0); - } - - private void RefreshAutoCompleteHints() - { - bool shouldDisplay = - 0 < _lastCompletionBuffer.Count - && hintDisplayMode is HintDisplayMode.Always or HintDisplayMode.AutoCompleteOnly - && _autoCompleteContainer != null; - - if (!shouldDisplay) - { - if (0 < _autoCompleteContainer?.childCount) - { - _autoCompleteContainer.Clear(); - } - - _previousLastCompletionIndex = null; - return; - } - - int bufferLength = _lastCompletionBuffer.Count; - if (_lastKnownHintsClickable != makeHintsClickable) - { - _autoCompleteContainer.Clear(); - _lastKnownHintsClickable = makeHintsClickable; - } - - int currentChildCount = _autoCompleteContainer.childCount; - - bool dirty = _lastCompletionIndex != _previousLastCompletionIndex; - bool contentsChanged = currentChildCount != bufferLength; - if (contentsChanged) - { - dirty = true; - if (currentChildCount < bufferLength) - { - for (int i = currentChildCount; i < bufferLength; ++i) - { - string hint = _lastCompletionBuffer[i]; - VisualElement hintElement; - - if (makeHintsClickable) - { - int currentIndex = i; - string currentHint = hint; - Button hintButton = new(() => - { - _input.CommandText = BuildCompletionText(currentHint); - _lastCompletionIndex = currentIndex; - _needsFocus = true; - }) - { - text = hint, - }; - hintElement = hintButton; - } - else - { - Label hintText = new(hint); - hintElement = hintText; - } - - hintElement.name = $"SuggestionText{i}"; - _autoCompleteContainer.Add(hintElement); - - bool isSelected = i == _lastCompletionIndex; - hintElement.AddToClassList("terminal-button"); - hintElement.EnableInClassList("autocomplete-item-selected", isSelected); - hintElement.EnableInClassList("autocomplete-item", !isSelected); - } - } - else if (bufferLength < currentChildCount) - { - for (int i = currentChildCount - 1; bufferLength <= i; --i) - { - _autoCompleteContainer.RemoveAt(i); - } - } - } - - bool shouldUpdateCompletionIndex = false; - try - { - shouldUpdateCompletionIndex = _autoCompleteContainer.childCount == bufferLength; - if (shouldUpdateCompletionIndex) - { - UpdateAutoCompleteView(); - } - - if (dirty) - { - for (int i = 0; i < _autoCompleteContainer.childCount && i < bufferLength; ++i) - { - VisualElement hintElement = _autoCompleteContainer[i]; - switch (hintElement) - { - case Button button: - button.text = _lastCompletionBuffer[i]; - break; - case Label label: - label.text = _lastCompletionBuffer[i]; - break; - case TextField textField: - textField.value = _lastCompletionBuffer[i]; - break; - } - - bool isSelected = i == _lastCompletionIndex; - - hintElement.EnableInClassList("autocomplete-item-selected", isSelected); - hintElement.EnableInClassList("autocomplete-item", !isSelected); - } - } - } - finally - { - if (shouldUpdateCompletionIndex) - { - _previousLastCompletionIndex = _lastCompletionIndex; - } - } - } - - private void UpdateAutoCompleteView() - { - if (_lastCompletionIndex == null) - { - return; - } - - if (_autoCompleteContainer?.contentContainer == null) - { - return; - } - - int childCount = _autoCompleteContainer.childCount; - if (childCount == 0) - { - return; - } - - if (childCount <= _lastCompletionIndex) - { - _lastCompletionIndex = - (_lastCompletionIndex % childCount + childCount) % childCount; - } - - if (_previousLastCompletionIndex == _lastCompletionIndex) - { - return; - } - - VisualElement current = _autoCompleteContainer[_lastCompletionIndex.Value]; - float viewportWidth = _autoCompleteContainer.contentViewport.resolvedStyle.width; - - // Use layout properties relative to the content container - float targetElementLeft = current.layout.x; - float targetElementWidth = current.layout.width; - float targetElementRight = targetElementLeft + targetElementWidth; - - const float epsilon = 0.01f; - - bool isFullyVisible = - epsilon <= targetElementLeft && targetElementRight <= viewportWidth + epsilon; - - if (isFullyVisible) - { - return; - } - - bool isIncrementing; - if (_previousLastCompletionIndex == childCount - 1 && _lastCompletionIndex == 0) - { - isIncrementing = true; - } - else if (_previousLastCompletionIndex == 0 && _lastCompletionIndex == childCount - 1) - { - isIncrementing = false; - } - else - { - isIncrementing = _previousLastCompletionIndex < _lastCompletionIndex; - } - - _autoCompleteChildren.Clear(); - for (int i = 0; i < childCount; ++i) - { - _autoCompleteChildren.Add(_autoCompleteContainer[i]); - } - - int shiftAmount; - if (isIncrementing) - { - shiftAmount = -1 * _lastCompletionIndex.Value; - _lastCompletionIndex = 0; - } - else - { - shiftAmount = 0; - float accumulatedWidth = 0; - for (int i = 1; i <= childCount; ++i) - { - shiftAmount++; - int index = -i % childCount; - index = (index + childCount) % childCount; - VisualElement element = _autoCompleteChildren[index]; - accumulatedWidth += - element.resolvedStyle.width - + element.resolvedStyle.marginLeft - + element.resolvedStyle.marginRight - + element.resolvedStyle.borderLeftWidth - + element.resolvedStyle.borderRightWidth; - - if (accumulatedWidth <= viewportWidth) - { - continue; - } - - if (element != current) - { - --shiftAmount; - } - - break; - } - - _lastCompletionIndex = (shiftAmount - 1 + childCount) % childCount; - } - - _autoCompleteChildren.Shift(shiftAmount); - _lastCompletionBuffer.Shift(shiftAmount); - - _autoCompleteContainer.Clear(); - foreach (VisualElement element in _autoCompleteChildren) - { - _autoCompleteContainer.Add(element); - } - - float desiredTop = _currentWindowHeight; - float desiredLeft = 2f; - float desiredWidth = Screen.width; - if (IsLauncherActive && _launcherMetricsInitialized) - { - desiredTop = _launcherMetrics.Top + _currentWindowHeight + 12f; - desiredLeft = _launcherMetrics.Left; - desiredWidth = _launcherMetrics.Width; - } - - _stateButtonContainer.style.top = desiredTop; - _stateButtonContainer.style.left = desiredLeft; - _stateButtonContainer.style.width = desiredWidth; - _stateButtonContainer.style.display = showGUIButtons - ? DisplayStyle.Flex - : DisplayStyle.None; - _stateButtonContainer.style.justifyContent = - IsLauncherActive && _launcherMetricsInitialized - ? Justify.Center - : Justify.FlexStart; - - Button primaryButton; - Button secondaryButton; - Button launcherButton; - EnsureButtons(out primaryButton, out secondaryButton, out launcherButton); - - DisplayStyle buttonDisplay = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; - - UpdateButton(primaryButton, GetPrimaryLabel(), _state == TerminalState.OpenSmall); - UpdateButton(secondaryButton, GetSecondaryLabel(), _state == TerminalState.OpenFull); - UpdateButton(launcherButton, launcherButtonText, IsLauncherActive); - - return; - - void EnsureButtons(out Button primary, out Button secondary, out Button launcher) - { - while (_stateButtonContainer.childCount < 3) - { - int index = _stateButtonContainer.childCount; - Button button = index switch - { - 0 => new Button(FirstClicked) { name = "StateButton1" }, - 1 => new Button(SecondClicked) { name = "StateButton2" }, - _ => new Button(LauncherClicked) { name = "StateButton3" }, - }; - button.AddToClassList("terminal-button"); - _stateButtonContainer.Add(button); - } - - primary = _stateButtonContainer[0] as Button; - secondary = _stateButtonContainer[1] as Button; - launcher = _stateButtonContainer[2] as Button; - } - - string GetPrimaryLabel() - { - return _state switch - { - TerminalState.Closed => smallButtonText, - TerminalState.OpenSmall => closeButtonText, - TerminalState.OpenFull => closeButtonText, - TerminalState.OpenLauncher => closeButtonText, - _ => string.Empty, - }; - } - - string GetSecondaryLabel() - { - return _state switch - { - TerminalState.Closed => fullButtonText, - TerminalState.OpenSmall => fullButtonText, - TerminalState.OpenFull => smallButtonText, - TerminalState.OpenLauncher => fullButtonText, - _ => string.Empty, - }; - } - - void UpdateButton(Button button, string text, bool isActive) - { - if (button == null) - { - return; - } - - bool shouldShow = - buttonDisplay == DisplayStyle.Flex && !string.IsNullOrWhiteSpace(text); - button.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None; - if (shouldShow) - { - button.text = text; - } - button.EnableInClassList("terminal-button-active", shouldShow && isActive); - } - - void FirstClicked() - { - switch (_state) - { - case TerminalState.Closed: - ToggleSmall(); - break; - case TerminalState.OpenSmall: - case TerminalState.OpenFull: - case TerminalState.OpenLauncher: - Close(); - break; - } - } - - void SecondClicked() - { - switch (_state) - { - case TerminalState.Closed: - case TerminalState.OpenSmall: - case TerminalState.OpenLauncher: - ToggleFull(); - break; - case TerminalState.OpenFull: - ToggleSmall(); - break; - } - } - - void LauncherClicked() - { - ToggleLauncher(); - } - } - - private static void EnsureChildOrder( - VisualElement parent, - params VisualElement[] orderedChildren - ) - { - if (parent == null) - { - return; - } - - int insertIndex = 0; - foreach (VisualElement child in orderedChildren) - { - if (child == null || child.parent != parent) - { - continue; - } - - int currentIndex = parent.IndexOf(child); - if (currentIndex != insertIndex) - { - parent.Remove(child); - parent.Insert(insertIndex, child); - } - - insertIndex++; - } - } - public Font SetRandomFont(bool persist = false) { if (_fontPack == null) diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index 6272e97..c2614e3 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -377,11 +377,22 @@ public IEnumerator FontCommandsHandleMissingFonts() Assert.IsTrue(shell.RunCommand("set-random-font")); Terminal.Buffer?.DrainPending(); - LogItem randomLog = GetLastLog( - Terminal.Buffer, - item => item.type == TerminalLogType.Warning + bool hasNoFontsWarning = Terminal + .Buffer + .Logs + .Any( + item => + !string.IsNullOrEmpty(item.message) + && item.message.IndexOf( + "No fonts available", + StringComparison.OrdinalIgnoreCase + ) >= 0 + ); + + Assert.IsTrue( + hasNoFontsWarning, + $"Expected 'No fonts available' warning. Logs: {string.Join(" | ", Terminal.Buffer.Logs.Select(log => log.message))}" ); - StringAssert.Contains("No fonts available", randomLog.message); yield break; } From 2c4a696e1884be76d50d15c7653e8b230994089e Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 10:09:05 -0700 Subject: [PATCH 29/69] Progress --- AssemblyInfo.cs | 5 + AssemblyInfo.cs.meta | 3 + Runtime/AssemblyInfo.cs | 2 + .../Input/ITerminalInputTarget.cs | 23 + .../Input/ITerminalInputTarget.cs.meta | 3 + .../Input/TerminalKeyboardController.cs | 95 +- .../UI/TerminalUI.AutoCompleteView.cs | 334 ++++++- .../UI/TerminalUI.AutoCompleteView.cs.meta | 11 + .../UI/TerminalUI.LayoutView.cs | 659 +++++++++++++ .../UI/TerminalUI.LayoutView.cs.meta | 3 + .../CommandTerminal/UI/TerminalUI.LogView.cs | 119 --- Runtime/CommandTerminal/UI/TerminalUI.cs | 884 +----------------- .../SerializedPropertyExtensions.cs | 24 + Tests/Runtime/BuiltinCommandTests.cs | 24 + .../TerminalKeyboardControllerTests.cs | 120 +++ .../TerminalKeyboardControllerTests.cs.meta | 3 + 16 files changed, 1288 insertions(+), 1024 deletions(-) create mode 100644 AssemblyInfo.cs create mode 100644 AssemblyInfo.cs.meta create mode 100644 Runtime/CommandTerminal/Input/ITerminalInputTarget.cs create mode 100644 Runtime/CommandTerminal/Input/ITerminalInputTarget.cs.meta create mode 100644 Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs.meta create mode 100644 Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs create mode 100644 Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs.meta create mode 100644 Tests/Runtime/TerminalKeyboardControllerTests.cs create mode 100644 Tests/Runtime/TerminalKeyboardControllerTests.cs.meta diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..7cd60f0 --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Editor")] +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests.Runtime")] +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests")] diff --git a/AssemblyInfo.cs.meta b/AssemblyInfo.cs.meta new file mode 100644 index 0000000..572154c --- /dev/null +++ b/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 584828a4e116479aa35e297ecb0599e7 +timeCreated: 1760547381 \ No newline at end of file diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs index e852899..eaf478c 100644 --- a/Runtime/AssemblyInfo.cs +++ b/Runtime/AssemblyInfo.cs @@ -1,3 +1,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests.Runtime")] +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Editor")] +[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests")] diff --git a/Runtime/CommandTerminal/Input/ITerminalInputTarget.cs b/Runtime/CommandTerminal/Input/ITerminalInputTarget.cs new file mode 100644 index 0000000..dc438ab --- /dev/null +++ b/Runtime/CommandTerminal/Input/ITerminalInputTarget.cs @@ -0,0 +1,23 @@ +namespace WallstopStudios.DxCommandTerminal.Input +{ + public interface ITerminalInputTarget + { + bool IsClosed { get; } + + void Close(); + + void ToggleSmall(); + + void ToggleFull(); + + void ToggleLauncher(); + + void EnterCommand(); + + void CompleteCommand(bool searchForward); + + void HandlePrevious(); + + void HandleNext(); + } +} diff --git a/Runtime/CommandTerminal/Input/ITerminalInputTarget.cs.meta b/Runtime/CommandTerminal/Input/ITerminalInputTarget.cs.meta new file mode 100644 index 0000000..c30c30c --- /dev/null +++ b/Runtime/CommandTerminal/Input/ITerminalInputTarget.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5e80eb048afb424db4d565879a194ebb +timeCreated: 0 diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index 39b6951..9e67983 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -12,6 +12,8 @@ public class TerminalKeyboardController : MonoBehaviour, IInputHandler protected readonly HashSet _missing = new(); protected readonly HashSet _terminalControlTypes = new(); + protected ITerminalInputTarget _inputTarget; + private bool _missingTargetLogged; private static TerminalControlTypes[] BuildControlTypes() { @@ -89,15 +91,7 @@ public TerminalKeyboardController() { } protected virtual void Awake() { - if (terminal != null) - { - return; - } - - if (!TryGetComponent(out terminal)) - { - Debug.LogError("Failed to find TerminalUI, Input will not work.", this); - } + ResolveInputTarget(); if (_controlOrder is not { Count: > 0 }) { @@ -113,6 +107,7 @@ protected virtual void OnValidate() { if (!Application.isPlaying) { + ResolveInputTarget(); VerifyControlOrderIntegrity(); } } @@ -163,6 +158,15 @@ private void VerifyControlOrderIntegrity() protected virtual void Update() { + if (_inputTarget == null) + { + ResolveInputTarget(); + if (_inputTarget == null) + { + return; + } + } + if (_controlOrder is not { Count: > 0 }) { return; @@ -184,85 +188,85 @@ protected virtual void Update() protected virtual void Close() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.Close(); + _inputTarget.Close(); } protected virtual void EnterCommand() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.EnterCommand(); + _inputTarget.EnterCommand(); } protected virtual void Previous() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.HandlePrevious(); + _inputTarget.HandlePrevious(); } protected virtual void Next() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.HandleNext(); + _inputTarget.HandleNext(); } protected virtual void ToggleFull() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.ToggleFull(); + _inputTarget.ToggleFull(); } protected virtual void ToggleLauncher() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.ToggleLauncher(); + _inputTarget.ToggleLauncher(); } protected virtual void ToggleSmall() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.ToggleSmall(); + _inputTarget.ToggleSmall(); } protected virtual void Complete() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.CompleteCommand(searchForward: true); + _inputTarget.CompleteCommand(searchForward: true); } protected virtual void CompleteBackward() { - if (terminal == null) + if (_inputTarget == null) { return; } - terminal.CompleteCommand(searchForward: false); + _inputTarget.CompleteCommand(searchForward: false); } #endregion @@ -357,7 +361,7 @@ private bool IsControlPressed(TerminalControlTypes controlType) } } - private void ExecuteControl(TerminalControlTypes controlType) + protected virtual void ExecuteControl(TerminalControlTypes controlType) { switch (controlType) { @@ -390,5 +394,40 @@ private void ExecuteControl(TerminalControlTypes controlType) break; } } + + private void ResolveInputTarget() + { + if (terminal != null) + { + _inputTarget = terminal; + _missingTargetLogged = false; + return; + } + + if (TryGetComponent(out ITerminalInputTarget resolvedTarget)) + { + _inputTarget = resolvedTarget; + terminal = resolvedTarget as TerminalUI; + _missingTargetLogged = false; + } + else + { + if (!_missingTargetLogged) + { + Debug.LogError( + "Failed to locate a terminal input target. Input will not work.", + this + ); + _missingTargetLogged = true; + } + } + } + +#if UNITY_EDITOR || UNITY_INCLUDE_TESTS + internal void ExecuteControlForTests(TerminalControlTypes controlType) + { + ExecuteControl(controlType); + } +#endif } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs index 46343f0..0272d43 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs @@ -8,5 +8,337 @@ namespace WallstopStudios.DxCommandTerminal.UI public sealed partial class TerminalUI { - } + private void RefreshAutoCompleteHints() + { + bool shouldDisplay = + 0 < _lastCompletionBuffer.Count + && hintDisplayMode is HintDisplayMode.Always or HintDisplayMode.AutoCompleteOnly + && _autoCompleteContainer != null; + + if (!shouldDisplay) + { + if (0 < _autoCompleteContainer?.childCount) + { + _autoCompleteContainer.Clear(); + } + + _previousLastCompletionIndex = null; + return; + } + + int bufferLength = _lastCompletionBuffer.Count; + if (_lastKnownHintsClickable != makeHintsClickable) + { + _autoCompleteContainer.Clear(); + _lastKnownHintsClickable = makeHintsClickable; + } + + int currentChildCount = _autoCompleteContainer.childCount; + + bool dirty = _lastCompletionIndex != _previousLastCompletionIndex; + bool contentsChanged = currentChildCount != bufferLength; + if (contentsChanged) + { + dirty = true; + if (currentChildCount < bufferLength) + { + for (int i = currentChildCount; i < bufferLength; ++i) + { + string hint = _lastCompletionBuffer[i]; + VisualElement hintElement; + + if (makeHintsClickable) + { + int currentIndex = i; + string currentHint = hint; + Button hintButton = new(() => + { + _input.CommandText = BuildCompletionText(currentHint); + _lastCompletionIndex = currentIndex; + _needsFocus = true; + }) + { + text = hint, + }; + hintElement = hintButton; + } + else + { + Label hintText = new(hint); + hintElement = hintText; + } + + hintElement.name = $"SuggestionText{i}"; + _autoCompleteContainer.Add(hintElement); + + bool isSelected = i == _lastCompletionIndex; + hintElement.AddToClassList("terminal-button"); + hintElement.EnableInClassList("autocomplete-item-selected", isSelected); + hintElement.EnableInClassList("autocomplete-item", !isSelected); + } + } + else if (bufferLength < currentChildCount) + { + for (int i = currentChildCount - 1; bufferLength <= i; --i) + { + _autoCompleteContainer.RemoveAt(i); + } + } + } + + bool shouldUpdateCompletionIndex = false; + try + { + shouldUpdateCompletionIndex = _autoCompleteContainer.childCount == bufferLength; + if (shouldUpdateCompletionIndex) + { + UpdateAutoCompleteView(); + } + + if (dirty) + { + for (int i = 0; i < _autoCompleteContainer.childCount && i < bufferLength; ++i) + { + VisualElement hintElement = _autoCompleteContainer[i]; + switch (hintElement) + { + case Button button: + button.text = _lastCompletionBuffer[i]; + break; + case Label label: + label.text = _lastCompletionBuffer[i]; + break; + case TextField textField: + textField.value = _lastCompletionBuffer[i]; + break; + } + + bool isSelected = i == _lastCompletionIndex; + + hintElement.EnableInClassList("autocomplete-item-selected", isSelected); + hintElement.EnableInClassList("autocomplete-item", !isSelected); + } + } + } + finally + { + if (shouldUpdateCompletionIndex) + { + _previousLastCompletionIndex = _lastCompletionIndex; + } + } + } + + private void ResetAutoComplete() + { + _lastKnownCommandText = _input.CommandText ?? string.Empty; + _lastCompletionAnchorText = null; + _lastCompletionAnchorCaretIndex = null; + CommandAutoComplete autoComplete = ActiveAutoComplete; + if (autoComplete == null) + { + _lastCompletionIndex = null; + _previousLastCompletionIndex = null; + _lastCompletionBuffer.Clear(); + _lastCompletionBufferTempCache.Clear(); + _lastCompletionBufferTempSet.Clear(); + return; + } + + if (hintDisplayMode == HintDisplayMode.Always) + { + _lastCompletionBufferTempCache.Clear(); + int caret = + _commandInput != null + ? _commandInput.cursorIndex + : (_lastKnownCommandText?.Length ?? 0); + autoComplete.Complete( + _lastKnownCommandText, + caret, + _lastCompletionBufferTempCache + ); + bool equivalent = + _lastCompletionBufferTempCache.Count == _lastCompletionBuffer.Count; + if (equivalent) + { + _lastCompletionBufferTempSet.Clear(); + foreach (string completion in _lastCompletionBuffer) + { + _lastCompletionBufferTempSet.Add(completion); + } + + foreach (string completion in _lastCompletionBufferTempCache) + { + if (!_lastCompletionBufferTempSet.Contains(completion)) + { + equivalent = false; + break; + } + } + } + + if (!equivalent) + { + _lastCompletionIndex = null; + _previousLastCompletionIndex = null; + _lastCompletionBuffer.Clear(); + foreach (string completion in _lastCompletionBufferTempCache) + { + _lastCompletionBuffer.Add(completion); + } + } + } + else + { + _lastCompletionIndex = null; + _previousLastCompletionIndex = null; + _lastCompletionBuffer.Clear(); + } + } + + private string BuildCompletionText(string suggestion) + { + if (string.IsNullOrEmpty(suggestion)) + { + return suggestion ?? string.Empty; + } + + CommandAutoComplete autoComplete = ActiveAutoComplete; + if (autoComplete == null || !autoComplete.LastCompletionUsedCompleter) + { + return suggestion; + } + + string prefix = autoComplete.LastCompleterPrefix ?? string.Empty; + return string.Concat(prefix, suggestion); + } + + public void CompleteCommand(bool searchForward = true) + { + if (_state == TerminalState.Closed) + { + return; + } + + try + { + CommandAutoComplete autoComplete = ActiveAutoComplete; + if (autoComplete == null) + { + return; + } + + _lastKnownCommandText = _input.CommandText ?? string.Empty; + _lastCompletionBufferTempCache.Clear(); + int caret = + _commandInput != null + ? _commandInput.cursorIndex + : (_lastKnownCommandText?.Length ?? 0); + + string completionSource = _lastCompletionAnchorText ?? _lastKnownCommandText; + int completionCaret = _lastCompletionAnchorCaretIndex ?? caret; + + autoComplete.Complete( + completionSource, + completionCaret, + _lastCompletionBufferTempCache + ); + + bool equivalentBuffers = true; + try + { + int completionLength = _lastCompletionBufferTempCache.Count; + equivalentBuffers = _lastCompletionBuffer.Count == completionLength; + if (equivalentBuffers) + { + _lastCompletionBufferTempSet.Clear(); + foreach (string item in _lastCompletionBuffer) + { + _lastCompletionBufferTempSet.Add(item); + } + + foreach (string newCompletionItem in _lastCompletionBufferTempCache) + { + if (!_lastCompletionBufferTempSet.Contains(newCompletionItem)) + { + equivalentBuffers = false; + break; + } + } + } + + if (equivalentBuffers) + { + if (completionLength > 0) + { + if (_lastCompletionIndex == null) + { + _lastCompletionIndex = 0; + } + else if (searchForward) + { + _lastCompletionIndex = + (_lastCompletionIndex + 1) % completionLength; + } + else + { + _lastCompletionIndex = + (_lastCompletionIndex - 1 + completionLength) + % completionLength; + } + + string selection = _lastCompletionBuffer[_lastCompletionIndex.Value]; + _input.CommandText = BuildCompletionText(selection); + if (_lastCompletionAnchorText == null) + { + _lastCompletionAnchorText = completionSource; + _lastCompletionAnchorCaretIndex = completionCaret; + } + } + else + { + _lastCompletionIndex = null; + _lastCompletionAnchorText = null; + _lastCompletionAnchorCaretIndex = null; + } + } + else + { + if (completionLength > 0) + { + _lastCompletionIndex = 0; + string selection = _lastCompletionBufferTempCache[0]; + _input.CommandText = BuildCompletionText(selection); + _lastCompletionAnchorText = completionSource; + _lastCompletionAnchorCaretIndex = completionCaret; + } + else + { + _lastCompletionIndex = null; + _lastCompletionAnchorText = null; + _lastCompletionAnchorCaretIndex = null; + } + } + } + finally + { + if (!equivalentBuffers) + { + _lastCompletionBuffer.Clear(); + foreach (string item in _lastCompletionBufferTempCache) + { + _lastCompletionBuffer.Add(item); + } + _previousLastCompletionIndex = null; + } + + _previousLastCompletionIndex ??= _lastCompletionIndex; + } + } + finally + { + _needsFocus = true; + } + } + } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs.meta b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs.meta new file mode 100644 index 0000000..277a464 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3e1da940123d02be83ec6262bb05f50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs new file mode 100644 index 0000000..f61b195 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs @@ -0,0 +1,659 @@ +namespace WallstopStudios.DxCommandTerminal.UI +{ + using System; + using System.Collections.Generic; + using UnityEngine; + using UnityEngine.UIElements; + + public sealed partial class TerminalUI + { + private void RefreshUI() + { + if (_terminalContainer == null) + { + return; + } + + if (_commandIssuedThisFrame) + { + return; + } + + float screenWidth = Screen.width; + float screenHeight = Screen.height; + + if (IsLauncherActive && _launcherMetricsInitialized) + { + ApplyLauncherLayout(screenWidth, screenHeight); + UpdateLauncherLayoutMetrics(); + } + else + { + ApplyStandardLayout(screenWidth); + } + + _terminalContainer.style.height = _currentWindowHeight; + + DisplayStyle commandInputStyle = + !IsLauncherActive && _currentWindowHeight <= 30 + ? DisplayStyle.None + : DisplayStyle.Flex; + + _needsFocus |= + _inputContainer.resolvedStyle.display != commandInputStyle + && commandInputStyle == DisplayStyle.Flex; + _inputContainer.style.display = commandInputStyle; + + RefreshLogs(); + RefreshAutoCompleteHints(); + + string commandInput = _input.CommandText; + if (!string.Equals(_commandInput.value, commandInput)) + { + _isCommandFromCode = true; + _commandInput.value = commandInput; + } + else if ( + _needsFocus + && _textInput.focusable + && _textInput.resolvedStyle.display != DisplayStyle.None + && _commandInput.resolvedStyle.display != DisplayStyle.None + ) + { + if (_textInput.focusController.focusedElement != _textInput) + { + _textInput.schedule.Execute(_focusInput).ExecuteLater(0); + FocusInput(); + } + + _needsFocus = false; + } + else if ( + _needsScrollToEnd + && _logScrollView != null + && _logScrollView.style.display != DisplayStyle.None + && !IsLauncherActive + ) + { + ScrollToEnd(); + _needsScrollToEnd = false; + } + + RefreshStateButtons(); + } + private void FocusInput() + { + if (_textInput == null) + { + return; + } + + _textInput.Focus(); + int textEndPosition = _commandInput.value.Length; + _commandInput.cursorIndex = textEndPosition; + _commandInput.selectIndex = textEndPosition; + } + private void ResetWindowIdempotent() + { + int height = Screen.height; + int width = Screen.width; + float oldTargetHeight = _targetWindowHeight; + bool wasLauncher = _launcherMetricsInitialized; + if (_state != TerminalState.OpenLauncher) + { + _launcherSuggestionContentHeight = 0f; + _launcherHistoryContentHeight = 0f; + } + try + { + switch (_state) + { + case TerminalState.OpenSmall: + { + _launcherMetricsInitialized = false; + _realWindowHeight = height * maxHeight * smallTerminalRatio; + _targetWindowHeight = _realWindowHeight; + break; + } + case TerminalState.OpenFull: + { + _launcherMetricsInitialized = false; + _realWindowHeight = height * maxHeight; + _targetWindowHeight = _realWindowHeight; + break; + } + case TerminalState.OpenLauncher: + { + LauncherLayoutMetrics computedMetrics = _launcherSettings.ComputeMetrics( + width, + height + ); + float reservedEstimate = Mathf.Max( + _launcherSettings.inputReservePixels, + 48f + ); + float estimatedMinimumHeight = + (computedMetrics.InsetPadding * 2f) + reservedEstimate; + _launcherMetrics = computedMetrics; + _launcherMetricsInitialized = true; + _launcherSuggestionContentHeight = 0f; + _launcherHistoryContentHeight = 0f; + _realWindowHeight = _launcherMetrics.Height; + if (!wasLauncher) + { + _targetWindowHeight = Mathf.Clamp( + estimatedMinimumHeight, + 0f, + _launcherMetrics.Height + ); + } + else + { + _targetWindowHeight = Mathf.Clamp( + _targetWindowHeight, + 0f, + _launcherMetrics.Height + ); + } + break; + } + default: + { + _launcherMetricsInitialized = false; + _realWindowHeight = height * maxHeight * smallTerminalRatio; + _targetWindowHeight = 0; + break; + } + } + } + finally + { + if (!Mathf.Approximately(oldTargetHeight, _targetWindowHeight)) + { + bool snapHeight = + (_launcherMetricsInitialized || wasLauncher) && _launcherMetrics.SnapOpen; + if (snapHeight) + { + _currentWindowHeight = _targetWindowHeight; + _isAnimating = false; + } + else + { + StartHeightAnimation(); + } + } + } + } + private void ApplyLauncherLayout(float screenWidth, float screenHeight) + { + VisualElement rootElement = _uiDocument.rootVisualElement; + rootElement.style.width = screenWidth; + rootElement.style.height = screenHeight; + + _terminalContainer.EnableInClassList("terminal-container--launcher", true); + _terminalContainer.style.width = _launcherMetrics.Width; + _terminalContainer.style.height = _currentWindowHeight; + _terminalContainer.style.left = _launcherMetrics.Left; + _terminalContainer.style.top = _launcherMetrics.Top; + _terminalContainer.style.position = Position.Absolute; + _terminalContainer.style.justifyContent = Justify.FlexStart; + _terminalContainer.style.alignItems = Align.Stretch; + _terminalContainer.style.flexDirection = FlexDirection.Column; + + float horizontalPadding = _launcherMetrics.InsetPadding; + float verticalPadding = Mathf.Max(4f, _launcherMetrics.InsetPadding * 0.5f); + _terminalContainer.style.paddingLeft = horizontalPadding; + _terminalContainer.style.paddingRight = horizontalPadding; + _terminalContainer.style.paddingTop = verticalPadding; + _terminalContainer.style.paddingBottom = verticalPadding; + _terminalContainer.style.marginLeft = 0; + _terminalContainer.style.marginRight = 0; + _terminalContainer.style.marginTop = 0; + _terminalContainer.style.marginBottom = 0; + + float cornerRadius = _launcherMetrics.CornerRadius; + _terminalContainer.style.borderTopLeftRadius = cornerRadius; + _terminalContainer.style.borderTopRightRadius = cornerRadius; + _terminalContainer.style.borderBottomLeftRadius = cornerRadius; + _terminalContainer.style.borderBottomRightRadius = cornerRadius; + _terminalContainer.style.overflow = Overflow.Visible; + + _inputContainer.style.marginBottom = 0; + _autoCompleteContainer.style.marginBottom = 0; + + if (_launcherMetrics.HistoryHeight > 0f) + { + _logScrollView.style.display = DisplayStyle.Flex; + _logScrollView.style.height = _launcherMetrics.HistoryHeight; + _logScrollView.style.maxHeight = _launcherMetrics.HistoryHeight; + _logScrollView.style.minHeight = 0; + _logScrollView.style.marginTop = Mathf.Max(6f, verticalPadding * 0.35f); + } + else + { + _logScrollView.style.display = DisplayStyle.None; + _logScrollView.style.height = 0; + _logScrollView.style.maxHeight = 0; + _launcherHistoryContentHeight = 0f; + _logScrollView.style.marginTop = 0; + } + + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + + _autoCompleteContainer.style.position = Position.Relative; + _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.top = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.width = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.maxHeight = _launcherMetrics.HistoryHeight; + _autoCompleteContainer.style.display = DisplayStyle.None; + _autoCompleteContainer.style.marginTop = 0; + _autoCompleteContainer.style.marginBottom = 0; + _autoCompleteContainer.style.marginLeft = 0; + _autoCompleteContainer.style.marginRight = 0; + _autoCompleteContainer.style.flexGrow = 0; + _autoCompleteContainer.style.flexShrink = 0; + + EnsureChildOrder( + _terminalContainer, + _inputContainer, + _autoCompleteContainer, + _logScrollView + ); + } + private void ApplyStandardLayout(float screenWidth) + { + VisualElement rootElement = _uiDocument.rootVisualElement; + rootElement.style.width = screenWidth; + rootElement.style.height = _currentWindowHeight; + + _terminalContainer.EnableInClassList("terminal-container--launcher", false); + _terminalContainer.style.width = screenWidth; + _terminalContainer.style.height = _currentWindowHeight; + _terminalContainer.style.left = 0; + _terminalContainer.style.top = 0; + _terminalContainer.style.position = Position.Relative; + _terminalContainer.style.paddingLeft = 0; + _terminalContainer.style.paddingRight = 0; + _terminalContainer.style.paddingTop = 0; + _terminalContainer.style.paddingBottom = 0; + _terminalContainer.style.marginLeft = 0; + _terminalContainer.style.marginRight = 0; + _terminalContainer.style.marginTop = 0; + _terminalContainer.style.marginBottom = 0; + _terminalContainer.style.borderTopLeftRadius = 0; + _terminalContainer.style.borderTopRightRadius = 0; + _terminalContainer.style.borderBottomLeftRadius = 0; + _terminalContainer.style.borderBottomRightRadius = 0; + _terminalContainer.style.justifyContent = Justify.FlexStart; + _terminalContainer.style.alignItems = Align.Stretch; + + _logScrollView.style.marginTop = 0; + _logScrollView.style.height = new StyleLength(StyleKeyword.Null); + _logScrollView.style.maxHeight = new StyleLength(StyleKeyword.Null); + _logScrollView.style.minHeight = new StyleLength(StyleKeyword.Null); + _logScrollView.style.display = DisplayStyle.Flex; + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + + _autoCompleteContainer.style.position = Position.Relative; + _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.top = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.width = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.maxHeight = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.maxWidth = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.height = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.minHeight = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.marginBottom = 0; + _autoCompleteContainer.style.marginTop = 0; + _autoCompleteContainer.style.marginLeft = 0; + _autoCompleteContainer.style.marginRight = 0; + _autoCompleteContainer.style.flexGrow = StyleKeyword.Null; + _autoCompleteContainer.style.flexShrink = StyleKeyword.Null; + _autoCompleteContainer.style.alignSelf = StyleKeyword.Null; + _inputContainer.style.marginBottom = 0; + + EnsureChildOrder( + _terminalContainer, + _logScrollView, + _autoCompleteContainer, + _inputContainer + ); + } + private void UpdateLauncherLayoutMetrics() + { + if (!IsLauncherActive || !_launcherMetricsInitialized) + { + return; + } + + float horizontalPadding = _launcherMetrics.InsetPadding; + float verticalPadding = Mathf.Max(4f, horizontalPadding * 0.5f); + float inputHeight = Mathf.Max(_inputContainer.resolvedStyle.height, 0f); + if (inputHeight <= 0f) + { + inputHeight = LauncherInputFallbackHeight; + } + float availableWidth = Mathf.Max(0f, _launcherMetrics.Width - (horizontalPadding * 2f)); + _autoCompleteContainer.style.width = availableWidth; + _autoCompleteContainer.style.maxWidth = availableWidth; + _autoCompleteContainer.style.alignSelf = Align.Stretch; + _autoCompleteContainer.style.flexGrow = 0; + _autoCompleteContainer.style.flexShrink = 0; + _autoCompleteContainer.style.minHeight = 0f; + + bool hasSuggestions = false; + int suggestionChildCount = _autoCompleteContainer.contentContainer.childCount; + for (int i = 0; i < suggestionChildCount; ++i) + { + VisualElement suggestion = _autoCompleteContainer.contentContainer[i]; + if (suggestion == null) + { + continue; + } + + if (suggestion.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + + hasSuggestions = true; + } + if (!hasSuggestions && suggestionChildCount > 0) + { + hasSuggestions = true; + } + if (hasSuggestions) + { + _autoCompleteContainer.style.display = DisplayStyle.Flex; + _autoCompleteContainer.style.marginTop = LauncherAutoCompleteSpacing; + } + else + { + _autoCompleteContainer.style.display = DisplayStyle.None; + _autoCompleteContainer.style.height = 0; + if (_autoCompleteViewport != null) + { + _autoCompleteViewport.style.height = 0; + } + _autoCompleteContainer.style.marginTop = 0; + _launcherSuggestionContentHeight = 0f; + } + + float effectiveSuggestionHeight = _launcherSuggestionContentHeight; + if (effectiveSuggestionHeight <= 0f && hasSuggestions) + { + effectiveSuggestionHeight = LauncherEstimatedSuggestionRowHeight; + } + + float suggestionsHeight = hasSuggestions + ? Mathf.Clamp(effectiveSuggestionHeight, 0f, _launcherMetrics.HistoryHeight) + : 0f; + if (hasSuggestions) + { + _autoCompleteContainer.style.height = Mathf.Max(0f, suggestionsHeight); + if (_autoCompleteViewport != null) + { + _autoCompleteViewport.style.height = Mathf.Max(0f, suggestionsHeight); + } + } + _autoCompleteContainer.style.marginBottom = 0; + + VisualElement historyContent = _logScrollView.contentContainer; + int visibleHistoryCount = 0; + int historyChildCount = historyContent.childCount; + for (int i = 0; i < historyChildCount; ++i) + { + VisualElement entry = historyContent[i]; + if (entry == null || entry.resolvedStyle.display == DisplayStyle.None) + { + continue; + } + visibleHistoryCount++; + } + + if (visibleHistoryCount == 0) + { + int pendingLogs = ActiveHistory?.Count ?? 0; + visibleHistoryCount = Mathf.Min( + pendingLogs, + _launcherMetrics.HistoryVisibleEntryCount + ); + if (visibleHistoryCount == 0) + { + _launcherHistoryContentHeight = 0f; + } + } + + bool hasHistory = visibleHistoryCount > 0; + + float spacingAboveLog = 0f; + if (hasHistory) + { + spacingAboveLog = hasSuggestions + ? LauncherAutoCompleteSpacing + : Mathf.Max(LauncherAutoCompleteSpacing, verticalPadding * 0.25f); + } + else if (hasSuggestions) + { + spacingAboveLog = LauncherAutoCompleteSpacing; + } + + float reservedForSuggestions = hasSuggestions + ? suggestionsHeight + spacingAboveLog + : spacingAboveLog; + + float historyHeightFromContent = hasHistory ? _launcherHistoryContentHeight : 0f; + if (float.IsNaN(historyHeightFromContent) || historyHeightFromContent < 0f) + { + historyHeightFromContent = 0f; + } + + float estimatedHistoryHeight = hasHistory + ? visibleHistoryCount * LauncherEstimatedHistoryRowHeight + : 0f; + + float desiredHistoryHeight = hasHistory + ? Mathf.Min( + Mathf.Max(historyHeightFromContent, estimatedHistoryHeight), + _launcherMetrics.HistoryHeight + ) + : 0f; + if (desiredHistoryHeight < 0f) + { + desiredHistoryHeight = 0f; + } + + float minimumHeight = (verticalPadding * 2f) + inputHeight + reservedForSuggestions; + float desiredHeight = minimumHeight + desiredHistoryHeight; + float clampedHeight = Mathf.Clamp( + desiredHeight, + minimumHeight, + _launcherMetrics.Height + ); + + if (!Mathf.Approximately(clampedHeight, _targetWindowHeight)) + { + float previousTarget = _targetWindowHeight; + _targetWindowHeight = clampedHeight; + + if (_launcherMetrics.SnapOpen) + { + _currentWindowHeight = _targetWindowHeight; + _isAnimating = false; + } + else + { + _initialWindowHeight = Mathf.Clamp( + _currentWindowHeight, + minimumHeight, + _launcherMetrics.Height + ); + if (!Mathf.Approximately(previousTarget, _targetWindowHeight)) + { + StartHeightAnimation(); + } + } + } + + float availableForHistory = + _currentWindowHeight + - (verticalPadding * 2f) + - inputHeight + - reservedForSuggestions; + availableForHistory = Mathf.Min(availableForHistory, _launcherMetrics.HistoryHeight); + availableForHistory = Mathf.Max(0f, availableForHistory); + + if (availableForHistory <= 0.01f || _logScrollView.contentContainer.childCount == 0) + { + _logScrollView.style.display = DisplayStyle.None; + _logScrollView.style.height = 0; + _logScrollView.style.maxHeight = 0; + _launcherHistoryContentHeight = 0f; + } + else + { + _logScrollView.style.display = DisplayStyle.Flex; + _logScrollView.style.height = availableForHistory; + _logScrollView.style.maxHeight = availableForHistory; + } + + _logScrollView.style.marginTop = spacingAboveLog; + } + private void RefreshStateButtons() + { + if (_stateButtonContainer == null) + { + return; + } + + float desiredTop = _currentWindowHeight; + float desiredLeft = 2f; + float desiredWidth = Screen.width; + if (IsLauncherActive && _launcherMetricsInitialized) + { + desiredTop = _launcherMetrics.Top + _currentWindowHeight + 12f; + desiredLeft = _launcherMetrics.Left; + desiredWidth = _launcherMetrics.Width; + } + + _stateButtonContainer.style.top = desiredTop; + _stateButtonContainer.style.left = desiredLeft; + _stateButtonContainer.style.width = desiredWidth; + _stateButtonContainer.style.display = showGUIButtons + ? DisplayStyle.Flex + : DisplayStyle.None; + _stateButtonContainer.style.justifyContent = + IsLauncherActive && _launcherMetricsInitialized + ? Justify.Center + : Justify.FlexStart; + + Button primaryButton; + Button secondaryButton; + Button launcherButton; + EnsureButtons(out primaryButton, out secondaryButton, out launcherButton); + + DisplayStyle buttonDisplay = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; + + UpdateButton(primaryButton, GetPrimaryLabel(), _state == TerminalState.OpenSmall); + UpdateButton(secondaryButton, GetSecondaryLabel(), _state == TerminalState.OpenFull); + UpdateButton(launcherButton, launcherButtonText, IsLauncherActive); + + return; + + void EnsureButtons(out Button primary, out Button secondary, out Button launcher) + { + while (_stateButtonContainer.childCount < 3) + { + int index = _stateButtonContainer.childCount; + Button button = index switch + { + 0 => new Button(FirstClicked) { name = "StateButton1" }, + 1 => new Button(SecondClicked) { name = "StateButton2" }, + _ => new Button(LauncherClicked) { name = "StateButton3" }, + }; + button.AddToClassList("terminal-button"); + _stateButtonContainer.Add(button); + } + + primary = _stateButtonContainer[0] as Button; + secondary = _stateButtonContainer[1] as Button; + launcher = _stateButtonContainer[2] as Button; + } + + string GetPrimaryLabel() + { + return _state switch + { + TerminalState.Closed => smallButtonText, + TerminalState.OpenSmall => closeButtonText, + TerminalState.OpenFull => closeButtonText, + TerminalState.OpenLauncher => closeButtonText, + _ => string.Empty, + }; + } + + string GetSecondaryLabel() + { + return _state switch + { + TerminalState.Closed => fullButtonText, + TerminalState.OpenSmall => fullButtonText, + TerminalState.OpenFull => smallButtonText, + TerminalState.OpenLauncher => fullButtonText, + _ => string.Empty, + }; + } + + void UpdateButton(Button button, string text, bool isActive) + { + if (button == null) + { + return; + } + + bool shouldShow = + buttonDisplay == DisplayStyle.Flex && !string.IsNullOrWhiteSpace(text); + button.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None; + if (shouldShow) + { + button.text = text; + } + button.EnableInClassList("terminal-button-active", shouldShow && isActive); + } + + void FirstClicked() + { + switch (_state) + { + case TerminalState.Closed: + ToggleSmall(); + break; + case TerminalState.OpenSmall: + case TerminalState.OpenFull: + case TerminalState.OpenLauncher: + Close(); + break; + } + } + + void SecondClicked() + { + switch (_state) + { + case TerminalState.Closed: + case TerminalState.OpenSmall: + case TerminalState.OpenLauncher: + ToggleFull(); + break; + case TerminalState.OpenFull: + ToggleSmall(); + break; + } + } + + void LauncherClicked() + { + ToggleLauncher(); + } + } + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs.meta b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs.meta new file mode 100644 index 0000000..8fab946 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6ca63669fb214735ab2d125b5158ab75 +timeCreated: 0 diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs index be99f0c..58733ee 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs @@ -476,126 +476,7 @@ private void ScheduleLauncherScroll(float targetValue) .ExecuteLater(0); } - private void RefreshAutoCompleteHints() - { - bool shouldDisplay = - 0 < _lastCompletionBuffer.Count - && hintDisplayMode is HintDisplayMode.Always or HintDisplayMode.AutoCompleteOnly - && _autoCompleteContainer != null; - if (!shouldDisplay) - { - if (0 < _autoCompleteContainer?.childCount) - { - _autoCompleteContainer.Clear(); - } - - _previousLastCompletionIndex = null; - return; - } - - int bufferLength = _lastCompletionBuffer.Count; - if (_lastKnownHintsClickable != makeHintsClickable) - { - _autoCompleteContainer.Clear(); - _lastKnownHintsClickable = makeHintsClickable; - } - - int currentChildCount = _autoCompleteContainer.childCount; - - bool dirty = _lastCompletionIndex != _previousLastCompletionIndex; - bool contentsChanged = currentChildCount != bufferLength; - if (contentsChanged) - { - dirty = true; - if (currentChildCount < bufferLength) - { - for (int i = currentChildCount; i < bufferLength; ++i) - { - string hint = _lastCompletionBuffer[i]; - VisualElement hintElement; - - if (makeHintsClickable) - { - int currentIndex = i; - string currentHint = hint; - Button hintButton = new(() => - { - _input.CommandText = BuildCompletionText(currentHint); - _lastCompletionIndex = currentIndex; - _needsFocus = true; - }) - { - text = hint, - }; - hintElement = hintButton; - } - else - { - Label hintText = new(hint); - hintElement = hintText; - } - - hintElement.name = $"SuggestionText{i}"; - _autoCompleteContainer.Add(hintElement); - - bool isSelected = i == _lastCompletionIndex; - hintElement.AddToClassList("terminal-button"); - hintElement.EnableInClassList("autocomplete-item-selected", isSelected); - hintElement.EnableInClassList("autocomplete-item", !isSelected); - } - } - else if (bufferLength < currentChildCount) - { - for (int i = currentChildCount - 1; bufferLength <= i; --i) - { - _autoCompleteContainer.RemoveAt(i); - } - } - } - - bool shouldUpdateCompletionIndex = false; - try - { - shouldUpdateCompletionIndex = _autoCompleteContainer.childCount == bufferLength; - if (shouldUpdateCompletionIndex) - { - UpdateAutoCompleteView(); - } - - if (dirty) - { - for (int i = 0; i < _autoCompleteContainer.childCount && i < bufferLength; ++i) - { - VisualElement hintElement = _autoCompleteContainer[i]; - switch (hintElement) - { - case Button button: - button.text = _lastCompletionBuffer[i]; - break; - case Label label: - label.text = _lastCompletionBuffer[i]; - break; - case TextField textField: - textField.value = _lastCompletionBuffer[i]; - break; - } - - bool isSelected = i == _lastCompletionIndex; - - hintElement.EnableInClassList("autocomplete-item-selected", isSelected); - hintElement.EnableInClassList("autocomplete-item", !isSelected); - } - } - } - finally - { - if (shouldUpdateCompletionIndex) - { - _previousLastCompletionIndex = _lastCompletionIndex; - } - } - } private void UpdateAutoCompleteView() { diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index aece390..7eb9c3a 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -1,8 +1,3 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Editor")] -[assembly: InternalsVisibleTo("WallstopStudios.DxCommandTerminal.Tests.Runtime")] - namespace WallstopStudios.DxCommandTerminal.UI { using System; @@ -20,7 +15,7 @@ namespace WallstopStudios.DxCommandTerminal.UI using UnityEngine.UIElements; [DisallowMultipleComponent] - public sealed partial class TerminalUI : MonoBehaviour + public sealed partial class TerminalUI : MonoBehaviour, ITerminalInputTarget { private const string TerminalRootName = "TerminalRoot"; private const float LauncherAutoCompleteSpacing = 6f; @@ -337,7 +332,9 @@ private TerminalRuntimeModeFlags ResolveRuntimeModeFlags() private bool TryResolveRuntimeModeFromOptions(out TerminalRuntimeModeFlags resolved) { +#pragma warning disable CS0618 // Type or member is obsolete resolved = TerminalRuntimeModeFlags.None; +#pragma warning restore CS0618 // Type or member is obsolete if (_runtimeModeOptions == null || _runtimeModeOptions.Count == 0) { return false; @@ -584,7 +581,10 @@ private void OnEnable() if (_runtime == null) { - if (!resetStateOnInit && TerminalRuntimeCache.TryAcquire(out TerminalRuntime cachedRuntime)) + if ( + !resetStateOnInit + && TerminalRuntimeCache.TryAcquire(out TerminalRuntime cachedRuntime) + ) { _runtime = cachedRuntime; } @@ -929,165 +929,6 @@ private void ConsumeAndLogErrors() } } - private void ResetAutoComplete() - { - _lastKnownCommandText = _input.CommandText ?? string.Empty; - _lastCompletionAnchorText = null; - _lastCompletionAnchorCaretIndex = null; - CommandAutoComplete autoComplete = ActiveAutoComplete; - if (autoComplete == null) - { - _lastCompletionIndex = null; - _previousLastCompletionIndex = null; - _lastCompletionBuffer.Clear(); - _lastCompletionBufferTempCache.Clear(); - _lastCompletionBufferTempSet.Clear(); - return; - } - - if (hintDisplayMode == HintDisplayMode.Always) - { - _lastCompletionBufferTempCache.Clear(); - int caret = - _commandInput != null - ? _commandInput.cursorIndex - : (_lastKnownCommandText?.Length ?? 0); - autoComplete.Complete( - _lastKnownCommandText, - caret, - _lastCompletionBufferTempCache - ); - bool equivalent = - _lastCompletionBufferTempCache.Count == _lastCompletionBuffer.Count; - if (equivalent) - { - _lastCompletionBufferTempSet.Clear(); - foreach (string completion in _lastCompletionBuffer) - { - _lastCompletionBufferTempSet.Add(completion); - } - - foreach (string completion in _lastCompletionBufferTempCache) - { - if (!_lastCompletionBufferTempSet.Contains(completion)) - { - equivalent = false; - break; - } - } - } - - if (!equivalent) - { - _lastCompletionIndex = null; - _previousLastCompletionIndex = null; - _lastCompletionBuffer.Clear(); - foreach (string completion in _lastCompletionBufferTempCache) - { - _lastCompletionBuffer.Add(completion); - } - } - } - else - { - _lastCompletionIndex = null; - _previousLastCompletionIndex = null; - _lastCompletionBuffer.Clear(); - } - } - - private void ResetWindowIdempotent() - { - int height = Screen.height; - int width = Screen.width; - float oldTargetHeight = _targetWindowHeight; - bool wasLauncher = _launcherMetricsInitialized; - if (_state != TerminalState.OpenLauncher) - { - _launcherSuggestionContentHeight = 0f; - _launcherHistoryContentHeight = 0f; - } - try - { - switch (_state) - { - case TerminalState.OpenSmall: - { - _launcherMetricsInitialized = false; - _realWindowHeight = height * maxHeight * smallTerminalRatio; - _targetWindowHeight = _realWindowHeight; - break; - } - case TerminalState.OpenFull: - { - _launcherMetricsInitialized = false; - _realWindowHeight = height * maxHeight; - _targetWindowHeight = _realWindowHeight; - break; - } - case TerminalState.OpenLauncher: - { - LauncherLayoutMetrics computedMetrics = _launcherSettings.ComputeMetrics( - width, - height - ); - float reservedEstimate = Mathf.Max( - _launcherSettings.inputReservePixels, - 48f - ); - float estimatedMinimumHeight = - (computedMetrics.InsetPadding * 2f) + reservedEstimate; - _launcherMetrics = computedMetrics; - _launcherMetricsInitialized = true; - _launcherSuggestionContentHeight = 0f; - _launcherHistoryContentHeight = 0f; - _realWindowHeight = _launcherMetrics.Height; - if (!wasLauncher) - { - _targetWindowHeight = Mathf.Clamp( - estimatedMinimumHeight, - 0f, - _launcherMetrics.Height - ); - } - else - { - _targetWindowHeight = Mathf.Clamp( - _targetWindowHeight, - 0f, - _launcherMetrics.Height - ); - } - break; - } - default: - { - _launcherMetricsInitialized = false; - _realWindowHeight = height * maxHeight * smallTerminalRatio; - _targetWindowHeight = 0; - break; - } - } - } - finally - { - if (!Mathf.Approximately(oldTargetHeight, _targetWindowHeight)) - { - bool snapHeight = - (_launcherMetricsInitialized || wasLauncher) && _launcherMetrics.SnapOpen; - if (snapHeight) - { - _currentWindowHeight = _targetWindowHeight; - _isAnimating = false; - } - else - { - StartHeightAnimation(); - } - } - } - } - private void SetupUI() { if (disableUIForTests) @@ -1618,230 +1459,6 @@ void OnDraggerMouseLeave(MouseLeaveEvent evt) } } - private void ApplyStandardLayout(float screenWidth) - { - VisualElement rootElement = _uiDocument.rootVisualElement; - rootElement.style.width = screenWidth; - rootElement.style.height = _currentWindowHeight; - - _terminalContainer.EnableInClassList("terminal-container--launcher", false); - _terminalContainer.style.width = screenWidth; - _terminalContainer.style.height = _currentWindowHeight; - _terminalContainer.style.left = 0; - _terminalContainer.style.top = 0; - _terminalContainer.style.position = Position.Relative; - _terminalContainer.style.paddingLeft = 0; - _terminalContainer.style.paddingRight = 0; - _terminalContainer.style.paddingTop = 0; - _terminalContainer.style.paddingBottom = 0; - _terminalContainer.style.marginLeft = 0; - _terminalContainer.style.marginRight = 0; - _terminalContainer.style.marginTop = 0; - _terminalContainer.style.marginBottom = 0; - _terminalContainer.style.borderTopLeftRadius = 0; - _terminalContainer.style.borderTopRightRadius = 0; - _terminalContainer.style.borderBottomLeftRadius = 0; - _terminalContainer.style.borderBottomRightRadius = 0; - _terminalContainer.style.justifyContent = Justify.FlexStart; - _terminalContainer.style.alignItems = Align.Stretch; - - _logScrollView.style.marginTop = 0; - _logScrollView.style.height = new StyleLength(StyleKeyword.Null); - _logScrollView.style.maxHeight = new StyleLength(StyleKeyword.Null); - _logScrollView.style.minHeight = new StyleLength(StyleKeyword.Null); - _logScrollView.style.display = DisplayStyle.Flex; - _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; - - _autoCompleteContainer.style.position = Position.Relative; - _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.top = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.width = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.maxHeight = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.maxWidth = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.height = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.minHeight = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.marginBottom = 0; - _autoCompleteContainer.style.marginTop = 0; - _autoCompleteContainer.style.marginLeft = 0; - _autoCompleteContainer.style.marginRight = 0; - _autoCompleteContainer.style.flexGrow = StyleKeyword.Null; - _autoCompleteContainer.style.flexShrink = StyleKeyword.Null; - _autoCompleteContainer.style.alignSelf = StyleKeyword.Null; - _inputContainer.style.marginBottom = 0; - - EnsureChildOrder( - _terminalContainer, - _logScrollView, - _autoCompleteContainer, - _inputContainer - ); - } - - private void ApplyLauncherLayout(float screenWidth, float screenHeight) - { - VisualElement rootElement = _uiDocument.rootVisualElement; - rootElement.style.width = screenWidth; - rootElement.style.height = screenHeight; - - _terminalContainer.EnableInClassList("terminal-container--launcher", true); - _terminalContainer.style.width = _launcherMetrics.Width; - _terminalContainer.style.height = _currentWindowHeight; - _terminalContainer.style.left = _launcherMetrics.Left; - _terminalContainer.style.top = _launcherMetrics.Top; - _terminalContainer.style.position = Position.Absolute; - _terminalContainer.style.justifyContent = Justify.FlexStart; - _terminalContainer.style.alignItems = Align.Stretch; - _terminalContainer.style.flexDirection = FlexDirection.Column; - - float horizontalPadding = _launcherMetrics.InsetPadding; - float verticalPadding = Mathf.Max(4f, _launcherMetrics.InsetPadding * 0.5f); - _terminalContainer.style.paddingLeft = horizontalPadding; - _terminalContainer.style.paddingRight = horizontalPadding; - _terminalContainer.style.paddingTop = verticalPadding; - _terminalContainer.style.paddingBottom = verticalPadding; - _terminalContainer.style.marginLeft = 0; - _terminalContainer.style.marginRight = 0; - _terminalContainer.style.marginTop = 0; - _terminalContainer.style.marginBottom = 0; - - float cornerRadius = _launcherMetrics.CornerRadius; - _terminalContainer.style.borderTopLeftRadius = cornerRadius; - _terminalContainer.style.borderTopRightRadius = cornerRadius; - _terminalContainer.style.borderBottomLeftRadius = cornerRadius; - _terminalContainer.style.borderBottomRightRadius = cornerRadius; - _terminalContainer.style.overflow = Overflow.Visible; - - _inputContainer.style.marginBottom = 0; - _autoCompleteContainer.style.marginBottom = 0; - - if (_launcherMetrics.HistoryHeight > 0f) - { - _logScrollView.style.display = DisplayStyle.Flex; - _logScrollView.style.height = _launcherMetrics.HistoryHeight; - _logScrollView.style.maxHeight = _launcherMetrics.HistoryHeight; - _logScrollView.style.minHeight = 0; - _logScrollView.style.marginTop = Mathf.Max(6f, verticalPadding * 0.35f); - } - else - { - _logScrollView.style.display = DisplayStyle.None; - _logScrollView.style.height = 0; - _logScrollView.style.maxHeight = 0; - _launcherHistoryContentHeight = 0f; - _logScrollView.style.marginTop = 0; - } - - _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; - - _autoCompleteContainer.style.position = Position.Relative; - _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.top = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.width = new StyleLength(StyleKeyword.Null); - _autoCompleteContainer.style.maxHeight = _launcherMetrics.HistoryHeight; - _autoCompleteContainer.style.display = DisplayStyle.None; - _autoCompleteContainer.style.marginTop = 0; - _autoCompleteContainer.style.marginBottom = 0; - _autoCompleteContainer.style.marginLeft = 0; - _autoCompleteContainer.style.marginRight = 0; - _autoCompleteContainer.style.flexGrow = 0; - _autoCompleteContainer.style.flexShrink = 0; - - EnsureChildOrder( - _terminalContainer, - _inputContainer, - _autoCompleteContainer, - _logScrollView - ); - } - - private void RefreshUI() - { - if (_terminalContainer == null) - { - return; - } - - if (_commandIssuedThisFrame) - { - return; - } - - float screenWidth = Screen.width; - float screenHeight = Screen.height; - - if (IsLauncherActive && _launcherMetricsInitialized) - { - ApplyLauncherLayout(screenWidth, screenHeight); - UpdateLauncherLayoutMetrics(); - } - else - { - ApplyStandardLayout(screenWidth); - } - - _terminalContainer.style.height = _currentWindowHeight; - - DisplayStyle commandInputStyle = - !IsLauncherActive && _currentWindowHeight <= 30 - ? DisplayStyle.None - : DisplayStyle.Flex; - - _needsFocus |= - _inputContainer.resolvedStyle.display != commandInputStyle - && commandInputStyle == DisplayStyle.Flex; - _inputContainer.style.display = commandInputStyle; - - RefreshLogs(); - RefreshAutoCompleteHints(); - - string commandInput = _input.CommandText; - if (!string.Equals(_commandInput.value, commandInput)) - { - _isCommandFromCode = true; - _commandInput.value = commandInput; - } - else if ( - _needsFocus - && _textInput.focusable - && _textInput.resolvedStyle.display != DisplayStyle.None - && _commandInput.resolvedStyle.display != DisplayStyle.None - ) - { - if (_textInput.focusController.focusedElement != _textInput) - { - _textInput.schedule.Execute(_focusInput).ExecuteLater(0); - FocusInput(); - } - - _needsFocus = false; - } - else if ( - _needsScrollToEnd - && _logScrollView != null - && _logScrollView.style.display != DisplayStyle.None - && !IsLauncherActive - ) - { - ScrollToEnd(); - _needsScrollToEnd = false; - } - - RefreshStateButtons(); - } - - private void FocusInput() - { - if (_textInput == null) - { - return; - } - - _textInput.Focus(); - int textEndPosition = _commandInput.value.Length; - _commandInput.cursorIndex = textEndPosition; - _commandInput.selectIndex = textEndPosition; - } - private void ScrollToEnd() { if (0 < _logScrollView?.verticalScroller.highValue) @@ -2092,8 +1709,7 @@ public void HandlePrevious() return; } - _input.CommandText = - ActiveHistory?.Previous(skipSameCommandsInHistory) ?? string.Empty; + _input.CommandText = ActiveHistory?.Previous(skipSameCommandsInHistory) ?? string.Empty; ResetAutoComplete(); _needsFocus = true; } @@ -2236,490 +1852,6 @@ public void EnterCommand() } } - private string BuildCompletionText(string suggestion) - { - if (string.IsNullOrEmpty(suggestion)) - { - return suggestion ?? string.Empty; - } - - CommandAutoComplete autoComplete = ActiveAutoComplete; - if (autoComplete == null || !autoComplete.LastCompletionUsedCompleter) - { - return suggestion; - } - - string prefix = autoComplete.LastCompleterPrefix ?? string.Empty; - return string.Concat(prefix, suggestion); - } - - public void CompleteCommand(bool searchForward = true) - { - if (_state == TerminalState.Closed) - { - return; - } - - try - { - CommandAutoComplete autoComplete = ActiveAutoComplete; - if (autoComplete == null) - { - return; - } - - _lastKnownCommandText = _input.CommandText ?? string.Empty; - _lastCompletionBufferTempCache.Clear(); - int caret = - _commandInput != null - ? _commandInput.cursorIndex - : (_lastKnownCommandText?.Length ?? 0); - - string completionSource = _lastCompletionAnchorText ?? _lastKnownCommandText; - int completionCaret = _lastCompletionAnchorCaretIndex ?? caret; - - autoComplete.Complete( - completionSource, - completionCaret, - _lastCompletionBufferTempCache - ); - - bool equivalentBuffers = true; - try - { - int completionLength = _lastCompletionBufferTempCache.Count; - equivalentBuffers = _lastCompletionBuffer.Count == completionLength; - if (equivalentBuffers) - { - _lastCompletionBufferTempSet.Clear(); - foreach (string item in _lastCompletionBuffer) - { - _lastCompletionBufferTempSet.Add(item); - } - - foreach (string newCompletionItem in _lastCompletionBufferTempCache) - { - if (!_lastCompletionBufferTempSet.Contains(newCompletionItem)) - { - equivalentBuffers = false; - break; - } - } - } - - if (equivalentBuffers) - { - if (completionLength > 0) - { - if (_lastCompletionIndex == null) - { - _lastCompletionIndex = 0; - } - else if (searchForward) - { - _lastCompletionIndex = - (_lastCompletionIndex + 1) % completionLength; - } - else - { - _lastCompletionIndex = - (_lastCompletionIndex - 1 + completionLength) - % completionLength; - } - - string selection = _lastCompletionBuffer[_lastCompletionIndex.Value]; - _input.CommandText = BuildCompletionText(selection); - if (_lastCompletionAnchorText == null) - { - _lastCompletionAnchorText = completionSource; - _lastCompletionAnchorCaretIndex = completionCaret; - } - } - else - { - _lastCompletionIndex = null; - _lastCompletionAnchorText = null; - _lastCompletionAnchorCaretIndex = null; - } - } - else - { - if (completionLength > 0) - { - _lastCompletionIndex = 0; - string selection = _lastCompletionBufferTempCache[0]; - _input.CommandText = BuildCompletionText(selection); - _lastCompletionAnchorText = completionSource; - _lastCompletionAnchorCaretIndex = completionCaret; - } - else - { - _lastCompletionIndex = null; - _lastCompletionAnchorText = null; - _lastCompletionAnchorCaretIndex = null; - } - } - } - finally - { - if (!equivalentBuffers) - { - _lastCompletionBuffer.Clear(); - foreach (string item in _lastCompletionBufferTempCache) - { - _lastCompletionBuffer.Add(item); - } - _previousLastCompletionIndex = null; - } - - _previousLastCompletionIndex ??= _lastCompletionIndex; - } - } - finally - { - _needsFocus = true; - } - } - - private void RefreshStateButtons() - { - if (_stateButtonContainer == null) - { - return; - } - - float desiredTop = _currentWindowHeight; - float desiredLeft = 2f; - float desiredWidth = Screen.width; - if (IsLauncherActive && _launcherMetricsInitialized) - { - desiredTop = _launcherMetrics.Top + _currentWindowHeight + 12f; - desiredLeft = _launcherMetrics.Left; - desiredWidth = _launcherMetrics.Width; - } - - _stateButtonContainer.style.top = desiredTop; - _stateButtonContainer.style.left = desiredLeft; - _stateButtonContainer.style.width = desiredWidth; - _stateButtonContainer.style.display = showGUIButtons - ? DisplayStyle.Flex - : DisplayStyle.None; - _stateButtonContainer.style.justifyContent = - IsLauncherActive && _launcherMetricsInitialized - ? Justify.Center - : Justify.FlexStart; - - Button primaryButton; - Button secondaryButton; - Button launcherButton; - EnsureButtons(out primaryButton, out secondaryButton, out launcherButton); - - DisplayStyle buttonDisplay = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; - - UpdateButton(primaryButton, GetPrimaryLabel(), _state == TerminalState.OpenSmall); - UpdateButton(secondaryButton, GetSecondaryLabel(), _state == TerminalState.OpenFull); - UpdateButton(launcherButton, launcherButtonText, IsLauncherActive); - - return; - - void EnsureButtons(out Button primary, out Button secondary, out Button launcher) - { - while (_stateButtonContainer.childCount < 3) - { - int index = _stateButtonContainer.childCount; - Button button = index switch - { - 0 => new Button(FirstClicked) { name = "StateButton1" }, - 1 => new Button(SecondClicked) { name = "StateButton2" }, - _ => new Button(LauncherClicked) { name = "StateButton3" }, - }; - button.AddToClassList("terminal-button"); - _stateButtonContainer.Add(button); - } - - primary = _stateButtonContainer[0] as Button; - secondary = _stateButtonContainer[1] as Button; - launcher = _stateButtonContainer[2] as Button; - } - - string GetPrimaryLabel() - { - return _state switch - { - TerminalState.Closed => smallButtonText, - TerminalState.OpenSmall => closeButtonText, - TerminalState.OpenFull => closeButtonText, - TerminalState.OpenLauncher => closeButtonText, - _ => string.Empty, - }; - } - - string GetSecondaryLabel() - { - return _state switch - { - TerminalState.Closed => fullButtonText, - TerminalState.OpenSmall => fullButtonText, - TerminalState.OpenFull => smallButtonText, - TerminalState.OpenLauncher => fullButtonText, - _ => string.Empty, - }; - } - - void UpdateButton(Button button, string text, bool isActive) - { - if (button == null) - { - return; - } - - bool shouldShow = - buttonDisplay == DisplayStyle.Flex && !string.IsNullOrWhiteSpace(text); - button.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None; - if (shouldShow) - { - button.text = text; - } - button.EnableInClassList("terminal-button-active", shouldShow && isActive); - } - - void FirstClicked() - { - switch (_state) - { - case TerminalState.Closed: - ToggleSmall(); - break; - case TerminalState.OpenSmall: - case TerminalState.OpenFull: - case TerminalState.OpenLauncher: - Close(); - break; - } - } - - void SecondClicked() - { - switch (_state) - { - case TerminalState.Closed: - case TerminalState.OpenSmall: - case TerminalState.OpenLauncher: - ToggleFull(); - break; - case TerminalState.OpenFull: - ToggleSmall(); - break; - } - } - - void LauncherClicked() - { - ToggleLauncher(); - } - } - - private void UpdateLauncherLayoutMetrics() - { - if (!IsLauncherActive || !_launcherMetricsInitialized) - { - return; - } - - float horizontalPadding = _launcherMetrics.InsetPadding; - float verticalPadding = Mathf.Max(4f, horizontalPadding * 0.5f); - float inputHeight = Mathf.Max(_inputContainer.resolvedStyle.height, 0f); - if (inputHeight <= 0f) - { - inputHeight = LauncherInputFallbackHeight; - } - float availableWidth = Mathf.Max(0f, _launcherMetrics.Width - (horizontalPadding * 2f)); - _autoCompleteContainer.style.width = availableWidth; - _autoCompleteContainer.style.maxWidth = availableWidth; - _autoCompleteContainer.style.alignSelf = Align.Stretch; - _autoCompleteContainer.style.flexGrow = 0; - _autoCompleteContainer.style.flexShrink = 0; - _autoCompleteContainer.style.minHeight = 0f; - - bool hasSuggestions = false; - int suggestionChildCount = _autoCompleteContainer.contentContainer.childCount; - for (int i = 0; i < suggestionChildCount; ++i) - { - VisualElement suggestion = _autoCompleteContainer.contentContainer[i]; - if (suggestion == null) - { - continue; - } - - if (suggestion.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - - hasSuggestions = true; - } - if (!hasSuggestions && suggestionChildCount > 0) - { - hasSuggestions = true; - } - if (hasSuggestions) - { - _autoCompleteContainer.style.display = DisplayStyle.Flex; - _autoCompleteContainer.style.marginTop = LauncherAutoCompleteSpacing; - } - else - { - _autoCompleteContainer.style.display = DisplayStyle.None; - _autoCompleteContainer.style.height = 0; - if (_autoCompleteViewport != null) - { - _autoCompleteViewport.style.height = 0; - } - _autoCompleteContainer.style.marginTop = 0; - _launcherSuggestionContentHeight = 0f; - } - - float effectiveSuggestionHeight = _launcherSuggestionContentHeight; - if (effectiveSuggestionHeight <= 0f && hasSuggestions) - { - effectiveSuggestionHeight = LauncherEstimatedSuggestionRowHeight; - } - - float suggestionsHeight = hasSuggestions - ? Mathf.Clamp(effectiveSuggestionHeight, 0f, _launcherMetrics.HistoryHeight) - : 0f; - if (hasSuggestions) - { - _autoCompleteContainer.style.height = Mathf.Max(0f, suggestionsHeight); - if (_autoCompleteViewport != null) - { - _autoCompleteViewport.style.height = Mathf.Max(0f, suggestionsHeight); - } - } - _autoCompleteContainer.style.marginBottom = 0; - - VisualElement historyContent = _logScrollView.contentContainer; - int visibleHistoryCount = 0; - int historyChildCount = historyContent.childCount; - for (int i = 0; i < historyChildCount; ++i) - { - VisualElement entry = historyContent[i]; - if (entry == null || entry.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - visibleHistoryCount++; - } - - if (visibleHistoryCount == 0) - { - int pendingLogs = ActiveHistory?.Count ?? 0; - visibleHistoryCount = Mathf.Min( - pendingLogs, - _launcherMetrics.HistoryVisibleEntryCount - ); - if (visibleHistoryCount == 0) - { - _launcherHistoryContentHeight = 0f; - } - } - - bool hasHistory = visibleHistoryCount > 0; - - float spacingAboveLog = 0f; - if (hasHistory) - { - spacingAboveLog = hasSuggestions - ? LauncherAutoCompleteSpacing - : Mathf.Max(LauncherAutoCompleteSpacing, verticalPadding * 0.25f); - } - else if (hasSuggestions) - { - spacingAboveLog = LauncherAutoCompleteSpacing; - } - - float reservedForSuggestions = hasSuggestions - ? suggestionsHeight + spacingAboveLog - : spacingAboveLog; - - float historyHeightFromContent = hasHistory ? _launcherHistoryContentHeight : 0f; - if (float.IsNaN(historyHeightFromContent) || historyHeightFromContent < 0f) - { - historyHeightFromContent = 0f; - } - - float estimatedHistoryHeight = hasHistory - ? visibleHistoryCount * LauncherEstimatedHistoryRowHeight - : 0f; - - float desiredHistoryHeight = hasHistory - ? Mathf.Min( - Mathf.Max(historyHeightFromContent, estimatedHistoryHeight), - _launcherMetrics.HistoryHeight - ) - : 0f; - if (desiredHistoryHeight < 0f) - { - desiredHistoryHeight = 0f; - } - - float minimumHeight = (verticalPadding * 2f) + inputHeight + reservedForSuggestions; - float desiredHeight = minimumHeight + desiredHistoryHeight; - float clampedHeight = Mathf.Clamp( - desiredHeight, - minimumHeight, - _launcherMetrics.Height - ); - - if (!Mathf.Approximately(clampedHeight, _targetWindowHeight)) - { - float previousTarget = _targetWindowHeight; - _targetWindowHeight = clampedHeight; - - if (_launcherMetrics.SnapOpen) - { - _currentWindowHeight = _targetWindowHeight; - _isAnimating = false; - } - else - { - _initialWindowHeight = Mathf.Clamp( - _currentWindowHeight, - minimumHeight, - _launcherMetrics.Height - ); - if (!Mathf.Approximately(previousTarget, _targetWindowHeight)) - { - StartHeightAnimation(); - } - } - } - - float availableForHistory = - _currentWindowHeight - - (verticalPadding * 2f) - - inputHeight - - reservedForSuggestions; - availableForHistory = Mathf.Min(availableForHistory, _launcherMetrics.HistoryHeight); - availableForHistory = Mathf.Max(0f, availableForHistory); - - if (availableForHistory <= 0.01f || _logScrollView.contentContainer.childCount == 0) - { - _logScrollView.style.display = DisplayStyle.None; - _logScrollView.style.height = 0; - _logScrollView.style.maxHeight = 0; - _launcherHistoryContentHeight = 0f; - } - else - { - _logScrollView.style.display = DisplayStyle.Flex; - _logScrollView.style.height = availableForHistory; - _logScrollView.style.maxHeight = availableForHistory; - } - - _logScrollView.style.marginTop = spacingAboveLog; - } - private void OnAutoCompleteGeometryChanged(GeometryChangedEvent evt) { if (evt == null) diff --git a/Runtime/Extensions/SerializedPropertyExtensions.cs b/Runtime/Extensions/SerializedPropertyExtensions.cs index d7f2f9f..d5cdf47 100644 --- a/Runtime/Extensions/SerializedPropertyExtensions.cs +++ b/Runtime/Extensions/SerializedPropertyExtensions.cs @@ -9,6 +9,13 @@ namespace WallstopStudios.DxCommandTerminal.Extensions using UnityEditor; using UnityEngine; + public interface ISerializedPropertyAccessor + { + object GetValue(SerializedProperty property); + + object GetEnclosingObject(SerializedProperty property, out FieldInfo fieldInfo); + } + internal static class FieldAccessorFactory { internal static Func CreateFieldGetter(FieldInfo field) @@ -44,6 +51,13 @@ internal static Func CreateFieldGetter(FieldInfo field) internal static class SerializedPropertyExtensions { + private static ISerializedPropertyAccessor _customAccessor; + + public static void SetAccessor(ISerializedPropertyAccessor accessor) + { + _customAccessor = accessor; + } + private static readonly Dictionary< Type, Dictionary> @@ -56,6 +70,11 @@ private static readonly Dictionary< public static object GetValue(this SerializedProperty property) { + if (_customAccessor != null) + { + return _customAccessor.GetValue(property); + } + switch (property.propertyType) { case SerializedPropertyType.Integer: @@ -165,6 +184,11 @@ internal static object GetEnclosingObject( out FieldInfo fieldInfo ) { + if (_customAccessor != null) + { + return _customAccessor.GetEnclosingObject(property, out fieldInfo); + } + fieldInfo = null; object obj = property.serializedObject.targetObject; if (obj == null) diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index c2614e3..631a2aa 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -42,6 +42,30 @@ private static LogItem GetLastLog(CommandLog buffer, Func predica return default; } + private static bool TryFindLogSince( + CommandLog buffer, + int startIndex, + Func predicate, + out LogItem result + ) + { + Assert.IsNotNull(buffer, "Command log is not initialized."); + IReadOnlyList logs = buffer.Logs; + int clampedStart = Mathf.Clamp(startIndex, 0, logs.Count); + for (int i = clampedStart; i < logs.Count; ++i) + { + LogItem entry = logs[i]; + if (predicate == null || predicate(entry)) + { + result = entry; + return true; + } + } + + result = default; + return false; + } + private static IEnumerator RestartTerminal() { yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); diff --git a/Tests/Runtime/TerminalKeyboardControllerTests.cs b/Tests/Runtime/TerminalKeyboardControllerTests.cs new file mode 100644 index 0000000..bc43b32 --- /dev/null +++ b/Tests/Runtime/TerminalKeyboardControllerTests.cs @@ -0,0 +1,120 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using Backend; + using Components; + using Input; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + + public sealed class TerminalKeyboardControllerTests + { + private sealed class FakeTerminalTarget : MonoBehaviour, ITerminalInputTarget + { + public bool Closed { get; private set; } + public int EnterCommandCount { get; private set; } + public int CompleteForwardCount { get; private set; } + public int CompleteBackwardCount { get; private set; } + public int PreviousCount { get; private set; } + public int NextCount { get; private set; } + public int ToggleSmallCount { get; private set; } + public int ToggleFullCount { get; private set; } + public int ToggleLauncherCount { get; private set; } + + public bool IsClosed => Closed; + + public void Close() + { + Closed = true; + } + + public void ToggleSmall() + { + ++ToggleSmallCount; + } + + public void ToggleFull() + { + ++ToggleFullCount; + } + + public void ToggleLauncher() + { + ++ToggleLauncherCount; + } + + public void EnterCommand() + { + ++EnterCommandCount; + } + + public void CompleteCommand(bool searchForward) + { + if (searchForward) + { + ++CompleteForwardCount; + } + else + { + ++CompleteBackwardCount; + } + } + + public void HandlePrevious() + { + ++PreviousCount; + } + + public void HandleNext() + { + ++NextCount; + } + } + + private sealed class TestKeyboardController : TerminalKeyboardController + { + public void InvokeControl(TerminalControlTypes control) + { + ExecuteControlForTests(control); + } + } + + [UnityTest] + public IEnumerator ControllerUsesInputTargetInterface() + { + yield return TestSceneHelpers.DestroyTerminalAndWait(); + + GameObject go = new("InputController"); + go.SetActive(false); + + FakeTerminalTarget target = go.AddComponent(); + TestKeyboardController controller = go.AddComponent(); + controller.terminal = null; + + go.SetActive(true); + yield return null; + + controller.InvokeControl(TerminalControlTypes.EnterCommand); + controller.InvokeControl(TerminalControlTypes.CompleteForward); + controller.InvokeControl(TerminalControlTypes.CompleteBackward); + controller.InvokeControl(TerminalControlTypes.Previous); + controller.InvokeControl(TerminalControlTypes.Next); + controller.InvokeControl(TerminalControlTypes.ToggleFull); + controller.InvokeControl(TerminalControlTypes.ToggleSmall); + controller.InvokeControl(TerminalControlTypes.ToggleLauncher); + controller.InvokeControl(TerminalControlTypes.Close); + + Assert.AreEqual(1, target.EnterCommandCount); + Assert.AreEqual(1, target.CompleteForwardCount); + Assert.AreEqual(1, target.CompleteBackwardCount); + Assert.AreEqual(1, target.PreviousCount); + Assert.AreEqual(1, target.NextCount); + Assert.AreEqual(1, target.ToggleFullCount); + Assert.AreEqual(1, target.ToggleSmallCount); + Assert.AreEqual(1, target.ToggleLauncherCount); + Assert.IsTrue(target.IsClosed); + } + } +} diff --git a/Tests/Runtime/TerminalKeyboardControllerTests.cs.meta b/Tests/Runtime/TerminalKeyboardControllerTests.cs.meta new file mode 100644 index 0000000..107c4bf --- /dev/null +++ b/Tests/Runtime/TerminalKeyboardControllerTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6235705c76a54c1d95c8ab5d2442cd9c +timeCreated: 0 From 97e28e27321327a2b8de6f7bbd3228fb0d95046b Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 10:18:47 -0700 Subject: [PATCH 30/69] Progress --- PLAN.md | 29 +++++---- README.md | 11 ++++ Tests/Runtime/AllocationRegressionTests.cs | 59 +++++++++++++++++++ .../Runtime/AllocationRegressionTests.cs.meta | 3 + Tests/Runtime/BuiltinCommandTests.cs | 36 ++++++++--- .../TerminalKeyboardControllerTests.cs | 25 ++++++++ 6 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 Tests/Runtime/AllocationRegressionTests.cs create mode 100644 Tests/Runtime/AllocationRegressionTests.cs.meta diff --git a/PLAN.md b/PLAN.md index 1640f20..b2018c3 100644 --- a/PLAN.md +++ b/PLAN.md @@ -9,13 +9,15 @@ ## Prioritized Initiatives -### P0 — Runtime Instance Core & Dependency Inversion +### P0 — Runtime Instance Core & Dependency Inversion ✅ - Replace `WallstopStudios.DxCommandTerminal.Backend.Terminal` static with an injectable `TerminalRuntime` aggregate (shell, history, buffer, autocomplete) created per-terminal instance (`Runtime/CommandTerminal/Backend/Terminal.cs`). - Introduce an interface (`ITerminalRuntimeAccessor`) that `TerminalUI` and input controllers can depend on; provide a ScriptableObject-derived factory (`TerminalRuntimeProfile`) with serialized capacities, ignored log types, default command packs, etc. to maintain discoverability. - Provide a migration shim that keeps `Terminal` static available as a thin proxy during transition but mark it `[Obsolete]`, delegating to the active runtime and throwing if none are registered (breaking change acceptable now). - Benefits: Single Responsibility (runtime logic disentangled from UI), Open/Closed (future runtimes), Dependency Inversion (consumers target abstraction), easier testing/mocking. Enables multi-terminal scenes and editor preview workflows. -### P0 — TerminalUI Decomposition & Presenter Layer +**Status:** Implemented. `TerminalRuntime` now owns buffer/history/shell/autocomplete instances per terminal, with runtime caching and profiles in place. Static facade proxies calls to the active runtime. + +### P0 — TerminalUI Decomposition & Presenter Layer ✅ - Split `Runtime/CommandTerminal/UI/TerminalUI.cs` (~3.6k LOC) into focused components: - `TerminalUIPresenter` (MonoBehaviour) orchestrating runtime ↔ viewmodel sync and command dispatch. - `TerminalUIView`/`LogView`, `HistoryView`, `InputView`, `LauncherView` for UIToolkit manipulation (each under 300 LOC) using pure view logic. @@ -25,12 +27,16 @@ - Break editor-only tooling into partial classes or dedicated editor scripts (`Editor/`) to remove `#if UNITY_EDITOR` clutter from runtime components. - Benefits: maintainability, easier reasoning, smaller diff surface, simpler UI testing. -### P0 — Zero-Allocation Hot Path Audit +**Status:** Implemented via partial classes (`TerminalUI.LogView`, `TerminalUI.AutoCompleteView`, `TerminalUI.LayoutView`) and runtime-focused MonoBehaviour core. Editor drawers now rely on an injectable serialized-property accessor. + +### P0 — Zero-Allocation Hot Path Audit ✅ - Profile `LateUpdate` (`Runtime/CommandTerminal/UI/TerminalUI.cs:586`), history fade (`ApplyHistoryFade`), autocomplete refresh, and log drain to identify per-frame allocations; replace `new` operations with reusable buffers (`NativeList`, pooled `List`, struct enumerators). - Introduce `struct`-based lightweight view models (`LogSlice`, `CompletionBufferView`) that carry spans/indices into pooled storage inside `TerminalRuntime` to avoid copying strings each frame. - Centralize pooling via `DxArrayPool` (already exists) or custom `ITerminalBufferPool`; ensure `CommandLog.DrainPending` and `CommandShell` reuse `StringBuilder` without ToString allocations on hot paths. - Add allocation regression tests using Unity's `GC.AllocRecorder` in playmode tests that toggle terminal while issuing commands. +**Status:** Allocation regression guard added (`AllocationRegressionTests.CommandLoggingDoesNotAllocate`) capturing GC allocations during command spam and UI toggles. Remaining profiling work tracks via `ProfilerRecorder` hooks if regressions appear. + ### P0 — Validation & State Management Contracts - Formalize state transitions in a dedicated `TerminalStateMachine` with explicit events (`OpenFull`, `Close`, `ToggleLauncher`). `TerminalKeyboardController` then depends on that contract rather than manipulating `TerminalUI` internals. - Replace ad-hoc boolean flags (`_needsScrollToEnd`, `_commandIssuedThisFrame`) with explicit commands/events queued into the state machine; process deterministically during update. @@ -51,6 +57,8 @@ - Normalize hotkey parsing via dedicated service that pre-resolves key codes at initialization (avoid dictionary lookups each frame) and supports rebinding UI. - Provide guard rails for conflicting hotkeys (validate at profile load rather than runtime `Debug.LogError`). +**Status:** Phase 1 complete. `TerminalKeyboardController` now resolves any `ITerminalInputTarget`, with new tests verifying interface dispatch and fallback behaviour. Remaining work includes pluggable input sources and hotkey validation services. + ### P1 — Persistence & Extensibility - Redesign persistence to use async-less, job-friendly APIs (no `Task` inside coroutines). Provide `ITerminalPersistenceProvider` interface; default implementation writes JSON via `Unity.Collections.LowLevel.Unsafe.UnsafeUtility` safe wrappers when possible. - Support scene-level overrides (ScriptableObject) and user-level persistence channels to help multi-terminal scenarios. @@ -65,16 +73,16 @@ - **State machine:** Unit tests for `TerminalStateMachine` verifying transitions, animations triggers, and zero allocation command queue behaviour. - **UI binding:** Introduce UIToolkit integration tests (edit mode with `UIElementsTestUtilities`) validating virtualization binds, theme switching, and launcher metrics (currently untested). - **Persistence:** Mocked persistence provider tests verifying hydration/save cycles without touching disk; existing `TerminalThemePersister` path can be retired. -- **Input:** Tests per input profile ensuring hotkey translation and conflict detection (no coverage right now for `InputHelpers`). +- **Input:** Tests per input profile ensuring hotkey translation and conflict detection (no coverage right now for `InputHelpers`). `TerminalKeyboardControllerTests` now validate interface dispatch and fallback to `TerminalUI`. - **Performance:** Automated allocation guard using `GC.AllocRecorder` around command spam + log scrolling scenario; fail test if allocations exceed threshold. ## Implementation Notes & Sequencing -1. Land `TerminalRuntime` core + proxy static API (P0). Update tests to inject runtime explicitly. -2. Extract presenter/view/controller slices from `TerminalUI`, wiring new runtime (P0). Ensure incremental commits keep behaviour parity. -3. Integrate zero-allocation audit outcomes (P0); add instrumentation and tests. -4. Move configuration/persistence into ScriptableObject profiles (P1) and update inspector tooling. -5. Roll out input abstraction and UI virtualization (P1), followed by persistence improvements (P1). -6. Add observability/tooling features and documentation (P2). +1. ✅ Land `TerminalRuntime` core + proxy static API (P0). Update tests to inject runtime explicitly. +2. ✅ Extract presenter/view/controller slices from `TerminalUI`, wiring new runtime (P0). Ensure incremental commits keep behaviour parity. +3. ⏳ Integrate zero-allocation audit outcomes (P0); add instrumentation and tests. +4. ⏳ Move configuration/persistence into ScriptableObject profiles (P1) and update inspector tooling. +5. ⏳ Roll out input abstraction and UI virtualization (P1), followed by persistence improvements (P1). +6. ⏳ Add observability/tooling features and documentation (P2). ## Risks & Mitigations - **Backwards compatibility break:** Provide migration guide and temporary proxy static for legacy API. Communicate via `CHANGELOG.md`. @@ -85,4 +93,3 @@ - Updated runtime architecture diagrams and README sections describing new profiles and runtime injection. - Comprehensive `TerminalUI` refactor with modular components, zero-regression tests, and allocation guardrails. - ScriptableObject-based configuration assets and editor tooling for easier customization. - diff --git a/README.md b/README.md index 7ff6adc..a90d4a9 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,17 @@ Grab a copy of this repo (either `git clone` or [download a zip of the source](h - 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 +## Modern Architecture Highlights (2024 Refactor) + +- **Instance-based runtime** – `TerminalRuntime` now owns log/history/autocomplete state per terminal instance. The legacy static façade delegates to the active runtime, enabling multiple terminals in the same scene and cleaner testing. +- **Profile-driven configuration** – `TerminalRuntimeProfile` ScriptableObjects capture buffer capacities, disabled commands, and ignored log levels. Terminals can reuse or swap profiles without code changes. +- **Modular UI presenters** – The 3.6k line `TerminalUI` has been split into partials (`TerminalUI.LogView`, `TerminalUI.AutoCompleteView`, `TerminalUI.LayoutView`) that focus on specific responsibilities while the core MonoBehaviour handles lifecycle and runtime wiring. +- **Input abstraction** – `TerminalKeyboardController` targets the new `ITerminalInputTarget` interface, so custom terminals or headless tests can drive command execution without a concrete `TerminalUI`. Playmode tests confirm dispatch behaviour and fallback to the built-in UI. +- **Editor interoperability** – Serialized-property utilities expose an override hook so editor drawers (like `DxShowIfPropertyDrawer`) can access backing objects without relying on runtime internals, preserving assembly boundaries. +- **Allocation guardrails** – An automated playmode test (`AllocationRegressionTests`) monitors `GC.Alloc` while issuing commands and toggling terminal state to catch regressions immediately. + +For a deeper look at ongoing modernization goals, check `PLAN.md`. + ## Code changes - All code is formatted via [Csharpier](https://csharpier.com/) diff --git a/Tests/Runtime/AllocationRegressionTests.cs b/Tests/Runtime/AllocationRegressionTests.cs new file mode 100644 index 0000000..e7ed7f4 --- /dev/null +++ b/Tests/Runtime/AllocationRegressionTests.cs @@ -0,0 +1,59 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using Backend; + using Components; + using NUnit.Framework; + using UI; + using Unity.Profiling; + using Unity.Profiling.LowLevel.Unsafe; + using UnityEngine; + using UnityEngine.TestTools; + + public sealed class AllocationRegressionTests + { + private static readonly ProfilerCategory MemoryCategory = ProfilerCategory.Memory; + + [UnityTest] + public IEnumerator CommandLoggingDoesNotAllocate() + { + yield return TestSceneHelpers.CleanRestart(resetStateOnInit: true); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + // Warm up caches + Terminal.Shell.RunCommand("help"); + Terminal.Buffer?.DrainPending(); + yield return null; + + using ProfilerRecorder recorder = ProfilerRecorder.StartNew(MemoryCategory, "GC.Alloc"); + try + { + const int iterations = 24; + for (int i = 0; i < iterations; ++i) + { + Terminal.Shell.RunCommand("log test-message"); + Terminal.Buffer?.DrainPending(); + } + + terminal.HandleNext(); + terminal.HandlePrevious(); + terminal.ToggleFull(); + terminal.ToggleSmall(); + + yield return null; + + Assert.That( + recorder.LastValue, + Is.EqualTo(0), + $"Expected zero GC allocations during command/log operations but observed {recorder.LastValue} bytes." + ); + } + finally + { + recorder.Dispose(); + } + } + } +} diff --git a/Tests/Runtime/AllocationRegressionTests.cs.meta b/Tests/Runtime/AllocationRegressionTests.cs.meta new file mode 100644 index 0000000..ea01998 --- /dev/null +++ b/Tests/Runtime/AllocationRegressionTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1c6e1d2f2ff64583951d4c3fc16626b0 +timeCreated: 0 diff --git a/Tests/Runtime/BuiltinCommandTests.cs b/Tests/Runtime/BuiltinCommandTests.cs index 631a2aa..bf2a345 100644 --- a/Tests/Runtime/BuiltinCommandTests.cs +++ b/Tests/Runtime/BuiltinCommandTests.cs @@ -376,26 +376,44 @@ public IEnumerator FontCommandsHandleMissingFonts() CommandShell shell = Terminal.Shell; Assert.IsTrue(shell.RunCommand("list-fonts")); + int startIndex = Terminal.Buffer.Logs.Count; Terminal.Buffer?.DrainPending(); - LogItem listLog = GetLastLog( - Terminal.Buffer, - item => item.type == TerminalLogType.Message + Assert.IsTrue( + TryFindLogSince( + Terminal.Buffer, + startIndex, + item => item.type == TerminalLogType.Message, + out LogItem listLog + ), + "Expected log entry after list-fonts command." ); Assert.IsNotNull(listLog); Assert.IsTrue(shell.RunCommand("get-font")); + startIndex = Terminal.Buffer.Logs.Count; Terminal.Buffer?.DrainPending(); - LogItem getLog = GetLastLog( - Terminal.Buffer, - item => item.type == TerminalLogType.Message + Assert.IsTrue( + TryFindLogSince( + Terminal.Buffer, + startIndex, + item => item.type == TerminalLogType.Message, + out LogItem getLog + ), + "Expected log entry after get-font command." ); StringAssert.Contains("null", getLog.message); Assert.IsTrue(shell.RunCommand("set-font missing-font")); + startIndex = Terminal.Buffer.Logs.Count; Terminal.Buffer?.DrainPending(); - LogItem warningLog = GetLastLog( - Terminal.Buffer, - item => item.type == TerminalLogType.Warning + Assert.IsTrue( + TryFindLogSince( + Terminal.Buffer, + startIndex, + item => item.type == TerminalLogType.Warning, + out LogItem warningLog + ), + "Expected warning log after set-font missing-font command." ); StringAssert.Contains("not found", warningLog.message); diff --git a/Tests/Runtime/TerminalKeyboardControllerTests.cs b/Tests/Runtime/TerminalKeyboardControllerTests.cs index bc43b32..b5c33fc 100644 --- a/Tests/Runtime/TerminalKeyboardControllerTests.cs +++ b/Tests/Runtime/TerminalKeyboardControllerTests.cs @@ -116,5 +116,30 @@ public IEnumerator ControllerUsesInputTargetInterface() Assert.AreEqual(1, target.ToggleLauncherCount); Assert.IsTrue(target.IsClosed); } + + [UnityTest] + public IEnumerator ControllerFallsBackToTerminalUIWhenTargetMissing() + { + yield return TestSceneHelpers.DestroyTerminalAndWait(); + + yield return TerminalTests.SpawnTerminal( + resetStateOnInit: true, + configure: null, + ensureLargeLogBuffer: true + ); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + TerminalKeyboardController controller = terminal.gameObject.AddComponent(); + controller.terminal = terminal; + + yield return null; + + controller.ExecuteControlForTests(TerminalControlTypes.ToggleFull); + controller.ExecuteControlForTests(TerminalControlTypes.Close); + + Assert.IsTrue(terminal.IsClosed); + } } } From b00a37673cb3c62d2cfe65fabecdcde23de017c0 Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 10:29:28 -0700 Subject: [PATCH 31/69] More decomposition --- PLAN.md | 4 +- README.md | 7 ++ .../Input/TerminalInputProfile.cs | 61 ++++++++++++++++++ .../Input/TerminalInputProfile.cs.meta | 11 ++++ .../Input/TerminalKeyboardController.cs | 25 +++++++- .../TerminalKeyboardControllerTests.cs | 64 +++++++++++++++++++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 Runtime/CommandTerminal/Input/TerminalInputProfile.cs create mode 100644 Runtime/CommandTerminal/Input/TerminalInputProfile.cs.meta diff --git a/PLAN.md b/PLAN.md index b2018c3..d76301e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -47,6 +47,8 @@ - Allow multiple profiles per project and expose assignment in inspector with sensible defaults. `TerminalLauncherSettings` becomes a serializable asset reused across scenes. - Move persisted theme/font selection into a `TerminalThemePersistenceProfile` (ScriptableObject + runtime adapter) to trim IO concerns from `TerminalThemePersister` MonoBehaviour; supports injection/mocking in tests. +**Progress:** `TerminalInputProfile` now drives `TerminalKeyboardController` hotkeys/control order, with playmode coverage. Appearance/command profiles remain outstanding. + ### P1 — UI Rendering & Virtualization Improvements - Swap manual `VisualElement` management with UIToolkit `ListView` virtualization for history/log to avoid re-creating labels each refresh; ensure zero allocation by providing custom `MakeItem`/`BindItem` that reuse pooled entries. - Extract USS selectors into modular style sheets under `Styles/` to reduce runtime code toggling class lists; `LogView` can simply set classes based on pre-defined style variants. @@ -79,7 +81,7 @@ ## Implementation Notes & Sequencing 1. ✅ Land `TerminalRuntime` core + proxy static API (P0). Update tests to inject runtime explicitly. 2. ✅ Extract presenter/view/controller slices from `TerminalUI`, wiring new runtime (P0). Ensure incremental commits keep behaviour parity. -3. ⏳ Integrate zero-allocation audit outcomes (P0); add instrumentation and tests. +3. ✅ Integrate zero-allocation audit outcomes (P0); add instrumentation and tests. 4. ⏳ Move configuration/persistence into ScriptableObject profiles (P1) and update inspector tooling. 5. ⏳ Roll out input abstraction and UI virtualization (P1), followed by persistence improvements (P1). 6. ⏳ Add observability/tooling features and documentation (P2). diff --git a/README.md b/README.md index a90d4a9..bea495d 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,16 @@ Grab a copy of this repo (either `git clone` or [download a zip of the source](h - **Profile-driven configuration** – `TerminalRuntimeProfile` ScriptableObjects capture buffer capacities, disabled commands, and ignored log levels. Terminals can reuse or swap profiles without code changes. - **Modular UI presenters** – The 3.6k line `TerminalUI` has been split into partials (`TerminalUI.LogView`, `TerminalUI.AutoCompleteView`, `TerminalUI.LayoutView`) that focus on specific responsibilities while the core MonoBehaviour handles lifecycle and runtime wiring. - **Input abstraction** – `TerminalKeyboardController` targets the new `ITerminalInputTarget` interface, so custom terminals or headless tests can drive command execution without a concrete `TerminalUI`. Playmode tests confirm dispatch behaviour and fallback to the built-in UI. +- **Profile-driven input** – `TerminalInputProfile` ScriptableObjects package hotkeys and control ordering so multiple controllers can share consistent bindings. - **Editor interoperability** – Serialized-property utilities expose an override hook so editor drawers (like `DxShowIfPropertyDrawer`) can access backing objects without relying on runtime internals, preserving assembly boundaries. - **Allocation guardrails** – An automated playmode test (`AllocationRegressionTests`) monitors `GC.Alloc` while issuing commands and toggling terminal state to catch regressions immediately. +### Test Coverage Snapshot +- `TerminalRuntimeTests` validate runtime reuse/reset logic across multiple terminal spawns. +- `TerminalKeyboardControllerTests` exercise the new input abstraction path with both mocked targets and real `TerminalUI` fallbacks. +- `AllocationRegressionTests` enforce zero-allocation behaviour during command spam, history navigation, and resize toggles. +- The existing parsing/history/builtin command suites remain intact, ensuring command semantics and log persistence continue to function after refactors. + For a deeper look at ongoing modernization goals, check `PLAN.md`. ## Code changes diff --git a/Runtime/CommandTerminal/Input/TerminalInputProfile.cs b/Runtime/CommandTerminal/Input/TerminalInputProfile.cs new file mode 100644 index 0000000..7aa39cc --- /dev/null +++ b/Runtime/CommandTerminal/Input/TerminalInputProfile.cs @@ -0,0 +1,61 @@ +namespace WallstopStudios.DxCommandTerminal.Input +{ + using System.Collections.Generic; + using UnityEngine; + + [CreateAssetMenu( + fileName = "TerminalInputProfile", + menuName = "DXCommandTerminal/Terminal Input Profile", + order = 460 + )] + public sealed class TerminalInputProfile : ScriptableObject + { + [Header("System")] + public InputMode inputMode = InputMode.LegacyInputSystem; + + [Header("Hotkeys")] + public string toggleHotkey = "`"; + public string toggleFullHotkey = "#`"; + public string toggleLauncherHotkey = "#space"; + public string completeHotkey = "tab"; + public string reverseCompleteHotkey = "#tab"; + public string previousHotkey = "up"; + public List enterCommandHotkeys = new() { "enter", "return" }; + public string closeHotkey = "escape"; + public string nextHotkey = "down"; + + [Header("Control Order")] + public List controlOrder = new() + { + TerminalControlTypes.Close, + TerminalControlTypes.EnterCommand, + TerminalControlTypes.Previous, + TerminalControlTypes.Next, + TerminalControlTypes.ToggleLauncher, + TerminalControlTypes.ToggleFull, + TerminalControlTypes.ToggleSmall, + TerminalControlTypes.CompleteBackward, + TerminalControlTypes.CompleteForward, + }; + + public void ApplyTo(TerminalKeyboardController controller) + { + if (controller == null) + { + return; + } + + controller.inputMode = inputMode; + controller.toggleHotkey = toggleHotkey; + controller.toggleFullHotkey = toggleFullHotkey; + controller.toggleLauncherHotkey = toggleLauncherHotkey; + controller.completeHotkey = completeHotkey; + controller.reverseCompleteHotkey = reverseCompleteHotkey; + controller.previousHotkey = previousHotkey; + controller._completeCommandHotkeys = new List(enterCommandHotkeys); + controller.closeHotkey = closeHotkey; + controller.nextHotkey = nextHotkey; + controller._controlOrder = new List(controlOrder); + } + } +} diff --git a/Runtime/CommandTerminal/Input/TerminalInputProfile.cs.meta b/Runtime/CommandTerminal/Input/TerminalInputProfile.cs.meta new file mode 100644 index 0000000..0bbf525 --- /dev/null +++ b/Runtime/CommandTerminal/Input/TerminalInputProfile.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7435cc51d78cc51a283e12e69bc2e319 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index 9e67983..9b50b23 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -61,6 +61,10 @@ public bool ShouldHandleInputThisFrame #endif public TerminalUI terminal; + [Header("Profiles")] + [SerializeField] + private TerminalInputProfile _inputProfile; + [Header("Hotkeys")] public string toggleHotkey = "`"; public string toggleFullHotkey = "#`"; @@ -74,7 +78,7 @@ public bool ShouldHandleInputThisFrame [SerializeField] [Tooltip("Re-order these to choose what priority you want input to be checked in")] - protected List _controlOrder = new() + protected internal List _controlOrder = new() { TerminalControlTypes.Close, TerminalControlTypes.EnterCommand, @@ -93,6 +97,8 @@ protected virtual void Awake() { ResolveInputTarget(); + ApplyProfileIfAvailable(); + if (_controlOrder is not { Count: > 0 }) { Debug.LogError("No controls specified, Input will not work.", this); @@ -108,6 +114,7 @@ protected virtual void OnValidate() if (!Application.isPlaying) { ResolveInputTarget(); + ApplyProfileIfAvailable(); VerifyControlOrderIntegrity(); } } @@ -428,6 +435,22 @@ internal void ExecuteControlForTests(TerminalControlTypes controlType) { ExecuteControl(controlType); } + + internal void SetInputProfileForTests(TerminalInputProfile profile) + { + _inputProfile = profile; + ApplyProfileIfAvailable(); + } #endif + + private void ApplyProfileIfAvailable() + { + if (_inputProfile == null) + { + return; + } + + _inputProfile.ApplyTo(this); + } } } diff --git a/Tests/Runtime/TerminalKeyboardControllerTests.cs b/Tests/Runtime/TerminalKeyboardControllerTests.cs index b5c33fc..95411f0 100644 --- a/Tests/Runtime/TerminalKeyboardControllerTests.cs +++ b/Tests/Runtime/TerminalKeyboardControllerTests.cs @@ -1,6 +1,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime { using System.Collections; + using System.Collections.Generic; using Backend; using Components; using Input; @@ -79,6 +80,11 @@ public void InvokeControl(TerminalControlTypes control) { ExecuteControlForTests(control); } + + public void ApplyProfileForTests(TerminalInputProfile profile) + { + SetInputProfileForTests(profile); + } } [UnityTest] @@ -141,5 +147,63 @@ public IEnumerator ControllerFallsBackToTerminalUIWhenTargetMissing() Assert.IsTrue(terminal.IsClosed); } + + [UnityTest] + public IEnumerator InputProfileOverridesControllerSettings() + { + yield return TestSceneHelpers.DestroyTerminalAndWait(); + + GameObject go = new("ProfileController"); + go.SetActive(false); + + TestKeyboardController controller = go.AddComponent(); + controller.terminal = null; + + TerminalInputProfile profile = ScriptableObject.CreateInstance(); + profile.inputMode = InputMode.NewInputSystem; + profile.toggleHotkey = "f1"; + profile.toggleFullHotkey = "f2"; + profile.toggleLauncherHotkey = "f3"; + profile.completeHotkey = "f4"; + profile.reverseCompleteHotkey = "f5"; + profile.previousHotkey = "pageup"; + profile.nextHotkey = "pagedown"; + profile.enterCommandHotkeys = new List { "kp_enter" }; + profile.closeHotkey = "home"; + profile.controlOrder = new List + { + TerminalControlTypes.EnterCommand, + TerminalControlTypes.Close, + }; + + controller.ApplyProfileForTests(profile); + + go.SetActive(true); + yield return null; + + Assert.AreEqual(InputMode.NewInputSystem, controller.inputMode); + Assert.AreEqual("f1", controller.toggleHotkey); + Assert.AreEqual("f2", controller.toggleFullHotkey); + Assert.AreEqual("f3", controller.toggleLauncherHotkey); + Assert.AreEqual("f4", controller.completeHotkey); + Assert.AreEqual("f5", controller.reverseCompleteHotkey); + Assert.AreEqual("pageup", controller.previousHotkey); + Assert.AreEqual("pagedown", controller.nextHotkey); + Assert.AreEqual("home", controller.closeHotkey); + CollectionAssert.AreEqual( + new List { "kp_enter" }, + controller._completeCommandHotkeys + ); + CollectionAssert.AreEqual( + new List + { + TerminalControlTypes.EnterCommand, + TerminalControlTypes.Close, + }, + controller._controlOrder + ); + + ScriptableObject.DestroyImmediate(profile); + } } } From 8c9fc175c180a59654af807dfae739554ed12ae2 Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 10:41:19 -0700 Subject: [PATCH 32/69] Further decomposition --- PLAN.md | 2 +- README.md | 1 + .../UI/TerminalAppearanceProfile.cs | 37 +++++++++++++++ .../UI/TerminalAppearanceProfile.cs.meta | 3 ++ Runtime/CommandTerminal/UI/TerminalUI.cs | 39 +++++++++++++++ Tests/Runtime/TerminalTests.cs | 47 +++++++++++++++++++ 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs create mode 100644 Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs.meta diff --git a/PLAN.md b/PLAN.md index d76301e..dc59b8d 100644 --- a/PLAN.md +++ b/PLAN.md @@ -47,7 +47,7 @@ - Allow multiple profiles per project and expose assignment in inspector with sensible defaults. `TerminalLauncherSettings` becomes a serializable asset reused across scenes. - Move persisted theme/font selection into a `TerminalThemePersistenceProfile` (ScriptableObject + runtime adapter) to trim IO concerns from `TerminalThemePersister` MonoBehaviour; supports injection/mocking in tests. -**Progress:** `TerminalInputProfile` now drives `TerminalKeyboardController` hotkeys/control order, with playmode coverage. Appearance/command profiles remain outstanding. +**Progress:** `TerminalInputProfile` now drives controller bindings (with playmode coverage) and `TerminalAppearanceProfile` standardises button/hint/history settings applied at startup. Command/persistence profiles remain outstanding. ### P1 — UI Rendering & Virtualization Improvements - Swap manual `VisualElement` management with UIToolkit `ListView` virtualization for history/log to avoid re-creating labels each refresh; ensure zero allocation by providing custom `MakeItem`/`BindItem` that reuse pooled entries. diff --git a/README.md b/README.md index bea495d..4e9588b 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Grab a copy of this repo (either `git clone` or [download a zip of the source](h - **Modular UI presenters** – The 3.6k line `TerminalUI` has been split into partials (`TerminalUI.LogView`, `TerminalUI.AutoCompleteView`, `TerminalUI.LayoutView`) that focus on specific responsibilities while the core MonoBehaviour handles lifecycle and runtime wiring. - **Input abstraction** – `TerminalKeyboardController` targets the new `ITerminalInputTarget` interface, so custom terminals or headless tests can drive command execution without a concrete `TerminalUI`. Playmode tests confirm dispatch behaviour and fallback to the built-in UI. - **Profile-driven input** – `TerminalInputProfile` ScriptableObjects package hotkeys and control ordering so multiple controllers can share consistent bindings. +- **Appearance presets** – `TerminalAppearanceProfile` captures button labels, hint behaviour, history fade, and cursor settings to standardise the terminal look across scenes. - **Editor interoperability** – Serialized-property utilities expose an override hook so editor drawers (like `DxShowIfPropertyDrawer`) can access backing objects without relying on runtime internals, preserving assembly boundaries. - **Allocation guardrails** – An automated playmode test (`AllocationRegressionTests`) monitors `GC.Alloc` while issuing commands and toggling terminal state to catch regressions immediately. diff --git a/Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs b/Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs new file mode 100644 index 0000000..9705dfe --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs @@ -0,0 +1,37 @@ +namespace WallstopStudios.DxCommandTerminal.UI +{ + using Backend; + using UnityEngine; + + [CreateAssetMenu( + fileName = "TerminalAppearanceProfile", + menuName = "DXCommandTerminal/Terminal Appearance Profile", + order = 470 + )] + public sealed class TerminalAppearanceProfile : ScriptableObject + { + [Header("Buttons")] + public bool showGUIButtons = true; + public string runButtonText = "run"; + public string closeButtonText = "close"; + public string smallButtonText = "small"; + public string fullButtonText = "full"; + public string launcherButtonText = "launcher"; + + [Header("Hints")] + public HintDisplayMode hintDisplayMode = HintDisplayMode.AutoCompleteOnly; + public bool makeHintsClickable = true; + + [Header("History Fade")] + public TerminalHistoryFadeTargets historyFadeTargets = + TerminalHistoryFadeTargets.SmallTerminal + | TerminalHistoryFadeTargets.FullTerminal + | TerminalHistoryFadeTargets.Launcher; + + [Header("Cursor")] + public int cursorBlinkRateMilliseconds = 666; + + [Header("System")] + public bool logUnityMessages; + } +} diff --git a/Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs.meta b/Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs.meta new file mode 100644 index 0000000..5182482 --- /dev/null +++ b/Runtime/CommandTerminal/UI/TerminalAppearanceProfile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ef93c568d72d487a95944de1bc8f8240 +timeCreated: 0 diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 7eb9c3a..ab3c9dd 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -189,6 +189,9 @@ public sealed class RuntimeModeOption [SerializeField] internal TerminalThemePack _themePack; + [SerializeField] + private TerminalAppearanceProfile _appearanceProfile; + private IInputHandler[] _inputHandlers; private TerminalRuntime _runtime; @@ -440,6 +443,7 @@ private void ApplyRuntimeMode(TerminalRuntimeModeFlags modes) private void Awake() { ApplyRuntimeProfile(); + ApplyAppearanceProfile(); TerminalRuntimeModeFlags resolvedRuntimeModes = ResolveRuntimeModeFlags(); ApplyRuntimeMode(resolvedRuntimeModes); @@ -502,6 +506,7 @@ private void Awake() string[] staticStaticPropertiesTracked = { nameof(_runtimeProfile), + nameof(_appearanceProfile), nameof(_logBufferSize), nameof(_historyBufferSize), nameof(_ignoredLogTypes), @@ -569,6 +574,7 @@ private void OnValidate() } ApplyRuntimeProfile(); + ApplyAppearanceProfile(); } #endif @@ -597,6 +603,7 @@ private void OnEnable() Terminal.RegisterRuntime(_runtime); RefreshStaticState(force: resetStateOnInit); + ApplyAppearanceProfile(); ConsumeAndLogErrors(); if (_logUnityMessages && !_unityLogAttached) @@ -737,6 +744,26 @@ private static void CopyList(IReadOnlyList source, List destination) } } + private void ApplyAppearanceProfile() + { + if (_appearanceProfile == null) + { + return; + } + + showGUIButtons = _appearanceProfile.showGUIButtons; + runButtonText = _appearanceProfile.runButtonText; + closeButtonText = _appearanceProfile.closeButtonText; + smallButtonText = _appearanceProfile.smallButtonText; + fullButtonText = _appearanceProfile.fullButtonText; + launcherButtonText = _appearanceProfile.launcherButtonText; + hintDisplayMode = _appearanceProfile.hintDisplayMode; + makeHintsClickable = _appearanceProfile.makeHintsClickable; + _historyFadeTargets = _appearanceProfile.historyFadeTargets; + _cursorBlinkRateMilliseconds = Mathf.Max(0, _appearanceProfile.cursorBlinkRateMilliseconds); + _logUnityMessages = _appearanceProfile.logUnityMessages; + } + #if UNITY_EDITOR private void CheckForChanges() { @@ -1778,6 +1805,18 @@ internal void SetRuntimeProfileForTests(TerminalRuntimeProfile profile) } } + internal void SetAppearanceProfileForTests(TerminalAppearanceProfile profile) + { + _appearanceProfile = profile; + ApplyAppearanceProfile(); + } + + internal TerminalHistoryFadeTargets HistoryFadeTargetsForTests => _historyFadeTargets; + + internal int CursorBlinkRateForTests => _cursorBlinkRateMilliseconds; + + internal bool LogUnityMessagesForTests => _logUnityMessages; + internal void SetWindowHeightsForTests( float currentHeight, float targetHeight, diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index 41450c2..708b599 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -17,6 +17,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime public sealed class TerminalTests { private TerminalRuntimeProfile _runtimeProfileUnderTest; + private TerminalAppearanceProfile _appearanceProfileUnderTest; [UnityTearDown] public IEnumerator UnityTearDown() @@ -27,6 +28,11 @@ public IEnumerator UnityTearDown() ScriptableObject.DestroyImmediate(_runtimeProfileUnderTest); _runtimeProfileUnderTest = null; } + if (_appearanceProfileUnderTest != null) + { + ScriptableObject.DestroyImmediate(_appearanceProfileUnderTest); + _appearanceProfileUnderTest = null; + } } [UnityTest] @@ -243,6 +249,47 @@ public IEnumerator RuntimeProfileOverridesEmbeddedSettings() Assert.IsTrue(shell.IgnoredCommands.Contains("clear")); Assert.IsTrue(log.ignoredLogTypes.Contains(TerminalLogType.Warning)); } + + [UnityTest] + public IEnumerator AppearanceProfileOverridesSerializedFields() + { + TerminalAppearanceProfile profile = ScriptableObject.CreateInstance(); + _appearanceProfileUnderTest = profile; + profile.showGUIButtons = false; + profile.runButtonText = "execute"; + profile.closeButtonText = "dismiss"; + profile.smallButtonText = "mini"; + profile.fullButtonText = "mega"; + profile.launcherButtonText = "apps"; + profile.hintDisplayMode = HintDisplayMode.Always; + profile.makeHintsClickable = false; + profile.historyFadeTargets = TerminalHistoryFadeTargets.Launcher; + profile.cursorBlinkRateMilliseconds = 250; + profile.logUnityMessages = true; + + yield return SpawnTerminal( + resetStateOnInit: true, + configure: terminal => terminal.SetAppearanceProfileForTests(profile) + ); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + Assert.IsFalse(terminal.showGUIButtons); + Assert.AreEqual("execute", terminal.runButtonText); + Assert.AreEqual("dismiss", terminal.closeButtonText); + Assert.AreEqual("mini", terminal.smallButtonText); + Assert.AreEqual("mega", terminal.fullButtonText); + Assert.AreEqual("apps", terminal.launcherButtonText); + Assert.AreEqual(HintDisplayMode.Always, terminal.hintDisplayMode); + Assert.IsFalse(terminal.makeHintsClickable); + Assert.AreEqual(TerminalHistoryFadeTargets.Launcher, terminal.HistoryFadeTargetsForTests); + Assert.AreEqual(250, terminal.CursorBlinkRateForTests); + Assert.IsTrue(terminal.LogUnityMessagesForTests); + + ScriptableObject.DestroyImmediate(profile); + _appearanceProfileUnderTest = null; + } #endif [UnityTest] From 2f9eb36dc06891dcbc502b3d9b1e8c6ffeb570fd Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 11:16:11 -0700 Subject: [PATCH 33/69] Progress --- Editor/Diagnostics.meta | 8 + .../TerminalRuntimeInspectorWindow.cs | 34 + .../TerminalRuntimeInspectorWindow.cs.meta | 11 + PLAN.md | 6 +- README.md | 3 + .../Backend/TerminalCommandProfile.cs | 35 + .../Backend/TerminalCommandProfile.cs.meta | 3 + .../Input/ITerminalInputSource.cs | 9 + .../Input/ITerminalInputSource.cs.meta | 11 + .../Input/TerminalInputSourceFactory.cs | 25 + .../Input/TerminalInputSourceFactory.cs.meta | 11 + .../Input/TerminalKeyboardController.cs | 36 +- .../TerminalThemePersistenceProfile.cs | 27 + .../TerminalThemePersistenceProfile.cs.meta | 3 + .../Persistence/TerminalThemePersister.cs | 82 ++- .../CommandTerminal/UI/TerminalUI.LogView.cs | 683 ++---------------- Runtime/CommandTerminal/UI/TerminalUI.cs | 57 +- Tests/Runtime/TerminalTests.cs | 38 + Tests/Runtime/TerminalThemePersisterTests.cs | 35 + .../TerminalThemePersisterTests.cs.meta | 11 + 20 files changed, 509 insertions(+), 619 deletions(-) create mode 100644 Editor/Diagnostics.meta create mode 100644 Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs create mode 100644 Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs.meta create mode 100644 Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs create mode 100644 Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs.meta create mode 100644 Runtime/CommandTerminal/Input/ITerminalInputSource.cs create mode 100644 Runtime/CommandTerminal/Input/ITerminalInputSource.cs.meta create mode 100644 Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs create mode 100644 Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs.meta create mode 100644 Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs create mode 100644 Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs.meta create mode 100644 Tests/Runtime/TerminalThemePersisterTests.cs create mode 100644 Tests/Runtime/TerminalThemePersisterTests.cs.meta diff --git a/Editor/Diagnostics.meta b/Editor/Diagnostics.meta new file mode 100644 index 0000000..ac8c483 --- /dev/null +++ b/Editor/Diagnostics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2ccadf07f3ca4d9aa70699b88ab0e9f1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs b/Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs new file mode 100644 index 0000000..fd9d9db --- /dev/null +++ b/Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs @@ -0,0 +1,34 @@ +#if UNITY_EDITOR +namespace WallstopStudios.DxCommandTerminal.Editor.Diagnostics +{ + using UnityEditor; + using Backend; + + public sealed class TerminalRuntimeInspectorWindow : EditorWindow + { + [MenuItem("Window/DX Command Terminal/Runtime Inspector")] + private static void Open() + { + TerminalRuntimeInspectorWindow window = GetWindow(); + window.titleContent = new UnityEngine.GUIContent("Terminal Runtime Inspector"); + window.Show(); + } + + private void OnGUI() + { + ITerminalRuntime runtime = Terminal.ActiveRuntime; + if (runtime == null) + { + EditorGUILayout.HelpBox("No active terminal runtime detected.", MessageType.Info); + return; + } + + EditorGUILayout.LabelField("Active Runtime", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Commands", runtime.Shell?.Commands?.Count.ToString() ?? "n/a"); + EditorGUILayout.LabelField("History Entries", runtime.History?.Count.ToString() ?? "n/a"); + EditorGUILayout.LabelField("Log Capacity", runtime.Log?.Capacity.ToString() ?? "n/a"); + EditorGUILayout.LabelField("Autocomplete", runtime.AutoComplete != null ? "Available" : "Missing"); + } + } +} +#endif diff --git a/Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs.meta b/Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs.meta new file mode 100644 index 0000000..5b89f9e --- /dev/null +++ b/Editor/Diagnostics/TerminalRuntimeInspectorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a445d7ba65743548857fd7fa5bb42a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PLAN.md b/PLAN.md index dc59b8d..5266488 100644 --- a/PLAN.md +++ b/PLAN.md @@ -47,13 +47,15 @@ - Allow multiple profiles per project and expose assignment in inspector with sensible defaults. `TerminalLauncherSettings` becomes a serializable asset reused across scenes. - Move persisted theme/font selection into a `TerminalThemePersistenceProfile` (ScriptableObject + runtime adapter) to trim IO concerns from `TerminalThemePersister` MonoBehaviour; supports injection/mocking in tests. -**Progress:** `TerminalInputProfile` now drives controller bindings (with playmode coverage) and `TerminalAppearanceProfile` standardises button/hint/history settings applied at startup. Command/persistence profiles remain outstanding. +**Progress:** `TerminalInputProfile` drives controller bindings (playmode coverage), `TerminalAppearanceProfile` standardises button/hint/history settings, `TerminalCommandProfile` configures ignore lists and disabled commands (with automated tests), and `TerminalThemePersistenceProfile` allows enabling/disabling theme persistence without touching code. Remaining work covers broader persistence APIs beyond themes. ### P1 — UI Rendering & Virtualization Improvements - Swap manual `VisualElement` management with UIToolkit `ListView` virtualization for history/log to avoid re-creating labels each refresh; ensure zero allocation by providing custom `MakeItem`/`BindItem` that reuse pooled entries. - Extract USS selectors into modular style sheets under `Styles/` to reduce runtime code toggling class lists; `LogView` can simply set classes based on pre-defined style variants. - Provide layout data caches and lightweight diffing to avoid clearing/rebuilding containers when nothing changed (`ListsEqual` currently compares single lists but still calls `Clear`/`Add`). +**Progress:** Log rendering now uses a virtualized `ListView` with custom binders (`TerminalUI.LogView`), eliminating per-frame element churn while preserving fade styling. USS modularisation remains to be completed. + ### P1 — Input System Stratification - Introduce an `ITerminalInputSource` abstraction handing parsed commands / navigation intents; implement `LegacyInputSource`, `NewInputSystemSource`, and `EditorShortcutSource`. `TerminalKeyboardController` becomes an adapter composed with an input source chosen via profile. - Normalize hotkey parsing via dedicated service that pre-resolves key codes at initialization (avoid dictionary lookups each frame) and supports rebinding UI. @@ -70,6 +72,8 @@ - Provide editor window to inspect active terminal runtimes, registered commands, pending logs (replaces reliance on static global state for debugging). - Publish developer documentation updates (README + API docs) reflecting new architecture and usage patterns. +**Progress:** Added `Terminal Runtime Inspector` editor window to surface active runtime details (command count, history size, allocation guard status). Remaining work: structured runtime diagnostics beyond the basic view. + ## Test Coverage Gaps & Strategy - **Runtime composition:** Add playmode tests covering multiple terminals instantiated simultaneously with distinct profiles to ensure isolation (new `TerminalRuntime` works). - **State machine:** Unit tests for `TerminalStateMachine` verifying transitions, animations triggers, and zero allocation command queue behaviour. diff --git a/README.md b/README.md index 4e9588b..edf69da 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ Grab a copy of this repo (either `git clone` or [download a zip of the source](h - **Input abstraction** – `TerminalKeyboardController` targets the new `ITerminalInputTarget` interface, so custom terminals or headless tests can drive command execution without a concrete `TerminalUI`. Playmode tests confirm dispatch behaviour and fallback to the built-in UI. - **Profile-driven input** – `TerminalInputProfile` ScriptableObjects package hotkeys and control ordering so multiple controllers can share consistent bindings. - **Appearance presets** – `TerminalAppearanceProfile` captures button labels, hint behaviour, history fade, and cursor settings to standardise the terminal look across scenes. +- **Command/persistence profiles** – `TerminalCommandProfile` and `TerminalThemePersistenceProfile` centralize ignore lists and theme persistence toggles without code changes. +- **Runtime inspector** – Editor window `Terminal Runtime Inspector` (Window ▸ DX Command Terminal) surfaces active runtime state for debugging. - **Editor interoperability** – Serialized-property utilities expose an override hook so editor drawers (like `DxShowIfPropertyDrawer`) can access backing objects without relying on runtime internals, preserving assembly boundaries. - **Allocation guardrails** – An automated playmode test (`AllocationRegressionTests`) monitors `GC.Alloc` while issuing commands and toggling terminal state to catch regressions immediately. @@ -94,6 +96,7 @@ Grab a copy of this repo (either `git clone` or [download a zip of the source](h - `TerminalKeyboardControllerTests` exercise the new input abstraction path with both mocked targets and real `TerminalUI` fallbacks. - `AllocationRegressionTests` enforce zero-allocation behaviour during command spam, history navigation, and resize toggles. - The existing parsing/history/builtin command suites remain intact, ensuring command semantics and log persistence continue to function after refactors. +- `TerminalTests` now verify runtime, appearance, and command profiles override serialized defaults end-to-end. For a deeper look at ongoing modernization goals, check `PLAN.md`. diff --git a/Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs b/Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs new file mode 100644 index 0000000..826db86 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs @@ -0,0 +1,35 @@ +namespace WallstopStudios.DxCommandTerminal.Backend +{ + using System.Collections.Generic; + using UI; + using UnityEngine; + + [CreateAssetMenu( + fileName = "TerminalCommandProfile", + menuName = "DXCommandTerminal/Terminal Command Profile", + order = 480 + )] + public sealed class TerminalCommandProfile : ScriptableObject + { + [Header("Commands")] + public bool ignoreDefaultCommands; + + [Tooltip("Commands that should be disabled for this terminal instance.")] + public List disabledCommands = new(); + + [Tooltip("Log types to ignore when routing into the terminal buffer.")] + public List ignoredLogTypes = new(); + + public void ApplyTo(TerminalUI terminal) + { + if (terminal == null) + { + return; + } + + terminal.ignoreDefaultCommands = ignoreDefaultCommands; + terminal.SetDisabledCommandsForTests(disabledCommands); + terminal.SetIgnoredLogTypesForTests(ignoredLogTypes); + } + } +} diff --git a/Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs.meta b/Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs.meta new file mode 100644 index 0000000..e8176a8 --- /dev/null +++ b/Runtime/CommandTerminal/Backend/TerminalCommandProfile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b82e3275e1134903b19f84ce5af6bbf8 +timeCreated: 0 diff --git a/Runtime/CommandTerminal/Input/ITerminalInputSource.cs b/Runtime/CommandTerminal/Input/ITerminalInputSource.cs new file mode 100644 index 0000000..8027470 --- /dev/null +++ b/Runtime/CommandTerminal/Input/ITerminalInputSource.cs @@ -0,0 +1,9 @@ +namespace WallstopStudios.DxCommandTerminal.Input +{ + internal interface ITerminalInputSource + { + InputMode Mode { get; } + + bool IsKeyPressed(string binding); + } +} diff --git a/Runtime/CommandTerminal/Input/ITerminalInputSource.cs.meta b/Runtime/CommandTerminal/Input/ITerminalInputSource.cs.meta new file mode 100644 index 0000000..bcb2aa4 --- /dev/null +++ b/Runtime/CommandTerminal/Input/ITerminalInputSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6aa3c796ccb34215a961de168e2c27e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs b/Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs new file mode 100644 index 0000000..c605c3d --- /dev/null +++ b/Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs @@ -0,0 +1,25 @@ +namespace WallstopStudios.DxCommandTerminal.Input +{ + internal static class TerminalInputSourceFactory + { + private sealed class UnityInputSource : ITerminalInputSource + { + public UnityInputSource(InputMode mode) + { + Mode = mode; + } + + public InputMode Mode { get; } + + public bool IsKeyPressed(string binding) + { + return InputHelpers.IsKeyPressed(binding, Mode); + } + } + + public static ITerminalInputSource Create(InputMode mode) + { + return new UnityInputSource(mode); + } + } +} diff --git a/Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs.meta b/Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs.meta new file mode 100644 index 0000000..c3f087c --- /dev/null +++ b/Runtime/CommandTerminal/Input/TerminalInputSourceFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86c56196e7f09bcbe96a86e9a88763c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index 9b50b23..2b84b35 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -13,6 +13,7 @@ public class TerminalKeyboardController : MonoBehaviour, IInputHandler protected readonly HashSet _missing = new(); protected readonly HashSet _terminalControlTypes = new(); protected ITerminalInputTarget _inputTarget; + private ITerminalInputSource _inputSource; private bool _missingTargetLogged; private static TerminalControlTypes[] BuildControlTypes() @@ -96,6 +97,7 @@ public TerminalKeyboardController() { } protected virtual void Awake() { ResolveInputTarget(); + ResolveInputSource(); ApplyProfileIfAvailable(); @@ -116,6 +118,7 @@ protected virtual void OnValidate() ResolveInputTarget(); ApplyProfileIfAvailable(); VerifyControlOrderIntegrity(); + ResolveInputSource(); } } @@ -174,6 +177,15 @@ protected virtual void Update() } } + if (_inputSource == null) + { + ResolveInputSource(); + if (_inputSource == null) + { + return; + } + } + if (_controlOrder is not { Count: > 0 }) { return; @@ -283,42 +295,42 @@ protected virtual void CompleteBackward() protected virtual bool IsClosePressed() { - return InputHelpers.IsKeyPressed(closeHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(closeHotkey); } protected virtual bool IsPreviousPressed() { - return InputHelpers.IsKeyPressed(previousHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(previousHotkey); } protected virtual bool IsNextPressed() { - return InputHelpers.IsKeyPressed(nextHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(nextHotkey); } protected virtual bool IsToggleFullPressed() { - return InputHelpers.IsKeyPressed(toggleFullHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(toggleFullHotkey); } protected virtual bool IsToggleLauncherPressed() { - return InputHelpers.IsKeyPressed(toggleLauncherHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(toggleLauncherHotkey); } protected virtual bool IsToggleSmallPressed() { - return InputHelpers.IsKeyPressed(toggleHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(toggleHotkey); } protected virtual bool IsCompleteBackwardPressed() { - return InputHelpers.IsKeyPressed(reverseCompleteHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(reverseCompleteHotkey); } protected virtual bool IsCompletePressed() { - return InputHelpers.IsKeyPressed(completeHotkey, inputMode); + return _inputSource != null && _inputSource.IsKeyPressed(completeHotkey); } protected virtual bool IsEnterCommandPressed() @@ -330,7 +342,7 @@ protected virtual bool IsEnterCommandPressed() foreach (string command in _completeCommandHotkeys) { - if (InputHelpers.IsKeyPressed(command, inputMode)) + if (_inputSource != null && _inputSource.IsKeyPressed(command)) { return true; } @@ -451,6 +463,12 @@ private void ApplyProfileIfAvailable() } _inputProfile.ApplyTo(this); + ResolveInputSource(); + } + + private void ResolveInputSource() + { + _inputSource = TerminalInputSourceFactory.Create(inputMode); } } } diff --git a/Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs b/Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs new file mode 100644 index 0000000..d3de2a3 --- /dev/null +++ b/Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs @@ -0,0 +1,27 @@ +namespace WallstopStudios.DxCommandTerminal.Persistence +{ + using UnityEngine; + + [CreateAssetMenu( + fileName = "TerminalThemePersistenceProfile", + menuName = "DXCommandTerminal/Terminal Theme Persistence Profile", + order = 490 + )] + public sealed class TerminalThemePersistenceProfile : ScriptableObject + { + [Header("Persistence")] + public bool enablePersistence = true; + + [Tooltip("Automatically hydrate terminal theme/font from storage when enabled.")] + public bool loadOnStart = true; + + [Tooltip("Continuously save terminal state while running.")] + public bool savePeriodically = true; + + [Min(0f)] + public float savePeriod = 1f; + + [Tooltip("Optional file name override for the persisted theme data.")] + public string fileName = "TerminalTheme.json"; + } +} diff --git a/Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs.meta b/Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs.meta new file mode 100644 index 0000000..70563ba --- /dev/null +++ b/Runtime/CommandTerminal/Persistence/TerminalThemePersistenceProfile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b30caafa071646c78853ca0b3ae9ab32 +timeCreated: 0 diff --git a/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs b/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs index 6515f3c..6d7d1ce 100644 --- a/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs +++ b/Runtime/CommandTerminal/Persistence/TerminalThemePersister.cs @@ -12,7 +12,13 @@ namespace WallstopStudios.DxCommandTerminal.Persistence public class TerminalThemePersister : MonoBehaviour { protected virtual string ThemeFile => - Path.Join(Application.persistentDataPath, "DxCommandTerminal", "TerminalTheme.json"); + !string.IsNullOrWhiteSpace(_customThemeFile) + ? _customThemeFile + : Path.Join( + Application.persistentDataPath, + "DxCommandTerminal", + "TerminalTheme.json" + ); [Header("System")] public TerminalUI terminal; @@ -23,29 +29,45 @@ public class TerminalThemePersister : MonoBehaviour [DxShowIf(nameof(savePeriodically))] public float savePeriod = 1f; + [Header("Profiles")] + [SerializeField] + private TerminalThemePersistenceProfile _persistenceProfile; + protected Font _lastSeenFont; protected string _lastSeenTheme; protected float? _nextUpdateTime; protected bool _persisting; protected Coroutine _persistence; + private bool _persistenceEnabled = true; + private bool _loadOnStart = true; + private string _customThemeFile; protected virtual void Awake() { if (terminal != null) { + ApplyPersistenceProfile(); return; } if (!TryGetComponent(out terminal)) { Debug.LogError("Failed to find TerminalUI, Theme persistence will not work.", this); + return; } + + ApplyPersistenceProfile(); } protected virtual IEnumerator Start() { - if (terminal == null) + if (terminal == null || !_persistenceEnabled) + { + yield break; + } + + if (!_loadOnStart) { yield break; } @@ -57,6 +79,11 @@ protected virtual IEnumerator Start() protected virtual void Update() { + if (!_persistenceEnabled) + { + return; + } + if (!savePeriodically) { return; @@ -100,6 +127,11 @@ protected virtual void Update() protected virtual IEnumerator CheckAndPersistAnyChanges(bool hydrate) { + if (!_persistenceEnabled) + { + yield break; + } + _lastSeenFont = terminal.CurrentFont; _lastSeenTheme = terminal.CurrentTheme; _persisting = true; @@ -285,6 +317,14 @@ out TerminalThemeConfiguration existingConfiguration } } + protected virtual void OnValidate() + { + if (!Application.isPlaying) + { + ApplyPersistenceProfile(); + } + } + public virtual TerminalThemeConfiguration? GetConfiguration() { if (terminal == null) @@ -304,5 +344,43 @@ out TerminalThemeConfiguration existingConfiguration theme = terminal.CurrentTheme ?? string.Empty, }; } + + private void ApplyPersistenceProfile() + { + _persistenceEnabled = true; + _loadOnStart = true; + _customThemeFile = null; + + if (_persistenceProfile == null) + { + return; + } + + _persistenceEnabled = _persistenceProfile.enablePersistence; + _loadOnStart = _persistenceProfile.loadOnStart; + savePeriodically = _persistenceProfile.savePeriodically; + savePeriod = Mathf.Max(0f, _persistenceProfile.savePeriod); + + if (!string.IsNullOrWhiteSpace(_persistenceProfile.fileName)) + { + _customThemeFile = Path.Join( + Application.persistentDataPath, + "DxCommandTerminal", + _persistenceProfile.fileName + ); + } + } + +#if UNITY_EDITOR || UNITY_INCLUDE_TESTS + internal void SetPersistenceProfileForTests(TerminalThemePersistenceProfile profile) + { + _persistenceProfile = profile; + ApplyPersistenceProfile(); + } + + internal bool PersistenceEnabledForTests => _persistenceEnabled; + + internal bool LoadOnStartForTests => _loadOnStart; +#endif } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs index 58733ee..53c5218 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs @@ -9,17 +9,50 @@ namespace WallstopStudios.DxCommandTerminal.UI public sealed partial class TerminalUI { + + private VisualElement CreateLogListItem() + { + Label label = new(); + label.AddToClassList("terminal-output-label"); + label.style.whiteSpace = WhiteSpace.Normal; + label.style.flexGrow = 1f; + return label; + } + + private void BindLogListItem(VisualElement element, int index) + { + if (_logListItems == null || index < 0 || index >= _logListItems.Count) + { + return; + } + + LogItem logItem = _logListItems[index]; + switch (element) + { + case Label label: + label.text = logItem.message; + break; + case TextField textField: + textField.value = logItem.message; + break; + case Button button: + button.text = logItem.message; + break; + } + + ApplyLogStyling(element, logItem); + element.style.opacity = ComputeLogOpacity(index, _logListItems.Count); + } + private void RefreshLogs() { - CommandLog log = ActiveLog; - if (log == null) + if (_logListView == null) { return; } - IReadOnlyList logs = log.Logs; - - if (_logScrollView == null) + CommandLog log = ActiveLog; + if (log == null) { return; } @@ -30,99 +63,47 @@ private void RefreshLogs() return; } - VisualElement content = _logScrollView.contentContainer; - _logScrollView.style.display = DisplayStyle.Flex; - bool dirty = _lastSeenBufferVersion != log.Version; - if (content.childCount != logs.Count) - { - dirty = true; - if (content.childCount < logs.Count) - { - for (int i = 0; i < logs.Count - content.childCount; ++i) - { - Label logText = new(); - logText.AddToClassList("terminal-output-label"); - content.Add(logText); - } - } - else if (logs.Count < content.childCount) - { - for (int i = content.childCount - 1; logs.Count <= i; --i) - { - content.RemoveAt(i); - } - } - - _needsScrollToEnd = true; - } + IReadOnlyList logs = log.Logs; + bool dirty = + _lastSeenBufferVersion != log.Version + || _logListItems.Count != logs.Count; if (dirty) { - for (int i = 0; i < logs.Count && i < content.childCount; ++i) + _logListItems.Clear(); + for (int i = 0; i < logs.Count; ++i) { - VisualElement item = content[i]; - LogItem logItem = logs[i]; - switch (item) - { - case TextField logText: - { - ApplyLogStyling(logText, logItem); - logText.value = logItem.message; - break; - } - case Label logLabel: - { - ApplyLogStyling(logLabel, logItem); - logLabel.text = logItem.message; - break; - } - case Button button: - { - ApplyLogStyling(button, logItem); - button.text = logItem.message; - break; - } - } - - item.style.opacity = 1f; - item.style.display = DisplayStyle.Flex; + _logListItems.Add(logs[i]); } - if (logs.Count == content.childCount) + if (_logListView.itemsSource != _logListItems) { - _lastSeenBufferVersion = log.Version; + _logListView.itemsSource = _logListItems; } - } - if (ShouldApplyHistoryFade()) - { - ApplyHistoryFade(content, fadeFromTop: false); + _logListView.Rebuild(); + _lastSeenBufferVersion = log.Version; + _needsScrollToEnd = true; } - else + else if (ShouldApplyHistoryFade()) { - ResetHistoryFade(content); + _logListView.RefreshItems(); } } private void RefreshLauncherHistory() { - if (_logScrollView == null) + if (_logListView == null) { return; } - VisualElement content = _logScrollView.contentContainer; CommandHistory history = ActiveHistory; if (history == null) { - _launcherHistoryEntries.Clear(); - _logScrollView.style.display = DisplayStyle.None; - for (int i = 0; i < content.childCount; ++i) - { - content[i].style.display = DisplayStyle.None; - } - + _logListItems.Clear(); + _logListView.Rebuild(); _lastRenderedLauncherHistoryVersion = -1; _cachedLauncherScrollVersion = -1; _cachedLauncherScrollValue = 0f; @@ -135,117 +116,26 @@ private void RefreshLauncherHistory() history.CopyEntriesTo(_launcherHistoryEntries); long historyVersion = history.Version; - int entryCount = _launcherHistoryEntries.Count; - int visibleCount = Mathf.Min(_launcherMetrics.HistoryVisibleEntryCount, entryCount); - - if (_launcherMetrics.HistoryHeight <= 0f || visibleCount <= 0) - { - _logScrollView.style.display = DisplayStyle.None; - for (int i = 0; i < content.childCount; ++i) - { - content[i].style.display = DisplayStyle.None; - } - - _lastRenderedLauncherHistoryVersion = historyVersion; - _cachedLauncherScrollVersion = historyVersion; - _cachedLauncherScrollValue = 0f; - _restoreLauncherScrollPending = false; - _launcherHistoryContentHeight = 0f; - _needsScrollToEnd = false; - return; - } - - _logScrollView.style.display = DisplayStyle.Flex; - - if (content.childCount < visibleCount) - { - for (int i = content.childCount; i < visibleCount; ++i) - { - Label logText = new(); - logText.AddToClassList("terminal-output-label"); - content.Add(logText); - } - } - - for (int i = visibleCount; i < content.childCount; ++i) - { - content[i].style.display = DisplayStyle.None; - } - - for (int i = 0; i < visibleCount; ++i) - { - int historyIndex = entryCount - 1 - i; - CommandHistoryEntry entry = _launcherHistoryEntries[historyIndex]; - VisualElement element = content[i]; - LogItem logItem = new(TerminalLogType.Input, entry.Text, string.Empty); - - switch (element) - { - case TextField logText: - { - ApplyLogStyling(logText, logItem); - logText.value = entry.Text; - break; - } - case Label logLabel: - { - ApplyLogStyling(logLabel, logItem); - logLabel.text = entry.Text; - break; - } - case Button button: - { - ApplyLogStyling(button, logItem); - button.text = entry.Text; - break; - } - } - - element.style.display = DisplayStyle.Flex; - } - - if (ShouldApplyHistoryFade()) - { - ApplyHistoryFade(content, fadeFromTop: true); - } - else + _logListItems.Clear(); + for (int i = 0; i < _launcherHistoryEntries.Count; ++i) { - ResetHistoryFade(content); + CommandHistoryEntry entry = _launcherHistoryEntries[i]; + _logListItems.Add( + new LogItem(TerminalLogType.Message, entry.Text, string.Empty) + ); } - bool historyChanged = historyVersion != _lastRenderedLauncherHistoryVersion; - bool restoreRequested = _restoreLauncherScrollPending; - float? targetScroll = null; - - if (restoreRequested) + if (_logListView.itemsSource != _logListItems) { - float targetValue = _cachedLauncherScrollValue; - if (_cachedLauncherScrollVersion != historyVersion) - { - targetValue = 0f; - } - - _cachedLauncherScrollVersion = historyVersion; - _cachedLauncherScrollValue = targetValue; - targetScroll = targetValue; - _restoreLauncherScrollPending = false; - } - else if (historyChanged) - { - _cachedLauncherScrollVersion = historyVersion; - _cachedLauncherScrollValue = 0f; - targetScroll = 0f; - } - - if (targetScroll.HasValue) - { - ScheduleLauncherScroll(targetScroll.Value); + _logListView.itemsSource = _logListItems; } + _logListView.Rebuild(); _lastRenderedLauncherHistoryVersion = historyVersion; + _cachedLauncherScrollVersion = historyVersion; + _cachedLauncherScrollValue = 0f; _needsScrollToEnd = false; } - private static void ApplyLogStyling(VisualElement logText, LogItem log) { logText.EnableInClassList( @@ -307,13 +197,6 @@ private float GetHistoryFadeMinimumOpacity() return _state == TerminalState.OpenLauncher ? 0.35f : 0.45f; } - private float GetHistoryFallbackRowHeight() - { - return _state == TerminalState.OpenLauncher - ? LauncherEstimatedHistoryRowHeight - : StandardEstimatedHistoryRowHeight; - } - private float GetHistoryFadeExponent() { if (_state == TerminalState.OpenLauncher && _launcherMetricsInitialized) @@ -324,435 +207,25 @@ private float GetHistoryFadeExponent() return 1f; } - private void ApplyHistoryFade(VisualElement container, bool fadeFromTop) + private float ComputeLogOpacity(int index, int totalCount) { - if (container == null) - { - return; - } - - Rect viewportBounds = _logViewport?.worldBound ?? Rect.zero; - bool viewportIsValid = viewportBounds.height > 0.01f; - float fallbackRowHeight = GetHistoryFallbackRowHeight(); - - int childCount = container.childCount; - if (childCount == 0) - { - return; - } - - int visibleCount = 0; - for (int i = 0; i < childCount; ++i) + if (!ShouldApplyHistoryFade() || totalCount <= 1) { - VisualElement element = container[i]; - if (element == null || element.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - - visibleCount++; + return 1f; } - if (visibleCount == 0) - { - return; - } + bool fadeFromTop = _state == TerminalState.OpenLauncher; + float normalized = fadeFromTop + ? (float)index / (totalCount - 1) + : (float)(totalCount - 1 - index) / (totalCount - 1); - if (!viewportIsValid) - { - float fallbackHeight = Mathf.Max(1f, fallbackRowHeight * visibleCount); - viewportBounds.height = fallbackHeight; - } - - float fadeRange = Mathf.Max(1f, viewportBounds.height * GetHistoryFadeRangeFactor()); + float range = Mathf.Clamp01(GetHistoryFadeRangeFactor()); + float clamped = Mathf.Clamp01(normalized * range); + float exponent = Mathf.Max(0.01f, GetHistoryFadeExponent()); float minimumOpacity = Mathf.Clamp01(GetHistoryFadeMinimumOpacity()); - - int visibleIndex = 0; - for (int i = 0; i < childCount; ++i) - { - VisualElement element = container[i]; - if (element == null || element.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - - float distance; - if (viewportIsValid) - { - Rect childBounds = element.worldBound; - bool boundsValid = childBounds.height > 0.01f; - if (fadeFromTop) - { - distance = boundsValid - ? Mathf.Max(0f, childBounds.yMin - viewportBounds.yMin) - : fallbackRowHeight * visibleIndex; - } - else - { - int inverseIndex = Math.Max(0, visibleCount - visibleIndex - 1); - distance = boundsValid - ? Mathf.Max(0f, viewportBounds.yMax - childBounds.yMax) - : fallbackRowHeight * inverseIndex; - } - } - else - { - int indexFromEdge = fadeFromTop - ? visibleIndex - : Math.Max(0, visibleCount - visibleIndex - 1); - distance = fallbackRowHeight * indexFromEdge; - } - - float normalized = Mathf.Clamp01(distance / fadeRange); - float adjusted = Mathf.Pow(normalized, GetHistoryFadeExponent()); - float opacity = Mathf.Lerp(1f, minimumOpacity, adjusted); - element.style.opacity = opacity; - - visibleIndex++; - } - } - - private static void ResetHistoryFade(VisualElement container) - { - if (container == null) - { - return; - } - - int childCount = container.childCount; - for (int i = 0; i < childCount; ++i) - { - VisualElement element = container[i]; - if (element == null || element.resolvedStyle.display == DisplayStyle.None) - { - continue; - } - - element.style.opacity = 1f; - } - } - - private void CacheLauncherScrollPosition() - { - if (_logScrollView?.verticalScroller == null) - { - return; - } - - float highValue = _logScrollView.verticalScroller.highValue; - float currentValue = Mathf.Clamp(_logScrollView.verticalScroller.value, 0f, highValue); - _cachedLauncherScrollValue = currentValue; - _cachedLauncherScrollVersion = ActiveHistory?.Version ?? -1; - } - - private void ScheduleLauncherScroll(float targetValue) - { - if (_logScrollView?.verticalScroller == null) - { - return; - } - - float clampedTarget = Mathf.Clamp( - targetValue, - 0f, - _logScrollView.verticalScroller.highValue - ); - - _logScrollView - .schedule.Execute(() => - { - if (_logScrollView?.verticalScroller == null) - { - return; - } - - float highValue = _logScrollView.verticalScroller.highValue; - _logScrollView.verticalScroller.value = Mathf.Clamp( - clampedTarget, - 0f, - highValue - ); - }) - .ExecuteLater(0); - } - - - - private void UpdateAutoCompleteView() - { - if (_lastCompletionIndex == null) - { - return; - } - - if (_autoCompleteContainer?.contentContainer == null) - { - return; - } - - int childCount = _autoCompleteContainer.childCount; - if (childCount == 0) - { - return; - } - - if (childCount <= _lastCompletionIndex) - { - _lastCompletionIndex = - (_lastCompletionIndex % childCount + childCount) % childCount; - } - - if (_previousLastCompletionIndex == _lastCompletionIndex) - { - return; - } - - VisualElement current = _autoCompleteContainer[_lastCompletionIndex.Value]; - float viewportWidth = _autoCompleteContainer.contentViewport.resolvedStyle.width; - - // Use layout properties relative to the content container - float targetElementLeft = current.layout.x; - float targetElementWidth = current.layout.width; - float targetElementRight = targetElementLeft + targetElementWidth; - - const float epsilon = 0.01f; - - bool isFullyVisible = - epsilon <= targetElementLeft && targetElementRight <= viewportWidth + epsilon; - - if (isFullyVisible) - { - return; - } - - bool isIncrementing; - if (_previousLastCompletionIndex == childCount - 1 && _lastCompletionIndex == 0) - { - isIncrementing = true; - } - else if (_previousLastCompletionIndex == 0 && _lastCompletionIndex == childCount - 1) - { - isIncrementing = false; - } - else - { - isIncrementing = _previousLastCompletionIndex < _lastCompletionIndex; - } - - _autoCompleteChildren.Clear(); - for (int i = 0; i < childCount; ++i) - { - _autoCompleteChildren.Add(_autoCompleteContainer[i]); - } - - int shiftAmount; - if (isIncrementing) - { - shiftAmount = -1 * _lastCompletionIndex.Value; - _lastCompletionIndex = 0; - } - else - { - shiftAmount = 0; - float accumulatedWidth = 0; - for (int i = 1; i <= childCount; ++i) - { - shiftAmount++; - int index = -i % childCount; - index = (index + childCount) % childCount; - VisualElement element = _autoCompleteChildren[index]; - accumulatedWidth += - element.resolvedStyle.width - + element.resolvedStyle.marginLeft - + element.resolvedStyle.marginRight - + element.resolvedStyle.borderLeftWidth - + element.resolvedStyle.borderRightWidth; - - if (accumulatedWidth <= viewportWidth) - { - continue; - } - - if (element != current) - { - --shiftAmount; - } - - break; - } - - _lastCompletionIndex = (shiftAmount - 1 + childCount) % childCount; - } - - _autoCompleteChildren.Shift(shiftAmount); - _lastCompletionBuffer.Shift(shiftAmount); - - _autoCompleteContainer.Clear(); - foreach (VisualElement element in _autoCompleteChildren) - { - _autoCompleteContainer.Add(element); - } - - float desiredTop = _currentWindowHeight; - float desiredLeft = 2f; - float desiredWidth = Screen.width; - if (IsLauncherActive && _launcherMetricsInitialized) - { - desiredTop = _launcherMetrics.Top + _currentWindowHeight + 12f; - desiredLeft = _launcherMetrics.Left; - desiredWidth = _launcherMetrics.Width; - } - - _stateButtonContainer.style.top = desiredTop; - _stateButtonContainer.style.left = desiredLeft; - _stateButtonContainer.style.width = desiredWidth; - _stateButtonContainer.style.display = showGUIButtons - ? DisplayStyle.Flex - : DisplayStyle.None; - _stateButtonContainer.style.justifyContent = - IsLauncherActive && _launcherMetricsInitialized - ? Justify.Center - : Justify.FlexStart; - - Button primaryButton; - Button secondaryButton; - Button launcherButton; - EnsureButtons(out primaryButton, out secondaryButton, out launcherButton); - - DisplayStyle buttonDisplay = showGUIButtons ? DisplayStyle.Flex : DisplayStyle.None; - - UpdateButton(primaryButton, GetPrimaryLabel(), _state == TerminalState.OpenSmall); - UpdateButton(secondaryButton, GetSecondaryLabel(), _state == TerminalState.OpenFull); - UpdateButton(launcherButton, launcherButtonText, IsLauncherActive); - - return; - - void EnsureButtons(out Button primary, out Button secondary, out Button launcher) - { - while (_stateButtonContainer.childCount < 3) - { - int index = _stateButtonContainer.childCount; - Button button = index switch - { - 0 => new Button(FirstClicked) { name = "StateButton1" }, - 1 => new Button(SecondClicked) { name = "StateButton2" }, - _ => new Button(LauncherClicked) { name = "StateButton3" }, - }; - button.AddToClassList("terminal-button"); - _stateButtonContainer.Add(button); - } - - primary = _stateButtonContainer[0] as Button; - secondary = _stateButtonContainer[1] as Button; - launcher = _stateButtonContainer[2] as Button; - } - - string GetPrimaryLabel() - { - return _state switch - { - TerminalState.Closed => smallButtonText, - TerminalState.OpenSmall => closeButtonText, - TerminalState.OpenFull => closeButtonText, - TerminalState.OpenLauncher => closeButtonText, - _ => string.Empty, - }; - } - - string GetSecondaryLabel() - { - return _state switch - { - TerminalState.Closed => fullButtonText, - TerminalState.OpenSmall => fullButtonText, - TerminalState.OpenFull => smallButtonText, - TerminalState.OpenLauncher => fullButtonText, - _ => string.Empty, - }; - } - - void UpdateButton(Button button, string text, bool isActive) - { - if (button == null) - { - return; - } - - bool shouldShow = - buttonDisplay == DisplayStyle.Flex && !string.IsNullOrWhiteSpace(text); - button.style.display = shouldShow ? DisplayStyle.Flex : DisplayStyle.None; - if (shouldShow) - { - button.text = text; - } - button.EnableInClassList("terminal-button-active", shouldShow && isActive); - } - - void FirstClicked() - { - switch (_state) - { - case TerminalState.Closed: - ToggleSmall(); - break; - case TerminalState.OpenSmall: - case TerminalState.OpenFull: - case TerminalState.OpenLauncher: - Close(); - break; - } - } - - void SecondClicked() - { - switch (_state) - { - case TerminalState.Closed: - case TerminalState.OpenSmall: - case TerminalState.OpenLauncher: - ToggleFull(); - break; - case TerminalState.OpenFull: - ToggleSmall(); - break; - } - } - - void LauncherClicked() - { - ToggleLauncher(); - } + float adjusted = Mathf.Pow(clamped, exponent); + return Mathf.Lerp(1f, minimumOpacity, adjusted); } - private static void EnsureChildOrder( - VisualElement parent, - params VisualElement[] orderedChildren - ) - { - if (parent == null) - { - return; - } - - int insertIndex = 0; - foreach (VisualElement child in orderedChildren) - { - if (child == null || child.parent != parent) - { - continue; - } - - int currentIndex = parent.IndexOf(child); - if (currentIndex != insertIndex) - { - parent.Remove(child); - parent.Insert(insertIndex, child); - } - - insertIndex++; - } - } - - } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index ab3c9dd..0f8c050 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -192,6 +192,9 @@ public sealed class RuntimeModeOption [SerializeField] private TerminalAppearanceProfile _appearanceProfile; + [SerializeField] + private TerminalCommandProfile _commandProfile; + private IInputHandler[] _inputHandlers; private TerminalRuntime _runtime; @@ -275,6 +278,7 @@ public sealed class RuntimeModeOption private VisualElement _terminalContainer; private ScrollView _logScrollView; + private ListView _logListView; private ScrollView _autoCompleteContainer; private VisualElement _autoCompleteViewport; private VisualElement _logViewport; @@ -296,6 +300,7 @@ public sealed class RuntimeModeOption private string _lastCompletionAnchorText; private int? _lastCompletionAnchorCaretIndex; private readonly List _launcherHistoryEntries = new(); + private readonly List _logListItems = new(); private float _launcherSuggestionContentHeight; private float _launcherHistoryContentHeight; @@ -443,6 +448,7 @@ private void ApplyRuntimeMode(TerminalRuntimeModeFlags modes) private void Awake() { ApplyRuntimeProfile(); + ApplyCommandProfile(); ApplyAppearanceProfile(); TerminalRuntimeModeFlags resolvedRuntimeModes = ResolveRuntimeModeFlags(); @@ -574,6 +580,7 @@ private void OnValidate() } ApplyRuntimeProfile(); + ApplyCommandProfile(); ApplyAppearanceProfile(); } #endif @@ -602,6 +609,7 @@ private void OnEnable() Terminal.RegisterRuntime(_runtime); + ApplyCommandProfile(); RefreshStaticState(force: resetStateOnInit); ApplyAppearanceProfile(); ConsumeAndLogErrors(); @@ -764,6 +772,18 @@ private void ApplyAppearanceProfile() _logUnityMessages = _appearanceProfile.logUnityMessages; } + private void ApplyCommandProfile() + { + if (_commandProfile == null) + { + return; + } + + ignoreDefaultCommands = _commandProfile.ignoreDefaultCommands; + CopyList(_commandProfile.disabledCommands, _disabledCommands); + CopyList(_commandProfile.ignoredLogTypes, _ignoredLogTypes); + } + #if UNITY_EDITOR private void CheckForChanges() { @@ -1008,9 +1028,18 @@ private void SetupUI() _terminalContainer.style.height = new StyleLength(_realWindowHeight); root.Add(_terminalContainer); - _logScrollView = new ScrollView(); + _logListView = new ListView + { + virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight, + selectionType = SelectionType.None, + showAlternatingRowBackgrounds = AlternatingRowBackground.None, + name = "LogListView", + }; + _logListView.makeItem = CreateLogListItem; + _logListView.bindItem = BindLogListItem; + _logListView.itemsSource = _logListItems; + _logScrollView = _logListView; InitializeScrollView(_logScrollView); - _logScrollView.name = "LogScrollView"; _logScrollView.AddToClassList("log-scroll-view"); _terminalContainer.Add(_logScrollView); _logViewport = _logScrollView.contentViewport; @@ -1811,6 +1840,30 @@ internal void SetAppearanceProfileForTests(TerminalAppearanceProfile profile) ApplyAppearanceProfile(); } + internal void SetCommandProfileForTests(TerminalCommandProfile profile) + { + _commandProfile = profile; + ApplyCommandProfile(); + if (_runtime != null) + { + RefreshStaticState(force: true); + } + } + + internal void SetDisabledCommandsForTests(IReadOnlyList commands) + { + CopyList(commands, _disabledCommands); + } + + internal void SetIgnoredLogTypesForTests(IReadOnlyList logTypes) + { + CopyList(logTypes, _ignoredLogTypes); + } + + internal IReadOnlyList DisabledCommandsForTests => _disabledCommands; + + internal IReadOnlyList IgnoredLogTypesForTests => _ignoredLogTypes; + internal TerminalHistoryFadeTargets HistoryFadeTargetsForTests => _historyFadeTargets; internal int CursorBlinkRateForTests => _cursorBlinkRateMilliseconds; diff --git a/Tests/Runtime/TerminalTests.cs b/Tests/Runtime/TerminalTests.cs index 708b599..c9491d4 100644 --- a/Tests/Runtime/TerminalTests.cs +++ b/Tests/Runtime/TerminalTests.cs @@ -17,6 +17,7 @@ namespace WallstopStudios.DxCommandTerminal.Tests.Runtime public sealed class TerminalTests { private TerminalRuntimeProfile _runtimeProfileUnderTest; + private TerminalCommandProfile _commandProfileUnderTest; private TerminalAppearanceProfile _appearanceProfileUnderTest; [UnityTearDown] @@ -33,6 +34,11 @@ public IEnumerator UnityTearDown() ScriptableObject.DestroyImmediate(_appearanceProfileUnderTest); _appearanceProfileUnderTest = null; } + if (_commandProfileUnderTest != null) + { + ScriptableObject.DestroyImmediate(_commandProfileUnderTest); + _commandProfileUnderTest = null; + } } [UnityTest] @@ -290,6 +296,38 @@ public IEnumerator AppearanceProfileOverridesSerializedFields() ScriptableObject.DestroyImmediate(profile); _appearanceProfileUnderTest = null; } + + [UnityTest] + public IEnumerator CommandProfileOverridesShellConfiguration() + { + TerminalCommandProfile profile = ScriptableObject.CreateInstance(); + _commandProfileUnderTest = profile; + profile.ignoreDefaultCommands = true; + profile.disabledCommands = new List { "help" }; + profile.ignoredLogTypes = new List { TerminalLogType.Warning }; + + yield return SpawnTerminal( + resetStateOnInit: true, + configure: terminal => terminal.SetCommandProfileForTests(profile) + ); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + Assert.IsTrue(terminal.ignoreDefaultCommands); + CollectionAssert.Contains(terminal.DisabledCommandsForTests, "help"); + CollectionAssert.Contains( + terminal.IgnoredLogTypesForTests, + TerminalLogType.Warning + ); + + CommandShell shell = terminal.Runtime.Shell; + Assert.IsTrue(shell.IgnoringDefaultCommands); + CollectionAssert.Contains(shell.IgnoredCommands, "help"); + + CommandLog log = terminal.Runtime.Log; + Assert.IsTrue(log.ignoredLogTypes.Contains(TerminalLogType.Warning)); + } #endif [UnityTest] diff --git a/Tests/Runtime/TerminalThemePersisterTests.cs b/Tests/Runtime/TerminalThemePersisterTests.cs new file mode 100644 index 0000000..a6ea65c --- /dev/null +++ b/Tests/Runtime/TerminalThemePersisterTests.cs @@ -0,0 +1,35 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using System.Collections; + using NUnit.Framework; + using Persistence; + using UI; + using UnityEngine; + using UnityEngine.TestTools; + + public sealed class TerminalThemePersisterTests + { + [UnityTest] + public IEnumerator PersistenceProfileCanDisableSaving() + { + yield return TerminalTests.SpawnTerminal( + resetStateOnInit: true, + configure: terminal => terminal.disableUIForTests = true, + ensureLargeLogBuffer: true + ); + + TerminalUI terminal = TerminalUI.Instance; + Assert.IsNotNull(terminal); + + TerminalThemePersister persister = terminal.gameObject.AddComponent(); + TerminalThemePersistenceProfile profile = ScriptableObject.CreateInstance(); + profile.enablePersistence = false; + persister.SetPersistenceProfileForTests(profile); + + yield return null; + + Assert.IsFalse(persister.PersistenceEnabledForTests); + ScriptableObject.DestroyImmediate(profile); + } + } +} diff --git a/Tests/Runtime/TerminalThemePersisterTests.cs.meta b/Tests/Runtime/TerminalThemePersisterTests.cs.meta new file mode 100644 index 0000000..879953b --- /dev/null +++ b/Tests/Runtime/TerminalThemePersisterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a43d9cbe4f4033684a6bc8ef379fc506 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 785367d720dac176d4b34cebc3090b8ed8d63871 Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 11:53:56 -0700 Subject: [PATCH 34/69] More progress --- PLAN.md | 101 ------------------ PLAN.md.meta | 7 -- .../UI/TerminalUI.AutoCompleteView.cs | 38 +++++++ .../UI/TerminalUI.LayoutView.cs | 33 ++++++ Runtime/CommandTerminal/UI/TerminalUI.cs | 76 ++++++++++--- 5 files changed, 130 insertions(+), 125 deletions(-) delete mode 100644 PLAN.md delete mode 100644 PLAN.md.meta diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 5266488..0000000 --- a/PLAN.md +++ /dev/null @@ -1,101 +0,0 @@ -# DXCommandTerminal Modernization Plan - -## Objectives -- Eliminate ambient static state and move toward instance-based, SOLID-compliant architecture without sacrificing discoverability or UX ergonomics. -- Achieve zero-allocation hot paths for terminal rendering, history traversal, and input handling while preserving runtime performance. -- Improve maintainability by decomposing the 3.6k line `TerminalUI` monolith and enforcing clearer module boundaries between runtime model, view, persistence, and tooling. -- Tighten correctness through better validation, deterministic behaviour, and comprehensive automated testing (runtime + UI smoke). -- Preserve or improve usability by keeping configuration approachable (consider ScriptableObject assets, inspectors, and presets) and providing guard rails for integrators and designers. - -## Prioritized Initiatives - -### P0 — Runtime Instance Core & Dependency Inversion ✅ -- Replace `WallstopStudios.DxCommandTerminal.Backend.Terminal` static with an injectable `TerminalRuntime` aggregate (shell, history, buffer, autocomplete) created per-terminal instance (`Runtime/CommandTerminal/Backend/Terminal.cs`). -- Introduce an interface (`ITerminalRuntimeAccessor`) that `TerminalUI` and input controllers can depend on; provide a ScriptableObject-derived factory (`TerminalRuntimeProfile`) with serialized capacities, ignored log types, default command packs, etc. to maintain discoverability. -- Provide a migration shim that keeps `Terminal` static available as a thin proxy during transition but mark it `[Obsolete]`, delegating to the active runtime and throwing if none are registered (breaking change acceptable now). -- Benefits: Single Responsibility (runtime logic disentangled from UI), Open/Closed (future runtimes), Dependency Inversion (consumers target abstraction), easier testing/mocking. Enables multi-terminal scenes and editor preview workflows. - -**Status:** Implemented. `TerminalRuntime` now owns buffer/history/shell/autocomplete instances per terminal, with runtime caching and profiles in place. Static facade proxies calls to the active runtime. - -### P0 — TerminalUI Decomposition & Presenter Layer ✅ -- Split `Runtime/CommandTerminal/UI/TerminalUI.cs` (~3.6k LOC) into focused components: - - `TerminalUIPresenter` (MonoBehaviour) orchestrating runtime ↔ viewmodel sync and command dispatch. - - `TerminalUIView`/`LogView`, `HistoryView`, `InputView`, `LauncherView` for UIToolkit manipulation (each under 300 LOC) using pure view logic. - - `TerminalThemeController` handling font/theme switching and coordinating with persistence. - - `TerminalAnimationController` dedicated to height easing, scroll positioning, fade logic. -- Apply Interface Segregation: each controller exposes minimal update contracts (`ILogView.UpdateLog(LogSlice slice)` etc.). Use composition over inheritance to keep testability high. -- Break editor-only tooling into partial classes or dedicated editor scripts (`Editor/`) to remove `#if UNITY_EDITOR` clutter from runtime components. -- Benefits: maintainability, easier reasoning, smaller diff surface, simpler UI testing. - -**Status:** Implemented via partial classes (`TerminalUI.LogView`, `TerminalUI.AutoCompleteView`, `TerminalUI.LayoutView`) and runtime-focused MonoBehaviour core. Editor drawers now rely on an injectable serialized-property accessor. - -### P0 — Zero-Allocation Hot Path Audit ✅ -- Profile `LateUpdate` (`Runtime/CommandTerminal/UI/TerminalUI.cs:586`), history fade (`ApplyHistoryFade`), autocomplete refresh, and log drain to identify per-frame allocations; replace `new` operations with reusable buffers (`NativeList`, pooled `List`, struct enumerators). -- Introduce `struct`-based lightweight view models (`LogSlice`, `CompletionBufferView`) that carry spans/indices into pooled storage inside `TerminalRuntime` to avoid copying strings each frame. -- Centralize pooling via `DxArrayPool` (already exists) or custom `ITerminalBufferPool`; ensure `CommandLog.DrainPending` and `CommandShell` reuse `StringBuilder` without ToString allocations on hot paths. -- Add allocation regression tests using Unity's `GC.AllocRecorder` in playmode tests that toggle terminal while issuing commands. - -**Status:** Allocation regression guard added (`AllocationRegressionTests.CommandLoggingDoesNotAllocate`) capturing GC allocations during command spam and UI toggles. Remaining profiling work tracks via `ProfilerRecorder` hooks if regressions appear. - -### P0 — Validation & State Management Contracts -- Formalize state transitions in a dedicated `TerminalStateMachine` with explicit events (`OpenFull`, `Close`, `ToggleLauncher`). `TerminalKeyboardController` then depends on that contract rather than manipulating `TerminalUI` internals. -- Replace ad-hoc boolean flags (`_needsScrollToEnd`, `_commandIssuedThisFrame`) with explicit commands/events queued into the state machine; process deterministically during update. -- Provide defensive checks and central error logging (reduce `Debug.LogError` scatter) to improve correctness and diagnosability. - -### P1 — Configurability via ScriptableObjects & Presets -- Create `TerminalAppearanceProfile`, `TerminalInputProfile`, `TerminalCommandProfile` ScriptableObjects living under `Resources/Wallstop Studios/DxCommandTerminal/` to encapsulate current serialized fields (hotkeys, history fade, button labels, etc.). -- Allow multiple profiles per project and expose assignment in inspector with sensible defaults. `TerminalLauncherSettings` becomes a serializable asset reused across scenes. -- Move persisted theme/font selection into a `TerminalThemePersistenceProfile` (ScriptableObject + runtime adapter) to trim IO concerns from `TerminalThemePersister` MonoBehaviour; supports injection/mocking in tests. - -**Progress:** `TerminalInputProfile` drives controller bindings (playmode coverage), `TerminalAppearanceProfile` standardises button/hint/history settings, `TerminalCommandProfile` configures ignore lists and disabled commands (with automated tests), and `TerminalThemePersistenceProfile` allows enabling/disabling theme persistence without touching code. Remaining work covers broader persistence APIs beyond themes. - -### P1 — UI Rendering & Virtualization Improvements -- Swap manual `VisualElement` management with UIToolkit `ListView` virtualization for history/log to avoid re-creating labels each refresh; ensure zero allocation by providing custom `MakeItem`/`BindItem` that reuse pooled entries. -- Extract USS selectors into modular style sheets under `Styles/` to reduce runtime code toggling class lists; `LogView` can simply set classes based on pre-defined style variants. -- Provide layout data caches and lightweight diffing to avoid clearing/rebuilding containers when nothing changed (`ListsEqual` currently compares single lists but still calls `Clear`/`Add`). - -**Progress:** Log rendering now uses a virtualized `ListView` with custom binders (`TerminalUI.LogView`), eliminating per-frame element churn while preserving fade styling. USS modularisation remains to be completed. - -### P1 — Input System Stratification -- Introduce an `ITerminalInputSource` abstraction handing parsed commands / navigation intents; implement `LegacyInputSource`, `NewInputSystemSource`, and `EditorShortcutSource`. `TerminalKeyboardController` becomes an adapter composed with an input source chosen via profile. -- Normalize hotkey parsing via dedicated service that pre-resolves key codes at initialization (avoid dictionary lookups each frame) and supports rebinding UI. -- Provide guard rails for conflicting hotkeys (validate at profile load rather than runtime `Debug.LogError`). - -**Status:** Phase 1 complete. `TerminalKeyboardController` now resolves any `ITerminalInputTarget`, with new tests verifying interface dispatch and fallback behaviour. Remaining work includes pluggable input sources and hotkey validation services. - -### P1 — Persistence & Extensibility -- Redesign persistence to use async-less, job-friendly APIs (no `Task` inside coroutines). Provide `ITerminalPersistenceProvider` interface; default implementation writes JSON via `Unity.Collections.LowLevel.Unsafe.UnsafeUtility` safe wrappers when possible. -- Support scene-level overrides (ScriptableObject) and user-level persistence channels to help multi-terminal scenarios. - -### P2 — Observability & Tooling -- Add structured diagnostics (allocation counters, command execution timing) behind development flag to help maintain zero allocation guarantee. -- Provide editor window to inspect active terminal runtimes, registered commands, pending logs (replaces reliance on static global state for debugging). -- Publish developer documentation updates (README + API docs) reflecting new architecture and usage patterns. - -**Progress:** Added `Terminal Runtime Inspector` editor window to surface active runtime details (command count, history size, allocation guard status). Remaining work: structured runtime diagnostics beyond the basic view. - -## Test Coverage Gaps & Strategy -- **Runtime composition:** Add playmode tests covering multiple terminals instantiated simultaneously with distinct profiles to ensure isolation (new `TerminalRuntime` works). -- **State machine:** Unit tests for `TerminalStateMachine` verifying transitions, animations triggers, and zero allocation command queue behaviour. -- **UI binding:** Introduce UIToolkit integration tests (edit mode with `UIElementsTestUtilities`) validating virtualization binds, theme switching, and launcher metrics (currently untested). -- **Persistence:** Mocked persistence provider tests verifying hydration/save cycles without touching disk; existing `TerminalThemePersister` path can be retired. -- **Input:** Tests per input profile ensuring hotkey translation and conflict detection (no coverage right now for `InputHelpers`). `TerminalKeyboardControllerTests` now validate interface dispatch and fallback to `TerminalUI`. -- **Performance:** Automated allocation guard using `GC.AllocRecorder` around command spam + log scrolling scenario; fail test if allocations exceed threshold. - -## Implementation Notes & Sequencing -1. ✅ Land `TerminalRuntime` core + proxy static API (P0). Update tests to inject runtime explicitly. -2. ✅ Extract presenter/view/controller slices from `TerminalUI`, wiring new runtime (P0). Ensure incremental commits keep behaviour parity. -3. ✅ Integrate zero-allocation audit outcomes (P0); add instrumentation and tests. -4. ⏳ Move configuration/persistence into ScriptableObject profiles (P1) and update inspector tooling. -5. ⏳ Roll out input abstraction and UI virtualization (P1), followed by persistence improvements (P1). -6. ⏳ Add observability/tooling features and documentation (P2). - -## Risks & Mitigations -- **Backwards compatibility break:** Provide migration guide and temporary proxy static for legacy API. Communicate via `CHANGELOG.md`. -- **Test fragility:** Introduce helper factories for runtimes in tests to keep fixtures concise. -- **Performance regressions:** Run allocation/performance tests per PR; add editor validation to flag accidental LINQ usage (see `linq_hits.txt`). - -## Deliverables -- Updated runtime architecture diagrams and README sections describing new profiles and runtime injection. -- Comprehensive `TerminalUI` refactor with modular components, zero-regression tests, and allocation guardrails. -- ScriptableObject-based configuration assets and editor tooling for easier customization. diff --git a/PLAN.md.meta b/PLAN.md.meta deleted file mode 100644 index 09aa529..0000000 --- a/PLAN.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: b215f2943aa6f434ea64ce6d734e5d64 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs index 0272d43..0122017 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs @@ -129,6 +129,44 @@ private void RefreshAutoCompleteHints() } } + private void UpdateAutoCompleteView() + { + if (_autoCompleteContainer == null) + { + return; + } + + int suggestionCount = _autoCompleteContainer.childCount; + if (suggestionCount == 0) + { + _lastCompletionIndex = null; + return; + } + + if (_lastCompletionIndex == null) + { + return; + } + + int clampedIndex = Mathf.Clamp( + _lastCompletionIndex.Value, + 0, + suggestionCount - 1 + ); + if (clampedIndex != _lastCompletionIndex.Value) + { + _lastCompletionIndex = clampedIndex; + } + + VisualElement selectedElement = _autoCompleteContainer[clampedIndex]; + if (selectedElement == null) + { + return; + } + + _autoCompleteContainer.ScrollTo(selectedElement); + } + private void ResetAutoComplete() { _lastKnownCommandText = _input.CommandText ?? string.Empty; diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs index f61b195..6c202b4 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs @@ -655,5 +655,38 @@ void LauncherClicked() ToggleLauncher(); } } + + private static void EnsureChildOrder(VisualElement parent, params VisualElement[] children) + { + if (parent == null || children == null || children.Length == 0) + { + return; + } + + int insertIndex = 0; + for (int i = 0; i < children.Length; ++i) + { + VisualElement child = children[i]; + if (child == null) + { + continue; + } + + if (child.parent != parent) + { + parent.Add(child); + } + + int currentIndex = parent.IndexOf(child); + if (currentIndex != insertIndex) + { + parent.Remove(child); + int boundedIndex = Mathf.Clamp(insertIndex, 0, parent.childCount); + parent.Insert(boundedIndex, child); + } + + insertIndex += 1; + } + } } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 0f8c050..ffe2445 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -942,6 +942,28 @@ public void SetState(TerminalState newState) } } + private void CacheLauncherScrollPosition() + { + if (_logScrollView == null) + { + _cachedLauncherScrollVersion = -1; + _cachedLauncherScrollValue = 0f; + return; + } + + CommandHistory history = ActiveHistory; + if (history == null) + { + _cachedLauncherScrollVersion = -1; + _cachedLauncherScrollValue = 0f; + return; + } + + _cachedLauncherScrollVersion = history.Version; + Scroller verticalScroller = _logScrollView.verticalScroller; + _cachedLauncherScrollValue = verticalScroller != null ? verticalScroller.value : 0f; + } + private static bool ListsEqual(List a, List b) { if (ReferenceEquals(a, b)) @@ -1038,23 +1060,43 @@ private void SetupUI() _logListView.makeItem = CreateLogListItem; _logListView.bindItem = BindLogListItem; _logListView.itemsSource = _logListItems; - _logScrollView = _logListView; - InitializeScrollView(_logScrollView); - _logScrollView.AddToClassList("log-scroll-view"); - _terminalContainer.Add(_logScrollView); - _logViewport = _logScrollView.contentViewport; - if (_logViewport != null) - { - _logViewport.style.flexGrow = 1f; - _logViewport.style.flexShrink = 1f; - _logViewport.style.minHeight = 0f; - _logViewport.style.overflow = Overflow.Hidden; - } - VisualElement logContent = _logScrollView.contentContainer; - logContent.style.flexDirection = FlexDirection.Column; - logContent.style.alignItems = Align.Stretch; - logContent.style.minHeight = 0f; - logContent.RegisterCallback(OnLogContentGeometryChanged); + _terminalContainer.Add(_logListView); + + EnsureLogScrollViewReady(); + + void EnsureLogScrollViewReady() + { + if (_logScrollView != null) + { + return; + } + + ScrollView listViewScrollView = _logListView.Q(); + if (listViewScrollView == null) + { + _logListView.schedule.Execute(EnsureLogScrollViewReady).ExecuteLater(0); + return; + } + + _logScrollView = listViewScrollView; + InitializeScrollView(_logScrollView); + _logScrollView.AddToClassList("log-scroll-view"); + + _logViewport = _logScrollView.contentViewport; + if (_logViewport != null) + { + _logViewport.style.flexGrow = 1f; + _logViewport.style.flexShrink = 1f; + _logViewport.style.minHeight = 0f; + _logViewport.style.overflow = Overflow.Hidden; + } + + VisualElement logContent = _logScrollView.contentContainer; + logContent.style.flexDirection = FlexDirection.Column; + logContent.style.alignItems = Align.Stretch; + logContent.style.minHeight = 0f; + logContent.RegisterCallback(OnLogContentGeometryChanged); + } _autoCompleteContainer = new ScrollView(ScrollViewMode.Horizontal) { From 91a1ca97777755f6f49fe02c04592021df4ccb3e Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 12:46:14 -0700 Subject: [PATCH 35/69] Progress --- .../Backend/BuiltinCommands.cs | 7 ++ .../Input/TerminalKeyboardController.cs | 29 +++-- .../UI/TerminalUI.AutoCompleteView.cs | 23 +++- .../UI/TerminalUI.LayoutView.cs | 8 +- .../CommandTerminal/UI/TerminalUI.LogView.cs | 77 ++++++++++-- Runtime/CommandTerminal/UI/TerminalUI.cs | 58 +++++++++ .../Runtime/TerminalLayoutRegressionTests.cs | 110 ++++++++++++++++++ .../TerminalLayoutRegressionTests.cs.meta | 4 + 8 files changed, 291 insertions(+), 25 deletions(-) create mode 100644 Tests/Runtime/TerminalLayoutRegressionTests.cs create mode 100644 Tests/Runtime/TerminalLayoutRegressionTests.cs.meta diff --git a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs index 7e8c24c..c0d13d2 100644 --- a/Runtime/CommandTerminal/Backend/BuiltinCommands.cs +++ b/Runtime/CommandTerminal/Backend/BuiltinCommands.cs @@ -69,11 +69,18 @@ public static void CommandListFonts(CommandArg[] args) if (terminal._fontPack == null) { Terminal.Log(TerminalLogType.Warning, "No font pack found."); + Terminal.Log(TerminalLogType.Message, "No fonts available."); return; } StringBuilder.Clear(); List fonts = terminal._fontPack._fonts; + if (fonts.Count == 0) + { + Terminal.Log(TerminalLogType.Message, "No fonts available."); + return; + } + for (int i = 0; i < fonts.Count; ++i) { if (i > 0) diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index 2b84b35..bd539fa 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -423,22 +423,31 @@ private void ResolveInputTarget() return; } - if (TryGetComponent(out ITerminalInputTarget resolvedTarget)) + if (!TryGetComponent(out ITerminalInputTarget resolvedTarget)) + { + MonoBehaviour[] behaviours = GetComponents(); + for (int i = 0; i < behaviours.Length && resolvedTarget == null; ++i) + { + if (behaviours[i] is ITerminalInputTarget candidate) + { + resolvedTarget = candidate; + } + } + } + + if (resolvedTarget != null) { _inputTarget = resolvedTarget; terminal = resolvedTarget as TerminalUI; _missingTargetLogged = false; } - else + else if (!_missingTargetLogged) { - if (!_missingTargetLogged) - { - Debug.LogError( - "Failed to locate a terminal input target. Input will not work.", - this - ); - _missingTargetLogged = true; - } + Debug.LogWarning( + "Failed to locate a terminal input target. Input will not work.", + this + ); + _missingTargetLogged = true; } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs index 0122017..5a5e346 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.AutoCompleteView.cs @@ -17,15 +17,34 @@ private void RefreshAutoCompleteHints() if (!shouldDisplay) { - if (0 < _autoCompleteContainer?.childCount) + if (_autoCompleteContainer != null) { - _autoCompleteContainer.Clear(); + _autoCompleteContainer.style.display = DisplayStyle.None; + _autoCompleteContainer.style.height = 0f; + _autoCompleteContainer.style.maxHeight = 0f; + if (0 < _autoCompleteContainer.childCount) + { + _autoCompleteContainer.Clear(); + } + + if (_autoCompleteViewport != null) + { + _autoCompleteViewport.style.height = 0f; + } } _previousLastCompletionIndex = null; return; } + _autoCompleteContainer.style.display = DisplayStyle.Flex; + _autoCompleteContainer.style.height = new StyleLength(StyleKeyword.Null); + _autoCompleteContainer.style.maxHeight = new StyleLength(StyleKeyword.Null); + if (_autoCompleteViewport != null) + { + _autoCompleteViewport.style.height = new StyleLength(StyleKeyword.Null); + } + int bufferLength = _lastCompletionBuffer.Count; if (_lastKnownHintsClickable != makeHintsClickable) { diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs index 6c202b4..a6fe080 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs @@ -313,9 +313,9 @@ private void ApplyStandardLayout(float screenWidth) EnsureChildOrder( _terminalContainer, - _logScrollView, + _inputContainer, _autoCompleteContainer, - _inputContainer + _logScrollView ); } private void UpdateLauncherLayoutMetrics() @@ -502,7 +502,9 @@ private void UpdateLauncherLayoutMetrics() availableForHistory = Mathf.Min(availableForHistory, _launcherMetrics.HistoryHeight); availableForHistory = Mathf.Max(0f, availableForHistory); - if (availableForHistory <= 0.01f || _logScrollView.contentContainer.childCount == 0) + bool hasHistoryContent = _logListItems.Count > 0; + + if (availableForHistory <= 0.01f || !hasHistoryContent) { _logScrollView.style.display = DisplayStyle.None; _logScrollView.style.height = 0; diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs index 53c5218..d6c5e15 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs @@ -93,17 +93,19 @@ private void RefreshLogs() private void RefreshLauncherHistory() { - if (_logListView == null) - { - return; - } - CommandHistory history = ActiveHistory; if (history == null) { _logListItems.Clear(); - _logListView.Rebuild(); + if (_logListView != null) + { + _logListView.Rebuild(); + } + else + { + PopulateManualLauncherHistory(); + } _lastRenderedLauncherHistoryVersion = -1; _cachedLauncherScrollVersion = -1; _cachedLauncherScrollValue = 0f; @@ -125,12 +127,19 @@ private void RefreshLauncherHistory() ); } - if (_logListView.itemsSource != _logListItems) + if (_logListView != null) { - _logListView.itemsSource = _logListItems; - } + if (_logListView.itemsSource != _logListItems) + { + _logListView.itemsSource = _logListItems; + } - _logListView.Rebuild(); + _logListView.Rebuild(); + } + else + { + PopulateManualLauncherHistory(); + } _lastRenderedLauncherHistoryVersion = historyVersion; _cachedLauncherScrollVersion = historyVersion; _cachedLauncherScrollValue = 0f; @@ -227,5 +236,53 @@ private float ComputeLogOpacity(int index, int totalCount) return Mathf.Lerp(1f, minimumOpacity, adjusted); } + private void PopulateManualLauncherHistory() + { + if (_logScrollView == null) + { + return; + } + + VisualElement container = _logScrollView.contentContainer; + if (container == null) + { + return; + } + + container.Clear(); + + int totalCount = _logListItems.Count; + for (int i = 0; i < totalCount; ++i) + { + LogItem logItem = _logListItems[i]; + VisualElement element = CreateLogListItem(); + switch (element) + { + case Label label: + label.text = logItem.message; + break; + case TextField textField: + textField.value = logItem.message; + break; + case Button button: + button.text = logItem.message; + break; + } + + ApplyLogStyling(element, logItem); + element.style.opacity = ComputeLogOpacity(i, totalCount); + container.Add(element); + } + + if (totalCount > 0) + { + _launcherHistoryContentHeight = totalCount * LauncherEstimatedHistoryRowHeight; + } + else + { + _launcherHistoryContentHeight = 0f; + } + } + } } diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index ffe2445..1a9d770 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -1847,10 +1847,21 @@ public void ToggleLauncher() // Internal test hooks internal TerminalState CurrentStateForTests => _state; + internal void ForceStateForTests(TerminalState state) + { + _state = state; + } + internal bool LauncherMetricsInitializedForTests => _launcherMetricsInitialized; internal ScrollView LogScrollViewForTests => _logScrollView; + internal ScrollView AutoCompleteContainerForTests => _autoCompleteContainer; + + internal VisualElement InputContainerForTests => _inputContainer; + + internal VisualElement TerminalContainerForTests => _terminalContainer; + internal void SetLauncherMetricsForTests( LauncherLayoutMetrics metrics, bool initialized = true @@ -1866,6 +1877,10 @@ internal void SetLauncherMetricsForTests( internal float CurrentWindowHeightForTests => _currentWindowHeight; + internal IList LogItemsForTests => _logListItems; + + internal IList CompletionBufferForTests => _lastCompletionBuffer; + internal void SetRuntimeProfileForTests(TerminalRuntimeProfile profile) { _runtimeProfile = profile; @@ -1923,6 +1938,15 @@ internal void SetWindowHeightsForTests( _isAnimating = isAnimating; } + internal void SetLauncherContentHeightsForTests( + float historyHeight, + float suggestionHeight + ) + { + _launcherHistoryContentHeight = historyHeight; + _launcherSuggestionContentHeight = suggestionHeight; + } + internal void SetLogScrollViewForTests(ScrollView scrollView) { _logScrollView = scrollView; @@ -1933,6 +1957,40 @@ internal void RefreshLauncherHistoryForTests() RefreshLauncherHistory(); } + internal void UpdateLauncherLayoutMetricsForTests() + { + UpdateLauncherLayoutMetrics(); + } + + internal void RefreshAutoCompleteHintsForTests() + { + RefreshAutoCompleteHints(); + } + + internal void InjectAutoCompleteContainerForTests(ScrollView container) + { + _autoCompleteContainer = container; + _autoCompleteViewport = container != null ? container.contentViewport : null; + } + + internal void InjectLayoutElementsForTests( + VisualElement terminalContainer, + VisualElement inputContainer, + ScrollView autoCompleteContainer, + ScrollView logScrollView + ) + { + _terminalContainer = terminalContainer; + _inputContainer = inputContainer; + _logScrollView = logScrollView; + InjectAutoCompleteContainerForTests(autoCompleteContainer); + } + + internal void SetHintDisplayModeForTests(HintDisplayMode mode) + { + hintDisplayMode = mode; + } + internal void ResetWindowForTests() { ResetWindowIdempotent(); diff --git a/Tests/Runtime/TerminalLayoutRegressionTests.cs b/Tests/Runtime/TerminalLayoutRegressionTests.cs new file mode 100644 index 0000000..11c4d73 --- /dev/null +++ b/Tests/Runtime/TerminalLayoutRegressionTests.cs @@ -0,0 +1,110 @@ +namespace WallstopStudios.DxCommandTerminal.Tests.Runtime +{ + using Backend; + using NUnit.Framework; + using UI; + using UnityEngine; + using UnityEngine.UIElements; + + public sealed class TerminalLayoutRegressionTests + { + [Test] + public void AutoCompleteContainerCollapsesWhenHintsCleared() + { + GameObject go = new GameObject("AutoCompleteRegressionTest"); + go.SetActive(false); + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + terminal.makeHintsClickable = false; + go.SetActive(true); + + try + { + ScrollView autoComplete = new ScrollView(); + terminal.InjectAutoCompleteContainerForTests(autoComplete); + terminal.SetHintDisplayModeForTests(HintDisplayMode.Always); + + terminal.CompletionBufferForTests.Clear(); + terminal.CompletionBufferForTests.Add("help"); + terminal.RefreshAutoCompleteHintsForTests(); + + Assert.That(autoComplete.style.display.value, Is.EqualTo(DisplayStyle.Flex)); + Assert.That(autoComplete.contentContainer.childCount, Is.EqualTo(1)); + + terminal.CompletionBufferForTests.Clear(); + terminal.RefreshAutoCompleteHintsForTests(); + + Assert.That(autoComplete.style.display.value, Is.EqualTo(DisplayStyle.None)); + Assert.That(autoComplete.contentContainer.childCount, Is.EqualTo(0)); + } + finally + { + Object.DestroyImmediate(go); + } + } + + [Test] + public void LauncherHistoryRemainsVisibleWhenItemsExist() + { + GameObject go = new GameObject("LauncherHistoryRegressionTest"); + go.SetActive(false); + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + go.SetActive(true); + + try + { + VisualElement terminalContainer = new VisualElement(); + VisualElement inputContainer = new VisualElement(); + ScrollView autoComplete = new ScrollView(); + ScrollView log = new ScrollView(); + terminal.InjectLayoutElementsForTests( + terminalContainer, + inputContainer, + autoComplete, + log + ); + + terminal.ForceStateForTests(TerminalState.OpenLauncher); + terminal.SetLauncherMetricsForTests( + new LauncherLayoutMetrics( + width: 640f, + height: 240f, + left: 0f, + top: 0f, + historyHeight: 160f, + cornerRadius: 12f, + insetPadding: 12f, + historyVisibleEntryCount: 4, + historyFadeExponent: 2f, + snapOpen: true, + animationDuration: 0.12f + ) + ); + + terminal.SetWindowHeightsForTests(200f, 200f); + terminal.SetLauncherContentHeightsForTests(historyHeight: 64f, suggestionHeight: 0f); + + terminal.LogItemsForTests.Clear(); + terminal.LogItemsForTests.Add( + new LogItem(TerminalLogType.Message, "run-tests", string.Empty) + ); + + terminal.UpdateLauncherLayoutMetricsForTests(); + + Assert.That(log.style.display.value, Is.EqualTo(DisplayStyle.Flex)); + Assert.That(log.style.height.value, Is.GreaterThan(0f)); + + terminal.LogItemsForTests.Clear(); + terminal.UpdateLauncherLayoutMetricsForTests(); + + Assert.That(log.style.display.value, Is.EqualTo(DisplayStyle.None)); + } + finally + { + Object.DestroyImmediate(go); + } + } + } +} + diff --git a/Tests/Runtime/TerminalLayoutRegressionTests.cs.meta b/Tests/Runtime/TerminalLayoutRegressionTests.cs.meta new file mode 100644 index 0000000..295675e --- /dev/null +++ b/Tests/Runtime/TerminalLayoutRegressionTests.cs.meta @@ -0,0 +1,4 @@ +fileFormatVersion: 2 +guid: 5ccba3fcf2d844399d58f357b0521448 +timeCreated: 1752787200 + From 89858be0c6af8306996a3e3b12122768a018dc5f Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 13:11:58 -0700 Subject: [PATCH 36/69] Progress --- .../Input/TerminalKeyboardController.cs | 6 +++--- Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs | 10 ++++++++++ Runtime/CommandTerminal/UI/TerminalUI.cs | 2 +- Styles/BaseStyles.uss | 6 +++--- Tests/Runtime/TerminalLayoutRegressionTests.cs | 7 ++++++- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs index bd539fa..9c54e47 100644 --- a/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs +++ b/Runtime/CommandTerminal/Input/TerminalKeyboardController.cs @@ -143,9 +143,8 @@ private void VerifyControlOrderIntegrity() } } - if (!equal) + if (!equal && _controlOrder is { Count: > 0 }) { - // Build missing list for message _missing.Clear(); for (int i = 0; i < ControlTypes.Length; ++i) { @@ -156,7 +155,7 @@ private void VerifyControlOrderIntegrity() } } - Debug.LogError( + Debug.LogWarning( $"Control Order is missing the following controls: [{string.Join(", ", _missing)}]. " + "Input for these will not be handled. Is this intentional?" + $"\nTerminal Control Types: [{string.Join(", ", ControlTypes)}]" @@ -164,6 +163,7 @@ private void VerifyControlOrderIntegrity() this ); } + } protected virtual void Update() diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs index a6fe080..447c68f 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs @@ -658,6 +658,16 @@ void LauncherClicked() } } + internal void ArrangeLauncherVisualHierarchyForTests() + { + EnsureChildOrder( + _terminalContainer, + _inputContainer, + _autoCompleteContainer, + _logScrollView + ); + } + private static void EnsureChildOrder(VisualElement parent, params VisualElement[] children) { if (parent == null || children == null || children.Length == 0) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 1a9d770..69638a2 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -21,7 +21,7 @@ public sealed partial class TerminalUI : MonoBehaviour, ITerminalInputTarget private const float LauncherAutoCompleteSpacing = 6f; private const float LauncherEstimatedSuggestionRowHeight = 32f; private const float LauncherEstimatedHistoryRowHeight = 28f; - private const float LauncherInputFallbackHeight = 30f; + private const float LauncherInputFallbackHeight = 24f; private const float StandardEstimatedHistoryRowHeight = 24f; private enum ScrollBarCaptureState diff --git a/Styles/BaseStyles.uss b/Styles/BaseStyles.uss index 28c6c4d..f312fc6 100644 --- a/Styles/BaseStyles.uss +++ b/Styles/BaseStyles.uss @@ -125,7 +125,7 @@ flex-shrink: 0; padding: 0; margin: 0; - height: 30px; + height: 24px; align-items: center; } @@ -161,7 +161,7 @@ border-bottom-left-radius: 4px; flex-grow: 0; flex-shrink: 0; - height: 22px; + height: 20px; align-items: center; padding: 2px 0; margin: 0; @@ -171,7 +171,7 @@ background-color: transparent; border-top-right-radius: 4px; border-bottom-right-radius: 4px; - height: 22px; + height: 20px; flex-grow: 1; flex-shrink: 1; padding: 0; diff --git a/Tests/Runtime/TerminalLayoutRegressionTests.cs b/Tests/Runtime/TerminalLayoutRegressionTests.cs index 11c4d73..40d87ee 100644 --- a/Tests/Runtime/TerminalLayoutRegressionTests.cs +++ b/Tests/Runtime/TerminalLayoutRegressionTests.cs @@ -64,6 +64,12 @@ public void LauncherHistoryRemainsVisibleWhenItemsExist() autoComplete, log ); + terminal.ArrangeLauncherVisualHierarchyForTests(); + + Assert.That(terminalContainer.childCount, Is.EqualTo(3)); + Assert.That(terminalContainer[0], Is.SameAs(inputContainer)); + Assert.That(terminalContainer[1], Is.SameAs(autoComplete)); + Assert.That(terminalContainer[2], Is.SameAs(log)); terminal.ForceStateForTests(TerminalState.OpenLauncher); terminal.SetLauncherMetricsForTests( @@ -107,4 +113,3 @@ public void LauncherHistoryRemainsVisibleWhenItemsExist() } } } - From b8f19c833422eb1d7cd99c33fcfe3714fc683ded Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 13:26:33 -0700 Subject: [PATCH 37/69] Progress --- .../UI/TerminalUI.LayoutView.cs | 39 ++++++- Runtime/CommandTerminal/UI/TerminalUI.cs | 5 + .../Runtime/TerminalLayoutRegressionTests.cs | 107 ++++++++++++++++++ 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs index 447c68f..4ee94b9 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs @@ -227,7 +227,10 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _logScrollView.style.height = _launcherMetrics.HistoryHeight; _logScrollView.style.maxHeight = _launcherMetrics.HistoryHeight; _logScrollView.style.minHeight = 0; + _logScrollView.style.flexGrow = 0f; + _logScrollView.style.flexShrink = 1f; _logScrollView.style.marginTop = Mathf.Max(6f, verticalPadding * 0.35f); + _logScrollView.style.marginBottom = 0; } else { @@ -236,6 +239,7 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _logScrollView.style.maxHeight = 0; _launcherHistoryContentHeight = 0f; _logScrollView.style.marginTop = 0; + _logScrollView.style.marginBottom = 0; } _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; @@ -292,6 +296,8 @@ private void ApplyStandardLayout(float screenWidth) _logScrollView.style.maxHeight = new StyleLength(StyleKeyword.Null); _logScrollView.style.minHeight = new StyleLength(StyleKeyword.Null); _logScrollView.style.display = DisplayStyle.Flex; + _logScrollView.style.flexGrow = 1f; + _logScrollView.style.flexShrink = 1f; _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; _autoCompleteContainer.style.position = Position.Relative; @@ -313,9 +319,9 @@ private void ApplyStandardLayout(float screenWidth) EnsureChildOrder( _terminalContainer, - _inputContainer, + _logScrollView, _autoCompleteContainer, - _logScrollView + _inputContainer ); } private void UpdateLauncherLayoutMetrics() @@ -451,10 +457,19 @@ private void UpdateLauncherLayoutMetrics() ? visibleHistoryCount * LauncherEstimatedHistoryRowHeight : 0f; + float maximumHistoryHeight = Mathf.Max( + _launcherMetrics.HistoryHeight, + _launcherMetrics.Height + - (verticalPadding * 2f) + - inputHeight + - reservedForSuggestions + ); + maximumHistoryHeight = Mathf.Max(0f, maximumHistoryHeight); + float desiredHistoryHeight = hasHistory ? Mathf.Min( Mathf.Max(historyHeightFromContent, estimatedHistoryHeight), - _launcherMetrics.HistoryHeight + maximumHistoryHeight ) : 0f; if (desiredHistoryHeight < 0f) @@ -499,7 +514,7 @@ private void UpdateLauncherLayoutMetrics() - (verticalPadding * 2f) - inputHeight - reservedForSuggestions; - availableForHistory = Mathf.Min(availableForHistory, _launcherMetrics.HistoryHeight); + availableForHistory = Mathf.Min(availableForHistory, maximumHistoryHeight); availableForHistory = Mathf.Max(0f, availableForHistory); bool hasHistoryContent = _logListItems.Count > 0; @@ -509,6 +524,8 @@ private void UpdateLauncherLayoutMetrics() _logScrollView.style.display = DisplayStyle.None; _logScrollView.style.height = 0; _logScrollView.style.maxHeight = 0; + _logScrollView.style.flexGrow = 0f; + _logScrollView.style.flexShrink = 1f; _launcherHistoryContentHeight = 0f; } else @@ -516,9 +533,12 @@ private void UpdateLauncherLayoutMetrics() _logScrollView.style.display = DisplayStyle.Flex; _logScrollView.style.height = availableForHistory; _logScrollView.style.maxHeight = availableForHistory; + _logScrollView.style.flexGrow = 0f; + _logScrollView.style.flexShrink = 1f; } _logScrollView.style.marginTop = spacingAboveLog; + _logScrollView.style.marginBottom = 0; } private void RefreshStateButtons() { @@ -658,6 +678,17 @@ void LauncherClicked() } } + + internal void ArrangeStandardVisualHierarchyForTests() + { + EnsureChildOrder( + _terminalContainer, + _logScrollView, + _autoCompleteContainer, + _inputContainer + ); + } + internal void ArrangeLauncherVisualHierarchyForTests() { EnsureChildOrder( diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 69638a2..edd1496 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -24,6 +24,11 @@ public sealed partial class TerminalUI : MonoBehaviour, ITerminalInputTarget private const float LauncherInputFallbackHeight = 24f; private const float StandardEstimatedHistoryRowHeight = 24f; + internal const float LauncherInputFallbackHeightForTests = LauncherInputFallbackHeight; + internal const float LauncherAutoCompleteSpacingForTests = LauncherAutoCompleteSpacing; + internal const float LauncherEstimatedHistoryRowHeightForTests = LauncherEstimatedHistoryRowHeight; + + private enum ScrollBarCaptureState { None = 0, diff --git a/Tests/Runtime/TerminalLayoutRegressionTests.cs b/Tests/Runtime/TerminalLayoutRegressionTests.cs index 40d87ee..3740939 100644 --- a/Tests/Runtime/TerminalLayoutRegressionTests.cs +++ b/Tests/Runtime/TerminalLayoutRegressionTests.cs @@ -43,6 +43,113 @@ public void AutoCompleteContainerCollapsesWhenHintsCleared() } } + [Test] + public void StandardLayoutPlacesInputAtBottom() + { + GameObject go = new GameObject("StandardLayoutRegressionTest"); + go.SetActive(false); + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + go.SetActive(true); + + try + { + VisualElement terminalContainer = new VisualElement(); + VisualElement inputContainer = new VisualElement(); + ScrollView autoComplete = new ScrollView(); + ScrollView log = new ScrollView(); + + terminal.InjectLayoutElementsForTests( + terminalContainer, + inputContainer, + autoComplete, + log + ); + terminal.ArrangeStandardVisualHierarchyForTests(); + + Assert.That(terminalContainer.childCount, Is.EqualTo(3)); + Assert.That(terminalContainer[0], Is.SameAs(log)); + Assert.That(terminalContainer[1], Is.SameAs(autoComplete)); + Assert.That(terminalContainer[2], Is.SameAs(inputContainer)); + } + finally + { + Object.DestroyImmediate(go); + } + } + + [Test] + public void LauncherHistoryUsesAvailableHeight() + { + GameObject go = new GameObject("LauncherHistoryHeightTest"); + go.SetActive(false); + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + go.SetActive(true); + + try + { + VisualElement terminalContainer = new VisualElement(); + VisualElement inputContainer = new VisualElement(); + ScrollView autoComplete = new ScrollView(); + ScrollView log = new ScrollView(); + terminal.InjectLayoutElementsForTests( + terminalContainer, + inputContainer, + autoComplete, + log + ); + terminal.ArrangeLauncherVisualHierarchyForTests(); + terminal.ForceStateForTests(TerminalState.OpenLauncher); + + LauncherLayoutMetrics metrics = new LauncherLayoutMetrics( + width: 640f, + height: 260f, + left: 0f, + top: 0f, + historyHeight: 180f, + cornerRadius: 12f, + insetPadding: 12f, + historyVisibleEntryCount: 6, + historyFadeExponent: 2f, + snapOpen: true, + animationDuration: 0.12f + ); + + terminal.SetLauncherMetricsForTests(metrics); + terminal.SetWindowHeightsForTests(metrics.Height, metrics.Height); + terminal.SetLauncherContentHeightsForTests(historyHeight: 260f, suggestionHeight: 0f); + + terminal.LogItemsForTests.Clear(); + terminal.LogItemsForTests.Add(new LogItem(TerminalLogType.Message, "entry", string.Empty)); + + terminal.UpdateLauncherLayoutMetricsForTests(); + + float verticalPadding = Mathf.Max(4f, metrics.InsetPadding * 0.5f); + float inputHeight = TerminalUI.LauncherInputFallbackHeightForTests; + float spacingAboveLog = Mathf.Max( + TerminalUI.LauncherAutoCompleteSpacingForTests, + verticalPadding * 0.25f + ); + float maximumHistoryHeight = Mathf.Max( + metrics.HistoryHeight, + metrics.Height - (verticalPadding * 2f) - inputHeight - spacingAboveLog + ); + maximumHistoryHeight = Mathf.Max(0f, maximumHistoryHeight); + + float expected = maximumHistoryHeight; + + Assert.That(log.style.display.value, Is.EqualTo(DisplayStyle.Flex)); + Assert.That(log.style.height.value, Is.EqualTo(expected).Within(0.001f)); + Assert.That(log.style.maxHeight.value, Is.EqualTo(expected).Within(0.001f)); + Assert.That(log.style.marginTop.value, Is.EqualTo(spacingAboveLog).Within(0.001f)); + } + finally + { + Object.DestroyImmediate(go); + } + } + [Test] public void LauncherHistoryRemainsVisibleWhenItemsExist() { From f6c3a0f070862ed694756a477644a94227627bd2 Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 14:50:15 -0700 Subject: [PATCH 38/69] Progress --- .../UI/TerminalUI.LayoutView.cs | 183 +++++++++++++----- .../CommandTerminal/UI/TerminalUI.LogView.cs | 2 +- Runtime/CommandTerminal/UI/TerminalUI.cs | 2 + .../Runtime/TerminalLayoutRegressionTests.cs | 74 ++++++- 4 files changed, 206 insertions(+), 55 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs index 4ee94b9..6ff5955 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs @@ -223,26 +223,38 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) if (_launcherMetrics.HistoryHeight > 0f) { - _logScrollView.style.display = DisplayStyle.Flex; - _logScrollView.style.height = _launcherMetrics.HistoryHeight; - _logScrollView.style.maxHeight = _launcherMetrics.HistoryHeight; - _logScrollView.style.minHeight = 0; - _logScrollView.style.flexGrow = 0f; - _logScrollView.style.flexShrink = 1f; - _logScrollView.style.marginTop = Mathf.Max(6f, verticalPadding * 0.35f); - _logScrollView.style.marginBottom = 0; + float marginTop = Mathf.Max(6f, verticalPadding * 0.35f); + StyleLength historyLength = new StyleLength(_launcherMetrics.HistoryHeight); + ApplyLogDisplay( + DisplayStyle.Flex, + historyLength, + historyLength, + new StyleLength(0f), + 0f, + 1f, + new StyleLength(marginTop), + new StyleLength(0f) + ); } else { - _logScrollView.style.display = DisplayStyle.None; - _logScrollView.style.height = 0; - _logScrollView.style.maxHeight = 0; + ApplyLogDisplay( + DisplayStyle.None, + new StyleLength(0f), + new StyleLength(0f), + new StyleLength(0f), + 0f, + 1f, + new StyleLength(0f), + new StyleLength(0f) + ); _launcherHistoryContentHeight = 0f; - _logScrollView.style.marginTop = 0; - _logScrollView.style.marginBottom = 0; } - _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + if (_logScrollView != null) + { + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + } _autoCompleteContainer.style.position = Position.Relative; _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); @@ -257,19 +269,35 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _autoCompleteContainer.style.flexGrow = 0; _autoCompleteContainer.style.flexShrink = 0; + VisualElement logElement = GetLogOrderElement(); EnsureChildOrder( _terminalContainer, _inputContainer, _autoCompleteContainer, - _logScrollView + logElement ); } private void ApplyStandardLayout(float screenWidth) { + if (_uiDocument == null) + { + return; + } + VisualElement rootElement = _uiDocument.rootVisualElement; + if (rootElement == null) + { + return; + } + rootElement.style.width = screenWidth; rootElement.style.height = _currentWindowHeight; + ConfigureStandardLayoutElements(screenWidth); + } + + private void ConfigureStandardLayoutElements(float screenWidth) + { _terminalContainer.EnableInClassList("terminal-container--launcher", false); _terminalContainer.style.width = screenWidth; _terminalContainer.style.height = _currentWindowHeight; @@ -291,14 +319,20 @@ private void ApplyStandardLayout(float screenWidth) _terminalContainer.style.justifyContent = Justify.FlexStart; _terminalContainer.style.alignItems = Align.Stretch; - _logScrollView.style.marginTop = 0; - _logScrollView.style.height = new StyleLength(StyleKeyword.Null); - _logScrollView.style.maxHeight = new StyleLength(StyleKeyword.Null); - _logScrollView.style.minHeight = new StyleLength(StyleKeyword.Null); - _logScrollView.style.display = DisplayStyle.Flex; - _logScrollView.style.flexGrow = 1f; - _logScrollView.style.flexShrink = 1f; - _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + ApplyLogDisplay( + DisplayStyle.Flex, + new StyleLength(StyleKeyword.Null), + new StyleLength(StyleKeyword.Null), + new StyleLength(StyleKeyword.Null), + 1f, + 1f, + new StyleLength(0f), + new StyleLength(0f) + ); + if (_logScrollView != null) + { + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + } _autoCompleteContainer.style.position = Position.Relative; _autoCompleteContainer.style.left = new StyleLength(StyleKeyword.Null); @@ -317,9 +351,10 @@ private void ApplyStandardLayout(float screenWidth) _autoCompleteContainer.style.alignSelf = StyleKeyword.Null; _inputContainer.style.marginBottom = 0; + VisualElement logElement = GetLogOrderElement(); EnsureChildOrder( _terminalContainer, - _logScrollView, + logElement, _autoCompleteContainer, _inputContainer ); @@ -457,14 +492,7 @@ private void UpdateLauncherLayoutMetrics() ? visibleHistoryCount * LauncherEstimatedHistoryRowHeight : 0f; - float maximumHistoryHeight = Mathf.Max( - _launcherMetrics.HistoryHeight, - _launcherMetrics.Height - - (verticalPadding * 2f) - - inputHeight - - reservedForSuggestions - ); - maximumHistoryHeight = Mathf.Max(0f, maximumHistoryHeight); + float maximumHistoryHeight = Mathf.Max(0f, _launcherMetrics.HistoryHeight); float desiredHistoryHeight = hasHistory ? Mathf.Min( @@ -521,24 +549,37 @@ private void UpdateLauncherLayoutMetrics() if (availableForHistory <= 0.01f || !hasHistoryContent) { - _logScrollView.style.display = DisplayStyle.None; - _logScrollView.style.height = 0; - _logScrollView.style.maxHeight = 0; - _logScrollView.style.flexGrow = 0f; - _logScrollView.style.flexShrink = 1f; + ApplyLogDisplay( + DisplayStyle.None, + new StyleLength(0f), + new StyleLength(0f), + new StyleLength(0f), + 0f, + 1f, + new StyleLength(spacingAboveLog), + new StyleLength(0f) + ); _launcherHistoryContentHeight = 0f; } else { - _logScrollView.style.display = DisplayStyle.Flex; - _logScrollView.style.height = availableForHistory; - _logScrollView.style.maxHeight = availableForHistory; - _logScrollView.style.flexGrow = 0f; - _logScrollView.style.flexShrink = 1f; + StyleLength historyLength = new StyleLength(availableForHistory); + ApplyLogDisplay( + DisplayStyle.Flex, + historyLength, + historyLength, + new StyleLength(0f), + 0f, + 1f, + new StyleLength(spacingAboveLog), + new StyleLength(0f) + ); } - _logScrollView.style.marginTop = spacingAboveLog; - _logScrollView.style.marginBottom = 0; + if (_logScrollView != null) + { + _logScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto; + } } private void RefreshStateButtons() { @@ -681,24 +722,74 @@ void LauncherClicked() internal void ArrangeStandardVisualHierarchyForTests() { + VisualElement logElement = GetLogOrderElement(); EnsureChildOrder( _terminalContainer, - _logScrollView, + logElement, _autoCompleteContainer, _inputContainer ); } + internal void ConfigureStandardLayoutForTests(float screenWidth) + { + ConfigureStandardLayoutElements(screenWidth); + } + internal void ArrangeLauncherVisualHierarchyForTests() { + VisualElement logElement = GetLogOrderElement(); EnsureChildOrder( _terminalContainer, _inputContainer, _autoCompleteContainer, - _logScrollView + logElement ); } + + private VisualElement GetLogOrderElement() + { + if (_logListView != null) + { + return _logListView; + } + + return _logScrollView; + } + + private void ApplyLogDisplay( + DisplayStyle display, + StyleLength height, + StyleLength maxHeight, + StyleLength minHeight, + float flexGrow, + float flexShrink, + StyleLength marginTop, + StyleLength marginBottom + ) + { + void Apply(VisualElement element) + { + if (element == null) + { + return; + } + + element.style.display = display; + element.style.height = height; + element.style.maxHeight = maxHeight; + element.style.minHeight = minHeight; + element.style.flexGrow = flexGrow; + element.style.flexShrink = flexShrink; + element.style.marginTop = marginTop; + element.style.marginBottom = marginBottom; + } + + Apply(_logListView); + Apply(_logScrollView); + } + private static void EnsureChildOrder(VisualElement parent, params VisualElement[] children) { if (parent == null || children == null || children.Length == 0) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs index d6c5e15..39945fe 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs @@ -119,7 +119,7 @@ private void RefreshLauncherHistory() long historyVersion = history.Version; _logListItems.Clear(); - for (int i = 0; i < _launcherHistoryEntries.Count; ++i) + for (int i = _launcherHistoryEntries.Count - 1; i >= 0; --i) { CommandHistoryEntry entry = _launcherHistoryEntries[i]; _logListItems.Add( diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index edd1496..96511c7 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -1861,6 +1861,8 @@ internal void ForceStateForTests(TerminalState state) internal ScrollView LogScrollViewForTests => _logScrollView; + internal ListView LogListViewForTests => _logListView; + internal ScrollView AutoCompleteContainerForTests => _autoCompleteContainer; internal VisualElement InputContainerForTests => _inputContainer; diff --git a/Tests/Runtime/TerminalLayoutRegressionTests.cs b/Tests/Runtime/TerminalLayoutRegressionTests.cs index 3740939..9a1bf65 100644 --- a/Tests/Runtime/TerminalLayoutRegressionTests.cs +++ b/Tests/Runtime/TerminalLayoutRegressionTests.cs @@ -65,12 +65,17 @@ public void StandardLayoutPlacesInputAtBottom() autoComplete, log ); - terminal.ArrangeStandardVisualHierarchyForTests(); + terminal.SetWindowHeightsForTests(200f, 200f); + terminal.ConfigureStandardLayoutForTests(800f); Assert.That(terminalContainer.childCount, Is.EqualTo(3)); Assert.That(terminalContainer[0], Is.SameAs(log)); Assert.That(terminalContainer[1], Is.SameAs(autoComplete)); Assert.That(terminalContainer[2], Is.SameAs(inputContainer)); + + Assert.That(log.style.display.value, Is.EqualTo(DisplayStyle.Flex)); + Assert.That(log.style.flexGrow.value, Is.EqualTo(1f).Within(0.001f)); + Assert.That(log.style.marginTop.value, Is.EqualTo(0f).Within(0.001f)); } finally { @@ -131,13 +136,7 @@ public void LauncherHistoryUsesAvailableHeight() TerminalUI.LauncherAutoCompleteSpacingForTests, verticalPadding * 0.25f ); - float maximumHistoryHeight = Mathf.Max( - metrics.HistoryHeight, - metrics.Height - (verticalPadding * 2f) - inputHeight - spacingAboveLog - ); - maximumHistoryHeight = Mathf.Max(0f, maximumHistoryHeight); - - float expected = maximumHistoryHeight; + float expected = metrics.HistoryHeight; Assert.That(log.style.display.value, Is.EqualTo(DisplayStyle.Flex)); Assert.That(log.style.height.value, Is.EqualTo(expected).Within(0.001f)); @@ -218,5 +217,64 @@ public void LauncherHistoryRemainsVisibleWhenItemsExist() Object.DestroyImmediate(go); } } + + [Test] + public void LauncherHistoryNewestEntryAppearsNearInput() + { + GameObject go = new GameObject("LauncherHistoryOrderTest"); + go.SetActive(false); + TerminalUI terminal = go.AddComponent(); + terminal.disableUIForTests = true; + go.SetActive(true); + + try + { + VisualElement terminalContainer = new VisualElement(); + VisualElement inputContainer = new VisualElement(); + ScrollView autoComplete = new ScrollView(); + ScrollView log = new ScrollView(); + terminal.InjectLayoutElementsForTests( + terminalContainer, + inputContainer, + autoComplete, + log + ); + terminal.ArrangeLauncherVisualHierarchyForTests(); + + terminal.ForceStateForTests(TerminalState.OpenLauncher); + terminal.SetLauncherMetricsForTests( + new LauncherLayoutMetrics( + width: 640f, + height: 240f, + left: 0f, + top: 0f, + historyHeight: 160f, + cornerRadius: 12f, + insetPadding: 12f, + historyVisibleEntryCount: 4, + historyFadeExponent: 2f, + snapOpen: true, + animationDuration: 0.12f + ) + ); + + CommandHistory history = terminal.Runtime.History; + Assert.IsNotNull(history); + history.Push("first", true, true); + history.Push("second", true, true); + history.Push("third", true, true); + + terminal.RefreshLauncherHistoryForTests(); + + Assert.That(terminal.LogItemsForTests.Count, Is.EqualTo(3)); + Assert.That(terminal.LogItemsForTests[0].message, Is.EqualTo("third")); + Assert.That(terminal.LogItemsForTests[1].message, Is.EqualTo("second")); + Assert.That(terminal.LogItemsForTests[2].message, Is.EqualTo("first")); + } + finally + { + Object.DestroyImmediate(go); + } + } } } From b27b7848ec44051a6161638792d70fdbdd558309 Mon Sep 17 00:00:00 2001 From: wallstop Date: Wed, 15 Oct 2025 15:49:33 -0700 Subject: [PATCH 39/69] Progress --- .../UI/TerminalUI.LayoutView.cs | 33 +++-- .../CommandTerminal/UI/TerminalUI.LogView.cs | 3 +- Runtime/CommandTerminal/UI/TerminalUI.cs | 39 ++++- .../Runtime/TerminalLayoutRegressionTests.cs | 133 +++++++++++++++++- 4 files changed, 182 insertions(+), 26 deletions(-) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs index 6ff5955..2810b29 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LayoutView.cs @@ -201,7 +201,7 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) _terminalContainer.style.flexDirection = FlexDirection.Column; float horizontalPadding = _launcherMetrics.InsetPadding; - float verticalPadding = Mathf.Max(4f, _launcherMetrics.InsetPadding * 0.5f); + float verticalPadding = Mathf.Max(6f, _launcherMetrics.InsetPadding * 0.3f); _terminalContainer.style.paddingLeft = horizontalPadding; _terminalContainer.style.paddingRight = horizontalPadding; _terminalContainer.style.paddingTop = verticalPadding; @@ -223,16 +223,14 @@ private void ApplyLauncherLayout(float screenWidth, float screenHeight) if (_launcherMetrics.HistoryHeight > 0f) { - float marginTop = Mathf.Max(6f, verticalPadding * 0.35f); - StyleLength historyLength = new StyleLength(_launcherMetrics.HistoryHeight); ApplyLogDisplay( DisplayStyle.Flex, - historyLength, - historyLength, + new StyleLength(_launcherMetrics.HistoryHeight), + new StyleLength(_launcherMetrics.HistoryHeight), new StyleLength(0f), 0f, 1f, - new StyleLength(marginTop), + new StyleLength(0f), new StyleLength(0f) ); } @@ -367,7 +365,7 @@ private void UpdateLauncherLayoutMetrics() } float horizontalPadding = _launcherMetrics.InsetPadding; - float verticalPadding = Mathf.Max(4f, horizontalPadding * 0.5f); + float verticalPadding = Mathf.Max(6f, horizontalPadding * 0.3f); float inputHeight = Mathf.Max(_inputContainer.resolvedStyle.height, 0f); if (inputHeight <= 0f) { @@ -405,7 +403,7 @@ private void UpdateLauncherLayoutMetrics() if (hasSuggestions) { _autoCompleteContainer.style.display = DisplayStyle.Flex; - _autoCompleteContainer.style.marginTop = LauncherAutoCompleteSpacing; + _autoCompleteContainer.style.marginTop = LauncherAutoCompleteSpacing * 0.5f; } else { @@ -466,21 +464,16 @@ private void UpdateLauncherLayoutMetrics() bool hasHistory = visibleHistoryCount > 0; + const float MinimumSpacing = 2f; float spacingAboveLog = 0f; - if (hasHistory) - { - spacingAboveLog = hasSuggestions - ? LauncherAutoCompleteSpacing - : Mathf.Max(LauncherAutoCompleteSpacing, verticalPadding * 0.25f); - } - else if (hasSuggestions) + if (hasHistory && hasSuggestions) { - spacingAboveLog = LauncherAutoCompleteSpacing; + spacingAboveLog = Mathf.Max(MinimumSpacing, LauncherAutoCompleteSpacing * 0.5f); } float reservedForSuggestions = hasSuggestions ? suggestionsHeight + spacingAboveLog - : spacingAboveLog; + : 0f; float historyHeightFromContent = hasHistory ? _launcherHistoryContentHeight : 0f; if (float.IsNaN(historyHeightFromContent) || historyHeightFromContent < 0f) @@ -736,6 +729,12 @@ internal void ConfigureStandardLayoutForTests(float screenWidth) ConfigureStandardLayoutElements(screenWidth); } + + internal void ApplyLauncherLayoutForTests(float width, float height) + { + ApplyLauncherLayout(width, height); + } + internal void ArrangeLauncherVisualHierarchyForTests() { VisualElement logElement = GetLogOrderElement(); diff --git a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs index 39945fe..9407757 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.LogView.cs @@ -123,7 +123,7 @@ private void RefreshLauncherHistory() { CommandHistoryEntry entry = _launcherHistoryEntries[i]; _logListItems.Add( - new LogItem(TerminalLogType.Message, entry.Text, string.Empty) + new LogItem(TerminalLogType.Input, entry.Text, string.Empty) ); } @@ -250,6 +250,7 @@ private void PopulateManualLauncherHistory() } container.Clear(); + container.style.justifyContent = Justify.FlexEnd; int totalCount = _logListItems.Count; for (int i = 0; i < totalCount; ++i) diff --git a/Runtime/CommandTerminal/UI/TerminalUI.cs b/Runtime/CommandTerminal/UI/TerminalUI.cs index 96511c7..2cefde7 100644 --- a/Runtime/CommandTerminal/UI/TerminalUI.cs +++ b/Runtime/CommandTerminal/UI/TerminalUI.cs @@ -18,7 +18,7 @@ namespace WallstopStudios.DxCommandTerminal.UI public sealed partial class TerminalUI : MonoBehaviour, ITerminalInputTarget { private const string TerminalRootName = "TerminalRoot"; - private const float LauncherAutoCompleteSpacing = 6f; + private const float LauncherAutoCompleteSpacing = 4f; private const float LauncherEstimatedSuggestionRowHeight = 32f; private const float LauncherEstimatedHistoryRowHeight = 28f; private const float LauncherInputFallbackHeight = 24f; @@ -1100,7 +1100,26 @@ void EnsureLogScrollViewReady() logContent.style.flexDirection = FlexDirection.Column; logContent.style.alignItems = Align.Stretch; logContent.style.minHeight = 0f; + logContent.style.justifyContent = Justify.FlexEnd; logContent.RegisterCallback(OnLogContentGeometryChanged); + StyleEmptyLabel(); + } + + void StyleEmptyLabel() + { + if (_logListView == null) + { + return; + } + + Label emptyLabel = _logListView.Q