diff --git a/taicpp/src/mplex/mplex_args.cpp b/taicpp/src/mplex/mplex_args.cpp new file mode 100644 index 0000000..b3ef848 --- /dev/null +++ b/taicpp/src/mplex/mplex_args.cpp @@ -0,0 +1,231 @@ +// MplexArgs and MplexCommandParser: transcription of tai.py:10472-10650. +#include "mplex_args.h" + +#include +#include + +#include "mplex_py.h" + +namespace detz::mplex { + +std::optional 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 MplexArgs::all(char flag) const { + std::vector 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 MplexArgs::int_flag(char flag, std::optional 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& words, + int min_args, int max_args) { + std::set takes; + std::set 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> flags; + std::vector 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(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(positional.size()) < min_args || (max_args >= 0 && static_cast(positional.size()) > max_args)) + throw MplexError("command " + name + ": wrong number of arguments"); + return MplexArgs(std::move(flags), std::move(positional)); +} + +std::vector> 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> commands; + std::vector 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(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 diff --git a/taicpp/src/mplex/mplex_config.cpp b/taicpp/src/mplex/mplex_config.cpp new file mode 100644 index 0000000..48e3c77 --- /dev/null +++ b/taicpp/src/mplex/mplex_config.cpp @@ -0,0 +1,301 @@ +// MplexBinding / MplexConfig: transcription of tai.py:10850-10985. +#include "mplex_config.h" + +#include + +#include +#include +#include +#include +#include + +#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(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(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(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::load(const std::string& path) { + auto config = std::make_shared(); + 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 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 diff --git a/taicpp/src/mplex/mplex_format.cpp b/taicpp/src/mplex/mplex_format.cpp new file mode 100644 index 0000000..5b2efa1 --- /dev/null +++ b/taicpp/src/mplex/mplex_format.cpp @@ -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 +#include +#include + +#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& 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(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(std::ldexp(frac, 53)); + int shift = exp - 53; // mag = mant * 2^shift, shift > 0 here + std::vector digits; // little endian base 10 + while (mant) { + digits.push_back(static_cast(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('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(text.size()); + while (pos < n) { + char ch = text[static_cast(pos)]; + if (ch == '#' && pos + 1 < n) { + if (text[static_cast(pos + 1)] == opener) ++depth; + pos += 2; + continue; + } + if (ch == closer) { + --depth; + if (depth == 0) return pos; + } + ++pos; + } + return -1; +} + +std::vector MplexFormat::split_commas(const std::string& text) { + std::vector 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(pos + 1), '{', '}'); + if (end < 0) { + out.push_back(ch); + ++pos; + } else { + out += evaluate(text.substr(pos + 2, static_cast(end) - (pos + 2)), resolve, depth); + pos = static_cast(end) + 1; + } + } else if (nxt == '(') { + long end = match_close(text, static_cast(pos + 1), '(', ')'); + if (end < 0) { + out.push_back(ch); + ++pos; + } else { + out += or_empty(resolve("(" + text.substr(pos + 2, static_cast(end) - (pos + 2)))); + pos = static_cast(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& 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 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(colon)); + std::string rest = inner.substr(static_cast(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(colon); +} + +std::string MplexFormat::apply_modifiers(const std::string& mods, const std::string& rest, const Resolver& resolve, + int depth) { + std::vector specs; + for (const auto& item : py::split(mods, ";")) + if (!item.empty()) specs.push_back(item); + std::optional text; + for (const std::string& spec : specs) { + if (spec == "==" || spec == "!=" || spec == "<" || spec == ">" || spec == "<=" || spec == ">=" || spec == "&&" || + spec == "||" || py::startswith(spec, "e|")) { + std::vector 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 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(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 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(py::len(text)); + if (limit >= 0 && length > limit) return py::slice(text, 0, static_cast(limit)) + marker; + if (limit < 0 && length > -limit) return marker + py::slice(text, static_cast(limit), 1L << 40); + return text; + } + if (py::startswith(spec, "s/")) { + std::vector 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 diff --git a/taicpp/src/mplex/mplex_layout.cpp b/taicpp/src/mplex/mplex_layout.cpp new file mode 100644 index 0000000..1ef057b --- /dev/null +++ b/taicpp/src/mplex/mplex_layout.cpp @@ -0,0 +1,340 @@ +// MplexLayoutNode and MplexLayout: transcription of tai.py:11261-11546. +#include "mplex_layout.h" + +#include +#include + +#include "mplex_pane.h" + +namespace detz::mplex { + +namespace { + +using Node = MplexLayoutNode::Ptr; + +long index_of(const std::vector& kids, const Node& node) { + for (size_t i = 0; i < kids.size(); ++i) + if (kids[i] == node) return static_cast(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 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::leaves() { + if (kind == "pane") return {shared_from_this()}; + std::vector 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& kids = node->children; + int count = static_cast(kids.size()); + const std::string kind = node->kind; + int total = (kind == "lr" ? w : h) - (count - 1); + std::vector sizes; + if (total < count) { + sizes.assign(static_cast(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(static_cast(s) * total) / static_cast(weight); + s = std::max(1, static_cast(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(index)] += 1; + diff -= 1; + moved = true; + } else if (sizes[static_cast(index)] > 1) { + sizes[static_cast(index)] -= 1; + diff += 1; + moved = true; + } + if (diff == 0) break; + } + if (!moved) break; + } + } + std::vector> order; + for (int i = 0; i < count; ++i) order.emplace_back(kids[static_cast(i)], sizes[static_cast(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::split(Node root, const Node& leaf, const std::string& kind, + bool before, + const std::shared_ptr& 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("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(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(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(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 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(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& kids = parent->children; + long index = index_of(kids, node); + bool use_after = prefer_after ? (index < static_cast(kids.size()) - 1) : (index == 0); + long first = use_after ? index : index - 1; + Node a = kids[static_cast(first)], b = kids[static_cast(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 MplexLayout::shares(int total, int count, bool spare_first) { + int room = total - (count - 1); + int base, extra; + divmod(room, count, base, extra); + std::vector sizes(static_cast(count), base); + if (spare_first) { + for (int i = 0; i < extra; ++i) sizes[static_cast(i)] += 1; + } else { + for (int i = count - extra; i < count; ++i) sizes[static_cast(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(parent->children.size()) - 1; + resize_by(leaf, kind, has_next ? change : -change, true); +} + +MplexLayout::Node MplexLayout::preset(const std::string& name, const std::vector>& panes, + int width, int height, int main_width, int main_height) { + int count = static_cast(panes.size()); + std::vector leaves; + for (const auto& pane : panes) leaves.push_back(std::make_shared("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& items, int w, int h, bool spare_first = true) { + Node node = std::make_shared(kind, nullptr, w, h); + for (const auto& item : items) node->adopt(item); + std::vector sizes = shares(kind == "lr" ? w : h, static_cast(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 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 lines; + for (int start = 0; start < count; start += columns) { + std::vector 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 MplexLayout::neighbour(const std::vector>& panes, + const std::shared_ptr& 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& 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> 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 diff --git a/taicpp/src/mplex/mplex_options.cpp b/taicpp/src/mplex/mplex_options.cpp new file mode 100644 index 0000000..feb1308 --- /dev/null +++ b/taicpp/src/mplex/mplex_options.cpp @@ -0,0 +1,195 @@ +// MplexOptions: transcription of tai.py:10723-10848. +#include "mplex_options.h" + +#include +#include + +#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& MplexOptions::defs() { + static const std::vector 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 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 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(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 diff --git a/taicpp/src/mplex/mplex_style.cpp b/taicpp/src/mplex/mplex_style.cpp new file mode 100644 index 0000000..c04abb5 --- /dev/null +++ b/taicpp/src/mplex/mplex_style.cpp @@ -0,0 +1,145 @@ +// MplexStyle: transcription of tai.py:10653-10720. +#include "mplex_style.h" + +#include + +#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