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
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
 <!-- Do not remove this comment, lines and table -->
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
| --- | --- |
| ADR | ADR0001V01R01 |
| ADR | ADR0001V01R02 |
| Version | 01 |
| Revision | 01 |
| Revision | 02 |
| Status | Accepted |
| Created | 2026-07-24 |
| Changed | 2026-07-24 |
| Changed | 2026-07-25 |
| Superseded | |
-->

<div align="center">
<img src="../../icon.png" alt="ConsolePlus" width="120" height="120" />

# ADR0001V01R01
# ADR0001V01R02
</div>

[ADR Index](README.md) • **Next:** [ADR0002 →](ADR0002V01R02-ImmutableCapabilityProfile.md)
Expand All @@ -23,11 +23,12 @@



# ADR0001V01R01 — Split PromptPlus 5.x into two projects (ConsolePlus + PromptPlus)
# ADR0001V01R02 — Split PromptPlus 5.x into two projects (ConsolePlus + PromptPlus)

- **Status:** Accepted
- **Version:** V01 / Revision R01
- **Version:** V01 / Revision R02
- **Created:** 2026-07-24
- **Changed:** 2026-07-25

## Context

Expand Down Expand Up @@ -60,17 +61,22 @@ ConsolePlus is the base layer and has **no dependency on PromptPlus**. PromptPlu
depends on ConsolePlus (one-directional) and reuses the same console driver
instance (`PromptPlus.Console` is the same as `ConsolePlus.Driver`).

