Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,16 @@ before:
`src` directory when the Bsn.Common sibling exists) BEFORE
dot-sourcing function files - keep that self-heal in place.
- The Bsn.Host ConPTY engine (`src/Bsn.Host/src/PtyRecorder.cs`) must
restore the console input mode in `finally`, close the PTY-side pipe
restore the console input mode AND the console output codepage in
`finally` (the recording temporarily switches the real console to
UTF-8 so the live proxy view decodes correctly on OEM-codepage
consoles), close the PTY-side pipe
handles after CreatePseudoConsole (EchoCon pattern), and emit cast files
as UTF-8 without BOM with LF line endings. Cast files support asciicast
as UTF-8 without BOM with LF line endings.
`Show-InteractiveTranscript` follows the same save/switch/restore
discipline for its playback writes (Windows PowerShell 5.1 encodes
`[Console]` output through the console output codepage, which turns
CP437-unrepresentable characters into '?'). Cast files support asciicast
v2 (absolute event times) and v3 (intervals since the previous event,
millisecond precision, trailing 'x' exit event) - keep both writers in
sync with ConvertTo-BsnAsciicast. `ConvertTo-BsnAsciicast` and
Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@ All notable changes to the modules in this repository are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and the modules adhere to [Semantic Versioning](https://semver.org/).

## [Bsn.Host 0.2.3] - 2026-11-07

### Fixed

- The LIVE recording view (what you watch while the recording runs) no
longer turns box-drawing text into CP437 mojibake on OEM-codepage
consoles (cmd, Windows PowerShell 5.1 hosts, fresh hidden consoles).
The cast file was always correct - every 0.2.2 capture test passed -
but the ConPTY proxy writes the stream to the real console as raw
UTF-8 bytes, and the console decodes those bytes with its own output
codepage (OEM 437), rendering `E2 94 80` as `Gamma-ohmega-acute`.
The engine now switches the console output codepage to UTF-8 for the
duration of the recording and restores the original value afterwards
(same restore discipline as the console input mode). Reportedly hit
with OpenCode after installing 0.2.2 from the gallery.
[#5](https://github.com/BSVN/Lmt.Commons/issues/5)
- `Show-InteractiveTranscript` playback no longer degrades characters
outside CP437 (e.g. OpenCode's heavy box-drawing border `U+2503`)
to literal `?` on OEM-codepage consoles: Windows PowerShell 5.1
hosts write `[Console]` output through the console output codepage,
which mangled exactly those characters while ASCII and CP437 blocks
survived. The playback now switches the console output encoding to
UTF-8 for its duration and restores it afterwards. The cast file
was always lossless UTF-8 - pwsh 7 playback was never affected.
[#5](https://github.com/BSVN/Lmt.Commons/issues/5)

### Added

- A live-view system test reproducing the exact topology: conlog wraps a
runner whose console sits at the OEM codepage while a byte-writing
child records - the captured live view must render the box characters
themselves with zero CP437 mojibake, and the console codepage must be
restored after the recording.
[#5](https://github.com/BSVN/Lmt.Commons/issues/5)
- A playback system test playing a real recording back through
powershell.exe 5.1 inside a conlog pseudoconsole at the OEM codepage:
corners, borders and text must render and no CP437-unrepresentable
character may degrade to `?`; plus unit tests asserting the console
output encoding is restored after playback and that heavy box
characters survive a CP437-encoding console losslessly.
[#5](https://github.com/BSVN/Lmt.Commons/issues/5)

## [Bsn.Host 0.2.2] - 2026-11-07

### Fixed
Expand Down
9 changes: 9 additions & 0 deletions docs/en-US/Start-InteractiveTranscript.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ events. `-RecordInput` records keystrokes as `i` events (off by default -
it can capture secrets; the fact is noted in the cast header; Windows only
this release).

On Windows the recorded session runs under a UTF-8 console codepage (the
programmatic `chcp 65001` via `[Console]::OutputEncoding`/`InputEncoding`),
and the engine temporarily switches the calling console's output codepage
to UTF-8 for the duration of the recording, restoring it afterwards. This
keeps the live view of byte-writing TUIs (OpenCode and other Go/Node
applications) free of CP437 mojibake even on OEM-codepage consoles such as
cmd or the Windows PowerShell 5.1 host - the cast file itself is UTF-8
encoded regardless.

## Why not record the outer session like Start-Transcript?

PowerShell exposes **no public hook, event or handler** for intercepting the
Expand Down
4 changes: 2 additions & 2 deletions src/Bsn.Host/Bsn.Host.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'Bsn.Host.psm1'
ModuleVersion = '0.2.2'
ModuleVersion = '0.2.3'
GUID = '9d84a7b8-1f2b-4d60-a4bd-3d7ef1d9f337'
Author = 'BSVN'
CompanyName = 'BSVN'
Expand All @@ -21,7 +21,7 @@
Tags = @('BSVN', 'Lmt', 'Host', 'Transcript', 'Recording', 'Terminal', 'Session', 'Asciicast', 'Asciinema', 'TUI', 'Audit', 'Screencast')
LicenseUri = 'https://github.com/BSVN/Lmt.Commons/blob/main/LICENSE'
ProjectUri = 'https://github.com/BSVN/Lmt.Commons'
ReleaseNotes = 'Bug fix release: recordings of byte-writing TUI applications (OpenCode and other Go/Node TUIs) are no longer corrupted to codepage mojibake - the recorded session bootstraps the console codepage to UTF-8; interop assembly loading is loader-managed (Import-Module -Force reuses the loaded assembly; cross-version imports get clear guidance and a session restart is required to switch versions). Also includes the 0.2.1 fixes: multi-byte UTF-8 survives chunk boundaries; cast files stay valid when the recording process dies; -Command values round-trip safely through pwsh -EncodedCommand; recording with both stdin and stdout redirected is a terminating error. Fixes issues #5, #6, #7, #8 and #15.'
ReleaseNotes = 'Bug fix release: the LIVE recording view (what you watch while recording) no longer turns box-drawing text into CP437 mojibake on OEM-codepage consoles (cmd, Windows PowerShell 5.1 hosts) - the engine switches the console to UTF-8 for the recording and restores it afterwards; the cast file was always correct. Also includes the 0.2.2 fixes: recordings of byte-writing TUI applications (OpenCode and other Go/Node TUIs) bootstrap the console codepage to UTF-8; interop assembly loading is loader-managed (Import-Module -Force reuses the loaded assembly; cross-version imports get clear guidance). Fixes issues #5 and #15.'
}
}
}
73 changes: 58 additions & 15 deletions src/Bsn.Host/Public/Show-InteractiveTranscript.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
Input ('i') events are not replayed; resize ('r') events are ignored
(the current console keeps its own size).

On Windows the console output encoding is temporarily switched to
UTF-8 for the playback and restored afterwards: Windows PowerShell
5.1 hosts write [Console] output through the console output codepage,
which would otherwise render characters outside CP437 (e.g. heavy
box-drawing borders) as literal '?'.

.PARAMETER Path
The asciicast v2 (.cast) file to play back.

Expand Down Expand Up @@ -57,25 +63,62 @@ function Show-InteractiveTranscript {
process {
$timeline = Get-BsnCastTimeline -Path $Path

$previousTime = 0.0
foreach ($timelineEvent in $timeline.Events) {
$eventTime = [double]$timelineEvent[0]
$delaySeconds = $eventTime - $previousTime
if (($IdleTimeLimit -gt 0) -and ($delaySeconds -gt $IdleTimeLimit)) {
$delaySeconds = $IdleTimeLimit
# Windows PowerShell 5.1 (.NET Framework) writes [Console] output
# through the console OUTPUT CODEPAGE (WriteFile), so playback
# characters outside that codepage - e.g. OpenCode's heavy
# box-drawing border U+2503 on an OEM-codepage (437) console - are
# replaced with literal '?' before rendering, while ASCII and CP437
# blocks survive (the recorded cast itself is lossless UTF-8; pwsh 7
# writes via WriteConsoleW and is unaffected). Switch the console
# output encoding to UTF-8 for the duration of the playback and
# restore the original afterwards - the same restore discipline the
# recording engine uses for the live proxy view (issue #5).
$previousOutputEncoding = [Console]::OutputEncoding
$changedOutputEncoding = $false
if ($previousOutputEncoding.CodePage -ne 65001) {
try {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$changedOutputEncoding = $true
}
$delaySeconds = $delaySeconds / $Speed

if ($delaySeconds -gt 0) {
Start-Sleep -Milliseconds ([int]($delaySeconds * 1000))
catch {
# No attached console or a host that refuses the switch (some
# IDE/redirected setups): playback continues with the current
# encoding, which is what previous versions did.
Write-Verbose -Message ('Could not switch the console output encoding to UTF-8 for playback: ' + $_.Exception.Message)
}
}

$eventType = [string]$timelineEvent[1]
if ($eventType -eq 'o') {
[Console]::Write([string]$timelineEvent[2])
try {
$previousTime = 0.0
foreach ($timelineEvent in $timeline.Events) {
$eventTime = [double]$timelineEvent[0]
$delaySeconds = $eventTime - $previousTime
if (($IdleTimeLimit -gt 0) -and ($delaySeconds -gt $IdleTimeLimit)) {
$delaySeconds = $IdleTimeLimit
}
$delaySeconds = $delaySeconds / $Speed

if ($delaySeconds -gt 0) {
Start-Sleep -Milliseconds ([int]($delaySeconds * 1000))
}

$eventType = [string]$timelineEvent[1]
if ($eventType -eq 'o') {
[Console]::Write([string]$timelineEvent[2])
}

$previousTime = $eventTime
}
}
finally {
if ($changedOutputEncoding) {
try {
[Console]::OutputEncoding = $previousOutputEncoding
}
catch {
Write-Verbose -Message ('Could not restore the console output encoding: ' + $_.Exception.Message)
}
}

$previousTime = $eventTime
}
}
}
5 changes: 5 additions & 0 deletions src/Bsn.Host/Public/Start-InteractiveTranscript.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ using module Bsn.Common
process output around the pseudo-console. Run from an interactive
console session for full capture.
- Non-console hosts receive a warning that recording may not work.
- On Windows, the console output codepage is temporarily switched to
UTF-8 while recording (the live proxy writes UTF-8 bytes) and
restored afterwards, so the live view renders byte-writing TUIs
correctly even on OEM-codepage consoles (cmd, Windows PowerShell
5.1 hosts).

Implements https://github.com/PowerShell/PowerShell/issues/28353
following the parameter and format semantics of PowerShell-RFC#421 at
Expand Down
12 changes: 11 additions & 1 deletion src/Bsn.Host/en-US/about_Bsn.Host.help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ LONG DESCRIPTION
Show-InteractiveTranscript plays recordings back in the console with
original or adjusted timing (-Speed, -IdleTimeLimit). Recordings also
play anywhere the open asciicast v2 ecosystem works, for example
'asciinema play <file>.cast'.
'asciinema play <file>.cast'. On Windows the console output encoding
is temporarily switched to UTF-8 during playback and restored
afterwards, so characters outside CP437 (heavy box-drawing borders)
do not degrade to '?' on OEM-codepage consoles or in Windows
PowerShell 5.1 hosts.

RECORDING OPTIONS (RFC 421 SEMANTICS)

Expand Down Expand Up @@ -83,6 +87,12 @@ LONG DESCRIPTION
process output around the pseudo-console. Run from an interactive
console session for full capture.

On Windows the console output codepage is temporarily switched to
UTF-8 while recording (the live proxy writes UTF-8 bytes) and
restored afterwards, so the live view renders byte-writing TUIs
correctly even on OEM-codepage consoles such as cmd or the Windows
PowerShell 5.1 host.

SCENARIOS

- Audit and compliance records of privileged interactive sessions
Expand Down
23 changes: 23 additions & 0 deletions src/Bsn.Host/src/PtyRecorder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ private struct SECURITY_ATTRIBUTES
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GetConsoleOutputCP();

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleOutputCP(uint wCodePageID);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, ref SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize);

Expand Down Expand Up @@ -177,6 +183,22 @@ public static RecordingResult Run(string commandLine, string castPath, bool reco
uint savedInputMode;
bool hadConsoleMode = GetConsoleMode(hRealIn, out savedInputMode);

// The live proxy writes the ConPTY output stream to the real console as
// raw UTF-8 bytes (the same bytes that go into the cast). The console
// decodes those bytes with its OUTPUT CODEPAGE: on an OEM-codepage
// console (437 - cmd, Windows PowerShell 5.1, fresh hidden consoles)
// the UTF-8 text of byte-writing TUIs renders as CP437 mojibake
// ('Gamma-ohmega-acute') in the LIVE view even though the cast is
// clean, because the console - not the recorder - decodes the bytes.
// Switch the console to UTF-8 for the duration of the recording and
// restore the original codepage afterwards; this is the console-host
// counterpart of the wrapper's codepage bootstrap on the pseudo
// console side (issue #5). A return value of 0 means the process has
// no attached console - nothing to switch or restore.
uint savedOutputCodePage = GetConsoleOutputCP();
bool changedOutputCodePage = (savedOutputCodePage != 0) && (savedOutputCodePage != 65001);
if (changedOutputCodePage) { SetConsoleOutputCP(65001); }

short width = 120;
short height = 30;
CONSOLE_SCREEN_BUFFER_INFO bufferInfo;
Expand Down Expand Up @@ -326,6 +348,7 @@ public static RecordingResult Run(string commandLine, string castPath, bool reco
state.Stop = true;
clock.Stop();
if (hadConsoleMode) { SetConsoleMode(hRealIn, savedInputMode); }
if (changedOutputCodePage) { SetConsoleOutputCP(savedOutputCodePage); }
if (cast != null) { cast.Dispose(); }
if (childThread != IntPtr.Zero) { CloseHandle(childThread); }
if (childProcess != IntPtr.Zero) { CloseHandle(childProcess); }
Expand Down
Loading
Loading