From 39daca9d22ca252f5506c7933959768ce1ec12a6 Mon Sep 17 00:00:00 2001 From: BareMetal Date: Tue, 6 Oct 2026 21:08:10 -0600 Subject: [PATCH 1/2] feat(xtext): render \x04 hex color codes in chat text --- src/common/meson.build | 14 ++++ src/common/tests/test-hexcolor.c | 116 +++++++++++++++++++++++++++++++ src/common/util.c | 44 ++++++++++++ src/common/util.h | 1 + src/fe-gtk/xtext.c | 67 +++++++++++++++++- src/fe-gtk/xtext.h | 5 ++ 6 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 src/common/tests/test-hexcolor.c diff --git a/src/common/meson.build b/src/common/meson.build index 53e7643a..f92c6990 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -197,3 +197,17 @@ test('Emoji Data Tests', emoji_data_tests, protocol: 'tap', timeout: 120, ) + +hexcolor_tests = executable('hexcolor_tests', + [ + 'tests/test-hexcolor.c', + 'util.c', + ], + include_directories: [config_h_include, include_directories('.')], + dependencies: [libgio_dep] + (libssl_dep.found() ? [libssl_dep] : []), +) + +test('Hex Color Tests', hexcolor_tests, + protocol: 'tap', + timeout: 120, +) diff --git a/src/common/tests/test-hexcolor.c b/src/common/tests/test-hexcolor.c new file mode 100644 index 00000000..d474249b --- /dev/null +++ b/src/common/tests/test-hexcolor.c @@ -0,0 +1,116 @@ +/* ZoiteChat + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include + +#include + +#include "../util.h" + +static void +test_parse_foreground (void) +{ + guint32 fg = 0, bg = 0xDEAD; + int has_bg = TRUE; + + g_assert_cmpint (hexcolor_parse ("FF8800text", 10, &fg, &bg, &has_bg), ==, 6); + g_assert_cmphex (fg, ==, 0xFF8800); + g_assert_false (has_bg); + g_assert_cmphex (bg, ==, 0xDEAD); +} + +static void +test_parse_foreground_and_background (void) +{ + guint32 fg = 0, bg = 0; + int has_bg = FALSE; + + g_assert_cmpint (hexcolor_parse ("112233,a0b0c0x", 14, &fg, &bg, &has_bg), ==, 13); + g_assert_cmphex (fg, ==, 0x112233); + g_assert_cmphex (bg, ==, 0xA0B0C0); + g_assert_true (has_bg); +} + +static void +test_parse_bad_background_keeps_foreground (void) +{ + guint32 fg = 0; + int has_bg = TRUE; + + /* ",12" is plain text after a valid foreground, not a background */ + g_assert_cmpint (hexcolor_parse ("112233,12 hi", 12, &fg, NULL, &has_bg), ==, 6); + g_assert_cmphex (fg, ==, 0x112233); + g_assert_false (has_bg); +} + +static void +test_parse_bare (void) +{ + /* a bare \004 resets colors, like a bare \003 */ + g_assert_cmpint (hexcolor_parse (" text", 5, NULL, NULL, NULL), ==, 0); + g_assert_cmpint (hexcolor_parse ("12345G", 6, NULL, NULL, NULL), ==, 0); + g_assert_cmpint (hexcolor_parse ("", 0, NULL, NULL, NULL), ==, 0); +} + +static void +test_parse_respects_length (void) +{ + g_assert_cmpint (hexcolor_parse ("FF8800", 5, NULL, NULL, NULL), ==, 0); + g_assert_cmpint (hexcolor_parse ("FF8800,112233", 12, NULL, NULL, NULL), ==, 6); +} + +static void +assert_stripped (const char *in, int flags, const char *expected) +{ + char out[128]; + + strip_color2 (in, -1, out, flags); + g_assert_cmpstr (out, ==, expected); +} + +static void +test_strip (void) +{ + assert_stripped ("\004FF8800alice\017: hi", STRIP_ALL, "alice: hi"); + assert_stripped ("\004112233,445566bg", STRIP_ALL, "bg"); + assert_stripped ("a\004 b", STRIP_ALL, "a b"); + assert_stripped ("\004FF8800\00312x", STRIP_ALL, "x"); + /* a trailing code with too few digits leaves the rest as text */ + assert_stripped ("x\004FF88", STRIP_ALL, "xFF88"); +} + +static void +test_strip_keeps_hex_without_strip_color (void) +{ + assert_stripped ("\004FF8800red\002", STRIP_ATTRIB, "\004FF8800red"); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/hexcolor/parse/foreground", test_parse_foreground); + g_test_add_func ("/hexcolor/parse/foreground-and-background", test_parse_foreground_and_background); + g_test_add_func ("/hexcolor/parse/bad-background", test_parse_bad_background_keeps_foreground); + g_test_add_func ("/hexcolor/parse/bare", test_parse_bare); + g_test_add_func ("/hexcolor/parse/length", test_parse_respects_length); + g_test_add_func ("/hexcolor/strip/all", test_strip); + g_test_add_func ("/hexcolor/strip/attrib-only", test_strip_keeps_hex_without_strip_color); + + return g_test_run (); +} diff --git a/src/common/util.c b/src/common/util.c index 77f286a6..4ad005c0 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -301,6 +301,42 @@ strip_color (const char *text, int len, int flags) return new_str; } +static int +hexcolor_component (const char *s, int avail, guint32 *rgb) +{ + int i; + guint32 value = 0; + + if (avail < 6) + return FALSE; + for (i = 0; i < 6; i++) + { + if (!g_ascii_isxdigit (s[i])) + return FALSE; + value = (value << 4) | g_ascii_xdigit_value (s[i]); + } + if (rgb) + *rgb = value; + return TRUE; +} + +// Parses the parameters of a \004 (hex color) code: RRGGBB[,RRGGBB]. +int +hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg) +{ + if (has_bg) + *has_bg = FALSE; + if (!hexcolor_component (s, avail, fg)) + return 0; + if (avail >= 7 && s[6] == ',' && hexcolor_component (s + 7, avail - 7, bg)) + { + if (has_bg) + *has_bg = TRUE; + return 13; + } + return 6; +} + /* CL: strip_color2 strips src and writes the output at dst; pass the same pointer in both arguments to strip in place. */ int @@ -330,6 +366,14 @@ strip_color2 (const char *src, int len, char *dst, int flags) if (!(flags & STRIP_COLOR)) goto pass_char; rcol = 2; break; + case '\004': /*ATTR_HEXCOLOR: */ + if (!(flags & STRIP_COLOR)) goto pass_char; + { + int skip = hexcolor_parse (src + 1, len, NULL, NULL, NULL); + src += skip; + len -= skip; + } + break; case HIDDEN_CHAR: /* CL: invisible text (for event formats only) */ /* this takes care of the topic */ if (!(flags & STRIP_HIDDEN)) goto pass_char; break; diff --git a/src/common/util.h b/src/common/util.h index 4774f3bc..f6b2d4d6 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -59,6 +59,7 @@ void util_exec (const char *cmd); #define STRIP_ALL 7 gchar *strip_color (const char *text, int len, int flags); int strip_color2 (const char *src, int len, char *dst, int flags); +int hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg); int strip_hidden_attribute (char *src, char *dst); char *errorstring (int err); int waitline (int sok, char *buf, int bufsize, int); diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index 37ec9b39..23cc2143 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -775,16 +775,35 @@ backend_draw_text_emph (GtkXText *xtext, gboolean dofill, int x, int y, cairo_destroy (cr); } +static XTextColor +xtext_color_for_index (GtkXText *xtext, int index) +{ + if (index == XTEXT_HEX_FG) + return xtext->hex_fg; + if (index == XTEXT_HEX_BG) + return xtext->hex_bg; + return xtext->palette[index]; +} + static void xtext_set_fg (GtkXText *xtext, int index) { - xtext->fgc = xtext->palette[index]; + xtext->fgc = xtext_color_for_index (xtext, index); } static void xtext_set_bg (GtkXText *xtext, int index) { - xtext->bgc = xtext->palette[index]; + xtext->bgc = xtext_color_for_index (xtext, index); +} + +static void +xtext_rgb_to_color (guint32 rgb, XTextColor *color) +{ + color->red = ((rgb >> 16) & 0xFF) / 255.0; + color->green = ((rgb >> 8) & 0xFF) / 255.0; + color->blue = (rgb & 0xFF) / 255.0; + color->alpha = 1.0; } static void @@ -2999,6 +3018,14 @@ gtk_xtext_strip_color (unsigned char *text, int len, unsigned char *outbuf, xtext_do_chunk (&c); rcol = 2; break; + case ATTR_HEXCOLOR: + xtext_do_chunk (&c); + { + int skip = hexcolor_parse ((char *)text + 1, len - 1, NULL, NULL, NULL); + text += skip; + len -= skip; + } + break; case ATTR_BEEP: case ATTR_RESET: case ATTR_REVERSE: @@ -3545,6 +3572,35 @@ gtk_xtext_render_str (GtkXText * xtext, int y, textentry * ent, pstr += j + 1; j = 0; break; + case ATTR_HEXCOLOR: + RENDER_FLUSH; + { + guint32 fg = 0, bg = 0; + int has_bg; + int skip = hexcolor_parse ((char *)str + i + 1, len - i - 1, &fg, &bg, &has_bg); + + if (skip == 0) + gtk_xtext_reset (xtext, mark, FALSE); + else + { + xtext_rgb_to_color (fg, &xtext->hex_fg); + xtext->col_fore = XTEXT_HEX_FG; + if (!mark) + xtext_set_fg (xtext, XTEXT_HEX_FG); + if (has_bg) + { + xtext_rgb_to_color (bg, &xtext->hex_bg); + xtext->col_back = XTEXT_HEX_BG; + xtext->backcolor = TRUE; + if (!mark) + xtext_set_bg (xtext, XTEXT_HEX_BG); + } + } + pstr += j + 1 + skip; + j = 0; + i += skip; + } + break; default: tmp = charlen (str + i); /* invalid utf8 safe guard */ @@ -3762,6 +3818,13 @@ find_next_wrap (GtkXText * xtext, textentry * ent, unsigned char *str, limit_offset++; str++; break; + case ATTR_HEXCOLOR: + { + int skip = hexcolor_parse ((char *)str + 1, (ent->str + ent->str_len) - str - 1, NULL, NULL, NULL); + limit_offset += 1 + skip; + str += 1 + skip; + } + break; case ATTR_HIDDEN: if (xtext->ignore_hidden) goto def; diff --git a/src/fe-gtk/xtext.h b/src/fe-gtk/xtext.h index 9b312d85..fdd442a9 100644 --- a/src/fe-gtk/xtext.h +++ b/src/fe-gtk/xtext.h @@ -38,6 +38,7 @@ GType gtk_xtext_get_type (void); #define ATTR_BOLD '\002' #define ATTR_COLOR '\003' +#define ATTR_HEXCOLOR '\004' #define ATTR_BLINK '\006' #define ATTR_BEEP '\007' #define ATTR_HIDDEN '\010' @@ -57,6 +58,8 @@ GType gtk_xtext_get_type (void); #define XTEXT_BG 102 #define XTEXT_MARKER 103 /* for marker line */ #define XTEXT_MAX_COLOR 98 +#define XTEXT_HEX_FG 200 /* not palette slots: colors from \004RRGGBB */ +#define XTEXT_HEX_BG 201 typedef struct textentry textentry; /* @@ -181,6 +184,8 @@ struct _GtkXText int col_fore; int col_back; + XTextColor hex_fg; + XTextColor hex_bg; int depth; /* gdk window depth */ From 0a4367b1da82030ea98335ef544c07a752662565 Mon Sep 17 00:00:00 2001 From: BareMetal Date: Sun, 11 Oct 2026 23:58:09 -0600 Subject: [PATCH 2/2] fix(xtext): move hexcolor_parse to its own file for the standalone xtext tests master's new xtext_tests target compiles xtext.c on its own with only GTK linked, so hexcolor_parse cannot live in util.c. Keep it in a dependency-free hexcolor.c that both test targets compile, and register it in the Visual Studio project for the Windows build. Claude-Session: https://claude.ai/code/session_01Jxd4AknGzdAENADWAGHCzx --- src/common/common.vcxproj | 2 ++ src/common/common.vcxproj.filters | 6 +++++ src/common/hexcolor.c | 42 +++++++++++++++++++++++++++++++ src/common/hexcolor.h | 16 ++++++++++++ src/common/meson.build | 2 ++ src/common/tests/test-hexcolor.c | 1 + src/common/util.c | 37 +-------------------------- src/common/util.h | 1 - src/fe-gtk/meson.build | 2 +- src/fe-gtk/xtext.c | 1 + 10 files changed, 72 insertions(+), 38 deletions(-) create mode 100644 src/common/hexcolor.c create mode 100644 src/common/hexcolor.h diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index c4fb0065..5f2107c2 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -43,6 +43,7 @@ + @@ -79,6 +80,7 @@ + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 5b5f19c1..76723605 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -98,6 +98,9 @@ Header Files + + Header Files + Header Files @@ -202,6 +205,9 @@ Source Files + + Source Files + Source Files diff --git a/src/common/hexcolor.c b/src/common/hexcolor.c new file mode 100644 index 00000000..ec7629a0 --- /dev/null +++ b/src/common/hexcolor.c @@ -0,0 +1,42 @@ +/* ZoiteChat + * Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB]. + * Kept free of other application dependencies so the xtext widget tests can + * compile it on its own. + * Distributed under GPL-2.0-or-later; see COPYING. */ + +#include "hexcolor.h" + +static int +hexcolor_component (const char *s, int avail, guint32 *rgb) +{ + int i; + guint32 value = 0; + + if (avail < 6) + return FALSE; + for (i = 0; i < 6; i++) + { + if (!g_ascii_isxdigit (s[i])) + return FALSE; + value = (value << 4) | g_ascii_xdigit_value (s[i]); + } + if (rgb) + *rgb = value; + return TRUE; +} + +int +hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg) +{ + if (has_bg) + *has_bg = FALSE; + if (!hexcolor_component (s, avail, fg)) + return 0; + if (avail >= 7 && s[6] == ',' && hexcolor_component (s + 7, avail - 7, bg)) + { + if (has_bg) + *has_bg = TRUE; + return 13; + } + return 6; +} diff --git a/src/common/hexcolor.h b/src/common/hexcolor.h new file mode 100644 index 00000000..32256224 --- /dev/null +++ b/src/common/hexcolor.h @@ -0,0 +1,16 @@ +/* ZoiteChat + * Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB]. + * See https://modern.ircdocs.horse/formatting + * Distributed under GPL-2.0-or-later; see COPYING. */ + +#ifndef ZOITECHAT_HEXCOLOR_H +#define ZOITECHAT_HEXCOLOR_H + +#include + +/* Parses the parameters following a \004 byte. Returns the number of bytes + * consumed (6 or 13), or 0 if no valid RRGGBB follows. fg, bg and has_bg + * may be NULL. */ +int hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg); + +#endif diff --git a/src/common/meson.build b/src/common/meson.build index f92c6990..e288d165 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -25,6 +25,7 @@ common_sources = [ 'tree.c', 'url.c', 'userlist.c', + 'hexcolor.c', 'util.c' ] @@ -201,6 +202,7 @@ test('Emoji Data Tests', emoji_data_tests, hexcolor_tests = executable('hexcolor_tests', [ 'tests/test-hexcolor.c', + 'hexcolor.c', 'util.c', ], include_directories: [config_h_include, include_directories('.')], diff --git a/src/common/tests/test-hexcolor.c b/src/common/tests/test-hexcolor.c index d474249b..b635a976 100644 --- a/src/common/tests/test-hexcolor.c +++ b/src/common/tests/test-hexcolor.c @@ -19,6 +19,7 @@ #include +#include "../hexcolor.h" #include "../util.h" static void diff --git a/src/common/util.c b/src/common/util.c index 4ad005c0..001dc083 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -47,6 +47,7 @@ #include "zoitechatc.h" #include #include "util.h" +#include "hexcolor.h" #if defined (__FreeBSD__) || defined (__APPLE__) #include @@ -301,42 +302,6 @@ strip_color (const char *text, int len, int flags) return new_str; } -static int -hexcolor_component (const char *s, int avail, guint32 *rgb) -{ - int i; - guint32 value = 0; - - if (avail < 6) - return FALSE; - for (i = 0; i < 6; i++) - { - if (!g_ascii_isxdigit (s[i])) - return FALSE; - value = (value << 4) | g_ascii_xdigit_value (s[i]); - } - if (rgb) - *rgb = value; - return TRUE; -} - -// Parses the parameters of a \004 (hex color) code: RRGGBB[,RRGGBB]. -int -hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg) -{ - if (has_bg) - *has_bg = FALSE; - if (!hexcolor_component (s, avail, fg)) - return 0; - if (avail >= 7 && s[6] == ',' && hexcolor_component (s + 7, avail - 7, bg)) - { - if (has_bg) - *has_bg = TRUE; - return 13; - } - return 6; -} - /* CL: strip_color2 strips src and writes the output at dst; pass the same pointer in both arguments to strip in place. */ int diff --git a/src/common/util.h b/src/common/util.h index f6b2d4d6..4774f3bc 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -59,7 +59,6 @@ void util_exec (const char *cmd); #define STRIP_ALL 7 gchar *strip_color (const char *text, int len, int flags); int strip_color2 (const char *src, int len, char *dst, int flags); -int hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg); int strip_hidden_attribute (char *src, char *dst); char *errorstring (int err); int waitline (int sok, char *buf, int bufsize, int); diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build index d74ff813..4f3d0aef 100644 --- a/src/fe-gtk/meson.build +++ b/src/fe-gtk/meson.build @@ -280,7 +280,7 @@ test('Emoji Font Tests', emoji_font_tests, ) xtext_tests = executable('xtext_tests', - 'tests/test-xtext.c', + ['tests/test-xtext.c', '../common/hexcolor.c'], include_directories: [config_h_include], dependencies: [gtk_dep], ) diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index 23cc2143..c9235567 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -35,6 +35,7 @@ #include "../common/zoitechat.h" #include "../common/fe.h" #include "../common/util.h" +#include "../common/hexcolor.h" #include "../common/zoitechatc.h" #include "../common/url.h"