**R02 — 2026-07-25:** this record absorbs the former ADR0011 ("Separation of
ConsolePlus and PromptPlus"), which restated the same split decision from the
ongoing-boundary angle without adding a distinct decision of its own. The
boundary must be kept clean going forward: neither project should leak
implementation details across the dependency direction established above.

## Consequences

- **Positive:** ConsolePlus can be consumed standalone for pure rendering; the
rendering engine evolves and releases on its own cadence; the
rendering/interaction boundary is explicit and independently testable.
- **Negative / trade-off:** two repositories to coordinate when the shared driver
contract changes; PromptPlus consumers upgrading from 5.x must take on the
ConsolePlus dependency.
ConsolePlus dependency; the boundary must be actively kept clean to avoid
leakage in either direction.

## Related

- [ADR0011](ADR0011V01R01-SeparationFromPromptPlus.md) details the ongoing
boundary and one-directional dependency between the two projects.
- PromptPlus ADR0001 records the same decision from the interaction side.
2 changes: 1 addition & 1 deletion docs/adr/ADR0002V01R02-ImmutableCapabilityProfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# ADR0002V01R02
</div>

[← ADR0001](ADR0001V01R01-SplitConsolePlusAndPromptPlus.md) • [ADR Index](README.md) • **Next:** [ADR0003 →](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md)
[← ADR0001](ADR0001V01R02-SplitConsolePlusAndPromptPlus.md) • [ADR Index](README.md) • **Next:** [ADR0003 →](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md)

---

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/ADR0010V01R01-MarkupLanguage.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# ADR0010V01R01
</div>

[← ADR0009](ADR0009V01R01-StyleModelColorOnly.md) • [ADR Index](README.md) • **Next:** [ADR0011 →](ADR0011V01R01-SeparationFromPromptPlus.md)
[← ADR0009](ADR0009V01R01-StyleModelColorOnly.md) • [ADR Index](README.md) • **Next:** [ADR0011 →](ADR0011V01R01-ShutdownStateRestoration.md)

---

Expand Down
51 changes: 0 additions & 51 deletions docs/adr/ADR0011V01R01-SeparationFromPromptPlus.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
| --- | --- |
| ADR | ADR0012V01R01 |
| ADR | ADR0011V01R01 |
| Version | 01 |
| Revision | 01 |
| Status | Accepted |
Expand All @@ -14,16 +14,16 @@
<div align="center">
<img src="../../icon.png" alt="ConsolePlus" width="120" height="120" />

# ADR0012V01R01
# ADR0011V01R01
</div>

[← ADR0011](ADR0011V01R01-SeparationFromPromptPlus.md) • [ADR Index](README.md) • **Next:** [ADR0013 →](ADR0013V01R01-EmojiShortcodeModel.md)
[← ADR0010](ADR0010V01R01-MarkupLanguage.md) • [ADR Index](README.md) • **Next:** [ADR0012 →](ADR0012V01R01-EmojiShortcodeModel.md)

---



# ADR0012V01R01 — Shutdown state restoration
# ADR0011V01R01 — Shutdown state restoration

- **Status:** Accepted
- **Version:** V01 / Revision R01
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
| --- | --- |
| ADR | ADR0013V01R01 |
| ADR | ADR0012V01R01 |
| Version | 01 |
| Revision | 01 |
| Status | Accepted |
Expand All @@ -14,16 +14,16 @@
<div align="center">
<img src="../../icon.png" alt="ConsolePlus" width="120" height="120" />

# ADR0013V01R01
# ADR0012V01R01
</div>

[← ADR0012](ADR0012V01R01-ShutdownStateRestoration.md) • [ADR Index](README.md) • **Next:** [ADR0014 →](ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md)
[← ADR0011](ADR0011V01R01-ShutdownStateRestoration.md) • [ADR Index](README.md) • **Next:** [ADR0013 →](ADR0013V01R01-LowLevelAnsiAndAlternateScreen.md)

---



# ADR0013V01R01 — Emoji shortcode model with fallback
# ADR0012V01R01 — Emoji shortcode model with fallback

- **Status:** Accepted
- **Version:** V01 / Revision R01
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
| --- | --- |
| ADR | ADR0014V01R01 |
| ADR | ADR0013V01R01 |
| Version | 01 |
| Revision | 01 |
| Status | Accepted |
Expand All @@ -14,16 +14,16 @@
<div align="center">
<img src="../../icon.png" alt="ConsolePlus" width="120" height="120" />

# ADR0014V01R01
# ADR0013V01R01
</div>

[← ADR0013](ADR0013V01R01-EmojiShortcodeModel.md) • [ADR Index](README.md) • **Next:** [ADR0015 →](ADR0015V01R02-GeneratedApiDocsOffLimits.md)
[← ADR0012](ADR0012V01R01-EmojiShortcodeModel.md) • [ADR Index](README.md) • **Next:** [ADR0014 →](ADR0014V01R02-GeneratedApiDocsOffLimits.md)

---



# ADR0014V01R01 — Low-level ANSI and alternate-screen API
# ADR0013V01R01 — Low-level ANSI and alternate-screen API

- **Status:** Accepted
- **Version:** V01 / Revision R01
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
| --- | --- |
| ADR | ADR0015V01R02 |
| ADR | ADR0014V01R02 |
| Version | 01 |
| Revision | 02 |
| Status | Accepted |
Expand All @@ -14,14 +14,14 @@
<div align="center">
<img src="../../icon.png" alt="ConsolePlus" width="120" height="120" />

# ADR0015V01R02
# ADR0014V01R02
</div>

[← ADR0014](ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md) • [ADR Index](README.md)
[← ADR0013](ADR0013V01R01-LowLevelAnsiAndAlternateScreen.md) • [ADR Index](README.md) • **Next:** [ADR0015 →](ADR0015V01R01-RedirectedConsoleIoContract.md)

---

# ADR0015V01R02 — Generated API docs are off-limits for manual edits; regeneration is gated on `ReleaseDoc`, not `Release`
# ADR0014V01R02 — Generated API docs are off-limits for manual edits; regeneration is gated on `ReleaseDoc`, not `Release`

- **Status:** Accepted
- **Version:** V01 / Revision R02
Expand Down
90 changes: 90 additions & 0 deletions docs/adr/ADR0015V01R01-RedirectedConsoleIoContract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!-- Do not remove this comment, lines and table -->
<!--
| Fields | Values |
| --- | --- |
| ADR | ADR0015V01R01 |
| Version | 01 |
| Revision | 01 |
| Status | Accepted |
| Created | 2026-07-28 |
| Changed | 2026-07-28 |
| Superseded | |
-->

<div align="center">
<img src="../../icon.png" alt="ConsolePlus" width="120" height="120" />

# ADR0015V01R01
</div>

[← ADR0014](ADR0014V01R02-GeneratedApiDocsOffLimits.md) • [ADR Index](README.md)

---

# ADR0015V01R01 — Redirected/headless console I/O: fail-safe writes, fail-loud reads

- **Status:** Accepted
- **Version:** V01 / Revision R01
- **Created:** 2026-07-28

## Context

A process whose console I/O is redirected (a file, a pipe, a CI runner, or a test host such as
`dotnet test` itself) has no live OS console handle. Two classes of member are affected differently,
and two real bugs (found 2026-07-28 while auditing whether ConsolePlus's public API runs error-free
under redirection) showed the driver was inconsistent about which failure mode applies where:

1. **Cursor/screen writes** — `Clear()`, `SetCursorPosition()`, the `CursorLeft`/`CursorTop` setters —
called the raw `Console.*` API unguarded in `NoAnsiConsoleAdapter`. Under a redirected/headless
process this threw `IOException: "The handle is invalid."`, even though `HideCursor()`/
`ShowCursor()` and `EnvironmentUtil.GetSafeWidth`/`GetSafeHeight`/`GetSafeTopCursor` already had an
established "Safe" pattern (catch `IOException`/`PlatformNotSupportedException`, degrade quietly)
for exactly this situation.
2. **Key reads** — `KeyAvailable`, `ReadKey`, `ReadKeyAsync` — only guarded on `_profile.Interactive`,
which reflects a **hardcoded list of known CI providers** (see
[Environment Detection](../environment-detection.md)), not the real `Console.IsInputRedirected`.
Any redirected process outside that list (including this project's own local `dotnet test` run)
hit the raw `Console.KeyAvailable`/`ReadKey` call, which throws a raw, undocumented
`InvalidOperationException` for `ReadKey` or behaves unpredictably for `KeyAvailable`.

`Read()`/`ReadLine()` were not part of this problem: they read from the input **text stream**, which
a redirected file/pipe legitimately provides — `Console.ReadLine()` works correctly against redirected
input. Only key-based reads need a **live interactive input buffer**, which redirection does not have.

## Decision

Establish one explicit, documented contract for every member that touches the console under
redirected/headless I/O, split by what the member does:

- **Presentation-only writes** (`Clear`, `SetCursorPosition`, `CursorLeft`/`CursorTop` setters,
`HideCursor`/`ShowCursor`) — **fail safe**. Wrap the raw `Console.*` call in
`catch (Exception ex) when (ex is PlatformNotSupportedException or IOException)` and silently
no-op (or return `false` for the two members that already have a boolean success contract). These
calls only affect what is visually drawn; there is nothing meaningful to report when there is no
screen to draw on, and forcing every caller to guard every cursor/clear call would be unreasonable.
- **Blocking/consuming reads** (`Read`, `ReadLine`, `ReadKey`, `ReadKeyAsync`) — **fail loud, predictably**.
Throw `InvalidOperationException("Console is not interactive.")` — the same message and type for
all four — instead of letting a raw platform exception (or an inconsistent one) leak through. A
read that cannot happen is a real logic error the caller must handle, not something to swallow.
- **The non-blocking probe** `KeyAvailable` — **fails safe**, returning `false` instead of throwing.
It already has a non-throwing contract (`while (!KeyAvailable) { ... }` is the documented polling
idiom in [Reading Input](../reading-input.md)); making it throw would break every such loop.
- `ReadKey`/`ReadKeyAsync`/`KeyAvailable` check **both** `_profile.Interactive` **and**
`Console.IsInputRedirected` explicitly — `Interactive` alone only reflects the CI-provider
heuristic and does not catch plain redirection outside that list. `Read()`/`ReadLine()` keep
checking `Interactive` only, since redirected input is a valid source for them.

## Consequences

- **Positive:** one predictable rule instead of case-by-case behavior — "does this member draw, or
does it consume a real keystroke?" answers which failure mode applies. No raw platform exceptions
(`IOException`, undocumented `InvalidOperationException` text) leak to callers. Matches the
project's existing "Safe" pattern (`EnvironmentUtil.GetSafe*`) instead of adding a second one.
- **Negative / trade-off:** a caller polling `KeyAvailable` in a loop with no independent exit
condition (no `CancellationToken`, no timeout) will now loop forever under redirected input instead
of crashing, because `KeyAvailable` no longer throws — it simply never becomes `true`. This is
correct for `KeyAvailable`'s own non-throwing contract, but any code built on top of it (such as
PromptPlus's interactive controls) must add its **own** upfront `IsInputRedirected` check if it
wants to fail fast instead of hanging — see PromptPlus's
[ADR0023 — Guard interactive controls against redirected console input](https://github.com/FRACerqueira/PromptPlus/blob/develop/docs/adr/ADR0023V01R01-GuardInteractiveControlsAgainstRedirectedInput.md),
which does exactly that on top of this contract.
14 changes: 7 additions & 7 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Architecture Decision Records (ADR)
</div>

[Back to Home](../../README.md) • **First:** [ADR0001 →](ADR0001V01R01-SplitConsolePlusAndPromptPlus.md)
[Back to Home](../../README.md) • **First:** [ADR0001 →](ADR0001V01R02-SplitConsolePlusAndPromptPlus.md)

---

Expand Down Expand Up @@ -45,7 +45,7 @@ projects (ConsolePlus + PromptPlus).

| ADR | Title | Version | Status |
| --- | --- | --- | --- |
| [ADR0001V01R01](ADR0001V01R01-SplitConsolePlusAndPromptPlus.md) | Split PromptPlus 5.x into two projects (ConsolePlus + PromptPlus) | V01 | Accepted |
| [ADR0001V01R02](ADR0001V01R02-SplitConsolePlusAndPromptPlus.md) | Split PromptPlus 5.x into two projects (ConsolePlus + PromptPlus) | V01 (R02) | Accepted |
| [ADR0002V01R02](ADR0002V01R02-ImmutableCapabilityProfile.md) | Immutable capability profile | V01 (R02) | Accepted |
| [ADR0003V01R01](ADR0003V01R01-AutomaticInitializationOnFirstAccess.md) | Automatic initialization on first access | V01 | Accepted |
| [ADR0004V01R01](ADR0004V01R01-AnsiVsNonAnsiDriverSelection.md) | ANSI vs non-ANSI driver selection | V01 | Accepted |
Expand All @@ -55,8 +55,8 @@ projects (ConsolePlus + PromptPlus).
| [ADR0008V01R01](ADR0008V01R01-ColorDepthModel.md) | Color depth model (`ColorSystem`) | V01 | Accepted |
| [ADR0009V01R01](ADR0009V01R01-StyleModelColorOnly.md) | Style model: color + overflow only | V01 | Accepted |
| [ADR0010V01R01](ADR0010V01R01-MarkupLanguage.md) | Inline markup language for styling | V01 | Accepted |
| [ADR0011V01R01](ADR0011V01R01-SeparationFromPromptPlus.md) | Separation of ConsolePlus and PromptPlus | V01 | Accepted |
| [ADR0012V01R01](ADR0012V01R01-ShutdownStateRestoration.md) | Shutdown state restoration | V01 | Accepted |
| [ADR0013V01R01](ADR0013V01R01-EmojiShortcodeModel.md) | Emoji shortcode model with fallback | V01 | Accepted |
| [ADR0014V01R01](ADR0014V01R01-LowLevelAnsiAndAlternateScreen.md) | Low-level ANSI and alternate-screen API | V01 | Accepted |
| [ADR0015V01R02](ADR0015V01R02-GeneratedApiDocsOffLimits.md) | Generated API docs are off-limits for manual edits; regeneration gated on `ReleaseDoc` | V01 (R02) | Accepted |
| [ADR0011V01R01](ADR0011V01R01-ShutdownStateRestoration.md) | Shutdown state restoration | V01 | Accepted |
| [ADR0012V01R01](ADR0012V01R01-EmojiShortcodeModel.md) | Emoji shortcode model with fallback | V01 | Accepted |
| [ADR0013V01R01](ADR0013V01R01-LowLevelAnsiAndAlternateScreen.md) | Low-level ANSI and alternate-screen API | V01 | Accepted |
| [ADR0014V01R02](ADR0014V01R02-GeneratedApiDocsOffLimits.md) | Generated API docs are off-limits for manual edits; regeneration gated on `ReleaseDoc` | V01 (R02) | Accepted |
| [ADR0015V01R01](ADR0015V01R01-RedirectedConsoleIoContract.md) | Redirected/headless console I/O: fail-safe writes, fail-loud reads | V01 | Accepted |
Loading
Loading