diff --git a/Directory.Build.props b/Directory.Build.props
index 7785887..659e4ec 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -54,10 +54,11 @@
+
- $(NoWarn);CA1014;CA1724;CA1812;IL2026;IL2067;IL2070;IL2075;IL3050;IL2104;IL3053
+ $(NoWarn);CA1014;CA1716;CA1724;CA1812;IL2026;IL2067;IL2070;IL2075;IL3050;IL2104;IL3053
diff --git a/kanban/done/020-001-add-stream-access-apis-to-iconsole.md b/kanban/done/020-001-add-stream-access-apis-to-iconsole.md
new file mode 100644
index 0000000..511504d
--- /dev/null
+++ b/kanban/done/020-001-add-stream-access-apis-to-iconsole.md
@@ -0,0 +1,92 @@
+# Add stream access APIs to IConsole
+
+## Description
+
+Add raw stream access methods to `IConsole` to match `System.Console` capabilities. This enables scenarios where code needs direct `Stream` access to stdin/stdout/stderr, or needs to redirect I/O via `TextReader`/`TextWriter`.
+
+Parent: #020
+
+## Checklist
+
+### Implementation
+- [x] Add `OpenStandardInput()` → `Stream` to `IConsole`
+- [x] Add `OpenStandardOutput()` → `Stream` to `IConsole`
+- [x] Add `OpenStandardError()` → `Stream` to `IConsole`
+- [x] Add `In` → `TextReader` property to `IConsole`
+- [x] Add `Out` → `TextWriter` property to `IConsole`
+- [x] Add `Error` → `TextWriter` property to `IConsole`
+- [x] Add `SetIn(TextReader)` method to `IConsole`
+- [x] Add `SetOut(TextWriter)` method to `IConsole`
+- [x] Add `SetError(TextWriter)` method to `IConsole`
+- [x] Implement in `TimeWarpConsole`
+- [x] Implement in `TimeWarpTerminal`
+
+### Testing
+- [x] Add `TestConsole` implementations for all new members
+- [x] Add `TestTerminal` implementations for all new members
+- [x] Add mock stream support to test implementations
+- [x] Add mock TextReader/TextWriter support to test implementations
+- [x] Write unit tests for `OpenStandardInput()`
+- [x] Write unit tests for `OpenStandardOutput()`
+- [x] Write unit tests for `OpenStandardError()`
+- [x] Write unit tests for `In`/`Out`/`Error` properties
+- [x] Write unit tests for `SetIn()`/`SetOut()`/`SetError()`
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+- Completed: ses_2e9f62f66ffeh05Tj3xKWlwLsS (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `iconsole.cs` - add interface members
+- `timewarp-console.cs` - implement in TimeWarpConsole
+- `timewarp-terminal.cs` - implement in TimeWarpTerminal
+- `test-console.cs` - add test implementations
+- `test-terminal.cs` - add test implementations
+
+### Design considerations
+- `OpenStandard*()` methods return `Stream` - test implementations need mock streams (e.g., `MemoryStream`)
+- `In`/`Out`/`Error` are `TextReader`/`TextWriter` - test implementations can use `StringReader`/`StringWriter`
+- RS0030 analyzer currently flags `Console.OpenStandard*` usage - this task satisfies that analyzer
+
+### Reference
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.openstandardinput
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+Added 9 stream access members to `IConsole`:
+- `Stream OpenStandardInput()` - opens stdin as a stream
+- `Stream OpenStandardOutput()` - opens stdout as a stream
+- `Stream OpenStandardError()` - opens stderr as a stream
+- `TextReader In { get; }` - standard input reader
+- `TextWriter Out { get; }` - standard output writer
+- `TextWriter Error { get; }` - standard error writer
+- `void SetIn(TextReader)` - sets standard input
+- `void SetOut(TextWriter)` - sets standard output
+- `void SetError(TextWriter)` - sets standard error
+
+### Files changed
+- `source/timewarp-terminal/iconsole.cs` - added 9 interface members
+- `source/timewarp-terminal/timewarp-console.cs` - implemented all 9 members
+- `source/timewarp-terminal/timewarp-terminal.cs` - implemented all 9 members
+- `source/timewarp-terminal/test-console.cs` - added mock streams and implementations
+- `source/timewarp-terminal/test-terminal.cs` - added mock streams and implementations
+- `Directory.Build.props` - added CA1716 to NoWarn (matching System.Console API names)
+- `tests/stream-access-01-basic.cs` - new test file (26 tests)
+
+### Test results
+- All 26 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
+
+### Design decisions
+- TestConsole/TestTerminal use MemoryStream for OpenStandard* methods
+- In/Out/Error properties use StringReader/StringWriter in test implementations
+- SetIn/SetOut/SetError update the internal readers/writers
+- Added CA1716 suppression for naming conflict with System.Console API (In/Out/Error/SetIn/SetOut/SetError)
diff --git a/kanban/done/020-002-add-encoding-and-redirection-apis-to-iconsole.md b/kanban/done/020-002-add-encoding-and-redirection-apis-to-iconsole.md
new file mode 100644
index 0000000..b771492
--- /dev/null
+++ b/kanban/done/020-002-add-encoding-and-redirection-apis-to-iconsole.md
@@ -0,0 +1,89 @@
+# Add encoding and redirection APIs to IConsole
+
+## Description
+
+Add encoding and redirection state properties to `IConsole` to match `System.Console` capabilities. This enables code to detect and control text encoding and check if streams are redirected.
+
+Parent: #020
+
+## Checklist
+
+### Implementation
+- [x] Add `InputEncoding` get/set property to `IConsole`
+- [x] Add `OutputEncoding` get/set property to `IConsole`
+- [x] Add `IsInputRedirected` property to `IConsole`
+- [x] Add `IsOutputRedirected` property to `IConsole`
+- [x] Add `IsErrorRedirected` property to `IConsole`
+- [x] Implement in `TimeWarpConsole`
+- [x] Implement in `TimeWarpTerminal`
+- [x] Consider deprecating `IsInteractive` on `ITerminal` in favor of explicit `!IsInputRedirected`
+
+### Testing
+- [x] Add `TestConsole` implementations for all new members
+- [x] Add `TestTerminal` implementations for all new members
+- [x] Add mock encoding support to test implementations (default to UTF-8)
+- [x] Add redirection state properties to test implementations (default to false)
+- [x] Write unit tests for `InputEncoding` get/set
+- [x] Write unit tests for `OutputEncoding` get/set
+- [x] Write unit tests for `IsInputRedirected`
+- [x] Write unit tests for `IsOutputRedirected`
+- [x] Write unit tests for `IsErrorRedirected`
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `iconsole.cs` - add interface members
+- `iterminal.cs` - consider IsInteractive deprecation
+- `timewarp-console.cs` - implement in TimeWarpConsole
+- `timewarp-terminal.cs` - implement in TimeWarpTerminal
+- `test-console.cs` - add test implementations
+- `test-terminal.cs` - add test implementations
+
+### Design considerations
+- Encoding properties should default to `Encoding.UTF8` in test implementations
+- Redirection properties should default to `false` in test implementations
+- `IsInteractive` on ITerminal currently returns `!Console.IsInputRedirected` - consider if this should be deprecated or kept as convenience
+
+### Reference
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.inputencoding
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.isinputredirected
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+- Added 5 new properties to `IConsole` interface:
+ - `Encoding InputEncoding { get; set; }`
+ - `Encoding OutputEncoding { get; set; }`
+ - `bool IsInputRedirected { get; }`
+ - `bool IsOutputRedirected { get; }`
+ - `bool IsErrorRedirected { get; }`
+
+- Implemented in `TimeWarpConsole` - delegates to Console properties
+- Implemented in `TimeWarpTerminal` - same as TimeWarpConsole
+- Implemented in `TestConsole` and `TestTerminal` with defaults:
+ - InputEncoding/OutputEncoding default to Encoding.UTF8
+ - IsInputRedirected/IsOutputRedirected/IsErrorRedirected default to false (settable)
+
+### Files changed
+- `source/timewarp-terminal/iconsole.cs` - added interface members
+- `source/timewarp-terminal/timewarp-console.cs` - implemented properties
+- `source/timewarp-terminal/timewarp-terminal.cs` - implemented properties
+- `source/timewarp-terminal/test-console.cs` - added test implementation
+- `source/timewarp-terminal/test-terminal.cs` - added test implementation
+- `tests/console-encoding-01-basic.cs` - new test file (22 tests)
+
+### Test results
+- All 22 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
+
+### Design decisions
+- Kept `IsInteractive` on ITerminal (not deprecated) - it's a convenience property that users may prefer over `!IsInputRedirected`
+- Redirection properties on test implementations are settable to allow testing different scenarios
diff --git a/kanban/done/020-003-add-rich-input-apis-to-iconsole.md b/kanban/done/020-003-add-rich-input-apis-to-iconsole.md
new file mode 100644
index 0000000..769991c
--- /dev/null
+++ b/kanban/done/020-003-add-rich-input-apis-to-iconsole.md
@@ -0,0 +1,78 @@
+# Add rich input APIs to IConsole
+
+## Description
+
+Add character-level input methods to `IConsole` to match `System.Console` capabilities. This enables reading single characters and key presses without requiring a full line.
+
+Parent: #020
+
+## Checklist
+
+### Implementation
+- [x] Add `Read()` → `int` method to `IConsole` (reads single character, returns -1 on EOF)
+- [x] Add `ReadKey()` overload without parameter to `IConsole` (defaults to intercept: false)
+- [x] Implement in `TimeWarpConsole`
+- [x] Implement in `TimeWarpTerminal`
+
+### Testing
+- [x] Add `TestConsole` implementations for all new members
+- [x] Add `TestTerminal` implementations for all new members
+- [x] Add character queue support to `TestConsole` for `Read()`
+- [x] Write unit tests for `Read()` returning single character
+- [x] Write unit tests for `Read()` returning -1 on EOF
+- [x] Write unit tests for `ReadKey()` without parameter (intercept: false)
+- [x] Write unit tests for `ReadKey()` with intercept: true (existing)
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `iconsole.cs` - add interface members
+- `timewarp-console.cs` - implement in TimeWarpConsole
+- `timewarp-terminal.cs` - implement in TimeWarpTerminal
+- `test-console.cs` - add test implementations
+- `test-terminal.cs` - add test implementations (may already have ReadKey support)
+
+### Design considerations
+- `Read()` returns `int` to allow -1 for EOF (same as `Console.Read()`)
+- `ReadKey()` without parameter should default to `intercept: false` (display the key)
+- `TestConsole` already has `ReadLine()` - need to add character-level input support
+
+### Reference
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.read
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.readkey
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+- Added `int Read()` method to `IConsole` - reads single character, returns -1 on EOF
+- Added `ConsoleKeyInfo ReadKey()` overload to `IConsole` - defaults to intercept: false
+- Implemented in `TimeWarpConsole` - wraps Console.Read() and Console.ReadKey(false)
+- Implemented in `TimeWarpTerminal` - wraps Console.Read() and Console.ReadKey(false)
+- Implemented in `TestConsole` with character queue:
+ - `QueueCharacters(string)` method to queue characters for Read()
+ - `CharactersInQueue` property
+ - `Read()` returns next char from queue, or -1 if empty
+ - `ReadKey()` throws NotSupportedException (use TestTerminal for key input)
+- Implemented in `TestTerminal`:
+ - `Read()` uses existing key queue
+ - `ReadKey()` overload without parameter calls ReadKey(false)
+
+### Files changed
+- `source/timewarp-terminal/iconsole.cs` - added interface members
+- `source/timewarp-terminal/timewarp-console.cs` - implemented Read/ReadKey
+- `source/timewarp-terminal/timewarp-terminal.cs` - implemented Read/ReadKey
+- `source/timewarp-terminal/test-console.cs` - added character queue and implementations
+- `source/timewarp-terminal/test-terminal.cs` - added Read/ReadKey implementations
+- `tests/rich-input-01-basic.cs` - new test file (15 tests)
+
+### Test results
+- All 15 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
diff --git a/kanban/done/020-004-add-cursor-properties-to-iterminal.md b/kanban/done/020-004-add-cursor-properties-to-iterminal.md
new file mode 100644
index 0000000..67227ad
--- /dev/null
+++ b/kanban/done/020-004-add-cursor-properties-to-iterminal.md
@@ -0,0 +1,86 @@
+# Add cursor properties to ITerminal
+
+## Description
+
+Add cursor properties to `ITerminal` to match `System.Console` capabilities. Currently only have method pair `SetCursorPosition()`/`GetCursorPosition()`. Add direct property access and visibility/size control.
+
+Parent: #020
+
+## Checklist
+
+### Implementation
+- [x] Add `CursorLeft` get/set property to `ITerminal`
+- [x] Add `CursorTop` get/set property to `ITerminal`
+- [x] Add `CursorVisible` get/set property to `ITerminal`
+- [x] Add `CursorSize` get/set property to `ITerminal` (1-100 percentage)
+- [x] Implement in `TimeWarpTerminal`
+
+### Testing
+- [x] Add `TestTerminal` implementations for all new members
+- [x] Add cursor position tracking to `TestTerminal` (currently has fields but not properties)
+- [x] Add `CursorVisible` property to `TestTerminal` (default: true)
+- [x] Add `CursorSize` property to `TestTerminal` (default: 100)
+- [x] Write unit tests for `CursorLeft` get/set
+- [x] Write unit tests for `CursorTop` get/set
+- [x] Write unit tests for `CursorVisible` get/set
+- [x] Write unit tests for `CursorSize` get/set (validate 1-100 range)
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `iterminal.cs` - add interface members
+- `timewarp-terminal.cs` - implement in TimeWarpTerminal
+- `test-terminal.cs` - add test implementations
+
+### Design considerations
+- `CursorLeft`/`CursorTop` properties are more idiomatic than the existing method pair
+- Consider keeping `SetCursorPosition()`/`GetCursorPosition()` for backward compatibility
+- `CursorSize` is 1-100 percentage (size of cursor, 1=small line, 100=full block)
+- `TestTerminal` already has `CursorLeft`/`CursorTop` fields - convert to properties
+
+### Reference
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.cursorleft
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.cursorvisible
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.cursorsize
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+- Added 4 cursor properties to `ITerminal`:
+ - `int CursorLeft { get; set; }` - cursor column position
+ - `int CursorTop { get; set; }` - cursor row position
+ - `bool CursorVisible { get; set; }` - whether cursor is visible
+ - `int CursorSize { get; set; }` - cursor size as percentage (1-100)
+
+- Implemented in `TimeWarpTerminal`:
+ - CursorLeft/CursorTop wrap Console properties with IOException handling
+ - CursorVisible/CursorSize have Windows platform guards (throw PlatformNotSupportedException on non-Windows)
+
+- Implemented in `TestTerminal`:
+ - Converted existing CursorLeft/CursorTop fields to properties
+ - Added CursorVisible property (default: true)
+ - Added CursorSize property (default: 100, validates 1-100 range)
+ - Backward compatibility: SetCursorPosition()/GetCursorPosition() still work
+
+### Files changed
+- `source/timewarp-terminal/iterminal.cs` - added interface properties
+- `source/timewarp-terminal/timewarp-terminal.cs` - added implementations with platform guards
+- `source/timewarp-terminal/test-terminal.cs` - converted fields to properties, added new properties
+- `tests/terminal-cursor-properties.cs` - new test file (13 tests)
+
+### Test results
+- All 13 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
+
+### Design decisions
+- Kept SetCursorPosition()/GetCursorPosition() for backward compatibility
+- CursorVisible/CursorSize throw PlatformNotSupportedException on non-Windows (matches Console behavior)
+- TestTerminal validates CursorSize range (1-100)
diff --git a/kanban/done/020-005-add-windowbuffer-geometry-to-iterminal.md b/kanban/done/020-005-add-windowbuffer-geometry-to-iterminal.md
new file mode 100644
index 0000000..2d48976
--- /dev/null
+++ b/kanban/done/020-005-add-windowbuffer-geometry-to-iterminal.md
@@ -0,0 +1,92 @@
+# Add window/buffer geometry to ITerminal
+
+## Description
+
+Add window and buffer geometry properties/methods to `ITerminal` to match `System.Console` capabilities. This enables code to query and control terminal dimensions.
+
+Parent: #020
+
+## Checklist
+
+### Implementation
+- [x] Add `WindowHeight` property to `ITerminal`
+- [x] Add `WindowLeft` property to `ITerminal`
+- [x] Add `WindowTop` property to `ITerminal`
+- [x] Add `BufferWidth` property to `ITerminal`
+- [x] Add `BufferHeight` property to `ITerminal`
+- [x] Add `SetWindowSize(int width, int height)` method to `ITerminal`
+- [x] Add `SetWindowPosition(int left, int top)` method to `ITerminal`
+- [x] Add `SetBufferSize(int width, int height)` method to `ITerminal`
+- [x] Add `MoveBufferArea(...)` method to `ITerminal`
+- [x] Add `LargestWindowWidth` property to `ITerminal`
+- [x] Add `LargestWindowHeight` property to `ITerminal`
+- [x] Implement in `TimeWarpTerminal`
+
+### Testing
+- [x] Add `TestTerminal` implementations for all new members
+- [x] Add window/buffer geometry properties to `TestTerminal` with sensible defaults
+- [x] Write unit tests for `WindowHeight`/`WindowWidth` (already have WindowWidth)
+- [x] Write unit tests for `WindowLeft`/`WindowTop`
+- [x] Write unit tests for `BufferWidth`/`BufferHeight`
+- [x] Write unit tests for `SetWindowSize()`
+- [x] Write unit tests for `SetWindowPosition()`
+- [x] Write unit tests for `SetBufferSize()`
+- [x] Write unit tests for `MoveBufferArea()`
+- [x] Write unit tests for `LargestWindowWidth`/`LargestWindowHeight`
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `iterminal.cs` - add interface members
+- `timewarp-terminal.cs` - implement in TimeWarpTerminal
+- `test-terminal.cs` - add test implementations
+
+### Design considerations
+- `WindowWidth` already exists on `ITerminal` - add `WindowHeight`
+- Test implementations need sensible defaults (e.g., 80x24 for window, 80x300 for buffer)
+- `MoveBufferArea` has complex signature - check Console.MoveBufferArea for parameters
+- Some properties may throw `IOException` on redirected output - handle gracefully
+
+### Reference
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.windowheight
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.setwindowsize
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.movebufferarea
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+Added 11 window/buffer geometry members to `ITerminal`:
+- `int WindowHeight { get; set; }` - window height
+- `int WindowLeft { get; set; }` - window left position
+- `int WindowTop { get; set; }` - window top position
+- `int BufferWidth { get; set; }` - buffer width
+- `int BufferHeight { get; set; }` - buffer height
+- `void SetWindowSize(int width, int height)` - set window size
+- `void SetWindowPosition(int left, int top)` - set window position
+- `void SetBufferSize(int width, int height)` - set buffer size
+- `void MoveBufferArea(...)` - move buffer area (9 parameters)
+- `int LargestWindowWidth { get; }` - largest possible window width
+- `int LargestWindowHeight { get; }` - largest possible window height
+
+### Files changed
+- `source/timewarp-terminal/iterminal.cs` - added interface members
+- `source/timewarp-terminal/timewarp-terminal.cs` - implemented with OperatingSystem.IsWindows() guards
+- `source/timewarp-terminal/test-terminal.cs` - added test implementations
+- `tests/terminal-window-buffer-geometry.cs` - new test file (21 tests)
+
+### Test results
+- All 21 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
+
+### Design decisions
+- WindowLeft, WindowTop, SetWindowPosition, SetBufferSize, MoveBufferArea are Windows-only (throw PlatformNotSupportedException on other platforms)
+- TestTerminal uses sensible defaults: WindowHeight=24, BufferWidth=80, BufferHeight=300, LargestWindowWidth=120, LargestWindowHeight=40
+- MoveBufferAreaCallCount property added to TestTerminal to track calls
diff --git a/kanban/done/020-006-add-color-state-apis-to-iterminal.md b/kanban/done/020-006-add-color-state-apis-to-iterminal.md
new file mode 100644
index 0000000..95a0e7b
--- /dev/null
+++ b/kanban/done/020-006-add-color-state-apis-to-iterminal.md
@@ -0,0 +1,81 @@
+# Add color state APIs to ITerminal
+
+## Description
+
+Add color state properties to `ITerminal` to match `System.Console` capabilities. This enables code to set foreground/background colors as terminal state (not inline ANSI styling).
+
+Parent: #020
+
+## Checklist
+
+### Implementation
+- [x] Add `ForegroundColor` get/set property to `ITerminal` (ConsoleColor)
+- [x] Add `BackgroundColor` get/set property to `ITerminal` (ConsoleColor)
+- [x] Add `ResetColor()` method to `ITerminal`
+- [x] Implement in `TimeWarpTerminal`
+
+### Testing
+- [x] Add `TestTerminal` implementations for all new members
+- [x] Add `ForegroundColor` property to `TestTerminal` (default: ConsoleColor.Gray)
+- [x] Add `BackgroundColor` property to `TestTerminal` (default: ConsoleColor.Black)
+- [x] Write unit tests for `ForegroundColor` get/set
+- [x] Write unit tests for `BackgroundColor` get/set
+- [x] Write unit tests for `ResetColor()` (resets to defaults)
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `iterminal.cs` - add interface members
+- `timewarp-terminal.cs` - implement in TimeWarpTerminal
+- `test-terminal.cs` - add test implementations
+
+### Design considerations
+- Current ANSI extension methods (`.Red()`, `.Green()`, etc.) are for **inline styling** - wrapping text with ANSI codes
+- These new properties are for **terminal state** - changing the active color for all subsequent output
+- `ResetColor()` should reset both foreground and background to defaults
+- `TimeWarpTerminal` implementation should use `AnsiColors` to generate ANSI codes for the ConsoleColor values
+
+### Reference
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.foregroundcolor
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.backgroundcolor
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.resetcolor
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+- Added 3 color state members to `ITerminal`:
+ - `ConsoleColor ForegroundColor { get; set; }` - foreground color
+ - `ConsoleColor BackgroundColor { get; set; }` - background color
+ - `void ResetColor()` - reset to default colors
+
+- Implemented in `TimeWarpTerminal`:
+ - ForegroundColor/BackgroundColor wrap Console properties with IOException handling
+ - ResetColor() calls Console.ResetColor()
+
+- Implemented in `TestTerminal`:
+ - ForegroundColor property (default: ConsoleColor.Gray)
+ - BackgroundColor property (default: ConsoleColor.Black)
+ - ResetColor() method - resets to defaults (Gray/Black)
+
+### Files changed
+- `source/timewarp-terminal/iterminal.cs` - added interface members
+- `source/timewarp-terminal/timewarp-terminal.cs` - added implementation
+- `source/timewarp-terminal/test-terminal.cs` - added test implementation
+- `tests/terminal-color-state-01-basic.cs` - new test file (7 tests)
+
+### Test results
+- All 7 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
+
+### Design decisions
+- These are terminal state properties (change color for all subsequent output)
+- Distinct from existing ANSI extension methods (`.Red()`, `.Green()`) which are for inline styling
+- TestTerminal defaults match Console defaults (Gray foreground, Black background)
diff --git a/kanban/done/020-007-add-controlutility-apis-to-iterminal.md b/kanban/done/020-007-add-controlutility-apis-to-iterminal.md
new file mode 100644
index 0000000..d1c5203
--- /dev/null
+++ b/kanban/done/020-007-add-controlutility-apis-to-iterminal.md
@@ -0,0 +1,95 @@
+# Add control/utility APIs to ITerminal
+
+## Description
+
+Add control and utility methods/properties to `ITerminal` to match `System.Console` capabilities. This enables beeps, title control, and Ctrl+C handling configuration.
+
+Parent: #020
+
+## Checklist
+
+### Implementation
+- [x] Add `Beep()` method to `ITerminal`
+- [x] Add `Beep(int frequency, int duration)` overload to `ITerminal`
+- [x] Add `TreatControlCAsInput` get/set property to `ITerminal`
+- [x] Add `Title` get/set property to `ITerminal`
+- [x] Add `KeyAvailable` property to `ITerminal`
+- [x] Implement in `TimeWarpTerminal`
+
+### Testing
+- [x] Add `TestTerminal` implementations for all new members
+- [x] Add `BeepCount` property to `TestTerminal` to track beep calls
+- [x] Add `TreatControlCAsInput` property to `TestTerminal` (default: false)
+- [x] Add `Title` property to `TestTerminal` (default: empty string)
+- [x] Add `KeyAvailable` property to `TestTerminal` (based on key queue)
+- [x] Write unit tests for `Beep()` (verify call count)
+- [x] Write unit tests for `Beep(int, int)` (verify parameters captured)
+- [x] Write unit tests for `TreatControlCAsInput` get/set
+- [x] Write unit tests for `Title` get/set
+- [x] Write unit tests for `KeyAvailable`
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `iterminal.cs` - add interface members
+- `timewarp-terminal.cs` - implement in TimeWarpTerminal
+- `test-terminal.cs` - add test implementations
+
+### Design considerations
+- `Beep()` in tests should not actually beep - just track that it was called
+- `Beep(frequency, duration)` - frequency is 37-32767 Hz, duration is milliseconds
+- `TreatControlCAsInput` - when true, Ctrl+C is passed to `ReadKey()` instead of raising `CancelKeyPress`
+- `KeyAvailable` - returns true if a key press is available in the input stream
+- `TestTerminal` already has `KeysInQueue` property - `KeyAvailable` can use this
+
+### Reference
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.beep
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.treatcontrolcasinput
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.title
+- https://learn.microsoft.com/en-us/dotnet/api/system.console.keyavailable
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+- Added 5 control/utility members to `ITerminal`:
+ - `void Beep()` - play beep sound
+ - `void Beep(int frequency, int duration)` - beep with custom frequency/duration
+ - `bool TreatControlCAsInput { get; set; }` - whether Ctrl+C is treated as input
+ - `string Title { get; set; }` - console title
+ - `bool KeyAvailable { get; }` - whether a key is available
+
+- Implemented in `TimeWarpTerminal`:
+ - Beep methods wrap Console.Beep (Windows only)
+ - TreatControlCAsInput wraps Console.TreatControlCAsInput
+ - Title wraps Console.Title (returns empty string on non-Windows)
+ - KeyAvailable wraps Console.KeyAvailable with IOException handling
+
+- Implemented in `TestTerminal`:
+ - BeepCount property - tracks number of beep calls
+ - LastBeepFrequency/LastBeepDuration - capture last beep parameters
+ - TreatControlCAsInput property (default: false)
+ - Title property (default: "")
+ - KeyAvailable property - returns KeysInQueue > 0
+
+### Files changed
+- `source/timewarp-terminal/iterminal.cs` - added interface members
+- `source/timewarp-terminal/timewarp-terminal.cs` - added implementation
+- `source/timewarp-terminal/test-terminal.cs` - added test implementation
+- `tests/terminal-control-utilities-01-basic.cs` - new test file (12 tests)
+
+### Test results
+- All 12 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
+
+### Design decisions
+- Beep methods are Windows-only (PlatformNotSupportedException on other platforms)
+- Title returns empty string on non-Windows platforms
+- TestTerminal tracks beep calls without actually beeping
diff --git a/kanban/done/020-008-mirror-new-apis-on-terminal-static-class.md b/kanban/done/020-008-mirror-new-apis-on-terminal-static-class.md
new file mode 100644
index 0000000..c4c786b
--- /dev/null
+++ b/kanban/done/020-008-mirror-new-apis-on-terminal-static-class.md
@@ -0,0 +1,101 @@
+# Mirror new APIs on Terminal static class
+
+## Description
+
+Mirror all new `IConsole` and `ITerminal` members on the `Terminal` static class. This ensures the static facade provides complete Console replacement.
+
+Parent: #020
+
+Depends on: #020-001 through #020-007 (all other child tasks must be complete)
+
+## Checklist
+
+### Implementation
+- [x] Mirror stream access APIs from IConsole on `Terminal`
+- [x] Mirror encoding APIs from IConsole on `Terminal`
+- [x] Mirror redirection state APIs from IConsole on `Terminal`
+- [x] Mirror rich input APIs from IConsole on `Terminal`
+- [x] Mirror cursor properties from ITerminal on `Terminal`
+- [x] Mirror window/buffer geometry from ITerminal on `Terminal`
+- [x] Mirror color state APIs from ITerminal on `Terminal`
+- [x] Mirror control/utility APIs from ITerminal on `Terminal`
+
+### Testing
+- [x] Write unit tests for static stream access methods
+- [x] Write unit tests for static encoding properties
+- [x] Write unit tests for static redirection properties
+- [x] Write unit tests for static input methods
+- [x] Write unit tests for static cursor properties
+- [x] Write unit tests for static window/buffer methods
+- [x] Write unit tests for static color state methods
+- [x] Write unit tests for static control/utility methods
+
+## Session
+
+- Created: ses_2f2ab32c3ffeoD0gwPTVU0agTi (2026-03-22)
+
+## Notes
+
+### Files to modify
+- `terminal-static.cs` - add all static members
+
+### Design considerations
+- All static members route to `Instance` (the configured `ITerminal`)
+- Properties should be simple pass-through: `public static int WindowHeight => Instance.WindowHeight;`
+- Methods should be simple pass-through: `public static void Beep() => Instance.Beep();`
+- This task should be done LAST after all interface changes are complete
+
+### Reference
+- See existing `Terminal` static class for pattern
+
+### Coding Standards
+Follow the `/csharp` skill for all implementation work.
+
+## Results
+
+### What was implemented
+Added 50+ static members to the `Terminal` class that route to `Instance`:
+
+**Stream access (9 members):**
+- `OpenStandardInput()`, `OpenStandardOutput()`, `OpenStandardError()`
+- `In`, `Out`, `Error` properties
+- `SetIn()`, `SetOut()`, `SetError()` methods
+
+**Encoding/redirection (5 members):**
+- `InputEncoding`, `OutputEncoding` properties
+- `IsInputRedirected`, `IsOutputRedirected`, `IsErrorRedirected` properties
+
+**Rich input (2 members):**
+- `Read()` method
+- `ReadKey()` method (parameterless)
+
+**Cursor properties (4 members):**
+- `CursorLeft`, `CursorTop`, `CursorVisible`, `CursorSize` properties
+
+**Window/buffer geometry (11 members):**
+- `WindowHeight`, `WindowLeft`, `WindowTop`, `BufferWidth`, `BufferHeight` properties
+- `LargestWindowWidth`, `LargestWindowHeight` properties
+- `SetWindowSize()`, `SetWindowPosition()`, `SetBufferSize()`, `MoveBufferArea()` methods
+
+**Color state (3 members):**
+- `ForegroundColor`, `BackgroundColor` properties
+- `ResetColor()` method
+
+**Control/utility (5 members):**
+- `Beep()`, `Beep(int, int)` methods
+- `TreatControlCAsInput`, `Title`, `KeyAvailable` properties
+
+### Files changed
+- `source/timewarp-terminal/terminal-static.cs` - added 50+ static members
+- `tests/terminal-static-08-new-apis.cs` - new test file (56 tests)
+
+### Test results
+- All 56 new tests pass
+- All existing tests pass
+- Build succeeds with 0 warnings
+
+### Design decisions
+- All static members are simple pass-through to `Instance`
+- Properties use expression-bodied members
+- Methods use expression-bodied statements
+- Follows existing pattern in `Terminal` static class
diff --git a/kanban/done/020-complete-console-api-surface-coverage-for-iconsoleiterminal.md b/kanban/done/020-complete-console-api-surface-coverage-for-iconsoleiterminal.md
new file mode 100644
index 0000000..e7873f1
--- /dev/null
+++ b/kanban/done/020-complete-console-api-surface-coverage-for-iconsoleiterminal.md
@@ -0,0 +1,63 @@
+# Complete Console API surface coverage for IConsole/ITerminal
+
+## Description
+
+Goal: **Completely replace the need for `System.Console`** so application code never has to touch it directly. Current API surface is partial—missing significant chunks of the Console API.
+
+This is a parent task. See child tasks for detailed implementation and testing checklists.
+
+## Checklist
+
+- [ ] #020-001: Add stream access APIs to IConsole
+- [ ] #020-002: Add encoding and redirection APIs to IConsole
+- [ ] #020-003: Add rich input APIs to IConsole
+- [ ] #020-004: Add cursor properties to ITerminal
+- [ ] #020-005: Add window/buffer geometry to ITerminal
+- [ ] #020-006: Add color state APIs to ITerminal
+- [ ] #020-007: Add control/utility APIs to ITerminal
+- [ ] #020-008: Mirror new APIs on Terminal static class (depends on all above)
+
+## Notes
+
+### Current State Analysis
+
+**IConsole currently exposes:**
+- `Write(string)` → `IConsole`
+- `WriteLine(string?)` → `IConsole`
+- `WriteLineAsync(string?)` → `Task`
+- `WriteErrorLine(string?)` → `IConsole`
+- `WriteErrorLineAsync(string?)` → `Task`
+- `ReadLine()` → `string?`
+
+**ITerminal currently exposes:**
+- All IConsole members (with covariant return types)
+- `ReadKey(bool intercept)` → `ConsoleKeyInfo`
+- `SetCursorPosition(int, int)` / `GetCursorPosition()` → `(int, int)`
+- `WindowWidth` → `int`
+- `IsInteractive` → `bool`
+- `SupportsColor` → `bool`
+- `SupportsHyperlinks` → `bool`
+- `Clear()` → `void`
+- `CancelKeyPress` event
+
+### Design Considerations
+
+1. **Fluent chaining**: All sync Write methods must return the interface type (IConsole/ITerminal) for fluent chaining. Async methods return Task.
+
+2. **Interface inheritance pattern**: `ITerminal : IConsole` uses `new` on sync Write methods for covariant return types. Adding sync Write methods to IConsole requires a `new` override in ITerminal.
+
+3. **Files that must change together**:
+ - `iconsole.cs` ↔ `iterminal.cs`
+ - `timewarp-terminal.cs`, `timewarp-console.cs`
+ - `test-terminal.cs`, `test-console.cs`
+ - `terminal-static.cs` (for static facade)
+
+4. **RS0030 analyzer**: Currently flags `Console.OpenStandard*` usage. Once we expose these on IConsole, the analyzer should be satisfied.
+
+### Reference
+
+System.Console API docs: https://learn.microsoft.com/en-us/dotnet/api/system.console
+
+## Coding Standards Reminder
+
+Follow the `/csharp` skill for all implementation work on this task and all child tasks.
diff --git a/kanban/in-progress/019-bring-repository-into-baseline-compliance.md b/kanban/in-progress/019-bring-repository-into-baseline-compliance.md
deleted file mode 100644
index a4fa58f..0000000
--- a/kanban/in-progress/019-bring-repository-into-baseline-compliance.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# Bring repository into baseline compliance
-
-## Description
-
-Fix all failing baseline audit checks to bring the repository into compliance. The audit revealed 6 failing checks that need to be addressed.
-
-## Checklist
-
-### Error-level issues (must fix)
-
-- [ ] Fix `.envrc` to use `PATH_add bin` instead of `export PATH="$PWD/bin:$PATH"`
-- [ ] Create `BannedSymbols.txt` in repository root
-- [ ] Add BannedApiAnalyzers configuration to `Directory.Build.props`
-- [ ] Fix dev CLI capabilities JSON trailing comma issue in `tools/dev-cli/dev.cs`
-
-### Warning-level issues (should fix)
-
-- [ ] Add `#region Purpose` annotations to 8 dev-cli files:
- - [ ] `tools/dev-cli/dev.cs`
- - [ ] `tools/dev-cli/endpoints/verify-samples.cs`
- - [ ] `tools/dev-cli/endpoints/build.cs`
- - [ ] `tools/dev-cli/endpoints/self-install.cs`
- - [ ] `tools/dev-cli/endpoints/check-version.cs`
- - [ ] `tools/dev-cli/endpoints/test.cs`
- - [ ] `tools/dev-cli/endpoints/clean.cs`
- - [ ] `tools/dev-cli/endpoints/workflow.cs`
-- [ ] Clean up orphaned CPM packages in `Directory.Packages.props`:
- - [ ] `TimeWarp.Build.Tasks` (version 1.0.0)
- - [ ] `GlobalUsingsAnalyzer` (version 1.4.0)
- - [ ] `Microsoft.CodeAnalysis.CSharp` (version 5.0.0)
- - [ ] `Microsoft.CodeAnalysis.Analyzers` (version 4.14.0)
-
-### Verification
-
-- [ ] Run `ganda repo audit` to verify all checks pass
-- [ ] Commit changes
-
-## Notes
-
-### Audit Results (2026-03-18)
-
-```
-Passed: 5 | Failed: 6
-
-Failing checks:
-1. baseline-envrc (Error) - .envrc does not contain PATH_add bin
-2. baseline-banned-symbols (Error) - BannedSymbols.txt is missing
-3. baseline-banned-api-analyzers (Error) - Directory.Build.props missing BannedApiAnalyzers config
-4. baseline-dev-cli-capabilities (Error) - Capabilities JSON has trailing comma
-5. baseline-region-annotations (Warning) - 8 files missing #region Purpose
-6. baseline-cpm-consistency (Warning) - 4 orphaned PackageVersion entries
-```
-
-### Passing checks (for reference)
-
-- `baseline-bin-dev` - bin/dev is present
-- `baseline-source-props` - source/Directory.Build.props exists
-- `baseline-msbuild-props` - msbuild/repository.props exists
-- `baseline-directory-packages` - Directory.Packages.props exists
-- `baseline-runfile-variables` - All #:project directives use MSBuild variables
-
-### Related Issue
-
-The NuGet release workflow failed due to workflow name mismatch:
-- Workflow renamed from `ci-cd.yml` to `workflow.yml`
-- NuGet trusted publishing policy still expects `ci-cd.yml`
-- This should be addressed separately (update NuGet policy or rename workflow back)
diff --git a/kanban/to-do/019-bring-repository-into-baseline-compliance.md b/kanban/to-do/019-bring-repository-into-baseline-compliance.md
deleted file mode 100644
index a4fa58f..0000000
--- a/kanban/to-do/019-bring-repository-into-baseline-compliance.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# Bring repository into baseline compliance
-
-## Description
-
-Fix all failing baseline audit checks to bring the repository into compliance. The audit revealed 6 failing checks that need to be addressed.
-
-## Checklist
-
-### Error-level issues (must fix)
-
-- [ ] Fix `.envrc` to use `PATH_add bin` instead of `export PATH="$PWD/bin:$PATH"`
-- [ ] Create `BannedSymbols.txt` in repository root
-- [ ] Add BannedApiAnalyzers configuration to `Directory.Build.props`
-- [ ] Fix dev CLI capabilities JSON trailing comma issue in `tools/dev-cli/dev.cs`
-
-### Warning-level issues (should fix)
-
-- [ ] Add `#region Purpose` annotations to 8 dev-cli files:
- - [ ] `tools/dev-cli/dev.cs`
- - [ ] `tools/dev-cli/endpoints/verify-samples.cs`
- - [ ] `tools/dev-cli/endpoints/build.cs`
- - [ ] `tools/dev-cli/endpoints/self-install.cs`
- - [ ] `tools/dev-cli/endpoints/check-version.cs`
- - [ ] `tools/dev-cli/endpoints/test.cs`
- - [ ] `tools/dev-cli/endpoints/clean.cs`
- - [ ] `tools/dev-cli/endpoints/workflow.cs`
-- [ ] Clean up orphaned CPM packages in `Directory.Packages.props`:
- - [ ] `TimeWarp.Build.Tasks` (version 1.0.0)
- - [ ] `GlobalUsingsAnalyzer` (version 1.4.0)
- - [ ] `Microsoft.CodeAnalysis.CSharp` (version 5.0.0)
- - [ ] `Microsoft.CodeAnalysis.Analyzers` (version 4.14.0)
-
-### Verification
-
-- [ ] Run `ganda repo audit` to verify all checks pass
-- [ ] Commit changes
-
-## Notes
-
-### Audit Results (2026-03-18)
-
-```
-Passed: 5 | Failed: 6
-
-Failing checks:
-1. baseline-envrc (Error) - .envrc does not contain PATH_add bin
-2. baseline-banned-symbols (Error) - BannedSymbols.txt is missing
-3. baseline-banned-api-analyzers (Error) - Directory.Build.props missing BannedApiAnalyzers config
-4. baseline-dev-cli-capabilities (Error) - Capabilities JSON has trailing comma
-5. baseline-region-annotations (Warning) - 8 files missing #region Purpose
-6. baseline-cpm-consistency (Warning) - 4 orphaned PackageVersion entries
-```
-
-### Passing checks (for reference)
-
-- `baseline-bin-dev` - bin/dev is present
-- `baseline-source-props` - source/Directory.Build.props exists
-- `baseline-msbuild-props` - msbuild/repository.props exists
-- `baseline-directory-packages` - Directory.Packages.props exists
-- `baseline-runfile-variables` - All #:project directives use MSBuild variables
-
-### Related Issue
-
-The NuGet release workflow failed due to workflow name mismatch:
-- Workflow renamed from `ci-cd.yml` to `workflow.yml`
-- NuGet trusted publishing policy still expects `ci-cd.yml`
-- This should be addressed separately (update NuGet policy or rename workflow back)
diff --git a/source/Directory.Build.props b/source/Directory.Build.props
index f5a7541..2da008d 100644
--- a/source/Directory.Build.props
+++ b/source/Directory.Build.props
@@ -4,7 +4,7 @@
- 1.0.0-beta.10
+ 1.0.0-beta.11
Steven T. Cramer
https://github.com/TimeWarpEngineering/timewarp-terminal
Unlicense
diff --git a/source/timewarp-terminal/iconsole.cs b/source/timewarp-terminal/iconsole.cs
index f4ee0fe..0c70e8f 100644
--- a/source/timewarp-terminal/iconsole.cs
+++ b/source/timewarp-terminal/iconsole.cs
@@ -72,4 +72,106 @@ public interface IConsole
/// The next line of characters from the input stream, or null if no more lines are available.
///
string? ReadLine();
+
+ ///
+ /// Reads the next character from the standard input stream.
+ ///
+ ///
+ /// The next character from the input stream, or -1 if no more characters are available.
+ ///
+ int Read();
+
+ ///
+ /// Obtains the next character or function key pressed by the user.
+ /// The pressed key is displayed in the console window.
+ ///
+ ///
+ /// An object that describes the constant and Unicode character,
+ /// if any, that correspond to the pressed console key.
+ ///
+ ConsoleKeyInfo ReadKey();
+
+ ///
+ /// Gets or sets the encoding the console uses to read input.
+ ///
+ /// The encoding used to read console input.
+ Encoding InputEncoding { get; set; }
+
+ ///
+ /// Gets or sets the encoding the console uses to write output.
+ ///
+ /// The encoding used to write console output.
+ Encoding OutputEncoding { get; set; }
+
+ ///
+ /// Gets a value indicating whether the input stream has been redirected from the standard input stream.
+ ///
+ /// true if input is redirected; otherwise, false.
+ bool IsInputRedirected { get; }
+
+ ///
+ /// Gets a value indicating whether the output stream has been redirected from the standard output stream.
+ ///
+ /// true if output is redirected; otherwise, false.
+ bool IsOutputRedirected { get; }
+
+ ///
+ /// Gets a value indicating whether the error stream has been redirected from the standard error stream.
+ ///
+ /// true if error output is redirected; otherwise, false.
+ bool IsErrorRedirected { get; }
+
+ ///
+ /// Acquires the standard input stream.
+ ///
+ /// The standard input stream.
+ Stream OpenStandardInput();
+
+ ///
+ /// Acquires the standard output stream.
+ ///
+ /// The standard output stream.
+ Stream OpenStandardOutput();
+
+ ///
+ /// Acquires the standard error output stream.
+ ///
+ /// The standard error output stream.
+ Stream OpenStandardError();
+
+ ///
+ /// Gets the standard input reader.
+ ///
+ /// A that represents the standard input stream.
+ TextReader In { get; }
+
+ ///
+ /// Gets the standard output writer.
+ ///
+ /// A that represents the standard output stream.
+ TextWriter Out { get; }
+
+ ///
+ /// Gets the standard error writer.
+ ///
+ /// A that represents the standard error output stream.
+ TextWriter Error { get; }
+
+ ///
+ /// Sets the property to the specified .
+ ///
+ /// A that represents the new standard input stream.
+ void SetIn(TextReader reader);
+
+ ///
+ /// Sets the property to the specified .
+ ///
+ /// A that represents the new standard output stream.
+ void SetOut(TextWriter writer);
+
+ ///
+ /// Sets the property to the specified .
+ ///
+ /// A that represents the new standard error output stream.
+ void SetError(TextWriter writer);
}
diff --git a/source/timewarp-terminal/iterminal.cs b/source/timewarp-terminal/iterminal.cs
index e20ffbe..ea669d9 100644
--- a/source/timewarp-terminal/iterminal.cs
+++ b/source/timewarp-terminal/iterminal.cs
@@ -62,6 +62,34 @@ public interface ITerminal : IConsole
///
ConsoleKeyInfo ReadKey(bool intercept);
+ ///
+ /// Gets or sets the column position of the cursor.
+ ///
+ /// The column position, 0-based from left to right.
+ int CursorLeft { get; set; }
+
+ ///
+ /// Gets or sets the row position of the cursor.
+ ///
+ /// The row position, 0-based from top to bottom.
+ int CursorTop { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the cursor is visible.
+ ///
+ /// true if the cursor is visible; otherwise, false.
+ bool CursorVisible { get; set; }
+
+ ///
+ /// Gets or sets the height of the cursor within a character cell.
+ ///
+ /// The cursor size as a percentage from 1 to 100.
+ ///
+ /// A value of 1 indicates a horizontal line at the bottom of the cell.
+ /// A value of 100 indicates a full block cursor.
+ ///
+ int CursorSize { get; set; }
+
///
/// Sets the position of the cursor.
///
@@ -76,10 +104,98 @@ public interface ITerminal : IConsole
(int Left, int Top) GetCursorPosition();
///
- /// Gets the width of the terminal window in characters.
+ /// Gets or sets the width of the terminal window in characters.
///
/// The width of the terminal window measured in columns.
- int WindowWidth { get; }
+ int WindowWidth { get; set; }
+
+ ///
+ /// Gets or sets the height of the terminal window in characters.
+ ///
+ /// The height of the terminal window measured in rows.
+ int WindowHeight { get; set; }
+
+ ///
+ /// Gets or sets the left position of the console window area.
+ ///
+ /// The leftmost position of the console window.
+ int WindowLeft { get; set; }
+
+ ///
+ /// Gets or sets the top position of the console window area.
+ ///
+ /// The topmost position of the console window.
+ int WindowTop { get; set; }
+
+ ///
+ /// Gets or sets the width of the buffer area.
+ ///
+ /// The width of the buffer area measured in columns.
+ int BufferWidth { get; set; }
+
+ ///
+ /// Gets or sets the height of the buffer area.
+ ///
+ /// The height of the buffer area measured in rows.
+ int BufferHeight { get; set; }
+
+ ///
+ /// Sets the dimensions of the console window to the specified values.
+ ///
+ /// The width of the console window measured in columns.
+ /// The height of the console window measured in rows.
+ void SetWindowSize(int width, int height);
+
+ ///
+ /// Sets the position of the console window relative to the screen buffer.
+ ///
+ /// The column position of the upper left corner of the console window.
+ /// The row position of the upper left corner of the console window.
+ void SetWindowPosition(int left, int top);
+
+ ///
+ /// Sets the height and width of the screen buffer area to the specified values.
+ ///
+ /// The width of the buffer area measured in columns.
+ /// The height of the buffer area measured in rows.
+ void SetBufferSize(int width, int height);
+
+ ///
+ /// Moves a specified source screen buffer area to a specified destination screen buffer area.
+ ///
+ /// The leftmost column of the source area.
+ /// The topmost row of the source area.
+ /// The number of columns in the source area.
+ /// The number of rows in the source area.
+ /// The leftmost column of the destination area.
+ /// The topmost row of the destination area.
+ /// The character used to fill the source area.
+ /// The foreground color used to fill the source area.
+ /// The background color used to fill the source area.
+ void MoveBufferArea
+ (
+ int sourceLeft,
+ int sourceTop,
+ int sourceWidth,
+ int sourceHeight,
+ int targetLeft,
+ int targetTop,
+ char sourceChar,
+ ConsoleColor sourceForeColor,
+ ConsoleColor sourceBackColor
+ );
+
+ ///
+ /// Gets the largest possible number of console window columns.
+ ///
+ /// The maximum width of the console window measured in columns.
+ int LargestWindowWidth { get; }
+
+ ///
+ /// Gets the largest possible number of console window rows.
+ ///
+ /// The maximum height of the console window measured in rows.
+ int LargestWindowHeight { get; }
///
/// Gets a value indicating whether the terminal is interactive.
@@ -112,6 +228,23 @@ public interface ITerminal : IConsole
///
bool SupportsHyperlinks { get; }
+ ///
+ /// Gets or sets the foreground color of the console.
+ ///
+ /// The foreground color. The default is gray.
+ ConsoleColor ForegroundColor { get; set; }
+
+ ///
+ /// Gets or sets the background color of the console.
+ ///
+ /// The background color. The default is black.
+ ConsoleColor BackgroundColor { get; set; }
+
+ ///
+ /// Resets the foreground and background console colors to their defaults.
+ ///
+ void ResetColor();
+
///
/// Clears the console buffer and corresponding console window of display information.
///
@@ -124,4 +257,44 @@ public interface ITerminal : IConsole
/// This event allows graceful handling of Ctrl+C for interactive applications like REPLs.
///
event ConsoleCancelEventHandler? CancelKeyPress;
+
+ ///
+ /// Plays a beep sound through the console speaker.
+ ///
+ void Beep();
+
+ ///
+ /// Plays a beep sound at the specified frequency and duration through the console speaker.
+ ///
+ ///
+ /// The frequency of the beep, ranging from 37 to 32767 hertz.
+ ///
+ ///
+ /// The duration of the beep, measured in milliseconds.
+ ///
+ void Beep(int frequency, int duration);
+
+ ///
+ /// Gets or sets a value indicating whether the Ctrl+C key combination
+ /// is treated as ordinary input or as an interrupt.
+ ///
+ ///
+ /// true if Ctrl+C is treated as ordinary input; false if it raises
+ /// the event. The default is false.
+ ///
+ bool TreatControlCAsInput { get; set; }
+
+ ///
+ /// Gets or sets the title to display in the console title bar.
+ ///
+ /// The string to display in the title bar of the console.
+ string Title { get; set; }
+
+ ///
+ /// Gets a value indicating whether a key press is available in the input stream.
+ ///
+ ///
+ /// true if a key press is available; otherwise, false.
+ ///
+ bool KeyAvailable { get; }
}
diff --git a/source/timewarp-terminal/terminal-static.cs b/source/timewarp-terminal/terminal-static.cs
index 64fe286..6d1a1a8 100644
--- a/source/timewarp-terminal/terminal-static.cs
+++ b/source/timewarp-terminal/terminal-static.cs
@@ -592,6 +592,116 @@ public static void WriteLink(string url, string text)
Instance.Write(link);
}
+ // Stream Access Methods (IConsole)
+
+ ///
+ /// Acquires the standard input stream.
+ ///
+ /// The standard input stream.
+ public static Stream OpenStandardInput() => Instance.OpenStandardInput();
+
+ ///
+ /// Acquires the standard output stream.
+ ///
+ /// The standard output stream.
+ public static Stream OpenStandardOutput() => Instance.OpenStandardOutput();
+
+ ///
+ /// Acquires the standard error output stream.
+ ///
+ /// The standard error output stream.
+ public static Stream OpenStandardError() => Instance.OpenStandardError();
+
+ ///
+ /// Gets the standard input reader.
+ ///
+ /// A that represents the standard input stream.
+ public static TextReader In => Instance.In;
+
+ ///
+ /// Gets the standard output writer.
+ ///
+ /// A that represents the standard output stream.
+ public static TextWriter Out => Instance.Out;
+
+ ///
+ /// Gets the standard error writer.
+ ///
+ /// A that represents the standard error output stream.
+ public static TextWriter Error => Instance.Error;
+
+ ///
+ /// Sets the property to the specified .
+ ///
+ /// A that represents the new standard input stream.
+ public static void SetIn(TextReader reader)
+ {
+ ArgumentNullException.ThrowIfNull(reader);
+ Instance.SetIn(reader);
+ }
+
+ ///
+ /// Sets the property to the specified .
+ ///
+ /// A that represents the new standard output stream.
+ public static void SetOut(TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ Instance.SetOut(writer);
+ }
+
+ ///
+ /// Sets the property to the specified .
+ ///
+ /// A that represents the new standard error output stream.
+ public static void SetError(TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ Instance.SetError(writer);
+ }
+
+ // Encoding Properties (IConsole)
+
+ ///
+ /// Gets or sets the encoding the console uses to read input.
+ ///
+ /// The encoding used to read console input.
+ public static Encoding InputEncoding
+ {
+ get => Instance.InputEncoding;
+ set => Instance.InputEncoding = value;
+ }
+
+ ///
+ /// Gets or sets the encoding the console uses to write output.
+ ///
+ /// The encoding used to write console output.
+ public static Encoding OutputEncoding
+ {
+ get => Instance.OutputEncoding;
+ set => Instance.OutputEncoding = value;
+ }
+
+ // Redirection Properties (IConsole)
+
+ ///
+ /// Gets a value indicating whether the input stream has been redirected from the standard input stream.
+ ///
+ /// true if input is redirected; otherwise, false.
+ public static bool IsInputRedirected => Instance.IsInputRedirected;
+
+ ///
+ /// Gets a value indicating whether the output stream has been redirected from the standard output stream.
+ ///
+ /// true if output is redirected; otherwise, false.
+ public static bool IsOutputRedirected => Instance.IsOutputRedirected;
+
+ ///
+ /// Gets a value indicating whether the error stream has been redirected from the standard error stream.
+ ///
+ /// true if error output is redirected; otherwise, false.
+ public static bool IsErrorRedirected => Instance.IsErrorRedirected;
+
// Input Methods
///
@@ -602,6 +712,24 @@ public static void WriteLink(string url, string text)
///
public static string? ReadLine() => Instance.ReadLine();
+ ///
+ /// Reads the next character from the standard input stream.
+ ///
+ ///
+ /// The next character from the input stream, or -1 if no more characters are available.
+ ///
+ public static int Read() => Instance.Read();
+
+ ///
+ /// Obtains the next character or function key pressed by the user.
+ /// The pressed key is displayed in the console window.
+ ///
+ ///
+ /// An object that describes the constant and Unicode character,
+ /// if any, that correspond to the pressed console key.
+ ///
+ public static ConsoleKeyInfo ReadKey() => Instance.ReadKey();
+
///
/// Obtains the next character or function key pressed by the user.
///
@@ -612,15 +740,252 @@ public static void WriteLink(string url, string text)
/// An object that describes the constant and Unicode character,
/// if any, that correspond to the pressed console key.
///
- public static ConsoleKeyInfo ReadKey(bool intercept = false) => Instance.ReadKey(intercept);
+ public static ConsoleKeyInfo ReadKey(bool intercept) => Instance.ReadKey(intercept);
+
+ // Cursor Properties (ITerminal)
+
+ ///
+ /// Gets or sets the column position of the cursor.
+ ///
+ /// The column position, 0-based from left to right.
+ public static int CursorLeft
+ {
+ get => Instance.CursorLeft;
+ set => Instance.CursorLeft = value;
+ }
+
+ ///
+ /// Gets or sets the row position of the cursor.
+ ///
+ /// The row position, 0-based from top to bottom.
+ public static int CursorTop
+ {
+ get => Instance.CursorTop;
+ set => Instance.CursorTop = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the cursor is visible.
+ ///
+ /// true if the cursor is visible; otherwise, false.
+ public static bool CursorVisible
+ {
+ get => Instance.CursorVisible;
+ set => Instance.CursorVisible = value;
+ }
+
+ ///
+ /// Gets or sets the height of the cursor within a character cell.
+ ///
+ /// The cursor size as a percentage from 1 to 100.
+ public static int CursorSize
+ {
+ get => Instance.CursorSize;
+ set => Instance.CursorSize = value;
+ }
+
+ // Window/Buffer Geometry Properties (ITerminal)
+
+ ///
+ /// Gets or sets the height of the terminal window in characters.
+ ///
+ /// The height of the terminal window measured in rows.
+ public static int WindowHeight
+ {
+ get => Instance.WindowHeight;
+ set => Instance.WindowHeight = value;
+ }
+
+ ///
+ /// Gets or sets the left position of the console window area.
+ ///
+ /// The leftmost position of the console window.
+ public static int WindowLeft
+ {
+ get => Instance.WindowLeft;
+ set => Instance.WindowLeft = value;
+ }
+
+ ///
+ /// Gets or sets the top position of the console window area.
+ ///
+ /// The topmost position of the console window.
+ public static int WindowTop
+ {
+ get => Instance.WindowTop;
+ set => Instance.WindowTop = value;
+ }
+
+ ///
+ /// Gets or sets the width of the buffer area.
+ ///
+ /// The width of the buffer area measured in columns.
+ public static int BufferWidth
+ {
+ get => Instance.BufferWidth;
+ set => Instance.BufferWidth = value;
+ }
+
+ ///
+ /// Gets or sets the height of the buffer area.
+ ///
+ /// The height of the buffer area measured in rows.
+ public static int BufferHeight
+ {
+ get => Instance.BufferHeight;
+ set => Instance.BufferHeight = value;
+ }
+
+ ///
+ /// Gets the largest possible number of console window columns.
+ ///
+ /// The maximum width of the console window measured in columns.
+ public static int LargestWindowWidth => Instance.LargestWindowWidth;
+
+ ///
+ /// Gets the largest possible number of console window rows.
+ ///
+ /// The maximum height of the console window measured in rows.
+ public static int LargestWindowHeight => Instance.LargestWindowHeight;
+
+ // Window/Buffer Geometry Methods (ITerminal)
+
+ ///
+ /// Sets the dimensions of the console window to the specified values.
+ ///
+ /// The width of the console window measured in columns.
+ /// The height of the console window measured in rows.
+ public static void SetWindowSize(int width, int height) => Instance.SetWindowSize(width, height);
+
+ ///
+ /// Sets the position of the console window relative to the screen buffer.
+ ///
+ /// The column position of the upper left corner of the console window.
+ /// The row position of the upper left corner of the console window.
+ public static void SetWindowPosition(int left, int top) => Instance.SetWindowPosition(left, top);
+
+ ///
+ /// Sets the height and width of the screen buffer area to the specified values.
+ ///
+ /// The width of the buffer area measured in columns.
+ /// The height of the buffer area measured in rows.
+ public static void SetBufferSize(int width, int height) => Instance.SetBufferSize(width, height);
+
+ ///
+ /// Moves a specified source screen buffer area to a specified destination screen buffer area.
+ ///
+ /// The leftmost column of the source area.
+ /// The topmost row of the source area.
+ /// The number of columns in the source area.
+ /// The number of rows in the source area.
+ /// The leftmost column of the destination area.
+ /// The topmost row of the destination area.
+ /// The character used to fill the source area.
+ /// The foreground color used to fill the source area.
+ /// The background color used to fill the source area.
+ public static void MoveBufferArea
+ (
+ int sourceLeft,
+ int sourceTop,
+ int sourceWidth,
+ int sourceHeight,
+ int targetLeft,
+ int targetTop,
+ char sourceChar,
+ ConsoleColor sourceForeColor,
+ ConsoleColor sourceBackColor
+ ) => Instance.MoveBufferArea(sourceLeft, sourceTop, sourceWidth, sourceHeight, targetLeft, targetTop, sourceChar, sourceForeColor, sourceBackColor);
+
+ // Color State Properties (ITerminal)
+
+ ///
+ /// Gets or sets the foreground color of the console.
+ ///
+ /// The foreground color. The default is gray.
+ public static ConsoleColor ForegroundColor
+ {
+ get => Instance.ForegroundColor;
+ set => Instance.ForegroundColor = value;
+ }
+
+ ///
+ /// Gets or sets the background color of the console.
+ ///
+ /// The background color. The default is black.
+ public static ConsoleColor BackgroundColor
+ {
+ get => Instance.BackgroundColor;
+ set => Instance.BackgroundColor = value;
+ }
+
+ ///
+ /// Resets the foreground and background console colors to their defaults.
+ ///
+ public static void ResetColor() => Instance.ResetColor();
+
+ // Control/Utility Properties (ITerminal)
+
+ ///
+ /// Gets or sets a value indicating whether the Ctrl+C key combination
+ /// is treated as ordinary input or as an interrupt.
+ ///
+ ///
+ /// true if Ctrl+C is treated as ordinary input; false if it raises
+ /// the event. The default is false.
+ ///
+ public static bool TreatControlCAsInput
+ {
+ get => Instance.TreatControlCAsInput;
+ set => Instance.TreatControlCAsInput = value;
+ }
+
+ ///
+ /// Gets or sets the title to display in the console title bar.
+ ///
+ /// The string to display in the title bar of the console.
+ public static string Title
+ {
+ get => Instance.Title;
+ set => Instance.Title = value;
+ }
+
+ ///
+ /// Gets a value indicating whether a key press is available in the input stream.
+ ///
+ ///
+ /// true if a key press is available; otherwise, false.
+ ///
+ public static bool KeyAvailable => Instance.KeyAvailable;
+
+ // Control/Utility Methods (ITerminal)
+
+ ///
+ /// Plays a beep sound through the console speaker.
+ ///
+ public static void Beep() => Instance.Beep();
+
+ ///
+ /// Plays a beep sound at the specified frequency and duration through the console speaker.
+ ///
+ ///
+ /// The frequency of the beep, ranging from 37 to 32767 hertz.
+ ///
+ ///
+ /// The duration of the beep, measured in milliseconds.
+ ///
+ public static void Beep(int frequency, int duration) => Instance.Beep(frequency, duration);
// Terminal Properties
///
- /// Gets the width of the terminal window in characters.
+ /// Gets or sets the width of the terminal window in characters.
///
/// The width of the terminal window measured in columns.
- public static int WindowWidth => Instance.WindowWidth;
+ public static int WindowWidth
+ {
+ get => Instance.WindowWidth;
+ set => Instance.WindowWidth = value;
+ }
///
/// Gets a value indicating whether the terminal is interactive.
diff --git a/source/timewarp-terminal/test-console.cs b/source/timewarp-terminal/test-console.cs
index 602f160..c494165 100644
--- a/source/timewarp-terminal/test-console.cs
+++ b/source/timewarp-terminal/test-console.cs
@@ -35,7 +35,89 @@ public sealed class TestConsole : IConsole, IDisposable
private readonly StringReader InputReader;
private readonly StringWriter OutputWriter;
private readonly StringWriter ErrorWriter;
+ private readonly Queue CharacterQueue;
private bool Disposed;
+ private TextReader InReader;
+ private TextWriter OutWriter;
+ private TextWriter ErrorWriterField;
+
+ ///
+ /// Gets or sets the mock standard input stream.
+ ///
+ public Stream StandardInputStream { get; set; }
+
+ ///
+ /// Gets or sets the mock standard output stream.
+ ///
+ public Stream StandardOutputStream { get; set; }
+
+ ///
+ /// Gets or sets the mock standard error stream.
+ ///
+ public Stream StandardErrorStream { get; set; }
+
+ ///
+ /// Gets or sets the input encoding for this test console.
+ ///
+ /// The encoding used for input. Defaults to .
+ public Encoding InputEncoding { get; set; } = Encoding.UTF8;
+
+ ///
+ /// Gets or sets the output encoding for this test console.
+ ///
+ /// The encoding used for output. Defaults to .
+ public Encoding OutputEncoding { get; set; } = Encoding.UTF8;
+
+ ///
+ /// Gets or sets a value indicating whether input is redirected.
+ ///
+ /// true if input is redirected; otherwise, false. Defaults to false.
+ public bool IsInputRedirected { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether output is redirected.
+ ///
+ /// true if output is redirected; otherwise, false. Defaults to false.
+ public bool IsOutputRedirected { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether error output is redirected.
+ ///
+ /// true if error output is redirected; otherwise, false. Defaults to false.
+ public bool IsErrorRedirected { get; set; }
+
+ ///
+ public Stream OpenStandardInput()
+ => StandardInputStream;
+
+ ///
+ public Stream OpenStandardOutput()
+ => StandardOutputStream;
+
+ ///
+ public Stream OpenStandardError()
+ => StandardErrorStream;
+
+ ///
+ public TextReader In => InReader;
+
+ ///
+ public TextWriter Out => OutWriter;
+
+ ///
+ public TextWriter Error => ErrorWriterField;
+
+ ///
+ public void SetIn(TextReader reader)
+ => InReader = reader ?? throw new ArgumentNullException(nameof(reader));
+
+ ///
+ public void SetOut(TextWriter writer)
+ => OutWriter = writer ?? throw new ArgumentNullException(nameof(writer));
+
+ ///
+ public void SetError(TextWriter writer)
+ => ErrorWriterField = writer ?? throw new ArgumentNullException(nameof(writer));
///
/// Initializes a new instance of with optional scripted input.
@@ -49,6 +131,13 @@ public TestConsole(string input = "")
InputReader = new StringReader(input);
OutputWriter = new StringWriter();
ErrorWriter = new StringWriter();
+ CharacterQueue = new Queue();
+ InReader = InputReader;
+ OutWriter = OutputWriter;
+ ErrorWriterField = ErrorWriter;
+ StandardInputStream = new MemoryStream();
+ StandardOutputStream = new MemoryStream();
+ StandardErrorStream = new MemoryStream();
}
///
@@ -99,6 +188,35 @@ public async Task WriteErrorLineAsync(string? message = null)
public string? ReadLine()
=> InputReader.ReadLine();
+ ///
+ public int Read()
+ {
+ if (CharacterQueue.Count > 0)
+ return CharacterQueue.Dequeue();
+
+ return -1;
+ }
+
+ ///
+ public ConsoleKeyInfo ReadKey()
+ => throw new NotSupportedException("TestConsole does not support key-by-key input. Use TestTerminal for interactive key input.");
+
+ ///
+ /// Queues characters for to return.
+ ///
+ /// The characters to queue.
+ public void QueueCharacters(string characters)
+ {
+ ArgumentNullException.ThrowIfNull(characters);
+ foreach (char c in characters)
+ CharacterQueue.Enqueue(c);
+ }
+
+ ///
+ /// Gets the number of characters currently in the queue.
+ ///
+ public int CharactersInQueue => CharacterQueue.Count;
+
///
/// Clears all captured output.
///
@@ -149,6 +267,9 @@ public void Dispose()
InputReader.Dispose();
OutputWriter.Dispose();
ErrorWriter.Dispose();
+ StandardInputStream.Dispose();
+ StandardOutputStream.Dispose();
+ StandardErrorStream.Dispose();
Disposed = true;
}
}
diff --git a/source/timewarp-terminal/test-terminal.cs b/source/timewarp-terminal/test-terminal.cs
index d7f354e..1f4ce89 100644
--- a/source/timewarp-terminal/test-terminal.cs
+++ b/source/timewarp-terminal/test-terminal.cs
@@ -45,9 +45,91 @@ public sealed class TestTerminal : ITerminal, IDisposable
private readonly StringWriter OutputWriter;
private readonly StringWriter ErrorWriter;
private readonly Queue KeyQueue;
- private int CursorLeft;
- private int CursorTop;
+ private int CursorLeftField;
+ private int CursorTopField;
+ private int CursorSizeField = 100;
private bool Disposed;
+ private TextReader InReader;
+ private TextWriter OutWriter;
+ private TextWriter ErrorWriterField;
+
+ ///
+ /// Gets or sets the mock standard input stream.
+ ///
+ public Stream StandardInputStream { get; set; }
+
+ ///
+ /// Gets or sets the mock standard output stream.
+ ///
+ public Stream StandardOutputStream { get; set; }
+
+ ///
+ /// Gets or sets the mock standard error stream.
+ ///
+ public Stream StandardErrorStream { get; set; }
+
+ ///
+ /// Gets or sets the input encoding for this test terminal.
+ ///
+ /// The encoding used for input. Defaults to .
+ public Encoding InputEncoding { get; set; } = Encoding.UTF8;
+
+ ///
+ /// Gets or sets the output encoding for this test terminal.
+ ///
+ /// The encoding used for output. Defaults to .
+ public Encoding OutputEncoding { get; set; } = Encoding.UTF8;
+
+ ///
+ /// Gets or sets a value indicating whether input is redirected.
+ ///
+ /// true if input is redirected; otherwise, false. Defaults to false.
+ public bool IsInputRedirected { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether output is redirected.
+ ///
+ /// true if output is redirected; otherwise, false. Defaults to false.
+ public bool IsOutputRedirected { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether error output is redirected.
+ ///
+ /// true if error output is redirected; otherwise, false. Defaults to false.
+ public bool IsErrorRedirected { get; set; }
+
+ ///
+ public Stream OpenStandardInput()
+ => StandardInputStream;
+
+ ///
+ public Stream OpenStandardOutput()
+ => StandardOutputStream;
+
+ ///
+ public Stream OpenStandardError()
+ => StandardErrorStream;
+
+ ///
+ public TextReader In => InReader;
+
+ ///
+ public TextWriter Out => OutWriter;
+
+ ///
+ public TextWriter Error => ErrorWriterField;
+
+ ///
+ public void SetIn(TextReader reader)
+ => InReader = reader ?? throw new ArgumentNullException(nameof(reader));
+
+ ///
+ public void SetOut(TextWriter writer)
+ => OutWriter = writer ?? throw new ArgumentNullException(nameof(writer));
+
+ ///
+ public void SetError(TextWriter writer)
+ => ErrorWriterField = writer ?? throw new ArgumentNullException(nameof(writer));
///
/// Initializes a new instance of with optional scripted line input.
@@ -65,10 +147,45 @@ public TestTerminal(string input = "")
WindowWidth = 80;
IsInteractive = false; // Testing is non-interactive by default
SupportsColor = true;
+ InReader = InputReader;
+ OutWriter = OutputWriter;
+ ErrorWriterField = ErrorWriter;
+ StandardInputStream = new MemoryStream();
+ StandardOutputStream = new MemoryStream();
+ StandardErrorStream = new MemoryStream();
// Suppress unused field warnings - these fields will be used when REPL is updated to use ITerminal
- _ = CursorLeft;
- _ = CursorTop;
+ _ = CursorLeftField;
+ _ = CursorTopField;
+ }
+
+ ///
+ public int CursorLeft
+ {
+ get => CursorLeftField;
+ set => CursorLeftField = value;
+ }
+
+ ///
+ public int CursorTop
+ {
+ get => CursorTopField;
+ set => CursorTopField = value;
+ }
+
+ ///
+ public bool CursorVisible { get; set; } = true;
+
+ ///
+ public int CursorSize
+ {
+ get => CursorSizeField;
+ set
+ {
+ if (value < 1 || value > 100)
+ throw new ArgumentOutOfRangeException(nameof(value), value, "CursorSize must be between 1 and 100.");
+ CursorSizeField = value;
+ }
}
///
@@ -127,6 +244,22 @@ public async Task WriteErrorLineAsync(string? message = null)
public string? ReadLine()
=> InputReader.ReadLine();
+ ///
+ public int Read()
+ {
+ if (KeyQueue.Count > 0)
+ {
+ ConsoleKeyInfo keyInfo = KeyQueue.Dequeue();
+ return keyInfo.KeyChar;
+ }
+
+ return -1;
+ }
+
+ ///
+ public ConsoleKeyInfo ReadKey()
+ => ReadKey(false);
+
///
public ConsoleKeyInfo ReadKey(bool intercept)
{
@@ -159,17 +292,81 @@ public ConsoleKeyInfo ReadKey(bool intercept)
///
public void SetCursorPosition(int left, int top)
{
- CursorLeft = left;
- CursorTop = top;
+ CursorLeftField = left;
+ CursorTopField = top;
}
///
public (int Left, int Top) GetCursorPosition()
- => (CursorLeft, CursorTop);
+ => (CursorLeftField, CursorTopField);
///
public int WindowWidth { get; set; }
+ ///
+ public int WindowHeight { get; set; } = 24;
+
+ ///
+ public int WindowLeft { get; set; }
+
+ ///
+ public int WindowTop { get; set; }
+
+ ///
+ public int BufferWidth { get; set; } = 80;
+
+ ///
+ public int BufferHeight { get; set; } = 300;
+
+ ///
+ public int LargestWindowWidth { get; set; } = 120;
+
+ ///
+ public int LargestWindowHeight { get; set; } = 40;
+
+ ///
+ /// Gets the number of times has been called.
+ ///
+ public int MoveBufferAreaCallCount { get; private set; }
+
+ ///
+ public void SetWindowSize(int width, int height)
+ {
+ WindowWidth = width;
+ WindowHeight = height;
+ }
+
+ ///
+ public void SetWindowPosition(int left, int top)
+ {
+ WindowLeft = left;
+ WindowTop = top;
+ }
+
+ ///
+ public void SetBufferSize(int width, int height)
+ {
+ BufferWidth = width;
+ BufferHeight = height;
+ }
+
+ ///
+ public void MoveBufferArea
+ (
+ int sourceLeft,
+ int sourceTop,
+ int sourceWidth,
+ int sourceHeight,
+ int targetLeft,
+ int targetTop,
+ char sourceChar,
+ ConsoleColor sourceForeColor,
+ ConsoleColor sourceBackColor
+ )
+ {
+ MoveBufferAreaCallCount++;
+ }
+
///
public bool IsInteractive { get; set; }
@@ -179,6 +376,19 @@ public void SetCursorPosition(int left, int top)
///
public bool SupportsHyperlinks { get; set; }
+ ///
+ public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Gray;
+
+ ///
+ public ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black;
+
+ ///
+ public void ResetColor()
+ {
+ ForegroundColor = ConsoleColor.Gray;
+ BackgroundColor = ConsoleColor.Black;
+ }
+
private ConsoleCancelEventHandler? CancelKeyPressHandler;
///
@@ -216,6 +426,42 @@ public void SimulateCancelKeyPress(ConsoleSpecialKey specialKey = ConsoleSpecial
public void Clear()
=> OutputWriter.WriteLine("[CLEAR]");
+ ///
+ /// Gets the number of times has been called.
+ ///
+ public int BeepCount { get; private set; }
+
+ ///
+ /// Gets the frequency of the last call.
+ ///
+ public int LastBeepFrequency { get; private set; }
+
+ ///
+ /// Gets the duration of the last call.
+ ///
+ public int LastBeepDuration { get; private set; }
+
+ ///
+ public void Beep()
+ => BeepCount++;
+
+ ///
+ public void Beep(int frequency, int duration)
+ {
+ BeepCount++;
+ LastBeepFrequency = frequency;
+ LastBeepDuration = duration;
+ }
+
+ ///
+ public bool TreatControlCAsInput { get; set; }
+
+ ///
+ public string Title { get; set; } = string.Empty;
+
+ ///
+ public bool KeyAvailable => KeyQueue.Count > 0;
+
// ========== Test Helper Methods ==========
///
@@ -349,6 +595,9 @@ public void Dispose()
InputReader.Dispose();
OutputWriter.Dispose();
ErrorWriter.Dispose();
+ StandardInputStream.Dispose();
+ StandardOutputStream.Dispose();
+ StandardErrorStream.Dispose();
Disposed = true;
}
diff --git a/source/timewarp-terminal/timewarp-console.cs b/source/timewarp-terminal/timewarp-console.cs
index 4d766db..bbe79a5 100644
--- a/source/timewarp-terminal/timewarp-console.cs
+++ b/source/timewarp-terminal/timewarp-console.cs
@@ -46,4 +46,68 @@ public Task WriteErrorLineAsync(string? message = null)
///
public string? ReadLine()
=> Console.ReadLine();
+
+ ///
+ public int Read()
+ => Console.Read();
+
+ ///
+ public ConsoleKeyInfo ReadKey()
+ => Console.ReadKey(false);
+
+ ///
+ public Encoding InputEncoding
+ {
+ get => Console.InputEncoding;
+ set => Console.InputEncoding = value;
+ }
+
+ ///
+ public Encoding OutputEncoding
+ {
+ get => Console.OutputEncoding;
+ set => Console.OutputEncoding = value;
+ }
+
+ ///
+ public bool IsInputRedirected => Console.IsInputRedirected;
+
+ ///
+ public bool IsOutputRedirected => Console.IsOutputRedirected;
+
+ ///
+ public bool IsErrorRedirected => Console.IsErrorRedirected;
+
+ ///
+ public Stream OpenStandardInput()
+ => Console.OpenStandardInput();
+
+ ///
+ public Stream OpenStandardOutput()
+ => Console.OpenStandardOutput();
+
+ ///
+ public Stream OpenStandardError()
+ => Console.OpenStandardError();
+
+ ///
+ public TextReader In => Console.In;
+
+ ///
+ public TextWriter Out => Console.Out;
+
+ ///
+ public TextWriter Error => Console.Error;
+
+ ///
+ public void SetIn(TextReader reader)
+ => Console.SetIn(reader);
+
+ ///
+ public void SetOut(TextWriter writer)
+ => Console.SetOut(writer);
+
+ ///
+ public void SetError(TextWriter writer)
+ => Console.SetError(writer);
}
diff --git a/source/timewarp-terminal/timewarp-terminal.cs b/source/timewarp-terminal/timewarp-terminal.cs
index de97ce9..13b45dd 100644
--- a/source/timewarp-terminal/timewarp-terminal.cs
+++ b/source/timewarp-terminal/timewarp-terminal.cs
@@ -71,10 +71,199 @@ public Task WriteErrorLineAsync(string? message = null)
public string? ReadLine()
=> Console.ReadLine();
+ ///
+ public int Read()
+ => Console.Read();
+
+ ///
+ public ConsoleKeyInfo ReadKey()
+ => Console.ReadKey(false);
+
+ ///
+ public Encoding InputEncoding
+ {
+ get => Console.InputEncoding;
+ set => Console.InputEncoding = value;
+ }
+
+ ///
+ public Encoding OutputEncoding
+ {
+ get => Console.OutputEncoding;
+ set => Console.OutputEncoding = value;
+ }
+
+ ///
+ public bool IsInputRedirected => Console.IsInputRedirected;
+
+ ///
+ public bool IsOutputRedirected => Console.IsOutputRedirected;
+
+ ///
+ public bool IsErrorRedirected => Console.IsErrorRedirected;
+
+ ///
+ public Stream OpenStandardInput()
+ => Console.OpenStandardInput();
+
+ ///
+ public Stream OpenStandardOutput()
+ => Console.OpenStandardOutput();
+
+ ///
+ public Stream OpenStandardError()
+ => Console.OpenStandardError();
+
+ ///
+ public TextReader In => Console.In;
+
+ ///
+ public TextWriter Out => Console.Out;
+
+ ///
+ public TextWriter Error => Console.Error;
+
+ ///
+ public void SetIn(TextReader reader)
+ => Console.SetIn(reader);
+
+ ///
+ public void SetOut(TextWriter writer)
+ => Console.SetOut(writer);
+
+ ///
+ public void SetError(TextWriter writer)
+ => Console.SetError(writer);
+
///
public ConsoleKeyInfo ReadKey(bool intercept)
=> Console.ReadKey(intercept);
+ ///
+ public int CursorLeft
+ {
+ get
+ {
+ try
+ {
+ return Console.CursorLeft;
+ }
+ catch (IOException)
+ {
+ return 0;
+ }
+ }
+ set
+ {
+ try
+ {
+ Console.CursorLeft = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public int CursorTop
+ {
+ get
+ {
+ try
+ {
+ return Console.CursorTop;
+ }
+ catch (IOException)
+ {
+ return 0;
+ }
+ }
+ set
+ {
+ try
+ {
+ Console.CursorTop = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public bool CursorVisible
+ {
+ get
+ {
+ if (!OperatingSystem.IsWindows())
+ return true;
+
+ try
+ {
+ return Console.CursorVisible;
+ }
+ catch (IOException)
+ {
+ return true;
+ }
+ }
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.CursorVisible = value;
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public int CursorSize
+ {
+ get
+ {
+ if (!OperatingSystem.IsWindows())
+ return 100;
+
+ try
+ {
+ return Console.CursorSize;
+ }
+ catch (IOException)
+ {
+ return 100;
+ }
+ }
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.CursorSize = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
///
public void SetCursorPosition(int left, int top)
{
@@ -117,10 +306,308 @@ public int WindowWidth
}
catch (IOException)
{
- // Return default width if console is redirected
return 80;
}
}
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.WindowWidth = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public int WindowHeight
+ {
+ get
+ {
+ try
+ {
+ return Console.WindowHeight;
+ }
+ catch (IOException)
+ {
+ return 24;
+ }
+ }
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.WindowHeight = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public int WindowLeft
+ {
+ get
+ {
+ try
+ {
+ return Console.WindowLeft;
+ }
+ catch (IOException)
+ {
+ return 0;
+ }
+ }
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.WindowLeft = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public int WindowTop
+ {
+ get
+ {
+ try
+ {
+ return Console.WindowTop;
+ }
+ catch (IOException)
+ {
+ return 0;
+ }
+ }
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.WindowTop = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public int BufferWidth
+ {
+ get
+ {
+ try
+ {
+ return Console.BufferWidth;
+ }
+ catch (IOException)
+ {
+ return 80;
+ }
+ }
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.BufferWidth = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public int BufferHeight
+ {
+ get
+ {
+ try
+ {
+ return Console.BufferHeight;
+ }
+ catch (IOException)
+ {
+ return 300;
+ }
+ }
+ set
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.BufferHeight = value;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public void SetWindowSize(int width, int height)
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.SetWindowSize(width, height);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+
+ ///
+ public void SetWindowPosition(int left, int top)
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.SetWindowPosition(left, top);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+
+ ///
+ public void SetBufferSize(int width, int height)
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.SetBufferSize(width, height);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ catch (IOException)
+ {
+ }
+ }
+
+ ///
+ public void MoveBufferArea
+ (
+ int sourceLeft,
+ int sourceTop,
+ int sourceWidth,
+ int sourceHeight,
+ int targetLeft,
+ int targetTop,
+ char sourceChar,
+ ConsoleColor sourceForeColor,
+ ConsoleColor sourceBackColor
+ )
+ {
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ try
+ {
+ Console.MoveBufferArea
+ (
+ sourceLeft,
+ sourceTop,
+ sourceWidth,
+ sourceHeight,
+ targetLeft,
+ targetTop,
+ sourceChar,
+ sourceForeColor,
+ sourceBackColor
+ );
+ }
+ catch (IOException)
+ {
+ }
+ }
+
+ ///
+ public int LargestWindowWidth
+ {
+ get
+ {
+ try
+ {
+ return Console.LargestWindowWidth;
+ }
+ catch (IOException)
+ {
+ return 120;
+ }
+ }
+ }
+
+ ///
+ public int LargestWindowHeight
+ {
+ get
+ {
+ try
+ {
+ return Console.LargestWindowHeight;
+ }
+ catch (IOException)
+ {
+ return 40;
+ }
+ }
}
///
@@ -172,7 +659,6 @@ private static bool DetectHyperlinkSupport()
return false;
}
- ///
///
public event ConsoleCancelEventHandler? CancelKeyPress
{
@@ -180,6 +666,71 @@ public event ConsoleCancelEventHandler? CancelKeyPress
remove => Console.CancelKeyPress -= value;
}
+ ///
+ public ConsoleColor ForegroundColor
+ {
+ get
+ {
+ try
+ {
+ return Console.ForegroundColor;
+ }
+ catch (IOException)
+ {
+ return ConsoleColor.Gray;
+ }
+ }
+ set
+ {
+ try
+ {
+ Console.ForegroundColor = value;
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public ConsoleColor BackgroundColor
+ {
+ get
+ {
+ try
+ {
+ return Console.BackgroundColor;
+ }
+ catch (IOException)
+ {
+ return ConsoleColor.Black;
+ }
+ }
+ set
+ {
+ try
+ {
+ Console.BackgroundColor = value;
+ }
+ catch (IOException)
+ {
+ }
+ }
+ }
+
+ ///
+ public void ResetColor()
+ {
+ try
+ {
+ Console.ResetColor();
+ }
+ catch (IOException)
+ {
+ }
+ }
+
+ ///
public void Clear()
{
try
@@ -191,4 +742,57 @@ public void Clear()
// Silently ignore if console is redirected
}
}
+
+ ///
+ public void Beep()
+ {
+ if (OperatingSystem.IsWindows())
+ Console.Beep();
+ }
+
+ ///
+ public void Beep(int frequency, int duration)
+ {
+ if (OperatingSystem.IsWindows())
+ Console.Beep(frequency, duration);
+ }
+
+ ///
+ public bool TreatControlCAsInput
+ {
+ get => Console.TreatControlCAsInput;
+ set => Console.TreatControlCAsInput = value;
+ }
+
+ ///
+ public string Title
+ {
+ get
+ {
+ if (OperatingSystem.IsWindows())
+ return Console.Title;
+ return string.Empty;
+ }
+ set
+ {
+ if (OperatingSystem.IsWindows())
+ Console.Title = value;
+ }
+ }
+
+ ///
+ public bool KeyAvailable
+ {
+ get
+ {
+ try
+ {
+ return Console.KeyAvailable;
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+ }
+ }
}
diff --git a/tests/console-encoding-01-basic.cs b/tests/console-encoding-01-basic.cs
new file mode 100644
index 0000000..e266138
--- /dev/null
+++ b/tests/console-encoding-01-basic.cs
@@ -0,0 +1,323 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test IConsole encoding and redirection properties
+using System.Text;
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.ConsoleEncoding
+{
+
+[TestTag("IConsole")]
+[TestTag("Encoding")]
+public class ConsoleEncodingBasicTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_default_input_encoding_to_utf8_in_test_console()
+ {
+ // Arrange & Act
+ using TestConsole console = new();
+
+ // Assert
+ console.InputEncoding.ShouldBe(Encoding.UTF8);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_output_encoding_to_utf8_in_test_console()
+ {
+ // Arrange & Act
+ using TestConsole console = new();
+
+ // Assert
+ console.OutputEncoding.ShouldBe(Encoding.UTF8);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_input_encoding_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ Encoding unicode = Encoding.Unicode;
+
+ // Act
+ console.InputEncoding = unicode;
+
+ // Assert
+ console.InputEncoding.ShouldBe(unicode);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_output_encoding_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ Encoding ascii = Encoding.ASCII;
+
+ // Act
+ console.OutputEncoding = ascii;
+
+ // Assert
+ console.OutputEncoding.ShouldBe(ascii);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_is_input_redirected_to_false_in_test_console()
+ {
+ // Arrange & Act
+ using TestConsole console = new();
+
+ // Assert
+ console.IsInputRedirected.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_is_output_redirected_to_false_in_test_console()
+ {
+ // Arrange & Act
+ using TestConsole console = new();
+
+ // Assert
+ console.IsOutputRedirected.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_is_error_redirected_to_false_in_test_console()
+ {
+ // Arrange & Act
+ using TestConsole console = new();
+
+ // Assert
+ console.IsErrorRedirected.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_is_input_redirected_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ console.IsInputRedirected = true;
+
+ // Assert
+ console.IsInputRedirected.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_is_output_redirected_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ console.IsOutputRedirected = true;
+
+ // Assert
+ console.IsOutputRedirected.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_is_error_redirected_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ console.IsErrorRedirected = true;
+
+ // Assert
+ console.IsErrorRedirected.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+}
+
+[TestTag("ITerminal")]
+[TestTag("Encoding")]
+public class TerminalEncodingBasicTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_default_input_encoding_to_utf8_in_test_terminal()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.InputEncoding.ShouldBe(Encoding.UTF8);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_output_encoding_to_utf8_in_test_terminal()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.OutputEncoding.ShouldBe(Encoding.UTF8);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_input_encoding_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ Encoding unicode = Encoding.Unicode;
+
+ // Act
+ terminal.InputEncoding = unicode;
+
+ // Assert
+ terminal.InputEncoding.ShouldBe(unicode);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_output_encoding_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ Encoding ascii = Encoding.ASCII;
+
+ // Act
+ terminal.OutputEncoding = ascii;
+
+ // Assert
+ terminal.OutputEncoding.ShouldBe(ascii);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_is_input_redirected_to_false_in_test_terminal()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.IsInputRedirected.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_is_output_redirected_to_false_in_test_terminal()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.IsOutputRedirected.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_is_error_redirected_to_false_in_test_terminal()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.IsErrorRedirected.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_is_input_redirected_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.IsInputRedirected = true;
+
+ // Assert
+ terminal.IsInputRedirected.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_is_output_redirected_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.IsOutputRedirected = true;
+
+ // Assert
+ terminal.IsOutputRedirected.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_is_error_redirected_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.IsErrorRedirected = true;
+
+ // Assert
+ terminal.IsErrorRedirected.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_access_encoding_via_iconsole_interface()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+#pragma warning disable CA1859 // Intentionally testing IConsole interface access
+ IConsole console = terminal;
+#pragma warning restore CA1859
+ Encoding utf32 = Encoding.UTF32;
+
+ // Act
+ console.InputEncoding = utf32;
+ console.OutputEncoding = utf32;
+
+ // Assert
+ console.InputEncoding.ShouldBe(utf32);
+ console.OutputEncoding.ShouldBe(utf32);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_access_redirection_via_iconsole_interface()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+#pragma warning disable CA1859 // Intentionally testing IConsole interface access
+ IConsole console = terminal;
+#pragma warning restore CA1859
+
+ // Act
+ console.IsInputRedirected.ShouldBeFalse();
+ console.IsOutputRedirected.ShouldBeFalse();
+ console.IsErrorRedirected.ShouldBeFalse();
+
+ // Assert - properties are accessible via IConsole interface
+ console.ShouldBeAssignableTo();
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.ConsoleEncoding
diff --git a/tests/rich-input-01-basic.cs b/tests/rich-input-01-basic.cs
new file mode 100644
index 0000000..5936ae1
--- /dev/null
+++ b/tests/rich-input-01-basic.cs
@@ -0,0 +1,256 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test IConsole rich input APIs (Read, ReadKey)
+#pragma warning disable CA1859 // Intentionally testing interface access
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.RichInput
+{
+
+[TestTag("IConsole")]
+[TestTag("Input")]
+public class ConsoleReadBasicTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_return_single_character_from_queue()
+ {
+ // Arrange
+ using TestConsole console = new();
+ console.QueueCharacters("A");
+
+ // Act
+ int result = console.Read();
+
+ // Assert
+ result.ShouldBe((int)'A');
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_negative_one_when_queue_empty()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ int result = console.Read();
+
+ // Assert
+ result.ShouldBe(-1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_characters_in_fifo_order()
+ {
+ // Arrange
+ using TestConsole console = new();
+ console.QueueCharacters("ABC");
+
+ // Act & Assert
+ console.Read().ShouldBe((int)'A');
+ console.Read().ShouldBe((int)'B');
+ console.Read().ShouldBe((int)'C');
+ console.Read().ShouldBe(-1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_throw_notsupported_for_readkey_in_testconsole()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act & Assert
+ NotSupportedException exception = Should.Throw(() => console.ReadKey());
+ exception.Message.ShouldContain("TestConsole does not support key-by-key input");
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_access_read_via_iconsole_interface()
+ {
+ // Arrange
+ using TestConsole console = new();
+ IConsole iconsole = console;
+ console.QueueCharacters("X");
+
+ // Act
+ int result = iconsole.Read();
+
+ // Assert
+ result.ShouldBe((int)'X');
+
+ await Task.CompletedTask;
+ }
+}
+
+[TestTag("ITerminal")]
+[TestTag("Input")]
+public class TerminalReadBasicTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_return_character_from_key_queue()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKeys("H");
+
+ // Act
+ int result = terminal.Read();
+
+ // Assert
+ result.ShouldBe((int)'H');
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_negative_one_when_key_queue_empty()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ int result = terminal.Read();
+
+ // Assert
+ result.ShouldBe(-1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_characters_from_queued_keys_in_order()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKeys("abc");
+
+ // Act & Assert
+ terminal.Read().ShouldBe((int)'a');
+ terminal.Read().ShouldBe((int)'b');
+ terminal.Read().ShouldBe((int)'c');
+ terminal.Read().ShouldBe(-1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_read_key_without_intercept_parameter()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKey(ConsoleKey.Enter);
+
+ // Act
+ ConsoleKeyInfo keyInfo = terminal.ReadKey();
+
+ // Assert
+ keyInfo.Key.ShouldBe(ConsoleKey.Enter);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_read_key_with_intercept_true()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKey(ConsoleKey.Tab);
+
+ // Act
+ ConsoleKeyInfo keyInfo = terminal.ReadKey(true);
+
+ // Assert
+ keyInfo.Key.ShouldBe(ConsoleKey.Tab);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_read_key_with_intercept_false()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKey(ConsoleKey.Escape);
+
+ // Act
+ ConsoleKeyInfo keyInfo = terminal.ReadKey(false);
+
+ // Assert
+ keyInfo.Key.ShouldBe(ConsoleKey.Escape);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_access_read_via_iconsole_interface()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ IConsole iconsole = terminal;
+ terminal.QueueKeys("Z");
+
+ // Act
+ int result = iconsole.Read();
+
+ // Assert
+ result.ShouldBe((int)'Z');
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_access_readkey_via_iconsole_interface()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ IConsole iconsole = terminal;
+ terminal.QueueKey(ConsoleKey.Spacebar);
+
+ // Act
+ ConsoleKeyInfo keyInfo = iconsole.ReadKey();
+
+ // Assert
+ keyInfo.Key.ShouldBe(ConsoleKey.Spacebar);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_dequeue_key_on_read()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKey(ConsoleKey.A);
+ terminal.QueueKey(ConsoleKey.B);
+
+ // Act
+ ConsoleKeyInfo first = terminal.ReadKey();
+ ConsoleKeyInfo second = terminal.ReadKey();
+
+ // Assert
+ first.Key.ShouldBe(ConsoleKey.A);
+ second.Key.ShouldBe(ConsoleKey.B);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_key_char_from_read()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKeys("X");
+
+ // Act
+ int result = terminal.Read();
+
+ // Assert
+ result.ShouldBe((int)'X');
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.RichInput
diff --git a/tests/stream-access-01-basic.cs b/tests/stream-access-01-basic.cs
new file mode 100755
index 0000000..6223cb0
--- /dev/null
+++ b/tests/stream-access-01-basic.cs
@@ -0,0 +1,441 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test IConsole stream access APIs
+using System.IO;
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.StreamAccess
+{
+
+[TestTag("IConsole")]
+[TestTag("Stream")]
+public class ConsoleStreamAccessTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_return_stream_from_open_standard_input_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ Stream stream = console.OpenStandardInput();
+
+ // Assert
+ stream.ShouldNotBeNull();
+ stream.ShouldBeSameAs(console.StandardInputStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_stream_from_open_standard_output_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ Stream stream = console.OpenStandardOutput();
+
+ // Assert
+ stream.ShouldNotBeNull();
+ stream.ShouldBeSameAs(console.StandardOutputStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_stream_from_open_standard_error_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ Stream stream = console.OpenStandardError();
+
+ // Assert
+ stream.ShouldNotBeNull();
+ stream.ShouldBeSameAs(console.StandardErrorStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_text_reader_from_in_property_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ TextReader reader = console.In;
+
+ // Assert
+ reader.ShouldNotBeNull();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_text_writer_from_out_property_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ TextWriter writer = console.Out;
+
+ // Assert
+ writer.ShouldNotBeNull();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_text_writer_from_error_property_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+
+ // Act
+ TextWriter writer = console.Error;
+
+ // Assert
+ writer.ShouldNotBeNull();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_in_reader_via_set_in_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ StringReader newReader = new("test input");
+
+ // Act
+ console.SetIn(newReader);
+
+ // Assert
+ console.In.ShouldBeSameAs(newReader);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_out_writer_via_set_out_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ StringWriter newWriter = new();
+
+ // Act
+ console.SetOut(newWriter);
+
+ // Assert
+ console.Out.ShouldBeSameAs(newWriter);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_error_writer_via_set_error_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ StringWriter newWriter = new();
+
+ // Act
+ console.SetError(newWriter);
+
+ // Assert
+ console.Error.ShouldBeSameAs(newWriter);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_allow_custom_standard_input_stream_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ MemoryStream customStream = new();
+
+ // Act
+ console.StandardInputStream = customStream;
+ Stream stream = console.OpenStandardInput();
+
+ // Assert
+ stream.ShouldBeSameAs(customStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_allow_custom_standard_output_stream_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ MemoryStream customStream = new();
+
+ // Act
+ console.StandardOutputStream = customStream;
+ Stream stream = console.OpenStandardOutput();
+
+ // Assert
+ stream.ShouldBeSameAs(customStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_allow_custom_standard_error_stream_in_test_console()
+ {
+ // Arrange
+ using TestConsole console = new();
+ MemoryStream customStream = new();
+
+ // Act
+ console.StandardErrorStream = customStream;
+ Stream stream = console.OpenStandardError();
+
+ // Assert
+ stream.ShouldBeSameAs(customStream);
+
+ await Task.CompletedTask;
+ }
+}
+
+[TestTag("ITerminal")]
+[TestTag("Stream")]
+public class TerminalStreamAccessTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_return_stream_from_open_standard_input_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ Stream stream = terminal.OpenStandardInput();
+
+ // Assert
+ stream.ShouldNotBeNull();
+ stream.ShouldBeSameAs(terminal.StandardInputStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_stream_from_open_standard_output_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ Stream stream = terminal.OpenStandardOutput();
+
+ // Assert
+ stream.ShouldNotBeNull();
+ stream.ShouldBeSameAs(terminal.StandardOutputStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_stream_from_open_standard_error_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ Stream stream = terminal.OpenStandardError();
+
+ // Assert
+ stream.ShouldNotBeNull();
+ stream.ShouldBeSameAs(terminal.StandardErrorStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_text_reader_from_in_property_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ TextReader reader = terminal.In;
+
+ // Assert
+ reader.ShouldNotBeNull();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_text_writer_from_out_property_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ TextWriter writer = terminal.Out;
+
+ // Assert
+ writer.ShouldNotBeNull();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_return_text_writer_from_error_property_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ TextWriter writer = terminal.Error;
+
+ // Assert
+ writer.ShouldNotBeNull();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_in_reader_via_set_in_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ StringReader newReader = new("test input");
+
+ // Act
+ terminal.SetIn(newReader);
+
+ // Assert
+ terminal.In.ShouldBeSameAs(newReader);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_out_writer_via_set_out_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ StringWriter newWriter = new();
+
+ // Act
+ terminal.SetOut(newWriter);
+
+ // Assert
+ terminal.Out.ShouldBeSameAs(newWriter);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_error_writer_via_set_error_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ StringWriter newWriter = new();
+
+ // Act
+ terminal.SetError(newWriter);
+
+ // Assert
+ terminal.Error.ShouldBeSameAs(newWriter);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_allow_custom_standard_input_stream_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ MemoryStream customStream = new();
+
+ // Act
+ terminal.StandardInputStream = customStream;
+ Stream stream = terminal.OpenStandardInput();
+
+ // Assert
+ stream.ShouldBeSameAs(customStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_allow_custom_standard_output_stream_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ MemoryStream customStream = new();
+
+ // Act
+ terminal.StandardOutputStream = customStream;
+ Stream stream = terminal.OpenStandardOutput();
+
+ // Assert
+ stream.ShouldBeSameAs(customStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_allow_custom_standard_error_stream_in_test_terminal()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ MemoryStream customStream = new();
+
+ // Act
+ terminal.StandardErrorStream = customStream;
+ Stream stream = terminal.OpenStandardError();
+
+ // Assert
+ stream.ShouldBeSameAs(customStream);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_access_stream_apis_via_iconsole_interface()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+#pragma warning disable CA1859 // Intentionally testing IConsole interface access
+ IConsole console = terminal;
+#pragma warning restore CA1859
+
+ // Act
+ Stream inputStream = console.OpenStandardInput();
+ Stream outputStream = console.OpenStandardOutput();
+ Stream errorStream = console.OpenStandardError();
+ TextReader reader = console.In;
+ TextWriter outWriter = console.Out;
+ TextWriter errorWriter = console.Error;
+
+ // Assert
+ inputStream.ShouldNotBeNull();
+ outputStream.ShouldNotBeNull();
+ errorStream.ShouldNotBeNull();
+ reader.ShouldNotBeNull();
+ outWriter.ShouldNotBeNull();
+ errorWriter.ShouldNotBeNull();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_readers_writers_via_iconsole_interface()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+#pragma warning disable CA1859 // Intentionally testing IConsole interface access
+ IConsole console = terminal;
+#pragma warning restore CA1859
+ StringReader newReader = new("test");
+ StringWriter newOutWriter = new();
+ StringWriter newErrorWriter = new();
+
+ // Act
+ console.SetIn(newReader);
+ console.SetOut(newOutWriter);
+ console.SetError(newErrorWriter);
+
+ // Assert
+ console.In.ShouldBeSameAs(newReader);
+ console.Out.ShouldBeSameAs(newOutWriter);
+ console.Error.ShouldBeSameAs(newErrorWriter);
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.StreamAccess
diff --git a/tests/terminal-color-state-01-basic.cs b/tests/terminal-color-state-01-basic.cs
new file mode 100644
index 0000000..95509e2
--- /dev/null
+++ b/tests/terminal-color-state-01-basic.cs
@@ -0,0 +1,117 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test color state properties on ITerminal
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.TerminalColorState
+{
+
+[TestTag("Terminal")]
+public class TerminalColorStateTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_get_and_set_foreground_color()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.ForegroundColor = ConsoleColor.Red;
+
+ // Assert
+ terminal.ForegroundColor.ShouldBe(ConsoleColor.Red);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_and_set_background_color()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.BackgroundColor = ConsoleColor.Blue;
+
+ // Assert
+ terminal.BackgroundColor.ShouldBe(ConsoleColor.Blue);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_foreground_color_to_gray()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.ForegroundColor.ShouldBe(ConsoleColor.Gray);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_background_color_to_black()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.BackgroundColor.ShouldBe(ConsoleColor.Black);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_reset_color_reset_foreground_to_gray()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.ForegroundColor = ConsoleColor.Green;
+
+ // Act
+ terminal.ResetColor();
+
+ // Assert
+ terminal.ForegroundColor.ShouldBe(ConsoleColor.Gray);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_reset_color_reset_background_to_black()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.BackgroundColor = ConsoleColor.Yellow;
+
+ // Act
+ terminal.ResetColor();
+
+ // Assert
+ terminal.BackgroundColor.ShouldBe(ConsoleColor.Black);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_reset_color_reset_both_colors()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.ForegroundColor = ConsoleColor.Cyan;
+ terminal.BackgroundColor = ConsoleColor.Magenta;
+
+ // Act
+ terminal.ResetColor();
+
+ // Assert
+ terminal.ForegroundColor.ShouldBe(ConsoleColor.Gray);
+ terminal.BackgroundColor.ShouldBe(ConsoleColor.Black);
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.TerminalColorState
diff --git a/tests/terminal-control-utilities-01-basic.cs b/tests/terminal-control-utilities-01-basic.cs
new file mode 100644
index 0000000..013736e
--- /dev/null
+++ b/tests/terminal-control-utilities-01-basic.cs
@@ -0,0 +1,208 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test control/utility APIs on ITerminal (Beep, TreatControlCAsInput, Title, KeyAvailable)
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.TerminalControlUtilities
+{
+
+[TestTag("Terminal")]
+public class TerminalControlUtilityTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_beep_increment_count()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.Beep();
+
+ // Assert
+ terminal.BeepCount.ShouldBe(1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_beep_multiple_times_increment_count()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.Beep();
+ terminal.Beep();
+ terminal.Beep();
+
+ // Assert
+ terminal.BeepCount.ShouldBe(3);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_beep_with_parameters_capture_frequency_and_duration()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.Beep(800, 200);
+
+ // Assert
+ terminal.LastBeepFrequency.ShouldBe(800);
+ terminal.LastBeepDuration.ShouldBe(200);
+ terminal.BeepCount.ShouldBe(1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_beep_with_parameters_overwrite_previous()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.Beep(500, 100);
+ terminal.Beep(1000, 300);
+
+ // Assert
+ terminal.LastBeepFrequency.ShouldBe(1000);
+ terminal.LastBeepDuration.ShouldBe(300);
+ terminal.BeepCount.ShouldBe(2);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_treat_control_c_as_input_default_false()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.TreatControlCAsInput.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_treat_control_c_as_input_get_and_set()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.TreatControlCAsInput = true;
+
+ // Assert
+ terminal.TreatControlCAsInput.ShouldBeTrue();
+
+ // Act
+ terminal.TreatControlCAsInput = false;
+
+ // Assert
+ terminal.TreatControlCAsInput.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_title_default_empty_string()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.Title.ShouldBe(string.Empty);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_title_get_and_set()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.Title = "My App";
+
+ // Assert
+ terminal.Title.ShouldBe("My App");
+
+ // Act
+ terminal.Title = "Different Title";
+
+ // Assert
+ terminal.Title.ShouldBe("Different Title");
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_key_available_false_when_no_keys_queued()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.KeyAvailable.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_key_available_true_when_keys_queued()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKey(ConsoleKey.A);
+
+ // Assert
+ terminal.KeyAvailable.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_key_available_false_after_keys_dequeued()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKey(ConsoleKey.A);
+
+ // Act
+ terminal.ReadKey();
+
+ // Assert
+ terminal.KeyAvailable.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_key_available_true_with_multiple_keys()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.QueueKeys("abc");
+
+ // Assert
+ terminal.KeyAvailable.ShouldBeTrue();
+
+ // Act - read one key
+ terminal.ReadKey();
+
+ // Assert - still true
+ terminal.KeyAvailable.ShouldBeTrue();
+
+ // Act - read remaining keys
+ terminal.ReadKey();
+ terminal.ReadKey();
+
+ // Assert - now false
+ terminal.KeyAvailable.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.TerminalControlUtilities
diff --git a/tests/terminal-cursor-properties.cs b/tests/terminal-cursor-properties.cs
new file mode 100644
index 0000000..2033dc5
--- /dev/null
+++ b/tests/terminal-cursor-properties.cs
@@ -0,0 +1,203 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test cursor properties on ITerminal
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.TerminalCursorProperties
+{
+
+[TestTag("Terminal")]
+public class TerminalCursorPropertyTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ public static async Task Should_get_and_set_cursor_left()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.CursorLeft = 5;
+
+ // Assert
+ terminal.CursorLeft.ShouldBe(5);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_and_set_cursor_top()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.CursorTop = 10;
+
+ // Assert
+ terminal.CursorTop.ShouldBe(10);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_and_set_cursor_visible()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.CursorVisible = false;
+
+ // Assert
+ terminal.CursorVisible.ShouldBeFalse();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_cursor_visible_to_true()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.CursorVisible.ShouldBeTrue();
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_and_set_cursor_size()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.CursorSize = 50;
+
+ // Assert
+ terminal.CursorSize.ShouldBe(50);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_cursor_size_to_100()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.CursorSize.ShouldBe(100);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_reject_cursor_size_below_1()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act & Assert
+ Should.Throw(() => terminal.CursorSize = 0);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_reject_cursor_size_above_100()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act & Assert
+ Should.Throw(() => terminal.CursorSize = 101);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_accept_cursor_size_at_minimum_1()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.CursorSize = 1;
+
+ // Assert
+ terminal.CursorSize.ShouldBe(1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_accept_cursor_size_at_maximum_100()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.CursorSize = 100;
+
+ // Assert
+ terminal.CursorSize.ShouldBe(100);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_cursor_position_still_works()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.SetCursorPosition(15, 25);
+
+ // Assert
+ terminal.CursorLeft.ShouldBe(15);
+ terminal.CursorTop.ShouldBe(25);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_cursor_position_still_works()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.SetCursorPosition(20, 30);
+
+ // Act
+ (int left, int top) = terminal.GetCursorPosition();
+
+ // Assert
+ left.ShouldBe(20);
+ top.ShouldBe(30);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_cursor_properties_sync_with_position_methods()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act - Set via property, get via method
+ terminal.CursorLeft = 12;
+ terminal.CursorTop = 8;
+ (int left, int top) = terminal.GetCursorPosition();
+
+ // Assert
+ left.ShouldBe(12);
+ top.ShouldBe(8);
+
+ // Act - Set via method, get via property
+ terminal.SetCursorPosition(5, 3);
+
+ // Assert
+ terminal.CursorLeft.ShouldBe(5);
+ terminal.CursorTop.ShouldBe(3);
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.TerminalCursorProperties
diff --git a/tests/terminal-static-08-new-apis.cs b/tests/terminal-static-08-new-apis.cs
new file mode 100644
index 0000000..b61d148
--- /dev/null
+++ b/tests/terminal-static-08-new-apis.cs
@@ -0,0 +1,1339 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test Terminal static facade class - New APIs from tasks 020-001 through 020-007
+using System.Text;
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.TerminalStaticNewApis
+{
+
+[TestTag("Terminal")]
+public class TerminalStaticNewApisTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ // ========== Stream Access Tests (IConsole - task 020-001) ==========
+
+ public static async Task Should_open_standard_input()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Stream stream = Terminal.OpenStandardInput();
+
+ // Assert
+ stream.ShouldBe(testTerminal.StandardInputStream);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_open_standard_output()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Stream stream = Terminal.OpenStandardOutput();
+
+ // Assert
+ stream.ShouldBe(testTerminal.StandardOutputStream);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_open_standard_error()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Stream stream = Terminal.OpenStandardError();
+
+ // Assert
+ stream.ShouldBe(testTerminal.StandardErrorStream);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_in_reader()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ TextReader reader = Terminal.In;
+
+ // Assert
+ reader.ShouldBe(testTerminal.In);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_out_writer()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ TextWriter writer = Terminal.Out;
+
+ // Assert
+ writer.ShouldBe(testTerminal.Out);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_error_writer()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ TextWriter writer = Terminal.Error;
+
+ // Assert
+ writer.ShouldBe(testTerminal.Error);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_in_reader()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+ StringReader newReader = new("test");
+
+ try
+ {
+ // Act
+ Terminal.SetIn(newReader);
+
+ // Assert
+ testTerminal.In.ShouldBe(newReader);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_out_writer()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+ StringWriter newWriter = new();
+
+ try
+ {
+ // Act
+ Terminal.SetOut(newWriter);
+
+ // Assert
+ testTerminal.Out.ShouldBe(newWriter);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_error_writer()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+ StringWriter newWriter = new();
+
+ try
+ {
+ // Act
+ Terminal.SetError(newWriter);
+
+ // Assert
+ testTerminal.Error.ShouldBe(newWriter);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ // ========== Encoding Tests (IConsole - task 020-002) ==========
+
+ public static async Task Should_get_input_encoding()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { InputEncoding = Encoding.ASCII };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Encoding encoding = Terminal.InputEncoding;
+
+ // Assert
+ encoding.ShouldBe(Encoding.ASCII);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_input_encoding()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.InputEncoding = Encoding.Unicode;
+
+ // Assert
+ testTerminal.InputEncoding.ShouldBe(Encoding.Unicode);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_output_encoding()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { OutputEncoding = Encoding.UTF32 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Encoding encoding = Terminal.OutputEncoding;
+
+ // Assert
+ encoding.ShouldBe(Encoding.UTF32);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_output_encoding()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.OutputEncoding = Encoding.Latin1;
+
+ // Assert
+ testTerminal.OutputEncoding.ShouldBe(Encoding.Latin1);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ // ========== Redirection Tests (IConsole - task 020-002) ==========
+
+ public static async Task Should_get_is_input_redirected()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { IsInputRedirected = true };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ bool redirected = Terminal.IsInputRedirected;
+
+ // Assert
+ redirected.ShouldBeTrue();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_is_output_redirected()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { IsOutputRedirected = true };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ bool redirected = Terminal.IsOutputRedirected;
+
+ // Assert
+ redirected.ShouldBeTrue();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_is_error_redirected()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { IsErrorRedirected = true };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ bool redirected = Terminal.IsErrorRedirected;
+
+ // Assert
+ redirected.ShouldBeTrue();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ // ========== Rich Input Tests (IConsole - task 020-003) ==========
+
+ public static async Task Should_read_character()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+ testTerminal.QueueKeys("abc");
+
+ try
+ {
+ // Act
+ int ch = Terminal.Read();
+
+ // Assert
+ ch.ShouldBe('a');
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_read_key_without_intercept()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+ testTerminal.QueueKey(ConsoleKey.Enter);
+
+ try
+ {
+ // Act
+ ConsoleKeyInfo keyInfo = Terminal.ReadKey();
+
+ // Assert
+ keyInfo.Key.ShouldBe(ConsoleKey.Enter);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ // ========== Cursor Properties Tests (ITerminal - task 020-004) ==========
+
+ public static async Task Should_get_cursor_left()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { CursorLeft = 10 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int left = Terminal.CursorLeft;
+
+ // Assert
+ left.ShouldBe(10);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_cursor_left()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.CursorLeft = 20;
+
+ // Assert
+ testTerminal.CursorLeft.ShouldBe(20);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_cursor_top()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { CursorTop = 5 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int top = Terminal.CursorTop;
+
+ // Assert
+ top.ShouldBe(5);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_cursor_top()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.CursorTop = 15;
+
+ // Assert
+ testTerminal.CursorTop.ShouldBe(15);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_cursor_visible()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { CursorVisible = false };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ bool visible = Terminal.CursorVisible;
+
+ // Assert
+ visible.ShouldBeFalse();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_cursor_visible()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.CursorVisible = false;
+
+ // Assert
+ testTerminal.CursorVisible.ShouldBeFalse();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_cursor_size()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { CursorSize = 50 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int size = Terminal.CursorSize;
+
+ // Assert
+ size.ShouldBe(50);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_cursor_size()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.CursorSize = 75;
+
+ // Assert
+ testTerminal.CursorSize.ShouldBe(75);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ // ========== Window/Buffer Geometry Tests (ITerminal - task 020-005) ==========
+
+ public static async Task Should_get_window_height()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { WindowHeight = 40 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int height = Terminal.WindowHeight;
+
+ // Assert
+ height.ShouldBe(40);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_window_height()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.WindowHeight = 50;
+
+ // Assert
+ testTerminal.WindowHeight.ShouldBe(50);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_window_width()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.WindowWidth = 150;
+
+ // Assert
+ testTerminal.WindowWidth.ShouldBe(150);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_window_left()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { WindowLeft = 5 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int left = Terminal.WindowLeft;
+
+ // Assert
+ left.ShouldBe(5);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_window_left()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.WindowLeft = 10;
+
+ // Assert
+ testTerminal.WindowLeft.ShouldBe(10);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_window_top()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { WindowTop = 3 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int top = Terminal.WindowTop;
+
+ // Assert
+ top.ShouldBe(3);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_window_top()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.WindowTop = 8;
+
+ // Assert
+ testTerminal.WindowTop.ShouldBe(8);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_buffer_width()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { BufferWidth = 200 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int width = Terminal.BufferWidth;
+
+ // Assert
+ width.ShouldBe(200);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_buffer_width()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.BufferWidth = 250;
+
+ // Assert
+ testTerminal.BufferWidth.ShouldBe(250);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_buffer_height()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { BufferHeight = 500 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int height = Terminal.BufferHeight;
+
+ // Assert
+ height.ShouldBe(500);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_buffer_height()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.BufferHeight = 600;
+
+ // Assert
+ testTerminal.BufferHeight.ShouldBe(600);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_largest_window_width()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { LargestWindowWidth = 200 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int width = Terminal.LargestWindowWidth;
+
+ // Assert
+ width.ShouldBe(200);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_largest_window_height()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { LargestWindowHeight = 60 };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ int height = Terminal.LargestWindowHeight;
+
+ // Assert
+ height.ShouldBe(60);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_window_size()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.SetWindowSize(100, 30);
+
+ // Assert
+ testTerminal.WindowWidth.ShouldBe(100);
+ testTerminal.WindowHeight.ShouldBe(30);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_window_position()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.SetWindowPosition(5, 10);
+
+ // Assert
+ testTerminal.WindowLeft.ShouldBe(5);
+ testTerminal.WindowTop.ShouldBe(10);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_buffer_size()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.SetBufferSize(150, 400);
+
+ // Assert
+ testTerminal.BufferWidth.ShouldBe(150);
+ testTerminal.BufferHeight.ShouldBe(400);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_move_buffer_area()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.MoveBufferArea(0, 0, 10, 5, 20, 10, ' ', ConsoleColor.White, ConsoleColor.Black);
+
+ // Assert
+ testTerminal.MoveBufferAreaCallCount.ShouldBe(1);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ // ========== Color State Tests (ITerminal - task 020-006) ==========
+
+ public static async Task Should_get_foreground_color()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { ForegroundColor = ConsoleColor.Cyan };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ ConsoleColor color = Terminal.ForegroundColor;
+
+ // Assert
+ color.ShouldBe(ConsoleColor.Cyan);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_foreground_color()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.ForegroundColor = ConsoleColor.Magenta;
+
+ // Assert
+ testTerminal.ForegroundColor.ShouldBe(ConsoleColor.Magenta);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_background_color()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { BackgroundColor = ConsoleColor.DarkBlue };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ ConsoleColor color = Terminal.BackgroundColor;
+
+ // Assert
+ color.ShouldBe(ConsoleColor.DarkBlue);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_background_color()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.BackgroundColor = ConsoleColor.DarkGreen;
+
+ // Assert
+ testTerminal.BackgroundColor.ShouldBe(ConsoleColor.DarkGreen);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_reset_color()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+ testTerminal.ForegroundColor = ConsoleColor.Red;
+ testTerminal.BackgroundColor = ConsoleColor.Blue;
+
+ try
+ {
+ // Act
+ Terminal.ResetColor();
+
+ // Assert
+ testTerminal.ForegroundColor.ShouldBe(ConsoleColor.Gray);
+ testTerminal.BackgroundColor.ShouldBe(ConsoleColor.Black);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ // ========== Control/Utility Tests (ITerminal - task 020-007) ==========
+
+ public static async Task Should_beep()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.Beep();
+
+ // Assert
+ testTerminal.BeepCount.ShouldBe(1);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_beep_with_frequency_and_duration()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.Beep(800, 200);
+
+ // Assert
+ testTerminal.BeepCount.ShouldBe(1);
+ testTerminal.LastBeepFrequency.ShouldBe(800);
+ testTerminal.LastBeepDuration.ShouldBe(200);
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_treat_control_c_as_input()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { TreatControlCAsInput = true };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ bool treatAsInput = Terminal.TreatControlCAsInput;
+
+ // Assert
+ treatAsInput.ShouldBeTrue();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_treat_control_c_as_input()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.TreatControlCAsInput = true;
+
+ // Assert
+ testTerminal.TreatControlCAsInput.ShouldBeTrue();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_title()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new() { Title = "Test Title" };
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ string title = Terminal.Title;
+
+ // Assert
+ title.ShouldBe("Test Title");
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_set_title()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ Terminal.Title = "New Title";
+
+ // Assert
+ testTerminal.Title.ShouldBe("New Title");
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_key_available()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+ testTerminal.QueueKey(ConsoleKey.A);
+
+ try
+ {
+ // Act
+ bool available = Terminal.KeyAvailable;
+
+ // Assert
+ available.ShouldBeTrue();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_get_key_available_false_when_empty()
+ {
+ // Arrange
+ ITerminal original = Terminal.Instance;
+ using TestTerminal testTerminal = new();
+ Terminal.Instance = testTerminal;
+
+ try
+ {
+ // Act
+ bool available = Terminal.KeyAvailable;
+
+ // Assert
+ available.ShouldBeFalse();
+ }
+ finally
+ {
+ Terminal.Instance = original;
+ }
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.TerminalStaticNewApis
diff --git a/tests/terminal-window-buffer-geometry.cs b/tests/terminal-window-buffer-geometry.cs
new file mode 100644
index 0000000..2fa0839
--- /dev/null
+++ b/tests/terminal-window-buffer-geometry.cs
@@ -0,0 +1,332 @@
+#!/usr/bin/dotnet --
+#:project $(SourceDirectory)timewarp-terminal/timewarp-terminal.csproj
+
+// Test window and buffer geometry properties on ITerminal
+
+#if !JARIBU_MULTI
+return await RunAllTests();
+#endif
+
+namespace TimeWarp.Terminal.Tests.Core.TerminalWindowBufferGeometry
+{
+
+[TestTag("Terminal")]
+public class TerminalWindowBufferGeometryTests
+{
+ [ModuleInitializer]
+ internal static void Register() => RegisterTests();
+
+ // ========== WindowHeight Tests ==========
+
+ public static async Task Should_get_and_set_window_height()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.WindowHeight = 40;
+
+ // Assert
+ terminal.WindowHeight.ShouldBe(40);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_window_height_to_24()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.WindowHeight.ShouldBe(24);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== WindowLeft Tests ==========
+
+ public static async Task Should_get_and_set_window_left()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.WindowLeft = 10;
+
+ // Assert
+ terminal.WindowLeft.ShouldBe(10);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_window_left_to_0()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.WindowLeft.ShouldBe(0);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== WindowTop Tests ==========
+
+ public static async Task Should_get_and_set_window_top()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.WindowTop = 15;
+
+ // Assert
+ terminal.WindowTop.ShouldBe(15);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_window_top_to_0()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.WindowTop.ShouldBe(0);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== BufferWidth Tests ==========
+
+ public static async Task Should_get_and_set_buffer_width()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.BufferWidth = 120;
+
+ // Assert
+ terminal.BufferWidth.ShouldBe(120);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_buffer_width_to_80()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.BufferWidth.ShouldBe(80);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== BufferHeight Tests ==========
+
+ public static async Task Should_get_and_set_buffer_height()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.BufferHeight = 500;
+
+ // Assert
+ terminal.BufferHeight.ShouldBe(500);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_buffer_height_to_300()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.BufferHeight.ShouldBe(300);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== SetWindowSize Tests ==========
+
+ public static async Task Should_set_window_size()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.SetWindowSize(100, 30);
+
+ // Assert
+ terminal.WindowWidth.ShouldBe(100);
+ terminal.WindowHeight.ShouldBe(30);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== SetWindowPosition Tests ==========
+
+ public static async Task Should_set_window_position()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.SetWindowPosition(5, 10);
+
+ // Assert
+ terminal.WindowLeft.ShouldBe(5);
+ terminal.WindowTop.ShouldBe(10);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== SetBufferSize Tests ==========
+
+ public static async Task Should_set_buffer_size()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.SetBufferSize(120, 400);
+
+ // Assert
+ terminal.BufferWidth.ShouldBe(120);
+ terminal.BufferHeight.ShouldBe(400);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== MoveBufferArea Tests ==========
+
+ public static async Task Should_call_move_buffer_area()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.MoveBufferArea
+ (
+ sourceLeft: 0,
+ sourceTop: 0,
+ sourceWidth: 10,
+ sourceHeight: 5,
+ targetLeft: 20,
+ targetTop: 10,
+ sourceChar: ' ',
+ sourceForeColor: ConsoleColor.Gray,
+ sourceBackColor: ConsoleColor.Black
+ );
+
+ // Assert
+ terminal.MoveBufferAreaCallCount.ShouldBe(1);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_track_multiple_move_buffer_area_calls()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.MoveBufferArea(0, 0, 10, 5, 20, 10, ' ', ConsoleColor.Gray, ConsoleColor.Black);
+ terminal.MoveBufferArea(5, 5, 15, 10, 30, 20, ' ', ConsoleColor.Gray, ConsoleColor.Black);
+ terminal.MoveBufferArea(10, 10, 20, 15, 40, 30, ' ', ConsoleColor.Gray, ConsoleColor.Black);
+
+ // Assert
+ terminal.MoveBufferAreaCallCount.ShouldBe(3);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== LargestWindowWidth Tests ==========
+
+ public static async Task Should_get_largest_window_width()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.LargestWindowWidth = 200;
+
+ // Act
+ int width = terminal.LargestWindowWidth;
+
+ // Assert
+ width.ShouldBe(200);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_largest_window_width_to_120()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.LargestWindowWidth.ShouldBe(120);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== LargestWindowHeight Tests ==========
+
+ public static async Task Should_get_largest_window_height()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+ terminal.LargestWindowHeight = 60;
+
+ // Act
+ int height = terminal.LargestWindowHeight;
+
+ // Assert
+ height.ShouldBe(60);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_largest_window_height_to_40()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.LargestWindowHeight.ShouldBe(40);
+
+ await Task.CompletedTask;
+ }
+
+ // ========== WindowWidth Tests (now has setter) ==========
+
+ public static async Task Should_get_and_set_window_width()
+ {
+ // Arrange
+ using TestTerminal terminal = new();
+
+ // Act
+ terminal.WindowWidth = 100;
+
+ // Assert
+ terminal.WindowWidth.ShouldBe(100);
+
+ await Task.CompletedTask;
+ }
+
+ public static async Task Should_default_window_width_to_80()
+ {
+ // Arrange & Act
+ using TestTerminal terminal = new();
+
+ // Assert
+ terminal.WindowWidth.ShouldBe(80);
+
+ await Task.CompletedTask;
+ }
+}
+
+} // namespace TimeWarp.Terminal.Tests.Core.TerminalWindowBufferGeometry