Sync Copilot context

This commit is contained in:
vczh
2026-07-24 03:13:15 -07:00
parent bb1fba960d
commit 8eec8747a2
6 changed files with 386 additions and 4 deletions
+2 -2
View File
@@ -40,9 +40,9 @@ Files from Import:
Online documentation: https://gaclib.net/doc/current/vlppos/home.html
VlppOS provides cross-platform OS abstraction for file system operations, streams, locale support, and multi-threading.
VlppOS provides cross-platform OS abstraction for file system operations, streams, locale support, multi-threading, inter-process communication, and terminal user interfaces.
Use this when you need to interact with the operating system in a portable way.
It offers locale-aware string manipulation, file system access, various stream types with encoding/decoding capabilities, and comprehensive multi-threading support with synchronization primitives.
It offers locale-aware string manipulation, file system access, various stream types with encoding/decoding capabilities, comprehensive multi-threading support with synchronization primitives, and an owner-thread cell-buffer TUI.
Its transport-agnostic `INetworkProtocol*` and `IChannel*` abstractions can be used in production, but every bundled concrete network-protocol implementation is for testing, validation and demonstration only. Production applications should implement their own protocol transport when inter-process communication is needed.
Detailed project guidance: [Index_VlppOS.md](./Index_VlppOS.md)
+16
View File
@@ -148,6 +148,22 @@ Testing-only layered loopback TCP and HTTP/1.1 APIs for asynchronous binary stre
[API Explanation](./KB_VlppOS_InterProcessAsyncSocketBasedMiniHttpApi.md)
#### Terminal User Interface
Cross-platform owner-thread terminal takeover with input, resize and timer callbacks, a validated cell buffer, Unicode-aware character placement, box-drawing primitives and deterministic backend injection for tests.
- Use `TUI::TryGetConsoleSize` to query the visible terminal before startup.
- Use `TUI::InstallListener`, `TUI::Start`, `TUI::RunOneCycle`, `TUI::Stop`, `TUI::IsInUse`, `TUI::IsStopRequested` and `TUI::UninstallListener` for lifecycle and owner-thread event processing.
- Use `TuiStartOptions`, `TuiColorMode` and `TUI::GetColorMode` to request and inspect terminal color emission.
- Use `ITuiCallback` with `TuiMouseInfo`, `TuiKeyInfo` and `TuiCharInfo` for startup, shutdown, resize, input and timer callbacks.
- Use `TUI::StartTimer` and `TUI::StopTimer` for deadline-driven callbacks on the TUI owner thread.
- Use `TUI::GetBuffer`, `TUI::GetBufferWidth`, `TUI::GetBufferHeight` and `TUI::RenderBuffer` for retained cell-buffer rendering.
- Use `TuiPixel`, `TuiPixelGlyph`, `TuiColor` and `TUI::MeasureChar` to represent and validate terminal cells and scalar widths.
- Use `TUI::PrintChar`, `TUI::DrawLineV`, `TUI::DrawLineH`, `TUI::DrawRect` and `TUI::Clear` for clipped drawing on the active buffer or a caller-owned buffer.
- Use `vl::console::unittest::ScopedTuiBackend` with `vl::console::unittest::ITuiBackend` for deterministic TUI tests.
[API Explanation](./KB_VlppOS_TerminalUserInterface.md)
### Design Explanation
#### Implementing an Injectable Feature
@@ -0,0 +1,340 @@
# Terminal User Interface
`vl::console::TUI`, declared in [TUI.h](../../Source/TUI/TUI.h), provides a cross-platform terminal takeover, owner-thread event loop, cell buffer, and drawing API. It is intended for applications that redraw the visible terminal as a complete user interface instead of using sequential `vl::console::Console` input and output.
All names on this page are in the `vl::console` namespace unless another namespace is shown.
## Starting and Stopping TUI
Install one or more `ITuiCallback` listeners before calling `TUI::Start`. `TUI::Start` takes control of the terminal, allocates the initial cell buffer, dispatches callbacks, runs the event loop, restores the terminal, and then returns.
```cpp
class Callback : public ITuiCallback
{
public:
void Starting() override
{
TUI::StartTimer(500);
}
void BufferSizeChanged() override
{
Redraw();
}
void Char(const TuiCharInfo& info) override
{
if (info.code == L'\x1B')
{
TUI::Stop();
}
}
void Timer() override
{
Redraw();
}
void Stopping() override
{
TUI::StopTimer();
}
void Redraw()
{
TUI::Clear({ 0, 0, 0 }, 0, 0, TUI::GetBufferWidth() - 1, TUI::GetBufferHeight() - 1);
TUI::RenderBuffer();
}
};
Callback callback;
CHECK_ERROR(TUI::InstallListener(&callback), L"Failed to install the TUI listener.");
try
{
TUI::Start({ .colorMode = TuiColorMode::Auto });
}
catch (...)
{
TUI::UninstallListener(&callback);
throw;
}
CHECK_ERROR(TUI::UninstallListener(&callback), L"Failed to uninstall the TUI listener.");
```
`ITuiCallback` is non-owning. The listener object must remain alive until it is uninstalled and no active callback can use it.
### Lifecycle Order
An inactive-to-active `TUI::Start` follows this observable order:
1. Validate the requested color mode and require `Console::IsEnabled()` to be true.
2. Take over the terminal, select a color mode, query the initial visible size, disable `Console`, and allocate the buffer.
3. Set `TUI::IsInUse()` to true.
4. Invoke `ITuiCallback::Starting`.
5. Invoke the initial `ITuiCallback::BufferSizeChanged` unless a listener requested `TUI::Stop` from `Starting`.
6. Repeatedly run event-loop cycles until a stop is requested.
7. Invoke `ITuiCallback::Stopping` for a normal stop.
8. Restore the terminal and re-enable `Console` before returning.
`TUI::Start` blocks on the calling thread for the whole active lifetime. Input, resize, timer, startup, and shutdown callbacks all run synchronously on that owner thread; TUI does not create a UI worker thread.
While TUI is active, the ordinary `Console` read, write, color, and title operations are disabled rather than redirected into the TUI buffer. Do not manually call `Console::Enable()` while TUI owns the terminal.
### Stop Is a Request
`TUI::Stop` sets a stop-request flag. It does not restore the terminal or invoke `Stopping` inside the current callback.
- No later non-stopping listener for the current event, or queued input event, is dispatched after the request is observed.
- The current callback and any nested `TUI::RunOneCycle` calls unwind first.
- `TUI::IsStopRequested()` lets an adapter suppress additional higher-level callbacks after requesting a stop.
- `TUI::Stop()` and `TUI::IsStopRequested()` are harmless while TUI is inactive; they respectively do nothing and return false.
- A stop request during `Stopping` is ignored because shutdown is already in progress.
Calling `TUI::Start` again from the active owner thread is a no-op. Calling it from another thread while active violates the owner-thread contract.
### Callback Exceptions
If `Starting`, an input callback, a resize callback, or a timer callback throws, TUI stops dispatching user callbacks, skips `Stopping`, restores acquired terminal and `Console` state, and rethrows the first callback exception. Catching an exception from a nested `RunOneCycle` does not clear it or allow the outer `Start` loop to continue.
If `Stopping` throws, TUI still restores terminal and `Console` state before rethrowing.
## Color Mode Selection
`TuiStartOptions::colorMode` defaults to `TuiColorMode::Auto` and requests one of these modes:
- `TuiColorMode::Auto` lets the backend choose.
- `TuiColorMode::TrueColor` emits full RGB colors when selected.
- `TuiColorMode::Color256` quantizes colors to a 256-color palette.
- `TuiColorMode::Color16` quantizes colors to a 16-color palette.
The backend may select a lower mode than requested. Call `TUI::GetColorMode()` while active to get the selected emission and quantization mode.
The selected mode describes how TUI produces output; it is not a guarantee that the terminal, font, or display renders every requested color exactly.
## Querying Terminal Size
`TUI::TryGetConsoleSize(vint& width, vint& height)` queries the visible terminal size and returns whether the query succeeded. It can be called before `TUI::Start`.
While active:
- `TUI::GetBufferWidth()` and `TUI::GetBufferHeight()` return the allocated cell-buffer dimensions.
- `TUI::GetBuffer()` returns the row-major buffer.
- Cell `(x, y)` is `TUI::GetBuffer()[y * TUI::GetBufferWidth() + x]`.
When the visible size changes, TUI:
1. Allocates a new buffer.
2. Preserves the overlapping rectangle.
3. Initializes newly exposed cells as empty character cells with white foreground and black background.
4. Removes an invalid width-two character cut by the new right boundary.
5. Replaces the public buffer pointer.
6. Invokes `ITuiCallback::BufferSizeChanged`.
Do not retain a pointer returned by `TUI::GetBuffer()` across a resize, after requesting `TUI::Stop`, or into a later `TUI::Start`.
## Event Callbacks
All `ITuiCallback` methods have default no-op implementations, so a listener only needs to override the events it uses.
### Listener Registration
`TUI::InstallListener` and `TUI::UninstallListener` return `bool`.
- Installing `nullptr` or an already installed pointer returns false.
- Uninstalling `nullptr` or an absent pointer returns false.
- Listener order is installation order.
- Installation and removal are allowed during callbacks on the owner thread.
- Removing a listener prevents it from being called later in the current event.
- Removing and reinstalling the same pointer creates a new registration; an old event snapshot cannot invoke that new registration.
- A listener installed during an event begins with the next event. A nested `TUI::RunOneCycle` is a distinct event and sees the current registrations.
### Mouse Events
`TuiMouseInfo` reports terminal-cell coordinates, wheel information, best-effort modifier state, and current left, middle, and right button state. `TuiMouseButton` identifies the button for down, up, and double-click callbacks.
Available callbacks are:
- `MouseMove`
- `MouseDown`
- `MouseUp`
- `MouseDoubleClick`
- `MouseVerticalWheel`
- `MouseHorizontalWheel`
### Key Events
`TuiKeyInfo` carries modifier and repeat information, but `TuiKeyInfo::code` is reserved for future key-code translation. Do not depend on a portable nonzero key code. Use `ITuiCallback::Char` for text and character controls.
Modifier fields are best effort. A terminal protocol that cannot distinguish Shift or Caps Lock from the resulting character reports the unobservable fields as false.
### Character Events Use Native `wchar_t` Units
`TuiCharInfo::code` is exactly one platform-native `wchar_t` code unit, not necessarily one complete Unicode scalar.
- On Windows, `wchar_t` is UTF-16. A supplementary scalar arrives as separate high- and low-surrogate `Char` callbacks in native order. Other event types may occur between them, and a stop requested after the high surrogate suppresses the queued low surrogate.
- On Linux and macOS, `wchar_t` is UTF-32. A decoded scalar normally arrives in one `Char` callback.
- Each callback carries the modifier state associated with that native input unit.
A scalar-oriented listener owns any incremental conversion state. Use `vl::encoding::UtfConversion<wchar_t>::To32` when conversion is needed. On UTF-16, retain at most one high surrogate, combine only an adjacent low surrogate in the listener's character-unit stream, and define how unmatched units are discarded or replaced without swallowing the current valid unit or control character.
Do not pass an individual UTF-16 surrogate to scalar APIs such as `TUI::MeasureChar` or `TUI::PrintChar`.
## Timers and Reentrant Event Pumping
`TUI::StartTimer(vint milliseconds)` starts or replaces the one TUI timer. The period must be positive. `TUI::StopTimer()` disables it.
The timer:
- Uses deadlines instead of busy polling.
- Shares the owner-thread event loop with terminal input and resize events.
- Invokes `ITuiCallback::Timer` on the `TUI::Start` thread.
- Is global to the active TUI, not one timer per listener.
`TUI::RunOneCycle()` is the public owner-thread event-pump primitive used by `TUI::Start`. One call waits as necessary and dispatches one bounded unit of queued input, resize, or timer work. It returns false once a stop is requested.
`RunOneCycle` is intentionally reentrant. A same-thread modal loop may call it from a callback, and the nested call may consume already queued events. Do not cache iterators, event references, buffer pointers that could be invalidated by resize, or assumptions about listener registration across a nested cycle.
## Pixels and the Cell Buffer
`TuiPixel` represents one terminal cell. Its `glyph` selects the active union member:
- `TuiPixelGlyph::Char` stores an empty cell (`c == 0`) or one Unicode scalar.
- `TuiPixelGlyph::Mergeable` stores four independently styled box-drawing arms.
- `TuiPixelGlyph::Unmergeable` stores a rounded corner and its direction.
- `TuiPixelGlyph::WideCharContinuation` marks the second cell occupied by a width-two scalar.
Every pixel also contains logical RGB foreground and background colors.
Each arm in `TuiMergeablePixel` is independently `None`, `ThinLine`, `ThickLine`, or `DoubleLine`. `TuiUnmergeablePixel` currently represents a `RoundCorner` with a left-top, right-top, left-bottom, or right-bottom direction.
### Reading a Pixel as Text
`TuiPixel::GetChar32()` returns:
- The stored scalar for a character cell.
- The Unicode box-drawing scalar for a valid mergeable or rounded-corner cell.
- Zero for an empty cell, a continuation cell, or an unsupported raw glyph state.
`TuiPixel::GetWChar()` returns the value only when it fits in exactly one native `wchar_t`. On UTF-16 Windows, it returns zero for supplementary scalars because they require two code units.
### Character Width
`TUI::MeasureChar(char32_t code)` classifies one Unicode scalar for the current platform:
- `0` means invalid, zero-width, control, or otherwise not independently printable.
- `1` means one terminal cell.
- `2` means two terminal cells.
This API works with Unicode scalars, while `TuiCharInfo::code` works with native `wchar_t` units. Convert character input before measuring it when the native encoding can use multiple units.
The cell model intentionally does not perform grapheme shaping, combining sequences, variation sequences, emoji ZWJ sequences, bidirectional layout, or complex-script layout. Each printable scalar is measured and stored independently.
### Width-Two Invariant
A width-two scalar is represented by:
1. A leading `TuiPixelGlyph::Char` cell containing the scalar.
2. A following `TuiPixelGlyph::WideCharContinuation` cell with identical foreground and background colors.
The drawing helpers repair a previous width-two pair before overwriting either half. Code that writes directly through `TUI::GetBuffer()` must preserve the same invariant.
`TUI::RenderBuffer()` validates the complete active buffer before rendering. It rejects invalid scalars, standalone zero-width or control characters, unsupported line-arm states, invalid rounded corners, missing or orphaned continuation cells, and color mismatches within width-two pairs.
## Drawing Operations
TUI provides two forms of each drawing operation:
- Active-buffer overloads operate on `TUI::GetBuffer()` and require an active TUI on the owner thread.
- Buffer-explicit overloads begin with `TuiPixel* buffer, vint width, vint height` and can draw into a caller-owned temporary buffer without starting TUI.
The operations are:
- `TUI::PrintChar`
- `TUI::DrawLineV`
- `TUI::DrawLineH`
- `TUI::DrawRect`
- `TUI::Clear`
Buffer-explicit overloads require a non-null buffer and positive dimensions.
### Printing Characters
`TuiPrintOptions` supplies foreground and background colors.
`TUI::PrintChar`:
- Requires a valid Unicode scalar.
- Writes width-one and width-two scalars using the cell representation above.
- Does nothing for a zero-width or non-printable scalar.
- Clips an out-of-bounds leading coordinate.
- Does not write a width-two scalar unless both cells fit.
- Replaces the complete old width-two character when either destination cell intersects it.
### Lines
`TuiLineOptions` selects `ThinLine`, `ThickLine`, or `DoubleLine`, a foreground color, and an optional background color.
- `DrawLineV(options, x, y1, y2)` requires `y1 <= y2`.
- `DrawLineH(options, x1, x2, y)` requires `x1 <= x2`.
- Endpoints are inclusive.
- Every cell, including both endpoints and a one-cell line, contains both opposing arms of the selected line style.
- Coordinates outside the buffer are clipped.
- A fully clipped line is a no-op.
- An empty `backgroundColor` preserves each destination background.
- A present `backgroundColor`, including black, replaces it.
### Rectangles
`TuiRectOptions` adds `TuiRectCorner::Sharp` or `TuiRectCorner::Round`.
`DrawRect(options, x1, y1, x2, y2)` requires `x1 < x2` and `y1 < y2`. Coordinates are inclusive and clipped to the buffer without inventing new corners at a clipped edge.
Sharp rectangles support thin, thick, and double lines. Rounded rectangles require a thin line; rounded corners are unmergeable.
### Clearing
`TUI::Clear(backgroundColor, x1, y1, x2, y2)` requires ordered inclusive ranges. It clips to the buffer and replaces affected cells with empty character cells, the specified background, and white foreground.
### Line Merging
Sharp line and rectangle operations merge compatible box-drawing arms when they overlap:
- A later operation replaces any arm direction that it supplies.
- The later foreground color applies to the resulting cell.
- The optional later background only replaces the destination background when it has a value.
- If the combined arms have an exact Unicode box-drawing character, the combined geometry is kept.
- If a combination containing double lines has no exact Unicode representation, the whole cell falls back to the later operation's unmerged shape.
Do not assume that every arbitrary thin, thick, and double four-arm combination is representable. Directly written mergeable states that have no Unicode representation fail `RenderBuffer`.
## Rendering
Drawing functions only modify cells. Call `TUI::RenderBuffer()` to submit the complete active buffer to the terminal.
TUI is a retained cell buffer rather than a stream of drawing commands:
- Rendering does not invoke user callbacks.
- A caller can update many cells and render once.
- Resize preserves only overlapping cells; applications that derive layout from logical state can repaint in `BufferSizeChanged`.
- TUI validates the entire buffer on each render, so direct buffer access remains subject to all glyph and width invariants.
## Thread Affinity
TUI state is owner-thread-only.
- Active operations, including `RunOneCycle`, `Stop`, timer control, active-buffer access, rendering, color-mode access, and listener mutation, must run on the thread that called `TUI::Start`.
- Cross-thread stop requests are not supported.
- `TUI::IsInUse()` and `TUI::IsStopRequested()` return false while inactive; while active they enforce the owner-thread check.
- `TryGetConsoleSize`, listener setup while inactive, pixel conversion, `MeasureChar`, and buffer-explicit drawing do not require an active TUI, but they do not make TUI global state thread-safe.
## Deterministic Backend Injection for Tests
The public header exposes a test-only boundary in `vl::console::unittest`:
- `ITuiBackend` abstracts startup, shutdown, size query, monotonic time, event reading, and rendering.
- `TuiBackendEvent` carries resize, mouse, key, and native-unit character events.
- `ScopedTuiBackend` temporarily installs a backend and restores the previous backend at scope exit.
Create or destroy a `ScopedTuiBackend` only while TUI is inactive. The backend must be non-null. While installed, it is also used by `TUI::TryGetConsoleSize`.
This boundary enables deterministic lifecycle, callback, timer, resize, rendering, and failure tests without controlling a real terminal. Production applications should use the platform backend selected by `TUI::Start`.
+12 -2
View File
@@ -8,12 +8,12 @@
- Crash early instead of adding error-tolerance fallbacks [9]
- Keep design documentation aligned with code after refactoring [8]
- Proactively remove code made redundant by refactoring [8]
- Fix behavior at the owning state instead of patching symptoms [7]
- Make `Stop()` drain asynchronous work before returning [6]
- Validate expectations against implementation and existing tests [5]
- Fix behavior at the owning state instead of patching symptoms [5]
- Use `WString::IndexOf` with `wchar_t` (not `const wchar_t*`) [4]
- Use `collections::BinarySearchLambda` on contiguous buffers (guard empty) [4]
- Verify and localize portability on every target OS [3]
- Verify and localize portability on every target OS [4]
- Use `vl::Exception` for expected semantic failures and `CHECK_ERROR` for invariants [3]
- Extract abstractions only for real shared behavior [3]
- Do not assume async callback owners are heap allocated [3]
@@ -45,6 +45,8 @@
- Use Win32 messages for native dialogs when UIA is unavailable [1]
- Keep generated makefiles platform-invariant [1]
- Group non-template C++ implementations by class in `.cpp` files [1]
- Use reentrant POSIX date-time conversions [1]
- Treat environment correlation as evidence, not a cause [1]
# Refinements
@@ -296,10 +298,18 @@ When shared code and tests pass on other platforms but fail on one target, fix t
When a layered transport needs a stricter response policy than its general parser, enforce that policy in the object that owns the physical connection. Keep the lower parser reusable, and let the connection owner classify the response, report a structured failure, and stop or retry the transport as appropriate.
## Treat environment correlation as evidence, not a cause
When a bug reproduces on one machine but not another, do not assume hardware speed or timing is causal merely because the machines differ. Reproduce the smallest differing state or response shape deterministically, then identify the missing transition or dependency that the other environment happens to mask.
## Verify and localize portability on every target OS
Run the relevant tests on every target operating system whose behavior is being claimed, and report only the platforms actually exercised. Use contrasts between passing and failing platforms to narrow investigation toward the failing platform's implementation before changing shared code, while retaining cross-platform regression verification.
## Use reentrant POSIX date-time conversions
On POSIX platforms, never retain pointers returned by `localtime()` or `gmtime()` across calls or threads because both may share process-wide `tm` storage. Use caller-owned stack values populated by `localtime_r()` and `gmtime_r()` instead, with separate values for simultaneous local and UTC conversions.
## Keep generated makefiles platform-invariant
Tracked `makefile` and `vmake.txt` output should remain identical across Linux and macOS. Keep every platform-guarded translation unit in the stable source list; the inactive translation unit should compile harmlessly through its preprocessor guards. Put monorepo-wide platform policy in the canonical shared `makefile-cpp` and select it at make execution time—for example, Darwin compile/framework options and Linux `-luring`—with libraries placed after object prerequisites.