diff --git a/taicpp/docs/mplex.md b/taicpp/docs/mplex.md new file mode 100644 index 0000000..794ba18 --- /dev/null +++ b/taicpp/docs/mplex.md @@ -0,0 +1,100 @@ +# 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](../../mplex.md) and +[../../uiflow.md](../../uiflow.md); the code being transcribed is the `Mplex*` and `Plexer*` classes +of [../../tai.py](../../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 of `MplexSpecTests` and `UiflowDocTests`: `mplex.md` + and `uiflow.md` must 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 against `TuiVtScreen` and `MplexKeys`. +- `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_spec` and `PlexerFlow` against tai.py. +- `tests/mplex_e2e.py` with `tests/mplex_cli_main.cpp`: the real client under a pty, as `tests/mplex.py` does. + +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::wregex` ECMAScript): 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's `unicodedata` by 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. diff --git a/taicpp/src/mplex/mplex_args.h b/taicpp/src/mplex/mplex_args.h new file mode 100644 index 0000000..fecb145 --- /dev/null +++ b/taicpp/src/mplex/mplex_args.h @@ -0,0 +1,49 @@ +// MplexArgs (getopt-style tmux argument parsing, tai.py:10472) and MplexCommandParser (tai.py:10545). +#pragma once +#include +#include +#include +#include + +#include "mplex_common.h" + +namespace detz::mplex { + +class MplexArgs { + public: + // One occurrence of a flag: a plain flag is Python's True, an argument flag carries its text. + struct FlagValue { + bool is_true = true; + std::string text; + }; + + MplexArgs() = default; + MplexArgs(std::map> flags_in, std::vector positional_in) + : flags(std::move(flags_in)), positional(std::move(positional_in)) {} + + std::map> flags; + std::vector positional; + + bool has(char flag) const { return flags.count(flag) != 0; } + // args.get(flag, default): the last value. A plain flag yields "True" (str(True)); callers + // that test truthiness of a plain flag must use has(). + std::optional get(char flag) const; + std::string get(char flag, const std::string& fallback) const; + std::vector all(char flag) const; // plain flags contribute "True" + // args.int(flag, default): value True or absent gives the default; bad text throws MplexError + // "-x expects a number: text". + std::optional int_flag(char flag, std::optional fallback = std::nullopt) const; + + // spec "bdt:" => -b and -d are flags, -t takes a value. max_args < 0 means unlimited. + static MplexArgs parse(const std::string& name, const std::string& spec, const std::vector& words, + int min_args = 0, int max_args = -1); +}; + +class MplexCommandParser { + public: + // tmux-syntax text to commands of words; throws MplexError on unterminated quotes/blocks. + static std::vector> parse(const std::string& text); + static std::string quote(const std::string& word); +}; + +} // namespace detz::mplex diff --git a/taicpp/src/mplex/mplex_canvas.h b/taicpp/src/mplex/mplex_canvas.h new file mode 100644 index 0000000..e8f522c --- /dev/null +++ b/taicpp/src/mplex/mplex_canvas.h @@ -0,0 +1,50 @@ +// MplexCanvas (tai.py:11882), MplexStatusText (:11993), MplexFont (:13149). +#pragma once +#include +#include +#include +#include + +#include "mplex_common.h" + +namespace detz::mplex { + +// A client-sized grid of cells plus a cursor, and the diff that turns two successive grids +// into the escape sequences that repaint only what changed. +class MplexCanvas { + public: + using Cursor = std::pair; // (x, y) + static constexpr Cell BLANK = Cell{L' ', 0, 0, 0}; + // GLYPHS["single"] etc: 11 glyphs: horiz vert tl tr bl br tee_r tee_l tee_d tee_u cross. + static const UStr& glyphs(const std::string& lines); // unknown name falls back to "single" + + MplexCanvas(int rows_in, int cols_in); + int rows, cols; + Grid cells; + std::optional cursor; + + // put(x, y, text, style, limit): limit < 0 means None. Returns the x after the text. + int put(int x, int y, const UStr& text, const Style& style = Style{}, int limit = -1); + int put(int x, int y, const std::string& text, const Style& style = Style{}, int limit = -1); + void put_cells(int x, int y, const Row& row); + void blit(int x, int y, const Grid& block, int width); + void fill(int x, int y, int w, int h, const Style& style = Style{}, wchar_t ch = L' '); + static wchar_t border_char(bool up, bool down, bool left, bool right, const std::string& lines); + // Escape bytes repainting this grid over previous (null when there is none); also returns the snapshot. + std::pair encode(const Grid* previous) const; +}; + +// Format-expanded text with #[...] style markers turned into styled cells. +class MplexStatusText { + public: + static Row cells(const UStr& text, const Style& base, const std::optional