Fix garbled output when terminal isn't UTF-8 at startup (#204)#208
Merged
Conversation
Setting Console.OutputEncoding replaces Console.Out with a new TextWriter. AnsiTerminal captured Console.Out at construction, so on a console that did not start in UTF-8 the cached writer stayed bound to the old encoding and rendered Unicode box-drawing characters as U+FFFD. - AnsiTerminal no longer caches Console.Out; the output writer is resolved on every flush via the Output property, so it always reflects the current Console.Out regardless of when the encoding was changed. - Consolidated UTF-8 encoding setup into ConsoleEnvironment.EnsureUtf8Output, invoked by the platform console, replacing three separate call sites. - Added regression tests covering writer resolution. Diagnosis and original fix by @logical-intent in #204 / #205. Closes #204
This was referenced May 18, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #204. Supersedes #205.
Problem
Setting
Console.OutputEncodingreplacesConsole.Outwith a brand-newTextWriterbound to the new encoding.AnsiTerminalcapturedConsole.Outin its constructor, so on a console that did not start in UTF-8 (e.g. code page 850, the VS debugger case in #204) the cached writer stayed bound to the old encoding. Unicode box-drawing characters written through it were replaced withU+FFFD.Credit to @logical-intent for diagnosing this precisely and proposing the original fix in #205.
Approach
#205 fixes this by reordering the constructor so
Console.Outis captured after the encoding flip. That works for the reported case, but it still cachesConsole.Out— andConsole.OutputEncodingis set again later byWindowsConsole/FallbackConsole.Initialize(), which invalidates the cached writer. The cache is correct only as long as every encoding flip in the process happens to be UTF-8 and is timed just right.This PR removes the coupling instead of timing around it:
AnsiTerminalno longer cachesConsole.Out. The output writer is resolved on every flush via a privateOutputproperty (_explicitOutput ?? Console.Out). It always reflects the currentConsole.Out, regardless of when — or how often — the encoding changes. The explicit-writer seam for tests/benchmarks is preserved.ConsoleEnvironment.EnsureUtf8Output(), invoked by the platform console duringInitialize(), replacing the three separateConsole.OutputEncoding =call sites (AnsiTerminal,WindowsConsole,FallbackConsole).Performance is unchanged:
Console.Out's getter is a single volatile static read in steady state, resolved once perFlush()(once per frame) — negligible against theToString()/encode/syscall the flush already does.Tests
Added
AnsiTerminalTestswith a deterministic regression test: construct anAnsiTerminal, swapConsole.OutviaConsole.SetOut(exactly what settingConsole.OutputEncodingdoes), then assert the flush lands in the new writer. This fails against both the original code and #205's caching approach, and passes only with non-caching. No real terminal or code-page juggling required.All 1004 tests pass locally.
Notes for reviewer
AnsiTerminalno longer setsConsole.OutputEncodingitself — the platform console owns it. In practiceAnsiTerminalis always used viaTerminaApplication, whoseRunAsynccallsplatformConsole.Initialize()before the first render, so this is covered. A standalonenew AnsiTerminal()used without a platform console would no longer force UTF-8; flag if that path should be supported.RELEASE_NOTES.md(0.8.1, today) is a guess — adjust to match release cadence.OpenTelemetry.ApiNU1902advisory inTermina.Demo.Streaming— pre-existing ondev, not introduced here.AnsiTerminal's remaining constructor side effect (EnterAlternateScreen) into an explicitInitialize()/Dispose()lifecycle so setup ordering is explicit rather than emergent from the DI graph.