diff --git a/WIP.Assert.md b/WIP.Assert.md new file mode 100644 index 0000000..46cb799 --- /dev/null +++ b/WIP.Assert.md @@ -0,0 +1,37 @@ +# Assert Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. + +Three sibling modules — `Exact`, `Strict`, `Permissive` — that expose the **same 15-member API**; only the comparison semantics differ: + +| Module | String comparisons | Other comparisons | +|---------------|--------------------|--------------------------------------------------------------------------------| +| `Exact` | case-sensitive | no implicit conversions; datatypes must match exactly (`5 <> 5.0`); `vbNullString <> ""`; `Empty` distinct from `0` / `False` / `""`; object default members **not** evaluated | +| `Strict` | case-sensitive | evaluated as if written directly in twinBASIC; object default members **not** evaluated | +| `Permissive` | case-insensitive | evaluated as if written directly in twinBASIC | + +`Null` is never equal to anything (not even itself) under any of the three flavours — use `IsNull` / `IsNotNull` to test for it. + +The 15 members per module (identical signatures across all three): + +| Member | Purpose | +|-------------------------------------------------|------------------------------------------------------------------| +| `Succeed()` | unconditionally records a pass | +| `Fail([Message])` | unconditionally records a failure | +| `Inconclusive([Message])` | records an inconclusive / skipped result | +| `AreEqual(Expected, Actual, [Message])` | value-equality assertion | +| `AreNotEqual(Expected, Actual, [Message])` | inverse of `AreEqual` | +| `AreSame(Expected, Actual, [Message])` | reference-identity (`Is`) assertion for objects | +| `AreNotSame(Expected, Actual, [Message])` | inverse of `AreSame` | +| `IsTrue(Condition, [Message])` | asserts the condition is `True` | +| `IsFalse(Condition, [Message])` | asserts the condition is `False` | +| `IsNothing(Value, [Message])` | asserts the object reference is `Nothing` | +| `IsNotNothing(Value, [Message])` | inverse of `IsNothing` | +| `IsNull(Value, [Message])` | asserts the value is `Null` | +| `IsNotNull(Value, [Message])` | inverse of `IsNull` | +| `SequenceEquals(Expected, Actual, [FailMessage])` | element-by-element comparison of two sequences / arrays | +| `NotSequenceEquals(Expected, Actual, [FailMessage])` | inverse of `SequenceEquals` | + +Surface each member as if it were an ordinary `Sub` (e.g. `Sub AreEqual(Expected, Actual, [Message])`); the source-side `Lib ""` / `Alias "#N"` / `PreserveSig` / `DeclareWide` decoration is internal pseudo-DLL plumbing and is **not** surfaced. Call out `[DebugOnly(True)]` (assertions compile out of release builds) and `[MustBeQualified(True)]` (callers must write the module name, e.g. `Strict.IsTrue(x)`). + +Layout: one page per module, listing all 15 members inline under `## ` headings (deep-linkable as `…/Strict#areequal`). Replicating 15 × 3 = 45 near-duplicate pages would add noise without value. diff --git a/WIP.CEF.md b/WIP.CEF.md new file mode 100644 index 0000000..62f68e4 --- /dev/null +++ b/WIP.CEF.md @@ -0,0 +1,48 @@ +# CEF Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. + +The entire user-facing surface lives in one source file (`CefControl.twin`): the private `CefBrowserBaseCtl` (where every event / method / property is declared), the private `CefEnvironmentOptions`, and the public `CefBrowser` control which inherits from `CefBrowserBaseCtl`. Everything else under the source tree (`CefControlGlobalWnd`, `APIs`, `MainModule`, `Registry`, `SpecialFolders`, the `CEF\Aliases` / `ApiEntryPoints` / `Globals` / `Initialize` / `Misc` modules, the 30 `_cef_*_t` internal enums and structs, the `CrossProcessIPC` IPC brokers, and every `Implementations/*` callback handler) is `Private Class` / `Private Module` plumbing and gets no doc page. Two user-facing enums live inside otherwise-private files: `CefLogSeverity` in the `_cef_log_severity_t` enum file, and `cefPrintOrientation` inline in `BrowserOM.twin`. + +Public user-facing surface (one control + one options class + two enums): + +| Symbol | Kind | Role | +|------------------------------|---------------------------------------|-----------------------------------------------------------------------------------------------| +| `CefBrowser` | Class (inherits `CefBrowserBaseCtl`) | the control itself, tagged `[WindowsControl("/Miscellaneous/cef64.png")]` | +| `CefEnvironmentOptions` | `Private Class`, exposed | reached via `Public EnvironmentOptions As CefEnvironmentOptions = New CefEnvironmentOptions` on the control | +| `CefLogSeverity` | `Enum` (in `_cef_log_severity_t.twin`)| used by `CefEnvironmentOptions.LogSeverity` | +| `cefPrintOrientation` | `Enum` (in `BrowserOM.twin`) | used by the `Orientation` parameter of `CefBrowser.PrintToPdf` | + +`CefBrowserBaseCtl` is `Private Class` but is where every public member is *declared* — `CefBrowser` itself adds nothing beyond inheriting from it. The page (`CefBrowser/index.md`, folder-style, parallel to `WebView2/`) describes the union; the base class is invisible to user code. + +`CefBrowser` inherits from `VB.BaseControlRectDockable`, so its Properties listing folds in the dockable-rect surface (`Name`, `Left`, `Top`, `Width`, `Height`, `Anchors`, `Dock`, ...) the same way VB-package and CustomControls control pages do. + +`CefBrowserRequestHeaders` is declared as `Alias CefBrowserRequestHeaders As Object` and appears in the `NavigationStarting` event signature. The underlying `Class CefRequestHeaders` is an empty placeholder for a future header collection. **No doc page;** mention on the `NavigationStarting` event entry that the parameter is currently typed `Object` and reserved for future use. + +The `CefBrowser` public surface: + +- **Events (12):** `Create`, `Ready`, `Error`, `NavigationStarting`, `NavigationComplete`, `SourceChanged`, `DocumentTitleChanged`, `DOMContentLoaded`, `PrintToPdfCompleted`, `PrintToPdfFailed`, `JsAsyncResult`, `JsMessage`. +- **Methods (14):** `Initialize`, `Navigate`, `NavigateToString`, `Reload`, `GoBack`, `GoForward`, `ExecuteScript`, `JsRun`, `JsRunAsync`, `PostWebMessage`, `SetVirtualHostNameToFolderMapping`, `ClearVirtualHostNameToFolderMapping`, `OpenDevToolsWindow`, `PrintToPdf`. +- **Properties (12):** `DocumentURL` (Get/Let), `DocumentTitle` (Get), `ZoomFactor` (Get/Let), `UserAgent` (Get/Let), `CanGoBack` (Get), `CanGoForward` (Get), `CefMajorVersion` (Get), `Visible` (Get/Let), `hWnd` (Get), `Parent` (Get), `EnvironmentOptions` (field), `CreateInitialized` (field, `Boolean`, defaults `True`). Plus a `Hidden` `Align` (Get/Let) inherited boilerplate. Plus the inherited rect-dockable surface (`Name`, `Left`, `Top`, `Width`, `Height`, `Anchors`, `Dock`, …). + +`CefEnvironmentOptions` is four bare `Public` fields: + +| Field | Type | +|----------------------------|------------------| +| `BrowserExecutableFolder` | `String` | +| `UserDataFolder` | `String` | +| `LogFilePath` | `String` | +| `LogSeverity` | `CefLogSeverity` | + +`CefLogSeverity` members: `CefLogDisable = 0`, `CefLogVerbose = 1`, `CefLogInfo = 2`, `CefLogWarning = 3`, `CefLogError = 4`, `CefLogFatal = 5`. + +`cefPrintOrientation` members: `cefPrintPortrait = 0`, `cefPrintLandscape = 1`. + +**WebView2-parity gap list** (called out on `CEF/index.md`, drawn from the sample-project examples where these are commented out as *"Sorry, this feature is not yet available in the CEF package"*): + +- Methods: `OpenTaskManagerWindow`, `AddObject` (host-object publication), the request-filter machinery (`AddWebResourceRequestedFilter`). +- Events: `AcceleratorKeyPressed`, `PermissionRequested`, `WebResourceRequested`, `ProcessFailed`, `ScriptDialogOpening`, `UserContextMenu`, `SuspendCompleted`, `SuspendFailed`, `DownloadStarting`, `NewWindowRequested`. + +`CefBrowser.NavigationComplete` carries `IsSuccess` and `WebErrorStatus` parameters, but `OnNavigationComplete_UI` currently hard-codes `IsSuccess = True` and `WebErrorStatus = 0` with `FIXME` comments — noted on the event entry. + +**Multi-version source.** The same `.twin` sources compile against three CEF runtimes (v49 / v109 / v145) selected via the `CEF_VERSION` conditional-compilation argument on the project. At runtime, `CefBrowser.CefMajorVersion` returns the value picked at compile time. The user picks a runtime at deploy time by downloading the matching ZIP from `github.com/twinbasic/cef-runtimes` and extracting to `%LocalAppData%\twinBASIC_CEF_Runtime\`, or by overriding `CefBrowser.EnvironmentOptions.BrowserExecutableFolder` before / during the `Create` event. The runtime download + version-picking section lives on `CEF/index.md`. diff --git a/WIP.CustomControls.md b/WIP.CustomControls.md new file mode 100644 index 0000000..0f06e03 --- /dev/null +++ b/WIP.CustomControls.md @@ -0,0 +1,81 @@ +# CustomControls Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. + +Two source-side packages, **one** doc-side package (`docs/Reference/CustomControls/`). The two source halves split by *role*: a **DESIGNER** framework (the abstract surface a custom control hooks into — `ICustomControl`, `ICustomForm`, the `CustomControlContext` / `CustomFormContext` / `CustomControlTimer` / `CustomControlsCollection` CoClasses, plus the `SerializeInfo` / `Canvas` UDTs and the enums) and a **runtime** half (the eight concrete `Waynes…` controls + shared appearance helpers + mixin base classes). + +Public user-facing surface, grouped by role. + +## Concrete controls + +Each is `Class ` (no `Public` modifier — implicitly public), tagged `[CustomControl("/miscellaneous/frm.png")]` (designer icon) and `[COMCreatable(False)]` (cannot be `New`'d through COM; instantiated by the designer). + +| Control | Implements | Co-located public types | +|------------------|-----------------------------------------------------------|--------------------------------------------------| +| `WaynesButton` | `ICustomControl` + `BaseControlFocusable` (mixin) | `WaynesButtonState` (private, but exposed) | +| `WaynesForm` | `ICustomControl` + `BaseForm` (mixin) | — (uses `WindowsFormOptions` from support file) | +| `WaynesFrame` | `ICustomControl` + `BaseControl` (mixin) | — | +| `WaynesGrid` | `ICustomControl` + `BaseControlFocusable` (mixin) | `Column`, `CellRenderingOptions` | +| `WaynesLabel` | `ICustomControl` + `BaseControl` (mixin) | — | +| `WaynesSlider` | `ICustomControl` + `BaseControlFocusable` (mixin) | `WaynesSliderState`, `SliderDirection` & `SliderDisplayValueFormat` (nested enums) | +| `WaynesTextBox` | `ICustomControl` + `BaseControlFocusable` (mixin) | `WaynesTextBoxState` | +| `WaynesTimer` | `ICustomControl` + `BaseControl` (mixin) | — | + +The "mixin" base classes (`BaseControl`, `BaseControlFocusable`, `BaseForm`) are pulled into each control via the twinBASIC `Implements Via _BaseControl = New ` syntax. The base classes themselves get **no doc page** (they're private and never named by user code), but the inherited members **must be folded into each control's Properties listing** the same way VB-package controls list their inherited surface. The visible inherited surface, by mixin: + +- `BaseControl` → `Name`, `Left`, `Top`, `Width`, `Height`, `Anchors`, `Dock`, `Visible`. +- `BaseControlFocusable` → all of `BaseControl` + `TabIndex`, `TabStop`. +- `BaseForm` → `FormDesignerId`, `Name`, `Left`, `Top`, `Width`, `Height`, `Controls`. + +The state-holder classes (`WaynesButtonState`, `WaynesSliderState`, `WaynesTextBoxState`) and `WindowsFormOptions` are declared `Private Class` but are exposed on the parent control via `Public WithEvents NormalState As WaynesButtonState` (etc.). Same situation as `WebView2EnvironmentOptions` — document them as **sub-pages** of the parent control using the folder-style layout. + +## Shared appearance helpers + +These helpers are reachable through `Public WithEvents …` properties on one or more of the eight controls: + +| Class | Reached as | +|-----------------|-------------------------------------------------------------------------------------| +| `Anchors` | `.Anchors` (via the mixin base) | +| `Corners` | `.Corners`, `CellRenderingOptions.Corners`, `.BackgroundCorners`, `BlockCorners` | +| `Corner` | `Corners.TopLeft` / `.TopRight` / `.BottomLeft` / `.BottomRight` | +| `Borders` | `.Borders`, `CellRenderingOptions.Borders`, `.BackgroundBorders`, `BlockBorders` | +| `Border` | element of `Borders.Elements()`; also `TextRendering.Outlines()` | +| `Fill` | `.BackgroundFill`, `.BlockFill`, `CellRenderingOptions.Fill`, `Border.Fill`, `Line.Fill`, `TextRendering.Fill` | +| `FillColorPoint` | element of `FillColorPoints.Values()` | +| `FillColorPoints` | `Fill.ColorPoints` | +| `Line` | `WaynesGrid.VerticalLineOptions` / `.HorizontalLineOptions` / `.ResizerBar` | +| `Padding` | `TextRendering.Padding` | +| `TextRendering` | `.TextRendering`, `WaynesLabel.TextRendering`, `CellRenderingOptions.TextRendering` | +| `FontStyle` | `TextRendering.Font` | +| `WindowsFormOptions` | `WaynesForm.WindowsOptions` (only one consumer) | + +These pair their small helpers on a single page (`Corner` inlines under `Corners.md`, `Border` under `Borders.md`, `FillColorPoint` and `FillColorPoints` under `Fill.md`, `FontStyle` under `TextRendering.md`). `WindowsFormOptions` is the exception: it has exactly one consumer (`WaynesForm`), so it sits as a folder-style sub-page of `WaynesForm/`, parallel to how WebView2 carries `EnvironmentOptions`. The `TextDecorator(s)` / `UDTs` / `MathSupport` / `ColorSupport` / mixin-bases content is package-internal and gets **no doc page**. + +## DESIGNER framework surface + +The framework half — what a *control author* writes against. Documented under `docs/Reference/CustomControls/Framework/`: + +| Symbol | Kind | Role | +|------------------------------|---------------------|---------------------------------------------------------------------------------------| +| `ICustomControl` | `Interface` | what every concrete control implements: `Initialize(Context)`, `Destroy()`, `Paint(Canvas)` | +| `ICustomForm` | `Interface` | analogous surface for form-class custom controls | +| `CustomControlContext` | `CoClass` | passed to `ICustomControl.Initialize`; offers `GetSerializer()`, `Repaint()`, `CreateTimer()`, `ChangeFocusedElement()` | +| `CustomFormContext` | `CoClass` | extends `CustomControlContext` with `Show()` / `Close()` | +| `CustomControlTimer` | `CoClass` | returned by `CustomControlContext.CreateTimer()`; `Interval`, `Enabled`, `OnTimer` event | +| `CustomControlsCollection` | `CoClass` | the `Controls` collection on a form — `Count`, `Item`, `Add`, `Remove`, `_NewEnum` | +| `SerializeInfo` | UDT | obtained from `Context.GetSerializer()`; exposes `RuntimeUISrz*` operations (deserialize, mode flags, …) | +| `Canvas` | UDT | parameter to `ICustomControl.Paint`; exposes `RuntimeUICCCanvasAddElement` + DPI / size getters | + +Both UDTs follow a pattern unique to twinBASIC: a `Pointer As LongPtr` field plus `Public DeclareWide PtrSafe Function/Sub … Lib "" Alias "#N"` pseudo-DLL declarations bound directly into the type. From a *caller* perspective these read as instance methods on the UDT (`Canvas.RuntimeUICCCanvasAddElement(descriptor)`); document them as methods, and **do not** surface the `Lib "<…>"` / `Alias "#N"` / `PreserveSig` / `DLLStackCheck` decoration (same treatment as Assert's pseudo-DLL plumbing). The verbose `RuntimeUISrz*` / `RuntimeUICC*` names are unfortunate but they *are* the public API — keep them as-is. + +The two underscore-prefixed default interfaces of each CoClass (`_CustomControlTimer`, `_CustomControlContext`, `_CustomFormContext`, `_CustomControlsCollection`, `_CustomControlTimerEvents`) are an implementation detail of the COM `[Default]`/`[Default, Source]` pattern — fold their members onto the CoClass page, **don't** give the interfaces their own pages. + +## Enumerations + +Public enums under `docs/Reference/CustomControls/Enumerations/`: + +- `CornerShape`, `FillPattern`, `TextAlignment`, `TextOverflowMode`, `DockMode`, `FontWeight`, `StartupPosition`, `BorderStyle`, `WindowState` — straightforward value enums. +- `Customtate` — **probable typo** for `CustomState`. Has the same three members as `WindowState` (`tbNormal` / `tbMinimized` / `tbMaximized`) and isn't referenced anywhere else in the package. Document it (since it's `Public`), but add a `> [!NOTE]` callout flagging the typo and pointing readers to `WindowState`. +- `ColorRGBA`, `PixelCount`, `PointSize` — these are declared as `Enum` only because twinBASIC doesn't yet have a `Type Foo = Long` alias syntax. Each carries a `FIXME` comment ("Substitute for an ALIAS to Long") and a single `[_MAX] = 0` placeholder member. Document them as **typedefs for `Long`** (the underlying storage type), not as real enums. Note in each that the alias is what user code actually sees on `Public Width As CustomControls.PixelCount` (etc.) — when the alias syntax lands, these enum stand-ins go away. + +Plus the two enums nested inside `WaynesSlider`: `SliderDirection` and `SliderDisplayValueFormat`. They live on the `WaynesSlider/index.md` page rather than under `Enumerations/` (locally scoped to the slider). diff --git a/WIP.WebView2.md b/WIP.WebView2.md new file mode 100644 index 0000000..89f3ead --- /dev/null +++ b/WIP.WebView2.md @@ -0,0 +1,26 @@ +# WebView2 Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. + +The user-facing surface is the `WebView2` control plus a small set of wrapper classes plus the `wv2…` enumerations. + +| Class | Role | +|-----------------------------|-------------------------------------------------------------------------------------------------------| +| `WebView2` | the control itself (`Inherits VB.BaseControlFocusableNoFont`, `[WindowsControl(...)]`) | +| `WebView2Header` | one HTTP header (Name / Value); value type returned by header iteration | +| `WebView2HeadersCollection` | enumerable wrapper used by `For Each` over request / response headers | +| `WebView2Request` | request side of a `WebResourceRequested` event — Method, Uri, Headers, ContentBytes, ContentUTF8 | +| `WebView2RequestHeaders` | mutable request-header collection — `GetHeader`, `Contains`, `AppendHeader`, `RemoveHeader`, … | +| `WebView2Response` | response side of a `WebResourceRequested` event — StatusCode, ReasonPhrase, Headers, ContentBytes… | +| `WebView2ResponseHeaders` | mutable response-header collection | + +`WebView2EnvironmentOptions` is declared `Private Class`, **but** the `WebView2` control exposes it via `Public EnvironmentOptions As WebView2EnvironmentOptions = New WebView2EnvironmentOptions`. It is documented as a sub-page of the `WebView2` control class — its `Public` fields (`BrowserExecutableFolder`, `UserDataFolder`, `AdditionalBrowserArguments`, `Language`, `TargetCompatibleBrowserVersion`, `AllowSingleSignOnUsingOSPrimaryAccount`, `ExclusiveUserDataFolderAccess`, `EnableTrackingPrevention`) are user-set before / during the `Create` event. + +The `WebView2` control class uses the **folder-style** layout (`WebView2/index.md`) because of its size and to host the `EnvironmentOptions` sub-page. + +Enumerations (ten of them: `wv2PermissionKind`, `wv2PermissionState`, `wv2ErrorStatus`, `wv2KeyEventKind`, `wv2WebResourceContext`, `wv2ProcessFailedKind`, `wv2ScriptDialogKind`, `wv2HostResourceAccessKind`, `wv2PrintOrientation`, `wv2DefaultDownloadCornerAlign`) live under `WebView2/Enumerations/`. `COREWEBVIEW2_PHYSICAL_KEY_STATUS` is a public `Type` (used in the `AcceleratorKeyPressed` event arguments) and lives under `WebView2/Types/`. + +Pre-existing site cross-references: + +- [`docs/Tutorials/WebView2/`](docs/Tutorials/WebView2) — task-oriented tutorial set; cross-link from / to the reference pages where useful. +- [`docs/Reference/VBRUN/Constants/ControlTypeConstants.md`](docs/Reference/VBRUN/Constants/ControlTypeConstants.md) — lists `vbWebView2 = 18`. diff --git a/WIP.WinEventLogLib.md b/WIP.WinEventLogLib.md new file mode 100644 index 0000000..6a32c81 --- /dev/null +++ b/WIP.WinEventLogLib.md @@ -0,0 +1,102 @@ +# WinEventLogLib Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. Sister packages: [WinServicesLib](WIP.WinServicesLib.md), [WinNamedPipesLib](WIP.WinNamedPipesLib.md). + +The user-facing surface is the generic `EventLog(Of T1, T2)` class plus a single helper module. The package's `Constants.twin` declares `Public Enum EventLogTypeConstants` inside a `Private Module`, so the enum does not actually surface — and the `EventLogHelperPrivate` module in `Helper.twin` is *intended-private* despite the `Public` modifier (only used by `EventLog.LogArray` internally). The Win32 API wrappers in `APIs.twin` are pure plumbing. + +Public user-facing surface (one generic class + one helper module): + +| Symbol | Kind | Role | +|---------------------------------|---------------------|---------------------------------------------------------------------------------------| +| `EventLog(Of T1, T2)` | Generic class | Main user-facing class. `T1` is the event-ID enum, `T2` is the category enum. | +| `EventLogHelperPublic` | Public module | Holds the low-level `RegisterEventLogInternal` helper. | +| `RegisterEventLogInternal` | `Sub` on the module | Registry-write helper; `EventLog.Register()` is the normal entry point. | + +`EventLog(Of T1, T2)` public members: + +- `Public Sub New(LogName As String)` — constructor. `LogName` is either a leaf name (`"MyService"`, registered under `Application\MyService`) or a full path (`"System\MyService"`, registered under `System\MyService`). +- `Public Sub LogSuccess(ByVal EventId As T1, ByVal CategoryId As T2, ParamArray AdditionalStrings())` — writes an **Information**-type event (`EVENTLOG_SUCCESS = 0`). The name "Success" is the Win32 SDK constant's literal name, *not* the audit-success category — the underlying event type is **Information**. +- `Public Sub LogFailure(ByVal EventId As T1, ByVal CategoryId As T2, ParamArray AdditionalStrings())` — writes an **Error**-type event (`EVENTLOG_ERROR_TYPE = 1`). +- `Public Sub Register()` — writes the registry entries under `HKLM\SYSTEM\CurrentControlSet\Services\EventLog\…` to declare this EXE as the message provider for the source. Calls `RegisterEventLogInternal(LogName, GetDeclaredMaxEnumValue(Of T2))` — the category count is derived from `T2`'s declared maximum value at compile time. + +Class-level decoration on `EventLog`: `[COMCreatable(False)]`, `[ClassId("4AEA12E8-…-EAEAEAEAEAEA")]` (the `EA` suffix triggers special compiler handling for generic classes). The `[Description]` attribute on the class is the basis for the page intro: *"This is the main event log (generic) class."* + +`EventLogHelperPublic` public members: + +- `Public Sub RegisterEventLogInternal(ByVal LogPath As String, ByVal CategoryCount As Long)` — the registry-write helper. Prepends `"Application\"` to *LogPath* if no backslash is present, opens `HKLM\SYSTEM\CurrentControlSet\Services\EventLog\` with `KEY_WRITE`, then writes `EventMessageFile` and `CategoryMessageFile` (both set to `App.ModulePath`) and `CategoryCount`. Requires admin rights (registry writes to HKLM). Raises run-time error 5 with the message *"Failed to register event log source (\)"* if the open fails. Normally callers use `EventLog.Register()`, which fills *CategoryCount* automatically. + +**Gaps and quirks** to surface on the docs (drawn from a static read of the source): + +- The `EventLogTypeConstants` enum has five values (`Success`, `Warning`, `Error`, `AuditSuccess`, `AuditFailure`) but the public class only exposes Information and Error event types — Warning and Audit events are not currently reachable. +- Method names follow the Win32 SDK constants verbatim: `LogSuccess` writes an *Information* event (because `EVENTLOG_SUCCESS = 0` is the Win32 spelling for the information type), and `LogFailure` writes an *Error* event. Call this out on the per-method entries. +- Message resources: the registry entries point at `App.ModulePath` (the running EXE) for both `EventMessageFile` and `CategoryMessageFile`. Windows therefore expects a message-table resource keyed by the `T1` and `T2` enum values to be embedded in the EXE. The `.twin` source does not itself synthesise that resource; whatever mechanism populates the resource sits in the compiler's special-handling path for the `[ClassId("…EAEAEAEAEAEA")]` magic-byte pattern. The docs describe what Windows expects without making strong claims about how the compiler delivers it. +- `Register()` requires elevation. Normal usage is to call it once during install (from an elevated installer), then call `LogSuccess` / `LogFailure` at runtime without elevation. + +## Canonical usage idiom — composition-delegation onto a service class + +The package's intended usage pattern — *not* obvious from the bare API — is composition-delegation: + +```tb +Class TBSERVICE001 + Implements ITbService + Implements EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES) Via _ + EventLog = New EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES)("Application\" & CurrentComponentName) + … + LogSuccess(service_started, status_changed, CurrentComponentName) ' surfaces directly +End Class +``` + +The `Implements Via = ` form is twinBASIC's composition-delegation syntax (see [`docs/Features/Language/Delegation.md`](docs/Features/Language/Delegation.md) if/once that page exists, or the [`CustomControls` mixin pattern](docs/Reference/CustomControls/index.md) for an analogous use). The class declares it `Implements EventLog(Of …)` and gives the compiler a private field plus a constructor expression; the compiler then auto-forwards every `Public` member of `EventLog` (`LogSuccess`, `LogFailure`, `Register`) through that field. The result: a service class that *contains* an `EventLog` instance and exposes its logging methods as if they were its own. + +Surface this on the `EventLog` page (and on the package index) as the **recommended pattern** for service / long-running classes. Spell out: + +- The constructor expression evaluates *once* (the first time the delegating class is instantiated, per twinBASIC's `Implements ... Via` semantics). +- The `T1` / `T2` type arguments must be identical at the `Implements` declaration and the constructor (the compiler enforces this). +- The `LogPath` is typically `"Application\" & CurrentComponentName` — `CurrentComponentName` is the compile-time class name, so the log path automatically tracks renames. +- The delegating class transparently inherits all three of `LogSuccess` / `LogFailure` / `Register`. Calling code can use them unqualified. + +## Message-table backing: `[PopulateFrom("json", …)]` on the enums + +The `T1` / `T2` enums are typically auto-populated from a JSON resource via the `[PopulateFrom]` attribute: + +```tb +Module MESSAGETABLE + [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "events", "name", "id")] + Enum EVENTS + End Enum + + [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "categories", "name", "id")] + Enum CATEGORIES + End Enum +End Module +``` + +…with `Resources\MESSAGETABLE\Strings.json`: + +```json +{ + "events": [ + { "id": -1073610751, "name": "service_started", "LCID_0000": "%1 service started" }, + { "id": -1073610750, "name": "service_startup_failed", "LCID_0000": "%1 service startup failed" }, + … + ], + "categories": [ + { "id": 1, "name": "status_changed", "LCID_0000": "Status Changed" } + ] +} +``` + +Two things are happening here: + +1. **The enum bodies are populated at compile time** — `Enum EVENTS` starts empty in the source, but after compilation it has members `service_started = -1073610751`, `service_startup_failed = -1073610750`, … (one per `"events"` entry in the JSON, keyed `name → id`). +2. **The same JSON is consumed by the compiler's `mc.exe`-equivalent** that emits the message-table resource into `App.ModulePath`. The `LCID_0000` strings are the message-table entries, and the `%1`, `%2`, … placeholders are filled at log time from the `AdditionalStrings` `ParamArray` to `LogSuccess` / `LogFailure`. The `CategoryCount` registry value (written by `Register()`) is the highest declared `id` in the `categories` block, which is what `GetDeclaredMaxEnumValue(Of T2)` recovers at compile time. + +So the round-trip is: JSON → compile-time enum population + message-table resource emission → registry entries that point Windows at the EXE → runtime `LogSuccess(EventId, CategoryId, …)` writes an event the Event Viewer can format using the embedded message-table strings. + +Surface this on the index page (under "Setting up message resources" or similar) with the JSON skeleton and the cross-reference to `[PopulateFrom]` (which is documented under `docs/Features/`, not in the reference set — link to that page if it exists, otherwise describe in-place). + +The negative event-ID values in the JSON (`-1073610751`) are the standard Win32 event-ID encoding: the high bits encode severity (`0xC0000000` = Error), facility (`0x...`), and customer bit. Don't unpack this on the docs; just note that *"event IDs follow the Win32 documented encoding — see Microsoft's 'Event Identifiers' reference"*. + +## Why `T1` / `T2` and not separate `EventIds` / `Categories` classes + +A class can only `Implements EventLog(Of T1, T2) Via …` *once*. If a service needs events from multiple unrelated message tables, it can compose multiple `EventLog` instances **as named fields** (no `Via`), accepting a small loss of ergonomics (calls become `MyEventLog.LogSuccess(…)` instead of `LogSuccess(…)`). Surface this as a one-line note on the index — most services share a single `MESSAGETABLE` module across all their classes, so the limitation rarely bites. diff --git a/WIP.WinNamedPipesLib.md b/WIP.WinNamedPipesLib.md new file mode 100644 index 0000000..a2fa50d --- /dev/null +++ b/WIP.WinNamedPipesLib.md @@ -0,0 +1,199 @@ +# WinNamedPipesLib Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. Sister packages: [WinServicesLib](WIP.WinServicesLib.md), [WinEventLogLib](WIP.WinEventLogLib.md). + +An IOCP-based async pipe framework. Four user-facing classes — `NamedPipeServer` and `NamedPipeServerConnection` on the server side, `NamedPipeClientManager` and `NamedPipeClientConnection` on the client side. The Win32 API wrappers, the package-internal `OverlappedTypeConstants` enum, the IOCP helper module, and the four `INamedPipe*Internal` interfaces (each declared alongside its matching public class as a refcount / dispatch helper for the IOCP worker threads) are all plumbing and get no doc page. + +The four classes are each tagged `[COMCreatable(False)]` — only the manager / server classes can be instantiated by user code (with `New`); the two `Connection` classes are constructed internally and handed back through events / return values. + +Public user-facing surface (four classes — two on each side): + +| Class | Role | +|-----------------------------|---------------------------------------------------------------------------------------------------------------| +| `NamedPipeServer` | The server. User-instantiated. Sets `PipeName`, calls `Start`, listens for events; one server hosts many clients. | +| `NamedPipeServerConnection` | One server-side per-client connection. Surfaced through `NamedPipeServer` events; carries `AsyncRead` / `AsyncWrite` / `AsyncClose`. | +| `NamedPipeClientManager` | The client-side coordinator. User-instantiated. Owns the IOCP worker threads; the `Connect` method returns a `NamedPipeClientConnection`. | +| `NamedPipeClientConnection` | One client-side connection. Returned by `NamedPipeClientManager.Connect`; carries `Connected` / `Disconnected` / `MessageReceived` / `MessageSent` events and `AsyncRead` / `AsyncWrite` / `AsyncClose`. | + +## `NamedPipeServer` public members + +Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[EventInterfaceId(...)]`, `[ClassId(...)]`. No `[Description("...")]` on the class itself. + +**Public fields** (each carries a `[Description("...")]`): + +- `PipeName As String` — *"the discoverable pipe name"*. Set this before `Start()` or `Start()` raises run-time error 5 (*"cannot start without specifying a pipe name"*). The Win32 pipe namespace path is `\\.\pipe\` (the package prepends `\\.\pipe\` itself). +- `NumThreadsIOCP As Long = 1` — *"the number of IOCP worker threads that will be created"*. Read once when `Start()` is called; the in-source `FIXME` notes that this should become read-only once started. +- `FreeThreadingEvents As Boolean = False` — *"set to TRUE to allow the server events ClientConnected / ClientReceivedDataAsync etc to be fired directly from the IOCP worker threads. set to FALSE to ensure the events get fired on the main UI thread."* The free-threaded path skips a Win32 message-loop round-trip; the marshalled path is safer because the events fire on the UI thread. +- `ContinuouslyReadFromPipe As Boolean = True` — *"set to TRUE to ensure ClientReceivedDataAsync events always fire without having to call AsyncRead manually."* When `False`, the consumer must call `AsyncRead` after each `ClientMessageReceived` to keep receiving. +- `MessageBufferSize As Long = 131072` — *"sets the initial size for ReadFile() buffers. does not affect the maximum message receive size, but can affect performance."* On `ERROR_MORE_DATA` the IOCP loop allocates a larger overflow buffer and re-issues the read, so messages larger than this size do work — but with one extra allocation per overflowed message. + +**Public events**: + +- `ServerReady()` — fires once after `Start()` when every IOCP worker has joined. +- `ClientConnected(Connection As NamedPipeServerConnection)` — a new client connection has completed. +- `ClientDisconnected(Connection As NamedPipeServerConnection)` — the connection has dropped and every outstanding async operation has returned. +- `ClientMessageReceived(Connection As NamedPipeServerConnection, ByRef Cookie As Variant, ByRef Data() As Byte)` — a message arrived. *Data* is a transient view over the IOCP read buffer (a hand-rolled `SAFEARRAY` whose backing memory is reused after the event); copy it if you need to keep it past the event handler. +- `ClientMessageSent(Connection As NamedPipeServerConnection, ByRef Cookie As Variant)` — a previously-issued `AsyncWrite` has completed. + +**Public methods**: + +- `Sub New()` — constructor; creates the hidden marshalling-window used for UI-thread event delivery. +- `Public Sub Start()` — creates the IOCP completion port and `NumThreadsIOCP` worker threads, then issues the first connection listener. Idempotent: calling `Start()` while already started is a no-op. +- `Public Sub Stop()` — cancels every outstanding I/O, joins the IOCP threads, closes pipe handles. Idempotent. Called automatically from `Class_Terminate`. +- `Sub AsyncBroadcast(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — issues `AsyncWrite` against every currently-connected `NamedPipeServerConnection`. +- `Public Sub ManualMessageLoopEnter()` / `Public Sub ManualMessageLoopLeave()` — drive a Win32 message loop manually (rare; only needed when the host process does not naturally pump messages — e.g. an unattended Windows service that wants the marshalled-event semantics rather than the free-threaded ones). `Leave` posts `WM_USER_QUITTING`, which `Enter` reads to break the loop. + +## `NamedPipeServerConnection` public members + +Tagged `[COMCreatable(False)]`. Not directly user-instantiable. + +**Public fields**: + +- `Handle As LongPtr` — the underlying Win32 pipe handle. Exposed but not normally needed; useful for low-level operations or debugging. +- `IsOpening As Boolean` — true while `Open()` is in progress (race-condition window between adding to the linked list and finishing `ConnectNamedPipe`). +- `IsConnected As Boolean` — true between the client connecting and the connection dropping. +- `CustomData As Variant` — *"free for use"*: opaque per-connection slot the consumer can attach state to. + +**Public methods**: + +- `Sub New(...)` — internal constructor; takes the parent server + pipe info. Never called by user code. +- `Public Sub AsyncClose()` — cancels outstanding I/O and closes the pipe handle. Called automatically from `Class_Terminate`. +- `Public Sub AsyncWrite(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — writes a message back to this specific client. Returns immediately; `NamedPipeServer.ClientMessageSent` fires when the write completes. +- `Public Sub AsyncRead(Optional ByRef Cookie As Variant = Empty, Optional OverlappedStruct As LongPtr)` — manually issues a read. Only needed when `NamedPipeServer.ContinuouslyReadFromPipe = False`; otherwise the server keeps the read pump fed automatically. + +No public events — message-received and connection-dropped notifications come through the parent `NamedPipeServer`. The class declares an internal `INamedPipeServerConnectionInternal` interface that the IOCP loop uses for refcounting; that interface is `Private` and gets no doc page. + +## `NamedPipeClientManager` public members + +Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[EventInterfaceId(...)]`, `[ClassId(...)]`. + +**Public fields** (each carries `[Description("...")]`, mirror the server's): + +- `NumThreadsIOCP As Long = 1` +- `MessageBufferSize As Long = 131072` +- `FreeThreadingEvents As Boolean = False` +- `ContinuouslyReadFromPipe As Boolean = True` + +These four are read once on the first `Connect()` call and propagated to every `NamedPipeClientConnection` created through that manager; subsequent changes do not affect connections that already exist. Source comment in `NamedPipeClientConnection` confirms: *"tip: set it in NamedPipeClientConnections before Connect()"*. + +**Public methods**: + +- `Sub New()` — constructor; creates the hidden marshalling window. +- `Public Function Connect(ByVal PipeName As String) As NamedPipeClientConnection` — opens a connection to a server (`\\.\pipe\`). Lazy on first call: creates the IOCP port and the worker threads. Returns a connection object that fires `Connected` once the async `CreateFileW` completes. +- `Public Sub Stop()` — cancels every outstanding I/O on every managed connection, joins the IOCP threads, frees the resources. Idempotent. Called automatically from `Class_Terminate`. +- `Public Function FindNamedPipes(Optional Pattern As String = "*") As Collection` — enumerates the named pipes currently published on the local machine (via `FindFirstFileW("\\.\pipe\")`). Returns a `Collection` of `String`. Useful as a discovery helper before calling `Connect`. + +No events on the manager itself — per-connection events live on the returned `NamedPipeClientConnection` objects. + +## `NamedPipeClientConnection` public members + +Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[ClassId(...)]`, `[EventInterfaceId(...)]`. Not directly user-instantiable — `NamedPipeClientManager.Connect` returns it. + +**Public fields**: + +- `PipeName As String` — the pipe name the connection targets. +- `Handle As LongPtr` — the underlying Win32 file handle. Same caveats as on the server-side connection. +- `CustomData As Variant` — *"free for use"*. + +**Public events**: + +- `Connected()` — the async `CreateFileW` has succeeded. +- `Disconnected()` — the connection has dropped and every outstanding async operation has returned. +- `MessageReceived(ByRef Cookie As Variant, ByRef Data() As Byte)` — a message arrived. *Data* has the same transient-view semantics as on the server. +- `MessageSent(ByRef Cookie As Variant)` — a previously-issued `AsyncWrite` has completed. + +**Public methods**: + +- `Sub New(...)` — internal constructor; never called by user code directly. +- `Public Sub AsyncClose()` — **critical:** the README says *"you MUST call AsyncClose on the client side, otherwise the connection is left alive when the object goes out of scope"*. Surface this on every relevant page. +- `Public Sub AsyncWrite(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — sends a message to the server. +- `Public Sub AsyncRead(Optional ByRef Cookie As Variant = Empty, Optional OverlappedStruct As LongPtr)` — manually issues a read. Same gating as on the server-side: only call this when `ContinuouslyReadFromPipe = False`. + +**Documented gaps / TODOs from `_README.txt`** (surface on the landing page): + +- *"we need a method to allow closing a client connection from the server side"* — there is no `NamedPipeServerConnection.Disconnect` or `.Close` user-method today. The server can stop the whole pipe (`NamedPipeServer.Stop`) but cannot selectively drop one client. +- *"named pipe error should be raised via Error events (rather than throwing an error on the worker threads)"* — internal IOCP errors currently bubble up as VBA run-time errors on worker threads rather than as `Error` events. No `Error` event exists on any of the four classes yet. +- *"remove max size 131072 of messages"* — the `MessageBufferSize` initial-buffer default is 131072 bytes. The IOCP overflow path (`ERROR_MORE_DATA` → larger buffer → re-issue read) does handle larger messages, but there may be a hard cap somewhere the author wants to remove; surface this as *"see TODO list in `_README.txt`"* rather than making a stronger claim. +- *"currently a lot of duplicate code in server + client"* — internal-refactor note. **Not** surfaced on the docs. + +**Cookie pattern.** Every `AsyncRead` and `AsyncWrite` accepts an optional *Cookie* (`Variant`). Whatever the consumer passes in flows through the IOCP completion buffer and is handed back out on the matching `MessageReceived` / `MessageSent` event. This is the package's mechanism for correlating individual writes with their completion notifications when many are in flight. + +**`Data() As Byte` transience.** Inside `MessageReceived` / `ClientMessageReceived`, *Data* is **not** a real `Byte` array — it is a hand-rolled `SAFEARRAY` whose `pvData` field points at the IOCP overlapped buffer. The buffer is recycled back into a free-list at the end of the event handler. Copy the bytes out (`ReDim`-and-copy, or `CStrConv` for text payloads) if you need them after returning from the handler. The source uses `PutMemPtr(VarPtr(safeArrayPtr), VarPtr(safeArrayPsuedo))` and clears it afterwards — surface this lifetime caveat on every event-page entry that carries *Data*. + +**Hidden message window.** Each `NamedPipeServer` and `NamedPipeClientManager` instance creates an invisible `STATIC`-class window with a subclassed `WndProc`, used to marshal IOCP-thread completions back to the UI thread when `FreeThreadingEvents = False`. Mention this on each class's intro paragraph — it explains why the consumer's process must be pumping a message loop for the default event-delivery semantics to work, and why `ManualMessageLoopEnter` / `ManualMessageLoopLeave` exist on `NamedPipeServer` for service / console hosts. + +## Canonical service-host idiom — `ManualMessageLoopEnter` paired with `ChangeState` + +`tbServiceTest2`'s `Sources\SERVICES\TBSERVICE001.twin` shows the standard pattern for a Windows service that hosts a `NamedPipeServer`. Surface this on the `NamedPipeServer.md` page (under a "Hosting inside a Windows service" sub-heading) and on the index landing: + +```tb +' On the service thread (ITbService.EntryPoint): +Set NamedPipeServer = New NamedPipeServer +NamedPipeServer.PipeName = "WaynesPipe_" & CurrentComponentName +ServiceManager.ReportStatus(vbServiceStatusRunning) + +NamedPipeServer.Start() +NamedPipeServer.ManualMessageLoopEnter() ' blocks until ManualMessageLoopLeave +NamedPipeServer.Stop() + +ServiceManager.ReportStatus(vbServiceStatusStopped) + +' On the dispatcher thread (ITbService.ChangeState): +Select Case dwControl + Case vbServiceControlStop, vbServiceControlShutdown + ServiceManager.ReportStatus(vbServiceStatusStopPending) + NamedPipeServer.ManualMessageLoopLeave() ' wakes the service thread +End Select +``` + +Key facts that aren't obvious from the per-method `[Description]`s: + +- The service-thread `EntryPoint` and the dispatcher-thread `ChangeState` are **different threads**. The `NamedPipeServer` member field is shared between them; the dispatcher-thread `ChangeState` calls `ManualMessageLoopLeave` on it to wake the service thread out of `ManualMessageLoopEnter`. +- `ManualMessageLoopLeave` is the **only** way to wake `ManualMessageLoopEnter` cleanly. There is no timeout, no second blocking primitive. If the service needs to react to other wake-up sources (paused state, custom control codes), it sets a shared flag *then* calls `ManualMessageLoopLeave` to break out, inspects the flag, and decides whether to re-enter the loop or proceed to shutdown. The `TBSERVICE002` variant in the same example demonstrates this with `IsPaused` / `IsStopping` shared `Public` fields and a `While IsStopping = False` outer loop. +- Pause / continue support uses the same pattern: `ChangeState` flips `IsPaused = True` and calls `ManualMessageLoopLeave`; the service thread sees the flag, reports `vbServiceStatusPaused`, enters a `Do While IsPaused : Sleep(500) : Loop`, then re-enters `ManualMessageLoopEnter` once `Continue` flips the flag back. +- `FreeThreadingEvents = False` (the default) is **required** for this pattern — events are marshalled to whichever thread is currently inside `ManualMessageLoopEnter`. Setting `FreeThreadingEvents = True` would deliver events on the IOCP worker thread instead and bypass the manual loop entirely (advanced; not the documented service idiom). + +The non-service equivalent — hosting the same `NamedPipeServer` inside a Form — is in `Sources\FORMS\InProcessNamedPipeServerForm.twin`: the Form's regular message loop pumps the marshalling window automatically, so the Form just calls `Server.Start()` in `Form_Load` and `Server.Stop` in `Form_Unload` without ever touching `ManualMessageLoopEnter` / `Leave`. Cross-reference both patterns on the `NamedPipeServer.md` page so the reader sees the choice point. + +## PropertyBag as the canonical message carrier + +Every example serialises structured payloads through the pipe as a `PropertyBag.Contents` `Byte()`: + +```tb +' Sender: +Dim propertyBag As New PropertyBag +propertyBag.WriteProperty("CommandID", "WHAT_TIME_IS_IT") +propertyBag.WriteProperty("Data", payload) +SelectedNamedPipe.AsyncWrite propertyBag.Contents + +' Receiver (inside MessageReceived event): +Dim propertyBag As New PropertyBag +propertyBag.Contents = Data ' deep-copies the bytes; safe past the event handler +Dim commandID As String = propertyBag.ReadProperty("CommandID") +… +``` + +Two reasons this pattern matters and should be surfaced on the docs: + +1. **The transient-`Data()` problem is solved by `PropertyBag`.** Assigning to `PropertyBag.Contents` deep-copies the byte buffer; once the assignment returns, the original IOCP buffer can be recycled without invalidating the data. This is the cleanest answer to *"how do I keep the data past the event handler?"* — call out on every `MessageReceived` / `ClientMessageReceived` page entry as the recommended capture mechanism. +2. **`PropertyBag` provides typed multi-field payloads** without the consumer having to design a wire protocol. Both sides agree on the property names (`"CommandID"`, `"ResponseCommandID"`, `"ResponseData"`, `"Data"`) and `PropertyBag` handles the encoding / decoding. Cross-link [`PropertyBag` reference](docs/Reference/VBRUN/PropertyBag/index.md) from the index landing. + +Surface as the **recommended** carrier; nothing in the package mandates it, raw `Byte()` works too, but every worked example uses `PropertyBag` and the integration story reads much more cleanly with it. + +## Discovery loop — `FindNamedPipes` + +`tbServiceTest2`'s `MainForm` shows the canonical client-side discovery pattern: a low-frequency `Timer` (the form uses `timerRefreshNamedPipes` with a multi-second interval) that calls `NamedPipeClients.FindNamedPipes("WaynesPipe_*")`, repopulates a `ListBox`, and preserves the user's current selection: + +```tb +For Each namePipeName In NamePipeClients.FindNamedPipes("WaynesPipe_*") + If namePipeName = NamedPipeSelected Then namedPipeSelectedIndex = Index + lstNamedPipes.AddItem(namePipeName) + Index += 1 +Next +``` + +Surface on the `NamedPipeClientManager.md` page (under the `FindNamedPipes` entry) as the recommended polling loop — the underlying `FindFirstFileW("\\.\pipe\…")` call is cheap enough to invoke every few seconds without measurable cost, and pipes appear / disappear too quickly for any event-driven discovery to be reliable. Don't claim there's no faster API; just say *"polling is the documented approach"*. + +## Service-side broadcast + +`AsyncBroadcast` (on `NamedPipeServer`) accepts a `Byte()` payload and issues `AsyncWrite` against every currently-connected `NamedPipeServerConnection`. Useful when the server has multiple concurrent connections and wants to push an update to all of them; the alternative is iterating the connections manually. diff --git a/WIP.WinNativeCommonCtls.md b/WIP.WinNativeCommonCtls.md new file mode 100644 index 0000000..3e93209 --- /dev/null +++ b/WIP.WinNativeCommonCtls.md @@ -0,0 +1,130 @@ +# WinNativeCommonCtls Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. + +A VB6-compatible replacement for **Microsoft Common Controls 6.0** (`MSCOMCTL.OCX`), written on top of the Win32 ComCtl32 controls (`COMCTL32.DLL` / `MSFTEDIT.DLL`). Ships eight controls that mirror the MSCOMCTL surface name-for-name where possible. First released v0.0.1.0 on 18-FEB-2023; independent of (but co-versioned with) the VB compatibility package. + +Each control is a heavy `BaseCtl` (where every event / method / property is implemented, tagged `[COMCreatable(False)]` + `[EventsUseDispInterface]`) plus a thin `` leaf (`Inherits BaseCtl`, tagged `[WindowsControl("/miscellaneous/ICONS??/??.png")]`). The leaf adds only a `Class_BeforeFirstMethodAccess` that calls `[_HiddenModule].EnsureContainerIsLoaded(Me)` — same `BaseCtl` / `` leaf split that CEF and the VB controls use. + +Eight controls (one `.twin` per pair): + +| File | `BaseCtl` inherits | Role | +|-------------------|---------------------------------|-----------------------------------------------------------------------------------------------| +| `DTPicker.twin` | `VB.BaseControlFocusable` | Date / time picker — calendar drop-down, single-date `Value`, custom format strings | +| `ImageList.twin` | `VB.BaseControlNotFocusable` | Off-screen image collection — feeds `ListView` / `TreeView` icons via `Icons` / `ImageList` properties | +| `ListView.twin` | `VB.BaseControlFocusable` | Multi-column list — four `View` modes (Icon / SmallIcon / List / Report), label-edit, checkboxes | +| `MonthView.twin` | `VB.BaseControlFocusable` | Full-month calendar grid — multi-select, bold-day callbacks, week-number / today display | +| `ProgressBar.twin`| `VB.BaseControlNotFocusable2` | Standard / Smooth / Marquee progress indicator with three visual states (Normal / Error / Paused) | +| `Slider.twin` | `VB.BaseControlFocusableNoFont` | Trackbar / slider — tick marks, range selection, vertical or horizontal orientation | +| `TreeView.twin` | `VB.BaseControlFocusable` | Hierarchical tree of `Node` objects — sorting, label-edit, checkboxes, image lists | +| `UpDown.twin` | `VB.BaseControlFocusableNoFont` | Spin control (up / down arrows) — pure Increment / Min / Max / Value; no auto-buddy binding | + +Every `BaseCtl` carries `[WithDispatchForwarding] Implements Control` (where `Control` is `Private Interface` in `Interfaces.twin`, marked `[COMExtensible]` — essentially an `Object` alias that makes the dispatch forwarding behave). They also implement a chorus of `VB.IWindowsControl`, `VB.IWindowElementEventsCommon`, `VB.IWindowElementEventsCommonControls`, `VB.IWindowElementEventsUC`, `VB.IWindowElementEventsAX` — these are the VB-package event-dispatch interfaces; do **not** surface them on the docs. Each control also implements one private `TbPrivate` interface (declared `[ComImport(True)]` inside the same `.twin`) that the package's collection sub-objects use to refcount and reach internal state without taking a strong reference; **no doc page** for those. + +## Public user-facing surface + +The eight leaf classes `DTPicker`, `ImageList`, `ListView`, `MonthView`, `ProgressBar`, `Slider`, `TreeView`, `UpDown` are what user code references at design time (via `[WindowsControl(...)]`) and at run time (`Dim lv As ListView`). The `BaseCtl` base classes are the implementation half — `[COMCreatable(False)]` and not user-instantiable, but **the entire user-visible surface is declared on them**. Document on the leaf's name (`ListView.md`), describe the full surface, and don't surface the `BaseCtl` split. + +The package also surfaces eight sub-object classes — collection plus item: + +| Class | Reached via | Notes | +|-----------------|----------------------------------------------------------|------------------------------------------------------------------------------------| +| `ListImages` | `ImageList.ListImages` (Get-only) | Enumerable; `Item(Index or Key)` default member; `Add`, `Remove`, `Clear`, `Exists` | +| `ListImage` | element of `ListImages` (returned from `Add`, indexed) | `Index` (read-only), `Key`, `Picture`, `Tag`, plus `Draw(hDC, x, y, Style)` and `ExtractIcon` | +| `ListItems` | `ListView.ListItems` (Get-only) | Enumerable; `Item(Index or Key)` default member; `Add`, `Remove`, `Clear` | +| `ListItem` | element of `ListItems` | `Text` (default), `SubItems(Index)`, `Icon`, `SmallIcon`, `Checked`, `Selected`, `Ghosted`, `Bold`, `BackColor`, `ForeColor`, `Tag`, `ToolTipText`, `EnsureVisible`, `Left` / `Top` / `Width` / `Height`, `Index` (RO), `Key`, `CreateDragImage` (`[Unimplemented]`) | +| `ColumnHeaders` | `ListView.ColumnHeaders` (Get-only) | Same shape as `ListItems`; `Add(Index, Key, Text, Width, Alignment, Icon)` returns `ColumnHeader` | +| `ColumnHeader` | element of `ColumnHeaders` | `Text` (default), `Width`, `Left` (RO), `Alignment` (typed `ListColumnAlignmentConstants`), `Position`, `SubItemIndex`, `Icon`, `Index` (RO), `Key`, `Tag` | +| `Nodes` | `TreeView.Nodes` (Get-only) | Enumerable; `Item(Index or Key)` default; `Add(Relative, Relationship, Key, Text, Image, SelectedImage)` returns `Node` | +| `Node` | element of `Nodes` | `Text` (default), `Parent`, `Child`, `Next`, `Previous`, `Root`, `FirstSibling`, `LastSibling`, `Children` (count), `Expanded`, `Selected`, `Checked`, `Bold`, `BackColor`, `ForeColor`, `Image`, `SelectedImage`, `Tag`, `FullPath`, `Visible` (RO), `Sorted`, `SortOrder`, `SortType`, `EnsureVisible`, `Index` (RO), `Key` | + +Every sub-object is `[COMCreatable(False)]` — its constructor takes a `BaseCtl` reference, so user code never instantiates these directly. They are returned from container methods (`Add`, `Item`) and reached through container properties. + +Container cross-references (typed as the `BaseCtl` parent, since the controls accept either the base or the leaf — but document the parameter as the **leaf**): + +- `TreeView.ImageList` / `Let` / `Set` — typed `As ImageListBaseCtl`; the user assigns an `ImageList`. +- `ListView.Icons` / `Let` / `Set`, `ListView.SmallIcons` / `Let` / `Set`, `ListView.ColumnHeaderIcons` / `Let` / `Set` — all three typed `As ImageListBaseCtl`; the user assigns an `ImageList`. + +`ListView.BorderStyle` is unusually typed `As TreeBorderStyleConstants` (declared in `TreeViewPublic`, not in a `ListView*` module) — the enum is shared across both controls. Surface this on the `BorderStyle` entry without trying to rationalise it. + +## Per-control highlights + +These are the points worth surfacing on each control's page that are *not* obvious from a flat property list: + +- **DTPicker** — the only control where most behaviour is in the calendar drop-down, not the inline display. The `Calendar*` colour properties (`CalendarBackColor`, `CalendarForeColor`, `CalendarTitleBackColor`, `CalendarTitleForeColor`, `CalendarTrailingForeColor`) act on the dropped-down month grid via `DTM_SETMCCOLOR`. `Format` (`DTPickerFormatConstants`) chooses between long-date / short-date / time / custom; when set to `dtpCustom`, the picker pulls `CustomFormat` (a `GetDateFormat`-style picture string). The control exposes `Year` / `Month` / `Week` / `Day` / `Hour` / `Minute` / `Second` accessors that decompose the current `Value`. `Value` is `Variant` — it can be `Null` (no date selected) when `CheckBox = True` and the user unchecks the box. Events `Format`, `FormatSize`, `CallbackKeyDown` fire when `Format = dtpCustom` and the format string contains a callback token. +- **ImageList** — purely off-screen; `Visible` does nothing user-meaningful (it's a "store of pictures" control). The `ImageWidth` / `ImageHeight` properties are read/write **only while empty** — once any image is added, the setter raises run-time error 35611 (*"Property is read-only if image list contains images"*). `ColorDepth` is fixed at construction time. `MaskColor` + `UseMaskColor = True` makes the masked pixels transparent when rendered into a control that consumes the image list. `Overlay(Key1, Key2)` composes two list-images into a single `StdPicture`. Bound-count tracking: an `ImageList` cannot be modified (clear / remove) while any control has it bound as `Icons` / `SmallIcons` / `ColumnHeaderIcons` / `ImageList`, throwing error 35617. +- **ListView** — the largest of the eight. `View` switches the visual mode (`lvwIcon` / `lvwSmallIcon` / `lvwList` / `lvwReport`); `Arrange` (`lvwNone` / `lvwAutoLeft` / `lvwAutoTop`) auto-flows the icon mode; `Report` mode is the only one that shows the `ColumnHeaders`. `LabelEdit` defaults to `lvwAutomatic` — F2 / click-and-wait edits a label in place; `lvwManual` requires `StartLabelEdit()` and `lvwDisabled` blocks editing. `TextBackground` (`lvwTransparent` / `lvwOpaque`) acts on the *item* text rendering, not the control's `BackColor`. `MultiSelect = True` enables Ctrl+click / Shift+click range selection. `CheckBoxes = True` adds a leading checkbox per row and fires `ItemCheck`. `AllowColumnReorder` only matters in Report view. `BorderStyle` is `TreeBorderStyleConstants` (`ccNone` / `ccFixedSingle`). The control surfaces `hWnd` and `hWndHeader` (the embedded `SysHeader32` window) separately. `Scroll` is `[Unimplemented]` per the source. `GetFirstVisible() As ListItem`, `SelectedItem` / `SelectedItemIndex` — the latter is read-only (assign through `ListItem.Selected = True` instead). +- **MonthView** — `MonthColumns` / `MonthRows` lay out a grid of side-by-side month panels (`ResizeToFit` auto-sizes the control to fit them). `Day` / `Month` / `Week` / `Year` are the same decomposition pattern as DTPicker. `MaxSelCount` is the upper bound of a multi-day selection (default 7, max ≈ 366 per the Win32 control); `SelStart` and `SelEnd` are the inclusive range. `MinDate` / `MaxDate` bound the navigable range. `Value` is the *current selection's start date* (same as `SelStart` when `MultiSelect = False`). The control fires both `Click` (any click) and `DateClick` (only when a date cell is hit, with the date passed as a parameter); same split for `DblClick` / `DateDblClick`. `GetDayBold` is an event-driven callback — the control fires it for each visible month asking for a `State()` array of which days to render bold; this is the mechanism for highlighting holidays, schedule entries, etc. `DayBold` is an alternative per-date setter. `GetMonthRange(IncludeTrailing, StartDate, EndDate)` returns the visible date span. +- **ProgressBar** — three orthogonal axes. `Min` / `Max` / `Value` are the standard range. `Step` + `StepIt()` advances the bar by `Step` units (typical loop pattern: `Min = 0`, `Max = N`, then `StepIt()` per iteration). `Scrolling = PrbScrollingStandard` (default) animates the bar in segments; `PrbScrollingSmooth` is the continuous block; `PrbScrollingMarquee` is the indeterminate animation (drive with `MarqueeAnimation = True` + `MarqueeSpeed`). `State` (`PrbStateNormal` / `Error` / `Paused`) tints the bar red / yellow per the OS theme. `Orientation` is `PrbOrientation` (`Horizontal` / `Vertical`). The control has `Click` / `DblClick` / `Mouse*` events but **no** `Change` despite the declaration — verify with the source if surfaced (`Change` is declared in the events region but not fired by any Win32 progress-bar notification). +- **Slider** — `Min` / `Max` / `Value` like a scrollbar; `SmallChange` is the arrow-key step, `LargeChange` is the PgUp / PgDn step. `SelStart` + `SelLength` create a highlighted selection range (visible when `SelectRange = True`). `TickFrequency` controls how often tick marks appear; `TickStyle` (`sldBottomRight` / `sldTopLeft` / `sldBoth` / `sldNoTicks`) chooses which side(s) of the channel they render on. `TextPosition` (`sldAboveLeft` / `sldBelowRight`) is for the optional tip text. `HideThumb = True` removes the draggable indicator. `ShowTip = True` enables the floating tooltip showing the current value during drag. `Orientation` is `OrientationConstants` (the shared horizontal / vertical enum used also by `UpDown`). +- **TreeView** — the second-largest control. `Style` (`TreeStyleConstants`, 8 values) is a composite of *show / hide* flags for tree-lines / plus-minus boxes / icons / text — the values name what's shown. `LineStyle` chooses `tvwRootLines` (lines from root nodes) or `tvwTreeLines` (lines only from children). `Sorted` / `SortOrder` / `SortType` apply at the root level; each `Node` has its own per-subtree `Sorted` / `SortOrder` / `SortType`. `LabelEdit` is the same gating as `ListView.LabelEdit` (`tvwAutomatic` / `Manual` / `Disabled`). `CheckBoxes = True` adds per-node checkboxes; `FullRowSelect` extends the selection highlight across the full row width. `Indentation` is in twips. `HitTest(x, y)` returns the `Node` at a point (for hover effects, drag-drop). `SelectedItem` (`Get` / `Let` / `Set`) and `DropHighlight` (`Get` / `Let` / `Set`) are both `Node`-typed. `StartLabelEdit()` for `Manual` mode. `GetVisibleCount()` returns how many full nodes the visible area shows. `Scroll` event new to tB. +- **UpDown** — pure spin control: `Min` / `Max` / `Value` / `Increment`. `Orientation` is `OrientationConstants` (horizontal pair of arrows or vertical, the more common). Events are `Change` (any time `Value` changes), `UpClick`, `DownClick`. There is *no* auto-buddy / partner-control facility in this version (the Win32 `UDS_AUTOBUDDY` flag is in the source enums but not exposed) — user code wires `UpClick` / `DownClick` to update the partner control manually. + +Common surface across every control: `Public Opacity As Double = 100` (with the *"REQUIRES TARGET OS 6.2+ FOR CHILD CONTROLS."* description), `Public TransparencyKey As OLE_COLOR = -1` (same OS requirement), and (where `FEATURE_OLEDRAGDROP` is enabled at compile time) `Public OLEDropMode As VBRUN.OLEDropConstants` plus the `OLECompleteDrag` / `OLEDragDrop` / `OLEDragOver` / `OLEGiveFeedback` / `OLESetData` / `OLEStartDrag` events. `Public OLEDrag()` method on every control. `Public Property Get Parent() As Object` and `Public Property Get Object() As Object` on every control. The inherited surface from `VB.BaseControl*` includes `Name`, `Left`, `Top`, `Width`, `Height`, `Anchors`, `Dock`, `Visible`, `Enabled`, `BackColor` / `ForeColor` / `Font` (where focusable), `Appearance`, `MousePointer` / `MouseIcon`, `ToolTipText`, `DragMode` / `DragIcon`, `Drag()`, `Refresh()`, `SetFocus()` (focusable variants), `ZOrder()`, `CausesValidation`, `TabIndex` / `TabStop` (focusable variants), `VisualStyles`, `hWnd`, `HelpContextID` / `WhatsThisHelpID`. + +## Per-control nested enums (fold onto the declaring control's page) + +These enums are declared *inside* each `BaseCtl` (`Enum ` without `Public`, which still surfaces because the enclosing class is public). Following the CustomControls convention for `WaynesSlider.SliderDirection`, document each on its declaring control's page rather than under `Enumerations/`: + +| Enum | Declared on | Members | +|---------------------------------|----------------------------|-------------------------------------------------------------------------------| +| `ImageListColorDepth` | `ImageListBaseCtl` | `ColorDepth4Bit = 4`, `ColorDepth8Bit = 8`, `ColorDepth16Bit = 16`, `ColorDepth24Bit = 24`, `ColorDepth32Bit = 32` | +| `ListViewConstants` | `ListViewBaseCtl` | `lvwIcon = 0`, `lvwSmallIcon = 1`, `lvwList = 2`, `lvwReport = 3` | +| `ListArrangeConstants` | `ListViewBaseCtl` | `lvwNone = 0`, `lvwAutoLeft = 1`, `lvwAutoTop = 2` | +| `ListTextBackgroundConstants` | `ListViewBaseCtl` | `lvwTransparent = 0`, `lvwOpaque = 1` | +| `ListLabelEditConstants` | `ListViewBaseCtl` | `lvwAutomatic = 0`, `lvwManual = 1`, `lvwDisabled = 2` | +| `ListColumnAlignmentConstants` | `ColumnHeader` | `lvwColumnLeft = 0`, `lvwColumnRight = 1`, `lvwColumnCenter = 2` | +| `PrbOrientation` | `ProgressBarBaseCtl` | `PrbOrientationHorizontal = 0`, `PrbOrientationVertical = 1` | +| `PrbScrolling` | `ProgressBarBaseCtl` | `PrbScrollingStandard = 0`, `PrbScrollingSmooth = 1`, `PrbScrollingMarquee = 2` | +| `PrbState` | `ProgressBarBaseCtl` | `PrbStateNormal = 1`, `PrbStateError = 2`, `PrbStatePaused = 3` | +| `TickStyleConstants` | `SliderBaseCtl` | `sldBottomRight = 0`, `sldTopLeft = 1`, `sldBoth = 2`, `sldNoTicks = 3` | +| `TextPositionConstants` | `SliderBaseCtl` | `sldAboveLeft = 0`, `sldBelowRight = 1` | + +Source-side spelling note: every enum is named `` (no `Public` modifier) but the *member* names use the historical VB6 prefix conventions — `lvw` for ListView, `tvw` for TreeView, `sld` for Slider, `dtp` for DTPicker, `Prb` for ProgressBar, `cc` for cross-control. Mixed casing in member names (`SldAboveLeft` literal in the source defaults vs `sldAboveLeft` declaration) is a source-side issue; surface members with the declared casing. + +## Module-level enums (under `Enumerations/`) + +Five `Consts.twin` modules in `SUPPORT/` carry Win32 SDK plumbing (message IDs, notification IDs, style flags, Win32 types like `NMHDR` / `SYSTEMTIME` / `LVCOLUMNW`) **plus** a small fraction of user-facing enums. The plumbing is unreachable by user code (mostly inside `Private Module …Consts`); the user-facing enums are split into a separate `Public Module` (TreeView's clean case) or coexist with the plumbing in an effectively-public bare `Module` (the rest). Either way, surface only the user-facing enums: + +| Enum | Declared in / module | Members | +|-------------------------------|------------------------------------------------------------|----------------------------------------------------------------------| +| `DTPickerFormatConstants` | `DTPickerConsts.twin` (module `DTPickerConsts`) | `dtpLongDate = 0`, `dtpShortDate = 1`, `dtpTime = 2`, `dtpCustom = 3` | +| `TreeBorderStyleConstants` | `TreeViewConsts.twin` (`Public Module TreeViewPublic`) | `ccNone = 0`, `ccFixedSingle = 1` | +| `TreeLabelEditConstants` | `TreeViewConsts.twin` (`TreeViewPublic`) | `tvwAutomatic = 0`, `tvwManual = 1`, `tvwDisabled = 2` | +| `TreeLineStyleConstants` | `TreeViewConsts.twin` (`TreeViewPublic`) | `tvwTreeLines = 0`, `tvwRootLines = 1` | +| `TreeStyleConstants` | `TreeViewConsts.twin` (`TreeViewPublic`) | 8 members: `tvwTextOnly`, `tvwPictureText`, `tvwPlusMinusText`, `tvwPlusMinusPictureText`, `tvwTreelinesText`, `tvwTreelinesPictureText`, `tvwTreelinesPlusMinusText`, `tvwTreelinesPlusMinusPictureText` | +| `TreeRelationshipConstants` | `TreeViewConsts.twin` (`TreeViewPublic`) | `tvwFirst = 0`, `tvwLast = 1`, `tvwNext = 2`, `tvwPrevious = 3`, `tvwChild = 4` | +| `TreeSortOrderConstants` | `TreeViewConsts.twin` (`TreeViewPublic`) | `tvwAscending = 0`, `tvwDescending = 1` | +| `TreeSortTypeConstants` | `TreeViewConsts.twin` (`TreeViewPublic`) | `tvwBinary = 0`, `tvwText = 1` | +| `OrientationConstants` | `Misc.twin` (`Private Module Miscellaneous`) | `ccOrientationHorizontal = 0`, `ccOrientationVertical = 1` — used by both **Slider** and **UpDown** | +| `ImlDrawConstants` | `ImageListConsts.twin` (`Private Module ImageListConsts`) | `ImlDrawNormal = 1`, `ImlDrawTransparent = 2`, `ImlDrawSelected = 4`, `ImlDrawFocus = 8`, `ImlDrawNoMask = 16` — flag combination; used as `[TypeHint(ImlDrawConstants)]` on `ListImage.Draw`'s `Style` parameter | + +For `OrientationConstants` and `ImlDrawConstants` (declared `Public Enum` inside a `Private Module`): the enclosing module is unreachable by name from user code, but the enum members are reachable because they're tagged through `[TypeHint]` on the consuming method's parameter and are also surfaced by the IDE's "implicit member visibility" — i.e. user code writes `Slider1.Orientation = ccOrientationVertical` and `ListImage.Draw(hdc, 0, 0, ImlDrawTransparent Or ImlDrawSelected)`. Document the enum and don't worry about qualification — the user's call site never needs `Module.Enum.Member` form. + +The remaining `Consts.twin` modules (`ImageListConsts`, `ListViewConsts`, `ProgressBarConsts`, `TreeViewConsts.TreeViewConsts` (the private half), `UpDownConsts`, `SliderConsts`, `MonthViewConsts`, `DTPickerConsts` non-`DTPickerFormatConstants` content) are package-internal — Win32 message IDs, style flags, notification structures (`NMHDR`, `NMLISTVIEW`, `NMDATETIMECHANGE`, …) that the controls use to talk to ComCtl32 but that the user never references. **No doc pages** for those; do not document `LVMessages`, `TVMessages`, `MonthViewMessages`, `SliderMessages`, `UpDownMessages`, `DTPickerMessages` and the associated `*Notifications` / `*Styles` enums. + +## Private classes (no doc page) + +- `Private Class ListViewHeaderSubclasser` (in `ListView.twin`) — subclasses the embedded `SysHeader32` window to intercept `HDM_LAYOUT` notifications for the column-resize handler. Implementation detail. +- `Private Class TreeViewNodeCheckState` / `ListViewNodeCheckState` / `TreeViewNodeClick` / `TreeViewNodeDblClick` (in `TreeViewNodeCheckState.twin`) — four `IScheduledCallback`-implementing dispatch helpers that the controls schedule onto the message loop to fire `NodeCheck` / `ItemCheck` / `NodeClick` / `DblClick` events at the right point in the click-handling sequence. Same role as the `…Internal` classes in WinNamedPipesLib; no doc page. +- `Class ImageListPropertyPage` (in `ImageListPropertyPage.twin`) — `[FormDesignerId]` `[PredeclaredId]` `[COMCreatable(False)]` Form class that's the IDE's design-time property editor for `ImageList` (the "Custom Properties..." button). Invoked from `ImageListBaseCtl.HandleInvokePropertyExtension`. Pure design-time tooling, never appears at run-time; no doc page. +- `Private Interface Control` / `IScheduledCallback` / `ITwinBasicDesignerExtensions` (in `Interfaces.twin`) — internal interfaces. `Control` is the empty marker interface that `[WithDispatchForwarding]` resolves names through. No doc pages. +- `Private Module Miscellaneous` (in `Misc.twin`) — `StrPtrSafe`, `CommonTreeViewGetNodeFromHandle`, `SyncBorderStyle` — internal helpers. `OrientationConstants` does surface from this module (see above) but the module itself doesn't get a doc page. +- `Private Module ImagesHelper` (in `ImagesHelper.twin`) — `GetBitsPerPixelFromPic`. Internal helper. No doc page. +- `Private Module ImageListConsts`, `ListViewConsts`, `ProgressBarConsts`, `TreeViewConsts` (the private half), and the bare `Module DTPickerConsts` / `MonthViewConsts` / `SliderConsts` / `UpDownConsts` (effectively public but Win32-plumbing-only) — covered above; no per-module doc page. + +## `[Unimplemented]` and `[Hidden]` members to flag + +- **DTPicker.RightToLeft** — tagged `[Unimplemented]`; flag with `> [!NOTE]`. +- **MonthView.RightToLeft** — same. +- **ListView.Scroll** event — tagged `[Unimplemented]`; flag on the event entry. +- **ListItem.CreateDragImage** — tagged `[Unimplemented]`; flag with `> [!NOTE]`. +- **ListImages.ControlDefault** — tagged `[Unimplemented]` and `[Hidden]`; **do not document** (`[Hidden]` means the IDE intentionally suppresses it). +- **ListView.AllowColumnReorder** — implemented but only takes effect in Report view; surface as a note on the property. + +Layout: folder-style for `ImageList/`, `ListView/`, `TreeView/` (each has 2–4 sub-object companions — `ListImage` + `ListImages`; `ListItem` + `ListItems` + `ColumnHeader` + `ColumnHeaders`; `Node` + `Nodes` — that are 1:1 with the container, same pattern as `CustomControls/WaynesButton/WaynesButtonState.md`). Single-file for the remaining five (`DTPicker.md`, `MonthView.md`, `ProgressBar.md`, `Slider.md`, `UpDown.md`). An `Enumerations/` folder holds the 10 module-level / shared enums; the 11 per-control nested enums fold onto their declaring control's page. + +## Pre-existing cross-references on the site + +- [`docs/Reference/VBRUN/Constants/ControlTypeConstants.md`](docs/Reference/VBRUN/Constants/ControlTypeConstants.md) already lists every control's `vb` constant: `vbProgressBar = 21`, `vbTreeView = 22`, `vbSlider = 26`, `vbUpDown = 27`, `vbDTPicker = 28`, `vbMonthView = 29`, `vbListView = 30`, `vbImageList = 31`. Each control's reference page should link back to its constant. +- [`docs/Reference/VBRUN/Constants/OLEDropConstants.md`](docs/Reference/VBRUN/Constants/OLEDropConstants.md) and the `OLEDragDrop` events are inherited surface — link the `OLEDropMode` entries to the constant. diff --git a/WIP.WinServicesLib.md b/WIP.WinServicesLib.md new file mode 100644 index 0000000..969c2a2 --- /dev/null +++ b/WIP.WinServicesLib.md @@ -0,0 +1,132 @@ +# WinServicesLib Package — Working Notes + +See [WIP.md](WIP.md) for the cross-package maintenance guide. Sister packages: [WinEventLogLib](WIP.WinEventLogLib.md), [WinNamedPipesLib](WIP.WinNamedPipesLib.md). + +A thin OS-services wrapper. The `[PredeclaredId]` singleton `Services` is the entry point; `ServiceManager` carries per-service configuration; `ServiceCreator(Of T)` is the generic factory; `ServiceState` is a read-only snapshot. The Win32 API wrappers, the `Private Module ServicesConstants` half of the constants file, the `ServiceControlHandlerCallback_Trampoline` helper, and the `IServiceCreator` / `IServiceManagerInternal` private interfaces are plumbing and get no doc page. The user-facing enums live in `Public Module ServicesConstantsPublic`. + +Public user-facing surface (three concrete classes + one generic class + one interface + four enums): + +| Symbol | Kind | Role | +|---------------------------------|-----------------------|---------------------------------------------------------------------------------------------------| +| `Services` | `[PredeclaredId]` Class | The singleton coordinator: `ConfigureNew`, `RunServiceDispatcher`, `InstallAll`, `UninstallAll`, `LaunchService`, `ControlService`, `QueryStateOfService`, `GetConfiguredService`, `_NewEnum`. Used as `Services.X` without `New`. | +| `ServiceManager` | Class | Per-service configuration + runtime status reporting. Returned by `Services.ConfigureNew()`. | +| `ServiceCreator(Of T)` | Generic class | The dispatcher's factory: `T` must implement `ITbService`; `CreateInstance` returns `New T`. | +| `ServiceState` | Class | Read-only state snapshot. Constructor (called via `Services.QueryStateOfService(Name)`) queries the SCM. | +| `ITbService` | Public Interface | The contract every service class implements: `EntryPoint`, `StartupFailed`, `ChangeState`. | +| `ServiceTypeConstants` | Enum | `tbServiceTypeOwnProcess`, `tbServiceTypeShareProcess`, etc. | +| `ServiceStartConstants` | Enum | `tbServiceStartAuto`, `tbServiceStartOnDemand`, etc. | +| `ServiceControlCodeConstants` | Enum | `vbServiceControlStop`, `vbServiceControlPause`, `vbServiceControlContinue`, etc. | +| `ServiceStatusConstants` | Enum | `vbServiceStatusRunning`, `vbServiceStatusStartPending`, `vbServiceStatusStopped`, etc. | + +The two private interfaces (`IServiceCreator`, `IServiceManagerInternal`) are pure implementation detail — same situation as `WinNamedPipesLib`'s `INamedPipe*Internal` interfaces. **No doc page**, and don't surface the underscored implementing members on the concrete classes either. + +The two `Private Module` declarations (`ServicesAPIs`, `ServicesConstants`) and the `Private Module ServicesHelper` are all internal — **no doc page**. + +## `Services` public members + +`[PredeclaredId]` Class. The compiler instantiates a singleton named `Services` at program start; user code calls `Services.X` directly without `New`. The class also doubles as an enumerable collection of the `ServiceManager` instances that have been configured (`For Each manager In Services`). + +**Public methods**: + +- `Function ConfigureNew() As ServiceManager` — *"Use this method to configure a service. Usually used during app startup."* Allocates a new `ServiceManager`, adds it to the internal collection, returns it. Typical use: `With Services.ConfigureNew : .Name = "MyService" : .InstanceCreator = New ServiceCreator(Of MyServiceClass) : End With`. +- `Sub RunServiceDispatcher()` — *"This method hands over to the OS for managing the starting/stopping of services via the main thread. This is a BLOCKING call, until the OS wants to shutdown the service EXE."* Builds a `SERVICE_TABLE_ENTRYW` from every configured `ServiceManager` and calls `StartServiceCtrlDispatcherW`. Returns only when the OS terminates the service host. Raises run-time error 5 if the dispatcher cannot start (typically when the EXE was launched normally rather than by the SCM). +- `Sub InstallAll()` — *"This method tries to register ALL of the configured services onto the system."* Iterates the configured `ServiceManager`s and calls `.Install()` on each. Requires admin. +- `Sub UninstallAll()` — *"This method tries to unregister ALL of the configured services off the system."* Iterates and calls `.Uninstall()` on each. Requires admin. +- `Function QueryStateOfService(ByVal ServiceName As String) As ServiceState` — returns a fresh `ServiceState` snapshot. Raises run-time error 5 if the service isn't installed. +- `Sub LaunchService(ByVal ServiceName As String, ParamArray LaunchArgs())` — start an installed service by name, optionally passing launch arguments through to its `ServiceManager.LaunchArgs()` field. Wraps `OpenServiceW(SERVICE_START)` + `StartServiceW`. Raises run-time error 5 on permission / not-installed / already-running. +- `Sub ControlService(ByVal ServiceName As String, ByVal ControlCode As ServiceControlCodeConstants)` — send an SCM control code to a running service. The required SCM permission is derived from the control code automatically (`SERVICE_STOP` for `vbServiceControlStop`, `SERVICE_PAUSE_CONTINUE` for the pause / continue / netbind / paramchange family, `SERVICE_INTERROGATE` for `vbServiceControlInterrogate`, `SERVICE_USER_DEFINED_CONTROL` for codes 128–255, `SERVICE_ALL_ACCESS` otherwise). For `vbServiceControlStop` the wrapper fills `SERVICE_CONTROL_STATUS_REASON_PARAMSW` with `SERVICE_STOP_REASON_FLAG_PLANNED | MAJOR_NONE | MINOR_NONE` — there is a `FIXME` to allow customising the reason code. + +**Public properties**: + +- `Property Get GetConfiguredService(ByVal Name As String) As ServiceManager` — look up a previously-configured `ServiceManager` by its `Name`. Raises run-time error 5 if not found. (Despite the `Get` syntax the lookup is parameterised by name; it's a property in name only.) + +**Public enumerator**: + +- `Property Get _NewEnum() As Variant` — `[Enumerator]`-tagged; enables `For Each manager In Services` over the configured `ServiceManager`s. *"Provides For-Each support for the services collection, exposing each configured service as a ServiceManager instance."* + +## `ServiceManager` public members + +`[COMCreatable(False)]`. User code never instantiates this directly — `Services.ConfigureNew()` returns it. The source-side constructor carries `[Description("For internal use. Dont create instances of ServiceManager manually, use Services.ConfigureNew instead")]` — surface that on the page intro. + +**Public field** (one): + +- `LaunchArgs() As String` — populated by `ServiceEntryPoint` from the `argv` the SCM hands over. `LaunchArgs(0)` is the *first user-supplied* argument (the SCM-supplied service name at `argv[0]` is dropped). The example uses it to gate startup: `If Join(ServiceManager.LaunchArgs) <> "MySecretPassword" Then …`. + +**Public properties** (each carries a `[Description("...")]`): + +- `InstanceCreator As IServiceCreator` (Get / Let / Set) — *"Set this to an instance of the ServiceCreator class to allow the OS to launch the instance of your service."* Typically `.InstanceCreator = New ServiceCreator(Of MyServiceClass)`. +- `Name As String` (Get / Let) — *"The name of the service, as listed in the OS services database."* +- `Description As String` (Get / Let) — *"The description of the service, as listed in the OS services database."* Applied via `ChangeServiceConfig2W(SERVICE_CONFIG_DESCRIPTION)` on every successful `Install()`. +- `Type As ServiceTypeConstants` (Get / Let) — *"The type of the service, typically `tbServiceTypeOwnProcess` or `tbServiceTypeShareProcess`."* Defaults to `tbServiceTypeOwnProcess`. +- `InstallStartMode As ServiceStartConstants` (Get / Let) — *"The start-mode of the service, typically `tbServiceStartOnDemand` or `tbServiceStartAuto`."* Defaults to `tbServiceStartOnDemand`. +- `InstallCmdLine As String` (Get / Let) — *"The command line arguments passed to the service EXE when the OS launches the service."* Defaults to `""""""`. **Usually overridden to add a discriminator argument** like `-startService` so the EXE knows whether it was launched by the SCM (run dispatcher) or by a user (show UI). Example: `.InstallCmdLine = """" & App.ModulePath & """ -startService"`. +- `DependentServices() As Variant` (Get / Let) — *"A list of dependent services that this service requires to be started before this service is launched (dependent services are auto-launched by the OS)."* Pass an `Array("OtherSvc1", "OtherSvc2")`. The setter stashes it; `Install()` packs it into a double-null-terminated string and hands it to `CreateServiceW`. +- `AutoInitializeCOM As Boolean` (Get / Let) — *"When TRUE, COM will be initialized for you on the new service thread in STA mode."* Defaults to `True`. Set to `False` if your service needs a different apartment model (call `CoInitializeEx` yourself from `EntryPoint`). +- `SupportsPausing As Boolean` (Get / Let) — *"When TRUE, the SCM will send `SERVICE_CONTROL_PAUSE` / `SERVICE_CONTROL_CONTINUE` notifications."* Defaults to `False`. The setter calls `ResyncStatus()` so toggling it mid-run takes effect immediately. (Most services set this to `True` once inside `EntryPoint` and then handle `vbServiceControlPause` / `vbServiceControlContinue` in `ChangeState`.) + +**Public methods**: + +- `Sub Install()` — *"This method attempts to install the configured service on the system."* Opens the SCM with `SC_MANAGER_CONNECT Or SC_MANAGER_CREATE_SERVICE`, calls `CreateServiceW`. If the service already exists, deletes it (via `OpenServiceW(SERVICE_DELETE)` + `DeleteService`) and **retries** the create — so `Install()` is effectively re-entrant / safe to call multiple times. On successful create, sets the description via `ChangeServiceConfig2W`. Raises run-time error 5 on permissions failure or unrecoverable create failure. **Requires admin elevation.** +- `Sub Uninstall()` — *"This method attempts to uninstall the configured service on the system."* Opens the SCM, opens the service with `SERVICE_DELETE`, calls `DeleteService`. Raises run-time error 5 if the service isn't registered or on permissions failure. **Requires admin elevation.** +- `Sub ReportStatus(ByVal dwCurrentState As ServiceStatusConstants, Optional ByVal dwWin32ExitCode As Long = ERRORCODE_NO_ERROR, Optional ByVal dwWaitHint As Long = 0)` — *"This method informs the OS of the current state of the service."* The user's `EntryPoint` is **required** to call `ReportStatus(vbServiceStatusRunning)` once steady-state is reached and `ReportStatus(vbServiceStatusStopped)` once shut-down completes; long start-up sequences should also call `ReportStatus(vbServiceStatusStartPending, , )` periodically to keep the SCM from killing the service. The `dwControlsAccepted` field of `SERVICE_STATUS` is filled automatically from the state and from `SupportsPausing` (Stop is always accepted except during `StartPending`; Pause/Continue is gated on `SupportsPausing`). The `dwCheckPoint` field auto-increments for pending states and resets on `Running`/`Stopped`. +- `Sub ResyncStatus()` — re-applies the cached `SERVICE_STATUS` to the SCM via `SetServiceStatus`. Called automatically from `ReportStatus` and from the `SupportsPausing` setter. User code rarely needs to call this directly; mention it for completeness. + +The class also carries two methods that are technically `Public`-by-default (no modifier) but are invoked only by the OS dispatcher / the package's own trampoline — `ServiceEntryPoint(ByVal dwArgc As Long, ByVal lpszArgv As LongPtr)` and `ServiceControlHandlerCallback(ByVal dwControl As Long, ByVal dwEventType As Long, ByVal lpEventData As LongPtr)`. **Do not list these as user-facing methods**; mention them at the very end of the page under "Internal hooks" with a `> [!NOTE]` saying the OS / package infrastructure invokes them and user code never calls them. + +## `ServiceCreator(Of T)` public members + +Generic class. `[COMCreatable(False)]`. `[Description("This class allows the service manager to create an instance of a particular service on-demand as needed")]` is the source intro. Tagged with the EA magic-byte `[ClassId("66170220-FEF3-4257-8FBA-EAEAEAEAEAEA")]` — same compiler-special-handling treatment as `WinEventLogLib`'s `EventLog(Of T1, T2)`. Do not surface the `ClassId` on the page. + +Type parameter constraint: `T` must implement `ITbService`. There is no syntactic `Where T : ITbService` constraint expressed in the source, but `Function CreateInstance() As ITbService` returning `New T` only compiles when `T` implements `ITbService` — flag this as the practical constraint on the page. + +**Public method**: + +- `Function CreateInstance() As ITbService` — `Implements IServiceCreator.CreateInstance`. Returns `New T`. Called once per service start by the package's dispatcher trampoline. User code never calls this directly; the typical usage is `.InstanceCreator = New ServiceCreator(Of MyServiceClass)` on a freshly-allocated `ServiceManager`. + +The page should be small (the surface is one method) and largely focused on explaining the `Of T` parameterisation + the `T : ITbService` constraint + how it slots into `ServiceManager.InstanceCreator`. + +## `ServiceState` public members + +`[COMCreatable(False)]`. Returned by `Services.QueryStateOfService(Name)`. The constructor takes the service name, opens the SCM with `SC_MANAGER_CONNECT`, opens the service with `SERVICE_QUERY_STATUS`, calls `QueryServiceStatusEx(SC_STATUS_PROCESS_INFO, ...)`, and snapshots a `SERVICE_STATUS_PROCESS` struct. **The snapshot is taken once at construction time and never refreshed** — to see updated state, call `Services.QueryStateOfService` again. + +The constructor raises run-time error 5 with descriptive messages on three failure modes: SCM open failed (*"Unable to open the Service manager..."*), service not installed (*"Service '' is not installed on this system"*), status query failed (*"Unable to query the service state"*). + +**Public properties** (all read-only `Get`): + +- `Type As ServiceTypeConstants` — the SCM-reported service type. +- `CurrentState As Long` — the SCM-reported state, but typed `Long` rather than `ServiceStatusConstants`. **Source carries a `' FIXME` comment** — surface as a `> [!NOTE]` that this returns the underlying `Long` value (which happens to match the `ServiceStatusConstants` enum values), and that callers wanting type-safety can `CType(state.CurrentState, ServiceStatusConstants)`. +- `CurrentStateText As String` — human-readable text: `"RUNNING"`, `"STOPPED"`, `"STARTING"`, `"STOPPING"`, `"PAUSED"`, `"PAUSING"`, `"CONTINUING"`, `"UNKNOWN STATE ()"`. +- `ControlsAccepted As Long` — bitmask of `SERVICE_ACCEPT_*` flags. **Source carries a `' FIXME` comment** — surface the same way as `CurrentState`. +- `ExitCode As Long` — the `dwWin32ExitCode` field. The Win32 documented sentinel `ERROR_SERVICE_SPECIFIC_ERROR` (`1066`) means "see `ServiceSpecificExitCode`". +- `ServiceSpecificExitCode As Long` — the service-defined exit code when `ExitCode = ERROR_SERVICE_SPECIFIC_ERROR`. Otherwise meaningless. +- `CheckPoint As Long` — the `dwCheckPoint` field; increments while the service is in a pending state and resets at steady state. +- `WaitHint As Long` — the `dwWaitHint` milliseconds field. +- `ProcessId As Long` — the OS process ID hosting the service (0 if not running). +- `Flags As Long` — the `dwServiceFlags` field (currently `SERVICE_RUNS_IN_SYSTEM_PROCESS = 1` is the only documented bit). + +## `ITbService` public members + +`Public Interface`. Tagged `[InterfaceId("5F137E12-5164-452E-911A-6FD9BF20EC81")]`. Description: *"All services must implement `ITbService`."* The contract is three subs: + +- `Sub EntryPoint(ByVal ServiceContext As ServiceManager)` — the main service body. Called by the package's dispatcher trampoline once the SCM has finished start-up handshaking. **Runs on the service thread** (a separate thread from the dispatcher). Inside this sub the implementor: + 1. Optionally validates startup conditions (e.g. checks `ServiceContext.LaunchArgs`). + 2. Calls `ServiceContext.ReportStatus(vbServiceStatusRunning)` once steady-state is reached (the dispatcher trampoline reports `vbServiceStatusStartPending` automatically before calling `EntryPoint`). + 3. Runs the long-running work loop. For pipe-server services this is the `NamedPipeServer.ManualMessageLoopEnter()` blocking call; for other services it might be a `Do While IsStopping = False` loop with a wait primitive. + 4. Calls `ServiceContext.ReportStatus(vbServiceStatusStopped)` before returning. +- `Sub StartupFailed(ByVal ServiceContext As ServiceManager)` — called if `RegisterServiceCtrlHandlerExW` failed (the control handler couldn't be hooked, e.g. the service was launched outside the SCM context). Typical implementation: log a failure event. Don't try to `ReportStatus` from here — the status handle is invalid. +- `Sub ChangeState(ByVal ServiceContext As ServiceManager, ByVal dwControl As ServiceControlCodeConstants, ByVal dwEventType As Long, ByVal lpEventData As LongPtr)` — the control-code dispatcher. **Runs on the main (dispatcher) thread**, not on the service thread. Typical pattern: `Select Case dwControl` over `vbServiceControlStop` / `vbServiceControlShutdown` / `vbServiceControlPause` / `vbServiceControlContinue`, set shared `Public` flags (`IsStopping`, `IsPaused`), call `ServiceContext.ReportStatus` to acknowledge the transition, signal the service thread to react (e.g. `NamedPipeServer.ManualMessageLoopLeave()`). The `dwEventType` + `lpEventData` parameters carry the event-specific payload for the codes that need it (`SERVICE_CONTROL_DEVICEEVENT`, `SERVICE_CONTROL_POWEREVENT`, `SERVICE_CONTROL_SESSIONCHANGE`, `SERVICE_CONTROL_HARDWAREPROFILECHANGE` — see Microsoft's `HandlerEx` documentation for the data layouts). + +**The two-thread split is the single most important fact about the interface** — every page entry should reinforce it. The example uses `Public IsPaused As Boolean` + `Public IsStopping As Boolean` shared fields on the service class to ferry state between the two threads, which is the documented pattern. + +## Enumerations + +Public enums (in `Public Module ServicesConstantsPublic`), one page each under `docs/Reference/WinServicesLib/Enumerations/`: + +- `ServiceTypeConstants` — `tbServiceTypeAdapter`, `tbServiceTypeSystemDriver`, `tbServiceTypeKernelDriver`, `tbServiceTypeRecognizerDriver`, `tbServiceTypeOwnProcess`, `tbServiceTypeShareProcess`, `tbServiceTypeOwnProcessInteractive`, `tbServiceTypeShareProcessInteractive`. The driver values (`tbServiceTypeSystemDriver`, `tbServiceTypeKernelDriver`, `tbServiceTypeRecognizerDriver`, `tbServiceTypeAdapter`) are only meaningful when registering a kernel-mode driver — twinBASIC services compile to a user-mode EXE and should use `tbServiceTypeOwnProcess` (one service per EXE) or `tbServiceTypeShareProcess` (multiple services hosted in one EXE; the example uses this). The `Interactive` variants are kept for compatibility but Windows Vista and later disallow them; flag with a `> [!NOTE]`. +- `ServiceStartConstants` — `tbServiceStartAuto`, `tbServiceStartBoot`, `tbServiceStartOnDemand`, `tbServiceStartDisabled`, `tbServiceStartDriverSystem`. `tbServiceStartBoot` and `tbServiceStartDriverSystem` only apply to kernel drivers. +- `ServiceControlCodeConstants` — 18 values mirroring the Win32 `SERVICE_CONTROL_*` constants. Source-side prefix is `vbServiceControl*` (carried over from VB6 — note the prefix is `vb`, not `tb`, in this enum; surface as-is, don't try to rationalise). +- `ServiceStatusConstants` — `vbServiceStatusStopped`, `vbServiceStatusStartPending`, `vbServiceStatusStopPending`, `vbServiceStatusRunning`, `vbServiceStatusContinuePending`, `vbServiceStatusPausePending`, `vbServiceStatusPaused`. Same `vb` prefix. + +Format pages like `WebView2/Enumerations/wv2PrintOrientation.md` — single intro paragraph, a value table with `{: #vbServiceXxx }` anchors per row for deep-linking. + +The package's `index.md` walks the reader through: (1) what a Windows service is; (2) the lifecycle — configure (`Services.ConfigureNew`) → install (elevated) → run (`Services.RunServiceDispatcher` blocks the main thread, SCM launches the service thread on demand); (3) the two-thread split between `EntryPoint` and `ChangeState`; (4) integration cross-links to `WinEventLogLib` (composition-delegation idiom) and `WinNamedPipesLib` (the `ManualMessageLoopEnter` / `Leave` service-hosting idiom). diff --git a/WIP.md b/WIP.md index 0c6c59e..3f581cd 100644 --- a/WIP.md +++ b/WIP.md @@ -4,25 +4,24 @@ Jekyll site (`just-the-docs` theme) deploying to `docs.twinbasic.com`. Source un ## Status -Reference documentation is **complete**. All eleven packages have full reference coverage adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages); the CEF and WebView2 packages also carry a tutorial set. - -| Package | Reference | Tutorials | -|--------------------------------------|-----------|-----------| -| VBA package | done | — | -| VBRUN package | done | — | -| VB package | done | — | -| WebView2Package | done | done | -| Assert package | done | — | -| CustomControls / CustomControlsPackage | done | — | -| cefPackage (CEF) | done | done | -| WinEventLogLib | done | — | -| WinNamedPipesLib | done | — | -| WinServicesLib | done | — | -| tbIDE | done | — | - -The rest of this file is the maintenance guide for adding new pages or updating existing ones — primary-source paths, page templates, cross-section linking conventions, the per-symbol workflow, and the integrity check. - -When working from a primary source: always read it first — **never paraphrase from memory.** +Reference documentation is **complete** for all twelve packages, adapted from primary sources (Microsoft VBA-Docs CC-BY-4.0 for the runtime library, `.twin` source for the twinBASIC-specific packages). The CEF and WebView2 packages also carry a tutorial set. + +| Package | Reference | Tutorials | +|--------------------------------------|-------------|-----------| +| VBA package | done | — | +| VBRUN package | done | — | +| VB package | done | — | +| WebView2Package | done | done | +| Assert package | done | — | +| CustomControls / CustomControlsPackage | done | — | +| cefPackage (CEF) | done | done | +| WinEventLogLib | done | — | +| WinNamedPipesLib | done | — | +| WinServicesLib | done | — | +| tbIDE | done | — | +| WinNativeCommonCtls | done | — | + +The rest of this file is the maintenance guide for updating existing pages or adding new ones — high-level package surface notes, page templates, cross-section linking conventions, and the integrity check. ## Where things live @@ -37,1205 +36,27 @@ When working from a primary source: always read it first — **never paraphrase - `docs/Reference/WinEventLogLib/` — Windows Event Log package: the generic `EventLog(Of T1, T2)` class and the `EventLogHelperPublic` module with its single `RegisterEventLogInternal` helper. Three pages total — `index.md`, `EventLog.md`, `EventLogHelperPublic.md`. - `docs/Reference/WinNamedPipesLib/` — Windows Named Pipes package: the IOCP-based async pipe framework — `NamedPipeServer` + `NamedPipeServerConnection` on the server side, `NamedPipeClientManager` + `NamedPipeClientConnection` on the client side. Five pages total (`index.md` + one per class). - `docs/Reference/WinServicesLib/` — Windows Services package: a thin OS-services wrapper. `Services` (predeclared singleton) coordinates one or more `ServiceManager` configurations; `ServiceCreator(Of T)` is the generic factory the dispatcher uses to instantiate each user-defined `ITbService` class; `ServiceState` is a read-only state snapshot for an installed service. Four public enums (`ServiceTypeConstants`, `ServiceStartConstants`, `ServiceControlCodeConstants`, `ServiceStatusConstants`) live under `Enumerations/`. +- `docs/Reference/WinNativeCommonCtls/` — Windows Native Common Controls compatibility package: a VB6-compatible Microsoft Common Controls 6.0 (`MSCOMCTL.OCX`) replacement, written on top of the Win32 ComCtl32 controls. Eight controls (**DTPicker**, **ImageList**, **ListView**, **MonthView**, **ProgressBar**, **Slider**, **TreeView**, **UpDown**), plus eight sub-object classes (**ListImages** / **ListImage**, **ListItems** / **ListItem**, **ColumnHeaders** / **ColumnHeader**, **Nodes** / **Node**) reached through container properties on the three collection-bearing controls, plus ~16 user-facing enumerations. Each control is a `BaseCtl` (`[COMCreatable(False)]`) plus a thin `` leaf tagged `[WindowsControl(...)]` — the same split VB-package and CEF use. - `docs/Reference/tbIDE/` — IDE Extensibility package (this is the **addin SDK**). The package is type-only — it ships **public interfaces + CoClasses** that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (`tbCreateCompilerAddin`) plus ~20 CoClasses grouped by role: the addin contract (`AddIn`), the root API (`Host`), the loaded `Project`, the editors collection (`Editor` / `CodeEditor` / `Editors`), the virtual file system (`FileSystem` / `FileSystemItem` / `Folder` / `File`), the in-IDE UI surface (`Toolbar` / `Toolbars` / `Button` / `ToolWindow` / `ToolWindows`), the HTML DOM inside a tool window (`HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` / `HtmlEventProperty` / `HtmlEventProperties`), the `DebugConsole`, `KeyboardShortcuts`, `Themes`, and the single concrete user-instantiable helper class `AddinTimer`. Flat layout — one page per CoClass / Class plus the index landing. - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - `docs/_includes/footer_custom.html` — overrides the theme's footer slot; renders the copyright line and, when `vba_attribution: true` is set in a page's frontmatter, an additional CC-BY-4.0 attribution line beneath it. -## VBA-Docs source (read-only) - -Cloned as a sibling of this repo. All paths below are relative to the repo root: - -``` -../VBA-Docs/Language/Reference/User-Interface-Help/-.md -``` - -Common kinds: `-statement`, `-function`, `-property`, `-method`, `-object`, `-operator`. Find the file with `ls ../VBA-Docs/Language/Reference/User-Interface-Help/ | grep -i ` before drafting. - -Used for: Core statements/keywords, the VBA package, and the VBRUN package. - -## TwinBASIC Package source (read-only) - -All of twinbasic's package sources are at: - -``` -..\tb-export\NewProject\Packages\VB\Sources\CONTROLS\STANDARD\.twin -..\tb-export\NewProject\Packages\VB\Sources\CONTROLS\OTHER\.twin -..\tb-export\NewProject\Packages\VB\Sources\BASE\Base*.twin -..\tb-export\NewProject\Packages\VBA\Sources\ -..\tb-export\NewProject\Packages\VBRUN\Sources\ -..\tb-export\NewProject\Packages\WebView2Package\Sources\ -..\tb-export\NewProject\Packages\Assert\Sources\ -..\tb-export\NewProject\Packages\CustomControls\Sources\ -..\tb-export\NewProject\Packages\CustomControlsPackage\Sources\ -..\tb-export\NewProject\Packages\cefPackages\Sources\ -..\tb-export\NewProject\Packages\WinEventLogLib\Sources\ -..\tb-export\NewProject\Packages\WinNamedPipesLib\Sources\ -..\tb-export\NewProject\Packages\WinServicesLib\Sources\ -etc. -``` - -For the **tbIDE** package, the sources are not in `..\tb-export\`. They live inside one of the six addin sample projects (any will do — the package is a binary-only compiler package and ships its `.twin` declarations alongside each sample). Use sample10's copy as the canonical path: - -``` -..\tbrepro\sample10\WaynesWorldAddInTest1\Packages\tbIDE\Sources\ ← 24 flat .twin files -..\tbrepro\sample10\WaynesWorldAddInTest1\Packages\tbIDE\LICENCE.md ← MIT, Wayne Phillips, 2022 -..\tbrepro\sample10\WaynesWorldAddInTest1\Packages\tbIDE\Settings ← project.name = "tbIDE", buildType = Package TWINPACK -``` - -The six matching consumer-side example addins live at: - -``` -..\tbrepro\sample10\WaynesWorldAddInTest1\Sources\MainModule.twin ← kitchen-sink: toolbar / toolwindow / DOM / events / Evaluate / ActiveEditors -..\tbrepro\sample11\WaynesWorldCPUMonitorTest1\Sources\MainModule.twin ← AddinTimer + chartjs custom-element + onClose cleanup -..\tbrepro\sample12\WaynesWorldMonacoEditorTest1\Sources\MainModule.twin ← monaco custom-element + .editor.AddEventListener -..\tbrepro\sample13\WaynesListViewAddIn\Sources\MainModule.twin ← listview custom-element + ApplyCss + raiseEvent() from inline HTML -..\tbrepro\sample14\WaynesVirtualListViewAddIn\Sources\MainModule.twin ← virtuallistview + onAsyncGetItemHTML + setAsyncResult + notifyChangedItem -..\tbrepro\sample15\tbGlobalSearchAddIn1\Sources\MainModule.twin ← real-world: FS traversal + ReadText + ActiveEditors.Open + persistent settings -``` - -Read them in roughly that order — sample10 introduces the addin idioms one by one, samples 11–14 each focus on a single advanced custom-element widget (chartjs / monaco / listview / virtuallistview), and sample15 is a complete, polished addin that exercises the file system + editor-navigation surface. - -For the CEF package, the examples live in a different folder: - -``` -..\tbrepro\cef\CEFSampleProject\Sources\ ← four worked examples + MainForm -``` - -For the WinServicesLib package — and the canonical integration story across **all three** "winlibs" packages (services + event log + named pipes wired together end-to-end) — the worked example lives at: - -``` -..\tbrepro\winlibs\tbServiceTest2\Sources\ - Startup.twin ← Sub Main: configures two services + dispatches - SERVICES\TBSERVICE001.twin, TBSERVICE002.twin ← user-implemented ITbService classes - FORMS\MainForm.twin ← non-service mode: control-panel UI - FORMS\InProcessNamedPipeServerForm.twin ← in-process pipe server (no service) - MISC\MESSAGETABLE.twin ← [PopulateFrom("json",...)] enums for the event log -..\tbrepro\winlibs\tbServiceTest2\Resources\MESSAGETABLE\Strings.json ← message-table backing JSON -``` - -Read this project end-to-end before extending the docs for any of WinServicesLib, WinEventLogLib, or WinNamedPipesLib — the three packages share a load-bearing set of idioms (composition-delegation on `EventLog(Of T1, T2)`, the manual-message-loop pattern coupling `NamedPipeServer` to a service's `ChangeState` handler, `PropertyBag` as the canonical pipe payload) that only become visible when you see them used together. - -### VB Controls - -The `STANDARD/` folder holds the leaf control classes. The `BASE/` folder defines the inheritance chain (e.g. `BaseControlWindowlessNoFocus` → `BaseControlRectDockable` → `BaseControlRect` → `BaseControl`); read those alongside the leaf class to know which `Public` members are actually visible. Members marked `Protected` or hidden behind `[Unimplemented]` should be flagged with a `> [!NOTE]` callout. - -These pages are fully original content — **omit** the `vba_attribution: true` frontmatter flag. - -### WebView2Package - -Layout of `..\tb-export\NewProject\Packages\WebView2Package\Sources\`: - -- `Classes/` — the implementation classes. Only a few are part of the user-facing surface; the rest are `Private` plumbing. -- `Abstract/` — raw `ICoreWebView2*` COM interfaces. Every one is declared `Private Interface`; these are pure implementation detail and get **no documentation page**. -- `Support/Enumerations.twin` — the `wv2…` enumerations, all in the `WebViewEnums` module. -- `Support/Types.twin` — the `WebViewTypes` module; currently only `COREWEBVIEW2_PHYSICAL_KEY_STATUS`. -- `Support/WebView2Misc.twin` — `Private Module`; helpers, no public surface. -- `EventCallbacks/` — currently empty. - -Public user-facing surface (from `grep '^Public Class' Classes/*.twin` plus the top-level `WebView2` class which has no explicit modifier): - -| Class | Role | -|-----------------------------|-------------------------------------------------------------------------------------------------------| -| `WebView2` | the control itself (`Inherits VB.BaseControlFocusableNoFont`, `[WindowsControl(...)]`) | -| `WebView2Header` | one HTTP header (Name / Value); value type returned by header iteration | -| `WebView2HeadersCollection` | enumerable wrapper used by `For Each` over request / response headers | -| `WebView2Request` | request side of a `WebResourceRequested` event — Method, Uri, Headers, ContentBytes, ContentUTF8 | -| `WebView2RequestHeaders` | mutable request-header collection — `GetHeader`, `Contains`, `AppendHeader`, `RemoveHeader`, … | -| `WebView2Response` | response side of a `WebResourceRequested` event — StatusCode, ReasonPhrase, Headers, ContentBytes… | -| `WebView2ResponseHeaders` | mutable response-header collection | - -`WebView2EnvironmentOptions` is declared `Private Class`, **but** the `WebView2` control exposes it via `Public EnvironmentOptions As WebView2EnvironmentOptions = New WebView2EnvironmentOptions`. Document it as a sub-page of the `WebView2` control class — its `Public` fields (`BrowserExecutableFolder`, `UserDataFolder`, `AdditionalBrowserArguments`, `Language`, `TargetCompatibleBrowserVersion`, `AllowSingleSignOnUsingOSPrimaryAccount`, `ExclusiveUserDataFolderAccess`, `EnableTrackingPrevention`) are user-set before / during the `Create` event. - -The other `Private Class` files (`WebView2DeferredCallback`, `WebView2DeferredRaiseEvent`, `WebView2DevToolsProtocolCallback`, `WebView2ExecuteScriptCompleteHandler`, `WebView2ExecuteScriptCompleteHandler2`) and their helper interfaces (`IDeferredCallback`, `IExecuteScriptCompleteCallback`) are deferral / callback plumbing — skip. - -The `WebView2` class itself is large (~1450 lines) and exposes Properties / Methods / Events plus the `EnvironmentOptions` member. Use the **folder-style** layout (`WebView2/index.md`) like `CheckBox/index.md` so the page can carry a TOC and the optional sub-pages (`WebView2/EnvironmentOptions.md`, etc.) sit beside it. - -Enumerations live in `Support/Enumerations.twin` (module `WebViewEnums`) and currently number ten: `wv2PermissionKind`, `wv2PermissionState`, `wv2ErrorStatus`, `wv2KeyEventKind`, `wv2WebResourceContext`, `wv2ProcessFailedKind`, `wv2ScriptDialogKind`, `wv2HostResourceAccessKind`, `wv2PrintOrientation`, `wv2DefaultDownloadCornerAlign`. Group them under `WebView2/Enumerations/` following the VBRUN `Constants/` precedent — one page per enum, with `AlignConstants.md` as the formatting model. - -`COREWEBVIEW2_PHYSICAL_KEY_STATUS` is a public `Type` in the `WebViewTypes` module; it surfaces through the `AcceleratorKeyPressed` event arguments. One page (`WebView2/Types/COREWEBVIEW2_PHYSICAL_KEY_STATUS.md`) is enough. - -The package is licensed **MIT** (copyright Wayne Phillips T/A iTech Masters, 2022) — independent of the CC-BY-4.0 VBA-Docs sources. These pages are fully original content; **omit** the `vba_attribution: true` flag, the same as VB-package pages. - -**Naming:** the source-side package symbol is `WebView2Package` (and the source folder is `..\tB-export\NewProject\Packages\WebView2Package\`), but the doc folder, URL segment, and page title all drop the doubled "Package" suffix — folder `docs/Reference/WebView2/`, permalink `/tB/Packages/WebView2/`, title `WebView2 Package` (space-separated, the same ` Package` convention VB / VBRUN / Assert use). Every child page sets `parent: WebView2 Package` (matching the title, not the URL segment). - -Pre-existing `WebView2` references on the site to keep aligned: - -- [`docs/Tutorials/WebView2/`](docs/Tutorials/WebView2) — task-oriented tutorial; new reference pages should cross-link to it where useful, and vice versa. -- [`docs/Reference/VBRUN/Constants/ControlTypeConstants.md`](docs/Reference/VBRUN/Constants/ControlTypeConstants.md) — already lists `vbWebView2 = 18`; the new `WebView2` reference page should link to that constant. - -### Assert - -Layout of `..\tb-export\NewProject\Packages\Assert\Sources\` is flat — three sibling files, no sub-folders, no `Abstract\` / `Support\` plumbing: - -- `Exact.twin` — module **Exact** -- `Strict.twin` — module **Strict** -- `Permissive.twin` — module **Permissive** - -All three modules expose the **same 15-member API**; only the comparison semantics differ. The differentiating semantics are spelled out in the module-level `[Description("...")]` block at the top of each `.twin`: - -| Module | String comparisons | Other comparisons | -|---------------|--------------------|--------------------------------------------------------------------------------| -| `Exact` | case-sensitive | no implicit conversions; datatypes must match exactly (`5 <> 5.0`); `vbNullString <> ""`; `Empty` distinct from `0` / `False` / `""`; object default members **not** evaluated | -| `Strict` | case-sensitive | evaluated as if written directly in twinBASIC; object default members **not** evaluated | -| `Permissive` | case-insensitive | evaluated as if written directly in twinBASIC | - -`Null` is never equal to anything (not even itself) under any of the three flavours — use `IsNull` / `IsNotNull` to test for it. - -The 15 members per module (identical signatures across all three): - -| Member | Purpose | -|-------------------------------------------------|------------------------------------------------------------------| -| `Succeed()` | unconditionally records a pass | -| `Fail([Message])` | unconditionally records a failure | -| `Inconclusive([Message])` | records an inconclusive / skipped result | -| `AreEqual(Expected, Actual, [Message])` | value-equality assertion | -| `AreNotEqual(Expected, Actual, [Message])` | inverse of `AreEqual` | -| `AreSame(Expected, Actual, [Message])` | reference-identity (`Is`) assertion for objects | -| `AreNotSame(Expected, Actual, [Message])` | inverse of `AreSame` | -| `IsTrue(Condition, [Message])` | asserts the condition is `True` | -| `IsFalse(Condition, [Message])` | asserts the condition is `False` | -| `IsNothing(Value, [Message])` | asserts the object reference is `Nothing` | -| `IsNotNothing(Value, [Message])` | inverse of `IsNothing` | -| `IsNull(Value, [Message])` | asserts the value is `Null` | -| `IsNotNull(Value, [Message])` | inverse of `IsNull` | -| `SequenceEquals(Expected, Actual, [FailMessage])` | element-by-element comparison of two sequences / arrays | -| `NotSequenceEquals(Expected, Actual, [FailMessage])` | inverse of `SequenceEquals` | - -Source-side every member is declared as `Public DeclareWide PtrSafe Sub Lib "" Alias "#N" (...)` and tagged `[DebugOnly(True), MustBeQualified(True), PreserveSig(False), UseGetLastError(False)]`. The `Lib` / `Alias` / `PreserveSig` / `UseGetLastError` decoration is internal pseudo-DLL plumbing for the runtime — **do not** surface it on the doc pages. The `[DebugOnly(True)]` tag matters to users (assertions compile out of release builds) and **should** be called out. The `[MustBeQualified(True)]` tag means callers must write the module name, e.g. `Strict.IsTrue(x)`. - -The module-level `[Description("…")]` is the only non-empty description on the source side — the per-member `[Description("")]` blocks are all empty placeholders, so member descriptions are fully original prose. - -**Layout decision** — deviation from the per-symbol pattern used elsewhere: - -Because the three modules share an *identical* API, replicating 15 member-pages × 3 modules = 45 near-duplicate pages would add noise without value. Use **one page per module** instead, listing all 15 members inline under `## ` headings (deep-linkable as `…/Strict#areequal`). The package landing page collects the three module pages and shows the semantics comparison table. - -So the layout is: - -- `docs/Reference/Assert/index.md` — package landing page; the three modules + side-by-side semantics table + a one-paragraph "what is this for" intro pointing at unit testing -- `docs/Reference/Assert/Exact.md` — single-file module page, all 15 members -- `docs/Reference/Assert/Strict.md` — same shape -- `docs/Reference/Assert/Permissive.md` — same shape - -That's 4 pages total. (If a future release of the package adds more modules or non-module classes, revisit.) - -**Naming:** - -- Folder / URL segment: `Assert/` (the package name is `Assert` per `Settings` → `project.name`; no doubled "Package" awkwardness like WebView2Package had). -- Index title: `Assert Package` — same ` Package` convention as VB / VBRUN / WebView2. -- Module page title: `` (just `Exact`, `Strict`, `Permissive` — they're modules, not classes, and "Module" is implied by context). -- Permalinks: `/tB/Packages/Assert/` for the index, `/tB/Packages/Assert/` for each module page. -- `parent: Assert Package` on each module page (matching the index `title:`, the same split VB / VBRUN / WebView2 use). - -**License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2022) — same situation as WebView2Package. Pages are fully original; **omit** the `vba_attribution: true` flag. - -### CustomControls / CustomControlsPackage - -Two source-side packages, **one** doc-side package. They always ship together and are co-versioned with twinBASIC — `CustomControlsPackage` lists `CustomControls` as a `isCompilerPackage` reference in its `Settings`, and neither is usable without the other. Document the union as `docs/Reference/CustomControls/`. - -The split on the source side is by *role*: - -- `..\tb-export\NewProject\Packages\CustomControls\Sources\CustomControls.twin` — the **DESIGNER** framework. A single file with `Module Constants` (the enums + the `SerializeInfo` / `Canvas` UDTs) plus the abstract surface a custom control hooks into (`ICustomControl`, `ICustomForm` interfaces; `CustomControlContext`, `CustomFormContext`, `CustomControlTimer`, `CustomControlsCollection` CoClasses). Project `appTitle` is `"CustomControls DESIGNER Package"`. -- `..\tb-export\NewProject\Packages\CustomControlsPackage\Sources\` — the **runtime**: eight concrete `Waynes…` controls plus `zTemporarySupport.twin`, a bag of shared appearance helpers and three mixin base classes. Project `appTitle` is `"Custom Controls Package"`. - -Public user-facing surface, grouped by role. - -#### Concrete controls (`CustomControlsPackage\Sources\Waynes*.twin`) - -Each is `Class ` (no `Public` modifier — implicitly public), tagged `[CustomControl("/miscellaneous/frm.png")]` (designer icon) and `[COMCreatable(False)]` (cannot be `New`'d through COM; instantiated by the designer). - -| Control | Implements | Co-located public types | -|------------------|-----------------------------------------------------------|--------------------------------------------------| -| `WaynesButton` | `ICustomControl` + `BaseControlFocusable` (mixin) | `WaynesButtonState` (private, but exposed) | -| `WaynesForm` | `ICustomControl` + `BaseForm` (mixin) | — (uses `WindowsFormOptions` from support file) | -| `WaynesFrame` | `ICustomControl` + `BaseControl` (mixin) | — | -| `WaynesGrid` | `ICustomControl` + `BaseControlFocusable` (mixin) | `Column`, `CellRenderingOptions` | -| `WaynesLabel` | `ICustomControl` + `BaseControl` (mixin) | — | -| `WaynesSlider` | `ICustomControl` + `BaseControlFocusable` (mixin) | `WaynesSliderState`, `SliderDirection` & `SliderDisplayValueFormat` (nested enums) | -| `WaynesTextBox` | `ICustomControl` + `BaseControlFocusable` (mixin) | `WaynesTextBoxState` | -| `WaynesTimer` | `ICustomControl` + `BaseControl` (mixin) | — | - -The "mixin" base classes (`BaseControl`, `BaseControlFocusable`, `BaseForm`) are declared `Private Class` in `zTemporarySupport.twin` and pulled into each control via the twinBASIC `Implements Via _BaseControl = New ` syntax. The base classes themselves get **no doc page** (they're private and never named by user code), but the inherited members **must be folded into each control's Properties listing** the same way VB-package controls list their inherited surface. The visible inherited surface, by mixin: - -- `BaseControl` → `Name`, `Left`, `Top`, `Width`, `Height`, `Anchors`, `Dock`, `Visible`. -- `BaseControlFocusable` → all of `BaseControl` + `TabIndex`, `TabStop`. -- `BaseForm` → `FormDesignerId`, `Name`, `Left`, `Top`, `Width`, `Height`, `Controls`. - -The state-holder classes (`WaynesButtonState`, `WaynesSliderState`, `WaynesTextBoxState`) and `WindowsFormOptions` are declared `Private Class` but are exposed on the parent control via `Public WithEvents NormalState As WaynesButtonState` (etc.). Same situation as `WebView2EnvironmentOptions` — document them as **sub-pages** of the parent control using the folder-style layout. - -#### Shared appearance helpers (`CustomControlsPackage\Sources\zTemporarySupport.twin`) - -Every helper in this file is `Private Class`, but the ones below are reachable through `Public WithEvents …` properties on one or more of the eight controls and **must be documented**: - -| Class | Reached as | -|-----------------|-------------------------------------------------------------------------------------| -| `Anchors` | `.Anchors` (via the mixin base) | -| `Corners` | `.Corners`, `CellRenderingOptions.Corners`, `.BackgroundCorners`, `BlockCorners` | -| `Corner` | `Corners.TopLeft` / `.TopRight` / `.BottomLeft` / `.BottomRight` | -| `Borders` | `.Borders`, `CellRenderingOptions.Borders`, `.BackgroundBorders`, `BlockBorders` | -| `Border` | element of `Borders.Elements()`; also `TextRendering.Outlines()` | -| `Fill` | `.BackgroundFill`, `.BlockFill`, `CellRenderingOptions.Fill`, `Border.Fill`, `Line.Fill`, `TextRendering.Fill` | -| `FillColorPoint` | element of `FillColorPoints.Values()` | -| `FillColorPoints` | `Fill.ColorPoints` | -| `Line` | `WaynesGrid.VerticalLineOptions` / `.HorizontalLineOptions` / `.ResizerBar` | -| `Padding` | `TextRendering.Padding` | -| `TextRendering` | `.TextRendering`, `WaynesLabel.TextRendering`, `CellRenderingOptions.TextRendering` | -| `FontStyle` | `TextRendering.Font` | -| `WindowsFormOptions` | `WaynesForm.WindowsOptions` (only one consumer) | - -Document these under `docs/Reference/CustomControls/Styles/`. Pair small helpers with their containers on a single page (the pairings happen to be self-evident from the table — `Corner` inlines under `Corners.md`, `Border` under `Borders.md`, `FillColorPoint` and `FillColorPoints` under `Fill.md`, `FontStyle` under `TextRendering.md`). `WindowsFormOptions` is the exception: it has exactly one consumer (`WaynesForm`), so put it as a sub-page of `WaynesForm/` (folder-style), parallel to how WebView2 carries `EnvironmentOptions`. - -The remaining `Private Class` / `Private Module` content in `zTemporarySupport.twin` is implementation-detail and gets **no doc page**: - -- `TextDecorator`, `TextDecorators` — used only inside `ElementDescriptor`, not surfaced on any control property. -- `UDTs` — a wrapper class whose `Public Type` declarations (`MouseEvent`, `KeyEvent`, `FocusEvent`, `ElementDescriptor`) and `Public Enum` declarations (`CaretPosition`, `SpecialKeyCodes`) only matter to someone writing a *new* custom control (they're passed to `AddressOf`-registered callbacks on `ElementDescriptor`). Defer documenting these until / unless an "authoring a custom control" tutorial calls for them. -- `BaseControl`, `BaseControlFocusable`, `BaseForm` — the mixin bases (covered above; members surface on each control, but the bases themselves are private). -- `Private Module MathSupport` / `Private Module ColorSupport` — internal. - -#### DESIGNER framework surface (`CustomControls\Sources\CustomControls.twin`) - -The framework half — what a *control author* writes against. Document at `docs/Reference/CustomControls/Framework/`: - -| Symbol | Kind | Role | -|------------------------------|---------------------|---------------------------------------------------------------------------------------| -| `ICustomControl` | `Interface` | what every concrete control implements: `Initialize(Context)`, `Destroy()`, `Paint(Canvas)` | -| `ICustomForm` | `Interface` | analogous surface for form-class custom controls | -| `CustomControlContext` | `CoClass` | passed to `ICustomControl.Initialize`; offers `GetSerializer()`, `Repaint()`, `CreateTimer()`, `ChangeFocusedElement()` | -| `CustomFormContext` | `CoClass` | extends `CustomControlContext` with `Show()` / `Close()` | -| `CustomControlTimer` | `CoClass` | returned by `CustomControlContext.CreateTimer()`; `Interval`, `Enabled`, `OnTimer` event | -| `CustomControlsCollection` | `CoClass` | the `Controls` collection on a form — `Count`, `Item`, `Add`, `Remove`, `_NewEnum` | -| `SerializeInfo` | UDT | obtained from `Context.GetSerializer()`; exposes `RuntimeUISrz*` operations (deserialize, mode flags, …) | -| `Canvas` | UDT | parameter to `ICustomControl.Paint`; exposes `RuntimeUICCCanvasAddElement` + DPI / size getters | - -Both UDTs follow a pattern unique to twinBASIC: a `Pointer As LongPtr` field plus `Public DeclareWide PtrSafe Function/Sub … Lib "" Alias "#N"` pseudo-DLL declarations bound directly into the type. From a *caller* perspective these read as instance methods on the UDT (`Canvas.RuntimeUICCCanvasAddElement(descriptor)`); document them as methods, and **do not** surface the `Lib "<…>"` / `Alias "#N"` / `PreserveSig` / `DLLStackCheck` decoration (same treatment as Assert's pseudo-DLL plumbing). The verbose `RuntimeUISrz*` / `RuntimeUICC*` names are unfortunate but they *are* the public API — keep them as-is. - -The two underscore-prefixed default interfaces of each CoClass (`_CustomControlTimer`, `_CustomControlContext`, `_CustomFormContext`, `_CustomControlsCollection`, `_CustomControlTimerEvents`) are an implementation detail of the COM `[Default]`/`[Default, Source]` pattern — fold their members onto the CoClass page, **don't** give the interfaces their own pages. - -#### Enumerations (`CustomControls.twin`, module `Constants`) - -Public enums to surface, one page each, under `docs/Reference/CustomControls/Enumerations/`: - -- `CornerShape`, `FillPattern`, `TextAlignment`, `TextOverflowMode`, `DockMode`, `FontWeight`, `StartupPosition`, `BorderStyle`, `WindowState` — straightforward value enums. -- `Customtate` — **probable typo** for `CustomState`. Has the same three members as `WindowState` (`tbNormal` / `tbMinimized` / `tbMaximized`) and isn't referenced anywhere else in the package. Document it (since it's `Public`), but add a `> [!NOTE]` callout flagging the typo and pointing readers to `WindowState`. -- `ColorRGBA`, `PixelCount`, `PointSize` — these are declared as `Enum` only because twinBASIC doesn't yet have a `Type Foo = Long` alias syntax. Each carries a `FIXME` comment ("Substitute for an ALIAS to Long") and a single `[_MAX] = 0` placeholder member. Document them as **typedefs for `Long`** (the underlying storage type), not as real enums. Note in each that the alias is what user code actually sees on `Public Width As CustomControls.PixelCount` (etc.) — when the alias syntax lands, these enum stand-ins go away. - -Plus the two enums nested inside `WaynesSlider`: `SliderDirection` and `SliderDisplayValueFormat`. Document them on the `WaynesSlider/index.md` page rather than under `Enumerations/` (they're locally scoped to the slider). - -#### Doc-side layout (folders / files) - -Compact form: - -``` -docs/Reference/CustomControls/ - index.md ← package landing; intro + role split + cross-links to the four groups - WaynesButton/index.md, WaynesButton/WaynesButtonState.md - WaynesForm/index.md, WaynesForm/WindowsFormOptions.md - WaynesFrame.md - WaynesGrid/index.md, WaynesGrid/Column.md, WaynesGrid/CellRenderingOptions.md - WaynesLabel.md - WaynesSlider/index.md, WaynesSlider/WaynesSliderState.md - WaynesTextBox/index.md, WaynesTextBox/WaynesTextBoxState.md - WaynesTimer.md - Styles/index.md, Styles/Anchors.md, Styles/Borders.md, Styles/Corners.md, - Styles/Fill.md, Styles/Line.md, Styles/Padding.md, Styles/TextRendering.md - Framework/index.md, Framework/Canvas.md, Framework/CustomControlContext.md, - Framework/CustomControlsCollection.md, Framework/CustomControlTimer.md, - Framework/CustomFormContext.md, Framework/ICustomControl.md, - Framework/ICustomForm.md, Framework/SerializeInfo.md - Enumerations/index.md, Enumerations/BorderStyle.md, Enumerations/ColorRGBA.md, - Enumerations/CornerShape.md, Enumerations/Customtate.md, Enumerations/DockMode.md, - Enumerations/FillPattern.md, Enumerations/FontWeight.md, Enumerations/PixelCount.md, - Enumerations/PointSize.md, Enumerations/StartupPosition.md, Enumerations/TextAlignment.md, - Enumerations/TextOverflowMode.md, Enumerations/WindowState.md -``` - -**Naming:** - -- Folder / URL segment: `CustomControls/` (drops the "Package" suffix; collapses the two source packages, same simplification WebView2 used). -- Index title: `CustomControls Package` — the ` Package` convention. -- Permalinks: `/tB/Packages/CustomControls/` for the landing; `/tB/Packages/CustomControls/` and `/tB/Packages/CustomControls//` for single-file vs folder-style controls; `/tB/Packages/CustomControls/Styles/`, `/tB/Packages/CustomControls/Framework/`, `/tB/Packages/CustomControls/Enumerations/` for the three sub-groups. -- `parent: CustomControls Package` on every child page (matching the index `title:`). -- The `Styles/`, `Framework/`, `Enumerations/` index pages set `parent: CustomControls Package` and `has_children: true`; their children set `parent: ` (the grouped-page pattern). Mirror exactly the structure WebView2 uses for its `Enumerations/`. - -**License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2022) — same situation as WebView2Package and Assert. Pages are fully original; **omit** the `vba_attribution: true` flag. - -### cefPackage (CEF) - -Layout of `..\tb-export\NewProject\Packages\cefPackages\Sources\`: - -- `CefControl.twin` — contains three classes: the private `CefBrowserBaseCtl` (where every event / method / property is declared), the private `CefEnvironmentOptions` (a bare-field options class), and the public `CefBrowser` control which only inherits from `CefBrowserBaseCtl` and sets the design-time icon. This single file is the entire user-facing surface of the package. -- `CefControlGlobalWnd.twin` — private internal message-window plumbing; no doc page. -- `APIs.twin` — private Win32 API declarations; no doc page. -- `MainModule.twin` — `PreSubMain` module that intercepts CEF sub-process launches; no doc page. -- `Registry.twin` — Win32 registry helpers; no doc page. -- `SpecialFolders.twin` — `KnownFolders_CSIDLs` enum + `GetSpecialFolder` helper; `Private Module`, no doc page (used only by the sample project, not exported from the package). -- `CEF/Aliases.twin`, `CEF/ApiEntryPoints.twin`, `CEF/Globals.twin`, `CEF/Initialize.twin`, `CEF/Misc.twin` — `Private Module` implementation detail; no doc pages. -- `CEF/Enums/_cef_*_t.twin` — internal C-ABI enums wrapped in `Private Module _cef_*_t`; one file (`_cef_log_severity_t.twin`) also declares the user-facing `CefLogSeverity` enum. The other 29 `_cef_*_t` enums never surface in the public API and get **no doc page**. -- `CEF/Structs/_cef_*_t.twin` — internal C-ABI structs (`cef_browser_settings_t`, `cef_pdf_print_settings_t`, …) wrapped in `Private Module`; no doc pages. `Structs/TODO.twin` declares an empty placeholder `Class CefRequestHeaders` — also no doc page (see the alias note below). -- `CEF/CrossProcessIPC/BrowserOM.twin`, `RendererOM.twin`, `RendererAsyncOM.twin` — `Private Class` (or unmarked-but-effectively-private) classes that broker IPC between the host and the CEF browser / renderer processes; no doc pages. **Exception:** `BrowserOM.twin` declares the user-facing `cefPrintOrientation` enum inline (used by `CefBrowser.PrintToPdf`); document it under `Enumerations/`. -- `CEF/Implementations/*.twin` — every file is `Private Class` / `Private Module`; CEF callback handlers (`Client`, `ClientLifeSpanHandler`, `ClientLoadHandler`, `AppRender…`, the `Exposed*Javascript*` and `*Task` classes, the `PrintToPDFCallback`, …). All implementation detail; no doc pages. - -Public user-facing surface (one control + one options class + two enums): - -| Symbol | Kind | Role | -|------------------------------|---------------------------------------|-----------------------------------------------------------------------------------------------| -| `CefBrowser` | Class (inherits `CefBrowserBaseCtl`) | the control itself, tagged `[WindowsControl("/Miscellaneous/cef64.png")]` | -| `CefEnvironmentOptions` | `Private Class`, exposed | reached via `Public EnvironmentOptions As CefEnvironmentOptions = New CefEnvironmentOptions` on the control | -| `CefLogSeverity` | `Enum` (in `_cef_log_severity_t.twin`)| used by `CefEnvironmentOptions.LogSeverity` | -| `cefPrintOrientation` | `Enum` (in `BrowserOM.twin`) | used by the `Orientation` parameter of `CefBrowser.PrintToPdf` | - -`CefBrowserBaseCtl` is `Private Class` but is where every public member is *declared* — `CefBrowser` itself adds nothing beyond inheriting from it. Document everything on the `CefBrowser/index.md` page (folder-style, parallel to `WebView2/`) and treat the base class as an internal split that doesn't surface. - -`CefBrowser` inherits from `VB.BaseControlRectDockable`, so its Properties listing must fold in the dockable-rect surface (`Name`, `Left`, `Top`, `Width`, `Height`, `Anchors`, `Dock`, ...) the same way VB-package control pages and CustomControls control pages do. - -`CefBrowserRequestHeaders` is declared at the top of `CefControl.twin` as `Alias CefBrowserRequestHeaders As Object` and appears in the `NavigationStarting` event signature. The underlying `Class CefRequestHeaders` lives at the bottom of `Structs/TODO.twin` with an empty body — it's a placeholder for a future header collection. **No doc page;** mention on the `NavigationStarting` event entry that the parameter is currently typed `Object` and reserved for future use. - -The `CefBrowser` public surface, from `grep '^\s*Public' CefControl.twin` plus a walk through `CefBrowserBaseCtl`: - -- **Events (12):** `Create`, `Ready`, `Error`, `NavigationStarting`, `NavigationComplete`, `SourceChanged`, `DocumentTitleChanged`, `DOMContentLoaded`, `PrintToPdfCompleted`, `PrintToPdfFailed`, `JsAsyncResult`, `JsMessage`. -- **Methods (14):** `Initialize`, `Navigate`, `NavigateToString`, `Reload`, `GoBack`, `GoForward`, `ExecuteScript`, `JsRun`, `JsRunAsync`, `PostWebMessage`, `SetVirtualHostNameToFolderMapping`, `ClearVirtualHostNameToFolderMapping`, `OpenDevToolsWindow`, `PrintToPdf`. -- **Properties (12):** `DocumentURL` (Get/Let), `DocumentTitle` (Get), `ZoomFactor` (Get/Let), `UserAgent` (Get/Let), `CanGoBack` (Get), `CanGoForward` (Get), `CefMajorVersion` (Get), `Visible` (Get/Let), `hWnd` (Get), `Parent` (Get), `EnvironmentOptions` (field), `CreateInitialized` (field, `Boolean`, defaults `True`). Plus a `Hidden` `Align` (Get/Let) inherited boilerplate. Plus the inherited rect-dockable surface (`Name`, `Left`, `Top`, `Width`, `Height`, `Anchors`, `Dock`, …). - -`CefEnvironmentOptions` is four bare `Public` fields: - -| Field | Type | -|----------------------------|------------------| -| `BrowserExecutableFolder` | `String` | -| `UserDataFolder` | `String` | -| `LogFilePath` | `String` | -| `LogSeverity` | `CefLogSeverity` | - -`CefLogSeverity` members: `CefLogDisable = 0`, `CefLogVerbose = 1`, `CefLogInfo = 2`, `CefLogWarning = 3`, `CefLogError = 4`, `CefLogFatal = 5`. - -`cefPrintOrientation` members: `cefPrintPortrait = 0`, `cefPrintLandscape = 1`. - -**WebView2-parity gap list** (call out on `CEF/index.md`, drawn from `Sources\Example1..4.twin` where these are commented out as *"Sorry, this feature is not yet available in the CEF package"*): - -- Methods: `OpenTaskManagerWindow`, `AddObject` (host-object publication), the request-filter machinery (`AddWebResourceRequestedFilter`). -- Events: `AcceleratorKeyPressed`, `PermissionRequested`, `WebResourceRequested`, `ProcessFailed`, `ScriptDialogOpening`, `UserContextMenu`, `SuspendCompleted`, `SuspendFailed`, `DownloadStarting`, `NewWindowRequested`. - -`CefBrowser.NavigationComplete` carries `IsSuccess` and `WebErrorStatus` parameters, but the source (`OnNavigationComplete_UI` in `CefControl.twin`) currently hard-codes `IsSuccess = True` and `WebErrorStatus = 0` with `FIXME` comments — note this on the event page. - -**Multi-version source.** The same `.twin` sources compile against three CEF runtimes (v49 / v109 / v145) selected via the `CEF_VERSION` conditional-compilation argument on the project. At runtime, `CefBrowser.CefMajorVersion` returns the value picked at compile time. The user picks a runtime at deploy time by downloading the matching ZIP from `github.com/twinbasic/cef-runtimes` and extracting to `%LocalAppData%\twinBASIC_CEF_Runtime\`, or by overriding `CefBrowser.EnvironmentOptions.BrowserExecutableFolder` before / during the `Create` event. The wiki entry will redirect to the new docs, so the runtime download + version-picking section lives on `CEF/index.md`. - -#### Doc-side layout (folders / files) - -Six pages total: - -``` -docs/Reference/CEF/ - index.md ← package landing; intro + WebView2 comparison + version table + runtime install + WebView2-parity gap list + class & enum lists - CefBrowser/index.md ← the control (folder-style, like WebView2/WebView2/index.md) - CefBrowser/EnvironmentOptions.md ← parallel to WebView2/WebView2/EnvironmentOptions.md - Enumerations/index.md - Enumerations/CefLogSeverity.md - Enumerations/cefPrintOrientation.md -``` - -**Naming:** - -- Folder / URL segment: `CEF/` (uppercase acronym, matches the wiki spelling; drops the `Package` suffix, same simplification as WebView2 and Assert). -- Index title: `CEF Package` — the ` Package` convention. -- Permalinks: `/tB/Packages/CEF/` for the landing, `/tB/Packages/CEF/CefBrowser/` for the control (folder-style), `/tB/Packages/CEF/CefBrowser/EnvironmentOptions` for the sub-page, `/tB/Packages/CEF/Enumerations/` for the enums. -- `parent: CEF Package` on every top-level child page. The two enum pages set `parent: Enumerations` and `grand_parent: CEF Package` (the grouped-page pattern; mirror exactly the structure WebView2 uses for its `Enumerations/`). The `EnvironmentOptions` sub-page sets `parent: CefBrowser` and `grand_parent: CEF Package`. - -**License:** MIT (Wayne Phillips T/A iTech Masters) — same situation as WebView2Package, Assert, and CustomControls. The Settings file doesn't carry an explicit `licence:` field but every other package by this author is MIT and the wiki implies the same. Pages are fully original; **omit** the `vba_attribution: true` flag. - -### WinEventLogLib - -Layout of `..\tb-export\NewProject\Packages\WinEventLogLib\Sources\` is flat — four `.twin` files plus three text files (`_README.txt`, `_LICENCE.txt`, `_RELEASE_HISTORY.txt`): - -- `APIs.twin` — `Private Module EventLogAPIs` wrapping six `advapi32.dll` entry points (`RegCreateKeyExW`, `RegSetValueExW`, `RegCloseKey`, `RegDeleteKeyExW`, `RegisterEventSourceW`, `ReportEventW`). No doc page. -- `Constants.twin` — `Private Module EventLogConstants` carrying `Public Enum EventLogTypeConstants` (`vbEventLogTypeSuccess`, `vbEventLogTypeAuditFailure`, `vbEventLogTypeAuditSuccess`, `vbEventLogTypeError`, `vbEventLogTypeWarning`). The module is `Private`, so the enum does not surface in the public API; no doc page. -- `EventLog.twin` — the generic `Public Class EventLog(Of T1, T2)`. The only user-facing class. -- `Helper.twin` — two modules: `Public Module EventLogHelperPrivate` (one helper `VariantArrayToStringArray`, used only by `EventLog.LogArray` internally — its name signals *intended* private but the module is declared `Public`; treat as internal and skip) and `Public Module EventLogHelperPublic` (one user-facing helper `RegisterEventLogInternal`). - -Public user-facing surface (one generic class + one helper module): - -| Symbol | Kind | Role | -|---------------------------------|---------------------|---------------------------------------------------------------------------------------| -| `EventLog(Of T1, T2)` | Generic class | Main user-facing class. `T1` is the event-ID enum, `T2` is the category enum. | -| `EventLogHelperPublic` | Public module | Holds the low-level `RegisterEventLogInternal` helper. | -| `RegisterEventLogInternal` | `Sub` on the module | Registry-write helper; `EventLog.Register()` is the normal entry point. | - -`EventLog(Of T1, T2)` public members: - -- `Public Sub New(LogName As String)` — constructor. `LogName` is either a leaf name (`"MyService"`, registered under `Application\MyService`) or a full path (`"System\MyService"`, registered under `System\MyService`). -- `Public Sub LogSuccess(ByVal EventId As T1, ByVal CategoryId As T2, ParamArray AdditionalStrings())` — writes an **Information**-type event (`EVENTLOG_SUCCESS = 0`). The name "Success" is the Win32 SDK constant's literal name, *not* the audit-success category — the underlying event type is **Information**. -- `Public Sub LogFailure(ByVal EventId As T1, ByVal CategoryId As T2, ParamArray AdditionalStrings())` — writes an **Error**-type event (`EVENTLOG_ERROR_TYPE = 1`). -- `Public Sub Register()` — writes the registry entries under `HKLM\SYSTEM\CurrentControlSet\Services\EventLog\…` to declare this EXE as the message provider for the source. Calls `RegisterEventLogInternal(LogName, GetDeclaredMaxEnumValue(Of T2))` — the category count is derived from `T2`'s declared maximum value at compile time. - -Class-level decoration on `EventLog`: `[COMCreatable(False)]`, `[ClassId("4AEA12E8-…-EAEAEAEAEAEA")]` (the `EA` suffix triggers special compiler handling for generic classes). The `[Description]` attribute on the class is the basis for the page intro: *"This is the main event log (generic) class."* - -`EventLogHelperPublic` public members: - -- `Public Sub RegisterEventLogInternal(ByVal LogPath As String, ByVal CategoryCount As Long)` — the registry-write helper. Prepends `"Application\"` to *LogPath* if no backslash is present, opens `HKLM\SYSTEM\CurrentControlSet\Services\EventLog\` with `KEY_WRITE`, then writes `EventMessageFile` and `CategoryMessageFile` (both set to `App.ModulePath`) and `CategoryCount`. Requires admin rights (registry writes to HKLM). Raises run-time error 5 with the message *"Failed to register event log source (\)"* if the open fails. Normally callers use `EventLog.Register()`, which fills *CategoryCount* automatically. - -**Gaps and quirks** to surface on the docs (drawn from a static read of the source): - -- The `EventLogTypeConstants` enum has five values (`Success`, `Warning`, `Error`, `AuditSuccess`, `AuditFailure`) but the public class only exposes Information and Error event types — Warning and Audit events are not currently reachable. -- Method names follow the Win32 SDK constants verbatim: `LogSuccess` writes an *Information* event (because `EVENTLOG_SUCCESS = 0` is the Win32 spelling for the information type), and `LogFailure` writes an *Error* event. Call this out on the per-method entries. -- Message resources: the registry entries point at `App.ModulePath` (the running EXE) for both `EventMessageFile` and `CategoryMessageFile`. Windows therefore expects a message-table resource keyed by the `T1` and `T2` enum values to be embedded in the EXE. The `.twin` source does not itself synthesise that resource — there is no `mc.exe` invocation or message-table emit visible in `WinEventLogLib\Sources\`; whatever mechanism populates the resource sits in the compiler's special-handling path for the `[ClassId("…EAEAEAEAEAEA")]` magic-byte pattern. The docs describe what Windows expects without making strong claims about how the compiler delivers it. -- `Register()` requires elevation. Normal usage is to call it once during install (from an elevated installer), then call `LogSuccess` / `LogFailure` at runtime without elevation. -- The package is described in `_README.txt` with a copy-pasted "NAMED PIPES PACKAGE" header (clearly an unintentional carry-over from another sister package); the body correctly says *"A simple framework for creating Windows event log entries from twinBASIC"*. Use the body, not the header. - -#### Canonical usage idiom — composition-delegation onto a service class - -`tbServiceTest2` (`..\tbrepro\winlibs\tbServiceTest2\Sources\`) shows the package's intended usage pattern, which is *not* obvious from the bare API: - -```tb -Class TBSERVICE001 - Implements ITbService - Implements EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES) Via _ - EventLog = New EventLog(Of MESSAGETABLE.EVENTS, MESSAGETABLE.CATEGORIES)("Application\" & CurrentComponentName) - … - LogSuccess(service_started, status_changed, CurrentComponentName) ' surfaces directly -End Class -``` - -The `Implements Via = ` form is twinBASIC's composition-delegation syntax (see [`docs/Features/Language/Delegation.md`](docs/Features/Language/Delegation.md) if/once that page exists, or the [`CustomControls` mixin pattern](docs/Reference/CustomControls/index.md) for an analogous use). The class declares it `Implements EventLog(Of …)` and gives the compiler a private field plus a constructor expression; the compiler then auto-forwards every `Public` member of `EventLog` (`LogSuccess`, `LogFailure`, `Register`) through that field. The result: a service class that *contains* an `EventLog` instance and exposes its logging methods as if they were its own. - -Surface this on the `EventLog` page (and on the package index) as the **recommended pattern** for service / long-running classes. Spell out: - -- The constructor expression evaluates *once* (the first time the delegating class is instantiated, per twinBASIC's `Implements ... Via` semantics). -- The `T1` / `T2` type arguments must be identical at the `Implements` declaration and the constructor (the compiler enforces this). -- The `LogPath` is typically `"Application\" & CurrentComponentName` — `CurrentComponentName` is the compile-time class name, so the log path automatically tracks renames. -- The delegating class transparently inherits all three of `LogSuccess` / `LogFailure` / `Register`. Calling code can use them unqualified. - -#### Message-table backing: `[PopulateFrom("json", …)]` on the enums - -The `T1` / `T2` enums are typically auto-populated from a JSON resource via the `[PopulateFrom]` attribute. `tbServiceTest2`'s `Sources\MISC\MESSAGETABLE.twin`: - -```tb -Module MESSAGETABLE - [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "events", "name", "id")] - Enum EVENTS - End Enum - - [PopulateFrom("json", "/Resources/MESSAGETABLE/Strings.json", "categories", "name", "id")] - Enum CATEGORIES - End Enum -End Module -``` - -…with `Resources\MESSAGETABLE\Strings.json`: - -```json -{ - "events": [ - { "id": -1073610751, "name": "service_started", "LCID_0000": "%1 service started" }, - { "id": -1073610750, "name": "service_startup_failed", "LCID_0000": "%1 service startup failed" }, - … - ], - "categories": [ - { "id": 1, "name": "status_changed", "LCID_0000": "Status Changed" } - ] -} -``` - -Two things are happening here: - -1. **The enum bodies are populated at compile time** — `Enum EVENTS` starts empty in the source, but after compilation it has members `service_started = -1073610751`, `service_startup_failed = -1073610750`, … (one per `"events"` entry in the JSON, keyed `name → id`). -2. **The same JSON is consumed by the compiler's `mc.exe`-equivalent** that emits the message-table resource into `App.ModulePath`. The `LCID_0000` strings are the message-table entries, and the `%1`, `%2`, … placeholders are filled at log time from the `AdditionalStrings` `ParamArray` to `LogSuccess` / `LogFailure`. The `CategoryCount` registry value (written by `Register()`) is the highest declared `id` in the `categories` block, which is what `GetDeclaredMaxEnumValue(Of T2)` recovers at compile time. - -So the round-trip is: JSON → compile-time enum population + message-table resource emission → registry entries that point Windows at the EXE → runtime `LogSuccess(EventId, CategoryId, …)` writes an event the Event Viewer can format using the embedded message-table strings. - -Surface this on the index page (under "Setting up message resources" or similar) with the JSON skeleton and the cross-reference to `[PopulateFrom]` (which is documented under `docs/Features/`, not in the reference set — link to that page if it exists, otherwise describe in-place). - -The negative event-ID values in the JSON (`-1073610751`) are the standard Win32 event-ID encoding: the high bits encode severity (`0xC0000000` = Error), facility (`0x...`), and customer bit. Don't unpack this on the docs; just note that *"event IDs follow the Win32 documented encoding — see Microsoft's 'Event Identifiers' reference"*. - -#### Why `T1` / `T2` and not separate `EventIds` / `Categories` classes - -A class can only `Implements EventLog(Of T1, T2) Via …` *once*. If a service needs events from multiple unrelated message tables, it can compose multiple `EventLog` instances **as named fields** (no `Via`), accepting a small loss of ergonomics (calls become `MyEventLog.LogSuccess(…)` instead of `LogSuccess(…)`). Surface this as a one-line note on the index — most services share a single `MESSAGETABLE` module across all their classes (as the example does), so the limitation rarely bites. - -**Layout decision** — three pages total, mirroring the small-package approach used by Assert: - -- `docs/Reference/WinEventLogLib/index.md` — landing page: intro, lifecycle (define enums → instantiate → Register once → LogSuccess / LogFailure), gaps and quirks, the class and module lists. -- `docs/Reference/WinEventLogLib/EventLog.md` — the generic class, single-file (no sub-pages — the surface is small). -- `docs/Reference/WinEventLogLib/EventLogHelperPublic.md` — the helper module, single-file (one Sub listed under a `## RegisterEventLogInternal` heading, same shape as Assert per-member sections). - -**Naming:** - -- Folder / URL segment: `WinEventLogLib/` (matches the source-side package name; no `Package` suffix to drop, the package isn't named with that suffix in `Settings`). -- Index title: `WinEventLogLib Package` — the ` Package` convention used by every other package landing. -- Permalinks: `/tB/Packages/WinEventLogLib/` for the landing; `/tB/Packages/WinEventLogLib/EventLog` and `/tB/Packages/WinEventLogLib/EventLogHelperPublic` for the two member pages. -- `parent: WinEventLogLib Package` on each child page (matching the index `title:`, the same split every other package uses). - -**License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2025; first release v0.1, 04-FEB-2025) — same situation as WebView2Package, Assert, CustomControls, and CEF. Pages are fully original content; **omit** the `vba_attribution: true` flag. - -### WinNamedPipesLib - -Layout of `..\tb-export\NewProject\Packages\WinNamedPipesLib\Sources\` is flat — seven `.twin` files plus three text files (`_README.txt`, `_LICENCE.txt`, `_RELEASE_HISTORY.txt`): - -- `APIs.twin` — `Private Module NamedPipesAPIs` wrapping Win32 declarations from `kernel32.dll`, `user32.dll`, and `oleaut32.dll` (`CreateNamedPipeW`, `ConnectNamedPipe`, `ReadFile` / `WriteFile`, `CreateIoCompletionPort`, `GetQueuedCompletionStatus`, `PostQueuedCompletionStatus`, `CreateThread`, …) plus the supporting `Type` declarations (`POINT`, `MSG`, `SAFEARRAYBOUND`, `SAFEARRAY_1D`, `OVERLAPPED_CUSTOM`, `FILETIME`, `WIN32_FIND_DATAW`) and a 32/64-bit `SetWindowLongPtrW` alias. No doc page. -- `Constants.twin` — `Private Module NamedPipesConstants` carrying Win32 constants (`PIPE_ACCESS_DUPLEX`, `PIPE_TYPE_MESSAGE`, `FILE_FLAG_OVERLAPPED`, `ERROR_IO_PENDING`, `ERROR_MORE_DATA`, …) and the package-internal `Enum OverlappedTypeConstants` (`tbOverlappedConnect`, `tbOverlappedRead`, `tbOverlappedWrite`). Module is private, so none of these surface in the public API; no doc page. -- `Helper.twin` — `Private Module NamedPipesHelper` with `ObjectFromPointer_*` and `ObjPtrRef` / `ObjReleaseRef` reference-counting helpers used internally to ferry COM pointers across IOCP worker threads. Module is private; no doc page. -- `NamedPipeServer.twin` — `Public Class NamedPipeServer`. The server-side entry point. Also declares the private `INamedPipeServerInternal` interface (implementation detail; no doc page). -- `NamedPipeServerConnection.twin` — `Public Class NamedPipeServerConnection`. Per-client connection object surfaced through `NamedPipeServer` events. Also declares the private `INamedPipeServerConnectionInternal` interface (no doc page). -- `NamedPipeClientManager.twin` — `Public Class NamedPipeClientManager`. Owns the client-side IOCP machinery and the `Connect` / `Stop` / `FindNamedPipes` entry points. Also declares the private `INamedPipeClientManagerInternal` interface (no doc page). -- `NamedPipeClientConnection.twin` — `Public Class NamedPipeClientConnection`. Returned by `NamedPipeClientManager.Connect`. Carries the `Connected` / `Disconnected` / `MessageReceived` / `MessageSent` events and the `AsyncWrite` / `AsyncRead` / `AsyncClose` methods. Also declares the private `INamedPipeClientConnectionInternal` interface (no doc page). - -The four classes are each tagged `[COMCreatable(False)]` — only the manager / server classes can be instantiated by user code (with `New`); the two `Connection` classes are constructed internally and handed back through events / return values. - -The private `INamedPipe*Internal` interfaces serve a marshalling-only role: each public class implements its matching internal interface so that the IOCP worker threads can refcount and dispatch through `stdole.IUnknown` without taking a strong reference to the parent class. They are *not* user-facing surface and get no documentation page. - -Public user-facing surface (four classes — two on each side): - -| Class | Role | -|-----------------------------|---------------------------------------------------------------------------------------------------------------| -| `NamedPipeServer` | The server. User-instantiated. Sets `PipeName`, calls `Start`, listens for events; one server hosts many clients. | -| `NamedPipeServerConnection` | One server-side per-client connection. Surfaced through `NamedPipeServer` events; carries `AsyncRead` / `AsyncWrite` / `AsyncClose`. | -| `NamedPipeClientManager` | The client-side coordinator. User-instantiated. Owns the IOCP worker threads; the `Connect` method returns a `NamedPipeClientConnection`. | -| `NamedPipeClientConnection` | One client-side connection. Returned by `NamedPipeClientManager.Connect`; carries `Connected` / `Disconnected` / `MessageReceived` / `MessageSent` events and `AsyncRead` / `AsyncWrite` / `AsyncClose`. | - -#### `NamedPipeServer` public members - -Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[EventInterfaceId(...)]`, `[ClassId(...)]`. No `[Description("...")]` on the class itself. - -**Public fields** (each carries a `[Description("...")]`): - -- `PipeName As String` — *"the discoverable pipe name"*. Set this before `Start()` or `Start()` raises run-time error 5 (*"cannot start without specifying a pipe name"*). The Win32 pipe namespace path is `\\.\pipe\` (the package prepends `\\.\pipe\` itself). -- `NumThreadsIOCP As Long = 1` — *"the number of IOCP worker threads that will be created"*. Read once when `Start()` is called; the in-source `FIXME` notes that this should become read-only once started. -- `FreeThreadingEvents As Boolean = False` — *"set to TRUE to allow the server events ClientConnected / ClientReceivedDataAsync etc to be fired directly from the IOCP worker threads. set to FALSE to ensure the events get fired on the main UI thread."* The free-threaded path skips a Win32 message-loop round-trip; the marshalled path is safer because the events fire on the UI thread. -- `ContinuouslyReadFromPipe As Boolean = True` — *"set to TRUE to ensure ClientReceivedDataAsync events always fire without having to call AsyncRead manually."* When `False`, the consumer must call `AsyncRead` after each `ClientMessageReceived` to keep receiving. -- `MessageBufferSize As Long = 131072` — *"sets the initial size for ReadFile() buffers. does not affect the maximum message receive size, but can affect performance."* On `ERROR_MORE_DATA` the IOCP loop allocates a larger overflow buffer and re-issues the read, so messages larger than this size do work — but with one extra allocation per overflowed message. - -**Public events**: - -- `ServerReady()` — fires once after `Start()` when every IOCP worker has joined. -- `ClientConnected(Connection As NamedPipeServerConnection)` — a new client connection has completed. -- `ClientDisconnected(Connection As NamedPipeServerConnection)` — the connection has dropped and every outstanding async operation has returned. -- `ClientMessageReceived(Connection As NamedPipeServerConnection, ByRef Cookie As Variant, ByRef Data() As Byte)` — a message arrived. *Data* is a transient view over the IOCP read buffer (a hand-rolled `SAFEARRAY` whose backing memory is reused after the event); copy it if you need to keep it past the event handler. -- `ClientMessageSent(Connection As NamedPipeServerConnection, ByRef Cookie As Variant)` — a previously-issued `AsyncWrite` has completed. - -**Public methods**: - -- `Sub New()` — constructor; creates the hidden marshalling-window used for UI-thread event delivery. -- `Public Sub Start()` — creates the IOCP completion port and `NumThreadsIOCP` worker threads, then issues the first connection listener. Idempotent: calling `Start()` while already started is a no-op. -- `Public Sub Stop()` — cancels every outstanding I/O, joins the IOCP threads, closes pipe handles. Idempotent. Called automatically from `Class_Terminate`. -- `Sub AsyncBroadcast(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — issues `AsyncWrite` against every currently-connected `NamedPipeServerConnection`. -- `Public Sub ManualMessageLoopEnter()` / `Public Sub ManualMessageLoopLeave()` — drive a Win32 message loop manually (rare; only needed when the host process does not naturally pump messages — e.g. an unattended Windows service that wants the marshalled-event semantics rather than the free-threaded ones). `Leave` posts `WM_USER_QUITTING`, which `Enter` reads to break the loop. - -#### `NamedPipeServerConnection` public members - -Tagged `[COMCreatable(False)]`. Not directly user-instantiable. - -**Public fields**: - -- `Handle As LongPtr` — the underlying Win32 pipe handle. Exposed but not normally needed; useful for low-level operations or debugging. -- `IsOpening As Boolean` — true while `Open()` is in progress (race-condition window between adding to the linked list and finishing `ConnectNamedPipe`). -- `IsConnected As Boolean` — true between the client connecting and the connection dropping. -- `CustomData As Variant` — *"free for use"*: opaque per-connection slot the consumer can attach state to. - -**Public methods**: - -- `Sub New(...)` — internal constructor; takes the parent server + pipe info. Never called by user code. -- `Public Sub AsyncClose()` — cancels outstanding I/O and closes the pipe handle. Called automatically from `Class_Terminate`. -- `Public Sub AsyncWrite(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — writes a message back to this specific client. Returns immediately; `NamedPipeServer.ClientMessageSent` fires when the write completes. -- `Public Sub AsyncRead(Optional ByRef Cookie As Variant = Empty, Optional OverlappedStruct As LongPtr)` — manually issues a read. Only needed when `NamedPipeServer.ContinuouslyReadFromPipe = False`; otherwise the server keeps the read pump fed automatically. - -No public events — message-received and connection-dropped notifications come through the parent `NamedPipeServer`. The class declares an internal `INamedPipeServerConnectionInternal` interface that the IOCP loop uses for refcounting; that interface is `Private` and gets no doc page. - -#### `NamedPipeClientManager` public members - -Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[EventInterfaceId(...)]`, `[ClassId(...)]`. - -**Public fields** (each carries `[Description("...")]`, mirror the server's): - -- `NumThreadsIOCP As Long = 1` -- `MessageBufferSize As Long = 131072` -- `FreeThreadingEvents As Boolean = False` -- `ContinuouslyReadFromPipe As Boolean = True` - -These four are read once on the first `Connect()` call and propagated to every `NamedPipeClientConnection` created through that manager; subsequent changes do not affect connections that already exist. Source comment in `NamedPipeClientConnection` confirms: *"tip: set it in NamedPipeClientConnections before Connect()"*. - -**Public methods**: - -- `Sub New()` — constructor; creates the hidden marshalling window. -- `Public Function Connect(ByVal PipeName As String) As NamedPipeClientConnection` — opens a connection to a server (`\\.\pipe\`). Lazy on first call: creates the IOCP port and the worker threads. Returns a connection object that fires `Connected` once the async `CreateFileW` completes. -- `Public Sub Stop()` — cancels every outstanding I/O on every managed connection, joins the IOCP threads, frees the resources. Idempotent. Called automatically from `Class_Terminate`. -- `Public Function FindNamedPipes(Optional Pattern As String = "*") As Collection` — enumerates the named pipes currently published on the local machine (via `FindFirstFileW("\\.\pipe\")`). Returns a `Collection` of `String`. Useful as a discovery helper before calling `Connect`. - -No events on the manager itself — per-connection events live on the returned `NamedPipeClientConnection` objects. - -#### `NamedPipeClientConnection` public members - -Tagged `[COMCreatable(False)]`, `[InterfaceId(...)]`, `[ClassId(...)]`, `[EventInterfaceId(...)]`. Not directly user-instantiable — `NamedPipeClientManager.Connect` returns it. - -**Public fields**: - -- `PipeName As String` — the pipe name the connection targets. -- `Handle As LongPtr` — the underlying Win32 file handle. Same caveats as on the server-side connection. -- `CustomData As Variant` — *"free for use"*. - -**Public events**: - -- `Connected()` — the async `CreateFileW` has succeeded. -- `Disconnected()` — the connection has dropped and every outstanding async operation has returned. -- `MessageReceived(ByRef Cookie As Variant, ByRef Data() As Byte)` — a message arrived. *Data* has the same transient-view semantics as on the server. -- `MessageSent(ByRef Cookie As Variant)` — a previously-issued `AsyncWrite` has completed. - -**Public methods**: - -- `Sub New(...)` — internal constructor; never called by user code directly. -- `Public Sub AsyncClose()` — **critical:** the README says *"you MUST call AsyncClose on the client side, otherwise the connection is left alive when the object goes out of scope"*. Surface this on every relevant page. -- `Public Sub AsyncWrite(ByRef Data() As Byte, Optional ByRef Cookie As Variant = Empty)` — sends a message to the server. -- `Public Sub AsyncRead(Optional ByRef Cookie As Variant = Empty, Optional OverlappedStruct As LongPtr)` — manually issues a read. Same gating as on the server-side: only call this when `ContinuouslyReadFromPipe = False`. - -**Documented gaps / TODOs from `_README.txt`** (surface on the landing page): - -- *"we need a method to allow closing a client connection from the server side"* — there is no `NamedPipeServerConnection.Disconnect` or `.Close` user-method today. The server can stop the whole pipe (`NamedPipeServer.Stop`) but cannot selectively drop one client. -- *"named pipe error should be raised via Error events (rather than throwing an error on the worker threads)"* — internal IOCP errors currently bubble up as VBA run-time errors on worker threads rather than as `Error` events. No `Error` event exists on any of the four classes yet. -- *"remove max size 131072 of messages"* — the `MessageBufferSize` initial-buffer default is 131072 bytes. The IOCP overflow path (`ERROR_MORE_DATA` → larger buffer → re-issue read) does handle larger messages, but there may be a hard cap somewhere the author wants to remove; surface this as *"see TODO list in `_README.txt`"* rather than making a stronger claim. -- *"currently a lot of duplicate code in server + client"* — internal-refactor note. **Not** surfaced on the docs. - -**Cookie pattern.** Every `AsyncRead` and `AsyncWrite` accepts an optional *Cookie* (`Variant`). Whatever the consumer passes in flows through the IOCP completion buffer and is handed back out on the matching `MessageReceived` / `MessageSent` event. This is the package's mechanism for correlating individual writes with their completion notifications when many are in flight. - -**`Data() As Byte` transience.** Inside `MessageReceived` / `ClientMessageReceived`, *Data* is **not** a real `Byte` array — it is a hand-rolled `SAFEARRAY` whose `pvData` field points at the IOCP overlapped buffer. The buffer is recycled back into a free-list at the end of the event handler. Copy the bytes out (`ReDim`-and-copy, or `CStrConv` for text payloads) if you need them after returning from the handler. The source uses `PutMemPtr(VarPtr(safeArrayPtr), VarPtr(safeArrayPsuedo))` and clears it afterwards — surface this lifetime caveat on every event-page entry that carries *Data*. - -**Hidden message window.** Each `NamedPipeServer` and `NamedPipeClientManager` instance creates an invisible `STATIC`-class window with a subclassed `WndProc`, used to marshal IOCP-thread completions back to the UI thread when `FreeThreadingEvents = False`. Mention this on each class's intro paragraph — it explains why the consumer's process must be pumping a message loop for the default event-delivery semantics to work, and why `ManualMessageLoopEnter` / `ManualMessageLoopLeave` exist on `NamedPipeServer` for service / console hosts. - -#### Canonical service-host idiom — `ManualMessageLoopEnter` paired with `ChangeState` - -`tbServiceTest2`'s `Sources\SERVICES\TBSERVICE001.twin` shows the standard pattern for a Windows service that hosts a `NamedPipeServer`. Surface this on the `NamedPipeServer.md` page (under a "Hosting inside a Windows service" sub-heading) and on the index landing: - -```tb -' On the service thread (ITbService.EntryPoint): -Set NamedPipeServer = New NamedPipeServer -NamedPipeServer.PipeName = "WaynesPipe_" & CurrentComponentName -ServiceManager.ReportStatus(vbServiceStatusRunning) - -NamedPipeServer.Start() -NamedPipeServer.ManualMessageLoopEnter() ' blocks until ManualMessageLoopLeave -NamedPipeServer.Stop() - -ServiceManager.ReportStatus(vbServiceStatusStopped) - -' On the dispatcher thread (ITbService.ChangeState): -Select Case dwControl - Case vbServiceControlStop, vbServiceControlShutdown - ServiceManager.ReportStatus(vbServiceStatusStopPending) - NamedPipeServer.ManualMessageLoopLeave() ' wakes the service thread -End Select -``` - -Key facts that aren't obvious from the per-method `[Description]`s: - -- The service-thread `EntryPoint` and the dispatcher-thread `ChangeState` are **different threads**. The `NamedPipeServer` member field is shared between them; the dispatcher-thread `ChangeState` calls `ManualMessageLoopLeave` on it to wake the service thread out of `ManualMessageLoopEnter`. -- `ManualMessageLoopLeave` is the **only** way to wake `ManualMessageLoopEnter` cleanly. There is no timeout, no second blocking primitive. If the service needs to react to other wake-up sources (paused state, custom control codes), it sets a shared flag *then* calls `ManualMessageLoopLeave` to break out, inspects the flag, and decides whether to re-enter the loop or proceed to shutdown. The `TBSERVICE002` variant in the same example demonstrates this with `IsPaused` / `IsStopping` shared `Public` fields and a `While IsStopping = False` outer loop. -- Pause / continue support uses the same pattern: `ChangeState` flips `IsPaused = True` and calls `ManualMessageLoopLeave`; the service thread sees the flag, reports `vbServiceStatusPaused`, enters a `Do While IsPaused : Sleep(500) : Loop`, then re-enters `ManualMessageLoopEnter` once `Continue` flips the flag back. -- `FreeThreadingEvents = False` (the default) is **required** for this pattern — events are marshalled to whichever thread is currently inside `ManualMessageLoopEnter`. Setting `FreeThreadingEvents = True` would deliver events on the IOCP worker thread instead and bypass the manual loop entirely (advanced; not the documented service idiom). - -The non-service equivalent — hosting the same `NamedPipeServer` inside a Form — is in `Sources\FORMS\InProcessNamedPipeServerForm.twin`: the Form's regular message loop pumps the marshalling window automatically, so the Form just calls `Server.Start()` in `Form_Load` and `Server.Stop` in `Form_Unload` without ever touching `ManualMessageLoopEnter` / `Leave`. Cross-reference both patterns on the `NamedPipeServer.md` page so the reader sees the choice point. - -#### PropertyBag as the canonical message carrier - -Every example serialises structured payloads through the pipe as a `PropertyBag.Contents` `Byte()`: - -```tb -' Sender: -Dim propertyBag As New PropertyBag -propertyBag.WriteProperty("CommandID", "WHAT_TIME_IS_IT") -propertyBag.WriteProperty("Data", payload) -SelectedNamedPipe.AsyncWrite propertyBag.Contents - -' Receiver (inside MessageReceived event): -Dim propertyBag As New PropertyBag -propertyBag.Contents = Data ' deep-copies the bytes; safe past the event handler -Dim commandID As String = propertyBag.ReadProperty("CommandID") -… -``` - -Two reasons this pattern matters and should be surfaced on the docs: - -1. **The transient-`Data()` problem is solved by `PropertyBag`.** Assigning to `PropertyBag.Contents` deep-copies the byte buffer; once the assignment returns, the original IOCP buffer can be recycled without invalidating the data. This is the cleanest answer to *"how do I keep the data past the event handler?"* — call out on every `MessageReceived` / `ClientMessageReceived` page entry as the recommended capture mechanism. -2. **`PropertyBag` provides typed multi-field payloads** without the consumer having to design a wire protocol. Both sides agree on the property names (`"CommandID"`, `"ResponseCommandID"`, `"ResponseData"`, `"Data"`) and `PropertyBag` handles the encoding / decoding. Cross-link [`PropertyBag` reference](docs/Reference/VBRUN/PropertyBag/index.md) from the index landing. - -Surface as the **recommended** carrier; nothing in the package mandates it, raw `Byte()` works too, but every worked example uses `PropertyBag` and the integration story reads much more cleanly with it. - -#### Discovery loop — `FindNamedPipes` - -`tbServiceTest2`'s `MainForm` shows the canonical client-side discovery pattern: a low-frequency `Timer` (the form uses `timerRefreshNamedPipes` with a multi-second interval) that calls `NamedPipeClients.FindNamedPipes("WaynesPipe_*")`, repopulates a `ListBox`, and preserves the user's current selection: - -```tb -For Each namePipeName In NamePipeClients.FindNamedPipes("WaynesPipe_*") - If namePipeName = NamedPipeSelected Then namedPipeSelectedIndex = Index - lstNamedPipes.AddItem(namePipeName) - Index += 1 -Next -``` - -Surface on the `NamedPipeClientManager.md` page (under the `FindNamedPipes` entry) as the recommended polling loop — the underlying `FindFirstFileW("\\.\pipe\…")` call is cheap enough to invoke every few seconds without measurable cost, and pipes appear / disappear too quickly for any event-driven discovery to be reliable. Don't claim there's no faster API; just say *"polling is the documented approach"*. - -#### Service-side broadcast - -`InProcessNamedPipeServerForm.twin` demonstrates `Server.AsyncBroadcast("BROADCAST")` (string coerced to `Byte()` via twinBASIC's implicit `String → Byte()` conversion). Useful when the same server has multiple concurrent connections and wants to push an update to all of them — the alternative is iterating over a user-maintained list of `NamedPipeServerConnection`s and calling `AsyncWrite` on each. The package handles the iteration internally. Mention on the `NamedPipeServer.AsyncBroadcast` entry. - -**Layout decision** — five pages total, one per public class plus the landing page: - -``` -docs/Reference/WinNamedPipesLib/ - index.md ← package landing; intro + IOCP model + cookie + transient-data caveat + gap list + class table - NamedPipeServer.md ← single-file: fields, events, methods - NamedPipeServerConnection.md ← single-file - NamedPipeClientManager.md ← single-file - NamedPipeClientConnection.md ← single-file -``` - -All four class pages are single-file (no folder-style — no natural sub-pages; the surface per class is medium-small, on the order of WebView2 wrapper classes). - -**Naming:** - -- Folder / URL segment: `WinNamedPipesLib/` (matches the source-side package name; no `Package` suffix to drop, same as `WinEventLogLib`). -- Index title: `WinNamedPipesLib Package` — the ` Package` convention. -- Permalinks: `/tB/Packages/WinNamedPipesLib/` for the landing; `/tB/Packages/WinNamedPipesLib/` for each of the four class pages. -- `parent: WinNamedPipesLib Package` on each child page (matching the index `title:`). - -**License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2025; first release v0.1, 04-FEB-2025) — same situation as WebView2Package, Assert, CustomControls, CEF, and WinEventLogLib. Pages are fully original content; **omit** the `vba_attribution: true` flag. - -### WinServicesLib - -Layout of `..\tb-export\NewProject\Packages\WinServicesLib\Sources\` is flat — eight `.twin` files plus three text files (`_README.txt`, `_LICENCE.txt`, `_RELEASE_HISTORY.txt`): - -- `APIs.twin` — `Private Module ServicesAPIs` wrapping fourteen `advapi32.dll` / `kernel32.dll` / `ole32.dll` / `oleaut32.dll` entry points (`StartServiceCtrlDispatcherW`, `OpenSCManagerW`, `CreateServiceW`, `RegisterServiceCtrlHandlerExW`, `SetServiceStatus`, `OpenServiceW`, `DeleteService`, `CloseServiceHandle`, `QueryServiceStatusEx`, `StartServiceW`, `ControlServiceExW`, `ChangeServiceConfig2W`, `CoInitializeEx`, `SysAllocStringPtr`) plus the supporting `Type` declarations (`SERVICE_STATUS`, `SERVICE_STATUS_PROCESS`, `SERVICE_CONTROL_STATUS_REASON_PARAMSW`, `SERVICE_CONFIG_DESCRIPTION`). No doc page. -- `Constants.twin` — two modules. `Private Module ServicesConstants` carries the Win32 access-flag constants (`SC_MANAGER_*`, `SERVICE_*` permission bits, `SERVICE_CONTROL_*` control codes, `SERVICE_ACCEPT_*` accepted-controls flags, etc.) plus the `SC_STATUS_TYPE` enum. **Public** module `ServicesConstantsPublic` carries the four user-facing enumerations (`ServiceTypeConstants`, `ServiceStartConstants`, `ServiceControlCodeConstants`, `ServiceStatusConstants`). The private module is internal; the public module's enums surface and need their own doc pages. -- `Helper.twin` — `Private Module ServicesHelper` with the IOCP-style trampoline (`ServiceControlHandlerCallback_Trampoline`) used in place of class-`AddressOf` plus a `VariantArrayToStringArray` helper. No doc page. -- `Interfaces.twin` — three interfaces: `Public Interface ITbService` (user-implemented), `Private Interface IServiceCreator` (internal — the public `ServiceCreator(Of T)` class implements it), `Private Interface IServiceManagerInternal` (internal). Only `ITbService` gets a doc page. -- `Services.twin` — the predeclared `Class Services` (no `Public`/`Private` modifier — `Class` defaults to public; tagged `[PredeclaredId]`, so it's used singleton-style as `Services.ConfigureNew`). The package's main entry point. -- `ServiceManager.twin` — `Public Class ServiceManager`. Per-service configuration + runtime status reporting. `[COMCreatable(False)]`. User code never instantiates this directly — it's returned by `Services.ConfigureNew()`. -- `ServiceCreator.twin` — `Public Class ServiceCreator(Of T)`. Generic factory `T → New T As ITbService`. `[COMCreatable(False)]`. Has the EA magic-byte `ClassId("66170220-FEF3-4257-8FBA-EAEAEAEAEAEA")` pattern, same compiler-special-handling as `WinEventLogLib`'s `EventLog(Of T1, T2)`. -- `ServiceState.twin` — `Class ServiceState` (no modifier — public by default). Read-only state snapshot for an installed service. `[COMCreatable(False)]`. Returned by `Services.QueryStateOfService`. - -Public user-facing surface (three concrete classes + one generic class + one interface + four enums): - -| Symbol | Kind | Role | -|---------------------------------|-----------------------|---------------------------------------------------------------------------------------------------| -| `Services` | `[PredeclaredId]` Class | The singleton coordinator: `ConfigureNew`, `RunServiceDispatcher`, `InstallAll`, `UninstallAll`, `LaunchService`, `ControlService`, `QueryStateOfService`, `GetConfiguredService`, `_NewEnum`. Used as `Services.X` without `New`. | -| `ServiceManager` | Class | Per-service configuration + runtime status reporting. Returned by `Services.ConfigureNew()`. | -| `ServiceCreator(Of T)` | Generic class | The dispatcher's factory: `T` must implement `ITbService`; `CreateInstance` returns `New T`. | -| `ServiceState` | Class | Read-only state snapshot. Constructor (called via `Services.QueryStateOfService(Name)`) queries the SCM. | -| `ITbService` | Public Interface | The contract every service class implements: `EntryPoint`, `StartupFailed`, `ChangeState`. | -| `ServiceTypeConstants` | Enum | `tbServiceTypeOwnProcess`, `tbServiceTypeShareProcess`, etc. | -| `ServiceStartConstants` | Enum | `tbServiceStartAuto`, `tbServiceStartOnDemand`, etc. | -| `ServiceControlCodeConstants` | Enum | `vbServiceControlStop`, `vbServiceControlPause`, `vbServiceControlContinue`, etc. | -| `ServiceStatusConstants` | Enum | `vbServiceStatusRunning`, `vbServiceStatusStartPending`, `vbServiceStatusStopped`, etc. | - -The two private interfaces (`IServiceCreator`, `IServiceManagerInternal`) are pure implementation detail — same situation as `WinNamedPipesLib`'s `INamedPipe*Internal` interfaces. **No doc page**, and don't surface the underscored implementing members on the concrete classes either. - -The two `Private Module` declarations (`ServicesAPIs`, `ServicesConstants`) and the `Private Module ServicesHelper` are all internal — **no doc page**. - -#### `Services` public members - -`[PredeclaredId]` Class. The compiler instantiates a singleton named `Services` at program start; user code calls `Services.X` directly without `New`. The class also doubles as an enumerable collection of the `ServiceManager` instances that have been configured (`For Each manager In Services`). - -**Public methods**: - -- `Function ConfigureNew() As ServiceManager` — *"Use this method to configure a service. Usually used during app startup."* Allocates a new `ServiceManager`, adds it to the internal collection, returns it. Typical use: `With Services.ConfigureNew : .Name = "MyService" : .InstanceCreator = New ServiceCreator(Of MyServiceClass) : End With`. -- `Sub RunServiceDispatcher()` — *"This method hands over to the OS for managing the starting/stopping of services via the main thread. This is a BLOCKING call, until the OS wants to shutdown the service EXE."* Builds a `SERVICE_TABLE_ENTRYW` from every configured `ServiceManager` and calls `StartServiceCtrlDispatcherW`. Returns only when the OS terminates the service host. Raises run-time error 5 if the dispatcher cannot start (typically when the EXE was launched normally rather than by the SCM). -- `Sub InstallAll()` — *"This method tries to register ALL of the configured services onto the system."* Iterates the configured `ServiceManager`s and calls `.Install()` on each. Requires admin. -- `Sub UninstallAll()` — *"This method tries to unregister ALL of the configured services off the system."* Iterates and calls `.Uninstall()` on each. Requires admin. -- `Function QueryStateOfService(ByVal ServiceName As String) As ServiceState` — returns a fresh `ServiceState` snapshot. Raises run-time error 5 if the service isn't installed. -- `Sub LaunchService(ByVal ServiceName As String, ParamArray LaunchArgs())` — start an installed service by name, optionally passing launch arguments through to its `ServiceManager.LaunchArgs()` field. Wraps `OpenServiceW(SERVICE_START)` + `StartServiceW`. Raises run-time error 5 on permission / not-installed / already-running. -- `Sub ControlService(ByVal ServiceName As String, ByVal ControlCode As ServiceControlCodeConstants)` — send an SCM control code to a running service. The required SCM permission is derived from the control code automatically (`SERVICE_STOP` for `vbServiceControlStop`, `SERVICE_PAUSE_CONTINUE` for the pause / continue / netbind / paramchange family, `SERVICE_INTERROGATE` for `vbServiceControlInterrogate`, `SERVICE_USER_DEFINED_CONTROL` for codes 128–255, `SERVICE_ALL_ACCESS` otherwise). For `vbServiceControlStop` the wrapper fills `SERVICE_CONTROL_STATUS_REASON_PARAMSW` with `SERVICE_STOP_REASON_FLAG_PLANNED | MAJOR_NONE | MINOR_NONE` — there is a `FIXME` to allow customising the reason code. - -**Public properties**: - -- `Property Get GetConfiguredService(ByVal Name As String) As ServiceManager` — look up a previously-configured `ServiceManager` by its `Name`. Raises run-time error 5 if not found. (Despite the `Get` syntax the lookup is parameterised by name; it's a property in name only.) - -**Public enumerator**: - -- `Property Get _NewEnum() As Variant` — `[Enumerator]`-tagged; enables `For Each manager In Services` over the configured `ServiceManager`s. *"Provides For-Each support for the services collection, exposing each configured service as a ServiceManager instance."* - -#### `ServiceManager` public members - -`[COMCreatable(False)]`. User code never instantiates this directly — `Services.ConfigureNew()` returns it. The source-side constructor carries `[Description("For internal use. Dont create instances of ServiceManager manually, use Services.ConfigureNew instead")]` — surface that on the page intro. - -**Public field** (one): - -- `LaunchArgs() As String` — populated by `ServiceEntryPoint` from the `argv` the SCM hands over. `LaunchArgs(0)` is the *first user-supplied* argument (the SCM-supplied service name at `argv[0]` is dropped). The example uses it to gate startup: `If Join(ServiceManager.LaunchArgs) <> "MySecretPassword" Then …`. - -**Public properties** (each carries a `[Description("...")]`): - -- `InstanceCreator As IServiceCreator` (Get / Let / Set) — *"Set this to an instance of the ServiceCreator class to allow the OS to launch the instance of your service."* Typically `.InstanceCreator = New ServiceCreator(Of MyServiceClass)`. -- `Name As String` (Get / Let) — *"The name of the service, as listed in the OS services database."* -- `Description As String` (Get / Let) — *"The description of the service, as listed in the OS services database."* Applied via `ChangeServiceConfig2W(SERVICE_CONFIG_DESCRIPTION)` on every successful `Install()`. -- `Type As ServiceTypeConstants` (Get / Let) — *"The type of the service, typically `tbServiceTypeOwnProcess` or `tbServiceTypeShareProcess`."* Defaults to `tbServiceTypeOwnProcess`. -- `InstallStartMode As ServiceStartConstants` (Get / Let) — *"The start-mode of the service, typically `tbServiceStartOnDemand` or `tbServiceStartAuto`."* Defaults to `tbServiceStartOnDemand`. -- `InstallCmdLine As String` (Get / Let) — *"The command line arguments passed to the service EXE when the OS launches the service."* Defaults to `""""""`. **Usually overridden to add a discriminator argument** like `-startService` so the EXE knows whether it was launched by the SCM (run dispatcher) or by a user (show UI). Example: `.InstallCmdLine = """" & App.ModulePath & """ -startService"`. -- `DependentServices() As Variant` (Get / Let) — *"A list of dependent services that this service requires to be started before this service is launched (dependent services are auto-launched by the OS)."* Pass an `Array("OtherSvc1", "OtherSvc2")`. The setter stashes it; `Install()` packs it into a double-null-terminated string and hands it to `CreateServiceW`. -- `AutoInitializeCOM As Boolean` (Get / Let) — *"When TRUE, COM will be initialized for you on the new service thread in STA mode."* Defaults to `True`. Set to `False` if your service needs a different apartment model (call `CoInitializeEx` yourself from `EntryPoint`). -- `SupportsPausing As Boolean` (Get / Let) — *"When TRUE, the SCM will send `SERVICE_CONTROL_PAUSE` / `SERVICE_CONTROL_CONTINUE` notifications."* Defaults to `False`. The setter calls `ResyncStatus()` so toggling it mid-run takes effect immediately. (Most services set this to `True` once inside `EntryPoint` and then handle `vbServiceControlPause` / `vbServiceControlContinue` in `ChangeState`.) - -**Public methods**: - -- `Sub Install()` — *"This method attempts to install the configured service on the system."* Opens the SCM with `SC_MANAGER_CONNECT Or SC_MANAGER_CREATE_SERVICE`, calls `CreateServiceW`. If the service already exists, deletes it (via `OpenServiceW(SERVICE_DELETE)` + `DeleteService`) and **retries** the create — so `Install()` is effectively re-entrant / safe to call multiple times. On successful create, sets the description via `ChangeServiceConfig2W`. Raises run-time error 5 on permissions failure or unrecoverable create failure. **Requires admin elevation.** -- `Sub Uninstall()` — *"This method attempts to uninstall the configured service on the system."* Opens the SCM, opens the service with `SERVICE_DELETE`, calls `DeleteService`. Raises run-time error 5 if the service isn't registered or on permissions failure. **Requires admin elevation.** -- `Sub ReportStatus(ByVal dwCurrentState As ServiceStatusConstants, Optional ByVal dwWin32ExitCode As Long = ERRORCODE_NO_ERROR, Optional ByVal dwWaitHint As Long = 0)` — *"This method informs the OS of the current state of the service."* The user's `EntryPoint` is **required** to call `ReportStatus(vbServiceStatusRunning)` once steady-state is reached and `ReportStatus(vbServiceStatusStopped)` once shut-down completes; long start-up sequences should also call `ReportStatus(vbServiceStatusStartPending, , )` periodically to keep the SCM from killing the service. The `dwControlsAccepted` field of `SERVICE_STATUS` is filled automatically from the state and from `SupportsPausing` (Stop is always accepted except during `StartPending`; Pause/Continue is gated on `SupportsPausing`). The `dwCheckPoint` field auto-increments for pending states and resets on `Running`/`Stopped`. -- `Sub ResyncStatus()` — re-applies the cached `SERVICE_STATUS` to the SCM via `SetServiceStatus`. Called automatically from `ReportStatus` and from the `SupportsPausing` setter. User code rarely needs to call this directly; mention it for completeness. - -The class also carries two methods that are technically `Public`-by-default (no modifier) but are invoked only by the OS dispatcher / the package's own trampoline — `ServiceEntryPoint(ByVal dwArgc As Long, ByVal lpszArgv As LongPtr)` and `ServiceControlHandlerCallback(ByVal dwControl As Long, ByVal dwEventType As Long, ByVal lpEventData As LongPtr)`. **Do not list these as user-facing methods**; mention them at the very end of the page under "Internal hooks" with a `> [!NOTE]` saying the OS / package infrastructure invokes them and user code never calls them. - -#### `ServiceCreator(Of T)` public members - -Generic class. `[COMCreatable(False)]`. `[Description("This class allows the service manager to create an instance of a particular service on-demand as needed")]` is the source intro. Tagged with the EA magic-byte `[ClassId("66170220-FEF3-4257-8FBA-EAEAEAEAEAEA")]` — same compiler-special-handling treatment as `WinEventLogLib`'s `EventLog(Of T1, T2)`. Do not surface the `ClassId` on the page. - -Type parameter constraint: `T` must implement `ITbService`. There is no syntactic `Where T : ITbService` constraint expressed in the source, but `Function CreateInstance() As ITbService` returning `New T` only compiles when `T` implements `ITbService` — flag this as the practical constraint on the page. - -**Public method**: - -- `Function CreateInstance() As ITbService` — `Implements IServiceCreator.CreateInstance`. Returns `New T`. Called once per service start by the package's dispatcher trampoline. User code never calls this directly; the typical usage is `.InstanceCreator = New ServiceCreator(Of MyServiceClass)` on a freshly-allocated `ServiceManager`. - -The page should be small (the surface is one method) and largely focused on explaining the `Of T` parameterisation + the `T : ITbService` constraint + how it slots into `ServiceManager.InstanceCreator`. - -#### `ServiceState` public members - -`[COMCreatable(False)]`. Returned by `Services.QueryStateOfService(Name)`. The constructor takes the service name, opens the SCM with `SC_MANAGER_CONNECT`, opens the service with `SERVICE_QUERY_STATUS`, calls `QueryServiceStatusEx(SC_STATUS_PROCESS_INFO, ...)`, and snapshots a `SERVICE_STATUS_PROCESS` struct. **The snapshot is taken once at construction time and never refreshed** — to see updated state, call `Services.QueryStateOfService` again. - -The constructor raises run-time error 5 with descriptive messages on three failure modes: SCM open failed (*"Unable to open the Service manager..."*), service not installed (*"Service '' is not installed on this system"*), status query failed (*"Unable to query the service state"*). - -**Public properties** (all read-only `Get`): - -- `Type As ServiceTypeConstants` — the SCM-reported service type. -- `CurrentState As Long` — the SCM-reported state, but typed `Long` rather than `ServiceStatusConstants`. **Source carries a `' FIXME` comment** — surface as a `> [!NOTE]` that this returns the underlying `Long` value (which happens to match the `ServiceStatusConstants` enum values), and that callers wanting type-safety can `CType(state.CurrentState, ServiceStatusConstants)`. -- `CurrentStateText As String` — human-readable text: `"RUNNING"`, `"STOPPED"`, `"STARTING"`, `"STOPPING"`, `"PAUSED"`, `"PAUSING"`, `"CONTINUING"`, `"UNKNOWN STATE ()"`. -- `ControlsAccepted As Long` — bitmask of `SERVICE_ACCEPT_*` flags. **Source carries a `' FIXME` comment** — surface the same way as `CurrentState`. -- `ExitCode As Long` — the `dwWin32ExitCode` field. The Win32 documented sentinel `ERROR_SERVICE_SPECIFIC_ERROR` (`1066`) means "see `ServiceSpecificExitCode`". -- `ServiceSpecificExitCode As Long` — the service-defined exit code when `ExitCode = ERROR_SERVICE_SPECIFIC_ERROR`. Otherwise meaningless. -- `CheckPoint As Long` — the `dwCheckPoint` field; increments while the service is in a pending state and resets at steady state. -- `WaitHint As Long` — the `dwWaitHint` milliseconds field. -- `ProcessId As Long` — the OS process ID hosting the service (0 if not running). -- `Flags As Long` — the `dwServiceFlags` field (currently `SERVICE_RUNS_IN_SYSTEM_PROCESS = 1` is the only documented bit). - -#### `ITbService` public members - -`Public Interface`. Tagged `[InterfaceId("5F137E12-5164-452E-911A-6FD9BF20EC81")]`. Description: *"All services must implement `ITbService`."* The contract is three subs: - -- `Sub EntryPoint(ByVal ServiceContext As ServiceManager)` — the main service body. Called by the package's dispatcher trampoline once the SCM has finished start-up handshaking. **Runs on the service thread** (a separate thread from the dispatcher). Inside this sub the implementor: - 1. Optionally validates startup conditions (e.g. checks `ServiceContext.LaunchArgs`). - 2. Calls `ServiceContext.ReportStatus(vbServiceStatusRunning)` once steady-state is reached (the dispatcher trampoline reports `vbServiceStatusStartPending` automatically before calling `EntryPoint`). - 3. Runs the long-running work loop. For pipe-server services this is the `NamedPipeServer.ManualMessageLoopEnter()` blocking call; for other services it might be a `Do While IsStopping = False` loop with a wait primitive. - 4. Calls `ServiceContext.ReportStatus(vbServiceStatusStopped)` before returning. -- `Sub StartupFailed(ByVal ServiceContext As ServiceManager)` — called if `RegisterServiceCtrlHandlerExW` failed (the control handler couldn't be hooked, e.g. the service was launched outside the SCM context). Typical implementation: log a failure event. Don't try to `ReportStatus` from here — the status handle is invalid. -- `Sub ChangeState(ByVal ServiceContext As ServiceManager, ByVal dwControl As ServiceControlCodeConstants, ByVal dwEventType As Long, ByVal lpEventData As LongPtr)` — the control-code dispatcher. **Runs on the main (dispatcher) thread**, not on the service thread. Typical pattern: `Select Case dwControl` over `vbServiceControlStop` / `vbServiceControlShutdown` / `vbServiceControlPause` / `vbServiceControlContinue`, set shared `Public` flags (`IsStopping`, `IsPaused`), call `ServiceContext.ReportStatus` to acknowledge the transition, signal the service thread to react (e.g. `NamedPipeServer.ManualMessageLoopLeave()`). The `dwEventType` + `lpEventData` parameters carry the event-specific payload for the codes that need it (`SERVICE_CONTROL_DEVICEEVENT`, `SERVICE_CONTROL_POWEREVENT`, `SERVICE_CONTROL_SESSIONCHANGE`, `SERVICE_CONTROL_HARDWAREPROFILECHANGE` — see Microsoft's `HandlerEx` documentation for the data layouts). - -**The two-thread split is the single most important fact about the interface** — every page entry should reinforce it. The example uses `Public IsPaused As Boolean` + `Public IsStopping As Boolean` shared fields on the service class to ferry state between the two threads, which is the documented pattern. - -#### Enumerations - -Public enums (in `Public Module ServicesConstantsPublic`), one page each under `docs/Reference/WinServicesLib/Enumerations/`: - -- `ServiceTypeConstants` — `tbServiceTypeAdapter`, `tbServiceTypeSystemDriver`, `tbServiceTypeKernelDriver`, `tbServiceTypeRecognizerDriver`, `tbServiceTypeOwnProcess`, `tbServiceTypeShareProcess`, `tbServiceTypeOwnProcessInteractive`, `tbServiceTypeShareProcessInteractive`. The driver values (`tbServiceTypeSystemDriver`, `tbServiceTypeKernelDriver`, `tbServiceTypeRecognizerDriver`, `tbServiceTypeAdapter`) are only meaningful when registering a kernel-mode driver — twinBASIC services compile to a user-mode EXE and should use `tbServiceTypeOwnProcess` (one service per EXE) or `tbServiceTypeShareProcess` (multiple services hosted in one EXE; the example uses this). The `Interactive` variants are kept for compatibility but Windows Vista and later disallow them; flag with a `> [!NOTE]`. -- `ServiceStartConstants` — `tbServiceStartAuto`, `tbServiceStartBoot`, `tbServiceStartOnDemand`, `tbServiceStartDisabled`, `tbServiceStartDriverSystem`. `tbServiceStartBoot` and `tbServiceStartDriverSystem` only apply to kernel drivers. -- `ServiceControlCodeConstants` — 18 values mirroring the Win32 `SERVICE_CONTROL_*` constants. Source-side prefix is `vbServiceControl*` (carried over from VB6 — note the prefix is `vb`, not `tb`, in this enum; surface as-is, don't try to rationalise). -- `ServiceStatusConstants` — `vbServiceStatusStopped`, `vbServiceStatusStartPending`, `vbServiceStatusStopPending`, `vbServiceStatusRunning`, `vbServiceStatusContinuePending`, `vbServiceStatusPausePending`, `vbServiceStatusPaused`. Same `vb` prefix. - -Format pages like `WebView2/Enumerations/wv2PrintOrientation.md` — single intro paragraph, a value table with `{: #vbServiceXxx }` anchors per row for deep-linking. - -#### Doc-side layout (folders / files) - -Ten pages total: - -``` -docs/Reference/WinServicesLib/ - index.md ← package landing; lifecycle + dual-thread model + install / launch flows + integration cross-links (event log + named pipes) - Services.md ← the predeclared singleton - ServiceManager.md ← per-service configuration + ReportStatus - ServiceCreator.md ← Of T generic factory - ServiceState.md ← read-only state snapshot - ITbService.md ← interface every service implements - Enumerations/index.md - Enumerations/ServiceTypeConstants.md - Enumerations/ServiceStartConstants.md - Enumerations/ServiceControlCodeConstants.md - Enumerations/ServiceStatusConstants.md -``` - -All five concrete pages are single-file (no folder-style — no natural sub-pages; each class's surface is medium-sized). - -The `index.md` should be substantial and walk the reader through: - -1. **What a Windows service is** (one paragraph: long-running background process supervised by the SCM, started before/independently of user logon, controlled via the Services control panel applet or `sc.exe`). -2. **Lifecycle**: configure (`Services.ConfigureNew`) → install (`Services.InstallAll` or per-manager `.Install`, **elevated**) → run (`Services.RunServiceDispatcher` blocks the EXE's main thread; SCM launches the service thread on demand). The example's `If InStr(Command, "-startService") > 0` branch is the canonical "same EXE for install-time UI and run-time service" pattern. -3. **The two-thread split**: `EntryPoint` and `ChangeState` run on different threads; surface this prominently with a small diagram or numbered explanation. -4. **Integration with the sister packages**: cross-link `Implements EventLog(Of …) Via EventLog = New EventLog(…)` (see `WinEventLogLib`) and the `NamedPipeServer.ManualMessageLoopEnter`/`Leave` service-hosting idiom (see `WinNamedPipesLib`). The worked example at `..\tbrepro\winlibs\tbServiceTest2\Sources\` ties all three together. - -**Naming:** - -- Folder / URL segment: `WinServicesLib/` (matches the source-side package name; no `Package` suffix to drop, same as `WinEventLogLib` and `WinNamedPipesLib`). -- Index title: `WinServicesLib Package` — the ` Package` convention. -- Permalinks: `/tB/Packages/WinServicesLib/` for the landing; `/tB/Packages/WinServicesLib/` for each class page; `/tB/Packages/WinServicesLib/Enumerations/` for each enum page. -- `parent: WinServicesLib Package` on each top-level child page. The four enum pages set `parent: Enumerations` and `grand_parent: WinServicesLib Package` (the grouped-page pattern; same shape as the WebView2 / CEF / CustomControls `Enumerations/` directories). - -**License:** MIT (copyright Wayne Phillips T/A iTech Masters, 2025; first release v0.1, 04-FEB-2025) — same situation as every other Wayne Phillips package. Pages are fully original content; **omit** the `vba_attribution: true` flag. - -### tbIDE - -The **addin SDK** — a type-only compiler package. Every public symbol is an interface or a CoClass; the actual implementations live in the IDE binary, and an addin DLL binds against the type declarations and lets the IDE marshal calls into its implementations at run time. Twenty-four flat `.twin` files in `..\tbrepro\sample10\WaynesWorldAddInTest1\Packages\tbIDE\Sources\`, each 8–57 lines (501 lines total) — there is no plumbing to skip, every file declares user-facing types. - -The `CHANGELOG.md` shipped in the package is a leftover copy-paste from a different package ("twinBASIC WinNativeForms") and is **not** about tbIDE — **do not** propagate that history onto the docs. - -#### How an addin is built and loaded - -From any of the six sample `Settings` files (the structure is identical across them): - -- `project.buildType`: **Standard DLL** — addins are not packages, they are DLLs that the IDE loads. -- `project.buildPath`: `${IdePath}\addins\${Architecture}\${ProjectName}.${FileExtension}` — the build output drops directly into `\addins\Win32\` or `\addins\Win64\`. The IDE scans this folder on startup. -- `project.references` includes the tbIDE compiler package: `id: {99DEC38C-75F6-4488-8EE7-2D52D83881D2}`, `isCompilerPackage: true`, `publisher: TWINBASIC-COMPILER`, `symbolId: tbIDE`. Same shape that `CustomControls` uses. - -The DLL **must** export a single factory function the IDE will call: - -```tb -Module MainModule - [DllExport] - Public Function tbCreateCompilerAddin(ByVal Host As Host) As AddIn - Return New MyAddinClass(Host) - End Function -End Module -``` - -The returned object must implement the [`AddIn`](#addin) interface (a single read-only `Name` property). The IDE releases the object when the addin is disabled or the IDE shuts down. Every sample uses this exact `tbCreateCompilerAddin` skeleton — surface it on the index landing as the canonical entry point. - -#### Public user-facing surface - -Twenty-four files declaring twenty-three public CoClasses + one concrete `Class` + one interface-only declaration: - -| File | Public symbols | Role | -|------------------------------|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| -| `Addin.twin` | `IAddInV1` + `AddIn` CoClass | The contract every addin's main class implements. One member: `Property Get Name() As String`. | -| `Host.twin` | `IHostV1` + `ItbHostEventsV1/V2/V3` + `Host` CoClass | Root of the API — passed to `tbCreateCompilerAddin`. Versioned events (see "Versioned event interfaces" below). | -| `AddinTimer.twin` | `Class AddinTimer` (no CoClass) | **The only concrete instantiable class** in the package. `New AddinTimer`; sets `Interval` (ms) + `Enabled`; fires `Timer` event. Internally wraps `SetTimer`/`KillTimer`. | -| `Button.twin` | `IButtonV1` + `IButtonEventsV1` + `Button` CoClass | Toolbar button. Returned by `Toolbar.AddButton`. `OnClick` event. | -| `CodeEditor.twin` | `ICodeEditorV1 Extends IEditorV1` + `CodeEditor` CoClass | A code-pane editor — selection, text, Monaco passthrough, `AddMonacoWidget` for inline overlay HTML. Nested `RevealArea` enum. | -| `DebugConsole.twin` | `IDebugConsoleV1` + `DebugConsole` CoClass | The DEBUG CONSOLE pane. `PrintText` (with optional colour), `Clear`, `SetFocus`. | -| `Editor.twin` | `IEditorV1` + `Editor` CoClass | Base interface for editors. Source-side comment: *"Castable to CodeEditor etc."* — i.e. `Dim ce As CodeEditor = editor` works because the underlying object implements both. | -| `Editors.twin` | `IEditorsV1` + `Editors` CoClass | Collection of active editors. `Item(Index)` default member, `Count`, `Open(Path, Line, Col, Options)`. Source-side note: *"There is currently only ONE active editor, accessible via Editors(0) syntax"*. | -| `File.twin` | `IFileV1` + `IFileV2 Extends IFileV1` + `File` CoClass | A virtual-FS file. V1: `Data` / `DataLen` / `Text` / `IsDirty`. V2 adds `ReadText(ReadTextFlags)`. Nested `ReadTextFlags` enum (one flag: `CommentsToWhitespace`). | -| `FileSystem.twin` | `IFileSystemV1` + `FileSystem` CoClass | Tiny — `RootFolder` and `ResolvePath(Path)` (path needs the `twinbasic:/` prefix). | -| `FileSystemItem.twin` | `IFileSystemItemV1` + `FileSystemItem` CoClass | Base for `File` and `Folder`. `Name`, `Path`, `Type`, `Parent`. Nested `FileSystemItemType` enum (`Folder`, `FileVIRTUALDOC`, `FileOTHER`, `FileTWIN`, `FileBAS`, `FileCLS`, `FileUIDESIGNER`, `FileJSON`). | -| `Folder.twin` | `IFolderV1 Extends IFileSystemItemV1` + `Folder` CoClass | `Count`, `Item(IndexOrName)`, `IsPackagesFolder`, plus for-each enumeration over `FileSystemItem` children. | -| `HtmlElement.twin` | `IHtmlElementV1` + `HtmlElement` CoClass | A DOM element inside a tool window. `Properties` (default member — see below), `ChildDomElements`, `Remove`, `AddEventListener(DomEventName, CallbackFunc, Optional Data)`. Plus one `[Hidden]` legacy `AddEventListenerOLD1`. | -| `HtmlElementProperties.twin` | `IHtmlElementPropertiesV1` (`[COMExtensible(True)]`) + CoClass | The dynamic property bag on a DOM element. `Item(DomPropertyName)` is the default member; the `[COMExtensible(True)]` attribute is what makes `.style.display = "flex"` resolve through `Item("style").Item("display")` at run time. | -| `HtmlElementProperty.twin` | `IHtmlElementPropertyV1` (`[COMExtensible(True)]`) + CoClass | One value in the bag. `Value` (Get/Let, default member), plus nested `Properties` for chained access (`.style.color = "white"`). | -| `HtmlElements.twin` | `IHtmlElementsV1` + `HtmlElements` CoClass | The `ChildDomElements` collection. `Item(ID)` default, `Add(ElementID, TagName)` returns the new child. Note `TagName` accepts both standard HTML tags AND the IDE's custom widget tags `chartjs` / `monaco` / `listview` / `virtuallistview`. | -| `IHtmlEventProperties.twin` | `IHtmlEventPropertiesV1` (`[COMExtensible(True)]`) + `HtmlEventProperties` CoClass | The event-payload bag. Like `HtmlElementProperties` but read-only and used inside event handler callbacks. | -| `IHtmlEventProperty.twin` | `IHtmlEventPropertyV1` (`[COMExtensible(True)]`) + `HtmlEventProperty` CoClass | One value in the event bag. | -| `KeyboardShortcuts.twin` | `IKeyboardShortcutsV1` + `KeyboardShortcuts` CoClass | Single member: `Add(keyString, Callback As LongPtr)`. `keyString` is a literal key like `"{CTRL}{SHIFT}d"` (prefixes `{CTRL}` / `{SHIFT}` / `{ALT}`). | -| `Project.twin` | `IProjectV1` + `Project` CoClass | The currently-loaded project. Lifecycle (`Save`, `Close`, `Build`, `Clean`), introspection (`Path`, `Name`, `BaseFolderPath`, `ProjectID`, version + architecture + build-output info), `Evaluate(ExprString)` (debug-console-style expression evaluation), `RootFolder` (entry into the virtual FS), and `LoadMetaData`/`SaveMetaData` (persistent per-addin key/value storage inside the `.twinproj`). Nested `VbBuildType` enum. | -| `Themes.twin` | `IThemesV1` + `Themes` CoClass | `ActiveThemeName` ("Classic" / "Dark" / "Light"), `ActiveThemeNameGroup` ("dark" / "light"). The `Host` events interface fires `OnChangedTheme` when this flips. | -| `ToolWindow.twin` | `IToolWindowV1` + `IToolWindowEventsV1` + `ToolWindow` CoClass | A dockable / floating tool window. `Title`, `Visible`, `Resizable`, `Close`, `ApplyCss(stylesString)`, `RootDomElement` (default member — the entry into the DOM tree). `OnClose` event. | -| `ToolWindows.twin` | `IToolWindowsV1` + `ToolWindows` CoClass | Single member: `Add(Name, Optional UniqueIdForPositionPersistance) As ToolWindow`. The second argument lets the IDE remember the floating-window position across IDE restarts. | -| `Toolbar.twin` | `IToolbarV1` + `Toolbar` CoClass | `AddSplitter` (vertical-bar separator), `AddButton(Id, Caption, Optional IconData)`. | -| `Toolbars.twin` | `IToolbarsV1` + `Toolbars` CoClass | `Item(Index)` default, `Count`. Source-side note: *"There is currently only ONE toolbar, accessible via the Toolbars(0) syntax"*. | - -#### The interface/CoClass split — what it means for the doc layout - -Almost every `.twin` declares one or two `Public Interface IV1 Extends stdole.IUnknown` followed by `Public CoClass ` with `[Default] Interface IV1` (and optionally `[Default, Source] Interface IEventsV1`). The pattern is the standard COM idiom for late-binding-friendly extensibility — the IDE implements the interfaces, the addin holds references typed at the CoClass. - -**The interfaces themselves get no doc page.** Users type their variables `As Host` / `As Button` / `As ToolWindow` (the CoClass), not `As IHostV1`. Fold the interface's members onto the CoClass's page; do not list both. Same convention CustomControls uses for its `_…` default interfaces. - -**Versioning is conveyed by interface chains.** Two cases visible in the source: - -- `IFileV1` → `IFileV2 Extends IFileV1` (V2 adds `ReadText(ReadTextFlags)`). The `File` CoClass declares `[Default] Interface IFileV2`. Document the V2 surface as the canonical `File` page; do not split V1 vs V2. (Mention in passing that `ReadText` is V2-only and consequently won't bind against very early IDE builds — though in practice every shipping IDE is V2+.) -- `IHostV1` → `ItbHostEventsV1` → `ItbHostEventsV2 Extends V1` → `ItbHostEventsV3 Extends V2`. The `Host` CoClass declares `[Default, Source] Interface ItbHostEventsV3`. The new members on V2 / V3 (`OnChangedActiveEditor`, `OnChangedTheme`) are each tagged **`[AllowUnpopulatedVtableEntry]`**, which is the mechanism that lets a newer addin compile against `ItbHostEventsV3` and still load against an older IDE that only implements `V1` — the IDE doesn't have to provide the V2/V3 entries. - -Document all `Host` events together on the `Host.md` page (the per-version split is a compatibility detail, not a user-facing concept). - -#### `AddinTimer` is the only user-instantiable class - -Every other public symbol is a CoClass exposed *to* the addin by the IDE — the addin receives instances via `Host`, never constructs them. `AddinTimer` is the exception: it's a concrete `Class AddinTimer` (not a CoClass) and the addin instantiates it with `New AddinTimer`. Internally it wraps `SetTimer` / `KillTimer` with a private `TimerCallback`, exposes `Interval` (ms) + `Enabled`, and fires a `Timer` event. - -Sample 11's CPU-monitor demonstrates the typical pattern: - -```tb -Private WithEvents Timer As AddinTimer -… -Set Timer = New AddinTimer -Timer.Interval = 500 -Timer.Enabled = True -… -Private Sub Timer_Timer() - ' fires every 500 ms -End Sub -Private Sub myToolWindow_OnClose() - Set Timer = Nothing ' stop the timer when the window closes -End Sub -``` - -The class uses the `Handles` syntax internally (`Private Sub Changed() Handles Enabled.OnPropertyLet, Interval.OnPropertyLet`) so any change to `Enabled` or `Interval` re-arms the underlying timer — surface this as *"set `Enabled = False` to stop, change `Interval` at any time"*, not as an implementation detail. - -`Class_Terminate` calls `KillTimer` so a dropped reference is sufficient to stop. Sample 15 demonstrates that direct Win32 `SetTimer` / `KillTimer` is also fine if `AddinTimer` doesn't fit — both patterns are valid; the package doesn't *require* the helper. - -#### The HTML / DOM surface - -Tool windows are rendered as HTML inside the IDE (the same browser surface the IDE uses for its own panes). The `HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` quartet is the addin's keyhole into the DOM. - -Three things make this surface unusual and have to be surfaced on the docs: - -1. **`[COMExtensible(True)]` on `HtmlElementProperties` / `HtmlElementProperty` / `HtmlEventProperties` / `HtmlEventProperty`.** The attribute opts the interface into IDispatch dynamic-member resolution, which is what makes `.style.display = "flex"` work — at compile time the right-hand `.style.display` resolves to `Item("style").Item("display").Value = "flex"` (the default-member dance), and the IDE resolves the names against the live DOM at run time. No member named `style` is *declared* on `IHtmlElementPropertiesV1`. Surface this on each of the four pages with a `> [!IMPORTANT]` callout: the property names accepted are **every DOM property of the underlying tag** (standard HTML attributes, CSS-style properties under `.style.…`, plus any custom-element-specific surface like `.chart.data.datasets(0).borderWidth` on a `chartjs` element or `.editor.setOption(...)` on a `monaco` element). The docs cannot enumerate them — refer the reader to the relevant DOM / library reference. -2. **The custom-element tags.** `HtmlElements.Add(id, tagName)` accepts standard HTML tags (`"div"`, `"input"`, `"span"`, `"h1"`, …) AND four IDE-specific widget tags: `"chartjs"` (Chart.js wrapper — surfaces a `.chart` property), `"monaco"` (the Monaco editor — surfaces a `.editor` property), `"listview"` (the IDE's listview widget — surfaces a `.listview` property with `addItem` / `removeItem` / etc.), and `"virtuallistview"` (the same with `setItemCount` + the `onAsyncGetItemHTML` event). All four are demonstrated in samples 11–14. Surface as *"the tag name is forwarded to the IDE's tool-window renderer, which understands the standard HTML tags plus the custom widget tags … see sample 11 / 12 / 13 / 14"*. -3. **`AddEventListener(DomEventName As String, CallbackFunc As LongPtr, Optional Data As Variant)`.** The callback is passed as `AddressOf SomeSub`, and `SomeSub` must have the signature `Sub(ByVal eventInfo As HtmlEventProperties)`. The `eventInfo` parameter is the IDE-marshalled equivalent of the JavaScript `Event` object — `eventInfo.key` / `eventInfo.target.value` / `eventInfo.target.id` are the usual fields, but again the property names are resolved against the *actual* event object at run time, not declared statically. Sample 13 also demonstrates **custom event names raised from inline HTML** via the IDE-side `raiseEvent("name", event, stopPropagation, …customData)` JavaScript helper; the custom-data values become `eventInfo.customData0`, `eventInfo.customData1`, … and are demonstrated in sample 15's `ClickedMatch` handler. Sample 14 demonstrates **async events** via `eventInfo.setAsyncResult("")` (the listener returns the requested HTML asynchronously back into the virtual list view's render cycle). - -Document the four `Html*` classes as the *contract* (`Item` default member, the `Value` accessor, the `Properties` chaining) and use the samples to illustrate the dynamic-resolution mechanism. Do not try to enumerate the resolved property surface — it's open-ended. - -The `HtmlEvent*` half of the quartet declares `Value` as **read-only Get** (vs `HtmlElementProperty`'s `Value` which has Get + Let) — that's the contract distinction between an inbound event payload and an outbound DOM property setter. Note this on each page. - -#### ToolWindow as a jQuery-style selector - -`ToolWindow` is the *root* of a tool window's DOM and **also doubles as a member-by-ID accessor**: `myToolWindow("#dataEntry").Value` (see sample 13) returns the `Value` of the child element whose `id` is `dataEntry`. There is no explicit member on `IToolWindowV1` that takes a string argument — the source-side `RootDomElement` carries `[DefaultMember]`, so `myToolWindow("#dataEntry")` resolves to `RootDomElement.Properties.Item("#dataEntry")`, which the dynamic resolver then interprets as a CSS-style selector against the rendered DOM. Surface this as *"the tool window's default member is `RootDomElement`, which is COM-extensible — string indexing accepts CSS selectors"* and call out the `#id` (single element by ID) form as the most common case. - -`ApplyCss(styles As String)` injects a `