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