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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user