19#include <ascii-chat/video/palette.h>
20#include <ascii-chat/common.h>
21#include <ascii-chat/platform/terminal.h>
22#include <ascii-chat/util/utf8.h>
29static const palette_def_t builtin_palettes[PALETTE_COUNT] = {
30 [PALETTE_STANDARD] = {.name =
"standard",
31 .chars = PALETTE_CHARS_STANDARD,
33 .requires_utf8 =
false,
34 .is_validated =
true},
36 {.name =
"blocks", .chars = PALETTE_CHARS_BLOCKS, .length = 11, .requires_utf8 =
true, .is_validated =
true},
38 {.name =
"digital", .chars = PALETTE_CHARS_DIGITAL, .length = 10, .requires_utf8 =
true, .is_validated =
true},
40 {.name =
"minimal", .chars = PALETTE_CHARS_MINIMAL, .length = 8, .requires_utf8 =
false, .is_validated =
true},
42 {.name =
"cool", .chars = PALETTE_CHARS_COOL, .length = 11, .requires_utf8 =
true, .is_validated =
true},
48 if (type >= PALETTE_COUNT || type == PALETTE_CUSTOM || type == PALETTE_UNSET) {
51 return &builtin_palettes[type];
57 if (!chars || len == 0) {
61 for (
size_t i = 0; i < len; i++) {
63 if ((
unsigned char)chars[i] >= 128) {
72 if (!chars || len == 0) {
73 SET_ERRNO(ERROR_INVALID_PARAM,
"Palette validation failed: empty or NULL palette");
78 SET_ERRNO(ERROR_INVALID_PARAM,
"Palette validation failed: palette too long (%zu chars, max 256)", len);
83 uint32_t codepoints[256];
86 if (codepoint_count == SIZE_MAX) {
87 SET_ERRNO(ERROR_INVALID_PARAM,
"Palette validation failed: invalid UTF-8 sequence");
92 for (
size_t i = 0; i < codepoint_count; i++) {
93 uint32_t cp = codepoints[i];
96 if (cp < 32 && cp !=
'\t') {
97 SET_ERRNO(ERROR_INVALID_PARAM,
"Palette validation failed: control character at position %zu", i);
110 uint8_t utf8_buf[5] = {0};
114 utf8_buf[0] = (uint8_t)cp;
115 }
else if (cp <= 0x7FF) {
116 utf8_buf[0] = (uint8_t)(0xC0 | (cp >> 6));
117 utf8_buf[1] = (uint8_t)(0x80 | (cp & 0x3F));
118 }
else if (cp <= 0xFFFF) {
119 utf8_buf[0] = (uint8_t)(0xE0 | (cp >> 12));
120 utf8_buf[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
121 utf8_buf[2] = (uint8_t)(0x80 | (cp & 0x3F));
122 }
else if (cp <= 0x10FFFF) {
123 utf8_buf[0] = (uint8_t)(0xF0 | (cp >> 18));
124 utf8_buf[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
125 utf8_buf[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
126 utf8_buf[3] = (uint8_t)(0x80 | (cp & 0x3F));
128 SET_ERRNO(ERROR_INVALID_PARAM,
"Palette validation failed: invalid codepoint at position %zu", i);
134 if (width < 0 || width > 2) {
135 SET_ERRNO(ERROR_INVALID_PARAM,
136 "Palette validation failed: character at position %zu has invalid width %d (must be 1 or 2)", i,
143 log_debug(
"Palette validation successful: %zu characters validated", codepoint_count);
154 SAFE_MEMSET(caps,
sizeof(utf8_capabilities_t), 0,
sizeof(utf8_capabilities_t));
157 const char *term = SAFE_GETENV(
"TERM");
161 SAFE_STRNCPY(caps->terminal_type, term,
sizeof(caps->terminal_type));
167 if (caps->utf8_support) {
168 SAFE_STRNCPY(caps->locale_encoding,
"UTF-8",
sizeof(caps->locale_encoding));
171 char *current_locale = setlocale(LC_CTYPE, NULL);
172 char *old_locale = NULL;
173 if (current_locale) {
174 SAFE_STRDUP(old_locale, current_locale);
176 if (setlocale(LC_CTYPE,
"")) {
178 const char *codeset = nl_langinfo(CODESET);
180 SAFE_STRNCPY(caps->locale_encoding, codeset,
sizeof(caps->locale_encoding));
184 SAFE_STRNCPY(caps->locale_encoding,
"CP1252",
sizeof(caps->locale_encoding));
188 (void)setlocale(LC_CTYPE, old_locale);
193 SAFE_FREE(old_locale);
199 const char *utf8_terminals[] = {
200 "xterm-256color",
"screen-256color",
"tmux-256color",
"alacritty",
"kitty",
"iterm",
201 "iterm2",
"gnome-terminal",
"konsole",
"terminology", NULL};
203 for (
int i = 0; utf8_terminals[i]; i++) {
204 if (strstr(term, utf8_terminals[i])) {
205 caps->utf8_support =
true;
211 log_debug(
"UTF-8 support detection: %s (term=%s, encoding=%s)", caps->utf8_support ?
"supported" :
"not supported",
212 caps->terminal_type[0] ? caps->terminal_type :
"unknown",
213 caps->locale_encoding[0] ? caps->locale_encoding :
"unknown");
215 return caps->utf8_support;
223 if (requested == PALETTE_CUSTOM) {
224 return PALETTE_CUSTOM;
228 log_warn(
"Invalid palette type %d, falling back to standard", requested);
229 return PALETTE_STANDARD;
233 if (palette->requires_utf8 && !client_utf8) {
234 log_warn(
"Client doesn't support UTF-8, falling back from %s", palette->name);
239 case PALETTE_DIGITAL:
242 return PALETTE_STANDARD;
254 log_debug(
"Client palette config: type=%d, custom_chars=%s", type, custom_chars ? custom_chars :
"(none)");
257 if (type == PALETTE_CUSTOM) {
258 if (!custom_chars || strlen(custom_chars) == 0) {
259 SET_ERRNO(ERROR_INVALID_PARAM,
"Custom palette requested but no characters provided");
264 SET_ERRNO(ERROR_INVALID_PARAM,
"Custom palette validation failed");
270 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid palette type: %d", type);
280 if (!palette_chars || palette_len == 0 || !luminance_mapping) {
281 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for client luminance palette");
286 for (
int i = 0; i < 256; i++) {
288 size_t palette_index = (i * (palette_len - 1) + 127) / 255;
289 if (palette_index >= palette_len) {
290 palette_index = palette_len - 1;
292 luminance_mapping[i] = palette_chars[palette_index];
300 size_t *client_palette_len,
char client_luminance_palette[256]) {
301 const palette_def_t *palette = NULL;
302 const char *chars_to_use = NULL;
303 size_t len_to_use = 0;
305 if (palette_type == PALETTE_CUSTOM) {
307 SET_ERRNO(ERROR_INVALID_PARAM,
"Client requested custom palette but custom_chars is NULL");
311 len_to_use = strlen(custom_chars);
312 if (len_to_use == 0) {
313 SET_ERRNO(ERROR_INVALID_PARAM,
"Client requested custom palette but custom_chars is empty");
316 if (len_to_use >= 256) {
317 SET_ERRNO(ERROR_INVALID_PARAM,
"Client custom palette too long: %zu chars", len_to_use);
323 SET_ERRNO(ERROR_INVALID_PARAM,
"Client custom palette validation failed");
327 chars_to_use = custom_chars;
331 SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid client palette type: %d", palette_type);
335 chars_to_use = palette->chars;
336 len_to_use = strlen(palette->chars);
339 log_debug(
"Using built-in palette: %s, chars='%s', char_count=%zu, byte_len=%zu", palette->name, chars_to_use,
340 palette->length, len_to_use);
344 SAFE_MEMCPY(client_palette_chars, len_to_use, chars_to_use, len_to_use);
345 client_palette_chars[len_to_use] =
'\0';
346 *client_palette_len = len_to_use;
350 SET_ERRNO(ERROR_INVALID_STATE,
"Failed to build client luminance palette");
354 log_debug(
"Initialized client palette: type=%d, %zu chars, first_char='%c', last_char='%c'", palette_type, len_to_use,
355 chars_to_use[0], chars_to_use[len_to_use - 1]);
364 if (!palette_string || *palette_string ==
'\0') {
368 utf8_palette_t *palette;
369 palette = SAFE_MALLOC(
sizeof(utf8_palette_t), utf8_palette_t *);
372 size_t char_count = 0;
373 const char *p = palette_string;
374 size_t total_bytes = strlen(palette_string);
377 size_t bytes_processed = 0;
378 while (bytes_processed < total_bytes) {
380 unsigned char c = (
unsigned char)p[0];
382 if ((c & 0x80) == 0) {
384 }
else if ((c & 0xE0) == 0xC0) {
386 }
else if ((c & 0xF0) == 0xE0) {
388 }
else if ((c & 0xF8) == 0xF0) {
393 if (bytes_processed + bytes > total_bytes) {
398 bytes_processed += bytes;
403 if (char_count == 0) {
404 SET_ERRNO(ERROR_INVALID_PARAM,
"Palette string contains no valid UTF-8 characters");
410 palette->chars = SAFE_MALLOC(char_count *
sizeof(utf8_char_info_t), utf8_char_info_t *);
411 palette->raw_string = SAFE_MALLOC(total_bytes + 1,
char *);
413 if (palette->chars == NULL || palette->raw_string == NULL) {
414 SAFE_FREE(palette->chars);
415 SAFE_FREE(palette->raw_string);
417 SET_ERRNO(ERROR_MEMORY,
"Failed to allocate palette character array");
421 SAFE_MEMCPY(palette->raw_string, total_bytes + 1, palette_string, total_bytes + 1);
422 palette->char_count = char_count;
423 palette->total_bytes = total_bytes;
431 char old_locale[256] = {0};
432 char *current_locale = setlocale(LC_CTYPE, NULL);
433 if (current_locale) {
434 SAFE_STRNCPY(old_locale, current_locale,
sizeof(old_locale));
436 (void)setlocale(LC_CTYPE,
"");
438 while (char_idx < char_count && bytes_processed < total_bytes) {
439 utf8_char_info_t *char_info = &palette->chars[char_idx];
442 unsigned char c = (
unsigned char)*p;
445 if ((c & 0x80) == 0) {
447 }
else if ((c & 0xE0) == 0xC0) {
449 }
else if ((c & 0xF0) == 0xE0) {
451 }
else if ((c & 0xF8) == 0xF0) {
456 if (bytes_processed + bytes > total_bytes) {
461 SAFE_MEMCPY(char_info->bytes, bytes, p, bytes);
463 SAFE_MEMSET(char_info->bytes + bytes, 4 - bytes, 0, 4 - bytes);
465 char_info->byte_len = bytes;
469 char_info->display_width = (width > 0 && width <= 2) ? width : 1;
472 bytes_processed += bytes;
477 if (old_locale[0] !=
'\0') {
478 (void)setlocale(LC_CTYPE, old_locale);
487 SAFE_FREE(palette->chars);
488 SAFE_FREE(palette->raw_string);
495 if (!palette || index >= palette->char_count) {
498 return &palette->chars[index];
506 return palette->char_count;
511 if (!palette || !utf8_char || char_bytes == 0 || char_bytes > 4) {
515 for (
size_t i = 0; i < palette->char_count; i++) {
516 const utf8_char_info_t *char_info = &palette->chars[i];
517 if (char_info->byte_len == char_bytes && memcmp(char_info->bytes, utf8_char, char_bytes) == 0) {
527 if (!palette || !utf8_char || char_bytes == 0 || char_bytes > 4) {
531 for (
size_t i = 0; i < palette->char_count; i++) {
532 const utf8_char_info_t *char_info = &palette->chars[i];
533 if (char_info->byte_len == char_bytes && memcmp(char_info->bytes, utf8_char, char_bytes) == 0) {
544 size_t *indices,
size_t max_indices) {
545 if (!palette || !utf8_char || char_bytes == 0 || char_bytes > 4 || !indices || max_indices == 0) {
549 size_t found_count = 0;
551 for (
size_t i = 0; i < palette->char_count && found_count < max_indices; i++) {
552 const utf8_char_info_t *char_info = &palette->chars[i];
553 if (char_info->byte_len == char_bytes && memcmp(char_info->bytes, utf8_char, char_bytes) == 0) {
554 indices[found_count++] = i;
void utf8_palette_destroy(utf8_palette_t *palette)
size_t utf8_palette_find_all_char_indices(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes, size_t *indices, size_t max_indices)
int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256], size_t *client_palette_len, char client_luminance_palette[256])
bool palette_requires_utf8_encoding(const char *chars, size_t len)
size_t utf8_palette_get_char_count(const utf8_palette_t *palette)
bool detect_client_utf8_support(utf8_capabilities_t *caps)
palette_type_t select_compatible_palette(palette_type_t requested, bool client_utf8)
const size_t DEFAULT_ASCII_PALETTE_LEN
const char DEFAULT_ASCII_PALETTE[]
const palette_def_t * get_builtin_palette(palette_type_t type)
int apply_palette_config(palette_type_t type, const char *custom_chars)
int build_client_luminance_palette(const char *palette_chars, size_t palette_len, char luminance_mapping[256])
size_t utf8_palette_find_char_index(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes)
bool validate_palette_chars(const char *chars, size_t len)
const utf8_char_info_t * utf8_palette_get_char(const utf8_palette_t *palette, size_t index)
bool utf8_palette_contains_char(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes)
utf8_palette_t * utf8_palette_create(const char *palette_string)
int utf8_display_width_n(const char *str, size_t max_bytes)
size_t utf8_to_codepoints(const char *str, uint32_t *out_codepoints, size_t max_codepoints)
int utf8_display_width(const char *str)