merge: worktree-agent-ad389a7af758a1e42 (mplex final)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 07:24:18 +01:00
co-authored by Claude Sonnet 5.5
4 changed files with 591 additions and 1 deletions
+9 -1
View File
@@ -39,6 +39,14 @@ if (opts.mplex.has_value()) {
like the Python (`sessions should be nested with care, unset TAI_PLEXER_ACTIVE to force`,
`open terminal failed: not a terminal`, `no server running on <sock>`, ...).
Argument quirk to keep in step with tai.py: argparse's `nargs=REMAINDER` swallows a literal `--`
inside the mplex words (tai.py: `--mplex send-keys -X -- cursor-up` loses the `--`), while
`tests/mplex_cli_main.cpp` passes argv through untouched. The `src/cli` parser must drop/keep `--`
exactly as `argparse.REMAINDER` does (verify against `python3 tai.py --mplex ls -- x`).
The `version` format variable is `detz::kVersion` when the build puts the generated `version.hpp`
on the include path (the Makefile's `-I$(GEN_INC)` does), `1.44.2` otherwise.
## `--plexer-wrap`
tai.py 40129-40133: same `home` computation, `profile = args.profile or "default"`:
@@ -79,6 +87,6 @@ behave as in tai.py.
## Verified
See the report of the porting run: `tests/mplex_flow_test.cpp` (spec/flow corpus against tai.py),
`tests/mplex_e2e.py` (six scenarios run through a pty against both `tai.py --mplex` and
`tests/mplex_e2e.py` (seven scenarios run through a pty against both `tai.py --mplex` and
`build/mplex_cli --mplex`, observations compared), `tests/mplex_tmux_oracle.py` (tmux 3.6 oracle for
the tmux controller).
+411
View File
@@ -0,0 +1,411 @@
"""Differential executor: runs every task of mplex_matrix_gen.py on tai.py --mplex and on the C++
binary, each in its own throwaway TAI_HOME, and compares step outputs and full state dumps.
usage: python3 -I mplex_matrix_exec.py <repo root> <cpp binary> <tasks.json> <out dir> [--jobs N] [--filter TEXT]
Normalized (only these): the TAI_HOME and work directory paths (replaced by <H> and <W>), times of the
forms "Mon Jan 1 12:00:00 2026" and "12:00 01-Jan-26" (tmux default formats), and the minute digits of the
clock mode screen. Everything else must be identical.
"""
import concurrent.futures
import difflib
import fcntl
import importlib.util
import json
import os
import pty
import re
import select
import shutil
import signal
import struct
import subprocess
import sys
import tempfile
import termios
import threading
import time
ROOT, CPP, TASKS, OUT = [os.path.abspath(p) for p in sys.argv[1:5]]
ARGS = sys.argv[5:]
JOBS = int(ARGS[ARGS.index("--jobs") + 1]) if "--jobs" in ARGS else 8
FILTER = ARGS[ARGS.index("--filter") + 1] if "--filter" in ARGS else ""
spec = importlib.util.spec_from_file_location("tai_mod", ROOT + "/tai.py")
tai = importlib.util.module_from_spec(spec)
sys.modules["tai_mod"] = tai
spec.loader.exec_module(tai)
CWD = "/tmp/mxcwd"
os.makedirs(CWD, exist_ok=True)
TIME_RES = [re.compile(r"[A-Z][a-z]{2} [A-Z][a-z]{2} [ \d]\d \d\d:\d\d:\d\d \d{4}"), re.compile(r"\d\d:\d\d \d\d-[A-Z][a-z]{2}-\d\d"), re.compile(r"\d\d:\d\d:\d\d"), re.compile(r"/dev/pts/\d+")]
BASE_CONFIG = '[options]\nautomatic-rename = false\nstatus-right = "R"\ndefault-command = "exec bash --norc --noprofile"\n'
P5_CONFIG = BASE_CONFIG + 'base-index = 1\npane-base-index = 1\nmode-keys = "vi"\nmain-pane-width = 40\nmain-pane-height = 12\n'
PRESETS = {
"P0": [["new-session", "-d", "-s", "main", "-x", "80", "-y", "24"]],
}
P0 = PRESETS["P0"]
P1 = P0 + [["new-session", "-d", "-s", "other", "-x", "80", "-y", "24"], ["new-window", "-t", "other"]]
P2 = P0 + [["new-window"], ["split-window", "-h"], ["new-window"], ["split-window", "-v"], ["split-window", "-h"], ["split-window", "-v"],
["split-window", "-h"], ["select-layout", "tiled"], ["select-window", "-t", "main:0"]]
P3 = P2 + [["select-window", "-t", "main:2"], ["resize-pane", "-Z"], ["select-pane", "-t", "main:1.0"], ["select-pane", "-m"],
["set-option", "-g", "status-position", "top"]]
P4 = P2 + [["set-buffer", "-b", "b1", "hello"], ["set-buffer", "world"], ["set-option", "-g", "status-position", "top"],
["select-window", "-t", "main:1"], ["send-keys", "-t", "main:1.0", "seq 1 100", "Enter"]]
P5 = P0 + [["new-window"], ["split-window", "-h"], ["new-window"], ["split-window", "-v"], ["split-window", "-h"], ["select-layout", "main-vertical"],
["select-window", "-t", "main:1"], ["select-layout", "even-vertical"], ["set-buffer", "-b", "b1", "hello"],
["send-keys", "-t", "main:1.1", "seq 1 100", "Enter"]]
PRESET_STEPS = {"P0": P0, "P1": P1, "P2": P2, "P3": P3, "P4": P4, "P5": P5, "P4c": P4 + [["@sleep", "0.8"], ["copy-mode"]], "P5c": P5 + [["@sleep", "0.8"], ["copy-mode"]]}
PRESET_CONFIG = {"P5": P5_CONFIG, "P5c": P5_CONFIG}
LIST_FORMAT = ("#{session_name}:#{window_index}.#{pane_index} #{pane_id} #{window_id} #{pane_left},#{pane_top} #{pane_width}x#{pane_height} "
"a=#{pane_active} l=#{pane_last} m=#{pane_marked} z=#{window_zoomed_flag} mode=#{pane_mode} hist=#{history_size} dead=#{pane_dead} "
"sp=#{scroll_position} cc=#{copy_cursor_x},#{copy_cursor_y} sel=#{selection_present} s=#{pane_search_string} "
"title=#{pane_title} syn=#{pane_synchronized} cmd=#{pane_current_command}")
WIN_FORMAT = ("#{session_name}:#{window_index} #{window_id} #{window_name} #{window_flags} #{window_panes} #{window_width}x#{window_height} "
"act=#{window_active} last=#{window_last_flag} z=#{window_zoomed_flag} mk=#{window_marked_flag}")
SES_FORMAT = "#{session_name} #{session_id} w=#{session_windows} att=#{session_attached}"
MARK = ["display-message", "-p", "@@ %s"]
class Impl:
def __init__(self, name, argv, config):
self.name = name
self.argv = argv
self.tmp = tempfile.mkdtemp(prefix="mx", dir="/tmp")
self.home = os.path.join(self.tmp, "h")
self.work = os.path.join(self.tmp, "w")
os.makedirs(os.path.join(self.home))
os.makedirs(self.work)
with open(os.path.join(self.home, "mplex.toml"), "w") as handle:
handle.write(config)
with open(os.path.join(self.work, "src.conf"), "w") as handle:
handle.write("set -g status-left X\nbogus-command\nset -g status-right Y\n")
with open(os.path.join(self.work, "buf.txt"), "w") as handle:
handle.write("from file\n")
self.env = dict(os.environ)
for name_ in ("TAI_PLEXER_ACTIVE", "TAI_PLEXER_SOCKET", "TAI_PLEXER_PANE", "TMUX", "VISUAL", "EDITOR"):
self.env.pop(name_, None)
self.env.update({"TAI_HOME": self.home, "HOME": self.work, "SHELL": "/bin/bash", "TERM": "xterm-256color", "TAI_PASSPHRASE": "",
"LANG": "C.UTF-8", "PS1": "$ "})
def norm(self, text):
text = text.replace(self.home, "<H>").replace(self.work, "<W>").replace(self.tmp, "<T>")
for regex in TIME_RES:
text = regex.sub("<TIME>", text)
return text
def sub(self, words):
return [w.replace("{W}", self.work) for w in words]
def cli(self, *commands):
"""commands: lists of words; chained with ';' like the command line does."""
words = []
for index, command in enumerate(commands):
if index:
words.append(";")
words.extend(self.sub(command))
try:
done = subprocess.run(self.argv + words, env=self.env, cwd=CWD, stdin=subprocess.DEVNULL, capture_output=True, text=True,
errors="replace", timeout=30)
except subprocess.TimeoutExpired:
return 124, "TIMEOUT"
return done.returncode, self.norm(done.stdout + "||" + done.stderr)
STATE_COMMANDS = [["list-sessions", "-F", SES_FORMAT], MARK[:2] + ["@@ 1"], ["list-windows", "-a", "-F", WIN_FORMAT], MARK[:2] + ["@@ 2"],
["list-panes", "-a", "-F", LIST_FORMAT], MARK[:2] + ["@@ 3"], ["show-options", "-g"], MARK[:2] + ["@@ 4"], ["show-options"],
MARK[:2] + ["@@ 5"], ["show-options", "-w"], MARK[:2] + ["@@ 6"], ["list-buffers"], MARK[:2] + ["@@ 7"], ["show-messages"],
MARK[:2] + ["@@ 8"], ["list-keys", "-T", "prefix"], MARK[:2] + ["@@ 9"], ["list-keys", "-T", "root"],
MARK[:2] + ["@@ 10"], ["list-clients", "-F", "#{client_session} #{client_width}x#{client_height} #{client_key_table}"]]
def dump(self):
"""Poll until every pane shows a prompt, then return captures plus the full state listing (two calls)."""
out = ""
for _ in range(40):
rc, ids = self.cli(["list-panes", "-a", "-F", "#{pane_id}"])
if rc != 0:
return "NOSERVER " + ids
panes = ids.split("||")[0].split()
commands = []
for pane in panes:
commands.append(["capture-pane", "-p", "-t", pane])
commands.append(["display-message", "-p", "@@ end"])
commands.append(["display-message", "-p", "@@ state"])
rc, out = self.cli(*(commands + self.STATE_COMMANDS))
head = out.split("@@ state")[0]
parts = head.split("@@ end")
if not panes or all(("$" in part or "bash" in part) for part in parts[:-1]):
break
time.sleep(0.1)
return out
def build(self, preset):
steps = PRESET_STEPS[preset]
for step in steps:
if step[0] == "@sleep":
time.sleep(float(step[1]))
continue
rc, out = self.cli(step)
if rc != 0:
return "BUILD FAIL %d %s %s" % (rc, step, out)
if preset in ("P4", "P4c", "P5", "P5c"):
time.sleep(0.4)
return ""
def kill(self):
try:
self.cli(["kill-server"])
except Exception:
pass
time.sleep(0.1)
shutil.rmtree(self.tmp, ignore_errors=True)
def delta(a, b):
if isinstance(a, tuple) or isinstance(b, tuple):
a, b = repr(a), repr(b)
return "\n".join(list(difflib.unified_diff(str(a).split("\n"), str(b).split("\n"), "py", "cpp", lineterm="", n=1))[:60])
def make_impls(preset):
config = PRESET_CONFIG.get(preset, BASE_CONFIG)
py = Impl("py", [sys.executable, "-I", ROOT + "/tai.py", "--mplex"], config)
cpp = Impl("cpp", [CPP, "--mplex"], config)
return py, cpp
def both(fn_py, fn_cpp):
results = [None, None]
def run(index, fn):
try:
results[index] = fn()
except Exception as exc: # noqa: BLE001
results[index] = "EXC %r" % (exc,)
threads = [threading.Thread(target=run, args=(0, fn_py)), threading.Thread(target=run, args=(1, fn_cpp))]
for t in threads:
t.start()
for t in threads:
t.join()
return results
def run_cmd_task(task):
py, cpp = make_impls(task["preset"])
log = []
try:
fails = both(lambda: py.build(task["preset"]), lambda: cpp.build(task["preset"]))
if fails[0] != fails[1] or fails[0]:
log.append({"step": "build", "diff": delta(fails[0], fails[1])})
return log
# state before any step
a, b = both(py.dump, cpp.dump)
if a != b:
log.append({"step": "initial dump", "diff": delta(a, b)})
return log
for step in task["steps"]:
ra, rb = both(lambda: py.cli(step), lambda: cpp.cli(step))
if ra != rb:
log.append({"step": step, "kind": "output", "diff": delta(ra, rb)})
a, b = both(py.dump, cpp.dump)
if a != b:
log.append({"step": step, "kind": "state", "diff": delta(a, b)})
# a diverged state would cascade: stop this chunk
break
finally:
both(py.kill, cpp.kill)
return log
class Client:
def __init__(self, impl, words):
self.impl = impl
self.screen = tai.TuiVtScreen(24, 80)
pid, fd = pty.fork()
if pid == 0:
os.chdir(CWD)
os.execvpe(impl.argv[0], impl.argv + words, impl.env)
self.pid, self.fd = pid, fd
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 24, 80, 0, 0))
self.settle(1.0)
def settle(self, quiet=0.3, limit=3.0):
end, last = time.time() + limit, time.time()
while time.time() < end and time.time() - last < quiet:
r, _, _ = select.select([self.fd], [], [], 0.05)
if r:
try:
data = os.read(self.fd, 65536)
except OSError:
return
if not data:
return
self.screen.feed(data)
last = time.time()
def send(self, data):
try:
os.write(self.fd, data)
except OSError:
pass
self.settle()
def text(self):
text = self.screen.text()
text = re.sub(r"/dev/pts/\d+", "/dev/pts/N", text).replace("^[", "")
text = "\n".join(line.rstrip() for line in text.split("\n")).rstrip("\n")
return re.sub(r"\d", "#", text) if "clock" in text.lower() else text
def close(self):
try:
os.kill(self.pid, signal.SIGKILL)
except OSError:
pass
try:
os.waitpid(self.pid, 0)
except OSError:
pass
try:
os.close(self.fd)
except OSError:
pass
def key_bytes(name):
return tai.MplexKeys.to_bytes(name)
def run_key_task(task):
py, cpp = make_impls(task["preset"])
log = []
clients = []
try:
fails = both(lambda: py.build(task["preset"]), lambda: cpp.build(task["preset"]))
if fails[0] != fails[1] or fails[0]:
return [{"step": "build", "diff": delta(fails[0], fails[1])}]
for impl in (py, cpp):
clients.append(Client(impl, ["attach-session", "-t", "main"]))
# copy-mode tables are entered by the preset (P4c/P5c); drop the attach screen noise first
data = key_bytes(task["key"])
if task["table"] == "prefix":
data = b"\x02" + data
for client in clients:
client.send(data)
time.sleep(0.8)
for client in clients:
client.settle(0.2)
screens = [c.text() for c in clients]
a, b = both(py.dump, cpp.dump)
if screens[0] != screens[1]:
log.append({"step": task["key"], "kind": "screen", "diff": delta(screens[0], screens[1])})
if a != b:
log.append({"step": task["key"], "kind": "state", "diff": delta(a, b)})
# second key: dismiss anything open, then compare again
for client in clients:
client.send(b"\x1b")
time.sleep(0.8)
for client in clients:
client.settle(0.2)
screens = [c.text() for c in clients]
a, b = both(py.dump, cpp.dump)
if screens[0] != screens[1]:
log.append({"step": task["key"] + " +Esc", "kind": "screen", "diff": delta(screens[0], screens[1])})
if a != b:
log.append({"step": task["key"] + " +Esc", "kind": "state", "diff": delta(a, b)})
finally:
for client in clients:
client.close()
both(py.kill, cpp.kill)
return log
CUSTOM_CONFIG = ('[options]\nautomatic-rename = false\nstatus-right = "R"\ndefault-command = "exec bash --norc --noprofile"\n'
'prefix2 = "F12"\nescape-time = 50\nrepeat-time = 2500\n[keys.root]\n"M-x" = "display-message root-hit"\n"F7" = "split-window -h"\n'
'[keys.prefix]\nr = { command = "select-pane -R", repeat = true }\nl = { command = "select-pane -L", repeat = true }\n"C-b" = false\n'
'q = "command-prompt -p x { rename-window \\"%%\\" }"\n[keys.mine]\na = "rename-window mine-a"\n')
def run_custom_task(task):
py, cpp = make_impls("P2")
for impl in (py, cpp):
with open(os.path.join(impl.home, "mplex.toml"), "w") as handle:
handle.write(CUSTOM_CONFIG)
log = []
clients = []
try:
fails = both(lambda: py.build("P2"), lambda: cpp.build("P2"))
if fails[0] != fails[1] or fails[0]:
return [{"step": "build", "diff": delta(fails[0], fails[1])}]
for impl in (py, cpp):
clients.append(Client(impl, ["attach-session", "-t", "main"]))
script = [b"\x1bx", b"\x1b[18~", b"\x1b[18~", b"\x02r", b"r", b"r", b"l", b"\x02", b"\x1b", b"\x1b[18~l", b"\x02q", b"hello\r", b"\x02" + b"b",
b"\x1b[18~", b"\x1b[18~" + b"x", b"\x1b[18~" + b"c", b"\x02l", b"x", b"\x1b[18~[", b"\x02[", b"q", b"\x1bOP", b"\x1b[A"]
for number, data in enumerate(script):
for client in clients:
client.send(data)
if number in (4, 9, 16):
time.sleep(3.0)
for client in clients:
client.settle(0.2)
screens = [c.text() for c in clients]
a, b = both(py.dump, cpp.dump)
if screens[0] != screens[1]:
log.append({"step": "script %d %r" % (number, data), "kind": "screen", "diff": delta(screens[0], screens[1])})
if a != b:
log.append({"step": "script %d %r" % (number, data), "kind": "state", "diff": delta(a, b)})
# custom table through switch-client -T
for impl in (py, cpp):
impl.cli(["switch-client", "-T", "mine"])
for client in clients:
client.send(b"a")
a, b = both(py.dump, cpp.dump)
if a != b:
log.append({"step": "table mine", "kind": "state", "diff": delta(a, b)})
finally:
for client in clients:
client.close()
both(py.kill, cpp.kill)
return log
def run_task(task):
try:
if task["kind"] == "cmd":
return run_cmd_task(task)
if task["kind"] == "key":
return run_key_task(task)
return run_custom_task(task)
except Exception as exc: # noqa: BLE001
return [{"step": "harness", "error": repr(exc)}]
def main():
tasks = json.load(open(TASKS))
if FILTER:
tasks = [t for t in tasks if any(f in t["id"] for f in FILTER.split(","))]
os.makedirs(OUT, exist_ok=True)
started = time.time()
done = mismatched = 0
steps = sum(len(t.get("steps", [])) or 1 for t in tasks)
report = open(os.path.join(OUT, "mismatches.jsonl"), "w")
with concurrent.futures.ThreadPoolExecutor(JOBS) as pool:
futures = {pool.submit(run_task, t): t for t in tasks}
for future in concurrent.futures.as_completed(futures):
task = futures[future]
log = future.result()
done += 1
if log:
mismatched += 1
report.write(json.dumps({"task": task["id"], "log": log}) + "\n")
report.flush()
if done % 25 == 0 or log:
print("[%d/%d] %s %s (%.0fs)" % (done, len(tasks), task["id"], "MISMATCH" if log else "ok", time.time() - started), flush=True)
print("tasks %d, steps %d, mismatching tasks %d, %.0fs" % (len(tasks), steps, mismatched, time.time() - started))
return 1 if mismatched else 0
if __name__ == "__main__":
sys.exit(main())
+162
View File
@@ -0,0 +1,162 @@
"""Scenario generator for the mplex differential matrix (taicpp/tests/mplex_matrix_exec.py).
usage: python3 -I mplex_matrix_gen.py <repo root> <out.json>
Command tasks: for every command in MPLEX_COMMAND_SPECS and every flag letter of its getopt spec,
steps are generated from value pools, grouped in chunks of CHUNK steps; each chunk runs from a
freshly built preset on both implementations. Key tasks: every default binding as real key bytes.
Placeholders {W} (work dir) are substituted by the executor.
"""
import importlib.util
import json
import sys
root, out = sys.argv[1], sys.argv[2]
spec = importlib.util.spec_from_file_location("tai_mod", root + "/tai.py")
tai = importlib.util.module_from_spec(spec)
sys.modules["tai_mod"] = tai
spec.loader.exec_module(tai)
CHUNK = 8
TARGETS = ["main", "main:1", "main:1.1", "%1", "@1", "$0", "+", "-", "!", "{last}", "{next}", ":.+", ":=0", "{top-left}",
"other", "bogus", "main:2", "main:2.{bottom-right}", ":", ":.", "=main", "main:", ":^", ":$", "{end}"]
POOLS = {
"t": TARGETS, "s": ["main", "other", "new1", "%1", "1", "main:1", "x"],
"b": ["b1", "buf", "nosuch"], "n": ["nm", "1 2"], "F": ["#{pane_id}", "#{session_name}:#{window_index}", "x#{bogus}y",
"#{?pane_active,A,B}", "#{window_flags}#{pane_index}"],
"f": ["1", "0", "#{pane_active}", "#{==:#{pane_index},1}"], "c": ["/tmp", "/nonexistent"],
"x": ["40", "10", "0", "abc", "200"], "y": ["10", "5", "0", "abc", "100"], "l": ["5", "50%", "1", "abc", "100%"],
"e": ["A=1", "bad"], "E": ["echo"], "N": ["2", "x"], "P": ["p"], "p": ["Q"], "I": ["init"], "T": ["prefix", "root", "copy-mode", "copy-mode-vi", "custom", "ttl"],
"d": ["1", "x"], "A": ["%1:on", "x"], "B": ["n:w:#{x}"], "C": ["80x24", "x"], "r": ["x"], "S": ["1"], "K": ["Up"], "O": ["name"], "k": ["x"],
}
POSITIONAL = {
"bind-key": [["F5", "display-message hi"], ["a", "bogus-cmd"], ["C-x", "split-window ; select-pane -L"], ["Zzz", "x"]],
"unbind-key": [[], ["F5"], ["C-b"], ["zz"]],
"set-option": [["status-position", "top"], ["mouse", "on"], ["base-index", "1"], ["history-limit", "5"], ["status", "off"],
["nonexistent", "1"], ["status-left", "[x]"], ["mode-keys", "vi"], ["status-style", "bg=red"], ["status", ""],
["prefix", "C-a"], ["synchronize-panes", "on"], ["remain-on-exit", "on"]],
"set-window-option": [["synchronize-panes", "on"], ["mode-keys", "vi"], ["x", "1"]],
"show-options": [[], ["status"], ["bogus"], ["prefix"]],
"show-window-options": [[], ["mode-keys"], ["bogus"]],
"send-keys": [["abc"], ["Enter"], ["C-c", "Up"], ["Space", "a"], ["bogusKey"], ["cancel"], ["cursor-up"], ["search-forward-text", "1"]],
"source-file": [["/nonexistent"], ["{W}/src.conf"]],
"new-window": [[], ["cat"], ["echo", "hi"]], "split-window": [[], ["cat"]], "new-session": [[], ["cat"]],
"run-shell": [["echo hi"], ["false"], ["echo #{pane_id}"], []], "if-shell": [["true", "display-message yes", "display-message no"], ["false", "display-message yes"],
["false", "display-message yes", "display-message no"]],
"command-prompt": [[], ["rename-window %1"]], "confirm-before": [["kill-pane"], ["bogus"]],
"find-window": [["bash"], ["zzz"], ["."]], "rename-session": [["newname"], ["a b"]], "rename-window": [["w new"], ["x"]],
"load-buffer": [["{W}/buf.txt"], ["/nonexistent"]], "save-buffer": [["{W}/out.txt"]],
"set-buffer": [["data"], [], ["a b c"]], "display-message": [[], ["hi #{pane_id}"], ["#{bogus}"]],
"display-panes": [[], ["display-message x"]], "resize-pane": [[], ["3"], ["x"]], "select-layout": [[], ["tiled"], ["even-vertical"], ["main-horizontal"], ["bogus"],
["main-vertical-mirrored"]],
"choose-tree": [[], ["x"]], "choose-buffer": [[], ["x"]], "choose-client": [[], ["x"]],
"list-keys": [[], ["F5"], ["C-b"]], "refresh-client": [[], ["x"]], "copy-mode": [[]], "attach-session": [[]],
"kill-session": [[]], "capture-pane": [[]],
}
SKIP_FLAG_VALUES_FOR = {"kill-server"}
LAST = {"kill-server"}
def cstr(name):
return name
def positional_sets(name, low, high):
if name in POSITIONAL:
return POSITIONAL[name]
if low == 0:
return [[]]
return [["x"] * low, ["1"] * low]
def flag_letters(spec_text):
letters, i = [], 0
while i < len(spec_text):
takes = i + 1 < len(spec_text) and spec_text[i + 1] == ":"
letters.append((spec_text[i], takes))
i += 2 if takes else 1
return letters
def steps_for(name, aliases, spec_text, low, high, reduced):
steps = []
pos = positional_sets(name, low, high)
if reduced:
pos = pos[:2]
for p in pos:
steps.append([name] + p)
for letter, takes in flag_letters(spec_text):
values = POOLS.get(letter, ["1", "x"]) if takes else [None]
if name in ("command-prompt",) and letter == "T":
values = ["command", "search", "target"]
if reduced:
values = values[:2]
for value in values:
flag = ["-" + letter] + ([value] if value is not None else [])
for p in pos[:3 if not reduced else 1]:
steps.append([name] + flag + p)
if not reduced:
# a few flag pairs
letters = [l for l, t in flag_letters(spec_text) if not t][:4]
for i in range(len(letters)):
for j in range(i + 1, len(letters)):
steps.append([name, "-" + letters[i], "-" + letters[j]] + pos[0])
if aliases:
steps.append([aliases[0]] + pos[0])
return steps
COPY_CMDS = ["cancel", "cursor-up", "cursor-down", "cursor-down-and-cancel", "cursor-left", "cursor-right", "start-of-line", "end-of-line",
"back-to-indentation", "top-line", "middle-line", "bottom-line", "history-top", "history-bottom", "page-up", "page-down",
"page-down-and-cancel", "halfpage-up", "halfpage-down", "halfpage-down-and-cancel", "scroll-up", "scroll-down", "scroll-down-and-cancel",
"scroll-middle", "scroll-top", "scroll-bottom", "cursor-centre-vertical", "cursor-centre-horizontal", "goto-line", "refresh-from-pane",
"toggle-position", "next-word", "next-word-end", "next-space", "next-space-end", "previous-word", "previous-space", "next-paragraph",
"previous-paragraph", "next-matching-bracket", "previous-matching-bracket", "jump-forward", "jump-backward", "jump-to-forward",
"jump-to-backward", "jump-again", "jump-reverse", "begin-selection", "stop-selection", "clear-selection", "select-line", "select-word",
"rectangle-toggle", "rectangle-on", "rectangle-off", "other-end", "set-mark", "jump-to-mark", "copy-selection", "copy-selection-and-cancel",
"copy-pipe", "copy-pipe-and-cancel", "append-selection", "append-selection-and-cancel", "copy-end-of-line", "copy-end-of-line-and-cancel",
"copy-pipe-end-of-line", "copy-pipe-end-of-line-and-cancel", "copy-line", "copy-line-and-cancel", "copy-pipe-line", "copy-pipe-line-and-cancel",
"search-forward", "search-backward", "search-forward-text", "search-backward-text", "search-forward-incremental",
"search-backward-incremental", "search-again", "search-reverse"]
ARGS = {"goto-line": ["5", "x"], "jump-forward": ["3"], "jump-backward": ["1"], "jump-to-forward": ["9"], "jump-to-backward": ["2"],
"search-forward": ["1"], "search-backward": ["9"], "search-forward-text": ["1"], "search-backward-text": ["5"],
"search-forward-incremental": ["=7", "+8"], "search-backward-incremental": ["=3"], "copy-pipe": ["cat >/dev/null"]}
def main():
tasks = []
presets = ["P0", "P1", "P2", "P3", "P4", "P5"]
for name, (aliases, spec_text, low, high) in tai.MPLEX_COMMAND_SPECS.items():
for preset in presets:
reduced = preset != "P3"
steps = steps_for(name, list(aliases), spec_text, low, high, reduced)
# drop steps that would hang a CLI or are pure noise
if name == "kill-server":
steps = [[name]]
chunks = [steps[i:i + CHUNK] for i in range(0, len(steps), CHUNK)] or [[]]
if reduced:
chunks = chunks[:1]
for number, chunk in enumerate(chunks):
if name == "kill-server" and preset != "P2":
continue
tasks.append({"id": "cmd:%s:%s:%d" % (name, preset, number), "kind": "cmd", "preset": preset, "steps": chunk})
# copy mode commands from the copy-mode presets (emacs P4, vi P5), eight per fresh chunk
for preset in ("P4", "P5"):
steps = []
for cmd in COPY_CMDS:
steps.append(["send-keys", "-X", cmd] + ARGS.get(cmd, []))
for number in range(0, len(steps), CHUNK):
tasks.append({"id": "copy:%s:%d" % (preset, number // CHUNK), "kind": "cmd", "preset": preset + "c", "steps": steps[number:number + CHUNK]})
# key bindings
for table, preset in (("prefix", "P2"), ("prefix", "P3"), ("copy-mode", "P4c"), ("copy-mode-vi", "P5c")):
for key_table, key, command, repeat, note in tai.MPLEX_DEFAULT_KEYS:
if key_table != table:
continue
tasks.append({"id": "key:%s:%s:%s" % (table, preset, key), "kind": "key", "preset": preset, "table": table, "key": key})
# extra key scenarios: root table rebinds, repeat, prefix2, escape-time
tasks.append({"id": "key:custom", "kind": "custom"})
json.dump(tasks, open(out, "w"))
print(len(tasks), "tasks", sum(len(t.get("steps", [])) for t in tasks), "steps")
main()
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# Differential matrix: every mplex command/flag/target and every default key binding on tai.py and on
# the C++ port, from several preset states. Writes only under taicpp/build/.
# usage (from taicpp/): sh tests/mplex_matrix_run.sh [filter] [jobs]
set -e
cd "$(dirname "$0")/.."
make -f build/int/Makefile -j8 build/int/mplex_cli
python3 -I tests/mplex_matrix_gen.py .. build/matrix/tasks.json
python3 -I tests/mplex_matrix_exec.py .. build/int/mplex_cli build/matrix/tasks.json build/matrix/run --jobs "${2:-12}" ${1:+--filter "$1"}