From cf72c9b39989d4247b8334c4e00d56df7edc7339 Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:48:03 +0000 Subject: [PATCH] fix: support NomadNet grayscale colors --- lib/tdeck_ui/UI/LXMF/NomadNetDocument.cpp | 34 ++++++++++++--- tests/native/test_app_launcher_nomadnet.cpp | 46 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetDocument.cpp b/lib/tdeck_ui/UI/LXMF/NomadNetDocument.cpp index e54e87c2..56667832 100644 --- a/lib/tdeck_ui/UI/LXMF/NomadNetDocument.cpp +++ b/lib/tdeck_ui/UI/LXMF/NomadNetDocument.cpp @@ -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(value[1])) || + !std::isdigit(static_cast(value[2]))) return false; + const uint32_t percent = static_cast(value[1] - '0') * 10 + + static_cast(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(levels[i]) + levels[i + 1] + 1) / 2; + if (scaled < midpoint) { + gray = levels[i]; + break; + } + } + result = static_cast(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; } diff --git a/tests/native/test_app_launcher_nomadnet.cpp b/tests/native/test_app_launcher_nomadnet.cpp index 48b482eb..140da631 100644 --- a/tests/native/test_app_launcher_nomadnet.cpp +++ b/tests/native/test_app_launcher_nomadnet.cpp @@ -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('0' + percent / 10); + token[2] = static_cast('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;