Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ indent_size = 4
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_expression_bodied_methods = false:warning
csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_operators = false:warning
csharp_style_expression_bodied_properties = false:warning
csharp_style_expression_bodied_indexers = false:warning
csharp_style_expression_bodied_accessors = false:warning
csharp_style_expression_bodied_local_functions = false:warning
csharp_style_expression_bodied_lambdas = false:warning
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_var_elsewhere = false:suggestion
csharp_style_var_for_built_in_types = false:suggestion
Expand Down
3 changes: 2 additions & 1 deletion .llm/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ Install the SDK selected by root `global.json` before running .NET tooling. CI a
1. `using` directives go INSIDE the namespace block (see any file in `Runtime/`); `#if` blocks INSIDE namespace; `#define` at file top.
2. All code is formatted via [CSharpier](https://csharpier.com/): `dotnet tool run csharpier format` (tools pinned in `.config/dotnet-tools.json`, enforced by pre-commit). Do not
hand-format.
3. Explicit types over `var`; explicit access modifiers on every member.
3. Explicit types over `var`; explicit access modifiers on every member. Use braces for all function bodies and property accessors, including small forwarding methods and lambdas.
Expression-bodied members and expression lambdas are forbidden. Switch-expression arms keep their required `=>` syntax.
4. Classes/structs are `sealed`/`readonly` where possible; collections exposed immutably by default - mutable access only when necessary.
5. Zero warnings. No dead code, no commented-out code. A Debug.Log-family call logs the exception itself, never `.Message` (the in-game funnel renders the message by design;
enforced by `npm --prefix tooling~ run lint:exception-tostring`).
Expand Down
11 changes: 8 additions & 3 deletions .llm/skills/formatting-and-linting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ Restore tools first (`dotnet tool restore`) - the `dotnet-tool-restore` pre-comm
## Formatting

1. Format only the files you touched: `dotnet tool run csharpier format -- <file1> <file2>`
2. Whole repo check: `dotnet tool run csharpier format --check .` (or run pre-commit).
3. NEVER hand-adjust CSharpier output (spacing, braces, wrapping). If output looks wrong, the fix is upstream CSharpier config, not manual edits.
2. Whole repo check: `dotnet tool run csharpier check .` (or run pre-commit).
3. For Markdown C# samples, use stdin with `--write-stdout --stdin-path /absolute/Example.cs`. Ignored temporary files can be skipped by the formatter.
4. NEVER hand-adjust CSharpier output (spacing, braces, wrapping). If output looks wrong, the fix is upstream CSharpier config, not manual edits.

## Style contract (from `.editorconfig` + README)

- `using` directives inside the namespace block; `#if` blocks inside namespace.
- Explicit types over `var`; explicit access modifiers everywhere.
- Braces always (`csharp_prefer_braces`).
- Braces always: methods, constructors, operators, local functions, property/indexer accessors, and lambdas use block bodies. Small forwarding methods follow the same rule.
`csharp_prefer_braces` covers control flow; `csharp_style_expression_bodied_* = false` covers function/member bodies. CSharpier preserves the chosen body form, so formatting
alone cannot enforce this rule. Sweep touched C# files for expression bodies after formatting. Keep the required `=>` syntax in switch-expression arms.
- Preserve callback return semantics when adding braces: `Action` callbacks execute statements; `Func` callbacks return values. Preserve `async`, `ref` returns, and exception flow.
Use syntax-aware review to distinguish methods/properties, expression lambdas, block lambdas, and switch arms; a raw `=>` match does not classify them.
- Modifier order: `public private protected internal file new static abstract virtual sealed readonly override extern unsafe volatile async required`.
- Files: UTF-8 BOM + CRLF for C# assets (repo default). Do not "fix" line endings on untouched files - that pollutes diffs. Exceptions: `.llm/**` and `tooling~/scripts/**` are
UTF-8 no BOM + LF (enforced by `.editorconfig` overrides and the LLM linters).
Expand Down
34 changes: 30 additions & 4 deletions .llm/skills/input-system-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ metadata:

## PlayerInput binding

Bind InputActions to these messages (UnityEvents/`SendMessage` style on the Terminal):
Default `Send Messages` actions use these message names on the Terminal:

| Message | Action |
| ------------------------ | ---------------------- |
Expand All @@ -28,6 +28,23 @@ Bind InputActions to these messages (UnityEvents/`SendMessage` style on the Term
| `CompleteCommand` | Auto-complete forward |
| `ReverseCompleteCommand` | Auto-complete backward |
| `EnterCommand` | Execute current buffer |
| `Navigate` | Move control focus |
| `Submit` | Activate focused UI |
| `Cancel` | Return to input/close |

`TerminalPlayerInputController` serializes all eleven `*ActionName` fields. Default names retain `Send Messages` routing. Custom names use `PlayerInput` Behavior
`Invoke C Sharp Events`; the controller subscribes to `onActionTriggered` while enabled. Identifiers accept names or `MapName/ActionName` paths from that player's action asset.
Keep each route on a distinct action. Resolve unqualified names in the current map before the asset. Missing, blank, or malformed identifiers do not claim native navigation.

When changing identifiers, sweep dispatch, typing-control lookup, repeat/cancel state, and native UI Toolkit ownership together. `Send Messages` derives handler names from action
names; replacing lookup literals alone does not route renamed actions. Callback routing must ignore `started` and mirror message phases: `performed`, plus `canceled` for Value
actions. Submit/Cancel also require pressed values. Do not mutate the player's notification behavior. Pair event subscriptions across enable/disable; test re-enabling for
duplicates. The controller fixture runs the same queued-device suite with defaults and serialized custom map paths. Keep both modes passing.

Both adapters call protected virtual typed action hooks: no arguments for command/history/toggle routes, `Vector2` for Navigate, and pressed `bool` for Submit/Cancel. Subclasses
use those hooks to customize both modes. Public `On*(InputValue)` overrides remain message-specific. Never fabricate InputValue or call its adapters with null from a
CallbackContext route: subclasses may read that argument. Preserve the typing gate before reading navigation's Vector2; a suppressed scalar character binding must not be cast. Test
every subclass hook once under both modes.

When using PlayerInput, UNCHECK `Use Hotkeys` on the Terminal - otherwise both paths fire.

Expand All @@ -45,16 +62,25 @@ mouse, joystick) and counts anything else as typing, so an unrecognized name fai
`AnyKeyControl` always types, a `KeyControl` defers to `ProducesTypedText(name)` unless `Keyboard.current.ctrlKey.isPressed`, and any other control types nothing. It fails OPEN (no
control -> the message runs), because a console that silently ignores `Close` is worse than a toggle that fires once.

Focus is read through `TerminalUI.AnyConsoleFieldOwnsFocus()`; a closed terminal does not count. `TerminalPlayerInputController` applies the rule to all eight messages through
`ShouldHandleMessage(actionName)`: the action name is the message name minus `On`, it resolves the action from `PlayerInput.currentActionMap` first (an action in another map has no
active control) then the asset, and a name that resolves to nothing leaves the message to run. The string rule's rows live in `Tests/Editor/InputHelpersTests.cs`; the control
Focus is read through `TerminalUI.AnyConsoleFieldOwnsFocus()`; a closed terminal does not count. `TerminalPlayerInputController` applies the rule to non-toggle messages through
`ShouldHandleMessage(actionName)`: each route passes its serialized identifier. It resolves the action from `PlayerInput.currentActionMap` first, then the asset; an action in
another map has no active control. A name that resolves to nothing leaves hand-sent messages to run. The string rule's rows live in `Tests/Editor/InputHelpersTests.cs`; the control
rule's rows and the Play Mode rig live in `Tests/Runtime/TerminalPlayerInputControllerTests.cs`, which needs a live device. Add rows in both directions to whichever owns the rule
you change.

Three limits to keep in step with the docs: a `ctrl+` chord is recognized only from live keyboard state (the control names the key, not the modifier), a `Value` action's release
message still names the key so a character binding is deferred on both halves but a non-typing one runs twice, and a hand-sent message or an unresolvable action name runs as it
always has. Only `shift+` and `ctrl+` are parsed in a binding string; `cmd+`/`super+`/`alt+` never resolve to a key.

## Navigation ownership and cancellation

A configured enabled/bound navigation action owns its native UI Toolkit route inside the terminal; explicit terminal API events still pass. Disabling the action/controller returns
ownership to native navigation. Test real queued stick/button input: direct API calls cannot expose duplicate native Submit dispatch. UI fixtures must load the shipped TSS so the
focus ring uses product geometry.

Cancel must stop pending repeats AND block held directions until neutral. A typing gate alone misses gamepad input. Test cancel while held, jitter/direction changes, mixed keyboard
and pad bindings, direct API cancel, and action-map handoff. Preserve the neutral latch when a typing control takes over; reset it only on a real release or ownership loss.

## History navigation semantics

- Up/Down navigation past either end yields a blank command (no "sticking").
Expand Down
12 changes: 9 additions & 3 deletions .llm/skills/llm-attribution/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,21 @@ One line per artifact. Keep it when editing LLM-generated content; remove it onl

## Reading review feedback

Fetch all review surfaces before triage: paginated PR reviews, inline review comments (including replies), issue comments, and GraphQL review threads. Check pagination on nested
comments too. Read human directives and bot leads, including resolved threads; do not infer the feedback set from check conclusions alone.

A review comment names a line at the commit the reviewer saw, not the head. Cursor Bugbot's review lags the branch by a push or two and marks its superseded run
`BUGBOT_REVIEW_STALE`, so a comment can describe code that is already fixed. Check the commit each comment names against the branch before acting:

```sh
gh api "repos/wallstop/DxCommandTerminal/pulls/<n>/comments" \
-q '.[] | "\(.user.login) \(.commit_id[0:7]) \(.path):\(.line)"'
gh log --oneline master..HEAD
gh api --paginate "repos/Ambiguous-Interactive/DxTerminal/pulls/<n>/comments" \
-q '.[] | "\(.user.login) \(.original_commit_id[0:7]) \(.path):\(.line)"'
git log --oneline master..HEAD
```

GitHub can remap `commit_id` to a later head while retaining the original review. Check `original_commit_id`, the review's `commit_id`, and the bot's named commit before calling a
finding current or stale. A resolved thread still needs a verified fix; a new green bot run does not erase human feedback.

`wallstop` is the human reviewer; a comment of theirs is a directive, so read it as one and apply it or answer it with evidence. A bot comment is a lead: verify the mechanism
before changing code, and when it is wrong, say so in a reply with the code and the measurement, rather than silently skipping it. Record a disagreement in `progress/` either way -
a bot finding that was wrong is a fact worth keeping.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Entry style follows [unity-helpers](https://github.com/Ambiguous-Interactive/uni

### Added

- Add serialized PlayerInput action names and custom-map routing through C# events ([#245](https://github.com/Ambiguous-Interactive/DxTerminal/issues/245)).

- Add gamepad control navigation, submit, cancel, and visible header focus ([#241](https://github.com/Ambiguous-Interactive/DxTerminal/issues/241),
[#245](https://github.com/Ambiguous-Interactive/DxTerminal/issues/245)).
- Add a current search-match row marker that preserves log text selection ([#242](https://github.com/Ambiguous-Interactive/DxTerminal/issues/242)).

- Add repeat collapse with entry-based search counts ([#242](https://github.com/wallstop/DxCommandTerminal/issues/242)).
- Add Top, Bottom, and Auto terminal anchoring with safe-area placement ([#241](https://github.com/wallstop/DxCommandTerminal/issues/241),
[#245](https://github.com/wallstop/DxCommandTerminal/issues/245)).
Expand Down
102 changes: 81 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,19 +241,36 @@ void OnEnable()
Terminal.Shell.AddCommand(
CommandBuilder
.Create("heal", "Heals a target")
.Arg<int>("amount", spec => spec.Required().Range(1, 100))
.Arg<string>("target", spec => spec.Default("self").Choices("self", "ally"))
.Handler((context, arguments) =>
{
int amount = arguments.Get<int>(0);
string target = arguments.Get<string>("target");
// ...
}),
.Arg<int>(
"amount",
spec =>
{
return spec.Required().Range(1, 100);
}
)
.Arg<string>(
"target",
spec =>
{
return spec.Default("self").Choices("self", "ally");
}
)
.Handler(
(context, arguments) =>
{
int amount = arguments.Get<int>(0);
string target = arguments.Get<string>("target");
// ...
}
),
out handle
);
}

void OnDisable() => handle?.Dispose();
void OnDisable()
{
handle?.Dispose();
}
```

Arguments parse through the same `CommandArg` parsers as `[RegisterCommand]` methods (including your registered custom parsers). Bad user input - unknown values, out-of-range
Expand All @@ -267,19 +284,42 @@ Subcommands route one registered command through its first argument, each with i
Terminal.Shell.AddCommand(
CommandBuilder
.Create("inventory", "Manage the inventory")
.Handler((context, arguments) => { /* runs for a bare `inventory` */ })
.Handler((context, arguments) => { })
.Subcommand(
"add",
add => add
.Arg<string>("item", spec => spec.Required())
.Arg<int>("count", spec => spec.Default(1).Range(1, 99))
.Handler((context, arguments) => { /* ... */ })
add =>
{
add.Arg<string>(
"item",
spec =>
{
return spec.Required();
}
)
.Arg<int>(
"count",
spec =>
{
return spec.Default(1).Range(1, 99);
}
)
.Handler((context, arguments) => { });
}
)
.Subcommand(
"remove",
remove => remove
.Arg<string>("item", spec => spec.Required())
.Handler((context, arguments) => { /* ... */ })
remove =>
{
remove
.Arg<string>(
"item",
spec =>
{
return spec.Required();
}
)
.Handler((context, arguments) => { });
}
),
out handle
);
Expand Down Expand Up @@ -463,11 +503,17 @@ CommandBuilder command = CommandBuilder
.Create("inspect-object")
.Arg<GameObject>(
"target",
spec => spec.Required()
.RawParser(adapter.TryParse)
.Choices(adapter.GetChoices, adapter.FormatChoice)
spec =>
{
return spec.Required().RawParser(adapter.TryParse).Choices(adapter.GetChoices, adapter.FormatChoice);
}
)
.Handler((context, arguments) => Terminal.Log("{0}", arguments.Get<GameObject>("target").name));
.Handler(
(context, arguments) =>
{
Terminal.Log("{0}", arguments.Get<GameObject>("target").name);
}
);
```

The same setup works for component types. `FormatChoice` preserves the original name; it does not normalize line endings. Raw parsing does not bypass command tokenization or
Expand Down Expand Up @@ -707,6 +753,20 @@ You can also use `PlayerInput` or similar to bind InputActions to all available
- `CompleteCommand`: Uses auto-complete to attempt to complete the current command using what is typed in the command buffer, cycling forwards through results.
- `ReverseCompleteCommand`: Uses auto-complete to attempt to complete the current command using what is typed in the command buffer, but cycling backwards through results.
- `EnterCommand`: Takes the current buffer and attempts to execute it as a command + parameters.
- `Navigate`: Moves focus through terminal controls. Bind a Vector2 Value action to a stick or D-pad.
- `Submit`: Activates the focused control or runs the command line. Bind a Button action.
- `Cancel`: Returns to command input, then closes on the next press. Bind a Button action.

The `TerminalPlayerInputController` inspector serializes an action name for each route. Existing `Send Messages` setups keep the default names above. To use different names, set
`PlayerInput` Behavior to `Invoke C Sharp Events` and assign those names on the controller. Use `MapName/ActionName` to select a specific map, for example `Console/MoveFocus`. The
controller subscribes automatically. Keep each route on a distinct action. Unqualified names resolve in the current action map first, then the player's asset.

Subclasses can override the protected action methods in either mode: `HandlePrevious()`, `HandleNext()`, `Close()`, `ToggleSmall()`, `ToggleFull()`, `CompleteCommand()`,
`ReverseCompleteCommand()`, `EnterCommand()`, `Navigate(Vector2)`, `Submit(bool)`, and `Cancel(bool)`. The public `On*(InputValue)` methods remain message-specific adapters.

Navigation starts at the header. Hold a direction to repeat. Cancel ignores held directions until they return to neutral. Focused controls show the theme's selected colors.
Enabled, bound navigation actions own their corresponding UI Toolkit events inside this terminal. Disable those actions to restore Unity's default navigation. Other UI documents
keep their input. Search marks the current matching row. Prev and Next move this marker without changing text selection.

## Note

Expand Down
Loading
Loading