ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
palette.c
Go to the documentation of this file.
1
8#include <stdbool.h>
9#include <stddef.h>
10#include <string.h>
11#include <locale.h>
12#include <stdlib.h>
13#include <wchar.h>
14#ifndef _WIN32
15#include <langinfo.h>
16#include <unistd.h>
17#endif
18
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> // For UTF-8 character width detection
23
24/* Default palette constants for legacy functions */
25const char DEFAULT_ASCII_PALETTE[] = PALETTE_CHARS_STANDARD;
26const size_t DEFAULT_ASCII_PALETTE_LEN = 23; // strlen(PALETTE_CHARS_STANDARD)
27
28// Built-in palette definitions
29static const palette_def_t builtin_palettes[PALETTE_COUNT] = {
30 [PALETTE_STANDARD] = {.name = "standard",
31 .chars = PALETTE_CHARS_STANDARD,
32 .length = 23,
33 .requires_utf8 = false,
34 .is_validated = true},
35 [PALETTE_BLOCKS] =
36 {.name = "blocks", .chars = PALETTE_CHARS_BLOCKS, .length = 11, .requires_utf8 = true, .is_validated = true},
37 [PALETTE_DIGITAL] =
38 {.name = "digital", .chars = PALETTE_CHARS_DIGITAL, .length = 10, .requires_utf8 = true, .is_validated = true},
39 [PALETTE_MINIMAL] =
40 {.name = "minimal", .chars = PALETTE_CHARS_MINIMAL, .length = 8, .requires_utf8 = false, .is_validated = true},
41 [PALETTE_COOL] =
42 {.name = "cool", .chars = PALETTE_CHARS_COOL, .length = 11, .requires_utf8 = true, .is_validated = true},
43 // PALETTE_CUSTOM is handled specially - no predefined entry
44};
45
46// Get a built-in palette definition
47const palette_def_t *get_builtin_palette(palette_type_t type) {
48 if (type >= PALETTE_COUNT || type == PALETTE_CUSTOM || type == PALETTE_UNSET) {
49 return NULL;
50 }
51 return &builtin_palettes[type];
52}
53
54// Check if a palette string contains UTF-8 characters
55bool palette_requires_utf8_encoding(const char *chars, size_t len) {
56 // Handle NULL or empty string
57 if (!chars || len == 0) {
58 return false;
59 }
60
61 for (size_t i = 0; i < len; i++) {
62 // Any byte with high bit set indicates UTF-8
63 if ((unsigned char)chars[i] >= 128) {
64 return true;
65 }
66 }
67 return false;
68}
69
70// Validate UTF-8 character sequences and terminal width
71bool validate_palette_chars(const char *chars, size_t len) {
72 if (!chars || len == 0) {
73 SET_ERRNO(ERROR_INVALID_PARAM, "Palette validation failed: empty or NULL palette");
74 return false;
75 }
76
77 if (len > 256) {
78 SET_ERRNO(ERROR_INVALID_PARAM, "Palette validation failed: palette too long (%zu chars, max 256)", len);
79 return false;
80 }
81
82 // Decode UTF-8 string into codepoints
83 uint32_t codepoints[256];
84 size_t codepoint_count = utf8_to_codepoints(chars, codepoints, 256);
85
86 if (codepoint_count == SIZE_MAX) {
87 SET_ERRNO(ERROR_INVALID_PARAM, "Palette validation failed: invalid UTF-8 sequence");
88 return false;
89 }
90
91 // Validate each codepoint
92 for (size_t i = 0; i < codepoint_count; i++) {
93 uint32_t cp = codepoints[i];
94
95 // Check for control characters (except tab)
96 if (cp < 32 && cp != '\t') {
97 SET_ERRNO(ERROR_INVALID_PARAM, "Palette validation failed: control character at position %zu", i);
98 return false;
99 }
100
101 // Convert codepoint to UTF-8 and check display width
102 // For ASCII characters, this is straightforward
103 // For multi-byte UTF-8, we need to check the display width
104 if (cp < 128) {
105 // ASCII character - always width 1
106 continue;
107 } else {
108 // Non-ASCII: use utf8_display_width to check character width
109 // Create a temporary buffer with the UTF-8 encoded character
110 uint8_t utf8_buf[5] = {0};
111
112 // Encode codepoint to UTF-8 (simple for demo, should use proper encoder)
113 if (cp <= 0x7F) {
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));
127 } else {
128 SET_ERRNO(ERROR_INVALID_PARAM, "Palette validation failed: invalid codepoint at position %zu", i);
129 return false;
130 }
131
132 // Check character width - allow width 1 and 2 (for emoji and wide characters)
133 int width = utf8_display_width((const char *)utf8_buf);
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,
137 width);
138 return false;
139 }
140 }
141 }
142
143 log_debug("Palette validation successful: %zu characters validated", codepoint_count);
144 return true;
145}
146
147// Detect client UTF-8 support from environment
148bool detect_client_utf8_support(utf8_capabilities_t *caps) {
149 if (!caps) {
150 return false;
151 }
152
153 // Initialize structure
154 SAFE_MEMSET(caps, sizeof(utf8_capabilities_t), 0, sizeof(utf8_capabilities_t));
155
156 // Check environment variables
157 const char *term = SAFE_GETENV("TERM");
158
159 // Store terminal type
160 if (term) {
161 SAFE_STRNCPY(caps->terminal_type, term, sizeof(caps->terminal_type));
162 }
163
164 // Use platform-specific UTF-8 detection from platform abstraction layer
165 caps->utf8_support = terminal_supports_utf8();
166
167 if (caps->utf8_support) {
168 SAFE_STRNCPY(caps->locale_encoding, "UTF-8", sizeof(caps->locale_encoding));
169 } else {
170 // Try to detect encoding via locale
171 char *current_locale = setlocale(LC_CTYPE, NULL);
172 char *old_locale = NULL;
173 if (current_locale) {
174 SAFE_STRDUP(old_locale, current_locale);
175 }
176 if (setlocale(LC_CTYPE, "")) {
177#ifndef _WIN32
178 const char *codeset = nl_langinfo(CODESET);
179 if (codeset) {
180 SAFE_STRNCPY(caps->locale_encoding, codeset, sizeof(caps->locale_encoding));
181 }
182#else
183 // Windows may not have locale set but still support UTF-8
184 SAFE_STRNCPY(caps->locale_encoding, "CP1252", sizeof(caps->locale_encoding));
185#endif
186 // Restore old locale
187 if (old_locale) {
188 (void)setlocale(LC_CTYPE, old_locale);
189 }
190 }
191 // Always free old_locale regardless of setlocale success
192 if (old_locale) {
193 SAFE_FREE(old_locale);
194 }
195 }
196
197 // Check for known UTF-8 supporting terminals
198 if (term) {
199 const char *utf8_terminals[] = {
200 "xterm-256color", "screen-256color", "tmux-256color", "alacritty", "kitty", "iterm",
201 "iterm2", "gnome-terminal", "konsole", "terminology", NULL};
202
203 for (int i = 0; utf8_terminals[i]; i++) {
204 if (strstr(term, utf8_terminals[i])) {
205 caps->utf8_support = true;
206 break;
207 }
208 }
209 }
210
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");
214
215 return caps->utf8_support;
216}
217
218// Select a compatible palette based on client capabilities
219palette_type_t select_compatible_palette(palette_type_t requested, bool client_utf8) {
220 const palette_def_t *palette = get_builtin_palette(requested);
221
222 // Custom palettes are validated separately
223 if (requested == PALETTE_CUSTOM) {
224 return PALETTE_CUSTOM;
225 }
226
227 if (!palette) {
228 log_warn("Invalid palette type %d, falling back to standard", requested);
229 return PALETTE_STANDARD;
230 }
231
232 // If palette requires UTF-8 but client doesn't support it, find fallback
233 if (palette->requires_utf8 && !client_utf8) {
234 log_warn("Client doesn't support UTF-8, falling back from %s", palette->name);
235
236 // Fallback hierarchy
237 switch (requested) {
238 case PALETTE_BLOCKS:
239 case PALETTE_DIGITAL:
240 case PALETTE_COOL:
241 default:
242 return PALETTE_STANDARD; // ASCII equivalent
243 }
244 }
245
246 return requested; // Compatible, use as requested
247}
248
249// Apply palette configuration for client-side initialization only (no global cache)
250int apply_palette_config(palette_type_t type, const char *custom_chars) {
251 // This function is now used only for client-side initialization
252 // Server uses initialize_client_palette() for per-client palettes
253
254 log_debug("Client palette config: type=%d, custom_chars=%s", type, custom_chars ? custom_chars : "(none)");
255
256 // Just validate the palette - no global state changes
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");
260 return -1;
261 }
262
263 if (!validate_palette_chars(custom_chars, strlen(custom_chars))) {
264 SET_ERRNO(ERROR_INVALID_PARAM, "Custom palette validation failed");
265 return -1;
266 }
267 } else {
268 const palette_def_t *palette = get_builtin_palette(type);
269 if (!palette) {
270 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid palette type: %d", type);
271 return -1;
272 }
273 }
274
275 return 0; // Validation successful, no global state to update
276}
277
278// Build a per-client luminance palette without affecting global cache
279int build_client_luminance_palette(const char *palette_chars, size_t palette_len, char luminance_mapping[256]) {
280 if (!palette_chars || palette_len == 0 || !luminance_mapping) {
281 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for client luminance palette");
282 return -1;
283 }
284
285 // Map 256 luminance values to palette indices for this specific client
286 for (int i = 0; i < 256; i++) {
287 // Linear mapping with proper rounding
288 size_t palette_index = (i * (palette_len - 1) + 127) / 255;
289 if (palette_index >= palette_len) {
290 palette_index = palette_len - 1;
291 }
292 luminance_mapping[i] = palette_chars[palette_index];
293 }
294
295 return 0;
296}
297
298// Initialize a client's palette cache from their capabilities
299int initialize_client_palette(palette_type_t palette_type, const char *custom_chars, char client_palette_chars[256],
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;
304
305 if (palette_type == PALETTE_CUSTOM) {
306 if (!custom_chars) {
307 SET_ERRNO(ERROR_INVALID_PARAM, "Client requested custom palette but custom_chars is NULL");
308 return -1;
309 }
310
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");
314 return -1;
315 }
316 if (len_to_use >= 256) {
317 SET_ERRNO(ERROR_INVALID_PARAM, "Client custom palette too long: %zu chars", len_to_use);
318 return -1;
319 }
320
321 // Validate custom palette
322 if (!validate_palette_chars(custom_chars, len_to_use)) {
323 SET_ERRNO(ERROR_INVALID_PARAM, "Client custom palette validation failed");
324 return -1;
325 }
326
327 chars_to_use = custom_chars;
328 } else {
329 palette = get_builtin_palette(palette_type);
330 if (!palette) {
331 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid client palette type: %d", palette_type);
332 return -1;
333 }
334
335 chars_to_use = palette->chars;
336 len_to_use = strlen(palette->chars); // Use actual byte count for UTF-8, not character count
337
338 // Skip validation for built-in palettes since they're already validated
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);
341 }
342
343 // Copy palette to client cache
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;
347
348 // Build client-specific luminance mapping
349 if (build_client_luminance_palette(chars_to_use, len_to_use, client_luminance_palette) != 0) {
350 SET_ERRNO(ERROR_INVALID_STATE, "Failed to build client luminance palette");
351 return -1;
352 }
353
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]);
356
357 return 0;
358}
359
360/* UTF-8 Palette Functions Implementation */
361
362// Create a UTF-8 palette structure from a string
363utf8_palette_t *utf8_palette_create(const char *palette_string) {
364 if (!palette_string || *palette_string == '\0') {
365 return NULL;
366 }
367
368 utf8_palette_t *palette;
369 palette = SAFE_MALLOC(sizeof(utf8_palette_t), utf8_palette_t *);
370
371 // Count UTF-8 characters (not bytes)
372 size_t char_count = 0;
373 const char *p = palette_string;
374 size_t total_bytes = strlen(palette_string);
375
376 // First pass: count characters
377 size_t bytes_processed = 0;
378 while (bytes_processed < total_bytes) {
379 int bytes = 1;
380 unsigned char c = (unsigned char)p[0];
381
382 if ((c & 0x80) == 0) {
383 bytes = 1; // ASCII
384 } else if ((c & 0xE0) == 0xC0) {
385 bytes = 2; // 2-byte UTF-8
386 } else if ((c & 0xF0) == 0xE0) {
387 bytes = 3; // 3-byte UTF-8
388 } else if ((c & 0xF8) == 0xF0) {
389 bytes = 4; // 4-byte UTF-8
390 }
391
392 // Verify we have enough bytes left
393 if (bytes_processed + bytes > total_bytes) {
394 break;
395 }
396
397 p += bytes;
398 bytes_processed += bytes;
399 char_count++;
400 }
401
402 // Validate we got at least one character
403 if (char_count == 0) {
404 SET_ERRNO(ERROR_INVALID_PARAM, "Palette string contains no valid UTF-8 characters");
405 SAFE_FREE(palette);
406 return NULL;
407 }
408
409 // Allocate character array
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 *);
412 // Explicit NULL checks to satisfy static analyzer (SAFE_MALLOC calls FATAL on failure)
413 if (palette->chars == NULL || palette->raw_string == NULL) {
414 SAFE_FREE(palette->chars);
415 SAFE_FREE(palette->raw_string);
416 SAFE_FREE(palette);
417 SET_ERRNO(ERROR_MEMORY, "Failed to allocate palette character array");
418 return NULL;
419 }
420
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; // Use strlen() value
424
425 // Second pass: parse characters
426 p = palette_string;
427 size_t char_idx = 0;
428 bytes_processed = 0;
429
430 // Set locale for wcwidth - save a copy of the old locale
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));
435 }
436 (void)setlocale(LC_CTYPE, "");
437
438 while (char_idx < char_count && bytes_processed < total_bytes) {
439 utf8_char_info_t *char_info = &palette->chars[char_idx];
440
441 // Determine UTF-8 byte length
442 unsigned char c = (unsigned char)*p;
443 int bytes = 1;
444
445 if ((c & 0x80) == 0) {
446 bytes = 1;
447 } else if ((c & 0xE0) == 0xC0) {
448 bytes = 2;
449 } else if ((c & 0xF0) == 0xE0) {
450 bytes = 3;
451 } else if ((c & 0xF8) == 0xF0) {
452 bytes = 4;
453 }
454
455 // Verify we have enough bytes left
456 if (bytes_processed + bytes > total_bytes) {
457 break;
458 }
459
460 // Copy bytes and null-terminate
461 SAFE_MEMCPY(char_info->bytes, bytes, p, bytes);
462 if (bytes < 4) {
463 SAFE_MEMSET(char_info->bytes + bytes, 4 - bytes, 0, 4 - bytes);
464 }
465 char_info->byte_len = bytes;
466
467 // Get display width using utf8_display_width
468 int width = utf8_display_width_n(p, bytes);
469 char_info->display_width = (width > 0 && width <= 2) ? width : 1;
470
471 p += bytes;
472 bytes_processed += bytes;
473 char_idx++;
474 }
475
476 // Restore locale
477 if (old_locale[0] != '\0') {
478 (void)setlocale(LC_CTYPE, old_locale);
479 }
480
481 return palette;
482}
483
484// Destroy a UTF-8 palette structure
485void utf8_palette_destroy(utf8_palette_t *palette) {
486 if (palette) {
487 SAFE_FREE(palette->chars);
488 SAFE_FREE(palette->raw_string);
489 SAFE_FREE(palette);
490 }
491}
492
493// Get the nth character from the palette
494const utf8_char_info_t *utf8_palette_get_char(const utf8_palette_t *palette, size_t index) {
495 if (!palette || index >= palette->char_count) {
496 return NULL;
497 }
498 return &palette->chars[index];
499}
500
501// Get the number of characters in the palette
502size_t utf8_palette_get_char_count(const utf8_palette_t *palette) {
503 if (!palette) {
504 return 0;
505 }
506 return palette->char_count;
507}
508
509// Check if palette contains a specific UTF-8 character
510bool utf8_palette_contains_char(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes) {
511 if (!palette || !utf8_char || char_bytes == 0 || char_bytes > 4) {
512 return false;
513 }
514
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) {
518 return true;
519 }
520 }
521
522 return false;
523}
524
525// Find the index of a UTF-8 character in the palette
526size_t utf8_palette_find_char_index(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes) {
527 if (!palette || !utf8_char || char_bytes == 0 || char_bytes > 4) {
528 return (size_t)-1;
529 }
530
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) {
534 return i;
535 }
536 }
537
538 return (size_t)-1;
539}
540
541// Find all indices of a UTF-8 character in the palette (handles duplicates)
542// Returns the number of indices found, fills indices array up to max_indices
543size_t utf8_palette_find_all_char_indices(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes,
544 size_t *indices, size_t max_indices) {
545 if (!palette || !utf8_char || char_bytes == 0 || char_bytes > 4 || !indices || max_indices == 0) {
546 return 0;
547 }
548
549 size_t found_count = 0;
550
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;
555 }
556 }
557
558 return found_count;
559}
void utf8_palette_destroy(utf8_palette_t *palette)
Definition palette.c:485
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)
Definition palette.c:543
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])
Definition palette.c:299
bool palette_requires_utf8_encoding(const char *chars, size_t len)
Definition palette.c:55
size_t utf8_palette_get_char_count(const utf8_palette_t *palette)
Definition palette.c:502
bool detect_client_utf8_support(utf8_capabilities_t *caps)
Definition palette.c:148
palette_type_t select_compatible_palette(palette_type_t requested, bool client_utf8)
Definition palette.c:219
const size_t DEFAULT_ASCII_PALETTE_LEN
Definition palette.c:26
const char DEFAULT_ASCII_PALETTE[]
Definition palette.c:25
const palette_def_t * get_builtin_palette(palette_type_t type)
Definition palette.c:47
int apply_palette_config(palette_type_t type, const char *custom_chars)
Definition palette.c:250
int build_client_luminance_palette(const char *palette_chars, size_t palette_len, char luminance_mapping[256])
Definition palette.c:279
size_t utf8_palette_find_char_index(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes)
Definition palette.c:526
bool validate_palette_chars(const char *chars, size_t len)
Definition palette.c:71
const utf8_char_info_t * utf8_palette_get_char(const utf8_palette_t *palette, size_t index)
Definition palette.c:494
bool utf8_palette_contains_char(const utf8_palette_t *palette, const char *utf8_char, size_t char_bytes)
Definition palette.c:510
utf8_palette_t * utf8_palette_create(const char *palette_string)
Definition palette.c:363
bool terminal_supports_utf8(void)
int utf8_display_width_n(const char *str, size_t max_bytes)
Definition utf8.c:92
size_t utf8_to_codepoints(const char *str, uint32_t *out_codepoints, size_t max_codepoints)
Definition utf8.c:187
int utf8_display_width(const char *str)
Definition utf8.c:46