7.9 KiB
mplex + uiflow (detz port)
Spec delta for the C++ port of tai's terminal multiplexer and the interactive UI flows. The
behavior specification is unchanged and lives in ../../mplex.md and
../../uiflow.md; the code being transcribed is the Mplex* and Plexer* classes
of ../../tai.py. This page records what the port adds: the file layout, the
Python-to-C++ mapping rules, the parity table, the tests and the known gaps.
Goal
A literal transpilation with identical behavior: the same keys, commands, options, formats, wire
frames, control protocol, config file and error texts. Where Python and C++ differ at the language
level, the port reproduces Python's semantics (mplex_py.h) instead of adopting C++'s.
Layout
All Mplex code is in taicpp/src/mplex/, namespace detz::mplex; the uiflow code is in
taicpp/src/plexer/plexer_flow.{h,cpp}, namespace detz::plexer. One class per header and source
pair; every file name is prefixed mplex_ so the flat include path of the Makefile cannot clash with
other modules. Class and method names are the Python names (snake_case methods, Mplex* classes).
Mapping rules
| Python | C++ |
|---|---|
str |
std::string (UTF-8) at API edges, UStr (std::wstring, one wchar_t per code point) wherever len, index or slice count code points |
bytes |
Bytes (std::string holding raw bytes) |
None |
std::optional, an empty shared_ptr, or Color 0 |
iterated dict |
OrderedMap (insertion order is iteration order) |
(fg, bg, attr) / cell tuple |
Style / Cell (ch == 0 is a never written cell) |
SGR colour fragment "38;5;12" |
Color (32 bit pack, lossless for every fragment tai.py produces) |
MplexError, ValueError |
MplexError, ValueError (both std::runtime_error) |
| closures | std::function |
back references (pane.window, window.session, layout parent) |
weak_ptr with an accessor returning shared_ptr |
| modes and overlays on a client | shared_ptr held by the owner; MplexServer::discard_pane clears pane.modes to break the pane/mode cycle |
json, tomllib, re, fnmatch, textwrap, shlex, time.strftime, subprocess.run |
mplex_json.h, mplex_toml.h, mplex_py.h (py::Regex, py::fnmatchcase, py::textwrap_wrap, py::shlex_quote, py::strftime, py::run_shell) |
Parity table
| tai.py symbol | C++ symbol | files |
|---|---|---|
MplexError |
MplexError |
mplex_common.h |
MplexKeys |
MplexKeys |
mplex_keys.{h,cpp} |
tui_parse_one and helpers |
tui_parse_one |
mplex_input.{h,cpp} |
MplexArgs, MplexCommandParser |
same | mplex_args.{h,cpp} |
MplexStyle |
MplexStyle |
mplex_style.{h,cpp} |
MplexOptions |
MplexOptions |
mplex_options.{h,cpp} |
MplexBinding, MplexConfig, MPLEX_DEFAULT_KEYS |
same | mplex_config.{h,cpp}, mplex_default_keys.cpp (generated) |
MplexFormat |
MplexFormat |
mplex_format.{h,cpp} |
MplexLayoutNode, MplexLayout |
same | mplex_layout.{h,cpp} |
TuiVtScreen (as used by panes) |
MplexVtScreen |
mplex_vtscreen.{h,cpp} |
MplexPane, MplexWindow, MplexSession |
same | mplex_pane.{h,cpp} |
MplexCanvas, MplexStatusText, MplexFont |
same | mplex_canvas.{h,cpp} |
MplexMode, MplexClockMode |
same | mplex_mode.{h,cpp}, mplex_clockmode.{h,cpp} |
MplexCopyMode |
MplexCopyMode |
mplex_copymode.{h,cpp} |
MplexPrompt |
MplexPrompt |
mplex_prompt.{h,cpp} |
MplexPaneNumbers, MplexFlowScreen |
same | mplex_panenumbers.{h,cpp}, mplex_flowscreen.{h,cpp} |
MplexChoice, MplexChooser |
same | mplex_chooser.{h,cpp} |
MplexRenderer |
MplexRenderer |
mplex_renderer.{h,cpp} |
MplexCtx, MplexTargets |
same | mplex_ctx.{h,cpp} |
MplexClientConn, MplexPending |
same | mplex_clientconn.{h,cpp} |
MplexServer |
MplexServer |
mplex_server.{h,cpp} |
MplexCommands, MPLEX_COMMAND_SPECS, MPLEX_DEFAULT_FORMATS |
same | mplex_commands.{h,cpp}, mplex_command_specs.cpp (generated) |
MplexClient, MplexCli, NativePlexerBackend, plexer_should_wrap, bashrc blocks |
same | mplex_client.{h,cpp} |
PlexerController, NativePlexerController, TmuxPlexerController, PlexerControllerFactory |
same | src/plexer/plexer_flow.{h,cpp} |
validate_flow_*, PlexerFlow, PlexerBackend |
same | src/plexer/plexer_flow.{h,cpp} |
The two generated files are produced from the Python tables; they carry a header saying so and
mplex_spec_test compares them with mplex.md.
Tests
tests/mplex_spec_test.cpp: the C++ twin ofMplexSpecTestsandUiflowDocTests:mplex.mdanduiflow.mdmust list exactly the options, commands, default bindings, copy-mode commands, caps and flow words that the C++ tables hold.tests/mplex_foundation_test.cpp: the Python-shaped helpers, JSON and TOML, against Python 3.14.tests/mplex_vt_test.cpp: the VT screen, input parser and keys againstTuiVtScreenandMplexKeys.tests/mplex_core_test.cpp: args, parser, style, options, config, format and layout against tai.py.tests/mplex_model_test.cpp: panes, canvas, prompt, chooser, flow screen, renderer, targets.tests/mplex_copy_test.cpp: copy mode against tai.py's recorded tmux results and a seeded fuzz.tests/mplex_server_test.cpp,tests/mplex_commands_test.cpp: the real C++ server over its socket, scripted command corpus compared with the Python server.tests/mplex_flow_test.cpp:validate_flow_specandPlexerFlowagainst tai.py.tests/mplex_e2e.pywithtests/mplex_cli_main.cpp: the real client under a pty, astests/mplex.pydoes.
Expected values in these tests are produced by running tai.py (and tmux 3.6 where mplex.md names it the oracle), never typed in from memory.
Known gaps of the port
Listed next to what would close them; the final status of each is recorded in the milestone report.
- Regular expressions (
py::Regex,std::wregexECMAScript): Python-only syntax (lookbehind, named groups, possessive quantifiers,\Z) is translated where an equivalent exists and rejected with a loud error otherwise; copy-mode search then falls back to a literal, like Python does for an invalid expression. - Unicode classification (
str.isalpha,isprintable, case mapping) uses the glibc C.UTF-8 tables, which may lag or lead CPython'sunicodedataby a Unicode version. - TOML error texts follow tomllib's wording; positions are reproduced, rare messages may differ.
- Anything else found while testing is added here, never hidden.
Feed panes (taicpp extension, orchestration)
A feed pane is a pane without a process: text is written into it through the control socket and the server
composes it like any other pane. They exist for sub agent panels; the full contract (protocol, layout
policy, lifecycle, caps, examples) is orchestration.md section 3. The Python app has
none of this, so it is outside the tables mplex_spec_test compares with mplex.md.
| Piece | Where |
|---|---|
| registry, layout, linger, orphan lease, the four commands | src/mplex/mplex_vpane.{h,cpp} (MplexVpanes) |
virtual_pane, header, header_style, open_log; screen has h - 1 rows; write is a no-op |
src/mplex/mplex_pane.{h,cpp} |
| header row drawn above the pane's grid | MplexRenderer::frame |
vpane_open, vpane_write, vpane_close, vpane_list dispatch |
MplexServer::dispatch_control |
entry forgotten when a pane is discarded (also by kill-pane) |
MplexServer::discard_pane |
| lease and linger sweep, once a second | MplexServer::tick |
| a window whose real panes are all gone is closed | MplexServer::kill_pane |
Behavior that differs from a process pane: no pty, no reaper, no input; list-panes, capture-pane -p,
kill-pane, copy mode, zoom and the read control command work; select-layout can be used but the next
panel open or close re-applies main-vertical with the main pane at 55 percent.