fix: support NomadNet grayscale colors

This commit is contained in:
torlando-agent[bot]
2026-08-16 01:25:07 +00:00
parent 97e3e8b09a
commit cf72c9b399
2 changed files with 75 additions and 5 deletions
+29 -5
View File
@@ -59,7 +59,31 @@ bool first_codepoint(const std::string& value, std::size_t offset,
return true;
}
bool color(const std::string& value, uint32_t& result) {
bool parse_micron_color(const std::string& value, uint32_t& result) {
if (value.size() == 3 && value[0] == 'g') {
if (!std::isdigit(static_cast<unsigned char>(value[1])) ||
!std::isdigit(static_cast<unsigned char>(value[2]))) return false;
const uint32_t percent = static_cast<uint32_t>(value[1] - '0') * 10 +
static_cast<uint32_t>(value[2] - '0');
// Urwid first scales gNN to 0..255, then chooses the nearest xterm
// grayscale ramp entry. Preserve that quantization before RGB565.
const uint32_t scaled = (percent * 255 * 2 + 100) / 200;
static constexpr uint8_t levels[] = {
0, 8, 18, 28, 38, 48, 58, 68, 78, 88, 98, 108, 118,
128, 132, 148, 158, 168, 178, 188, 198, 208, 218, 228, 238, 255,
};
uint8_t gray = levels[sizeof(levels) - 1];
for (std::size_t i = 0; i + 1 < sizeof(levels); ++i) {
const uint32_t midpoint =
(static_cast<uint32_t>(levels[i]) + levels[i + 1] + 1) / 2;
if (scaled < midpoint) {
gray = levels[i];
break;
}
}
result = static_cast<uint32_t>(gray) * 0x010101;
return true;
}
if (value.size() != 3 && value.size() != 6) return false;
if (!std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isxdigit(c); })) return false;
char* end = nullptr;
@@ -127,7 +151,7 @@ void parse_inline(Document& doc, Block& block, const std::string& line, Style& s
if (i < line.size() && line[i] == 'T') { ++i; count = 6; }
if (i + count <= line.size()) {
uint32_t value = 0;
if (color(line.substr(i, count), value)) {
if (parse_micron_color(line.substr(i, count), value)) {
style.has_foreground = true;
style.foreground = value;
i += count;
@@ -138,7 +162,7 @@ void parse_inline(Document& doc, Block& block, const std::string& line, Style& s
if (i < line.size() && line[i] == 'T') { ++i; count = 6; }
if (i + count <= line.size()) {
uint32_t value = 0;
if (color(line.substr(i, count), value)) {
if (parse_micron_color(line.substr(i, count), value)) {
style.has_background = true;
style.background = value;
i += count;
@@ -236,13 +260,13 @@ Document DocumentParser::parse(const char* source, std::size_t size) const {
}
if (line.rfind("#!bg=", 0) == 0) {
uint32_t value = 0;
if (color(line.substr(5), value)) { doc.has_background = true; doc.background = value; }
if (parse_micron_color(line.substr(5), value)) { doc.has_background = true; doc.background = value; }
else doc.malformed = true;
continue;
}
if (line.rfind("#!fg=", 0) == 0) {
uint32_t value = 0;
if (color(line.substr(5), value)) { doc.has_foreground = true; doc.foreground = value; }
if (parse_micron_color(line.substr(5), value)) { doc.has_foreground = true; doc.foreground = value; }
else doc.malformed = true;
continue;
}
@@ -231,6 +231,52 @@ int main(int argc, char** argv) {
check("FT inline truecolor parses", saw_truecolor_foreground);
check("BT inline truecolor parses", saw_truecolor_background);
// Byte-exact Urwid 2.6.16 AttrSpec g00..g99 truecolor expansion.
static constexpr uint32_t grayscale_rgb[100] = {
0x000000, 0x000000, 0x080808, 0x080808, 0x080808, 0x121212, 0x121212, 0x121212, 0x121212, 0x1c1c1c,
0x1c1c1c, 0x1c1c1c, 0x1c1c1c, 0x262626, 0x262626, 0x262626, 0x262626, 0x303030, 0x303030, 0x303030,
0x303030, 0x3a3a3a, 0x3a3a3a, 0x3a3a3a, 0x3a3a3a, 0x444444, 0x444444, 0x444444, 0x444444, 0x4e4e4e,
0x4e4e4e, 0x4e4e4e, 0x4e4e4e, 0x585858, 0x585858, 0x585858, 0x585858, 0x626262, 0x626262, 0x626262,
0x626262, 0x6c6c6c, 0x6c6c6c, 0x6c6c6c, 0x6c6c6c, 0x767676, 0x767676, 0x767676, 0x767676, 0x808080,
0x808080, 0x848484, 0x848484, 0x848484, 0x848484, 0x949494, 0x949494, 0x949494, 0x949494, 0x949494,
0x9e9e9e, 0x9e9e9e, 0x9e9e9e, 0x9e9e9e, 0xa8a8a8, 0xa8a8a8, 0xa8a8a8, 0xa8a8a8, 0xb2b2b2, 0xb2b2b2,
0xb2b2b2, 0xb2b2b2, 0xbcbcbc, 0xbcbcbc, 0xbcbcbc, 0xbcbcbc, 0xc6c6c6, 0xc6c6c6, 0xc6c6c6, 0xc6c6c6,
0xd0d0d0, 0xd0d0d0, 0xd0d0d0, 0xd0d0d0, 0xdadada, 0xdadada, 0xdadada, 0xdadada, 0xe4e4e4, 0xe4e4e4,
0xe4e4e4, 0xe4e4e4, 0xeeeeee, 0xeeeeee, 0xeeeeee, 0xeeeeee, 0xeeeeee, 0xffffff, 0xffffff, 0xffffff,
};
for (std::size_t percent = 0; percent < 100; ++percent) {
std::string token = "g00";
token[1] = static_cast<char>('0' + percent / 10);
token[2] = static_cast<char>('0' + percent % 10);
const auto foreground_page = parser.parse(std::string("#!fg=") + token + "\ntext");
const auto background_page = parser.parse(std::string("#!bg=") + token + "\ntext");
const std::string name = std::string("page grayscale matches Urwid for ") + token;
check(name.c_str(),
foreground_page.has_foreground && foreground_page.foreground == grayscale_rgb[percent] &&
background_page.has_background && background_page.background == grayscale_rgb[percent] &&
!foreground_page.malformed && !background_page.malformed);
}
auto inline_grayscale = parser.parse("`Fg02dark`f `Bg51mid`b `Fg99light`f");
bool saw_dark_gray = false;
bool saw_mid_gray_background = false;
bool saw_light_gray = false;
for (const auto& run : inline_grayscale.blocks.front().runs) {
if (run.text == "dark")
saw_dark_gray = run.has_foreground && run.foreground == 0x080808;
else if (run.text == "mid")
saw_mid_gray_background = run.has_background && run.background == 0x848484;
else if (run.text == "light")
saw_light_gray = run.has_foreground && run.foreground == 0xffffff;
}
check("inline grayscale foreground and background match Urwid",
saw_dark_gray && saw_mid_gray_background && saw_light_gray);
for (const char* invalid : {"g0", "g100", "G50", "gx0", "g0x"}) {
const auto malformed_grayscale = parser.parse(std::string("#!fg=") + invalid + "\ntext");
const std::string name = std::string("malformed grayscale is rejected: ") + invalid;
check(name.c_str(),
malformed_grayscale.malformed && !malformed_grayscale.has_foreground);
}
CompactPage::RunRecord light_background_run{};
light_background_run.style = CompactPage::HAS_BACKGROUND;
light_background_run.background = 0xe8b4f0;