Windows Backend
This page documents the internal Windows backend used by
Terminal.
It is intended for contributors who need to understand where Windows-specific terminal behavior lives and how shutdown cleanup is coordinated.
For the public backend API contract that this implementation satisfies, see Backend.
Relevant Source Files
You will find the implementation primarily in:
src/erbsland/cterm/impl/WindowsBackend.hppsrc/erbsland/cterm/impl/WindowsBackend.cppsrc/erbsland/cterm/impl/WindowsSignalDispatcher.hppsrc/erbsland/cterm/impl/WindowsSignalDispatcher.cpp
Initialization and Restore
The Windows backend is implemented by erbsland::cterm::impl::WindowsBackend.
As on the POSIX side, the library expects exactly one active backend instance at a time.
The constructor stores that global instance and, unless
TerminalFlag::NoSignalHandling is set,
creates a WindowsSignalDispatcher that watches for console termination events.
initializePlatform() performs platform setup in the following order:
It switches the console input and output code pages to UTF-8 with
SetConsoleCP()andSetConsoleOutputCP().It disables iostream synchronization with the C stdio layer and detaches
std::cinfrom the implicitstd::coutflush, so console I/O is driven only by the C++ streams used by the backend.It enables
ENABLE_VIRTUAL_TERMINAL_PROCESSINGon the standard output handle so the Windows console interprets ANSI escape sequences.It queries the current cursor visibility with
GetConsoleCursorInfo(), stores that state, and hides the cursor withSetConsoleCursorInfo().It marks the backend as initialized.
The backend does not switch into a special key-input mode during initialization.
Input remains in Input::Mode::ReadLine until the caller explicitly switches to key mode.
restorePlatform() performs the corresponding cleanup:
If initialization never completed, it returns immediately.
If the backend saved a cursor state, it restores that state through the Win32 cursor API.
If the backend believes the alternate screen buffer is active, it emits
ESC[?1049lto leave it.It emits
ESC[0mto reset colors andESC[?25hto force the cursor visible.It writes a trailing newline and flushes
std::cout.
Two details are worth keeping in mind when you extend this code:
supportsCursorVisibilityCodes()returnsfalse, so normal cursor visibility changes use the Win32 API rather than ANSI escape sequences.The restore path still emits
ESC[?25has an additional safety step, even though the primary cursor restoration is done through the Win32 API.
Reading Keys from the Console
The Windows backend supports two input paths.
In Input::Mode::ReadLine, readKey() delegates to readLine(), and readLine() uses std::getline(std::cin, input).
In Input::Mode::Key, readKey() works directly with the Windows console input queue:
It waits on
STD_INPUT_HANDLEwithWaitForSingleObject().A timeout of
0means to wait indefinitely.Any non-zero timeout is currently divided by two before it is passed to
WaitForSingleObject().After the wait succeeds, the backend repeatedly calls
GetNumberOfConsoleInputEvents()andReadConsoleInputW()until the queued events are drained.It ignores all non-key events.
It ignores key-release events and only reacts to records with
bKeyDown != 0.It maps well-known virtual key codes such as arrows, page navigation keys, insert, delete, enter, escape, tab, backspace, and
F1throughF12to the correspondingKeyvalues.For other keys, it decodes
uChar.UnicodeCharas UTF-16, including surrogate pairs, and collects the resulting Unicode text input.
The return value is the last recognized key seen while draining the queue. As a result, one call can consume multiple queued key events but returns only one library key.
Character input therefore no longer depends on the legacy ASCII field of the console record.
Special keys are still handled through virtual key codes, while textual input is decoded from the UTF-16 payload that
ReadConsoleInputW() provides.
Detecting Terminal Size and Interactive Console Availability
The Windows backend exposes isInteractive() directly.
Internally, interactivity is inferred from whether the backend can successfully query the active console screen buffer.
detectScreenSize() uses the following logic:
It obtains
STD_OUTPUT_HANDLEwithGetStdHandle().If the handle is null or invalid, it returns
std::nullopt.It calls
GetConsoleScreenBufferInfo()on that handle.If that call fails, it returns
std::nullopt.It computes the visible terminal dimensions from
info.srWindowrather than from the full scrollback buffer size.If the computed width or height is not positive, it returns
std::nullopt.Otherwise, it returns
Sizewith the visible window width and height.
For contributors, this means:
isInteractive()anddetectScreenSize()rely on the same console-buffer probe.A successful
GetConsoleScreenBufferInfo()call is effectively treated as proof that an interactive console is present.Redirected output or non-console hosts usually appear as “no detectable screen size”.
The reported size is the visible console window, not the total underlying buffer.
Handling Termination Events and Restoring the Screen
The Windows backend uses WindowsSignalDispatcher to move termination handling out of the raw console-control callback and into a normal worker thread.
This mirrors the POSIX design goal, even though the Windows callback model differs from POSIX signals.
The dispatcher is set up like this:
The constructor stores a global dispatcher pointer in an atomic variable.
It starts a watcher thread that waits on a condition variable.
It registers a console-control handler with
SetConsoleCtrlHandler().
The registered callback onConsoleControl() recognizes these events:
CTRL_C_EVENTmapped to exit code130CTRL_BREAK_EVENTmapped to exit code131CTRL_CLOSE_EVENT,CTRL_LOGOFF_EVENT, andCTRL_SHUTDOWN_EVENTmapped to exit code1
The callback does not restore the terminal directly.
Instead, it calls pushSignal(), which stores the first pending exit code under a mutex and notifies the watcher thread.
Additional events are ignored once one exit code is already pending.
The watcher thread wakes up, extracts the exit code, releases the mutex, and calls the backend callback in a normal thread context.
That backend callback is WindowsBackend::handleProcessSignal():
It calls
restoreGlobalPlatform()so the cursor, alternate screen, and color state are cleaned up.It terminates the process immediately with
std::_Exit(exitCode).
When the backend or dispatcher is destroyed during normal shutdown, the dispatcher first unregisters the console-control handler, then stops and joins the watcher thread, and finally clears the global dispatcher pointer.
For maintainers, the key point is that screen restoration never happens inside the raw Windows console-control callback itself. The callback only forwards the event. The actual cleanup runs on the dedicated watcher thread, where mutexes, iostreams, and the usual backend code are safe to use.