tui¶
The terminal UI. Namespace core::tui, directory src/core/tui/, targets core::tui_output
(with CORE_CPP_WITH_TUI_OUTPUT) and core::tui (with CORE_CPP_WITH_TUI). Both are on by
default and off under Emscripten; CORE_CPP_WITH_TUI forces CORE_CPP_WITH_TUI_OUTPUT on, and
CORE_CPP_WITH_TUI_OUTPUT defaults to whatever CORE_CPP_WITH_TUI is on a first configure (a
cache entry keeps its value after that, so turning the TUI off in an existing tree leaves the leaf
on).
Imported from endo's src/tui at f774a210, which includes the coroutine-runtime work fastcached
upstreamed there. The runtime has since been rewritten onto core::net::EventLoop.
Status
Available. Its runtime is composed on core::net::EventLoop, so core::tui links
core::net; core::tui_output still links base alone.
core::tui_output¶
A static library that links base and nothing else: no libunicode, no coroutines, not
even platform. Its row in the module table says DEPS base, so the configure
refuses any other link from it. A program that only prints styled text and progress — Lightweight's
dbtool — links this and takes nothing else with it: configured with CORE_CPP_WITH_TUI off and
CORE_CPP_WITH_TUI_OUTPUT on, core-cpp builds the leaf alone and neither finds nor fetches
libunicode. Before 0.2.1 the leaf had no option of its own and came only with CORE_CPP_WITH_TUI,
which fetches libunicode and, through libunicode's configure, UCD.zip.
tests/consumer-tui-output is that consumer, and CI's consumer-smoke (tui-output) builds it.
TerminalOutputbuffers escape sequences and flushes them: styled text (Style,RgbColor,UnderlineStyle), cursor movement, erasing, the alternate screen, the scroll region, double-width and double-height lines, cursor shapes (CursorShape), sixel payloads, OSC 52 clipboard writes and OSC 8 hyperlinks, plus the queries a capability probe sends (cursor position, cell size, DECRQM, DA1).writeToDestination()is the one seam every byte goes through. Override it to retarget the stream — a capture buffer in a test, a pipe, a second terminal — without reimplementing any of the composition.isTerminal()answers for that same destination: the default asks the operating system about the process's standard output, and a subclass answers for its own.SyncGuardbrackets a frame in DEC mode 2026 so a terminal does not paint a half-drawn one. It writes through theTerminalOutputit was made from, so a retargeted output is bracketed on its own stream, and only when that output'sisTerminal()is true: on any other destination it flushes at both ends and writes no sequence, so a caller takes one unconditionally.ClipboardProtocol.hppcomposes both terminal clipboard protocols and reads their answers, with no I/O:encodeOsc52(),encodeOsc5522Write()(kitty's protocol: any MIME type, 4096-byte chunks),parseOsc5522WriteStatus(), anddescribe()for aClipboardWriteError.TerminalOutput::copyToClipboard()uses the same OSC 52 encoder.buildSgrSequence()turns aStyleinto one SGR sequence.core::tui::protocolsholds the sequence constants the input and output sides share (the Kitty keyboard protocol, bracketed paste, mouse and focus tracking, colour-scheme notification, win32-input-mode, OSC 8),appendHyperlinkOpen(),appendMouseTrackingChange(), which writes the change between twoMouseTrackingmodes, andparseSixelFromDeviceAttributes(), which reads a DA1 answer.Result<T>andVoidResult, the module'sstd::expectedaliases.
core::tui¶
The rest, on top of core::tui_output, platform, async and libunicode,
and on stb when CORE_CPP_WITH_IMAGES is on. Native only: there is no terminal under Emscripten.
- Input.
TerminalInputputs the terminal in raw mode, enables the protocols above and decodes what comes back throughVtParserinto anInputEvent— keys (KeyCode,Modifier), mouse, paste, focus, resize, and the protocol reports a query waits for.Terminalpairs it with aTerminalOutputand owns the query round-trips (queryCursorPosition(),queryCellSize(),queryDecMode(),queryDeviceAttributes()), each on an injected clock and each bounded. - Clipboard.
ClipboardWritercopies through the terminal rather than an OS clipboard API, so it works over SSH: OSC 5522 where the terminal says it knows mode 5522, OSC 52 otherwise. The first copy asks with DECRQM followed by DA1, which every terminal answers, so the probe ends at the DA1 reply and times out only when nothing answers at all; the answer is kept for the writer's lifetime. An OSC 5522 copy waits for the terminal's status (DONEor an error such asEPERM); an OSC 52 copy cannot be confirmed, and a non-text MIME type over it is refused before anything is sent. Each copy opens aTerminalChannelfrom an injected factory:openControllingTerminal()opens/dev/tty(orCONIN$/CONOUT$) beside standard I/O, so redirected output does not stop a copy, with echo and line buffering off while it is open; a process outside the terminal's foreground process group gets a write-only channel and OSC 52. Before an exchange it will wait on -- the probe, an OSC 5522 write -- the writer discards input already queued (TerminalChannel::discardPendingInput()), so a reply that arrived after an earlier exchange gave up waiting does not answer this one; typeahead goes with it. An OSC 52 copy waits for nothing and discards nothing. A reply that arrives after the channel has closed cannot be caught: a terminal that confirms an OSC 5522 copy later than the status timeout leaves that reply for whatever reads the terminal next.VtParserdecodes OSC replies intoOscResponseonly when constructed withOscRecognition::Response; by defaultESC ]stays Alt+]. - Mouse.
TerminalInput::setMouseTracking(), orTerminal::setMouseTracking(), asks the terminal for aMouseTrackingmode:Buttons(DEC 1000),Drag(1002) orAnyMotion(1003), each with SGR encoding (1006). The default isOff, because a terminal that reports the mouse stops selecting text on a click-and-drag; an application opts in. Contour's passive mode 2029 is enabled either way, and where the terminal confirms it, hover tooltips raise the mode toAnyMotion(mouseTracking()answers the raised mode).VtParserdecodes every mode's reports intoMouseEvents: motion with a button held is aMoveof that button, motion with none aMoveof button 3. - Pointer.
Screenroutes a mouse event to the top-most visible component under it -- overlays first, the one shown last on top, then the tree -- andcomponentAt(row, col)answers the same question for a caller, such as a drag source looking for its drop target. A press makes the component that handled it the capture target: until the release, every move and that release go to it, in its own coordinates and wherever the pointer is, so a component sees its whole drag. The release ends the capture, as do the next press,releasePointer(), and the component leaving the screen -- destroyed, removed from the tree or hidden as an overlay. Scroll events are never captured, and a press hides the tooltip, so none opens mid-drag. - Drawing.
Bufferis a grid ofCells,Canvasa clipped view of one, andScreenthe renderer that diffs a frame against the last and writes only what changed, inline, full-screen or in a fixed area.Theme,StyledText,Text,Box,RectandHyperlinkEmittersit under it. - Components.
Componentand the widgets over it:InputField(multi-line editing, undo, kill ring, ghost text, selection),List,TreeTableView,Dialog,StatusBar,LogPanel,Spinner,ProgressBar,Tooltip,QuestionComponent, and the popupsCompletionPopup,CommandPalettePopupandFuzzyPickerPopupwithPopupKeyDispatchandScrollableSelection. - Completion.
core::tui::completer:CompleterwithCompletionConfig,CompletionProvider,CompletionItem,FuzzyMatchwithFuzzyConfigandFuzzyMatchResult, andSmartCaseMatchwithSmartCaseConfig. The namespace is the one the directory names, asruntime/iscore::tui::runtime; endo's TUI is one flatnamespace tui, and the import kept that until core-cpp#30. - Markdown and syntax.
MarkdownRendererwithMarkdownTable,MarkdownHtmland the inline grammar, andGenericSyntaxHighlighter, a lexer per language, withSyntaxHighlighterRegistryfor the languages core::tui does not ship (below). - Images. With
CORE_CPP_WITH_IMAGES:loadImage(),resizeImage()andreadClipboardImage()over stb,encodeSixel(), andFilesystemImageProvider, which implements the always-presentImageProviderinterfaceMarkdownRenderertakes. A file name is UTF-8 on every platform: stb is built withSTBI_WINDOWS_UTF8, so on Windows it opens the name through the wide API. - Runtime.
core::tui::runtime:TuiRuntimegives the TUI its input vocabulary --co_await runtime.nextEvent(),nextEventFor(),nextActivity(),nextAgentReady()-- and forwards everything else to the event loop it is constructed on:blockOn(),spawn(),delay(),sleepUntil(),waitReadable(),waitWritable(), the clock and the root stop source. Input arrives through an injectedInputSource, which names the handles to watch and decodes what is ready behind them;TerminalInputSourceis that over aTerminal, andTuiRuntime(loop, terminal)makes one for you.runModal()drives a modal to its result; for a deadline usecore::net::withTimeout(&runtime.loop(), …). - The end of the input. When the input handle reaches its end -- a terminal that hung up (EIO,
or an end of file that
poll(2)confirms as a hangup), a pipe whose writer closed, a Windows console that was closed or detached, or a handle the loop refuses to watch -- the source'sinputClosed()answers true, the runtime stops watching the handle, andruntime.inputClosed()answers true from then on. One end reaches no handle: a Windows console whose host process died is never signalled again. The runtime therefore asks the source, throughprobeEnd(), everyTuiRuntimeOptions::endProbe(one second by default), and ends the input the same way once the console can no longer answer; a source whose end does arrive as readiness answersEndProbe::NotNeededthe first time and is not asked again.TerminalInput::poll()asks the same question between slices of at most a second of its wait, so apoll(-1)on such a console returns. The input waits deliver what was read before the end (and a pending agent message, fornextActivity()), and then throwcore::async::OperationCancelledwithout parking. An application that catches that cancellation asksinputClosed()to tell "the terminal is gone, exit" from an interrupt. Before 0.2.1 the runtime re-parked on a handle at its end, which answers at once and yields nothing, so withSIGHUPignored a hung-up terminal spun the process at 100% CPU (core-cpp#49). - Test doubles.
MockTerminalOutputrecords what a renderer did semantically instead of emitting VT,runtime::testing::ScriptedInputSourcescripts the decoding and, withcloseInput(), the end of the input (readiness comes fromcore::net::testing::ScriptedBackendor from a realcore::platform::SystemPipe), andTestHelpers.hppreads a renderedBufferback as text.testing::ScriptedTerminalis a terminal on aManualClockfor anything built onTerminalChannel: how it answers the OSC 5522 probe and a write, and every byte it was sent, decoded back withdecodedPayload(),mimeType()andtarget().
Registering a language¶
GenericSyntaxHighlighter ships a lexer for the languages of LanguageId — C and C++, CMake,
Python, Bash, Markdown, JSON, YAML, git diffs, assembly, PowerShell, CMD, XML and INI — and for no
others. An application that has its own language teaches it to core::tui rather than core::tui
shipping it, which is what keeps one application's vocabulary out of a library four of them link
(core-cpp#24).
A SyntaxHighlighterRegistry is that seam. It is an ordinary object: construct one, fill it, and
pass it to whatever renders the text. There is no process-wide registry, so two parts of one
program can hold different ones and a test never has to undo a registration.
auto highlighters = core::tui::SyntaxHighlighterRegistry {};
auto const wobble = highlighters.registerLanguage({
.name = "wobble",
.extensions = { ".wob" },
.fenceTags = { "wobble", "wob" },
.highlight = [](std::string_view line, core::tui::HighlightState state) {
return highlightWobbleLine(line, state); // the application's own lexer
},
});
if (!wobble)
log("wobble was refused: {}", wobble.error().token);
registerLanguage() returns a LanguageId of its own, from the reserved range that begins at
FirstRegisteredLanguageId, or a LanguageRegistrationFailure saying which name, extension or
fence tag was already claimed. It refuses rather than shadows — replacing would repoint an id
already handed out, and whoever held that id would get a wrong answer that looks right — and a
refused definition leaves the registry exactly as it was.
Every entry point then takes the registry as a trailing argument that defaults to nullptr,
meaning the built-in languages alone:
auto renderer = core::tui::MarkdownRenderer { output, theme, &highlighters }; // ```wobble fences
auto const styled = core::tui::StyledText::fromMarkdown(text, width, &theme, &highlighters);
auto const language = core::tui::detectLanguageFromPath("draft.wob", &highlighters);
auto const [map, next] = core::tui::highlightLine(line, language, state, &highlighters);
A registry answers for the built-in languages too — built-in rows are consulted first — so registering one costs an application nothing it already had.
A registered id belongs to the registry that issued it
Registered ids are dense from FirstRegisteredLanguageId in registration order and carry
nothing that identifies their registry, so passing one to a different registry is a
precondition violation — the same contract a std::vector::iterator has with its container.
If that registry issued an id in the same position, the line is highlighted as its
language, silently and wrongly; only an id past the end of it gives plain text. A program that
holds one registry, which is the shape this is designed for, cannot hit this. One that holds
two keeps each id with its own registry.
Built-in ids — everything below FirstRegisteredLanguageId — are not issued by anybody and
are portable: they mean the same language in any registry and in none.
The three built-in tables the module ships are ExtensionLanguageTable, FenceTagLanguageTable
and FilenameLanguageTable, and a registered language claims extensions and fence tags but never
a file name: CMakeLists.txt, .clang-format and .editorconfig are well known beyond any
one project, whereas an application knows what its own configuration file is called and names the
language for it itself, rather than asking detectLanguageFromPath() to guess. A registration
whose extension would be shadowed by a file-name row is refused rather than left dead.
Layout¶
The module's root holds only platform-independent code. What goes through the operating system is
in posix/ and windows/, which the per-platform source lists select, so no file there guards
itself with an #ifdef of its platform:
posix/— thetermiosraw mode, the SIGWINCH self-pipe,poll(2), thewrite/readretry loop,isatty, and the clipboard tools (wl-paste,xclip).windows/— the console modes and code pages, console input records decoded as UTF-8,WaitForMultipleObjects, the resize event, andGetConsoleScreenBufferInfo.
TerminalInput's own handles live in an opaque NativeState those two define, so
<core/tui/TerminalInput.hpp> names neither <termios.h> nor <windows.h>. No file in this
module chooses its platform with an #ifdef, and runtime/ has no platform directory at all: the
multiplexed wait its two TerminalEventSource bodies held is the event loop's, on every
platform.