mplex: headers, generated tables, spec test, spec delta doc; fix set_active aliasing, version from generated header
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,49 @@
|
||||
// MplexArgs (getopt-style tmux argument parsing, tai.py:10472) and MplexCommandParser (tai.py:10545).
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<char, std::vector<FlagValue>> flags_in, std::vector<std::string> positional_in)
|
||||
: flags(std::move(flags_in)), positional(std::move(positional_in)) {}
|
||||
|
||||
std::map<char, std::vector<FlagValue>> flags;
|
||||
std::vector<std::string> 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<std::string> get(char flag) const;
|
||||
std::string get(char flag, const std::string& fallback) const;
|
||||
std::vector<std::string> 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<long long> int_flag(char flag, std::optional<long long> 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<std::string>& 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<std::vector<std::string>> parse(const std::string& text);
|
||||
static std::string quote(const std::string& word);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,50 @@
|
||||
// MplexCanvas (tai.py:11882), MplexStatusText (:11993), MplexFont (:13149).
|
||||
#pragma once
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<int, int>; // (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> 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<Bytes, Grid> 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<Style>& reset = std::nullopt);
|
||||
};
|
||||
|
||||
class MplexFont {
|
||||
public:
|
||||
static constexpr int HEIGHT = 5;
|
||||
static int width(const UStr& text);
|
||||
static Grid draw(const UStr& text, Color bg); // five rows of cells
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,62 @@
|
||||
// MplexChoice (tai.py:13368) and MplexChooser (:13380): choose-tree / choose-buffer / choose-client.
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_json.h"
|
||||
#include "mplex_mode.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexChoice {
|
||||
public:
|
||||
using Ptr = std::shared_ptr<MplexChoice>;
|
||||
explicit MplexChoice(const std::string& label_in, Json payload_in = Json(), std::vector<Ptr> children_in = {},
|
||||
bool expanded_in = false)
|
||||
: label(label_in), payload(std::move(payload_in)), children(std::move(children_in)), expanded(expanded_in) {}
|
||||
std::string label;
|
||||
Json payload; // Python tuples/ints/strs; compared with == and repr()
|
||||
std::vector<Ptr> children;
|
||||
bool expanded;
|
||||
int depth = 0;
|
||||
bool tag = false;
|
||||
};
|
||||
|
||||
class MplexChooser : public MplexMode {
|
||||
public:
|
||||
using Builder = std::function<std::vector<MplexChoice::Ptr>()>;
|
||||
using OnChoose = std::function<void(const MplexChoice::Ptr&, MplexClientConn&)>;
|
||||
using OnDelete = std::function<void(const MplexChoice::Ptr&, MplexClientConn&)>;
|
||||
// preview(item, width, height) -> rows of cells
|
||||
using Preview = std::function<Grid(const MplexChoice::Ptr&, int, int)>;
|
||||
static const char* const HOT_KEYS; // "0123456789"
|
||||
|
||||
MplexChooser(MplexServer* server_in, std::shared_ptr<MplexPane> pane_in, const std::string& kind, Builder builder_in,
|
||||
OnChoose on_choose_in, OnDelete on_delete_in = nullptr, Preview preview_in = nullptr,
|
||||
std::optional<Json> selected = std::nullopt, std::function<void()> on_close_in = nullptr);
|
||||
|
||||
Builder builder;
|
||||
std::vector<MplexChoice::Ptr> items;
|
||||
std::function<void()> on_close;
|
||||
OnChoose on_choose;
|
||||
OnDelete on_delete;
|
||||
Preview preview;
|
||||
bool show_preview;
|
||||
int cursor = 0;
|
||||
std::vector<MplexChoice::Ptr> visible;
|
||||
|
||||
void reflatten();
|
||||
MplexChoice::Ptr selected() const;
|
||||
void rebuild();
|
||||
void close() override;
|
||||
int list_height() const;
|
||||
ModeBlock render(MplexClientConn& client) override;
|
||||
void move(int delta);
|
||||
void key(const std::string& name, const Bytes& raw, MplexClientConn& client) override;
|
||||
void delete_node(const MplexChoice::Ptr& node, MplexClientConn& client); // Python: delete()
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,50 @@
|
||||
// MplexClient (tai.py:16804), MplexCli (:16963), NativePlexerBackend (:17012) and
|
||||
// plexer_should_wrap (:17026): the attached terminal and the --mplex / --plexer-wrap entry.
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
// The attached terminal: forwards keys and resizes to the server, paints what it sends.
|
||||
class MplexClient {
|
||||
public:
|
||||
MplexClient(const std::string& home, const std::vector<std::vector<std::string>>& commands);
|
||||
static Bytes frame(const std::string& kind, const Bytes& payload = Bytes());
|
||||
int run();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::shared_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
// tai.py --mplex <command...>: any mplex command; new-session and attach-session attach this terminal.
|
||||
class MplexCli {
|
||||
public:
|
||||
static bool attaches(const std::string& name, const std::vector<std::string>& rest);
|
||||
static std::vector<std::vector<std::string>> split_commands(const std::vector<std::string>& words);
|
||||
// Returns the process exit code. Python main(home, words) writes errors to stderr.
|
||||
static int main(const std::string& home, const std::vector<std::string>& words);
|
||||
};
|
||||
|
||||
// What the shell hook execs into: a new mplex session attached to this terminal (--plexer-wrap).
|
||||
class NativePlexerBackend {
|
||||
public:
|
||||
NativePlexerBackend(const std::string& home, const std::string& profile, const std::vector<std::string>& shell_cmd = {})
|
||||
: home(home), profile(profile), shell_cmd(shell_cmd) {}
|
||||
std::string home, profile;
|
||||
std::vector<std::string> shell_cmd;
|
||||
// The int exit code of MplexCli.main, or an error string when pty support is missing (Python returns the str).
|
||||
int run();
|
||||
};
|
||||
|
||||
bool plexer_should_wrap(const std::vector<std::pair<std::string, std::string>>& env);
|
||||
|
||||
extern const char* const PLEXER_BASHRC_MARK_BEGIN;
|
||||
extern const char* const PLEXER_BASHRC_MARK_END;
|
||||
extern const char* const PLEXER_BASHRC_BLOCK;
|
||||
extern const char* const FISH_PLEXER_BLOCK;
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,68 @@
|
||||
// MplexClientConn (tai.py:13865) and MplexPending (:15332).
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_pane.h"
|
||||
#include "mplex_prompt.h"
|
||||
#include "mplex_mode.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexServer;
|
||||
|
||||
// A connection that has not yet said whether it speaks frames or one JSON line.
|
||||
struct MplexPending {
|
||||
explicit MplexPending(int fd_in);
|
||||
int fd; // the accepted, non-blocking socket
|
||||
Bytes buf;
|
||||
double created;
|
||||
};
|
||||
|
||||
// Server-side state of one connection: framing, key table position, prompt, overlay, last frame.
|
||||
class MplexClientConn : public std::enable_shared_from_this<MplexClientConn> {
|
||||
public:
|
||||
using Frame = std::pair<std::string, Bytes>; // (kind byte as 1-char string, payload)
|
||||
MplexClientConn(MplexServer* server_in, int fd_in, int ident);
|
||||
|
||||
MplexServer* server;
|
||||
int fd; // socket; -1 once closed
|
||||
int id;
|
||||
Bytes inbuf;
|
||||
Bytes out;
|
||||
int rows = 24, cols = 80;
|
||||
std::shared_ptr<MplexSession> session;
|
||||
bool attached = false;
|
||||
std::string table = "root";
|
||||
bool repeating = false;
|
||||
double repeat_until = 0.0;
|
||||
Bytes keybuf;
|
||||
std::optional<double> esc_deadline;
|
||||
std::shared_ptr<MplexPrompt> prompt;
|
||||
std::optional<std::pair<std::string, double>> message; // (text, expiry monotonic)
|
||||
std::shared_ptr<MplexOverlay> overlay;
|
||||
std::optional<Grid> prev; // last frame snapshot
|
||||
std::optional<std::pair<int, int>> cursor;
|
||||
bool dirty = true;
|
||||
double last_render = 0.0;
|
||||
long pid = 0;
|
||||
std::string tty, term, cwd;
|
||||
MplexPane::EnvList env;
|
||||
std::optional<int> env_pane;
|
||||
bool closing = false;
|
||||
double created;
|
||||
|
||||
// Queue a frame; false (and prev reset) when the out queue is over MPLEX_CLIENT_OUT_MAX.
|
||||
bool send(const std::string& kind, const Bytes& payload = Bytes());
|
||||
// Complete frames in what arrived; nullopt when the stream is corrupt (length over MPLEX_FRAME_MAX).
|
||||
std::optional<std::vector<Frame>> frames(const Bytes& data);
|
||||
void redispatch(const std::string& name, const Bytes& raw);
|
||||
void flush();
|
||||
void close();
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,16 @@
|
||||
// MplexClockMode (tai.py:13182).
|
||||
#pragma once
|
||||
#include "mplex_mode.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexClockMode : public MplexMode {
|
||||
public:
|
||||
MplexClockMode(MplexServer* server_in, std::shared_ptr<MplexPane> pane_in) : MplexMode(server_in, std::move(pane_in)) {
|
||||
name = "clock-mode";
|
||||
}
|
||||
ModeBlock render(MplexClientConn& client) override;
|
||||
void key(const std::string& name, const Bytes& raw, MplexClientConn& client) override;
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,113 @@
|
||||
// Generated from MPLEX_COMMAND_SPECS, MPLEX_DEFAULT_FORMATS, MPLEX_UNSUPPORTED and MPLEX_LAYOUT_NAMES in ../../../tai.py.
|
||||
// Regenerate, never hand edit: the C++ spec test compares these tables with mplex.md and tai.py.
|
||||
#include "mplex_commands.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
const std::vector<MplexCommandSpec>& mplex_command_specs() {
|
||||
static const std::vector<MplexCommandSpec> table = {
|
||||
{"attach-session", {"attach", "a"}, "dErxc:f:t:", 0, 0},
|
||||
{"bind-key", {"bind"}, "nrT:N:", 1, -1},
|
||||
{"break-pane", {"breakp"}, "abdPF:n:s:t:", 0, 0},
|
||||
{"capture-pane", {"capturep"}, "aCeJMNpPqTb:E:S:t:", 0, 0},
|
||||
{"choose-buffer", {}, "NrZF:f:K:O:t:", 0, 1},
|
||||
{"choose-client", {}, "NrZF:f:K:O:t:", 0, 1},
|
||||
{"choose-tree", {}, "GNrswZF:f:K:O:t:", 0, 1},
|
||||
{"clear-history", {"clearhist"}, "Ht:", 0, 0},
|
||||
{"clock-mode", {}, "t:", 0, 0},
|
||||
{"command-prompt", {}, "1bFkliNI:p:t:T:", 0, 1},
|
||||
{"confirm-before", {"confirm"}, "byc:p:t:", 1, 1},
|
||||
{"copy-mode", {}, "deHMqSus:t:", 0, 0},
|
||||
{"delete-buffer", {"deleteb"}, "b:", 0, 0},
|
||||
{"detach-client", {"detach"}, "aPE:s:t:", 0, 0},
|
||||
{"display-message", {"display"}, "aCIlNpvc:d:F:t:", 0, 1},
|
||||
{"display-panes", {"displayp"}, "bNd:t:", 0, 1},
|
||||
{"find-window", {"findw"}, "CiNrTZt:", 1, 1},
|
||||
{"has-session", {"has"}, "t:", 0, 0},
|
||||
{"if-shell", {"if"}, "bFt:", 2, 3},
|
||||
{"kill-pane", {"killp"}, "at:", 0, 0},
|
||||
{"kill-server", {}, "", 0, 0},
|
||||
{"kill-session", {}, "aCt:", 0, 0},
|
||||
{"kill-window", {"killw"}, "at:", 0, 0},
|
||||
{"last-pane", {"lastp"}, "deZt:", 0, 0},
|
||||
{"last-window", {"last"}, "t:", 0, 0},
|
||||
{"list-buffers", {"lsb"}, "F:f:", 0, 0},
|
||||
{"list-clients", {"lsc"}, "F:f:t:", 0, 0},
|
||||
{"list-commands", {"lscm"}, "F:", 0, 1},
|
||||
{"list-keys", {"lsk"}, "1aNP:T:", 0, 1},
|
||||
{"list-panes", {"lsp"}, "asF:f:t:", 0, 0},
|
||||
{"list-sessions", {"ls"}, "F:f:", 0, 0},
|
||||
{"list-windows", {"lsw"}, "aF:f:t:", 0, 0},
|
||||
{"load-buffer", {"loadb"}, "b:t:", 1, 1},
|
||||
{"move-window", {"movew"}, "abdkrs:t:", 0, 0},
|
||||
{"new-session", {"new"}, "AdDEPXc:e:F:f:n:s:t:x:y:", 0, -1},
|
||||
{"new-window", {"neww"}, "abdkPSc:e:F:n:t:", 0, -1},
|
||||
{"next-layout", {"nextl"}, "t:", 0, 0},
|
||||
{"next-window", {"next"}, "at:", 0, 0},
|
||||
{"paste-buffer", {"pasteb"}, "dprs:b:t:", 0, 0},
|
||||
{"previous-layout", {"prevl"}, "t:", 0, 0},
|
||||
{"previous-window", {"prev"}, "at:", 0, 0},
|
||||
{"refresh-client", {"refresh"}, "cDlLRSUA:B:C:f:r:t:", 0, 1},
|
||||
{"rename-session", {"rename"}, "t:", 1, 1},
|
||||
{"rename-window", {"renamew"}, "t:", 1, 1},
|
||||
{"resize-pane", {"resizep"}, "DLMRTUZx:y:t:", 0, 1},
|
||||
{"rotate-window", {"rotatew"}, "DUZt:", 0, 0},
|
||||
{"run-shell", {"run"}, "bCEc:d:t:", 0, 1},
|
||||
{"save-buffer", {"saveb"}, "ab:", 1, 1},
|
||||
{"select-layout", {"selectl"}, "Enopt:", 0, 1},
|
||||
{"select-pane", {"selectp"}, "DdeLlMmRUZT:t:", 0, 0},
|
||||
{"select-window", {"selectw"}, "lnpTt:", 0, 0},
|
||||
{"send-keys", {"send"}, "FHKlMRXc:N:t:", 0, -1},
|
||||
{"send-prefix", {}, "2t:", 0, 0},
|
||||
{"set-buffer", {"setb"}, "awb:n:t:", 0, 1},
|
||||
{"set-option", {"set"}, "aFgopqsuUwt:", 1, 2},
|
||||
{"set-window-option", {"setw"}, "aFgoqut:", 1, 2},
|
||||
{"show-buffer", {"showb"}, "b:", 0, 0},
|
||||
{"show-messages", {"showmsgs"}, "JTt:", 0, 0},
|
||||
{"show-options", {"show"}, "AgHpqsvwt:", 0, 1},
|
||||
{"show-window-options", {"showw"}, "gvt:", 0, 1},
|
||||
{"source-file", {"source"}, "Fnqvt:", 1, -1},
|
||||
{"split-window", {"splitw"}, "bdefhIPvZc:e:F:l:t:", 0, -1},
|
||||
{"start-server", {"start"}, "", 0, 0},
|
||||
{"suspend-client", {"suspendc"}, "t:", 0, 0},
|
||||
{"swap-pane", {"swapp"}, "dDUZs:t:", 0, 0},
|
||||
{"swap-window", {"swapw"}, "ds:t:", 0, 0},
|
||||
{"switch-client", {"switchc"}, "ElnprZc:t:T:", 0, 0},
|
||||
{"unbind-key", {"unbind"}, "anqT:", 0, 1},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
const MplexCommandSpec* mplex_find_spec(const std::string& name) {
|
||||
for (const auto& item : mplex_command_specs())
|
||||
if (item.name == name) return &item;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& mplex_unsupported() {
|
||||
static const std::vector<std::string> table = {"display-menu", "display-popup", "customize-mode", "lock-server", "lock-session", "lock-client", "pipe-pane", "join-pane", "move-pane", "link-window", "unlink-window", "respawn-pane", "respawn-window", "resize-window", "set-hook", "show-hooks", "wait-for", "server-access", "choose-tree-mouse"};
|
||||
return table;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& mplex_layout_names() {
|
||||
static const std::vector<std::string> table = {"even-horizontal", "even-vertical", "main-horizontal", "main-horizontal-mirrored", "main-vertical", "main-vertical-mirrored", "tiled"};
|
||||
return table;
|
||||
}
|
||||
|
||||
const std::string& mplex_default_format(const std::string& command) {
|
||||
static const std::vector<std::pair<std::string, std::string>> table = {
|
||||
{"list-sessions", "#{session_name}: #{session_windows} windows (created #{t:session_created})#{\?session_attached, (attached),}"},
|
||||
{"list-windows", "#{window_index}: #{window_name}#{window_flags} (#{window_panes} panes) [#{window_width}x#{window_height}] #{window_id}#{\?window_active, (active),}"},
|
||||
{"list-panes", "#{pane_index}: [#{pane_width}x#{pane_height}] [history #{history_size}/#{history_limit}] #{pane_id}#{\?pane_active, (active),}"},
|
||||
{"list-clients", "#{client_tty}: #{session_name} [#{client_width}x#{client_height} #{client_termname}]"},
|
||||
{"list-buffers", "#{buffer_name}: #{buffer_size} bytes: \"#{buffer_sample}\""},
|
||||
{"display-message", "[#{session_name}] #{window_index}:#{window_name}, current pane #{pane_index} - (%H:%M %d-%b-%y)"},
|
||||
};
|
||||
for (const auto& item : table)
|
||||
if (item.first == command) return item.second;
|
||||
throw std::out_of_range("no default format for " + command);
|
||||
}
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,49 @@
|
||||
// MplexCommands: tmux's command set (tai.py:15423-16800) and the command tables
|
||||
// MPLEX_COMMAND_SPECS (:15342), MPLEX_DEFAULT_FORMATS (:15413), MPLEX_UNSUPPORTED (:10023),
|
||||
// MPLEX_LAYOUT_NAMES (:10263). Owner: fork F6. Handlers are cmd_<name with _>(ctx, args).
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_args.h"
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_ctx.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexServer;
|
||||
|
||||
// name: (aliases, getopt spec, min positional, max positional); max < 0 is None.
|
||||
struct MplexCommandSpec {
|
||||
std::string name;
|
||||
std::vector<std::string> aliases;
|
||||
std::string spec;
|
||||
int min_args;
|
||||
int max_args;
|
||||
};
|
||||
// In the order of the Python dict literal.
|
||||
const std::vector<MplexCommandSpec>& mplex_command_specs();
|
||||
const MplexCommandSpec* mplex_find_spec(const std::string& name);
|
||||
const std::vector<std::string>& mplex_unsupported();
|
||||
const std::vector<std::string>& mplex_layout_names();
|
||||
// MPLEX_DEFAULT_FORMATS[name]; throws std::out_of_range for names without one.
|
||||
const std::string& mplex_default_format(const std::string& command);
|
||||
|
||||
class MplexCommands {
|
||||
public:
|
||||
explicit MplexCommands(MplexServer* server);
|
||||
~MplexCommands();
|
||||
// Resolve a word to a command name: exact, alias, unsupported (MplexError), unique prefix.
|
||||
static std::string resolve(const std::string& word);
|
||||
// Run `cmd_<name>`; the result is the command's output text ("" for None).
|
||||
std::string call(const std::string& name, MplexCtx& ctx, const MplexArgs& args);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,143 @@
|
||||
// Shared vocabulary of the Mplex port: Python-shaped primitives so the classes can be
|
||||
// transcribed line for line. Spec: taicpp/docs/mplex.md, source of truth: ../../tai.py.
|
||||
//
|
||||
// Mapping rules (every Mplex* file follows them):
|
||||
// str -> std::string (UTF-8) at API edges, UStr (std::wstring, one wchar_t per
|
||||
// code point, Linux wchar_t is 32 bit) wherever len/index/slice count code points
|
||||
// bytes -> Bytes (std::string holding raw bytes)
|
||||
// None -> std::optional / empty shared_ptr / 0 Color
|
||||
// dict (iterated) -> OrderedMap (insertion order kept, like Python)
|
||||
// tuple (fg,bg,a) -> Style; cell tuple -> Cell; cell None -> Cell with ch == 0
|
||||
// exceptions -> MplexError for user-facing failures, ValueError for Python ValueError
|
||||
// closures -> std::function
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
using Bytes = std::string;
|
||||
using UStr = std::wstring;
|
||||
|
||||
// A user-facing failure: the text is shown verbatim as the command's error.
|
||||
class MplexError : public std::runtime_error {
|
||||
public:
|
||||
explicit MplexError(const std::string& text) : std::runtime_error(text) {}
|
||||
};
|
||||
|
||||
// Python ValueError / generic Exception raised inside the control protocol; the text is
|
||||
// what short_error() would print.
|
||||
class ValueError : public std::runtime_error {
|
||||
public:
|
||||
explicit ValueError(const std::string& text) : std::runtime_error(text) {}
|
||||
};
|
||||
|
||||
// ---- text ------------------------------------------------------------------
|
||||
|
||||
// bytes.decode("utf-8", "replace"): maximal invalid subparts become one U+FFFD, like CPython.
|
||||
UStr to_ustr(const std::string& utf8);
|
||||
std::string to_utf8(const UStr& text);
|
||||
std::string to_utf8(wchar_t ch);
|
||||
// text.encode("utf-8", "replace") for strings that may carry lone surrogates: same as to_utf8.
|
||||
|
||||
// short_error(exc): str(exc) or the type name, capped at 300 chars.
|
||||
std::string short_error(const std::exception& exc, const std::string& type_name = "Exception");
|
||||
|
||||
// ---- colours, styles, cells ----------------------------------------------------
|
||||
|
||||
// An SGR colour fragment ("31", "104", "38;5;12", "48;2;1;2;3") packed in 32 bits. 0 is None.
|
||||
// Lossless for every fragment tai.py produces (see mplex.md, Style strings).
|
||||
using Color = uint32_t;
|
||||
Color color_from_sgr(const std::string& fragment); // "" -> 0
|
||||
std::string color_sgr(Color color); // 0 -> ""
|
||||
|
||||
// (fg, bg, attr) as MplexStyle produces and MplexCanvas stores.
|
||||
struct Style {
|
||||
Color fg = 0;
|
||||
Color bg = 0;
|
||||
int attr = 0;
|
||||
bool operator==(const Style& o) const { return fg == o.fg && bg == o.bg && attr == o.attr; }
|
||||
bool operator!=(const Style& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// (char, fg, bg, attr). ch == 0 stands for Python's None ("never written").
|
||||
struct Cell {
|
||||
wchar_t ch = 0;
|
||||
Color fg = 0;
|
||||
Color bg = 0;
|
||||
int attr = 0;
|
||||
bool present() const { return ch != 0; }
|
||||
bool operator==(const Cell& o) const { return ch == o.ch && fg == o.fg && bg == o.bg && attr == o.attr; }
|
||||
bool operator!=(const Cell& o) const { return !(*this == o); }
|
||||
};
|
||||
using Row = std::vector<Cell>;
|
||||
using Grid = std::vector<Row>;
|
||||
|
||||
constexpr Cell blank_cell() { return Cell{L' ', 0, 0, 0}; } // MplexCanvas.BLANK
|
||||
|
||||
// ---- ordered dict ----------------------------------------------------------------
|
||||
|
||||
// Python dict semantics for the parts the port uses: insertion order is iteration order,
|
||||
// setting an existing key keeps its position, pop/erase removes it.
|
||||
template <typename K, typename V>
|
||||
class OrderedMap {
|
||||
public:
|
||||
using Entry = std::pair<K, V>;
|
||||
bool contains(const K& key) const { return find_index(key) >= 0; }
|
||||
V* find(const K& key) {
|
||||
int i = find_index(key);
|
||||
return i < 0 ? nullptr : &items_[static_cast<size_t>(i)].second;
|
||||
}
|
||||
const V* find(const K& key) const {
|
||||
int i = find_index(key);
|
||||
return i < 0 ? nullptr : &items_[static_cast<size_t>(i)].second;
|
||||
}
|
||||
V& set(const K& key, V value) {
|
||||
int i = find_index(key);
|
||||
if (i >= 0) {
|
||||
items_[static_cast<size_t>(i)].second = std::move(value);
|
||||
return items_[static_cast<size_t>(i)].second;
|
||||
}
|
||||
items_.emplace_back(key, std::move(value));
|
||||
return items_.back().second;
|
||||
}
|
||||
// dict.pop(key, None): returns whether it was there.
|
||||
bool erase(const K& key) {
|
||||
int i = find_index(key);
|
||||
if (i < 0) return false;
|
||||
items_.erase(items_.begin() + i);
|
||||
return true;
|
||||
}
|
||||
void clear() { items_.clear(); }
|
||||
size_t size() const { return items_.size(); }
|
||||
bool empty() const { return items_.empty(); }
|
||||
std::vector<K> keys() const {
|
||||
std::vector<K> out;
|
||||
for (const auto& e : items_) out.push_back(e.first);
|
||||
return out;
|
||||
}
|
||||
std::vector<V> values() const {
|
||||
std::vector<V> out;
|
||||
for (const auto& e : items_) out.push_back(e.second);
|
||||
return out;
|
||||
}
|
||||
const std::vector<Entry>& items() const { return items_; }
|
||||
std::vector<Entry>& items() { return items_; }
|
||||
|
||||
private:
|
||||
int find_index(const K& key) const {
|
||||
for (size_t i = 0; i < items_.size(); ++i)
|
||||
if (items_[i].first == key) return static_cast<int>(i);
|
||||
return -1;
|
||||
}
|
||||
std::vector<Entry> items_;
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,54 @@
|
||||
// MplexBinding and MplexConfig: tmux 3.6 default key tables plus ~/.tai/mplex.toml. tai.py:10850.
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_options.h"
|
||||
#include "mplex_toml.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
struct MplexBinding {
|
||||
std::string command;
|
||||
bool repeat = false;
|
||||
std::string note;
|
||||
};
|
||||
|
||||
// One row of MPLEX_DEFAULT_KEYS: (table, key, command, repeat, note). Generated from tai.py.
|
||||
struct DefaultKey {
|
||||
const char* table;
|
||||
const char* key;
|
||||
const char* command;
|
||||
bool repeat;
|
||||
const char* note;
|
||||
};
|
||||
const std::vector<DefaultKey>& mplex_default_keys(); // mplex_default_keys.cpp
|
||||
|
||||
class MplexConfig {
|
||||
public:
|
||||
static constexpr size_t ERRORS_MAX = 50;
|
||||
using Table = OrderedMap<std::string, MplexBinding>;
|
||||
|
||||
MplexConfig(); // options + default tables
|
||||
MplexOptions options;
|
||||
OrderedMap<std::string, Table> tables; // insertion ordered, like the Python dicts
|
||||
std::vector<std::string> errors;
|
||||
std::string path;
|
||||
int skipped = 0;
|
||||
|
||||
void error(const std::string& text);
|
||||
void bind(const std::string& table, const std::string& key, const std::string& command, bool repeat = false,
|
||||
const std::string& note = "");
|
||||
bool unbind(const std::string& table, const std::string& key);
|
||||
const MplexBinding* lookup(const std::string& table, const std::string& key) const;
|
||||
static std::string default_path(const std::string& home);
|
||||
static std::shared_ptr<MplexConfig> load(const std::string& path);
|
||||
void apply_file(const std::string& path);
|
||||
void apply_data(const TomlValue& data);
|
||||
|
||||
private:
|
||||
void apply_binding(const std::string& table, const std::string& key, const TomlValue& spec);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,48 @@
|
||||
// MplexCtx (tai.py:13684) and MplexTargets (:13712).
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_pane.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexServer;
|
||||
class MplexClientConn;
|
||||
|
||||
// What a command runs against: the issuing client and its current session, window, pane.
|
||||
class MplexCtx {
|
||||
public:
|
||||
MplexCtx(MplexServer* server_in, std::shared_ptr<MplexClientConn> client_in = nullptr,
|
||||
std::shared_ptr<MplexPane> pane_in = nullptr, bool interactive_in = false);
|
||||
MplexServer* server;
|
||||
std::shared_ptr<MplexClientConn> client;
|
||||
bool interactive;
|
||||
int depth = 0;
|
||||
std::shared_ptr<MplexPane> pane;
|
||||
std::shared_ptr<MplexWindow> window;
|
||||
std::shared_ptr<MplexSession> session;
|
||||
};
|
||||
|
||||
// tmux target syntax: session, session:window, session:window.pane, %pane, @window, $session.
|
||||
class MplexTargets {
|
||||
public:
|
||||
struct WindowTarget {
|
||||
std::shared_ptr<MplexSession> session;
|
||||
std::optional<int> index;
|
||||
std::shared_ptr<MplexWindow> window;
|
||||
};
|
||||
struct PaneTarget {
|
||||
std::shared_ptr<MplexSession> session;
|
||||
std::shared_ptr<MplexWindow> window;
|
||||
std::shared_ptr<MplexPane> pane;
|
||||
};
|
||||
static std::shared_ptr<MplexSession> session(MplexCtx& ctx, const std::string& spec);
|
||||
static WindowTarget window(MplexCtx& ctx, const std::string& spec);
|
||||
static std::optional<int> window_index(const std::shared_ptr<MplexSession>& session, const std::string& text);
|
||||
static PaneTarget pane(MplexCtx& ctx, const std::string& spec);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,247 @@
|
||||
// Generated from MPLEX_DEFAULT_KEYS in ../../../tai.py (tmux 3.6 defaults minus mouse bindings).
|
||||
// Regenerate, never hand edit: the C++ spec test compares this table with mplex.md and tai.py.
|
||||
#include "mplex_config.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
const std::vector<DefaultKey>& mplex_default_keys() {
|
||||
static const std::vector<DefaultKey> table = {
|
||||
{"copy-mode", "Escape", "send-keys -X cancel", false, ""},
|
||||
{"copy-mode", "Space", "send-keys -X page-down", false, ""},
|
||||
{"copy-mode", ",", "send-keys -X jump-reverse", false, ""},
|
||||
{"copy-mode", ";", "send-keys -X jump-again", false, ""},
|
||||
{"copy-mode", "F", "command-prompt -1 -p \"(jump backward)\" { send-keys -X jump-backward -- \"%%\" }", false, ""},
|
||||
{"copy-mode", "N", "send-keys -X search-reverse", false, ""},
|
||||
{"copy-mode", "P", "send-keys -X toggle-position", false, ""},
|
||||
{"copy-mode", "R", "send-keys -X rectangle-toggle", false, ""},
|
||||
{"copy-mode", "T", "command-prompt -1 -p \"(jump to backward)\" { send-keys -X jump-to-backward -- \"%%\" }", false, ""},
|
||||
{"copy-mode", "X", "send-keys -X set-mark", false, ""},
|
||||
{"copy-mode", "f", "command-prompt -1 -p \"(jump forward)\" { send-keys -X jump-forward -- \"%%\" }", false, ""},
|
||||
{"copy-mode", "g", "command-prompt -p \"(goto line)\" { send-keys -X goto-line -- \"%%\" }", false, ""},
|
||||
{"copy-mode", "n", "send-keys -X search-again", false, ""},
|
||||
{"copy-mode", "q", "send-keys -X cancel", false, ""},
|
||||
{"copy-mode", "r", "send-keys -X refresh-from-pane", false, ""},
|
||||
{"copy-mode", "t", "command-prompt -1 -p \"(jump to forward)\" { send-keys -X jump-to-forward -- \"%%\" }", false, ""},
|
||||
{"copy-mode", "Home", "send-keys -X start-of-line", false, ""},
|
||||
{"copy-mode", "End", "send-keys -X end-of-line", false, ""},
|
||||
{"copy-mode", "NPage", "send-keys -X page-down", false, ""},
|
||||
{"copy-mode", "PPage", "send-keys -X page-up", false, ""},
|
||||
{"copy-mode", "Up", "send-keys -X cursor-up", false, ""},
|
||||
{"copy-mode", "Down", "send-keys -X cursor-down", false, ""},
|
||||
{"copy-mode", "Left", "send-keys -X cursor-left", false, ""},
|
||||
{"copy-mode", "Right", "send-keys -X cursor-right", false, ""},
|
||||
{"copy-mode", "M-1", "command-prompt -N -I 1 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-2", "command-prompt -N -I 2 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-3", "command-prompt -N -I 3 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-4", "command-prompt -N -I 4 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-5", "command-prompt -N -I 5 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-6", "command-prompt -N -I 6 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-7", "command-prompt -N -I 7 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-8", "command-prompt -N -I 8 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-9", "command-prompt -N -I 9 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode", "M-<", "send-keys -X history-top", false, ""},
|
||||
{"copy-mode", "M->", "send-keys -X history-bottom", false, ""},
|
||||
{"copy-mode", "M-R", "send-keys -X top-line", false, ""},
|
||||
{"copy-mode", "M-b", "send-keys -X previous-word", false, ""},
|
||||
{"copy-mode", "M-f", "send-keys -X next-word-end", false, ""},
|
||||
{"copy-mode", "M-l", "send-keys -X cursor-centre-horizontal", false, ""},
|
||||
{"copy-mode", "M-m", "send-keys -X back-to-indentation", false, ""},
|
||||
{"copy-mode", "M-r", "send-keys -X middle-line", false, ""},
|
||||
{"copy-mode", "M-v", "send-keys -X page-up", false, ""},
|
||||
{"copy-mode", "M-w", "send-keys -X copy-pipe-and-cancel", false, ""},
|
||||
{"copy-mode", "M-x", "send-keys -X jump-to-mark", false, ""},
|
||||
{"copy-mode", "M-{", "send-keys -X previous-paragraph", false, ""},
|
||||
{"copy-mode", "M-}", "send-keys -X next-paragraph", false, ""},
|
||||
{"copy-mode", "M-Up", "send-keys -X halfpage-up", false, ""},
|
||||
{"copy-mode", "M-Down", "send-keys -X halfpage-down", false, ""},
|
||||
{"copy-mode", "C-Space", "send-keys -X begin-selection", false, ""},
|
||||
{"copy-mode", "C-a", "send-keys -X start-of-line", false, ""},
|
||||
{"copy-mode", "C-b", "send-keys -X cursor-left", false, ""},
|
||||
{"copy-mode", "C-c", "send-keys -X cancel", false, ""},
|
||||
{"copy-mode", "C-e", "send-keys -X end-of-line", false, ""},
|
||||
{"copy-mode", "C-f", "send-keys -X cursor-right", false, ""},
|
||||
{"copy-mode", "C-g", "send-keys -X clear-selection", false, ""},
|
||||
{"copy-mode", "C-k", "send-keys -X copy-pipe-end-of-line-and-cancel", false, ""},
|
||||
{"copy-mode", "C-l", "send-keys -X cursor-centre-vertical", false, ""},
|
||||
{"copy-mode", "C-n", "send-keys -X cursor-down", false, ""},
|
||||
{"copy-mode", "C-p", "send-keys -X cursor-up", false, ""},
|
||||
{"copy-mode", "C-r", "command-prompt -i -I \"#{pane_search_string}\" -T search -p \"(search up)\" { send-keys -X search-backward-incremental -- \"%%\" }", false, ""},
|
||||
{"copy-mode", "C-s", "command-prompt -i -I \"#{pane_search_string}\" -T search -p \"(search down)\" { send-keys -X search-forward-incremental -- \"%%\" }", false, ""},
|
||||
{"copy-mode", "C-v", "send-keys -X page-down", false, ""},
|
||||
{"copy-mode", "C-w", "send-keys -X copy-pipe-and-cancel", false, ""},
|
||||
{"copy-mode", "C-Up", "send-keys -X scroll-up", false, ""},
|
||||
{"copy-mode", "C-Down", "send-keys -X scroll-down", false, ""},
|
||||
{"copy-mode", "C-M-b", "send-keys -X previous-matching-bracket", false, ""},
|
||||
{"copy-mode", "C-M-f", "send-keys -X next-matching-bracket", false, ""},
|
||||
{"copy-mode-vi", "Enter", "send-keys -X copy-pipe-and-cancel", false, ""},
|
||||
{"copy-mode-vi", "Escape", "send-keys -X clear-selection", false, ""},
|
||||
{"copy-mode-vi", "Space", "send-keys -X begin-selection", false, ""},
|
||||
{"copy-mode-vi", "#", "send-keys -FX search-backward -- \"#{copy_cursor_word}\"", false, ""},
|
||||
{"copy-mode-vi", "$", "send-keys -X end-of-line", false, ""},
|
||||
{"copy-mode-vi", "%", "send-keys -X next-matching-bracket", false, ""},
|
||||
{"copy-mode-vi", "*", "send-keys -FX search-forward -- \"#{copy_cursor_word}\"", false, ""},
|
||||
{"copy-mode-vi", ",", "send-keys -X jump-reverse", false, ""},
|
||||
{"copy-mode-vi", "/", "command-prompt -T search -p \"(search down)\" { send-keys -X search-forward -- \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "0", "send-keys -X start-of-line", false, ""},
|
||||
{"copy-mode-vi", "1", "command-prompt -N -I 1 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "2", "command-prompt -N -I 2 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "3", "command-prompt -N -I 3 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "4", "command-prompt -N -I 4 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "5", "command-prompt -N -I 5 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "6", "command-prompt -N -I 6 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "7", "command-prompt -N -I 7 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "8", "command-prompt -N -I 8 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "9", "command-prompt -N -I 9 -p (repeat) { send-keys -N \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", ":", "command-prompt -p \"(goto line)\" { send-keys -X goto-line -- \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", ";", "send-keys -X jump-again", false, ""},
|
||||
{"copy-mode-vi", "\?", "command-prompt -T search -p \"(search up)\" { send-keys -X search-backward -- \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "A", "send-keys -X append-selection-and-cancel", false, ""},
|
||||
{"copy-mode-vi", "B", "send-keys -X previous-space", false, ""},
|
||||
{"copy-mode-vi", "D", "send-keys -X copy-pipe-end-of-line-and-cancel", false, ""},
|
||||
{"copy-mode-vi", "E", "send-keys -X next-space-end", false, ""},
|
||||
{"copy-mode-vi", "F", "command-prompt -1 -p \"(jump backward)\" { send-keys -X jump-backward -- \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "G", "send-keys -X history-bottom", false, ""},
|
||||
{"copy-mode-vi", "H", "send-keys -X top-line", false, ""},
|
||||
{"copy-mode-vi", "J", "send-keys -X scroll-down", false, ""},
|
||||
{"copy-mode-vi", "K", "send-keys -X scroll-up", false, ""},
|
||||
{"copy-mode-vi", "L", "send-keys -X bottom-line", false, ""},
|
||||
{"copy-mode-vi", "M", "send-keys -X middle-line", false, ""},
|
||||
{"copy-mode-vi", "N", "send-keys -X search-reverse", false, ""},
|
||||
{"copy-mode-vi", "P", "send-keys -X toggle-position", false, ""},
|
||||
{"copy-mode-vi", "T", "command-prompt -1 -p \"(jump to backward)\" { send-keys -X jump-to-backward -- \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "V", "send-keys -X select-line", false, ""},
|
||||
{"copy-mode-vi", "W", "send-keys -X next-space", false, ""},
|
||||
{"copy-mode-vi", "X", "send-keys -X set-mark", false, ""},
|
||||
{"copy-mode-vi", "^", "send-keys -X back-to-indentation", false, ""},
|
||||
{"copy-mode-vi", "b", "send-keys -X previous-word", false, ""},
|
||||
{"copy-mode-vi", "e", "send-keys -X next-word-end", false, ""},
|
||||
{"copy-mode-vi", "f", "command-prompt -1 -p \"(jump forward)\" { send-keys -X jump-forward -- \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "g", "send-keys -X history-top", false, ""},
|
||||
{"copy-mode-vi", "h", "send-keys -X cursor-left", false, ""},
|
||||
{"copy-mode-vi", "j", "send-keys -X cursor-down", false, ""},
|
||||
{"copy-mode-vi", "k", "send-keys -X cursor-up", false, ""},
|
||||
{"copy-mode-vi", "l", "send-keys -X cursor-right", false, ""},
|
||||
{"copy-mode-vi", "n", "send-keys -X search-again", false, ""},
|
||||
{"copy-mode-vi", "o", "send-keys -X other-end", false, ""},
|
||||
{"copy-mode-vi", "q", "send-keys -X cancel", false, ""},
|
||||
{"copy-mode-vi", "r", "send-keys -X refresh-from-pane", false, ""},
|
||||
{"copy-mode-vi", "t", "command-prompt -1 -p \"(jump to forward)\" { send-keys -X jump-to-forward -- \"%%\" }", false, ""},
|
||||
{"copy-mode-vi", "v", "send-keys -X rectangle-toggle", false, ""},
|
||||
{"copy-mode-vi", "w", "send-keys -X next-word", false, ""},
|
||||
{"copy-mode-vi", "z", "send-keys -X scroll-middle", false, ""},
|
||||
{"copy-mode-vi", "{", "send-keys -X previous-paragraph", false, ""},
|
||||
{"copy-mode-vi", "}", "send-keys -X next-paragraph", false, ""},
|
||||
{"copy-mode-vi", "BSpace", "send-keys -X cursor-left", false, ""},
|
||||
{"copy-mode-vi", "Home", "send-keys -X start-of-line", false, ""},
|
||||
{"copy-mode-vi", "End", "send-keys -X end-of-line", false, ""},
|
||||
{"copy-mode-vi", "NPage", "send-keys -X page-down", false, ""},
|
||||
{"copy-mode-vi", "PPage", "send-keys -X page-up", false, ""},
|
||||
{"copy-mode-vi", "Up", "send-keys -X cursor-up", false, ""},
|
||||
{"copy-mode-vi", "Down", "send-keys -X cursor-down", false, ""},
|
||||
{"copy-mode-vi", "Left", "send-keys -X cursor-left", false, ""},
|
||||
{"copy-mode-vi", "Right", "send-keys -X cursor-right", false, ""},
|
||||
{"copy-mode-vi", "M-x", "send-keys -X jump-to-mark", false, ""},
|
||||
{"copy-mode-vi", "C-b", "send-keys -X page-up", false, ""},
|
||||
{"copy-mode-vi", "C-c", "send-keys -X cancel", false, ""},
|
||||
{"copy-mode-vi", "C-d", "send-keys -X halfpage-down", false, ""},
|
||||
{"copy-mode-vi", "C-e", "send-keys -X scroll-down", false, ""},
|
||||
{"copy-mode-vi", "C-f", "send-keys -X page-down", false, ""},
|
||||
{"copy-mode-vi", "C-h", "send-keys -X cursor-left", false, ""},
|
||||
{"copy-mode-vi", "C-j", "send-keys -X copy-pipe-and-cancel", false, ""},
|
||||
{"copy-mode-vi", "C-u", "send-keys -X halfpage-up", false, ""},
|
||||
{"copy-mode-vi", "C-v", "send-keys -X rectangle-toggle", false, ""},
|
||||
{"copy-mode-vi", "C-y", "send-keys -X scroll-up", false, ""},
|
||||
{"copy-mode-vi", "C-Up", "send-keys -X scroll-up", false, ""},
|
||||
{"copy-mode-vi", "C-Down", "send-keys -X scroll-down", false, ""},
|
||||
{"prefix", "Space", "next-layout", false, "Select next layout"},
|
||||
{"prefix", "!", "break-pane", false, "Break pane to a new window"},
|
||||
{"prefix", "\"", "split-window", false, "Split window vertically"},
|
||||
{"prefix", "#", "list-buffers", false, "List all paste buffers"},
|
||||
{"prefix", "$", "command-prompt -I \"#S\" { rename-session \"%%\" }", false, "Rename current session"},
|
||||
{"prefix", "%", "split-window -h", false, "Split window horizontally"},
|
||||
{"prefix", "&", "confirm-before -p \"kill-window #W\? (y/n)\" kill-window", false, "Kill current window"},
|
||||
{"prefix", "'", "command-prompt -T window-target -p index { select-window -t \":%%\" }", false, "Prompt for window index to select"},
|
||||
{"prefix", "(", "switch-client -p", false, "Switch to previous client"},
|
||||
{"prefix", ")", "switch-client -n", false, "Switch to next client"},
|
||||
{"prefix", ",", "command-prompt -I \"#W\" { rename-window \"%%\" }", false, "Rename current window"},
|
||||
{"prefix", "-", "delete-buffer", false, "Delete the most recent paste buffer"},
|
||||
{"prefix", ".", "command-prompt -T target { move-window -t \"%%\" }", false, "Move the current window"},
|
||||
{"prefix", "/", "command-prompt -k -p key { list-keys -1N \"%%\" }", false, "Describe key binding"},
|
||||
{"prefix", "0", "select-window -t :=0", false, "Select window 0"},
|
||||
{"prefix", "1", "select-window -t :=1", false, "Select window 1"},
|
||||
{"prefix", "2", "select-window -t :=2", false, "Select window 2"},
|
||||
{"prefix", "3", "select-window -t :=3", false, "Select window 3"},
|
||||
{"prefix", "4", "select-window -t :=4", false, "Select window 4"},
|
||||
{"prefix", "5", "select-window -t :=5", false, "Select window 5"},
|
||||
{"prefix", "6", "select-window -t :=6", false, "Select window 6"},
|
||||
{"prefix", "7", "select-window -t :=7", false, "Select window 7"},
|
||||
{"prefix", "8", "select-window -t :=8", false, "Select window 8"},
|
||||
{"prefix", "9", "select-window -t :=9", false, "Select window 9"},
|
||||
{"prefix", ":", "command-prompt", false, "Prompt for a command"},
|
||||
{"prefix", ";", "last-pane", false, "Move to the previously active pane"},
|
||||
{"prefix", "<", "display-menu", false, "Display window menu"},
|
||||
{"prefix", "=", "choose-buffer -Z", false, "Choose a paste buffer from a list"},
|
||||
{"prefix", ">", "display-menu", false, "Display pane menu"},
|
||||
{"prefix", "\?", "list-keys -N", false, "List key bindings"},
|
||||
{"prefix", "C", "customize-mode -Z", false, "Customize options"},
|
||||
{"prefix", "D", "choose-client -Z", false, "Choose and detach a client from a list"},
|
||||
{"prefix", "E", "select-layout -E", false, "Spread panes out evenly"},
|
||||
{"prefix", "L", "switch-client -l", false, "Switch to the last client"},
|
||||
{"prefix", "M", "select-pane -M", false, "Clear the marked pane"},
|
||||
{"prefix", "[", "copy-mode", false, "Enter copy mode"},
|
||||
{"prefix", "]", "paste-buffer -p", false, "Paste the most recent paste buffer"},
|
||||
{"prefix", "c", "new-window", false, "Create a new window"},
|
||||
{"prefix", "d", "detach-client", false, "Detach the current client"},
|
||||
{"prefix", "f", "command-prompt { find-window -Z \"%%\" }", false, "Search for a pane"},
|
||||
{"prefix", "i", "display-message", false, "Display window information"},
|
||||
{"prefix", "l", "last-window", false, "Select the previously current window"},
|
||||
{"prefix", "m", "select-pane -m", false, "Toggle the marked pane"},
|
||||
{"prefix", "n", "next-window", false, "Select the next window"},
|
||||
{"prefix", "o", "select-pane -t :.+", false, "Select the next pane"},
|
||||
{"prefix", "p", "previous-window", false, "Select the previous window"},
|
||||
{"prefix", "q", "display-panes", false, "Display pane numbers"},
|
||||
{"prefix", "r", "refresh-client", false, "Redraw the current client"},
|
||||
{"prefix", "s", "choose-tree -Zs", false, "Choose a session from a list"},
|
||||
{"prefix", "t", "clock-mode", false, "Show a clock"},
|
||||
{"prefix", "w", "choose-tree -Zw", false, "Choose a window from a list"},
|
||||
{"prefix", "x", "confirm-before -p \"kill-pane #P\? (y/n)\" kill-pane", false, "Kill the active pane"},
|
||||
{"prefix", "z", "resize-pane -Z", false, "Zoom the active pane"},
|
||||
{"prefix", "{", "swap-pane -U", false, "Swap the active pane with the pane above"},
|
||||
{"prefix", "}", "swap-pane -D", false, "Swap the active pane with the pane below"},
|
||||
{"prefix", "~", "show-messages", false, "Show messages"},
|
||||
{"prefix", "DC", "refresh-client -c", true, "Reset so the visible part of the window follows the cursor"},
|
||||
{"prefix", "PPage", "copy-mode -u", false, "Enter copy mode and scroll up"},
|
||||
{"prefix", "Up", "select-pane -U", true, "Select the pane above the active pane"},
|
||||
{"prefix", "Down", "select-pane -D", true, "Select the pane below the active pane"},
|
||||
{"prefix", "Left", "select-pane -L", true, "Select the pane to the left of the active pane"},
|
||||
{"prefix", "Right", "select-pane -R", true, "Select the pane to the right of the active pane"},
|
||||
{"prefix", "M-1", "select-layout even-horizontal", false, "Set the even-horizontal layout"},
|
||||
{"prefix", "M-2", "select-layout even-vertical", false, "Set the even-vertical layout"},
|
||||
{"prefix", "M-3", "select-layout main-horizontal", false, "Set the main-horizontal layout"},
|
||||
{"prefix", "M-4", "select-layout main-vertical", false, "Set the main-vertical layout"},
|
||||
{"prefix", "M-5", "select-layout tiled", false, "Select the tiled layout"},
|
||||
{"prefix", "M-6", "select-layout main-horizontal-mirrored", false, "Set the main-horizontal-mirrored layout"},
|
||||
{"prefix", "M-7", "select-layout main-vertical-mirrored", false, "Set the main-vertical-mirrored layout"},
|
||||
{"prefix", "M-n", "next-window -a", false, "Select the next window with an alert"},
|
||||
{"prefix", "M-o", "rotate-window -D", false, "Rotate through the panes in reverse"},
|
||||
{"prefix", "M-p", "previous-window -a", false, "Select the previous window with an alert"},
|
||||
{"prefix", "M-Up", "resize-pane -U 5", true, "Resize the pane up by 5"},
|
||||
{"prefix", "M-Down", "resize-pane -D 5", true, "Resize the pane down by 5"},
|
||||
{"prefix", "M-Left", "resize-pane -L 5", true, "Resize the pane left by 5"},
|
||||
{"prefix", "M-Right", "resize-pane -R 5", true, "Resize the pane right by 5"},
|
||||
{"prefix", "C-b", "send-prefix", false, "Send the prefix key"},
|
||||
{"prefix", "C-o", "rotate-window", false, "Rotate through the panes"},
|
||||
{"prefix", "C-z", "suspend-client", false, "Suspend the current client"},
|
||||
{"prefix", "C-Up", "resize-pane -U", true, "Resize the pane up"},
|
||||
{"prefix", "C-Down", "resize-pane -D", true, "Resize the pane down"},
|
||||
{"prefix", "C-Left", "resize-pane -L", true, "Resize the pane left"},
|
||||
{"prefix", "C-Right", "resize-pane -R", true, "Resize the pane right"},
|
||||
{"prefix", "S-Up", "refresh-client -U 10", true, "Move the visible part of the window up"},
|
||||
{"prefix", "S-Down", "refresh-client -D 10", true, "Move the visible part of the window down"},
|
||||
{"prefix", "S-Left", "refresh-client -L 10", true, "Move the visible part of the window left"},
|
||||
{"prefix", "S-Right", "refresh-client -R 10", true, "Move the visible part of the window right"},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,32 @@
|
||||
// MplexFlowScreen: one plexer_show screen on a native client (tai.py:13248).
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_json.h"
|
||||
#include "mplex_mode.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
struct MplexFlowRecord;
|
||||
|
||||
class MplexFlowScreen : public MplexOverlay {
|
||||
public:
|
||||
MplexFlowScreen(MplexServer* server_in, int flow_id_in, const std::string& title_in, const std::string& text_in,
|
||||
const std::vector<std::string>& options_in, int cursor_in, double until_in);
|
||||
MplexServer* server;
|
||||
int flow_id;
|
||||
std::string title, text;
|
||||
std::vector<std::string> options;
|
||||
int cursor;
|
||||
Style box, bold, sel, dim;
|
||||
|
||||
MplexFlowRecord* record() const; // server.flows.get(flow_id), null when gone
|
||||
// answer(client, status, value): value None / True / an option index.
|
||||
void answer(MplexClientConn& client, const std::string& status, const Json& value = Json());
|
||||
void key(const std::string& name, const Bytes& raw, MplexClientConn& client) override;
|
||||
std::vector<std::string> rows(int width) const;
|
||||
void draw(MplexCanvas& canvas, int area_y) override;
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,32 @@
|
||||
// MplexFormat: tmux format strings. tai.py:10987.
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexFormat {
|
||||
public:
|
||||
// resolve(name) -> value, or nullopt for Python None. "(command" names are #(shell) requests.
|
||||
using Resolver = std::function<std::optional<std::string>(const std::string&)>;
|
||||
static constexpr int DEPTH_MAX = 12;
|
||||
|
||||
static long match_close(const std::string& text, long start, char opener, char closer);
|
||||
static std::vector<std::string> split_commas(const std::string& text);
|
||||
static std::string expand(const std::string& text, const Resolver& resolve, int depth = 0);
|
||||
static bool truthy(const std::optional<std::string>& value);
|
||||
static std::string value_of(const std::string& text, const Resolver& resolve, int depth);
|
||||
static std::string evaluate(const std::string& inner, const Resolver& resolve, int depth);
|
||||
static long modifier_colon(const std::string& inner);
|
||||
static std::string apply_modifiers(const std::string& mods, const std::string& rest, const Resolver& resolve, int depth);
|
||||
static std::string apply_one(const std::string& spec, const std::string& text, const Resolver& resolve, int depth);
|
||||
static std::string compare(const std::string& op, const std::string& left, const std::string& right);
|
||||
static std::string with_time(const std::string& text, double stamp = -1.0); // time.strftime when "%" in text
|
||||
static std::string strip_styles(const std::string& text);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,34 @@
|
||||
// The slice of the tai.py TUI input parser that MplexKeys.split uses: tui_parse_one and the
|
||||
// events it yields (TuiKey and the event kinds that are not keys). Implemented in mplex_input.cpp.
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
// What tui_parse_one returns in its first slot.
|
||||
struct TuiEvent {
|
||||
enum class Kind {
|
||||
None, // Python None: consumed bytes that are not an event (or an empty buffer)
|
||||
NeedMore, // "need_more"
|
||||
Escape, // "escape"
|
||||
Key, // TuiKey
|
||||
Other, // TuiClick / TuiScroll / TuiPaste / TuiModeReply: anything MplexKeys maps to ""
|
||||
};
|
||||
Kind kind = Kind::None;
|
||||
std::string key; // TuiKey.key, e.g. "up", "ctrl+x", "alt+f", "f5"
|
||||
std::string chr; // TuiKey.char (UTF-8)
|
||||
std::vector<std::string> modifiers; // TuiKey.modifiers
|
||||
};
|
||||
|
||||
struct TuiParsed {
|
||||
TuiEvent event;
|
||||
Bytes rest; // the unconsumed tail of the buffer
|
||||
};
|
||||
|
||||
// tui_parse_one(buf, final): parse one event from the head of buf (see tai.py 23910).
|
||||
TuiParsed tui_parse_one(const Bytes& buf, bool final);
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,30 @@
|
||||
// MplexKeys: key names in tmux notation, byte decoding, byte encoding. tai.py:10270.
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_input.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexKeys {
|
||||
public:
|
||||
using Token = std::pair<std::string, Bytes>; // (name, raw); name is "" for sequences that are not keys
|
||||
struct Split {
|
||||
std::vector<Token> tokens;
|
||||
Bytes rest;
|
||||
};
|
||||
|
||||
static std::string canonical(const std::string& spec); // throws MplexError("empty key name"/"unknown key: ...")
|
||||
static std::string name_of_char(const Bytes& raw);
|
||||
static Split split(const Bytes& data, bool final = false);
|
||||
static Bytes to_bytes(const std::string& name, bool app_cursor = false); // throws MplexError
|
||||
static Bytes app_cursor_form(const Bytes& raw);
|
||||
|
||||
private:
|
||||
static std::string name_of_event(const TuiEvent& event, const Bytes& raw);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,52 @@
|
||||
// MplexLayoutNode and MplexLayout: the split tree. tai.py:11261.
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexPane;
|
||||
|
||||
class MplexLayoutNode : public std::enable_shared_from_this<MplexLayoutNode> {
|
||||
public:
|
||||
using Ptr = std::shared_ptr<MplexLayoutNode>;
|
||||
// kind: "pane", "lr" (side by side) or "tb" (stacked).
|
||||
MplexLayoutNode(const std::string& kind_in, std::shared_ptr<MplexPane> pane_in = nullptr, int w_in = 1, int h_in = 1);
|
||||
|
||||
bool reverse = false;
|
||||
std::string kind;
|
||||
std::vector<Ptr> children;
|
||||
std::weak_ptr<MplexLayoutNode> parent; // back reference
|
||||
std::shared_ptr<MplexPane> pane;
|
||||
int x = 0, y = 0, w = 1, h = 1;
|
||||
|
||||
Ptr parent_node() const { return parent.lock(); }
|
||||
std::vector<Ptr> leaves();
|
||||
Ptr adopt(const Ptr& child);
|
||||
};
|
||||
|
||||
class MplexLayout {
|
||||
public:
|
||||
using Node = MplexLayoutNode::Ptr;
|
||||
static int size_of(const Node& node, const std::string& kind);
|
||||
static void set_size(const Node& node, const std::string& kind, int value);
|
||||
static void assign(const Node& node, int x, int y, int w, int h);
|
||||
// Insert new_pane beside leaf; returns (root, new_leaf). size < 0 means None.
|
||||
static std::pair<Node, Node> split(Node root, const Node& leaf, const std::string& kind, bool before,
|
||||
const std::shared_ptr<MplexPane>& new_pane, int size = -1);
|
||||
static Node remove(Node root, const Node& leaf); // returns the new root (null when empty)
|
||||
static void resize_by(const Node& leaf, const std::string& kind, int delta, bool prefer_after);
|
||||
static std::vector<int> shares(int total, int count, bool spare_first);
|
||||
static void resize_to(const Node& leaf, const std::string& kind, int size);
|
||||
static Node preset(const std::string& name, const std::vector<std::shared_ptr<MplexPane>>& panes, int width,
|
||||
int height, int main_width, int main_height);
|
||||
// The pane to move to in direction "L"/"R"/"U"/"D", or null.
|
||||
static std::shared_ptr<MplexPane> neighbour(const std::vector<std::shared_ptr<MplexPane>>& panes,
|
||||
const std::shared_ptr<MplexPane>& current, char direction, int width,
|
||||
int height);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,50 @@
|
||||
// MplexMode: pane mode base (tai.py:12017), and MplexOverlay, the shared face of the client
|
||||
// overlays (MplexPaneNumbers, MplexFlowScreen) that Python duck-types.
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexServer;
|
||||
class MplexPane;
|
||||
class MplexClientConn;
|
||||
class MplexCanvas;
|
||||
|
||||
// What Mode.render returns: a pane-sized block of cells and the cursor inside it (None = hidden).
|
||||
struct ModeBlock {
|
||||
Grid grid;
|
||||
std::optional<std::pair<int, int>> cursor; // (x, y)
|
||||
};
|
||||
|
||||
// Pane mode base: owns the pane's keyboard and its drawing while it is on top.
|
||||
class MplexMode : public std::enable_shared_from_this<MplexMode> {
|
||||
public:
|
||||
MplexMode(MplexServer* server_in, std::shared_ptr<MplexPane> pane_in) : server(server_in), pane(std::move(pane_in)) {}
|
||||
virtual ~MplexMode() = default;
|
||||
|
||||
std::string name = "mode";
|
||||
MplexServer* server;
|
||||
std::shared_ptr<MplexPane> pane; // cycle with pane->modes is broken by MplexServer::discard_pane
|
||||
|
||||
virtual ModeBlock render(MplexClientConn& client); // base: NotImplementedError
|
||||
virtual void key(const std::string& name, const Bytes& raw, MplexClientConn& client); // base: no-op
|
||||
virtual void on_output(long pushed, long evicted); // base: no-op
|
||||
virtual void on_resize(); // base: no-op
|
||||
virtual void close(); // pane.pop_mode(self); server.mark_dirty(window)
|
||||
};
|
||||
|
||||
// Python duck typing: a client overlay has key(), draw() and an expiry (`until`, monotonic seconds).
|
||||
class MplexOverlay {
|
||||
public:
|
||||
virtual ~MplexOverlay() = default;
|
||||
double until = 0.0;
|
||||
virtual void key(const std::string& name, const Bytes& raw, MplexClientConn& client) = 0;
|
||||
virtual void draw(MplexCanvas& canvas, int area_y) = 0;
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,61 @@
|
||||
// MplexOptions: typed option store with a parent chain (window, session, global). tai.py:10723.
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
// A Python option value: bool, int or str.
|
||||
struct OptValue {
|
||||
enum class Kind { Bool, Int, Str };
|
||||
Kind kind = Kind::Str;
|
||||
bool b = false;
|
||||
long long i = 0;
|
||||
std::string s;
|
||||
static OptValue of_bool(bool v) { OptValue o; o.kind = Kind::Bool; o.b = v; return o; }
|
||||
static OptValue of_int(long long v) { OptValue o; o.kind = Kind::Int; o.i = v; return o; }
|
||||
static OptValue of_str(std::string v) { OptValue o; o.kind = Kind::Str; o.s = std::move(v); return o; }
|
||||
bool is_bool() const { return kind == Kind::Bool; }
|
||||
bool is_int() const { return kind == Kind::Int; }
|
||||
bool is_str() const { return kind == Kind::Str; }
|
||||
// Python truthiness of the value.
|
||||
bool truthy() const { return kind == Kind::Bool ? b : kind == Kind::Int ? i != 0 : !s.empty(); }
|
||||
// str(value) as a format variable: True/False are "1"/"0" at the call site, str() otherwise.
|
||||
std::string str() const { return kind == Kind::Bool ? (b ? "True" : "False") : kind == Kind::Int ? std::to_string(i) : s; }
|
||||
bool operator==(const OptValue& o) const {
|
||||
return kind == o.kind && b == o.b && i == o.i && s == o.s;
|
||||
}
|
||||
};
|
||||
|
||||
struct OptionDef {
|
||||
std::string name;
|
||||
std::string kind; // "str" "bool" "key" "colour" "style" "int:LOW:HIGH" "choice:a|b"
|
||||
OptValue def;
|
||||
};
|
||||
|
||||
class MplexOptions {
|
||||
public:
|
||||
static const char* const WORD_SEPARATORS;
|
||||
// DEFS in the order of the Python dict literal (the order show-options and list output use).
|
||||
static const std::vector<OptionDef>& defs();
|
||||
static const OptionDef* find_def(const std::string& name);
|
||||
|
||||
explicit MplexOptions(MplexOptions* parent_in = nullptr) : parent(parent_in) {}
|
||||
|
||||
MplexOptions* parent = nullptr;
|
||||
std::map<std::string, OptValue> values; // lookup only
|
||||
std::vector<std::string> value_order; // insertion order of names in values (dict order)
|
||||
|
||||
static OptValue check(const std::string& name, const std::string& raw); // throws MplexError
|
||||
OptValue get(const std::string& name) const; // throws std::out_of_range-like KeyError via MplexError for unknown names
|
||||
bool is_set(const std::string& name) const { return values.count(name) != 0; }
|
||||
OptValue set(const std::string& name, const std::string& raw, bool append = false);
|
||||
void toggle(const std::string& name);
|
||||
void unset(const std::string& name);
|
||||
static std::string show(const OptValue& value); // on/off/str
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -362,7 +362,7 @@ MplexLayoutNode::Ptr MplexWindow::leaf_of(const std::shared_ptr<MplexPane>& pane
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MplexWindow::set_active(const std::shared_ptr<MplexPane>& pane) {
|
||||
void MplexWindow::set_active(std::shared_ptr<MplexPane> pane) {
|
||||
if (pane != active) {
|
||||
last_pane = active;
|
||||
active = pane;
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// MplexPane (tai.py:11549), MplexWindow (:11775), MplexSession (:11825).
|
||||
#pragma once
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_layout.h"
|
||||
#include "mplex_options.h"
|
||||
#include "mplex_vtscreen.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexMode;
|
||||
class MplexWindow;
|
||||
class MplexSession;
|
||||
|
||||
// One pty-backed shell: its screen, scrollback, output queue and mode stack.
|
||||
class MplexPane : public std::enable_shared_from_this<MplexPane> {
|
||||
public:
|
||||
using EnvList = std::vector<std::pair<std::string, std::string>>;
|
||||
MplexPane(int ident, int cols, int rows, int history_limit, const std::string& home_dir,
|
||||
const std::string& stamp = "0");
|
||||
~MplexPane();
|
||||
|
||||
int id;
|
||||
std::weak_ptr<MplexWindow> window_ref; // back reference; use window() / set_window()
|
||||
int x = 0, y = 0, w, h;
|
||||
MplexVtScreen screen;
|
||||
int master_fd = -1; // -1 == None
|
||||
std::optional<pid_t> pid;
|
||||
bool dead = false;
|
||||
std::optional<int> exit_status;
|
||||
Bytes pending_in;
|
||||
long dropped = 0;
|
||||
std::vector<std::shared_ptr<MplexMode>> modes;
|
||||
int prefix_count = 1;
|
||||
double created;
|
||||
double last_active = 0.0;
|
||||
std::string shell_name;
|
||||
// last_search = (pattern, forward, ignore_case...) as MplexCopyMode stores it; see mplex_copymode.h.
|
||||
struct SearchState {
|
||||
std::string text;
|
||||
bool forward = true;
|
||||
bool literal = false;
|
||||
};
|
||||
std::optional<SearchState> last_search;
|
||||
std::string log_path;
|
||||
long log_size = 0;
|
||||
long input_bytes = 0;
|
||||
|
||||
std::shared_ptr<MplexWindow> window() const { return window_ref.lock(); }
|
||||
void set_window(const std::shared_ptr<MplexWindow>& value) { window_ref = value; }
|
||||
std::string tag() const; // "%<id>"
|
||||
std::string title() const; // screen.title or hostname
|
||||
void spawn(const std::string& file, const std::vector<std::string>& argv, const std::string& cwd,
|
||||
const EnvList& env);
|
||||
void place(int nx, int ny, int nw, int nh);
|
||||
void apply_winsize();
|
||||
void feed(const Bytes& data);
|
||||
void log(const Bytes& data);
|
||||
void write(const Bytes& data);
|
||||
void drain();
|
||||
std::shared_ptr<MplexMode> top_mode() const;
|
||||
std::shared_ptr<MplexMode> push_mode(const std::shared_ptr<MplexMode>& mode);
|
||||
void pop_mode(const std::shared_ptr<MplexMode>& mode = nullptr);
|
||||
template <typename T>
|
||||
std::shared_ptr<T> find_mode() const {
|
||||
for (const auto& mode : modes)
|
||||
if (auto hit = std::dynamic_pointer_cast<T>(mode)) return hit;
|
||||
return nullptr;
|
||||
}
|
||||
std::string foreground_command();
|
||||
std::string current_path();
|
||||
size_t history_size() const;
|
||||
void clear_history();
|
||||
void close(bool hard = false);
|
||||
std::string tail_log(long lines, size_t chunk_size = 65536, size_t max_bytes = 1 << 24);
|
||||
|
||||
private:
|
||||
int log_fd_ = -1; // None == -1
|
||||
};
|
||||
|
||||
class MplexWindow : public std::enable_shared_from_this<MplexWindow> {
|
||||
public:
|
||||
MplexWindow(int ident, const std::string& name_in, int width_in, int height_in, MplexOptions* parent_options);
|
||||
|
||||
int id;
|
||||
std::string name;
|
||||
MplexLayoutNode::Ptr root;
|
||||
std::shared_ptr<MplexPane> active;
|
||||
std::shared_ptr<MplexPane> last_pane;
|
||||
std::shared_ptr<MplexPane> zoomed;
|
||||
int width, height;
|
||||
int layout_index = -1;
|
||||
MplexOptions options;
|
||||
bool auto_rename = true;
|
||||
double created;
|
||||
std::weak_ptr<MplexSession> session_ref; // back reference
|
||||
|
||||
std::shared_ptr<MplexSession> session() const { return session_ref.lock(); }
|
||||
void set_session(const std::shared_ptr<MplexSession>& value) { session_ref = value; }
|
||||
std::string tag() const; // "@<id>"
|
||||
std::vector<std::shared_ptr<MplexPane>> panes() const;
|
||||
void relayout();
|
||||
std::vector<std::shared_ptr<MplexPane>> visible_panes() const;
|
||||
MplexLayoutNode::Ptr leaf_of(const std::shared_ptr<MplexPane>& pane) const;
|
||||
void set_active(std::shared_ptr<MplexPane> pane); // by value: callers pass window->last_pane, which set_active overwrites
|
||||
};
|
||||
|
||||
class MplexSession {
|
||||
public:
|
||||
MplexSession(int ident, const std::string& name_in, MplexOptions* parent_options, const std::string& cwd_in,
|
||||
const MplexPane::EnvList& env_in);
|
||||
|
||||
int id;
|
||||
std::string name;
|
||||
OrderedMap<int, std::shared_ptr<MplexWindow>> windows; // dict order = creation order
|
||||
std::optional<int> current;
|
||||
std::vector<std::shared_ptr<MplexWindow>> history;
|
||||
double created;
|
||||
MplexOptions options;
|
||||
std::string cwd;
|
||||
MplexPane::EnvList env;
|
||||
double last_attached;
|
||||
std::optional<std::pair<int, int>> size; // (cols, rows); the Python code sets/reads it dynamically
|
||||
|
||||
std::string tag() const; // "$<id>"
|
||||
std::shared_ptr<MplexWindow> window() const;
|
||||
std::vector<int> indexes() const; // sorted(self.windows)
|
||||
int free_index(int base) const;
|
||||
std::optional<int> last() const; // index of the window that was current before this one
|
||||
bool select(int index);
|
||||
std::optional<int> index_of(const std::shared_ptr<MplexWindow>& window) const;
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,20 @@
|
||||
// MplexPaneNumbers: the display-panes overlay (tai.py:13209).
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
#include "mplex_mode.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexWindow;
|
||||
|
||||
class MplexPaneNumbers : public MplexOverlay {
|
||||
public:
|
||||
MplexPaneNumbers(MplexServer* server_in, std::shared_ptr<MplexWindow> window_in, double until_in);
|
||||
MplexServer* server;
|
||||
std::shared_ptr<MplexWindow> window;
|
||||
void key(const std::string& name, const Bytes& raw, MplexClientConn& client) override;
|
||||
void draw(MplexCanvas& canvas, int area_y) override;
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,54 @@
|
||||
// MplexPrompt: the command line in the status row (tai.py:12999).
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexServer;
|
||||
class MplexClientConn;
|
||||
|
||||
// Keyword arguments of MplexPrompt.__init__ (history, single, numeric, key_name, on_change,
|
||||
// on_cancel, on_step, confirm).
|
||||
struct MplexPromptOptions {
|
||||
std::shared_ptr<std::vector<std::string>> history; // null -> a private empty list
|
||||
bool single = false, numeric = false, key_name = false, confirm = false;
|
||||
std::function<void(const std::string&)> on_change;
|
||||
std::function<void()> on_cancel;
|
||||
std::function<void(const std::string&, const std::string&)> on_step; // (key name, text)
|
||||
};
|
||||
|
||||
class MplexPrompt : public std::enable_shared_from_this<MplexPrompt> {
|
||||
public:
|
||||
using History = std::shared_ptr<std::vector<std::string>>; // shared by reference with server.prompt_history
|
||||
using Options = MplexPromptOptions;
|
||||
MplexPrompt(const std::string& label_in, const std::string& text_in,
|
||||
std::function<void(const std::string&)> on_done_in, Options options = {});
|
||||
|
||||
UStr label; // code point text: pos indexes into text
|
||||
UStr text;
|
||||
long pos;
|
||||
std::function<void(const std::string&)> on_done;
|
||||
History history;
|
||||
std::optional<long> hist_index;
|
||||
bool single, numeric, key_name;
|
||||
std::function<void(const std::string&)> on_change;
|
||||
std::function<void()> on_cancel;
|
||||
std::function<void(const std::string&, const std::string&)> on_step;
|
||||
bool confirm;
|
||||
|
||||
std::pair<UStr, long> view() const; // (label + text, len(label) + pos)
|
||||
void finish(MplexServer& server, MplexClientConn& client, const std::string& value);
|
||||
void cancel(MplexClientConn& client);
|
||||
void changed();
|
||||
static std::string printable(const std::string& name, const Bytes& raw);
|
||||
void key(MplexServer& server, MplexClientConn& client, const std::string& name, const Bytes& raw);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,29 @@
|
||||
// MplexRenderer: composes one client's whole screen (tai.py:13535).
|
||||
#pragma once
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#include "mplex_canvas.h"
|
||||
#include "mplex_clientconn.h"
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_mode.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexServer;
|
||||
|
||||
class MplexRenderer {
|
||||
public:
|
||||
explicit MplexRenderer(MplexServer* server_in) : server(server_in) {}
|
||||
MplexServer* server;
|
||||
|
||||
ModeBlock block_of(const std::shared_ptr<MplexPane>& pane, MplexClientConn& client);
|
||||
MplexCanvas frame(MplexClientConn& client);
|
||||
void borders(MplexCanvas& canvas, const std::shared_ptr<MplexWindow>& window, int area_y);
|
||||
void status(MplexCanvas& canvas, MplexClientConn& client, const std::shared_ptr<MplexSession>& session,
|
||||
const std::shared_ptr<MplexWindow>& window, int row);
|
||||
static Row fit_list(const Row& cells, int avail, int current_at, const Style& base);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -27,13 +27,22 @@
|
||||
#include "mplex_keys.h"
|
||||
#include "mplex_py.h"
|
||||
#include "mplex_renderer.h"
|
||||
#if __has_include("version.hpp")
|
||||
#include "version.hpp"
|
||||
#define MPLEX_HAVE_VERSION_HPP 1
|
||||
#endif
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
namespace {
|
||||
|
||||
// tai.py VERSION: the `version` format variable.
|
||||
constexpr const char* MPLEX_VERSION_TEXT = "1.44.2";
|
||||
// tai.py VERSION: the `version` format variable. The build generates version.hpp from VERSION
|
||||
// (src/cli/version.hpp.in); without it (standalone mplex builds) the value of the day is used.
|
||||
#ifdef MPLEX_HAVE_VERSION_HPP
|
||||
const std::string MPLEX_VERSION_TEXT(detz::kVersion);
|
||||
#else
|
||||
const std::string MPLEX_VERSION_TEXT = "1.44.2";
|
||||
#endif
|
||||
|
||||
MplexServer* g_server = nullptr;
|
||||
|
||||
@@ -541,7 +550,7 @@ void MplexServer::discard_pane(const std::shared_ptr<MplexPane>& pane) {
|
||||
pane->modes.clear(); // breaks the pane <-> mode ownership cycle
|
||||
}
|
||||
|
||||
void MplexServer::select_pane(const std::shared_ptr<MplexWindow>& window, const std::shared_ptr<MplexPane>& pane) {
|
||||
void MplexServer::select_pane(std::shared_ptr<MplexWindow> window, std::shared_ptr<MplexPane> pane) {
|
||||
if (window->zoomed && pane != window->zoomed) {
|
||||
window->zoomed = nullptr;
|
||||
window->relayout();
|
||||
@@ -727,7 +736,7 @@ std::optional<std::string> MplexServer::variable(const std::string& name, const
|
||||
}
|
||||
if (name == "host") return py::gethostname();
|
||||
if (name == "host_short") return py::split(py::gethostname(), ".")[0];
|
||||
if (name == "version") return std::string(MPLEX_VERSION_TEXT);
|
||||
if (name == "version") return MPLEX_VERSION_TEXT;
|
||||
if (session) {
|
||||
if (name == "session_name") return session->name;
|
||||
if (name == "session_id") return session->tag();
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// MplexServer (tai.py:13942-15330): owns every session, window, pane and client. One select
|
||||
// loop, no threads. Method and field names are the Python ones.
|
||||
#pragma once
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_args.h"
|
||||
#include "mplex_clientconn.h"
|
||||
#include "mplex_common.h"
|
||||
#include "mplex_config.h"
|
||||
#include "mplex_ctx.h"
|
||||
#include "mplex_flowscreen.h"
|
||||
#include "mplex_format.h"
|
||||
#include "mplex_json.h"
|
||||
#include "mplex_pane.h"
|
||||
#include "mplex_prompt.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexCommands;
|
||||
class MplexRenderer;
|
||||
|
||||
// Hard caps (mplex.md, "Hard caps"). Names are the Python constants.
|
||||
constexpr const char* MPLEX_SOCKET_NAME = "server.sock";
|
||||
constexpr const char* MPLEX_LOCK_NAME = "server.lock";
|
||||
constexpr const char* MPLEX_LOG_NAME = "server.log";
|
||||
constexpr const char* MPLEX_CONFIG_NAME = "mplex.toml";
|
||||
constexpr const char* MPLEX_MAGIC = "\x01";
|
||||
constexpr long MPLEX_FRAME_MAX = 1L << 22;
|
||||
constexpr long MPLEX_CLIENT_OUT_MAX = 1L << 22;
|
||||
constexpr long MPLEX_PANE_IN_MAX = 1L << 22;
|
||||
constexpr int MPLEX_MAX_SESSIONS = 64;
|
||||
constexpr int MPLEX_MAX_WINDOWS = 256;
|
||||
constexpr int MPLEX_MAX_PANES = 256;
|
||||
constexpr int MPLEX_MAX_CLIENTS = 32;
|
||||
constexpr int MPLEX_MESSAGES_MAX = 200;
|
||||
constexpr int MPLEX_HISTORY_CAP = 200000;
|
||||
constexpr int MPLEX_BUFFERS_CAP = 100;
|
||||
constexpr int MPLEX_PROMPT_HISTORY_CAP = 100;
|
||||
constexpr int MPLEX_COMMAND_DEPTH_MAX = 16;
|
||||
constexpr double MPLEX_SHELL_TIMEOUT = 5.0;
|
||||
constexpr long MPLEX_SHELL_OUTPUT_MAX = 65536;
|
||||
constexpr long MPLEX_PASTE_MAX = 1L << 22;
|
||||
constexpr double MPLEX_RENDER_INTERVAL = 0.012;
|
||||
constexpr double MPLEX_REAP_KILL_AFTER = 2.0;
|
||||
constexpr double MPLEX_IDLE_EXIT = 15.0;
|
||||
constexpr int MPLEX_LOOP_ERRORS_MAX = 50;
|
||||
constexpr long MPLEX_LOG_MAX = 1L << 24;
|
||||
constexpr long MPLEX_CONFIG_MAX = 1L << 20;
|
||||
constexpr int MPLEX_FLOW_MAX = 16;
|
||||
constexpr int MPLEX_FLOW_KEEP = 300;
|
||||
constexpr const char* PLEXER_DIR_NAME = "plexer";
|
||||
constexpr const char* PLEXER_PANE_ENV = "TAI_PLEXER_PANE";
|
||||
constexpr const char* PLEXER_SOCKET_ENV = "TAI_PLEXER_SOCKET";
|
||||
constexpr const char* PLEXER_ACTIVE_ENV = "TAI_PLEXER_ACTIVE";
|
||||
constexpr long PLEXER_RECV_MAX = 1L << 20;
|
||||
constexpr int PLEXER_FLOW_TITLE_MAX = 200;
|
||||
constexpr int PLEXER_FLOW_TEXT_MAX = 2000;
|
||||
constexpr int PLEXER_FLOW_LABEL_MAX = 200;
|
||||
constexpr int PLEXER_FLOW_ANSWER_MAX = 2000;
|
||||
constexpr int PLEXER_FLOW_OPTIONS_MAX = 12;
|
||||
constexpr int PLEXER_FLOW_TIMEOUT_MAX = 600;
|
||||
|
||||
// plexer_dir(home): <home>/plexer, created 0700.
|
||||
std::string plexer_dir(const std::string& home);
|
||||
|
||||
// A paste buffer ({"name", "data"}).
|
||||
struct MplexBuffer {
|
||||
std::string name;
|
||||
std::string data;
|
||||
};
|
||||
|
||||
// The answer a UI flow collected: {"status", "value"}.
|
||||
struct MplexFlowAnswer {
|
||||
std::string status;
|
||||
Json value;
|
||||
};
|
||||
|
||||
// One pending UI flow (server.flows[id]).
|
||||
struct MplexFlowRecord {
|
||||
int id = 0;
|
||||
std::shared_ptr<MplexClientConn> client;
|
||||
std::string kind;
|
||||
double until = 0.0;
|
||||
double started = 0.0;
|
||||
std::optional<double> settled;
|
||||
std::optional<MplexFlowAnswer> answer;
|
||||
std::shared_ptr<MplexFlowScreen> overlay;
|
||||
std::shared_ptr<MplexPrompt> prompt;
|
||||
};
|
||||
|
||||
class MplexServer {
|
||||
public:
|
||||
explicit MplexServer(const std::string& home);
|
||||
~MplexServer();
|
||||
|
||||
std::string home, dir, sock_path;
|
||||
std::shared_ptr<MplexConfig> config;
|
||||
MplexOptions* options; // &config->options
|
||||
OrderedMap<int, std::shared_ptr<MplexSession>> sessions;
|
||||
OrderedMap<int, std::shared_ptr<MplexWindow>> windows;
|
||||
OrderedMap<int, std::shared_ptr<MplexPane>> panes;
|
||||
std::vector<std::shared_ptr<MplexClientConn>> clients;
|
||||
std::vector<std::shared_ptr<MplexPending>> conns;
|
||||
std::vector<MplexBuffer> buffers;
|
||||
std::deque<std::pair<double, std::string>> messages; // maxlen MPLEX_MESSAGES_MAX
|
||||
std::map<std::string, MplexPrompt::History> prompt_history;
|
||||
OrderedMap<int, MplexFlowRecord> flows;
|
||||
int flow_seq = 0;
|
||||
std::vector<std::pair<double, std::function<void()>>> timers;
|
||||
std::vector<std::pair<pid_t, double>> reaping;
|
||||
std::shared_ptr<MplexPane> marked;
|
||||
std::map<std::string, int> ids;
|
||||
bool had_session = false;
|
||||
bool running = false;
|
||||
int listener = -1;
|
||||
int lock_fd = -1;
|
||||
std::unique_ptr<MplexRenderer> renderer;
|
||||
std::unique_ptr<MplexCommands> commands;
|
||||
std::map<std::string, std::pair<double, std::string>> shell_cache;
|
||||
double started;
|
||||
std::string stamp;
|
||||
double next_tick = 0.0;
|
||||
std::optional<std::string> notice;
|
||||
|
||||
// ---- bookkeeping
|
||||
int next_id(const std::string& kind);
|
||||
void log_message(const std::string& text);
|
||||
std::shared_ptr<MplexSession> latest_session() const;
|
||||
std::vector<std::shared_ptr<MplexClientConn>> attached_clients(const std::shared_ptr<MplexSession>& session = nullptr) const;
|
||||
MplexOptions* pane_options(const std::shared_ptr<MplexPane>& pane);
|
||||
void message(const std::shared_ptr<MplexClientConn>& client, const std::string& text, std::optional<long> milliseconds = std::nullopt);
|
||||
void mark_dirty(const std::shared_ptr<MplexWindow>& window = nullptr);
|
||||
void mark_all_dirty();
|
||||
void add_timer(double deadline, std::function<void()> callback);
|
||||
|
||||
// ---- buffers and copy
|
||||
std::string add_buffer(const std::string& text, const std::optional<std::string>& name = std::nullopt);
|
||||
std::string top_buffer_text() const;
|
||||
MplexBuffer& find_buffer(const std::string& name); // throws MplexError("no buffers"/"unknown buffer: x")
|
||||
void run_shell_input(const std::string& command, const std::string& text);
|
||||
void store_copy(const std::string& text, const std::string& command = "", bool append = false);
|
||||
|
||||
// ---- sizing
|
||||
void resize_session(const std::shared_ptr<MplexSession>& session);
|
||||
void resize_all();
|
||||
|
||||
// ---- model
|
||||
std::string default_shell(MplexOptions& options) const;
|
||||
std::string resolve_term(const std::string& name) const;
|
||||
std::shared_ptr<MplexPane> spawn_pane(const std::shared_ptr<MplexWindow>& window, int width, int height,
|
||||
const std::vector<std::string>& words, const std::string& cwd,
|
||||
const MplexPane::EnvList* env_extra = nullptr);
|
||||
void register_window(const std::shared_ptr<MplexSession>& session, const std::shared_ptr<MplexWindow>& window, int index,
|
||||
bool select);
|
||||
std::shared_ptr<MplexWindow> create_window(const std::shared_ptr<MplexSession>& session, const std::string& name,
|
||||
const std::string& cwd, const std::vector<std::string>& words,
|
||||
const MplexPane::EnvList* env_extra, int index, bool select);
|
||||
std::shared_ptr<MplexSession> create_session(const std::string& name, const std::string& cwd,
|
||||
const MplexPane::EnvList& env, const std::vector<std::string>& words,
|
||||
const std::string& window_name, std::pair<int, int> size);
|
||||
std::string auto_session_name() const;
|
||||
std::shared_ptr<MplexPane> split_pane(const std::shared_ptr<MplexWindow>& window, const std::shared_ptr<MplexPane>& target,
|
||||
const std::string& kind, bool before, const std::vector<std::string>& words,
|
||||
const std::string& cwd, const MplexPane::EnvList* env_extra, int size, bool select);
|
||||
void discard_pane(const std::shared_ptr<MplexPane>& pane);
|
||||
void select_pane(std::shared_ptr<MplexWindow> window, std::shared_ptr<MplexPane> pane); // by value: pane may alias window->last_pane
|
||||
void kill_pane(const std::shared_ptr<MplexPane>& pane);
|
||||
void kill_window(const std::shared_ptr<MplexWindow>& window);
|
||||
void renumber(const std::shared_ptr<MplexSession>& session);
|
||||
void kill_session(const std::shared_ptr<MplexSession>& session);
|
||||
void attach_client(const std::shared_ptr<MplexClientConn>& client, const std::shared_ptr<MplexSession>& session);
|
||||
void detach_client(const std::shared_ptr<MplexClientConn>& client, const std::string& text = "", const std::string& kind = "D");
|
||||
|
||||
// ---- formats
|
||||
std::string window_flags(const std::shared_ptr<MplexSession>& session, const std::shared_ptr<MplexWindow>& window);
|
||||
MplexFormat::Resolver resolver_for(const std::shared_ptr<MplexSession>& session, const std::shared_ptr<MplexWindow>& window,
|
||||
const std::shared_ptr<MplexPane>& pane, const std::shared_ptr<MplexClientConn>& client = nullptr);
|
||||
std::string shell_output(const std::string& command);
|
||||
std::optional<std::string> variable(const std::string& name, const std::shared_ptr<MplexSession>& session,
|
||||
const std::shared_ptr<MplexWindow>& window, const std::shared_ptr<MplexPane>& pane,
|
||||
const std::shared_ptr<MplexClientConn>& client);
|
||||
// Format-expand text for a command context (strftime applied); extra overrides variables.
|
||||
std::string expand(const std::string& text, MplexCtx& ctx, const std::map<std::string, std::string>* extra = nullptr);
|
||||
|
||||
// ---- key handling
|
||||
bool is_prefix(const std::shared_ptr<MplexSession>& session, const std::string& name);
|
||||
void input_bytes(const std::shared_ptr<MplexClientConn>& client, const Bytes& data);
|
||||
void process_keys(const std::shared_ptr<MplexClientConn>& client, bool final);
|
||||
void handle_key(const std::shared_ptr<MplexClientConn>& client, const std::string& name, const Bytes& raw);
|
||||
void forward(const std::shared_ptr<MplexPane>& pane, const std::shared_ptr<MplexWindow>& window, const Bytes& raw);
|
||||
void run_binding(const std::shared_ptr<MplexClientConn>& client, const MplexBinding& binding,
|
||||
const std::shared_ptr<MplexPane>& pane = nullptr);
|
||||
void run_checked(MplexCtx& ctx, const std::string& text);
|
||||
void show_output(MplexCtx& ctx, const std::string& text);
|
||||
void ask_confirm(const std::shared_ptr<MplexClientConn>& client, const std::string& text, std::function<void()> callback);
|
||||
void refresh_choosers();
|
||||
|
||||
// ---- command execution
|
||||
std::string run_line(MplexCtx& ctx, const std::string& text, int depth = 0);
|
||||
std::string run_commands(MplexCtx& ctx, const std::vector<std::vector<std::string>>& commands, int depth = 0);
|
||||
std::string run_words(MplexCtx& ctx, const std::vector<std::string>& words);
|
||||
std::string run_one(MplexCtx& ctx, const std::vector<std::string>& words, int depth);
|
||||
|
||||
// ---- sockets and control protocol
|
||||
int open_listener(); // -1 when another server holds the lock
|
||||
void accept_connection(); // Python: accept()
|
||||
void pending_read(const std::shared_ptr<MplexPending>& pending);
|
||||
void drop_pending(const std::shared_ptr<MplexPending>& pending);
|
||||
void control(const std::shared_ptr<MplexPending>& pending);
|
||||
std::shared_ptr<MplexPane> control_pane(const Json& request);
|
||||
Json dispatch_control(const Json& request);
|
||||
void flow_prune(double now);
|
||||
void flow_ui_clear(MplexFlowRecord& found);
|
||||
std::shared_ptr<MplexClientConn> flow_client(const std::shared_ptr<MplexPane>& pane);
|
||||
struct FlowScreenSpec {
|
||||
std::string kind, title, text, initial;
|
||||
std::vector<std::string> options;
|
||||
int default_index = 0;
|
||||
};
|
||||
FlowScreenSpec flow_check_screen(const Json& raw);
|
||||
Json flow_show(const Json& request);
|
||||
void flow_answered(int flow_id, const std::string& status, const Json& value = Json());
|
||||
Json flow_result(const Json& request);
|
||||
Json flow_cancel(const Json& request);
|
||||
void client_read(const std::shared_ptr<MplexClientConn>& client);
|
||||
void client_data(const std::shared_ptr<MplexClientConn>& client, const Bytes& data);
|
||||
void on_hello(const std::shared_ptr<MplexClientConn>& client, const Bytes& payload);
|
||||
void drop_client(const std::shared_ptr<MplexClientConn>& client);
|
||||
|
||||
// ---- main loop
|
||||
int serve();
|
||||
void stop();
|
||||
void loop();
|
||||
void iterate();
|
||||
bool clients_busy() const;
|
||||
void sweep_bad_fds();
|
||||
double wakeup(double now);
|
||||
void run_deadlines(double now);
|
||||
void tick(double now);
|
||||
void pane_read(const std::shared_ptr<MplexPane>& pane);
|
||||
void pane_exited(const std::shared_ptr<MplexPane>& pane);
|
||||
void reap_children(double now);
|
||||
void render_clients(double now);
|
||||
void close_finished();
|
||||
void shutdown();
|
||||
static bool probe(const std::string& path);
|
||||
static bool ensure_running(const std::string& home, double wait = 5.0);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,23 @@
|
||||
// MplexStyle: tmux style strings to (fg, bg, attr). tai.py:10653.
|
||||
#pragma once
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class MplexStyle {
|
||||
public:
|
||||
static constexpr int ATTR_BOLD = 1, ATTR_DIM = 2, ATTR_UNDERLINE = 4, ATTR_REVERSE = 8, ATTR_STRIKE = 16,
|
||||
ATTR_ITALIC = 32, ATTR_BLINK = 64, ATTR_HIDDEN = 128;
|
||||
// None (0) for default/terminal/empty; throws MplexError("bad colour: ...").
|
||||
static Color color(const std::string& text, bool background);
|
||||
// Apply a style string on top of base; "default" returns to reset (base when absent).
|
||||
static Style parse(const std::string& text, const Style& base = Style{},
|
||||
const std::optional<Style>& reset = std::nullopt);
|
||||
static std::string sgr(Color fg, Color bg, int attr);
|
||||
static std::string sgr(const Style& style) { return sgr(style.fg, style.bg, style.attr); }
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,42 @@
|
||||
// TOML 1.0 reader with tomllib.loads semantics (Python 3.11+ standard library), including its
|
||||
// error texts ("Invalid value (at line 1, column 5)", ...). Implemented in mplex_toml.cpp.
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_common.h"
|
||||
|
||||
namespace detz::mplex {
|
||||
|
||||
class TomlError : public std::runtime_error { // tomllib.TOMLDecodeError (a ValueError)
|
||||
public:
|
||||
explicit TomlError(const std::string& text) : std::runtime_error(text) {}
|
||||
};
|
||||
|
||||
class TomlValue {
|
||||
public:
|
||||
enum class Kind { Bool, Int, Float, String, Array, Table, Datetime };
|
||||
using Array = std::vector<TomlValue>;
|
||||
using Table = std::vector<std::pair<std::string, TomlValue>>; // insertion ordered
|
||||
|
||||
Kind kind = Kind::Table;
|
||||
bool boolean = false;
|
||||
long long integer = 0;
|
||||
double number = 0.0;
|
||||
std::string text; // String content, or the ISO text of a Datetime
|
||||
Array array;
|
||||
Table table;
|
||||
|
||||
bool is_table() const { return kind == Kind::Table; }
|
||||
bool is_string() const { return kind == Kind::String; }
|
||||
bool is_bool() const { return kind == Kind::Bool; }
|
||||
bool is_int() const { return kind == Kind::Int; } // never true for Bool
|
||||
bool is_array() const { return kind == Kind::Array; }
|
||||
const TomlValue* get(const std::string& key) const;
|
||||
|
||||
// tomllib.loads(text): throws TomlError.
|
||||
static TomlValue loads(const std::string& text);
|
||||
};
|
||||
|
||||
} // namespace detz::mplex
|
||||
@@ -0,0 +1,156 @@
|
||||
// uiflow (uiflow.md): the controller side of the plexer protocol (PlexerController, its native
|
||||
// and tmux backends, the factory), the plexer_show spec validation and the PlexerFlow driver.
|
||||
// Port of tai.py:9302-9985. Namespace detz::plexer, names are the Python ones.
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_json.h"
|
||||
|
||||
namespace detz::plexer {
|
||||
|
||||
using detz::mplex::Json;
|
||||
|
||||
constexpr const char* PLEXER_DIR_NAME = "plexer";
|
||||
constexpr const char* PLEXER_PANE_ENV = "TAI_PLEXER_PANE";
|
||||
constexpr const char* PLEXER_SOCKET_ENV = "TAI_PLEXER_SOCKET";
|
||||
constexpr const char* PLEXER_ACTIVE_ENV = "TAI_PLEXER_ACTIVE";
|
||||
constexpr long PLEXER_RECV_MAX = 1L << 20;
|
||||
constexpr int PLEXER_FLOW_VERSION = 1;
|
||||
constexpr int PLEXER_FLOW_SCREENS_MAX = 32;
|
||||
constexpr int PLEXER_FLOW_FIELDS_MAX = 12;
|
||||
constexpr int PLEXER_FLOW_OPTIONS_MAX = 12;
|
||||
constexpr int PLEXER_FLOW_ACTIONS_MAX = 12;
|
||||
constexpr int PLEXER_FLOW_VISITS_MAX = 64;
|
||||
constexpr int PLEXER_FLOW_ID_MAX = 64;
|
||||
constexpr int PLEXER_FLOW_TITLE_MAX = 200;
|
||||
constexpr int PLEXER_FLOW_TEXT_MAX = 2000;
|
||||
constexpr int PLEXER_FLOW_LABEL_MAX = 200;
|
||||
constexpr int PLEXER_FLOW_ANSWER_MAX = 2000;
|
||||
constexpr int PLEXER_FLOW_TIMEOUT_DEFAULT = 300;
|
||||
constexpr int PLEXER_FLOW_TIMEOUT_MIN = 5;
|
||||
constexpr int PLEXER_FLOW_TIMEOUT_MAX = 600;
|
||||
constexpr double PLEXER_FLOW_POLL = 0.25;
|
||||
extern const std::vector<std::string> PLEXER_FLOW_TYPES; // choice text info
|
||||
extern const std::vector<std::string> PLEXER_FLOW_KINDS; // message choice text
|
||||
constexpr int MPLEX_FLOW_MAX = 16;
|
||||
constexpr int MPLEX_FLOW_KEEP = 300;
|
||||
|
||||
// plexer_dir(home): <home>/plexer, created 0700.
|
||||
std::string plexer_dir(const std::string& home);
|
||||
|
||||
// Raised by a PlexerController when the backend can't be reached or refuses a command.
|
||||
class PlexerError : public std::runtime_error {
|
||||
public:
|
||||
explicit PlexerError(const std::string& text) : std::runtime_error(text) {}
|
||||
};
|
||||
|
||||
// ---- controller side ---------------------------------------------------------------------
|
||||
|
||||
class PlexerController {
|
||||
public:
|
||||
virtual ~PlexerController() = default;
|
||||
std::string kind = "abstract";
|
||||
virtual Json send_keys(const std::string& text, bool enter = false) = 0;
|
||||
virtual Json read_pane(int lines = 200) = 0;
|
||||
void type_text(const std::string& text, double delay = 0.03, bool enter = true);
|
||||
virtual Json status() = 0;
|
||||
// One blocking question on the user's terminal. screen: {"title","text","kind","options","initial","default"}.
|
||||
// Returns {"status":"ok","value":...} / {"status":"cancelled"} / {"status":"timeout"}; throws PlexerError.
|
||||
virtual Json ask(const Json& screen, double timeout) = 0;
|
||||
};
|
||||
|
||||
class NativePlexerController : public PlexerController {
|
||||
public:
|
||||
NativePlexerController(const std::string& socket_path_in, const std::optional<std::string>& pane_in = std::nullopt);
|
||||
std::string socket_path;
|
||||
std::optional<std::string> pane;
|
||||
Json call(const Json& payload); // Python _call
|
||||
Json send_keys(const std::string& text, bool enter = false) override;
|
||||
Json read_pane(int lines = 200) override;
|
||||
Json status() override;
|
||||
Json ask(const Json& screen, double timeout) override;
|
||||
};
|
||||
|
||||
class TmuxPlexerController : public PlexerController {
|
||||
public:
|
||||
explicit TmuxPlexerController(const std::string& pane_in);
|
||||
std::string pane;
|
||||
std::string tmux(const std::vector<std::string>& args); // Python _tmux
|
||||
Json send_keys(const std::string& text, bool enter = false) override;
|
||||
Json read_pane(int lines = 200) override;
|
||||
Json status() override;
|
||||
Json ask(const Json& screen, double timeout) override;
|
||||
std::string ask_result_path();
|
||||
std::optional<std::string> await_answer(const std::string& path, double budget = 1.0);
|
||||
void ask_dismiss();
|
||||
// Returns the answer file content, nullopt when dismissed, or "timeout" via timed_out.
|
||||
struct RunOutcome {
|
||||
bool timed_out = false;
|
||||
std::optional<std::string> content;
|
||||
};
|
||||
RunOutcome ask_run(const std::vector<std::string>& argv, const std::string& path, double timeout);
|
||||
Json ask_choice(const Json& screen, double timeout);
|
||||
RunOutcome ask_popup(const std::string& title, const std::string& body, const std::string& reader, double timeout);
|
||||
Json ask_text(const Json& screen, double timeout);
|
||||
Json ask_message(const Json& screen, double timeout);
|
||||
};
|
||||
|
||||
class PlexerControllerFactory {
|
||||
public:
|
||||
// Which env var is set decides the backend; null when neither is.
|
||||
static std::unique_ptr<PlexerController> detect();
|
||||
};
|
||||
|
||||
// ---- spec validation ------------------------------------------------------------------------
|
||||
|
||||
// Each returns the normalized value and an error text ("" when fine), like the Python tuples.
|
||||
std::pair<std::string, std::string> validate_flow_id(const Json& raw, const std::string& what);
|
||||
std::pair<std::string, std::string> validate_flow_text(const Json& raw, const std::string& what, size_t limit);
|
||||
std::pair<Json, std::string> validate_flow_options(const Json& raw, const std::string& what);
|
||||
std::pair<Json, std::string> validate_flow_condition(const Json& raw, const std::string& what,
|
||||
const std::vector<std::string>& known);
|
||||
// Returns (normalized app, "") or (Json null, reason).
|
||||
std::pair<Json, std::string> validate_flow_spec(const Json& spec);
|
||||
|
||||
// ---- the flow driver ----------------------------------------------------------------------------
|
||||
|
||||
class PlexerFlow {
|
||||
public:
|
||||
explicit PlexerFlow(const Json& app);
|
||||
Json app;
|
||||
std::map<std::string, Json> by_id;
|
||||
std::vector<std::string> order;
|
||||
Json answers = Json::object();
|
||||
Json action; // None until an action is taken
|
||||
int visits = 0;
|
||||
double deadline = 0.0;
|
||||
|
||||
static bool condition_holds(const Json& clauses, const Json& answers);
|
||||
std::string follow_up(const Json& screen);
|
||||
std::string screen_text(const Json& screen, const Json* field = nullptr);
|
||||
const Json* first_askable(const Json& screen);
|
||||
double remaining() const;
|
||||
Json run(PlexerController& controller);
|
||||
// Returns: null optional to continue with follow_up, a screen id, or a final outcome.
|
||||
struct ScreenResult {
|
||||
enum class Kind { Continue, Jump, Outcome } kind = Kind::Continue;
|
||||
std::string target;
|
||||
Json outcome;
|
||||
};
|
||||
ScreenResult run_screen(PlexerController& controller, const Json& screen);
|
||||
};
|
||||
|
||||
// PlexerBackend (the extension point the shell hook execs into).
|
||||
class PlexerBackend {
|
||||
public:
|
||||
virtual ~PlexerBackend() = default;
|
||||
virtual int run() = 0;
|
||||
};
|
||||
|
||||
} // namespace detz::plexer
|
||||
@@ -0,0 +1,221 @@
|
||||
// Mirror of MplexSpecTests and UiflowDocTests (test_tai.py): ../mplex.md and ../uiflow.md are the
|
||||
// specification, and must list exactly what the C++ tables hold. Tables compared: options,
|
||||
// commands, default key bindings, copy-mode commands, caps, wire kinds, flow caps.
|
||||
// Usage: mplex_spec_test [path-to-repo-root] (default: the directory above taicpp/)
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <regex>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mplex_commands.h"
|
||||
#include "mplex_config.h"
|
||||
#include "mplex_copymode.h"
|
||||
#include "mplex_options.h"
|
||||
#include "mplex_server.h"
|
||||
#include "plexer_flow.h"
|
||||
|
||||
using namespace detz::mplex;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
int checks = 0;
|
||||
|
||||
void check(bool ok, const std::string& what) {
|
||||
++checks;
|
||||
if (!ok) {
|
||||
++failures;
|
||||
std::cerr << "FAIL: " << what << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::string slurp(const std::string& path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
std::stringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// section(heading): text after "\n<heading>\n" up to the next "\n#..### " heading.
|
||||
std::string section(const std::string& doc, const std::string& heading) {
|
||||
size_t start = doc.find("\n" + heading + "\n");
|
||||
if (start == std::string::npos) {
|
||||
check(false, "heading missing: " + heading);
|
||||
return "";
|
||||
}
|
||||
std::string rest = doc.substr(start + heading.size() + 2);
|
||||
static const std::regex next("\n#{1,3} ");
|
||||
std::smatch m;
|
||||
if (std::regex_search(rest, m, next)) return rest.substr(0, static_cast<size_t>(m.position(0)));
|
||||
return rest;
|
||||
}
|
||||
|
||||
std::vector<std::string> first_column(const std::string& text) {
|
||||
std::vector<std::string> out;
|
||||
size_t pos = 0;
|
||||
while (pos < text.size()) {
|
||||
size_t eol = text.find('\n', pos);
|
||||
if (eol == std::string::npos) eol = text.size();
|
||||
std::string line = text.substr(pos, eol - pos);
|
||||
pos = eol + 1;
|
||||
if (line.rfind("| `", 0) != 0) continue;
|
||||
size_t close = line.find("` |", 3);
|
||||
if (close == std::string::npos) continue;
|
||||
out.push_back(line.substr(3, close - 3));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct DocBinding {
|
||||
std::string key, command;
|
||||
bool repeat;
|
||||
};
|
||||
|
||||
std::string unescape_pipes(std::string text) {
|
||||
size_t pos;
|
||||
while ((pos = text.find("\\|")) != std::string::npos) text.replace(pos, 2, "|");
|
||||
return text;
|
||||
}
|
||||
|
||||
// ^\| `(.+?)` \| `(.+?)` \| (yes)? (non-greedy, per line)
|
||||
std::vector<DocBinding> documented_bindings(const std::string& text) {
|
||||
std::vector<DocBinding> out;
|
||||
static const std::regex row("^\\| `(.+?)` \\| `(.+?)` \\| (yes)?");
|
||||
size_t pos = 0;
|
||||
while (pos < text.size()) {
|
||||
size_t eol = text.find('\n', pos);
|
||||
if (eol == std::string::npos) eol = text.size();
|
||||
std::string line = text.substr(pos, eol - pos);
|
||||
pos = eol + 1;
|
||||
std::smatch m;
|
||||
if (std::regex_search(line, m, row))
|
||||
out.push_back({unescape_pipes(m[1].str()), unescape_pipes(m[2].str()), m[3].matched && !m[3].str().empty()});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool contains(const std::string& doc, const std::string& needle) { return doc.find(needle) != std::string::npos; }
|
||||
|
||||
void em_dash_free(const std::string& doc, const std::string& what) {
|
||||
check(!contains(doc, "\xe2\x80\x94"), what + " contains an em dash");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
std::string root = argc > 1 ? argv[1] : "../..";
|
||||
std::string doc = slurp(root + "/mplex.md");
|
||||
std::string flow = slurp(root + "/uiflow.md");
|
||||
check(!doc.empty(), "mplex.md readable under " + root);
|
||||
check(!flow.empty(), "uiflow.md readable under " + root);
|
||||
|
||||
// test_options_table_matches_the_code
|
||||
{
|
||||
std::vector<std::string> documented = first_column(section(doc, "## Options"));
|
||||
std::vector<std::string> code;
|
||||
for (const auto& def : MplexOptions::defs()) code.push_back(def.name);
|
||||
std::sort(documented.begin(), documented.end());
|
||||
std::sort(code.begin(), code.end());
|
||||
check(documented == code, "options table matches the code");
|
||||
}
|
||||
// test_commands_table_matches_the_code
|
||||
{
|
||||
std::vector<std::string> documented = first_column(section(doc, "## Commands"));
|
||||
std::vector<std::string> code;
|
||||
for (const auto& spec : mplex_command_specs()) code.push_back(spec.name);
|
||||
std::sort(documented.begin(), documented.end());
|
||||
std::sort(code.begin(), code.end());
|
||||
check(documented == code, "commands table matches the code");
|
||||
}
|
||||
// test_unsupported_commands_are_all_named
|
||||
for (const auto& name : mplex_unsupported())
|
||||
if (name != "choose-tree-mouse") check(contains(doc, "`" + name + "`"), "unsupported command named: " + name);
|
||||
// test_every_default_binding_is_documented
|
||||
{
|
||||
const std::vector<std::pair<std::string, std::string>> tables = {
|
||||
{"prefix", "### prefix table (after `C-b`)"},
|
||||
{"copy-mode", "### copy-mode table (`mode-keys emacs`)"},
|
||||
{"copy-mode-vi", "### copy-mode-vi table (`mode-keys vi`)"}};
|
||||
for (const auto& [table, heading] : tables) {
|
||||
std::vector<DocBinding> documented = documented_bindings(section(doc, heading));
|
||||
std::vector<DocBinding> expected;
|
||||
for (const auto& key : mplex_default_keys())
|
||||
if (table == key.table) expected.push_back({key.key, key.command, key.repeat});
|
||||
bool same = documented.size() == expected.size();
|
||||
for (size_t i = 0; same && i < documented.size(); ++i)
|
||||
same = documented[i].key == expected[i].key && documented[i].command == expected[i].command &&
|
||||
documented[i].repeat == expected[i].repeat;
|
||||
check(same, "default bindings documented in order: " + table);
|
||||
}
|
||||
}
|
||||
// test_every_copy_mode_command_is_documented
|
||||
for (const auto& name : MplexCopyMode::command_names())
|
||||
check(contains(doc, "`" + name + "`"), "copy-mode command documented: " + name);
|
||||
// test_every_cap_is_documented (+ the values the spec table states)
|
||||
{
|
||||
const std::vector<std::string> caps = {
|
||||
"MPLEX_MAX_SESSIONS", "MPLEX_MAX_WINDOWS", "MPLEX_MAX_PANES", "MPLEX_MAX_CLIENTS", "MPLEX_FRAME_MAX",
|
||||
"MPLEX_CLIENT_OUT_MAX", "MPLEX_PANE_IN_MAX", "MPLEX_HISTORY_CAP", "MPLEX_LOG_MAX", "MPLEX_BUFFERS_CAP",
|
||||
"MPLEX_MESSAGES_MAX", "MPLEX_PROMPT_HISTORY_CAP", "MPLEX_COMMAND_DEPTH_MAX", "MPLEX_LOOP_ERRORS_MAX",
|
||||
"MPLEX_SHELL_TIMEOUT", "MPLEX_CONFIG_MAX", "MPLEX_PASTE_MAX", "MPLEX_RENDER_INTERVAL", "MPLEX_REAP_KILL_AFTER",
|
||||
"MPLEX_FLOW_MAX", "MPLEX_FLOW_KEEP"};
|
||||
for (const auto& name : caps) check(contains(doc, "`" + name + "`"), "cap documented: " + name);
|
||||
check(MPLEX_MAX_SESSIONS == 64 && MPLEX_MAX_WINDOWS == 256 && MPLEX_MAX_PANES == 256 && MPLEX_MAX_CLIENTS == 32,
|
||||
"object caps 64 / 256 / 256 / 32");
|
||||
check(MPLEX_FRAME_MAX == (4L << 20) && MPLEX_CLIENT_OUT_MAX == (4L << 20) && MPLEX_PANE_IN_MAX == (4L << 20),
|
||||
"frame, client out and pane in caps are 4 MiB");
|
||||
check(MPLEX_HISTORY_CAP == 200000, "history cap 200000");
|
||||
check(MPLEX_LOG_MAX == (16L << 20), "log max 16 MiB");
|
||||
check(MPLEX_BUFFERS_CAP == 100, "buffers cap 100");
|
||||
check(MPLEX_MESSAGES_MAX == 200 && MPLEX_PROMPT_HISTORY_CAP == 100, "messages 200, prompt history 100");
|
||||
check(MPLEX_COMMAND_DEPTH_MAX == 16, "command depth 16");
|
||||
check(MPLEX_LOOP_ERRORS_MAX == 50, "loop errors 50");
|
||||
check(MPLEX_SHELL_TIMEOUT == 5.0, "shell timeout 5 s");
|
||||
check(MPLEX_CONFIG_MAX == (1L << 20), "config max 1 MiB");
|
||||
check(MPLEX_PASTE_MAX == (4L << 20), "paste max 4 MiB");
|
||||
check(MPLEX_RENDER_INTERVAL == 0.012, "render interval 12 ms");
|
||||
check(MPLEX_REAP_KILL_AFTER == 2.0, "reap kill after 2 s");
|
||||
check(MPLEX_FLOW_MAX == 16 && MPLEX_FLOW_KEEP == 300, "flow max 16, keep 300 s");
|
||||
}
|
||||
// test_wire_kinds_are_documented
|
||||
for (const char* kind : {"H", "I", "R", "A", "O", "M", "E", "D", "X", "Z"})
|
||||
check(contains(doc, std::string("`") + kind + "`"), std::string("wire kind documented: ") + kind);
|
||||
// test_no_em_dashes
|
||||
em_dash_free(doc, "mplex.md");
|
||||
em_dash_free(flow, "uiflow.md");
|
||||
em_dash_free(slurp(root + "/taicpp/docs/mplex.md"), "taicpp/docs/mplex.md");
|
||||
em_dash_free(slurp(root + "/taicpp/docs/wiring/mplex.md"), "taicpp/docs/wiring/mplex.md");
|
||||
|
||||
// UiflowDocTests.test_every_cap_is_documented (with the values)
|
||||
{
|
||||
const std::vector<std::string> caps = {
|
||||
"PLEXER_FLOW_VERSION", "PLEXER_FLOW_SCREENS_MAX", "PLEXER_FLOW_FIELDS_MAX", "PLEXER_FLOW_OPTIONS_MAX",
|
||||
"PLEXER_FLOW_ACTIONS_MAX", "PLEXER_FLOW_VISITS_MAX", "PLEXER_FLOW_ID_MAX", "PLEXER_FLOW_TITLE_MAX",
|
||||
"PLEXER_FLOW_TEXT_MAX", "PLEXER_FLOW_LABEL_MAX", "PLEXER_FLOW_ANSWER_MAX", "PLEXER_FLOW_TIMEOUT_MIN",
|
||||
"PLEXER_FLOW_TIMEOUT_DEFAULT", "PLEXER_FLOW_TIMEOUT_MAX", "PLEXER_FLOW_POLL", "MPLEX_FLOW_MAX",
|
||||
"MPLEX_FLOW_KEEP"};
|
||||
for (const auto& name : caps) check(contains(flow, "`" + name + "`"), "flow cap documented: " + name);
|
||||
namespace pf = detz::plexer;
|
||||
check(pf::PLEXER_FLOW_VERSION == 1 && pf::PLEXER_FLOW_SCREENS_MAX == 32 && pf::PLEXER_FLOW_FIELDS_MAX == 12 &&
|
||||
pf::PLEXER_FLOW_OPTIONS_MAX == 12 && pf::PLEXER_FLOW_ACTIONS_MAX == 12 && pf::PLEXER_FLOW_VISITS_MAX == 64 &&
|
||||
pf::PLEXER_FLOW_ID_MAX == 64 && pf::PLEXER_FLOW_TITLE_MAX == 200 && pf::PLEXER_FLOW_TEXT_MAX == 2000 &&
|
||||
pf::PLEXER_FLOW_LABEL_MAX == 200 && pf::PLEXER_FLOW_ANSWER_MAX == 2000,
|
||||
"flow caps match uiflow.md table");
|
||||
check(pf::PLEXER_FLOW_TIMEOUT_MIN == 5 && pf::PLEXER_FLOW_TIMEOUT_DEFAULT == 300 && pf::PLEXER_FLOW_TIMEOUT_MAX == 600 &&
|
||||
pf::PLEXER_FLOW_POLL == 0.25,
|
||||
"flow timeouts 5 / 300 / 600 and poll 0.25");
|
||||
}
|
||||
// UiflowDocTests.test_contract_words_are_documented
|
||||
for (const char* word : {"choice", "text", "info", "message", "show_if", "$done", "$cancel", "next", "actions",
|
||||
"cancelled", "timeout"})
|
||||
check(contains(flow, word), std::string("contract word documented: ") + word);
|
||||
|
||||
std::printf("mplex_spec_test: %d checks, %d failures\n", checks, failures);
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user