mplex: port args, command parser, style, options, config, format, layout with differential tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 01:31:27 +02:00
co-authored by Claude Sonnet 5.5
parent 796b0b6ef7
commit fdbfd4cb52
8 changed files with 2728 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
// MplexArgs and MplexCommandParser: transcription of tai.py:10472-10650.
#include "mplex_args.h"
#include <climits>
#include <set>
#include "mplex_py.h"
namespace detz::mplex {
std::optional<std::string> MplexArgs::get(char flag) const {
auto found = flags.find(flag);
if (found == flags.end() || found->second.empty()) return std::nullopt;
const FlagValue& last = found->second.back();
return last.is_true ? std::string("True") : last.text;
}
std::string MplexArgs::get(char flag, const std::string& fallback) const {
auto value = get(flag);
return value ? *value : fallback;
}
std::vector<std::string> MplexArgs::all(char flag) const {
std::vector<std::string> out;
auto found = flags.find(flag);
if (found == flags.end()) return out;
for (const auto& item : found->second) out.push_back(item.is_true ? std::string("True") : item.text);
return out;
}
std::optional<long long> MplexArgs::int_flag(char flag, std::optional<long long> fallback) const {
auto found = flags.find(flag);
if (found == flags.end() || found->second.empty()) return fallback;
const FlagValue& last = found->second.back();
if (last.is_true) return fallback;
try {
return py::to_int(last.text);
} catch (const ValueError&) {
// A valid Python int beyond 64 bits (arbitrary precision there): saturate, every consumer
// clamps it. Anything else is not a number.
if (py::is_int_text(last.text)) return py::strip(last.text)[0] == '-' ? LLONG_MIN : LLONG_MAX;
throw MplexError(std::string("-") + flag + " expects a number: " + last.text);
}
}
MplexArgs MplexArgs::parse(const std::string& name, const std::string& spec, const std::vector<std::string>& words,
int min_args, int max_args) {
std::set<char> takes;
std::set<char> known;
size_t index = 0;
while (index < spec.size()) {
known.insert(spec[index]);
if (index + 1 < spec.size() && spec[index + 1] == ':') {
takes.insert(spec[index]);
++index;
}
++index;
}
std::map<char, std::vector<FlagValue>> flags;
std::vector<std::string> positional;
size_t pos = 0;
while (pos < words.size()) {
const std::string& raw = words[pos];
if (raw == "--") {
for (size_t k = pos + 1; k < words.size(); ++k) positional.push_back(words[k]);
break;
}
UStr word = to_ustr(raw);
if (word.size() < 2 || word[0] != L'-') {
for (size_t k = pos; k < words.size(); ++k) positional.push_back(words[k]);
break;
}
size_t inner = 1;
while (inner < word.size()) {
wchar_t letter = word[inner];
char key = (letter > 0 && letter < 128) ? static_cast<char>(letter) : '\0';
if (key == '\0' || known.count(key) == 0)
throw MplexError("command " + name + ": unknown flag -" + to_utf8(letter));
if (takes.count(key)) {
std::string value;
if (inner + 1 < word.size()) {
value = to_utf8(word.substr(inner + 1));
} else {
++pos;
if (pos >= words.size()) throw MplexError("command " + name + ": -" + to_utf8(letter) + " expects an argument");
value = words[pos];
}
FlagValue fv;
fv.is_true = false;
fv.text = value;
flags[key].push_back(fv);
break;
}
flags[key].push_back(FlagValue{true, ""});
++inner;
}
++pos;
}
if (static_cast<int>(positional.size()) < min_args || (max_args >= 0 && static_cast<int>(positional.size()) > max_args))
throw MplexError("command " + name + ": wrong number of arguments");
return MplexArgs(std::move(flags), std::move(positional));
}
std::vector<std::vector<std::string>> MplexCommandParser::parse(const std::string& text) {
// Every delimiter is ASCII, and UTF-8 continuation bytes never equal an ASCII byte, so a
// byte scan sees exactly the structure the Python code point scan sees.
std::vector<std::vector<std::string>> commands;
std::vector<std::string> words;
std::string cur;
bool has = false;
size_t i = 0;
const size_t n = text.size();
auto end_word = [&]() {
if (has) words.push_back(cur);
cur.clear();
has = false;
};
auto end_command = [&]() {
end_word();
if (!words.empty()) commands.push_back(words);
words.clear();
};
while (i < n) {
char c = text[i];
if (c == ' ' || c == '\t' || c == '\r') {
end_word();
++i;
} else if (c == '\n' || c == ';') {
end_command();
++i;
} else if (c == '\\') {
if (i + 1 < n) {
if (text[i + 1] == '\n') {
i += 2;
continue;
}
cur.push_back(text[i + 1]);
has = true;
}
i += 2;
} else if (c == '\'') {
size_t j = text.find('\'', i + 1);
if (j == std::string::npos) throw MplexError("unterminated single quote");
cur.append(text, i + 1, j - (i + 1));
has = true;
i = j + 1;
} else if (c == '"') {
size_t j = i + 1;
std::string piece;
while (j < n && text[j] != '"') {
if (text[j] == '\\' && j + 1 < n) {
char nxt = text[j + 1];
switch (nxt) {
case 'n': piece.push_back('\n'); break;
case 't': piece.push_back('\t'); break;
case 'e': piece.push_back('\x1b'); break;
case 'r': piece.push_back('\r'); break;
default: piece.push_back(nxt); break;
}
j += 2;
} else {
piece.push_back(text[j]);
++j;
}
}
if (j >= n) throw MplexError("unterminated double quote");
cur += piece;
has = true;
i = j + 1;
} else if (c == '{' && !has) {
int depth = 0;
size_t j = i;
char quote = '\0';
while (j < n) {
char ch = text[j];
if (quote) {
if (ch == '\\' && quote == '"') {
++j;
} else if (ch == quote) {
quote = '\0';
}
} else if (ch == '\'' || ch == '"') {
quote = ch;
} else if (ch == '\\') {
++j;
} else if (ch == '{') {
++depth;
} else if (ch == '}') {
--depth;
if (depth == 0) break;
}
++j;
}
if (depth != 0 || j >= n) throw MplexError("unterminated { block");
cur += py::strip(text.substr(i + 1, j - (i + 1)));
has = true;
i = j + 1;
} else {
cur.push_back(c);
has = true;
++i;
}
}
end_command();
return commands;
}
std::string MplexCommandParser::quote(const std::string& word) {
UStr w = to_ustr(word);
bool plain = !w.empty();
for (wchar_t ch : w) {
if (!(py::isalnum(ch) || (ch < 128 && std::string("-_./:=%@+,").find(static_cast<char>(ch)) != std::string::npos))) {
plain = false;
break;
}
}
if (plain) return word;
std::string out = "\"";
for (char ch : word) {
if (ch == '\\') out += "\\\\";
else if (ch == '"') out += "\\\"";
else if (ch == '$') out += "\\$";
else out.push_back(ch);
}
out.push_back('"');
return out;
}
} // namespace detz::mplex
+301
View File
@@ -0,0 +1,301 @@
// MplexBinding / MplexConfig: transcription of tai.py:10850-10985.
#include "mplex_config.h"
#include <sys/stat.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iterator>
#include "mplex_args.h"
#include "mplex_commands.h"
#include "mplex_keys.h"
#include "mplex_py.h"
namespace detz::mplex {
namespace {
constexpr long CONFIG_MAX = 1L << 20; // MPLEX_CONFIG_MAX
const char* const CONFIG_NAME = "mplex.toml"; // MPLEX_CONFIG_NAME
std::string os_error_text(int err, const std::string& path) {
return "[Errno " + std::to_string(err) + "] " + std::strerror(err) + ": " + py::repr(path);
}
// Python's repr() of a parsed TOML value, for the places where the original formats a value with str().
std::string py_repr(const TomlValue& v);
std::string py_str(const TomlValue& v) {
switch (v.kind) {
case TomlValue::Kind::String: return v.text;
default: return py_repr(v);
}
}
std::string py_repr(const TomlValue& v) {
switch (v.kind) {
case TomlValue::Kind::Bool: return v.boolean ? "True" : "False";
case TomlValue::Kind::Int: return std::to_string(v.integer);
case TomlValue::Kind::Float: return py::format_float(v.number);
case TomlValue::Kind::String: return py::repr(v.text);
case TomlValue::Kind::Datetime: return v.text;
case TomlValue::Kind::Array: {
std::string out = "[";
for (size_t i = 0; i < v.array.size(); ++i) {
if (i) out += ", ";
out += py_repr(v.array[i]);
}
return out + "]";
}
case TomlValue::Kind::Table: {
std::string out = "{";
bool first = true;
for (const auto& item : v.table) {
if (!first) out += ", ";
first = false;
out += py::repr(item.first) + ": " + py_repr(item.second);
}
return out + "}";
}
}
return "";
}
bool truthy(const TomlValue& v) {
switch (v.kind) {
case TomlValue::Kind::Bool: return v.boolean;
case TomlValue::Kind::Int: return v.integer != 0;
case TomlValue::Kind::Float: return v.number != 0.0;
case TomlValue::Kind::String: return !v.text.empty();
case TomlValue::Kind::Array: return !v.array.empty();
case TomlValue::Kind::Table: return !v.table.empty();
case TomlValue::Kind::Datetime: return true;
}
return true;
}
// bytes.decode("utf-8") failure text as CPython's UnicodeDecodeError.__str__ prints it; "" when valid.
std::string utf8_decode_error(const std::string& s) {
const size_t n = s.size();
size_t i = 0;
auto fmt = [&](size_t start, size_t end, const char* reason) {
char buf[160];
if (end - start == 1)
std::snprintf(buf, sizeof buf, "'utf-8' codec can't decode byte 0x%02x in position %zu: %s",
static_cast<unsigned char>(s[start]), start, reason);
else
std::snprintf(buf, sizeof buf, "'utf-8' codec can't decode bytes in position %zu-%zu: %s", start, end - 1, reason);
return std::string(buf);
};
while (i < n) {
unsigned char b = static_cast<unsigned char>(s[i]);
if (b < 0x80) {
++i;
continue;
}
int need;
unsigned char lo = 0x80, hi = 0xBF;
if (b >= 0xC2 && b <= 0xDF) {
need = 1;
} else if (b >= 0xE0 && b <= 0xEF) {
need = 2;
if (b == 0xE0) lo = 0xA0;
if (b == 0xED) hi = 0x9F;
} else if (b >= 0xF0 && b <= 0xF4) {
need = 3;
if (b == 0xF0) lo = 0x90;
if (b == 0xF4) hi = 0x8F;
} else {
return fmt(i, i + 1, "invalid start byte");
}
size_t j = i + 1;
for (int k = 0; k < need; ++k) {
if (j >= n) return fmt(i, n, "unexpected end of data");
unsigned char c = static_cast<unsigned char>(s[j]);
unsigned char l = (k == 0) ? lo : 0x80, h = (k == 0) ? hi : 0xBF;
if (c < l || c > h) return fmt(i, j, "invalid continuation byte");
++j;
}
i = j;
}
return "";
}
} // namespace
MplexConfig::MplexConfig() {
for (const auto& item : mplex_default_keys())
bind(item.table, MplexKeys::canonical(item.key), item.command, item.repeat, item.note);
}
void MplexConfig::error(const std::string& text) {
++skipped;
if (errors.size() < ERRORS_MAX) errors.push_back(text);
}
void MplexConfig::bind(const std::string& table, const std::string& key, const std::string& command, bool repeat,
const std::string& note) {
Table* found = tables.find(table);
if (found == nullptr) found = &tables.set(table, Table());
found->set(key, MplexBinding{command, repeat, note});
}
bool MplexConfig::unbind(const std::string& table, const std::string& key) {
Table* found = tables.find(table);
if (found == nullptr) return false;
return found->erase(key);
}
const MplexBinding* MplexConfig::lookup(const std::string& table, const std::string& key) const {
const Table* found = tables.find(table);
if (found == nullptr) return nullptr;
return found->find(key);
}
std::string MplexConfig::default_path(const std::string& home) { return py::join_path(home, CONFIG_NAME); }
std::shared_ptr<MplexConfig> MplexConfig::load(const std::string& path) {
auto config = std::make_shared<MplexConfig>();
config->path = path;
config->apply_file(path);
return config;
}
void MplexConfig::apply_file(const std::string& path) {
struct stat st;
if (::stat(path.c_str(), &st) != 0) {
if (errno == ENOENT) return;
error(path + ": cannot read: " + os_error_text(errno, path));
return;
}
if (st.st_size > CONFIG_MAX) {
error(path + ": larger than " + std::to_string(CONFIG_MAX) + " bytes, ignored");
return;
}
std::string raw;
{
std::FILE* handle = std::fopen(path.c_str(), "rb");
if (handle == nullptr) {
int err = errno;
if (err == ENOENT) return;
error(path + ": cannot read: " + os_error_text(err, path));
return;
}
char buf[65536];
size_t got;
while ((got = std::fread(buf, 1, sizeof buf, handle)) > 0) raw.append(buf, got);
bool failed = std::ferror(handle) != 0;
int err = errno;
std::fclose(handle);
if (failed) {
error(path + ": cannot read: " + os_error_text(err, path));
return;
}
}
std::string bad = utf8_decode_error(raw);
if (!bad.empty()) {
error(path + ": not valid TOML: " + bad);
return;
}
TomlValue data;
try {
data = TomlValue::loads(raw);
} catch (const TomlError& exc) {
error(path + ": not valid TOML: " + exc.what());
return;
}
apply_data(data);
}
void MplexConfig::apply_data(const TomlValue& data) {
for (const auto& item : data.table) {
if (item.first != "options" && item.first != "keys")
error("unknown top-level key [" + item.first + "] (use options, keys)");
}
const TomlValue* options_node = data.get("options");
static const TomlValue empty_table;
const TomlValue* options_value = options_node ? options_node : &empty_table;
if (!options_value->is_table()) {
error("[options] must be a table");
options_value = &empty_table;
}
for (const auto& item : options_value->table) {
const std::string& name = item.first;
const TomlValue& value = item.second;
try {
std::string raw;
if (value.is_bool()) {
raw = value.boolean ? "on" : "off";
} else if (value.is_string()) {
raw = value.text;
} else if (value.is_int()) {
raw = std::to_string(value.integer);
} else {
throw MplexError("value must be a string, number or boolean");
}
options.set(name, raw);
} catch (const MplexError& exc) {
error("options." + name + ": " + exc.what());
}
}
const TomlValue* keys_node = data.get("keys");
const TomlValue* keys = keys_node ? keys_node : &empty_table;
if (!keys->is_table()) {
error("[keys] must be a table");
return;
}
const TomlValue* clear_node = keys->get("clear");
std::vector<std::string> clear;
if (clear_node != nullptr) {
if (!clear_node->is_array()) {
error("keys.clear must be a list of table names");
} else {
for (const auto& table : clear_node->array) clear.push_back(py_str(table));
}
}
for (const auto& table : clear) tables.set(table, Table());
for (const auto& item : keys->table) {
if (item.first == "clear") continue;
if (!item.second.is_table()) {
error("keys." + item.first + " must be a table of key = command");
continue;
}
for (const auto& entry : item.second.table) apply_binding(item.first, entry.first, entry.second);
}
}
void MplexConfig::apply_binding(const std::string& table, const std::string& key, const TomlValue& spec) {
std::string where = "keys." + table + "." + key;
try {
std::string name = MplexKeys::canonical(key);
if ((spec.is_bool() && !spec.boolean) || (spec.is_string() && spec.text.empty())) {
unbind(table, name);
return;
}
std::string note;
bool repeat = false;
const TomlValue* command = &spec;
if (spec.is_table()) {
for (const auto& item : spec.table) {
if (item.first != "command" && item.first != "repeat" && item.first != "note")
throw MplexError("unknown field " + item.first + " (use command, repeat, note)");
}
command = spec.get("command");
const TomlValue* rep = spec.get("repeat");
repeat = rep ? truthy(*rep) : false;
const TomlValue* nt = spec.get("note");
note = nt ? py_str(*nt) : "";
}
if (command == nullptr || !command->is_string() || py::strip(command->text).empty())
throw MplexError("command must be a non-empty string, or false to unbind");
for (const auto& words : MplexCommandParser::parse(command->text)) MplexCommands::resolve(words[0]);
bind(table, name, command->text, repeat, note);
} catch (const MplexError& exc) {
error(where + ": " + exc.what());
}
}
} // namespace detz::mplex
+416
View File
@@ -0,0 +1,416 @@
// MplexFormat: transcription of tai.py:10987-11258.
//
// Text is handled as UTF-8 bytes wherever only ASCII delimiters matter (a continuation byte can
// never equal an ASCII byte, so the scan sees what the Python code point scan sees); the
// modifiers that count characters (=N:, n:) convert to code points first.
#include "mplex_format.h"
#include <algorithm>
#include <cmath>
#include <ctime>
#include "mplex_py.h"
namespace detz::mplex {
namespace {
const char* short_name(char c) {
switch (c) {
case 'D': return "pane_id";
case 'F': return "window_flags";
case 'H': return "host";
case 'I': return "window_index";
case 'P': return "pane_index";
case 'S': return "session_name";
case 'T': return "pane_title";
case 'W': return "window_name";
case 'h': return "host_short";
default: return nullptr;
}
}
std::string or_empty(const std::optional<std::string>& value) { return value ? *value : std::string(); }
// str(int(value)) for an integral double, exact like Python's arbitrary precision int.
std::string integral_to_string(double value) {
if (value == 0.0) return "0";
bool negative = value < 0;
double mag = std::fabs(value);
if (mag < 9.2e18) return std::to_string(static_cast<long long>(value));
int exp = 0;
double frac = std::frexp(mag, &exp); // mag = frac * 2^exp, frac in [0.5, 1)
unsigned long long mant = static_cast<unsigned long long>(std::ldexp(frac, 53));
int shift = exp - 53; // mag = mant * 2^shift, shift > 0 here
std::vector<int> digits; // little endian base 10
while (mant) {
digits.push_back(static_cast<int>(mant % 10));
mant /= 10;
}
for (int s = 0; s < shift; ++s) {
int carry = 0;
for (auto& d : digits) {
int v = d * 2 + carry;
d = v % 10;
carry = v / 10;
}
if (carry) digits.push_back(carry);
}
std::string out;
if (negative) out.push_back('-');
for (size_t i = digits.size(); i-- > 0;) out.push_back(static_cast<char>('0' + digits[i]));
return out;
}
double python_mod(double a, double b) {
double mod = std::fmod(a, b);
if (mod != 0.0) {
if ((b < 0) != (mod < 0)) mod += b;
} else {
mod = std::copysign(0.0, b);
}
return mod;
}
std::string float_result(double value) {
if (std::isfinite(value) && value == std::floor(value)) return integral_to_string(value);
return py::format_float(value);
}
} // namespace
long MplexFormat::match_close(const std::string& text, long start, char opener, char closer) {
int depth = 1;
long pos = start + 1;
const long n = static_cast<long>(text.size());
while (pos < n) {
char ch = text[static_cast<size_t>(pos)];
if (ch == '#' && pos + 1 < n) {
if (text[static_cast<size_t>(pos + 1)] == opener) ++depth;
pos += 2;
continue;
}
if (ch == closer) {
--depth;
if (depth == 0) return pos;
}
++pos;
}
return -1;
}
std::vector<std::string> MplexFormat::split_commas(const std::string& text) {
std::vector<std::string> parts;
std::string cur;
int depth = 0;
size_t pos = 0;
while (pos < text.size()) {
char ch = text[pos];
if (ch == '#' && pos + 1 < text.size()) {
char nxt = text[pos + 1];
if (nxt == ',' && depth == 0) {
cur += "#,";
pos += 2;
continue;
}
if (nxt == '{' || nxt == '(') {
++depth;
cur.append(text, pos, 2);
pos += 2;
continue;
}
cur.append(text, pos, 2);
pos += 2;
continue;
}
if (ch == '}' || ch == ')') depth = std::max(0, depth - 1);
if (ch == ',' && depth == 0) {
parts.push_back(cur);
cur.clear();
} else {
cur.push_back(ch);
}
++pos;
}
parts.push_back(cur);
return parts;
}
std::string MplexFormat::expand(const std::string& text, const Resolver& resolve, int depth) {
if (depth > DEPTH_MAX || text.empty()) return "";
std::string out;
size_t pos = 0;
const size_t n = text.size();
while (pos < n) {
char ch = text[pos];
if (ch != '#' || pos + 1 >= n) {
out.push_back(ch);
++pos;
continue;
}
char nxt = text[pos + 1];
if (nxt == '#') {
out.push_back('#');
pos += 2;
} else if (nxt == '{') {
long end = match_close(text, static_cast<long>(pos + 1), '{', '}');
if (end < 0) {
out.push_back(ch);
++pos;
} else {
out += evaluate(text.substr(pos + 2, static_cast<size_t>(end) - (pos + 2)), resolve, depth);
pos = static_cast<size_t>(end) + 1;
}
} else if (nxt == '(') {
long end = match_close(text, static_cast<long>(pos + 1), '(', ')');
if (end < 0) {
out.push_back(ch);
++pos;
} else {
out += or_empty(resolve("(" + text.substr(pos + 2, static_cast<size_t>(end) - (pos + 2))));
pos = static_cast<size_t>(end) + 1;
}
} else if (nxt == '[') {
size_t end = text.find(']', pos + 2);
if (end == std::string::npos) {
out.push_back(ch);
++pos;
} else {
out.append(text, pos, end + 1 - pos);
pos = end + 1;
}
} else if (short_name(nxt) != nullptr) {
out += or_empty(resolve(short_name(nxt)));
pos += 2;
} else if (nxt == ',' || nxt == '}') {
out.push_back(nxt);
pos += 2;
} else {
out.push_back(ch);
++pos;
}
}
return out;
}
bool MplexFormat::truthy(const std::optional<std::string>& value) {
return value.has_value() && *value != "" && *value != "0";
}
std::string MplexFormat::value_of(const std::string& text, const Resolver& resolve, int depth) {
if (text.find('#') != std::string::npos) return expand(text, resolve, depth + 1);
return or_empty(resolve(text));
}
std::string MplexFormat::evaluate(const std::string& inner, const Resolver& resolve, int depth) {
if (!inner.empty() && inner[0] == '?') {
std::vector<std::string> parts = split_commas(inner.substr(1));
size_t index = 0;
while (index + 1 < parts.size()) {
std::string cond = value_of(parts[index], resolve, depth);
if (truthy(cond)) return expand(parts[index + 1], resolve, depth + 1);
index += 2;
}
if (index < parts.size()) return expand(parts[index], resolve, depth + 1);
return "";
}
long colon = modifier_colon(inner);
if (colon < 0) return or_empty(resolve(inner));
std::string mods = inner.substr(0, static_cast<size_t>(colon));
std::string rest = inner.substr(static_cast<size_t>(colon) + 1);
return apply_modifiers(mods, rest, resolve, depth);
}
long MplexFormat::modifier_colon(const std::string& inner) {
size_t colon = inner.find(':');
if (colon == std::string::npos || colon == 0) return -1;
std::string head = inner.substr(0, colon);
if (head.find('#') != std::string::npos || head.find('{') != std::string::npos || head.find(' ') != std::string::npos)
return -1;
return static_cast<long>(colon);
}
std::string MplexFormat::apply_modifiers(const std::string& mods, const std::string& rest, const Resolver& resolve,
int depth) {
std::vector<std::string> specs;
for (const auto& item : py::split(mods, ";"))
if (!item.empty()) specs.push_back(item);
std::optional<std::string> text;
for (const std::string& spec : specs) {
if (spec == "==" || spec == "!=" || spec == "<" || spec == ">" || spec == "<=" || spec == ">=" || spec == "&&" ||
spec == "||" || py::startswith(spec, "e|")) {
std::vector<std::string> parts = split_commas(rest);
std::string left = !parts.empty() ? expand(parts[0], resolve, depth + 1) : "";
std::string right = parts.size() > 1 ? expand(parts[1], resolve, depth + 1) : "";
return compare(spec, left, right);
}
if (spec == "m" || spec == "m/r" || spec == "m/ri" || spec == "m/i") {
std::vector<std::string> parts = split_commas(rest);
std::string pattern = !parts.empty() ? expand(parts[0], resolve, depth + 1) : "";
std::string subject = parts.size() > 1 ? expand(parts[1], resolve, depth + 1) : "";
bool ignore = py::endswith(spec, "i");
if (py::startswith(spec, "m/r")) {
try {
py::Regex re(to_ustr(pattern), ignore);
return re.search(to_ustr(subject)).found ? "1" : "0";
} catch (const py::RegexError&) {
return "0";
}
}
return py::fnmatchcase(ignore ? py::lower(subject) : subject, ignore ? py::lower(pattern) : pattern) ? "1" : "0";
}
if (spec == "l") return rest;
if (!text.has_value()) text = value_of(rest, resolve, depth);
text = apply_one(spec, *text, resolve, depth);
}
return text.has_value() ? *text : std::string();
}
std::string MplexFormat::apply_one(const std::string& spec, const std::string& text, const Resolver& resolve, int depth) {
if (spec == "E" || spec == "T") return expand(text, resolve, depth + 1);
if (spec == "q") {
static const std::string special = "\\\"'$#;{}[]()*?<>&|~ ";
std::string out;
for (char ch : text) {
if (special.find(ch) != std::string::npos) out.push_back('\\');
out.push_back(ch);
}
return out;
}
if (spec == "t") {
double stamp;
try {
stamp = py::to_float(text);
} catch (const ValueError&) {
return text;
}
if (!std::isfinite(stamp)) return text;
double floored = std::floor(stamp);
if (floored < -9.2e18 || floored > 9.2e18) return text;
time_t when = static_cast<time_t>(floored);
struct tm parts;
if (localtime_r(&when, &parts) == nullptr) return text;
char buf[128];
size_t len = std::strftime(buf, sizeof buf, "%a %b %d %H:%M:%S %Y", &parts);
return std::string(buf, len);
}
if (spec == "n") return std::to_string(py::len(text));
if (spec == "b") return py::basename(text);
if (spec == "d") return py::dirname(text);
if (spec == "!") return truthy(text) ? "0" : "1";
if (spec == "!!") return truthy(text) ? "1" : "0";
if (!spec.empty() && spec[0] == '=') {
std::string body = spec.substr(1);
std::string marker;
if (!body.empty() && body[0] == '/') {
std::vector<std::string> pieces = py::split(body, "/", 2);
pieces.push_back("");
pieces.push_back("");
body = pieces[1];
marker = pieces[2];
}
long long limit;
try {
limit = py::to_int(body);
} catch (const ValueError&) {
return text;
}
long long length = static_cast<long long>(py::len(text));
if (limit >= 0 && length > limit) return py::slice(text, 0, static_cast<long>(limit)) + marker;
if (limit < 0 && length > -limit) return marker + py::slice(text, static_cast<long>(limit), 1L << 40);
return text;
}
if (py::startswith(spec, "s/")) {
std::vector<std::string> fields = py::split(spec, "/");
if (fields.size() >= 3) {
try {
py::Regex re(to_ustr(fields[1]));
return to_utf8(re.sub(to_ustr(fields[2]), to_ustr(text)));
} catch (const py::RegexError&) {
return text;
}
}
}
(void)resolve;
return text;
}
std::string MplexFormat::compare(const std::string& op, const std::string& left, const std::string& right) {
if (py::startswith(op, "e|")) {
std::string kind = op.find('|') != std::string::npos ? py::split(op, "|")[1] : "";
double a, b;
try {
a = py::to_float(left);
b = py::to_float(right);
} catch (const ValueError&) {
return "0";
}
if (kind == "+") return float_result(a + b);
if (kind == "-") return float_result(a - b);
if (kind == "*") return float_result(a * b);
if (kind == "/") return float_result(b != 0.0 ? a / b : 0.0);
if (kind == "%") return float_result(b != 0.0 ? python_mod(a, b) : 0.0);
if (kind == "==") return a == b ? "1" : "0";
if (kind == "!=") return a != b ? "1" : "0";
if (kind == "<") return a < b ? "1" : "0";
if (kind == ">") return a > b ? "1" : "0";
if (kind == "<=") return a <= b ? "1" : "0";
if (kind == ">=") return a >= b ? "1" : "0";
return "0";
}
if (op == "==") return left == right ? "1" : "0";
if (op == "!=") return left != right ? "1" : "0";
if (op == "&&") return truthy(left) && truthy(right) ? "1" : "0";
if (op == "||") return truthy(left) || truthy(right) ? "1" : "0";
double a, b;
bool numeric = true;
try {
a = py::to_float(left);
b = py::to_float(right);
} catch (const ValueError&) {
numeric = false;
a = b = 0;
}
bool result;
if (numeric) {
if (op == "<") result = a < b;
else if (op == ">") result = a > b;
else if (op == "<=") result = a <= b;
else result = a >= b;
} else {
int order = left.compare(right); // UTF-8 byte order equals code point order
if (op == "<") result = order < 0;
else if (op == ">") result = order > 0;
else if (op == "<=") result = order <= 0;
else result = order >= 0;
}
return result ? "1" : "0";
}
std::string MplexFormat::with_time(const std::string& text, double stamp) {
if (text.find('%') == std::string::npos) return text;
try {
return py::strftime(text, stamp);
} catch (const ValueError&) {
return text;
}
}
std::string MplexFormat::strip_styles(const std::string& text) {
std::string out;
size_t i = 0;
while (i < text.size()) {
if (text[i] == '#' && i + 1 < text.size() && text[i + 1] == '[') {
size_t end = text.find(']', i + 2);
if (end != std::string::npos) {
i = end + 1;
continue;
}
}
out.push_back(text[i]);
++i;
}
return out;
}
} // namespace detz::mplex
+340
View File
@@ -0,0 +1,340 @@
// MplexLayoutNode and MplexLayout: transcription of tai.py:11261-11546.
#include "mplex_layout.h"
#include <algorithm>
#include <cmath>
#include "mplex_pane.h"
namespace detz::mplex {
namespace {
using Node = MplexLayoutNode::Ptr;
long index_of(const std::vector<Node>& kids, const Node& node) {
for (size_t i = 0; i < kids.size(); ++i)
if (kids[i] == node) return static_cast<long>(i);
return -1;
}
// Python floor division and modulo (divmod) for ints.
void divmod(int a, int b, int& q, int& r) {
q = a / b;
r = a % b;
if (r != 0 && ((r < 0) != (b < 0))) {
--q;
r += b;
}
}
} // namespace
MplexLayoutNode::MplexLayoutNode(const std::string& kind_in, std::shared_ptr<MplexPane> pane_in, int w_in, int h_in)
: kind(kind_in), pane(std::move(pane_in)), w(w_in), h(h_in) {}
std::vector<MplexLayoutNode::Ptr> MplexLayoutNode::leaves() {
if (kind == "pane") return {shared_from_this()};
std::vector<Ptr> found;
for (auto& child : children) {
auto sub = child->leaves();
found.insert(found.end(), sub.begin(), sub.end());
}
return found;
}
MplexLayoutNode::Ptr MplexLayoutNode::adopt(const Ptr& child) {
child->parent = shared_from_this();
children.push_back(child);
return child;
}
int MplexLayout::size_of(const Node& node, const std::string& kind) { return kind == "lr" ? node->w : node->h; }
void MplexLayout::set_size(const Node& node, const std::string& kind, int value) {
if (kind == "lr") node->w = value;
else node->h = value;
}
void MplexLayout::assign(const Node& node, int x, int y, int w, int h) {
node->x = x;
node->y = y;
node->w = w;
node->h = h;
if (node->kind == "pane") return;
std::vector<Node>& kids = node->children;
int count = static_cast<int>(kids.size());
const std::string kind = node->kind;
int total = (kind == "lr" ? w : h) - (count - 1);
std::vector<int> sizes;
if (total < count) {
sizes.assign(static_cast<size_t>(count), 1);
} else {
for (auto& kid : kids) sizes.push_back(std::max(1, size_of(kid, kind)));
int weight = 0;
for (int s : sizes) weight += s;
if (weight == 0) weight = count;
if (weight != total) {
for (auto& s : sizes) {
// int(round(size * total / weight)): true division, round half to even
double scaled = static_cast<double>(static_cast<long long>(s) * total) / static_cast<double>(weight);
s = std::max(1, static_cast<int>(std::nearbyint(scaled)));
}
}
int sum = 0;
for (int s : sizes) sum += s;
int diff = total - sum;
while (diff != 0) {
bool moved = false;
for (int step = 0; step < count; ++step) {
int index = diff > 0 ? step : count - 1 - step;
if (diff > 0) {
sizes[static_cast<size_t>(index)] += 1;
diff -= 1;
moved = true;
} else if (sizes[static_cast<size_t>(index)] > 1) {
sizes[static_cast<size_t>(index)] -= 1;
diff += 1;
moved = true;
}
if (diff == 0) break;
}
if (!moved) break;
}
}
std::vector<std::pair<Node, int>> order;
for (int i = 0; i < count; ++i) order.emplace_back(kids[static_cast<size_t>(i)], sizes[static_cast<size_t>(i)]);
if (node->reverse) std::reverse(order.begin(), order.end());
int pos = kind == "lr" ? x : y;
for (auto& [kid, size] : order) {
if (kind == "lr") assign(kid, pos, y, size, h);
else assign(kid, x, pos, w, size);
pos += size + 1;
}
}
std::pair<MplexLayout::Node, MplexLayout::Node> MplexLayout::split(Node root, const Node& leaf, const std::string& kind,
bool before,
const std::shared_ptr<MplexPane>& new_pane,
int size) {
int total = size_of(leaf, kind);
if (total < 3) throw MplexError("no space for new pane");
int avail = total - 1;
int new_size;
if (size == -1) {
new_size = before ? avail - avail / 2 : avail / 2;
} else {
new_size = std::max(1, std::min(avail - 1, size));
}
int old_size = avail - new_size;
Node fresh = std::make_shared<MplexLayoutNode>("pane", new_pane, leaf->w, leaf->h);
Node parent = leaf->parent_node();
if (parent && parent->kind == kind) {
long index = index_of(parent->children, leaf);
parent->children.insert(parent->children.begin() + (before ? index : index + 1), fresh);
fresh->parent = parent;
} else {
Node box = std::make_shared<MplexLayoutNode>(kind, nullptr, leaf->w, leaf->h);
box->x = leaf->x;
box->y = leaf->y;
box->parent = parent;
if (!parent) {
root = box;
} else {
parent->children[static_cast<size_t>(index_of(parent->children, leaf))] = box;
}
if (before) {
box->adopt(fresh);
box->adopt(leaf);
} else {
box->adopt(leaf);
box->adopt(fresh);
}
}
set_size(leaf, kind, old_size);
set_size(fresh, kind, new_size);
return {root, fresh};
}
MplexLayout::Node MplexLayout::remove(Node root, const Node& leaf) {
Node parent = leaf->parent_node();
if (!parent) return nullptr;
long index = index_of(parent->children, leaf);
parent->children.erase(parent->children.begin() + index);
Node neighbour = index > 0 ? parent->children[static_cast<size_t>(index - 1)] : parent->children[0];
const std::string kind = parent->kind;
set_size(neighbour, kind, size_of(neighbour, kind) + size_of(leaf, kind) + 1);
if (parent->children.size() == 1) {
Node only = parent->children[0];
Node grand = parent->parent_node();
only->w = parent->w;
only->h = parent->h;
only->parent = grand;
if (!grand) return only;
long slot = index_of(grand->children, parent);
if (only->kind == grand->kind) {
std::vector<Node> moved = only->children;
grand->children.erase(grand->children.begin() + slot);
grand->children.insert(grand->children.begin() + slot, moved.begin(), moved.end());
for (auto& child : moved) child->parent = grand;
} else {
grand->children[static_cast<size_t>(slot)] = only;
}
}
return root;
}
void MplexLayout::resize_by(const Node& leaf, const std::string& kind, int delta, bool prefer_after) {
Node node = leaf;
while (node->parent_node() && node->parent_node()->kind != kind) node = node->parent_node();
Node parent = node->parent_node();
if (!parent) throw MplexError("no border to resize");
std::vector<Node>& kids = parent->children;
long index = index_of(kids, node);
bool use_after = prefer_after ? (index < static_cast<long>(kids.size()) - 1) : (index == 0);
long first = use_after ? index : index - 1;
Node a = kids[static_cast<size_t>(first)], b = kids[static_cast<size_t>(first + 1)];
int size_a = size_of(a, kind), size_b = size_of(b, kind);
int new_a = std::max(1, std::min(size_a + size_b - 1, size_a + delta));
set_size(a, kind, new_a);
set_size(b, kind, size_a + size_b - new_a);
}
std::vector<int> MplexLayout::shares(int total, int count, bool spare_first) {
int room = total - (count - 1);
int base, extra;
divmod(room, count, base, extra);
std::vector<int> sizes(static_cast<size_t>(count), base);
if (spare_first) {
for (int i = 0; i < extra; ++i) sizes[static_cast<size_t>(i)] += 1;
} else {
for (int i = count - extra; i < count; ++i) sizes[static_cast<size_t>(i)] += 1;
}
return sizes;
}
void MplexLayout::resize_to(const Node& leaf, const std::string& kind, int size) {
int change = size - size_of(leaf, kind);
Node node = leaf;
while (node->parent_node() && node->parent_node()->kind != kind) node = node->parent_node();
Node parent = node->parent_node();
if (!parent) throw MplexError("no border to resize");
bool has_next = index_of(parent->children, node) < static_cast<long>(parent->children.size()) - 1;
resize_by(leaf, kind, has_next ? change : -change, true);
}
MplexLayout::Node MplexLayout::preset(const std::string& name, const std::vector<std::shared_ptr<MplexPane>>& panes,
int width, int height, int main_width, int main_height) {
int count = static_cast<int>(panes.size());
std::vector<Node> leaves;
for (const auto& pane : panes) leaves.push_back(std::make_shared<MplexLayoutNode>("pane", pane, 1, 1));
const std::string suffix = "-mirrored";
bool mirrored = name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0;
std::string base = mirrored ? name.substr(0, name.size() - suffix.size()) : name;
if (count == 1) {
leaves[0]->w = width;
leaves[0]->h = height;
return leaves[0];
}
auto box = [&](const std::string& kind, const std::vector<Node>& items, int w, int h, bool spare_first = true) {
Node node = std::make_shared<MplexLayoutNode>(kind, nullptr, w, h);
for (const auto& item : items) node->adopt(item);
std::vector<int> sizes = shares(kind == "lr" ? w : h, static_cast<int>(items.size()), spare_first);
for (size_t i = 0; i < items.size(); ++i) {
if (kind == "lr") {
items[i]->w = sizes[i];
items[i]->h = h;
} else {
items[i]->w = w;
items[i]->h = sizes[i];
}
}
return node;
};
if (base == "even-horizontal") return box("lr", leaves, width, height);
if (base == "even-vertical") return box("tb", leaves, width, height);
if (base == "main-horizontal" || base == "main-vertical") {
bool horizontal = base == "main-horizontal";
int span = horizontal ? height : width;
int main_size = std::max(1, std::min(span - 2, horizontal ? main_height : main_width));
Node main = leaves[0];
std::vector<Node> rest(leaves.begin() + 1, leaves.end());
int other_size = span - main_size - 1;
Node others, root;
if (horizontal) {
others = rest.size() == 1 ? rest[0] : box("lr", rest, width, other_size);
main->w = width;
main->h = main_size;
others->w = width;
others->h = other_size;
root = box("tb", {main, others}, width, height);
} else {
others = rest.size() == 1 ? rest[0] : box("tb", rest, other_size, height);
main->w = main_size;
main->h = height;
others->w = other_size;
others->h = height;
root = box("lr", {main, others}, width, height);
}
if (horizontal) {
main->h = main_size;
others->h = other_size;
} else {
main->w = main_size;
others->w = other_size;
}
root->reverse = mirrored;
return root;
}
int rows = 1, columns = 1;
while (rows * columns < count) {
rows += 1;
if (rows * columns < count) columns += 1;
}
std::vector<Node> lines;
for (int start = 0; start < count; start += columns) {
std::vector<Node> chunk(leaves.begin() + start, leaves.begin() + std::min(count, start + columns));
lines.push_back(chunk.size() == 1 ? chunk[0] : box("lr", chunk, width, 1, false));
}
if (lines.size() == 1) return lines[0];
Node root = box("tb", lines, width, height, false);
for (auto& line : lines) line->w = width;
return root;
}
std::shared_ptr<MplexPane> MplexLayout::neighbour(const std::vector<std::shared_ptr<MplexPane>>& panes,
const std::shared_ptr<MplexPane>& current, char direction, int width,
int height) {
int left = current->x, top = current->y;
int right = current->x + current->w, bottom = current->y + current->h;
auto overlaps = [&](const std::shared_ptr<MplexPane>& other) {
if (direction == 'L' || direction == 'R') return other->y < bottom && other->y + other->h > top;
return other->x < right && other->x + other->w > left;
};
std::vector<std::shared_ptr<MplexPane>> wanted;
for (const auto& other : panes) {
if (other == current || !overlaps(other)) continue;
if (direction == 'L' && other->x + other->w + 1 == left) wanted.push_back(other);
else if (direction == 'R' && other->x == right + 1) wanted.push_back(other);
else if (direction == 'U' && other->y + other->h + 1 == top) wanted.push_back(other);
else if (direction == 'D' && other->y == bottom + 1) wanted.push_back(other);
}
if (wanted.empty()) {
for (const auto& other : panes) {
if (other == current || !overlaps(other)) continue;
if (direction == 'L' && other->x + other->w >= width) wanted.push_back(other);
else if (direction == 'R' && other->x == 0) wanted.push_back(other);
else if (direction == 'U' && other->y + other->h >= height) wanted.push_back(other);
else if (direction == 'D' && other->y == 0) wanted.push_back(other);
}
}
if (wanted.empty()) return nullptr;
auto best = wanted.begin();
for (auto it = wanted.begin(); it != wanted.end(); ++it)
if ((*it)->last_active > (*best)->last_active) best = it;
return *best;
}
} // namespace detz::mplex
+195
View File
@@ -0,0 +1,195 @@
// MplexOptions: transcription of tai.py:10723-10848.
#include "mplex_options.h"
#include <cstdlib>
#include <stdexcept>
#include "mplex_keys.h"
#include "mplex_py.h"
#include "mplex_style.h"
namespace detz::mplex {
namespace {
constexpr int HISTORY_CAP = 200000; // MPLEX_HISTORY_CAP
constexpr int BUFFERS_CAP = 100; // MPLEX_BUFFERS_CAP
OptionDef def_of(const char* name, const std::string& kind, OptValue value) { return OptionDef{name, kind, std::move(value)}; }
OptionDef S(const char* name, const std::string& kind, const char* value) { return def_of(name, kind, OptValue::of_str(value)); }
OptionDef I(const char* name, const std::string& kind, long long value) { return def_of(name, kind, OptValue::of_int(value)); }
OptionDef B(const char* name, bool value) { return def_of(name, "bool", OptValue::of_bool(value)); }
} // namespace
const char* const MplexOptions::WORD_SEPARATORS = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~";
const std::vector<OptionDef>& MplexOptions::defs() {
static const std::vector<OptionDef> table = {
S("prefix", "key", "C-b"),
S("prefix2", "key", "None"),
I("base-index", "int:0:9999", 0),
I("pane-base-index", "int:0:9999", 0),
I("escape-time", "int:0:10000", 10),
I("repeat-time", "int:0:10000", 500),
I("history-limit", "int:0:" + std::to_string(HISTORY_CAP), 2000),
S("mode-keys", "choice:vi|emacs", ""),
S("status-keys", "choice:vi|emacs", ""),
B("status", true),
S("status-position", "choice:top|bottom", "bottom"),
I("status-interval", "int:0:3600", 15),
S("status-justify", "choice:left|centre|center|right", "left"),
S("status-left", "str", "[#{session_name}] "),
S("status-right", "str", "\"#{=21:pane_title}\" %H:%M %d-%b-%y"),
I("status-left-length", "int:0:1000", 10),
I("status-right-length", "int:0:1000", 40),
S("status-style", "style", "bg=green,fg=black"),
S("status-left-style", "style", "default"),
S("status-right-style", "style", "default"),
S("message-style", "style", "bg=yellow,fg=black"),
S("message-command-style", "style", "bg=black,fg=yellow"),
S("mode-style", "style", "noattr,bg=yellow,fg=black"),
S("copy-mode-match-style", "style", "bg=cyan,fg=black"),
S("copy-mode-current-match-style", "style", "bg=magenta,fg=black"),
S("copy-mode-mark-style", "style", "bg=red,fg=black"),
I("display-time", "int:0:600000", 750),
I("display-panes-time", "int:1:600000", 1000),
S("display-panes-colour", "colour", "blue"),
S("display-panes-active-colour", "colour", "red"),
S("window-status-format", "str", "#I:#W#{?window_flags,#{window_flags}, }"),
S("window-status-current-format", "str", "#I:#W#{?window_flags,#{window_flags}, }"),
S("window-status-separator", "str", " "),
S("window-status-style", "style", "default"),
S("window-status-current-style", "style", "default"),
S("window-status-last-style", "style", "default"),
S("pane-border-style", "style", "default"),
S("pane-active-border-style", "str", "#{?pane_in_mode,fg=yellow,#{?synchronize-panes,fg=red,fg=green}}"),
S("pane-border-lines", "choice:single|double|heavy|simple", "single"),
S("clock-mode-colour", "colour", "blue"),
S("clock-mode-style", "choice:12|24", "24"),
I("main-pane-width", "int:1:10000", 80),
I("main-pane-height", "int:1:10000", 24),
B("renumber-windows", false),
B("automatic-rename", true),
S("automatic-rename-format", "str", "#{?pane_in_mode,[mplex],#{pane_current_command}}#{?pane_dead,[dead],}"),
B("allow-set-title", true),
S("default-shell", "str", ""),
S("default-command", "str", ""),
S("default-terminal", "str", "tmux-256color"),
B("detach-on-destroy", true),
B("wrap-search", true),
S("word-separators", "str", WORD_SEPARATORS),
B("remain-on-exit", false),
B("synchronize-panes", false),
S("copy-command", "str", ""),
I("buffer-limit", "int:1:" + std::to_string(BUFFERS_CAP), 50),
S("set-clipboard", "choice:on|external|off", "off"),
};
return table;
}
const OptionDef* MplexOptions::find_def(const std::string& name) {
for (const auto& item : defs())
if (item.name == name) return &item;
return nullptr;
}
OptValue MplexOptions::check(const std::string& name, const std::string& text) {
const OptionDef* def = find_def(name);
if (def == nullptr) throw MplexError("unknown or unsupported option: " + name);
const std::string& kind = def->kind;
if (kind == "str") return OptValue::of_str(text);
if (kind == "bool") {
std::string low = py::lower(py::strip(text));
if (low == "on" || low == "yes" || low == "true" || low == "1") return OptValue::of_bool(true);
if (low == "off" || low == "no" || low == "false" || low == "0") return OptValue::of_bool(false);
throw MplexError("value is invalid: " + text);
}
if (kind == "key") {
std::string stripped = py::strip(text);
if (stripped == "None" || stripped == "none" || stripped.empty()) return OptValue::of_str("None");
return OptValue::of_str(MplexKeys::canonical(text));
}
if (kind == "colour") {
MplexStyle::color(text, false);
return OptValue::of_str(text);
}
if (kind == "style") {
if (text.find("#{") == std::string::npos) MplexStyle::parse(text);
return OptValue::of_str(text);
}
if (py::startswith(kind, "int:")) {
std::vector<std::string> parts = py::split(kind, ":");
const std::string& low = parts[1];
const std::string& high = parts[2];
long long value;
try {
value = py::to_int(text);
} catch (const ValueError&) {
throw MplexError("value is invalid: " + text);
}
if (value < std::atoll(low.c_str()) || value > std::atoll(high.c_str()))
throw MplexError("value out of range " + low + ".." + high + ": " + text);
return OptValue::of_int(value);
}
if (py::startswith(kind, "choice:")) {
std::vector<std::string> choices = py::split(kind.substr(7), "|");
for (const auto& choice : choices)
if (choice == text) return OptValue::of_str(text);
throw MplexError("value is invalid (use " + py::join("|", choices) + "): " + text);
}
throw MplexError("bad option kind for " + name);
}
OptValue MplexOptions::get(const std::string& name) const {
const MplexOptions* node = this;
while (node != nullptr) {
auto found = node->values.find(name);
if (found != node->values.end()) return found->second;
node = node->parent;
}
const OptionDef* def = find_def(name);
if (def == nullptr) throw std::out_of_range("KeyError: " + name);
if (name == "mode-keys" || name == "status-keys") {
std::string editor = py::getenv_or("VISUAL");
if (editor.empty()) editor = py::getenv_or("EDITOR");
return OptValue::of_str(editor.find("vi") != std::string::npos ? "vi" : "emacs");
}
return def->def;
}
OptValue MplexOptions::set(const std::string& name, const std::string& raw, bool append) {
OptValue value = check(name, raw);
if (append && value.is_str() && values.count(name)) value.s = values[name].s + value.s;
if (!values.count(name)) value_order.push_back(name);
values[name] = value;
return value;
}
void MplexOptions::toggle(const std::string& name) {
const OptionDef* def = find_def(name);
if (def == nullptr || def->kind != "bool") throw MplexError("only boolean options can be toggled: " + name);
bool now = get(name).truthy();
if (!values.count(name)) value_order.push_back(name);
values[name] = OptValue::of_bool(!now);
}
void MplexOptions::unset(const std::string& name) {
if (find_def(name) == nullptr) throw MplexError("unknown or unsupported option: " + name);
if (values.erase(name)) {
for (size_t i = 0; i < value_order.size(); ++i) {
if (value_order[i] == name) {
value_order.erase(value_order.begin() + static_cast<long>(i));
break;
}
}
}
}
std::string MplexOptions::show(const OptValue& value) {
if (value.is_bool()) return value.b ? "on" : "off";
if (value.is_int()) return std::to_string(value.i);
return value.s;
}
} // namespace detz::mplex
+145
View File
@@ -0,0 +1,145 @@
// MplexStyle: transcription of tai.py:10653-10720.
#include "mplex_style.h"
#include <cctype>
#include "mplex_py.h"
namespace detz::mplex {
namespace {
const char* const NAMES[8] = {"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"};
int name_index(const std::string& text) {
for (int i = 0; i < 8; ++i)
if (text == NAMES[i]) return i;
return -1;
}
int attr_of(const std::string& low) {
if (low == "bold" || low == "bright") return 1;
if (low == "dim") return 2;
if (low == "italics") return 32;
if (low == "underscore" || low == "underline") return 4;
if (low == "blink") return 64;
if (low == "reverse") return 8;
if (low == "hidden") return 128;
if (low == "strikethrough") return 16;
return 0;
}
// int(text, 16) as CPython does it for the 2 character slices of a "#rrggbb" colour:
// surrounding whitespace, a sign, an optional 0x prefix and single underscores are accepted.
bool python_hex(const std::string& raw, long long& value) {
std::string s = py::strip(raw);
size_t i = 0;
bool negative = false;
if (i < s.size() && (s[i] == '+' || s[i] == '-')) {
negative = s[i] == '-';
++i;
}
if (i + 1 < s.size() && s[i] == '0' && (s[i + 1] == 'x' || s[i + 1] == 'X')) {
i += 2;
if (i < s.size() && s[i] == '_') ++i;
}
if (i >= s.size()) return false;
long long acc = 0;
bool last_underscore = true; // a leading underscore is not allowed
bool any = false;
for (; i < s.size(); ++i) {
char c = s[i];
if (c == '_') {
if (last_underscore) return false;
last_underscore = true;
continue;
}
int digit;
if (c >= '0' && c <= '9') digit = c - '0';
else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10;
else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10;
else return false;
acc = acc * 16 + digit;
last_underscore = false;
any = true;
}
if (!any || last_underscore) return false;
value = negative ? -acc : acc;
return true;
}
} // namespace
Color MplexStyle::color(const std::string& raw, bool background) {
std::string text = py::lower(py::strip(raw));
if (text == "default" || text == "terminal" || text.empty()) return 0;
int base = background ? 40 : 30;
int idx = name_index(text);
if (idx >= 0) return color_from_sgr(std::to_string(base + idx));
if (py::startswith(text, "bright")) {
int rest = name_index(text.substr(6));
if (rest >= 0) return color_from_sgr(std::to_string(base + 60 + rest));
}
if (py::startswith(text, "colour") || py::startswith(text, "color")) {
std::string digits = py::startswith(text, "colour") ? text.substr(6) : text.substr(5);
if (py::all_digits(to_ustr(digits))) {
long long value = 1000;
if (digits.size() <= 18) value = py::to_int(digits); // ValueError for non ASCII digits, as int() would
if (value < 256) return color_from_sgr(std::to_string(background ? 48 : 38) + ";5;" + std::to_string(value));
}
}
if (!text.empty() && text[0] == '#' && py::len(text) == 7) {
std::string r = py::slice(text, 1, 3), g = py::slice(text, 3, 5), b = py::slice(text, 5, 7);
long long red, green, blue;
// Python would format a negative component ("#-f0000" gives "38;2;-15;0;0", a malformed SGR
// the packed Color cannot carry); it is rejected here instead (documented deviation).
if (!python_hex(r, red) || !python_hex(g, green) || !python_hex(b, blue) || red < 0 || green < 0 || blue < 0)
throw MplexError("bad colour: " + text);
return color_from_sgr(std::to_string(background ? 48 : 38) + ";2;" + std::to_string(red) + ";" + std::to_string(green) +
";" + std::to_string(blue));
}
throw MplexError("bad colour: " + text);
}
Style MplexStyle::parse(const std::string& text, const Style& base, const std::optional<Style>& reset_in) {
Style cur = base;
Style reset = reset_in ? *reset_in : base;
for (const std::string& item : py::split_commas_ws(py::strip(text))) {
if (item.empty()) continue;
std::string low = py::lower(item);
if (low == "default") {
cur = reset;
} else if (low == "none" || low == "noattr") {
cur.attr = 0;
} else if (py::startswith(low, "fg=")) {
cur.fg = color(py::slice(item, 3, 1L << 40), false);
} else if (py::startswith(low, "bg=")) {
cur.bg = color(py::slice(item, 3, 1L << 40), true);
} else if (attr_of(low)) {
cur.attr |= attr_of(low);
} else if (py::startswith(low, "no") && attr_of(py::slice(low, 2, 1L << 40))) {
cur.attr &= ~attr_of(py::slice(low, 2, 1L << 40));
} else if (py::startswith(low, "align=") || py::startswith(low, "list=") || py::startswith(low, "range=") ||
py::startswith(low, "push-default") || py::startswith(low, "pop-default") ||
py::startswith(low, "fill=") || py::startswith(low, "norange") || py::startswith(low, "nolist")) {
continue;
} else {
throw MplexError("bad style: " + item);
}
}
return cur;
}
std::string MplexStyle::sgr(Color fg, Color bg, int attr) {
std::string out = "\x1b[0";
static const struct { int bit; const char* code; } bits[] = {{1, "1"}, {2, "2"}, {32, "3"}, {4, "4"},
{64, "5"}, {8, "7"}, {128, "8"}, {16, "9"}};
for (const auto& b : bits)
if (attr & b.bit) out += std::string(";") + b.code;
if (fg) out += ";" + color_sgr(fg);
if (bg) out += ";" + color_sgr(bg);
out += "m";
return out;
}
} // namespace detz::mplex
+562
View File
@@ -0,0 +1,562 @@
#!/usr/bin/env python3
# Generates the expected values for taicpp/tests/mplex_core_test.cpp by running the real
# ../tai.py classes (MplexCommandParser, MplexArgs, MplexStyle, MplexOptions, MplexConfig,
# MplexFormat, MplexLayout). Run from taicpp/: python3 -I tests/mplex_core_gen.py
# Writes build/core_cases.txt and build/core_cfg/*.toml (nothing else).
import importlib.util
import os
import random
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
os.environ["TZ"] = "UTC"
time.tzset()
os.environ["VISUAL"] = "nvi"
os.environ.pop("EDITOR", None)
spec = importlib.util.spec_from_file_location("tai_mod", os.path.join(ROOT, "..", "tai.py"))
tai = importlib.util.module_from_spec(spec)
sys.modules["tai_mod"] = tai
spec.loader.exec_module(tai)
OUT = os.path.join(ROOT, "build")
CFG = os.path.join(OUT, "core_cfg")
os.makedirs(CFG, exist_ok=True)
rng = random.Random(20260701)
lines = []
def esc(text):
out = []
for ch in text:
o = ord(ch)
if ch == "\\":
out.append("\\\\")
elif ch == "\n":
out.append("\\n")
elif ch == "\t":
out.append("\\t")
elif ch == "\r":
out.append("\\r")
elif o < 0x20 and ch not in "":
out.append("\\x%02x" % o)
else:
out.append(ch)
return "".join(out)
def emit(*fields):
lines.append("\t".join(esc(str(f)) for f in fields))
def enc_list(items):
return str(len(items)) + "".join("\x1f" + str(x) for x in items)
def outcome(func):
try:
return "OK:" + func()
except tai.MplexError as exc:
return "ERR:" + str(exc)
except Exception as exc: # noqa: BLE001
return "EXC:" + type(exc).__name__
# --------------------------------------------------------------------------- parser
P = tai.MplexCommandParser
parser_inputs = [
"", " ", "a", "a b", " a b ", "a;b", "a ; b", "a;;b;", ";", "a\nb", "a\\\nb", "a\\b", "a\\", "\\", "'a b'", "'a;b' c",
"'unterminated", '"unterminated', '"a b"', '"a\\nb"', '"a\\tb\\e\\r\\q"', '"a\\"b"', '"a\\', '"\\', "a'b c'd", 'a"b c"d',
"{ a b }", "{a;b}", "{ send-keys -X cancel }", "x {a b}", "{a {b} c}", "{a '}' c}", '{a "}" c}', "{a \\} c}", "{", "{a", "{{a}",
"a { b }", "a{b}", "{a}b", "{a} {b}", "{}", "{ }", "command-prompt -I \"#S\" { rename-session \"%%\" }",
"confirm-before -p \"kill-window #W? (y/n)\" kill-window", "set -g status-left '[#S] ' ; set -g mouse on",
"a\tb", "a\rb", "café über", "'café'", '"\\é"', "\\é", "a;b\\;c", "a \\; b", "x '' y", 'x "" y',
"''", '""', "a''b", "a\"\"b", "send-keys -l 'hello world' ; send-keys Enter", "if-shell 'true' { display ok } { display no }",
"{\n one\n two\n}", "{ a } ; { b }", "bind-key -T prefix '|' split-window -h", "\\\\", "\\'", "'\\'", "\"\\\\\"",
"{'}", "{\"}", "{a\\", "{'a\\'}", "\u0001\u0002", "a\x00b", "; ;",
]
alphabet = ["a", "b", " ", " ", "'", '"', "\\", "{", "}", ";", "\n", "$", "é", "#", "n", "t", "-", "x y", "{a}", "'q'", '"w"']
for _ in range(1500):
parser_inputs.append("".join(rng.choice(alphabet) for _ in range(rng.randint(1, 14))))
for text in parser_inputs:
def run(text=text):
return "\x1e".join("\x1f".join(words) if words else "" for words in P.parse(text)) + ("|%d" % len(P.parse(text)))
emit("P", text, outcome(run))
words_for_quote = ["", "a", "a b", "a-b_c.d/e:f=g%h@i+j,k", "a\"b", "a\\b", "a$b", "café", "éè", "-", "--x", "x y z", "'", "☃", "1", "a;b", "{}", "\n", "\t"]
for word in words_for_quote:
emit("Q", word, P.quote(word))
# --------------------------------------------------------------------------- args
A = tai.MplexArgs
def flags_dump(args):
parts = []
for key in sorted(args.flags):
vals = ["T" if v is True else "S" + v for v in args.flags[key]]
parts.append(key + ":" + enc_list(vals))
gets = []
ints = []
for key in sorted(args.flags):
gets.append(key + "=" + repr(args.get(key)))
gets.append(key + "*" + repr(args.all(key)))
try:
ints.append(key + "=" + repr(args.int(key, 7)))
except tai.MplexError as exc:
ints.append(key + "=ERR " + str(exc))
return "\x1e".join([enc_list(parts), enc_list(args.positional), enc_list(gets), enc_list(ints)])
arg_values = ["", "x", "12", "-3", " 4 ", "1_0", "abc", "-", "--", "-x", "café", "0", "+5", "٣", "9" * 30]
for name, (aliases, spec_text, low, high) in tai.MPLEX_COMMAND_SPECS.items():
letters = [c for c in spec_text if c != ":"]
takes = {spec_text[i - 1] for i in range(1, len(spec_text)) if spec_text[i] == ":"}
cases = []
cases.append([])
cases.append(["pos"])
cases.append(["pos1", "pos2"])
cases.append(["a", "b", "c", "d"])
cases.append(["--"])
cases.append(["--", "-x"])
cases.append(["-"])
cases.append(["-q"])
cases.append(["-é"])
cases.append(["-" + (letters[0] if letters else "z")])
for _ in range(40):
words = []
for _i in range(rng.randint(0, 6)):
roll = rng.random()
if roll < 0.45 and letters:
combo = "".join(rng.choice(letters) for _k in range(rng.randint(1, 3)))
words.append("-" + combo)
elif roll < 0.6 and takes:
letter = rng.choice(sorted(takes))
if rng.random() < 0.5:
words.append("-" + letter + rng.choice(arg_values))
else:
words.append("-" + letter)
words.append(rng.choice(arg_values))
elif roll < 0.7:
words.append("--")
else:
words.append(rng.choice(arg_values))
cases.append(words)
for words in cases:
def run(name=name, spec_text=spec_text, words=words, low=low, high=high):
return flags_dump(A.parse(name, spec_text, words, low, high))
emit("A", name, spec_text, low, -1 if high is None else high, enc_list(words), outcome(run))
# --------------------------------------------------------------------------- style
S = tai.MplexStyle
colors = ["", "default", "terminal", "red", "RED", " Blue ", "brightred", "brightwhite", "bright", "brightx", "colour0", "colour255",
"colour256", "color5", "colour", "colour 5", "colour005", "colour²", "colour٣", "#ff0000", "#FFAA00", "#12345", "#1234567",
"#gg0000", "# 1 2 3", "#+f+f+f", "#-f0000", "# f 0 0 ", "#0x0000", "#_f0000", "#f_0000", "#ff_ff_", "colour" + "9" * 30, "nonsense", "é", "BrightBlack"]
for text in colors:
for bg in (False, True):
emit("C", text, int(bg), outcome(lambda text=text, bg=bg: repr(S.color(text, bg))))
frags = [None, "31", "42", "93", "104", "38;5;12", "48;5;200", "38;2;1;2;3", "48;2;255;0;128"]
texts = ["", "default", "bg=green,fg=black", "bold", "noattr,bg=yellow,fg=black", "fg=red bg=blue", "fg=default", "nobold", "none",
"bold,dim,italics,underscore,blink,reverse,hidden,strikethrough", "bright", "nodim,noitalics", "align=left,list=on", "range=left", "fill=red",
"push-default", "pop-default", "norange", "nolist", "fg=bogus", "bg=#zzzzzz", "xyz", "no", "nobogus", " , ,", "FG=RED,BG=Blue,BOLD",
"fg=colour196,bg=colour21,underline", "fg=#112233,bg=#445566", "default,bold", "bold,default", "fg=red,default,bg=blue", "noreverse", "reverse,noreverse",
"underline,nounderline", "dim,nobold", "é"]
for text in texts:
for base in [(None, None, 0), ("31", "42", 5), ("38;5;12", None, 255)]:
for reset in (None, (None, "104", 2)):
def run(text=text, base=base, reset=reset):
r = S.parse(text, base, reset)
return "|".join(["-" if x is None else str(x) for x in r])
emit("S", text, enc_list(["-" if x is None else x for x in base[:2]]) + "\x1e" + str(base[2]),
"-" if reset is None else enc_list([reset[0] or "-", reset[1] or "-"]) + "\x1e" + str(reset[2]), outcome(run))
for fg in frags:
for bg in frags:
for attr in (0, 1, 2, 4, 8, 16, 32, 64, 128, 255, 37):
emit("G", fg or "-", bg or "-", attr, S.sgr(fg, bg, attr))
# --------------------------------------------------------------------------- options
O = tai.MplexOptions
def show_val(v):
if v is True:
return "bool:True"
if v is False:
return "bool:False"
if isinstance(v, int):
return "int:%d" % v
return "str:" + v
samples = {
"str": ["", "x", "hello world", "#{x}", "café"],
"bool": ["on", "off", "yes", "no", "true", "false", "1", "0", " ON ", "TRUE", "maybe", "", "2", "é"],
"key": ["C-a", "None", "none", "", " none ", "M-x", "F5", "BTab", "C-M-a", "S-Up", "^a", "Space", "bogus", "C-", "C-é", "Escape", "PageUp", " "],
"colour": ["red", "default", "colour5", "#ffffff", "bogus", "", "brightblue", "colour999"],
"style": ["bg=red", "default", "bold,fg=blue", "bogus", "", "#{?x,a,b}", "fg=zz", "noattr,bg=yellow,fg=black"],
"int": ["0", "1", "5", "-1", "10001", "x", "", " 7 ", "1_0", "999999999999", "3.5", "+4", "٤"],
"choice": ["vi", "emacs", "top", "bottom", "left", "centre", "center", "right", "12", "24", "single", "double", "heavy", "simple", "on", "external", "off", "VI", "", "x"],
}
names = [d for d in O.DEFS]
for name in names:
kind = O.DEFS[name][0]
base_kind = kind.split(":")[0]
pool = list(samples[base_kind])
if base_kind == "int":
_t, lo, hi = kind.split(":")
pool += [lo, hi, str(int(lo) - 1), str(int(hi) + 1)]
for raw in pool:
emit("O", name, raw, outcome(lambda name=name, raw=raw: show_val(O.check(name, raw))))
for raw in ["x"]:
emit("O", "no-such-option", raw, outcome(lambda: show_val(O.check("no-such-option", "x"))))
emit("O", "mouse", raw, outcome(lambda: show_val(O.check("mouse", "on"))))
emit("D", enc_list([n for n in O.DEFS]), enc_list([show_val(O.DEFS[n][1]) + "~" + O.DEFS[n][0] for n in O.DEFS]), O.WORD_SEPARATORS)
def dump_levels(levels):
return "\x1e".join(enc_list(["%s=%s" % (k, show_val(v)) for k, v in lvl.values.items()]) for lvl in levels)
for scenario in range(120):
g = O()
s = O(g)
w = O(s)
levels = [g, s, w]
emit("OBEGIN", scenario)
pool_names = rng.sample(names, 8) + ["status", "prefix", "mode-keys", "status-left", "history-limit", "bogus-name"]
for _ in range(40):
lvl = rng.randrange(3)
obj = levels[lvl]
name = rng.choice(pool_names)
op = rng.choice(["set", "set", "set", "append", "get", "get", "toggle", "unset", "is_set", "show"])
if name in O.DEFS:
kind = O.DEFS[name][0].split(":")[0]
raw = rng.choice(samples[kind])
else:
raw = "x"
if op in ("set", "append"):
res = outcome(lambda: show_val(obj.set(name, raw, append=(op == "append"))))
elif op == "get":
res = outcome(lambda: show_val(obj.get(name)))
elif op == "toggle":
res = outcome(lambda: (obj.toggle(name), "")[1])
elif op == "unset":
res = outcome(lambda: (obj.unset(name), "")[1])
elif op == "is_set":
res = "OK:" + str(obj.is_set(name))
else:
res = outcome(lambda: O.show(obj.get(name)))
emit("OOP", lvl, op, name, raw, res, dump_levels(levels))
# --------------------------------------------------------------------------- config
cfg_samples = [
"",
"[options]\nprefix = \"C-a\"\nprefix2 = \"F12\"\nbase-index = 1\nmode-keys = \"vi\"\nstatus-position = \"top\"\n",
"[options]\nstatus = false\nrenumber-windows = true\nhistory-limit = 10000\nstatus-left = \"[#S] \"\nstatus-style = \"bg=blue,fg=white\"\n",
"[options]\nbogus = 1\nstatus = 3.5\nprefix = [1]\nhistory-limit = 999999\nbase-index = \"x\"\nmode-keys = \"nano\"\nmouse = true\n",
"[options]\nstatus-left = 5\nprefix = 5\n",
"[options]\nwhen = 1979-05-27T07:32:00Z\nbase-index = 2024-01-01\n",
"[unknown]\nx = 1\n[options]\nstatus = true\n[other]\ny = 2\n",
"options = 3\nkeys = 4\n",
"options = \"a\"\n[keys]\nx = 1\n",
"[keys]\nclear = [\"prefix\"]\n[keys.prefix]\n\"|\" = \"split-window -h\"\n\"-\" = \"split-window -v\"\n\"C-b\" = false\nr = { command = \"source-file ~/.tai/mplex.toml\", note = \"reload\", repeat = false }\nUp = { command = \"select-pane -U\", repeat = true }\n",
"[keys.prefix]\n\"C-b\" = \"\"\nz = \"\"\nx = false\nbogus-key = \"split-window\"\n\"C-\" = \"split-window\"\nq = \"nosuchcommand\"\nw = \"split-window ; nosuchcommand\"\ne = \"\"\n",
"[keys.root]\n\"M-Left\" = \"previous-window\"\n\"M-Right\" = \"next-window\"\n[keys.copy-mode-vi]\ny = \"send-keys -X copy-selection-and-cancel\"\n[keys.mytable]\nA = \"display-message hi\"\n",
"[keys]\nclear = \"prefix\"\n[keys.prefix]\na = 1\nb = true\nc = [\"x\"]\nd = { command = 5 }\ne = { cmd = \"split-window\" }\nf = { command = \"split-window\", repeat = 1, note = 5 }\ng = { command = \" \" }\nh = { repeat = true }\n",
"[keys]\nclear = [\"prefix\", \"root\", 5, \"newtable\"]\n[keys.prefix]\nc = \"new-window\"\n",
"[keys]\nprefix = \"split-window\"\n[keys.root]\n",
"[keys]\nclear = [\"copy-mode\"]\n",
"[keys.prefix]\nSpace = { command = \"next-layout\", repeat = true, note = \"café\" }\n\"M-x\" = \"unsupported-cmd\"\nd = \"detach\"\nk = \"kill-ser\"\nl = \"display-menu\"\n",
"[keys.prefix]\nk = \"kill\"\n",
"[keys.prefix]\na = \"split-window { oops\"\nb = 'display-message \"unterminated'\n",
"this is not toml\n",
"[options\nstatus = true\n",
"[options]\nstatus = true\nstatus = false\n",
"[options]\nstatus = \"unterminated\n",
"[options]\nprefix = \"C-é\"\n",
"x = 1\ny = 2\n[options]\nstatus = true\n",
]
# many error producers to hit the ERRORS_MAX cap (50)
many = "[options]\n" + "".join("opt%d = 1\n" % i for i in range(70))
cfg_samples.append(many)
many_keys = "[keys.prefix]\n" + "".join("bogus%d = \"nosuchcommand\"\n" % i for i in range(60))
cfg_samples.append(many_keys)
# mutate good samples by deleting or inserting characters (random but deterministic)
good = cfg_samples[1] + cfg_samples[9] + cfg_samples[11]
for _ in range(60):
pieces = list(good)
for _k in range(rng.randint(1, 4)):
pos = rng.randrange(len(pieces))
if rng.random() < 0.5:
del pieces[pos]
else:
pieces.insert(pos, rng.choice(list("\"'=[]{}.,\n# -aZ0")))
cfg_samples.append("".join(pieces))
def cfg_dump(config):
opts = enc_list(["%s=%s" % (k, show_val(v)) for k, v in config.options.values.items()])
tabs = []
for tname, table in config.tables.items():
rows = ["%s\x1d%s\x1d%s\x1d%s" % (k, b.command, int(bool(b.repeat)), b.note) for k, b in table.items()]
tabs.append(tname + "\x1c" + enc_list(rows))
return "\x1e".join([opts, enc_list(tabs), enc_list(config.errors), str(config.skipped), config.path])
def write_cfg(index, data, mode="w"):
path = os.path.join("build", "core_cfg", "%03d.toml" % index)
with open(os.path.join(ROOT, path), "wb") as handle:
handle.write(data if isinstance(data, bytes) else data.encode("utf-8"))
return path
os.chdir(ROOT)
index = 0
for text in cfg_samples:
path = write_cfg(index, text)
index += 1
emit("K", path, cfg_dump(tai.MplexConfig.load(path)))
for data in (b"\xff\xfe[options]\n", b"[options]\nstatus = \"\xe2\x82\"\n", b"abc\xc3", b"\xc3\x28", b"\xe0\x80\x80", b"\xf5\x80\x80\x80", b"[options]\n\x80\n", b"\xed\xa0\x80"):
path = write_cfg(index, data)
index += 1
emit("K", path, cfg_dump(tai.MplexConfig.load(path)))
big = write_cfg(index, b"#" * (tai.MPLEX_CONFIG_MAX + 1))
index += 1
emit("K", big, cfg_dump(tai.MplexConfig.load(big)))
exact = write_cfg(index, b"#" * tai.MPLEX_CONFIG_MAX)
index += 1
emit("K", exact, cfg_dump(tai.MplexConfig.load(exact)))
emit("K", "build/core_cfg/missing.toml", cfg_dump(tai.MplexConfig.load("build/core_cfg/missing.toml")))
os.makedirs("build/core_cfg/adir.toml", exist_ok=True)
emit("K", "build/core_cfg/adir.toml", cfg_dump(tai.MplexConfig.load("build/core_cfg/adir.toml")))
emit("K", "build/core_cfg/missing/deeper.toml", cfg_dump(tai.MplexConfig.load("build/core_cfg/missing/deeper.toml")))
emit("K", "", cfg_dump(tai.MplexConfig.load("")))
emit("DEFAULTPATH", tai.MplexConfig.default_path("/home/x/.tai"), tai.MplexConfig.default_path("rel"))
fresh = tai.MplexConfig()
emit("K", "-", cfg_dump(fresh))
emit("LOOKUP", "".join("%s/%s=%s;" % (t, k, fresh.lookup(t, k) is not None) for t, k in (("prefix", "C-b"), ("prefix", "nope"), ("root", "C-b"), ("nonexistent", "x"), ("copy-mode", "Escape"))))
for t, k in (("prefix", "C-b"), ("prefix", "nope"), ("nonexistent", "x")):
emit("UNBIND", t, k, int(fresh.unbind(t, k)), cfg_dump(fresh)[:0])
# --------------------------------------------------------------------------- format
F = tai.MplexFormat
VARS = {
"session_name": "work", "session_id": "$3", "window_index": "2", "window_name": "vim edit", "window_flags": "*Z", "pane_index": "1",
"pane_title": "host:/home/user/project", "pane_id": "%7", "host": "box.example.com", "host_short": "box", "empty": "", "zero": "0",
"one": "1", "num_a": "7", "num_b": "2", "num_c": "-3.5", "big": "1e30", "tiny": "0.1", "path": "/usr/local/bin/tool.sh", "path2": "/",
"uni": "café über ☃ ok", "word": "hello", "sentence": "the quick brown fox", "stamp": "86400", "negstamp": "-1.5", "badstamp": "abc",
"hugestamp": "1e30", "fl": "2.5", "sp": "a b", "special": "a b;c'd\"e$f#g{h}i[j](k)*l?m<n>o&p|q~r\\s", "pane_in_mode": "0", "synchronize-panes": "0",
"inf": "inf", "nan": "nan", "ws": " 12 ", "a_b": "x", "cmd": "ls -l",
}
for k, v in VARS.items():
emit("V", k, v)
def resolver(name):
if name.startswith("("):
return "SH<" + name[1:] + ">"
return VARS.get(name)
fmt_cases = [
"", "plain", "#S #I #W #F #P #T #H #h #D", "##", "#", "a#", "##{x}", "#{session_name}", "#{missing}", "[#{session_name}] ", "#{",
"#{unterminated", "#(", "#(echo hi)", "#(echo #(inner) x)", "#(a", "#[fg=red]x#[default]", "#[bold", "#[a]#[b]", "x#,y", "x#}y", "#{?empty,yes,no}", "#{?one,yes,no}",
"#{?zero,yes,no}", "#{?missing,yes,no}", "#{?one,yes}", "#{?one}", "#{?empty,a,zero,b,one,c,d}", "#{?empty,a,zero,b,empty,c,d}", "#{?empty,a,zero,b}",
"#{?#{one},A,B}", "#{?pane_in_mode,fg=yellow,#{?synchronize-panes,fg=red,fg=green}}", "#{?window_flags,#{window_flags}, }",
"#{?one,a#,b,c}", "#{?one,#{?zero,x,y},z}", "#{==:#{num_a},7}", "#{==:a,a}", "#{==:a,b}", "#{!=:a,b}", "#{!=:a,a}", "#{<:1,2}", "#{<:10,9}", "#{<:b,a}", "#{>:b,a}", "#{<=:a,a}",
"#{>=:2,10}", "#{>=:abc,abd}", "#{&&:1,1}", "#{&&:1,0}", "#{||:0,0}", "#{||:0,1}", "#{&&:,1}", "#{||:#{one},#{zero}}", "#{e|+:7,2}", "#{e|-:7,2}", "#{e|*:7,2}", "#{e|/:7,2}", "#{e|%:7,2}",
"#{e|+:#{num_a},#{num_b}}", "#{e|/:1,0}", "#{e|%:1,0}", "#{e|*:1e30,1e30}", "#{e|+:0.1,0.2}", "#{e|/:1,3}", "#{e|*:big,1}", "#{e|+:x,1}", "#{e|==:1,1}", "#{e|!=:1,1}", "#{e|<:1,2}", "#{e|>:1,2}",
"#{e|<=:1,1}", "#{e|>=:1,2}", "#{e|?:1,2}", "#{e|%:-7,3}", "#{e|%:7,-3}", "#{e|%:-7.5,2}", "#{e|+:inf,1}", "#{e|*:inf,0}", "#{e|-:inf,inf}", "#{e|+:nan,1}", "#{e|*:1e300,1e300}", "#{e|*:-1,0}", "#{e|+:123456789012345678,1}",
"#{e|*:99999999999,99999999999}", "#{e|*:1e18,10}", "#{e|*:1e19,10}", "#{e|+:2e308,0}",
"#{m:*vim*,#{window_name}}", "#{m:vim,#{window_name}}", "#{m:V*,#{window_name}}", "#{m/i:V*,#{window_name}}", "#{m/r:^vim,#{window_name}}", "#{m/r:[,#{window_name}}", "#{m/ri:VIM,#{window_name}}",
"#{m/r:v.m,x}", "#{m:??m*,#{window_name}}", "#{m:[a-z]*,#{word}}", "#{m:[!a-z]*,#{word}}", "#{m:,}",
"#{=3:window_name}", "#{=-3:window_name}", "#{=0:window_name}", "#{=100:window_name}", "#{=-100:window_name}", "#{=/3/...:window_name}", "#{=/-3/...:window_name}", "#{=3:uni}", "#{=-4:uni}", "#{=21:pane_title}",
"#{=x:window_name}", "#{=:window_name}", "#{=/:window_name}", "#{=/5:window_name}", "#{=/5/a/b:sentence}", "#{=+3:sentence}", "#{= 3:sentence}",
"#{E:status}", "#{E:#{word}}", "#{T:#{word}}", "#{q:special}", "#{q:sp}", "#{n:word}", "#{n:uni}", "#{n:empty}", "#{b:path}", "#{d:path}", "#{b:path2}", "#{d:path2}", "#{d:word}",
"#{!:one}", "#{!:zero}", "#{!:empty}", "#{!:missing}", "#{!!:one}", "#{!!:word}", "#{!!:zero}", "#{l:literal text}", "#{l:#{word}}", "#{l:a:b}", "#{s/l/L/:word}", "#{s/l+/_/:sentence}", "#{s/(o)/[\\1]/:sentence}",
"#{s/[/x/:word}", "#{s/a/b:word}", "#{s//-/:word}", "#{s/ /_/:sentence}", "#{s/e/E/g:sentence}", "#{s/(?P<x>e)/<\\g<x>>/:word}", "#{s/a/\\q/:word}", "#{s/^/>/:word}", "#{s/$/</:word}", "#{s/\\w+/W/:sentence}",
"#{t:stamp}", "#{t:negstamp}", "#{t:badstamp}", "#{t:hugestamp}", "#{t:empty}", "#{t:inf}", "#{t:nan}", "#{t:fl}", "#{t:one}", "#{t:zero}",
"#{n;=3:word}", "#{=3;n:word}", "#{b;=3:path}", "#{=3;q:special}", "#{;:word}", "#{;;n:word}", "#{n;n:word}", "#{n;!:word}", "#{n;l:word}", "#{l;n:word}", "#{n;m:*,x}", "#{q;E:special}",
"#{x y:word}", "#{x#y:word}", "#{:word}", "#{a:b:c}", "#{unknown:word}", "#{n:#{word}}", "#{n:#{missing}}", "#{=3:#{sentence}}", "#{?one,#{=3:sentence},no}", "#{n:#(echo hi)}",
"#{session_name}:#{window_index}.#{pane_index}", "#{==:#{session_name},work}", "#{?#{==:#{session_name},work},mine,other}", "[#S:#I:#W#{?window_flags,#{window_flags}, }]",
"\"#{=21:pane_title}\" %H:%M %d-%b-%y", "#[fg=red]#{session_name}#[default] #{host}", "#{#{word}}", "#{#{#{word}}}", "#{x}#{y}", "#{?one,#{?one,#{?one,deep,d},d},d}",
"#{?one,#,,#,}", "#{?one,#},#}}", "#{?empty,#},#}}", "#, #}", "#{?one,a,b,c}", "#{?empty,a,b,c}", "#{==:#,,#,}", "#{?one,a#{b,c}", "#{?one,(a,b),c}", "#{?one,#(x,y),z}", "#{?one,#{x,y},z}",
"%H", "100%", "#{word}%", "café #{uni} ☃", "#é", "#{é}", "#{uni}#{uni}#{uni}", "#W#", "#WW", "#Z", "#{ }", "#{n: }", "#{?,a,b}", "#{?}", "#{?,}", "#{?,,}",
"#{e|+:#{num_a},#{num_c}}", "#{e|*:#{num_c},2}", "#{e|/:#{num_a},2}", "#{e|%:#{num_a},3}", "#{e|-:0,0}", "#{e|*:-0.0,1}", "#{e|/:6,3}", "#{e|/:7,2}", "#{e|+:1_000,1}", "#{e|+: 1 ,1}", "#{e|+:1e2,1}",
]
# deep nesting beyond DEPTH_MAX
nest = "x"
for _ in range(16):
nest = "#{?one," + nest + ",n}"
fmt_cases.append(nest)
nest2 = "#{word}"
for _ in range(16):
nest2 = "#{E:" + nest2 + "}"
fmt_cases.append(nest2)
frag = ["#{word}", "#{?one,a,b}", "#{n:sentence}", "#{=3:sentence}", "#S", "##", "#,", "#}", "text", " ", ",", "#{m:*,x}", "#{e|+:1,2}", "#{==:a,a}", "#(cmd)", "#[fg=red]", "{", "}", "(", ")", "#{?", ":", ";", "#{s/o/0/:word}", "%Y", "#{t:stamp}", "#{q:special}", "é"]
for _ in range(2500):
fmt_cases.append("".join(rng.choice(frag) for _ in range(rng.randint(1, 8))))
for text in fmt_cases:
emit("F", text, outcome(lambda text=text: F.expand(text, resolver)))
for text in ["", "no percent", "%Y-%m-%d %H:%M:%S", "%H:%M %d-%b-%y", "%a %A %b %B %j %y %%", "100%", "%", "%Q", "%E", "café %H", "%-d", "%s", "%Z %z"]:
for stamp in (0.0, 86400.5, 1700000000.0, 1e9):
emit("W", text, repr(stamp), outcome(lambda text=text, stamp=stamp: F.with_time(text, stamp)))
for text in ["", "#[a]", "x#[a]y#[b]z", "#[", "#[a", "#[a]]", "##[a]", "a#[b\nc]d", "#[]#[]", "#[a]#[b", "café#[x]é", "#[a] #[a]", "#[#[a]]", "#[[]]"]:
emit("X", text, F.strip_styles(text))
for value in (None, "", "0", "1", "x", " ", "00", "false"):
emit("TRUTHY", "-" if value is None else "S" + value, int(F.truthy(value)))
for opener, closer, text, start in (("{", "}", "#{a#{b}c}d", 1), ("(", ")", "#(a#(b)c)d", 1), ("{", "}", "#{a", 1), ("{", "}", "#{a}}", 1), ("{", "}", "x#{#}}", 2)):
emit("MC", opener, closer, text, start, F.match_close(text, start, opener, closer))
for text in ["a,b", "a#,b", "a,#{x,y},b", "a,#(x,y),b", "", ",", "a,", ",a", "a#{b,c}", "#,#,", "}a,b)", "a),b", "#x,y", "a#"]:
emit("SC", text, enc_list(F.split_commas(text)))
# --------------------------------------------------------------------------- layout
L = tai.MplexLayout
N = tai.MplexLayoutNode
class Pane:
def __init__(self, ident):
self.id = ident
self.x = self.y = 0
self.w = self.h = 1
self.last_active = 0.0
def tree_dump(node):
if node is None:
return "None"
if node.kind == "pane":
return "P%d(%d,%d,%d,%d)[%d,%d,%d,%d]" % (node.pane.id, node.x, node.y, node.w, node.h, node.pane.x, node.pane.y, node.pane.w, node.pane.h)
return "%s%s(%d,%d,%d,%d){%s}" % (node.kind, "r" if node.reverse else "", node.x, node.y, node.w, node.h, ",".join(tree_dump(c) for c in node.children))
def sync(root):
if root is None:
return
for leaf in root.leaves():
leaf.pane.x, leaf.pane.y, leaf.pane.w, leaf.pane.h = leaf.x, leaf.y, leaf.w, leaf.h
def check_links(node):
# parent pointers must match structure
for child in node.children:
assert child.parent is node
names_l = list(tai.MPLEX_LAYOUT_NAMES) + ["bogus-layout"]
for scenario in range(160):
emit("LBEGIN", scenario)
panes = [Pane(0)]
counter = 1
root = N("pane", pane=panes[0], w=80, h=24)
W, H = 80, 24
L.assign(root, 0, 0, W, H)
sync(root)
emit("LOP", "init", "", tree_dump(root))
for _ in range(rng.randint(15, 45)):
leaves = root.leaves()
roll = rng.random()
if roll < 0.28:
idx = rng.randrange(len(leaves))
kind = rng.choice(["lr", "tb"])
before = rng.random() < 0.5
size = rng.choice([None, None, 1, 2, 3, 5, 10, 30, 100, 0, -4])
arg = "%d %s %d %s" % (idx, kind, int(before), "N" if size is None else size)
newp = Pane(counter)
counter += 1
def run():
global root
new_root, fresh = L.split(root, leaves[idx], kind, before, newp, size)
root = new_root
panes.append(newp)
return tree_dump(root)
res = outcome(run)
emit("LOP", "split", arg, res)
if res.startswith("ERR"):
continue
L.assign(root, 0, 0, W, H)
sync(root)
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
elif roll < 0.42 and len(leaves) > 1:
idx = rng.randrange(len(leaves))
victim = leaves[idx]
root = L.remove(root, victim)
panes = [p for p in panes if p is not victim.pane] + []
emit("LOP", "remove", str(idx), tree_dump(root))
L.assign(root, 0, 0, W, H)
sync(root)
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
elif roll < 0.6:
idx = rng.randrange(len(leaves))
kind = rng.choice(["lr", "tb"])
delta = rng.randint(-12, 12)
prefer = rng.random() < 0.5
res = outcome(lambda: (L.resize_by(leaves[idx], kind, delta, prefer), tree_dump(root))[1])
emit("LOP", "resize_by", "%d %s %d %d" % (idx, kind, delta, int(prefer)), res)
L.assign(root, 0, 0, W, H)
sync(root)
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
elif roll < 0.72:
idx = rng.randrange(len(leaves))
kind = rng.choice(["lr", "tb"])
size = rng.randint(1, 60)
res = outcome(lambda: (L.resize_to(leaves[idx], kind, size), tree_dump(root))[1])
emit("LOP", "resize_to", "%d %s %d" % (idx, kind, size), res)
L.assign(root, 0, 0, W, H)
sync(root)
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
elif roll < 0.82:
W = rng.choice([1, 2, 3, 5, 8, 20, 40, 80, 120, 200, 233])
H = rng.choice([1, 2, 3, 4, 6, 12, 24, 40, 50, 77])
L.assign(root, 0, 0, W, H)
sync(root)
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
elif roll < 0.92:
for p in panes:
p.last_active = float(rng.randint(0, 5))
emit("LOP", "active", enc_list([int(p.last_active) for p in panes]), "")
for p in panes:
for d in "LRUD":
got = L.neighbour([leaf.pane for leaf in root.leaves()], p, d, W, H)
emit("LOP", "neighbour", "%d %s %d %d" % (p.id, d, W, H), "-" if got is None else got.pane.id if hasattr(got, "pane") else got.id)
else:
name = rng.choice(names_l)
count = rng.randint(1, 9)
mw = rng.choice([1, 5, 20, 80, 200])
mh = rng.choice([1, 5, 12, 24, 100])
pset = [Pane(counter + i) for i in range(count)]
counter += count
panes = pset
root = L.preset(name, pset, W, H, mw, mh)
emit("LOP", "preset", "%s %d %d %d %d %d" % (name, count, W, H, mw, mh), tree_dump(root))
L.assign(root, 0, 0, W, H)
sync(root)
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
for name in names_l:
for count in range(1, 10):
for (W, H) in [(1, 1), (2, 2), (3, 7), (10, 5), (20, 10), (80, 24), (80, 23), (81, 25), (120, 40), (200, 50), (233, 61)]:
for (mw, mh) in [(80, 24), (1, 1), (200, 100)]:
pset = [Pane(i) for i in range(count)]
root = L.preset(name, pset, W, H, mw, mh)
pre = tree_dump(root)
L.assign(root, 0, 0, W, H)
sync(root)
emit("LPRESET", "%s %d %d %d %d %d" % (name, count, W, H, mw, mh), pre, tree_dump(root))
for total in range(-2, 40):
for count in range(1, 9):
for first in (True, False):
emit("LSHARES", total, count, int(first), enc_list(L.shares(total, count, first)))
open(os.path.join(OUT, "core_cases.txt"), "w", encoding="utf-8", newline="\n").write("\n".join(lines) + "\n")
print("cases:", len(lines))
+538
View File
@@ -0,0 +1,538 @@
// Differential test of the Mplex core classes (args, command parser, style, options, config,
// format, layout) against tai.py. The expected values are produced by tests/mplex_core_gen.py
// from the real Python classes (build/core_cases.txt). Run from taicpp/:
// python3 -I tests/mplex_core_gen.py && build/mplex_core_test
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#include "mplex_args.h"
#include "mplex_commands.h"
#include "mplex_config.h"
#include "mplex_format.h"
#include "mplex_keys.h"
#include "mplex_layout.h"
#include "mplex_options.h"
#include "mplex_pane.h"
#include "mplex_py.h"
#include "mplex_style.h"
using namespace detz::mplex;
// ---- stand-ins for classes other forks own (only what the layout needs) ----------------
// MplexPane / MplexVtScreen construction is stubbed so this test links without the pty code.
MplexVtScreen::MplexVtScreen(int r, int c) : rows(r), cols(c) {}
MplexPane::MplexPane(int ident, int cols, int rows, int, const std::string&, const std::string&)
: id(ident), w(std::max(1, cols)), h(std::max(1, rows)), screen(std::max(1, rows), std::max(1, cols)), created(0) {}
MplexPane::~MplexPane() {}
// MplexCommands::resolve is another fork's; when its object is not linked this transcription
// (tai.py:15430) stands in so MplexConfig can be exercised.
__attribute__((weak)) std::string MplexCommands::resolve(const std::string& word) {
if (mplex_find_spec(word)) return word;
for (const auto& spec : mplex_command_specs())
for (const auto& alias : spec.aliases)
if (alias == word) return spec.name;
for (const auto& name : mplex_unsupported())
if (name == word) throw MplexError("command not supported by mplex: " + word);
std::vector<std::string> matches;
for (const auto& spec : mplex_command_specs())
if (spec.name.compare(0, word.size(), word) == 0) matches.push_back(spec.name);
if (matches.size() == 1) return matches[0];
if (matches.size() > 1) {
std::sort(matches.begin(), matches.end());
std::string list;
for (size_t i = 0; i < matches.size() && i < 5; ++i) list += (i ? ", " : "") + matches[i];
throw MplexError("ambiguous command: " + word + " (" + list + ")");
}
throw MplexError("unknown command: " + word);
}
namespace {
long failures = 0, checks = 0, known_deviations = 0;
// Documented deviation: MplexArgs::int_flag returns a 64 bit value; a Python int beyond that
// saturates (every consumer clamps it). Rewrites such expected values in the "ints" section.
std::string saturate_big_ints(const std::string& want) {
size_t cut = want.rfind('\x1e');
if (cut == std::string::npos) return want;
std::string head = want.substr(0, cut + 1), tail = want.substr(cut + 1), out;
for (size_t i = 0; i < tail.size();) {
size_t j = i;
if (tail[j] == '-') ++j;
size_t digits = j;
while (digits < tail.size() && tail[digits] >= '0' && tail[digits] <= '9') ++digits;
if (digits - j >= 19 && (j == 0 || tail[j - 1] != 'x') && i > 0 && tail[i - 1] == '=') {
++known_deviations;
out += tail[i] == '-' ? "-9223372036854775808" : "9223372036854775807";
i = digits;
} else {
out.push_back(tail[i]);
++i;
}
}
return head + out;
}
std::map<std::string, long> per_kind_checks, per_kind_failures;
void check(const std::string& kind, const std::string& label, const std::string& got, const std::string& want) {
++checks;
++per_kind_checks[kind];
if (got == want) return;
++failures;
++per_kind_failures[kind];
if (per_kind_failures[kind] <= 6) {
std::cerr << "MISMATCH [" << kind << "] " << label << "\n got : " << got << "\n want: " << want << "\n";
}
}
std::string unesc(const std::string& s) {
std::string out;
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] != '\\' || i + 1 >= s.size()) {
out.push_back(s[i]);
continue;
}
char n = s[++i];
if (n == 'n') out.push_back('\n');
else if (n == 't') out.push_back('\t');
else if (n == 'r') out.push_back('\r');
else if (n == '\\') out.push_back('\\');
else if (n == 'x' && i + 2 < s.size()) {
out.push_back(static_cast<char>(std::stoi(s.substr(i + 1, 2), nullptr, 16)));
i += 2;
} else {
out.push_back('\\');
out.push_back(n);
}
}
return out;
}
std::vector<std::string> fields_of(const std::string& line) {
std::vector<std::string> out;
size_t start = 0;
while (true) {
size_t tab = line.find('\t', start);
if (tab == std::string::npos) {
out.push_back(unesc(line.substr(start)));
break;
}
out.push_back(unesc(line.substr(start, tab - start)));
start = tab + 1;
}
return out;
}
std::string enc_list(const std::vector<std::string>& items) {
std::string out = std::to_string(items.size());
for (const auto& item : items) out += "\x1f" + item;
return out;
}
std::vector<std::string> dec_list(const std::string& text) {
std::vector<std::string> out;
size_t start = 0;
bool first = true;
while (true) {
size_t sep = text.find('\x1f', start);
std::string piece = text.substr(start, sep == std::string::npos ? std::string::npos : sep - start);
if (first) first = false;
else out.push_back(piece);
if (sep == std::string::npos) break;
start = sep + 1;
}
return out;
}
std::vector<std::string> split_on(const std::string& text, char sep) {
std::vector<std::string> out;
size_t start = 0;
while (true) {
size_t at = text.find(sep, start);
if (at == std::string::npos) {
out.push_back(text.substr(start));
break;
}
out.push_back(text.substr(start, at - start));
start = at + 1;
}
return out;
}
template <typename F>
std::string outcome(F func) {
try {
return "OK:" + func();
} catch (const MplexError& exc) {
return std::string("ERR:") + exc.what();
} catch (const ValueError&) {
return "EXC:ValueError";
} catch (const std::out_of_range&) {
return "EXC:KeyError";
} catch (const std::exception& exc) {
return std::string("EXC:?") + exc.what();
}
}
std::string show_val(const OptValue& v) {
if (v.is_bool()) return v.b ? "bool:True" : "bool:False";
if (v.is_int()) return "int:" + std::to_string(v.i);
return "str:" + v.s;
}
std::string frag_or_dash(Color c) { return c ? color_sgr(c) : "-"; }
Color frag_color(const std::string& s) { return s == "-" ? 0 : color_from_sgr(s); }
// ---- args ------------------------------------------------------------------------------
std::string args_dump(const MplexArgs& args) {
std::vector<std::string> parts, gets, ints;
for (const auto& [key, values] : args.flags) {
std::vector<std::string> vals;
for (const auto& v : values) vals.push_back(v.is_true ? "T" : "S" + v.text);
parts.push_back(std::string(1, key) + ":" + enc_list(vals));
}
for (const auto& [key, values] : args.flags) {
(void)values;
auto got = args.get(key);
const auto& last = args.flags.at(key).back();
gets.push_back(std::string(1, key) + "=" + (last.is_true ? "True" : py::repr(*got)));
std::string all = "[";
bool first = true;
for (const auto& v : args.flags.at(key)) {
if (!first) all += ", ";
first = false;
all += v.is_true ? "True" : py::repr(v.text);
}
all += "]";
gets.push_back(std::string(1, key) + "*" + all);
try {
auto n = args.int_flag(key, 7);
ints.push_back(std::string(1, key) + "=" + std::to_string(*n));
} catch (const MplexError& exc) {
ints.push_back(std::string(1, key) + "=ERR " + exc.what());
}
}
return enc_list(parts) + "\x1e" + enc_list(args.positional) + "\x1e" + enc_list(gets) + "\x1e" + enc_list(ints);
}
// ---- config ------------------------------------------------------------------------------
std::string cfg_dump(const MplexConfig& config) {
std::vector<std::string> opts;
for (const auto& name : config.options.value_order) opts.push_back(name + "=" + show_val(config.options.values.at(name)));
std::vector<std::string> tabs;
for (const auto& [tname, table] : config.tables.items()) {
std::vector<std::string> rows;
for (const auto& [key, b] : table.items())
rows.push_back(key + "\x1d" + b.command + "\x1d" + (b.repeat ? "1" : "0") + "\x1d" + b.note);
tabs.push_back(tname + "\x1c" + enc_list(rows));
}
return enc_list(opts) + "\x1e" + enc_list(tabs) + "\x1e" + enc_list(config.errors) + "\x1e" + std::to_string(config.skipped) +
"\x1e" + config.path;
}
// ---- layout ------------------------------------------------------------------------------
using PanePtr = std::shared_ptr<MplexPane>;
std::string tree_dump(const MplexLayoutNode::Ptr& node) {
if (!node) return "None";
if (node->kind == "pane") {
const auto& p = node->pane;
return "P" + std::to_string(p->id) + "(" + std::to_string(node->x) + "," + std::to_string(node->y) + "," +
std::to_string(node->w) + "," + std::to_string(node->h) + ")[" + std::to_string(p->x) + "," + std::to_string(p->y) +
"," + std::to_string(p->w) + "," + std::to_string(p->h) + "]";
}
std::string out = node->kind + (node->reverse ? "r" : "") + "(" + std::to_string(node->x) + "," + std::to_string(node->y) +
"," + std::to_string(node->w) + "," + std::to_string(node->h) + "){";
for (size_t i = 0; i < node->children.size(); ++i) {
if (i) out += ",";
out += tree_dump(node->children[i]);
}
return out + "}";
}
void sync_panes(const MplexLayoutNode::Ptr& root) {
if (!root) return;
for (auto& leaf : root->leaves()) {
leaf->pane->x = leaf->x;
leaf->pane->y = leaf->y;
leaf->pane->w = leaf->w;
leaf->pane->h = leaf->h;
}
}
struct LayoutState {
std::vector<PanePtr> panes;
MplexLayoutNode::Ptr root;
int W = 80, H = 24;
int counter = 1;
};
PanePtr new_pane(int ident) { return std::make_shared<MplexPane>(ident, 1, 1, 0, "", "0"); }
} // namespace
int main(int argc, char** argv) {
const std::string path = argc > 1 ? argv[1] : "build/core_cases.txt";
setenv("TZ", "UTC", 1);
tzset();
setenv("VISUAL", "nvi", 1);
unsetenv("EDITOR");
std::ifstream in(path);
if (!in) {
std::cerr << "cannot open " << path << " (run tests/mplex_core_gen.py first)\n";
return 2;
}
std::map<std::string, std::string> vars;
auto resolver = [&](const std::string& name) -> std::optional<std::string> {
if (!name.empty() && name[0] == '(') return "SH<" + name.substr(1) + ">";
auto found = vars.find(name);
if (found == vars.end()) return std::nullopt;
return found->second;
};
MplexOptions g, s(&g), w(&s);
std::vector<MplexOptions*> levels = {&g, &s, &w};
LayoutState layout;
std::unique_ptr<MplexConfig> fresh;
std::string line;
long lineno = 0;
while (std::getline(in, line)) {
++lineno;
std::vector<std::string> f = fields_of(line);
const std::string& kind = f[0];
std::string label = "line " + std::to_string(lineno) + " " + (f.size() > 1 ? f[1] : "");
if (kind == "P") {
check(kind, label, outcome([&] {
auto commands = MplexCommandParser::parse(f[1]);
std::string out;
for (size_t i = 0; i < commands.size(); ++i) {
if (i) out += "\x1e";
for (size_t j = 0; j < commands[i].size(); ++j) {
if (j) out += "\x1f";
out += commands[i][j];
}
}
return out + "|" + std::to_string(commands.size());
}),
f[2]);
} else if (kind == "Q") {
check(kind, label, MplexCommandParser::quote(f[1]), f[2]);
} else if (kind == "A") {
check(kind, label + " " + f[5], outcome([&] {
return args_dump(MplexArgs::parse(f[1], f[2], dec_list(f[5]), std::stoi(f[3]), std::stoi(f[4])));
}),
saturate_big_ints(f[6]));
} else if (kind == "C") {
std::string want = f[3];
if (want.find(";-") != std::string::npos) { // documented deviation: negative "#-f0000" components
++known_deviations;
want = "ERR:bad colour: " + py::lower(py::strip(f[1]));
}
check(kind, label, outcome([&] {
Color c = MplexStyle::color(f[1], f[2] == "1");
return c ? "'" + color_sgr(c) + "'" : std::string("None");
}),
want);
} else if (kind == "S") {
auto decode_style = [](const std::string& text) {
auto parts = split_on(text, '\x1e');
auto pair = dec_list(parts[0]);
return Style{frag_color(pair[0]), frag_color(pair[1]), std::stoi(parts[1])};
};
Style base = decode_style(f[2]);
std::optional<Style> reset;
if (f[3] != "-") reset = decode_style(f[3]);
check(kind, label + " base=" + f[2], outcome([&] {
Style r = MplexStyle::parse(f[1], base, reset);
return frag_or_dash(r.fg) + "|" + frag_or_dash(r.bg) + "|" + std::to_string(r.attr);
}),
f[4]);
} else if (kind == "G") {
check(kind, label, MplexStyle::sgr(frag_color(f[1]), frag_color(f[2]), std::stoi(f[3])), f[4]);
} else if (kind == "O") {
check(kind, label + " " + f[2], outcome([&] { return show_val(MplexOptions::check(f[1], f[2])); }), f[3]);
} else if (kind == "D") {
std::vector<std::string> names, defaults;
for (const auto& d : MplexOptions::defs()) {
names.push_back(d.name);
defaults.push_back(show_val(d.def) + "~" + d.kind);
}
check(kind, "names", enc_list(names), f[1]);
check(kind, "defaults", enc_list(defaults), f[2]);
check(kind, "word separators", MplexOptions::WORD_SEPARATORS, f[3]);
} else if (kind == "OBEGIN") {
g = MplexOptions();
s = MplexOptions(&g);
w = MplexOptions(&s);
} else if (kind == "OOP") {
MplexOptions& obj = *levels[static_cast<size_t>(std::stoi(f[1]))];
const std::string& op = f[2];
const std::string& name = f[3];
std::string res;
if (op == "set" || op == "append") res = outcome([&] { return show_val(obj.set(name, f[4], op == "append")); });
else if (op == "get") res = outcome([&] { return show_val(obj.get(name)); });
else if (op == "toggle") res = outcome([&] { obj.toggle(name); return std::string(); });
else if (op == "unset") res = outcome([&] { obj.unset(name); return std::string(); });
else if (op == "is_set") res = std::string("OK:") + (obj.is_set(name) ? "True" : "False");
else res = outcome([&] { return MplexOptions::show(obj.get(name)); });
std::string dump;
for (size_t i = 0; i < levels.size(); ++i) {
std::vector<std::string> items;
for (const auto& n : levels[i]->value_order) items.push_back(n + "=" + show_val(levels[i]->values.at(n)));
if (i) dump += "\x1e";
dump += enc_list(items);
}
check(kind, label + " " + op, res, f[5]);
check("OOPDUMP", label + " " + op, dump, f[6]);
} else if (kind == "K") {
if (f[1] == "-") {
MplexConfig config;
check(kind, "fresh", cfg_dump(config), f[2]);
fresh = std::make_unique<MplexConfig>();
} else {
auto config = MplexConfig::load(f[1]);
check(kind, label, cfg_dump(*config), f[2]);
}
} else if (kind == "DEFAULTPATH") {
check(kind, label, MplexConfig::default_path("/home/x/.tai") + "|" + MplexConfig::default_path("rel"),
f[1] + "|" + f[2]);
} else if (kind == "LOOKUP") {
std::string got;
for (auto pair : std::vector<std::pair<std::string, std::string>>{{"prefix", "C-b"}, {"prefix", "nope"}, {"root", "C-b"},
{"nonexistent", "x"}, {"copy-mode", "Escape"}})
got += pair.first + "/" + pair.second + "=" + (fresh->lookup(pair.first, pair.second) ? "True" : "False") + ";";
check(kind, label, got, f[1]);
} else if (kind == "UNBIND") {
check(kind, label, fresh->unbind(f[1], f[2]) ? "1" : "0", f[3]);
} else if (kind == "V") {
vars[f[1]] = f[2];
} else if (kind == "F") {
check(kind, label, outcome([&] { return MplexFormat::expand(f[1], resolver); }), f[2]);
} else if (kind == "W") {
check(kind, label, outcome([&] { return MplexFormat::with_time(f[1], std::stod(f[2])); }), f[3]);
} else if (kind == "X") {
check(kind, label, MplexFormat::strip_styles(f[1]), f[2]);
} else if (kind == "TRUTHY") {
std::optional<std::string> v;
if (f[1] != "-") v = f[1].substr(1);
check(kind, label, MplexFormat::truthy(v) ? "1" : "0", f[2]);
} else if (kind == "MC") {
check(kind, label, std::to_string(MplexFormat::match_close(f[3], std::stol(f[4]), f[1][0], f[2][0])), f[5]);
} else if (kind == "SC") {
check(kind, label, enc_list(MplexFormat::split_commas(f[1])), f[2]);
} else if (kind == "LBEGIN") {
layout = LayoutState();
layout.panes = {new_pane(0)};
layout.root = std::make_shared<MplexLayoutNode>("pane", layout.panes[0], 80, 24);
MplexLayout::assign(layout.root, 0, 0, 80, 24);
sync_panes(layout.root);
} else if (kind == "LOP") {
const std::string& op = f[1];
auto& st = layout;
auto leaves_now = [&] { return st.root->leaves(); };
if (op == "init") {
check("LOP:init", label + " init", tree_dump(st.root), f[3]);
} else if (op == "split") {
auto parts = split_on(f[2], ' ');
size_t idx = std::stoul(parts[0]);
int size = parts[3] == "N" ? -1 : std::stoi(parts[3]);
auto leaves = leaves_now();
PanePtr np = new_pane(st.counter++);
std::string res = outcome([&] {
auto [nr, fresh_leaf] = MplexLayout::split(st.root, leaves[idx], parts[1], parts[2] == "1", np, size);
(void)fresh_leaf;
st.root = nr;
st.panes.push_back(np);
return tree_dump(st.root);
});
check("LOP:split", label + " split " + f[2], res, f[3]);
} else if (op == "remove") {
size_t idx = std::stoul(f[2]);
auto leaves = leaves_now();
PanePtr victim = leaves[idx]->pane;
st.root = MplexLayout::remove(st.root, leaves[idx]);
std::vector<PanePtr> kept;
for (auto& p : st.panes)
if (p != victim) kept.push_back(p);
st.panes = kept;
check("LOP:remove", label + " remove", tree_dump(st.root), f[3]);
} else if (op == "resize_by") {
auto parts = split_on(f[2], ' ');
auto leaves = leaves_now();
std::string res = outcome([&] {
MplexLayout::resize_by(leaves[std::stoul(parts[0])], parts[1], std::stoi(parts[2]), parts[3] == "1");
return tree_dump(st.root);
});
check("LOP:resize_by", label + " resize_by " + f[2], res, f[3]);
} else if (op == "resize_to") {
auto parts = split_on(f[2], ' ');
auto leaves = leaves_now();
std::string res = outcome([&] {
MplexLayout::resize_to(leaves[std::stoul(parts[0])], parts[1], std::stoi(parts[2]));
return tree_dump(st.root);
});
check("LOP:resize_to", label + " resize_to " + f[2], res, f[3]);
} else if (op == "assign") {
auto parts = split_on(f[2], ' ');
st.W = std::stoi(parts[0]);
st.H = std::stoi(parts[1]);
MplexLayout::assign(st.root, 0, 0, st.W, st.H);
sync_panes(st.root);
check("LOP:assign", label + " assign " + f[2], tree_dump(st.root), f[3]);
} else if (op == "active") {
auto values = dec_list(f[2]);
for (size_t i = 0; i < values.size() && i < st.panes.size(); ++i) st.panes[i]->last_active = std::stod(values[i]);
} else if (op == "neighbour") {
auto parts = split_on(f[2], ' ');
int id = std::stoi(parts[0]);
PanePtr current;
for (auto& p : st.panes)
if (p->id == id) current = p;
std::vector<PanePtr> panes;
for (auto& leaf : st.root->leaves()) panes.push_back(leaf->pane);
PanePtr got = MplexLayout::neighbour(panes, current, parts[1][0], std::stoi(parts[2]), std::stoi(parts[3]));
check("LOP:neighbour", label + " neighbour " + f[2], got ? std::to_string(got->id) : "-", f[3]);
} else if (op == "preset") {
auto parts = split_on(f[2], ' ');
int count = std::stoi(parts[1]);
std::vector<PanePtr> pset;
for (int i = 0; i < count; ++i) pset.push_back(new_pane(st.counter + i));
st.counter += count;
st.panes = pset;
st.root = MplexLayout::preset(parts[0], pset, std::stoi(parts[2]), std::stoi(parts[3]), std::stoi(parts[4]),
std::stoi(parts[5]));
check("LOP:preset", label + " preset " + f[2], tree_dump(st.root), f[3]);
}
} else if (kind == "LPRESET") {
auto parts = split_on(f[1], ' ');
int count = std::stoi(parts[1]);
std::vector<PanePtr> pset;
for (int i = 0; i < count; ++i) pset.push_back(new_pane(i));
auto root = MplexLayout::preset(parts[0], pset, std::stoi(parts[2]), std::stoi(parts[3]), std::stoi(parts[4]),
std::stoi(parts[5]));
check(kind, label + " pre " + f[1], tree_dump(root), f[2]);
MplexLayout::assign(root, 0, 0, std::stoi(parts[2]), std::stoi(parts[3]));
sync_panes(root);
check(kind, label + " post " + f[1], tree_dump(root), f[3]);
} else if (kind == "LSHARES") {
check(kind, label, enc_list([&] {
std::vector<std::string> out;
for (int v : MplexLayout::shares(std::stoi(f[1]), std::stoi(f[2]), f[3] == "1")) out.push_back(std::to_string(v));
return out;
}()),
f[4]);
} else {
std::cerr << "unknown case kind " << kind << " at line " << lineno << "\n";
++failures;
}
}
for (const auto& [kind, count] : per_kind_checks)
std::cout << kind << ": " << count << " checks, " << per_kind_failures[kind] << " failures\n";
std::cout << "known deviations applied (int64 saturation, negative #-hex colour): " << known_deviations << "\n";
std::cout << "TOTAL " << checks << " checks, " << failures << " failures\n";
return failures ? 1 : 0;
}