ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
colorize.c
Go to the documentation of this file.
1
7#include <ascii-chat/log/colorize.h>
8#include <ascii-chat/log/logging.h>
9#include <ascii-chat/util/string.h>
10#include <ascii-chat/platform/system.h>
11#include <ascii-chat/platform/terminal.h>
12#include <ascii-chat/platform/util.h>
13#include <ascii-chat/video/ansi.h>
14#include <ctype.h>
15#include <string.h>
16#include <stdio.h>
17#include <stdbool.h>
18
26static bool is_known_unit(const char *str, size_t max_len) {
27 if (!str || max_len == 0) {
28 return false;
29 }
30
31 // Known units: byte sizes, time units, frequency, percentage, and count descriptors
32 const char *known_units[] = {
33 // Byte sizes
34 "B",
35 "KB",
36 "MB",
37 "GB",
38 "TB",
39 "PB",
40 "EB",
41 "KiB",
42 "MiB",
43 "GiB",
44 "TiB",
45 "PiB",
46 "EiB",
47 // Time
48 "ms",
49 "us",
50 "ns",
51 "ps",
52 "s",
53 "sec",
54 "second",
55 "seconds",
56 "m",
57 "min",
58 "minute",
59 "minutes",
60 "h",
61 "hr",
62 "hour",
63 "hours",
64 // Frequency
65 "Hz",
66 "kHz",
67 "MHz",
68 "GHz",
69 // Percentage
70 "%",
71 // Count descriptors (commonly used in logs)
72 "items",
73 "item",
74 "entries",
75 "entry",
76 "packets",
77 "packet",
78 "frames",
79 "frame",
80 "messages",
81 "message",
82 "connections",
83 "connection",
84 "clients",
85 "client",
86 "events",
87 "event",
88 "bytes",
89 "bits",
90 "retries",
91 "retry",
92 "attempts",
93 "attempt",
94 "chunks",
95 "chunk",
96 "blocks",
97 "block",
98 };
99
100 const size_t num_units = sizeof(known_units) / sizeof(known_units[0]);
101 for (size_t i = 0; i < num_units; i++) {
102 size_t unit_len = strlen(known_units[i]);
103 if (unit_len > max_len) {
104 continue;
105 }
106 // Case-insensitive comparison for units
107 if (platform_strncasecmp(str, known_units[i], unit_len) == 0 && (unit_len == max_len || !isalpha(str[unit_len]))) {
108 return true;
109 }
110 }
111
112 return false;
113}
114
126static bool is_numeric_pattern(const char *str, size_t pos, size_t *end_pos) {
127 if (!isdigit(str[pos]) && str[pos] != '.') {
128 return false;
129 }
130
131 // Don't match numbers in the middle of words - check if preceded by alphanumeric, hyphen, or underscore
132 if (pos > 0 && (isalnum(str[pos - 1]) || str[pos - 1] == '-' || str[pos - 1] == '_')) {
133 return false;
134 }
135
136 size_t i = pos;
137
138 // Handle hex numbers (0x...)
139 if (str[i] == '0' && (str[i + 1] == 'x' || str[i + 1] == 'X')) {
140 i += 2;
141 while (isxdigit(str[i])) {
142 i++;
143 }
144 *end_pos = i;
145 return true;
146 }
147
148 // Handle regular numbers and decimals
149 bool has_digit = false;
150 while (isdigit(str[i])) {
151 has_digit = true;
152 i++;
153 }
154
155 // Handle decimal point
156 if (str[i] == '.' && isdigit(str[i + 1])) {
157 i++;
158 while (isdigit(str[i])) {
159 i++;
160 }
161 has_digit = true;
162 }
163
164 if (!has_digit) {
165 return false;
166 }
167
168 // Check for dimension format (1920x1080)
169 if (str[i] == 'x' || str[i] == 'X') {
170 if (isdigit(str[i + 1])) {
171 i++;
172 while (isdigit(str[i])) {
173 i++;
174 }
175 }
176 }
177
178 // Check for fraction format (1/2, 3/4, etc.)
179 if (str[i] == '/' && isdigit(str[i + 1])) {
180 i++;
181 while (isdigit(str[i])) {
182 i++;
183 }
184 }
185
186 // Check for known units with optional spaces (e.g., "25 MB", "1024 GiB", "69.9%")
187 // Only include units if they're in the known units list to avoid colorizing random words
188 size_t j = i;
189 bool has_space = false;
190
191 // First try: units immediately following (no space) like "69.9%"
192 if ((isalpha(str[j]) || str[j] == '%') && (j == i || !isalnum(str[j - 1]))) {
193 size_t unit_start = j;
194 while ((isalpha(str[j]) || str[j] == '%') && (j - unit_start) < 32) {
195 j++;
196 }
197 size_t unit_len = j - unit_start;
198
199 // Check if this is a known unit
200 if (is_known_unit(str + unit_start, unit_len)) {
201 // It's a known unit immediately following, include it
202 i = j;
203 } else {
204 // Not a known unit, reset j
205 j = i;
206 }
207 }
208
209 // Second try: units with spaces (e.g., "25 MB")
210 if (i == j) {
211 while (str[j] == ' ' || str[j] == '\t') {
212 has_space = true;
213 j++;
214 }
215
216 if (has_space && (isalpha(str[j]) || str[j] == '%')) {
217 // Collect potential unit (up to 32 characters)
218 size_t unit_start = j;
219 while ((isalpha(str[j]) || str[j] == '%') && (j - unit_start) < 32) {
220 j++;
221 }
222 size_t unit_len = j - unit_start;
223
224 // Check if this is a known unit
225 if (is_known_unit(str + unit_start, unit_len)) {
226 // It's a known unit, include the space and unit
227 i = j;
228 }
229 // Otherwise, don't include - just the number (i stays as is)
230 }
231 }
232
233 *end_pos = i;
234 return true;
235}
236
250static bool is_file_path(const char *str, size_t pos, size_t *end_pos) {
251 size_t i = pos;
252 bool found = false;
253
254 // Check for Windows absolute path (C:\...)
255 if (pos > 0 && isalpha(str[pos]) && str[pos + 1] == ':' && str[pos + 2] == '\\') {
256 i = pos + 2; // Start after the colon
257 found = true;
258 }
259 // Check for Windows UNC path (\\server\share)
260 else if (str[pos] == '\\' && str[pos + 1] == '\\') {
261 i = pos + 2;
262 found = true;
263 }
264 // Check for Unix absolute path (/)
265 else if (str[pos] == '/') {
266 i = pos;
267 found = true;
268 }
269 // Check for relative path (., .., or alphanumeric followed by /)
270 else if ((str[pos] == '.' && (str[pos + 1] == '/' || str[pos + 1] == '\\')) ||
271 (str[pos] == '.' && str[pos + 1] == '.' && (str[pos + 2] == '/' || str[pos + 2] == '\\'))) {
272 i = pos;
273 found = true;
274 } else if (isalnum(str[pos]) || str[pos] == '_' || str[pos] == '-') {
275 // Could be start of relative path like "src/main.c"
276 // Look ahead for slash to confirm it's a path
277 size_t lookahead = pos;
278 while ((isalnum(str[lookahead]) || str[lookahead] == '_' || str[lookahead] == '-' || str[lookahead] == '.' ||
279 str[lookahead] == '/' || str[lookahead] == '\\') &&
280 str[lookahead] != '\0') {
281 if (str[lookahead] == '/' || str[lookahead] == '\\') {
282 // Found a slash, this is a path
283 i = pos;
284 found = true;
285 break;
286 }
287 lookahead++;
288 }
289 }
290
291 if (!found) {
292 return false;
293 }
294
295 // Collect the path characters
296 while (str[i] != '\0' && str[i] != ' ' && str[i] != '\t' && str[i] != '\n' && str[i] != ':' && str[i] != ',' &&
297 str[i] != ')' && str[i] != ']' && str[i] != '}' && str[i] != '"' && str[i] != '\'') {
298 if ((str[i] == '/' || str[i] == '\\') || isalnum(str[i]) || strchr("._-~", str[i])) {
299 i++;
300 } else {
301 break;
302 }
303 }
304
305 // Must have at least one slash to be a valid path
306 bool has_slash = false;
307 for (size_t j = pos; j < i; j++) {
308 if (str[j] == '/' || str[j] == '\\') {
309 has_slash = true;
310 break;
311 }
312 }
313
314 if (!has_slash) {
315 return false;
316 }
317
318 *end_pos = i;
319 return true;
320}
321
332static bool is_url(const char *str, size_t pos, size_t *end_pos) {
333 // Check for common URL schemes: http, https, ftp, ws, wss
334 const char *schemes[] = {"https://", "http://", "ftp://", "wss://", "ws://"};
335
336 // Try each scheme
337 for (size_t s = 0; s < sizeof(schemes) / sizeof(schemes[0]); s++) {
338 size_t scheme_len = strlen(schemes[s]);
339 if (strncmp(str + pos, schemes[s], scheme_len) == 0) {
340 // Found the scheme, now collect the URL
341 size_t i = pos + scheme_len;
342
343 // URL continues until whitespace, common punctuation, or special chars
344 // Stop at: space, tab, newline, ), ], }, ", ', <, >
345 while (str[i] != '\0' && str[i] != ' ' && str[i] != '\t' && str[i] != '\n' && str[i] != ')' && str[i] != ']' &&
346 str[i] != '}' && str[i] != '"' && str[i] != '\'' && str[i] != '<' && str[i] != '>' && str[i] != ',') {
347 i++;
348 }
349
350 // Must have at least one character after the scheme
351 if (i > pos + scheme_len) {
352 *end_pos = i;
353 return true;
354 }
355 }
356 }
357
358 return false;
359}
360
371static bool is_env_var(const char *str, size_t pos, size_t *end_pos) {
372 if (str[pos] != '$') {
373 return false;
374 }
375
376 size_t i = pos + 1;
377
378 // Environment variables should be all caps or underscore
379 if (!isupper(str[i]) && str[i] != '_') {
380 return false;
381 }
382
383 while ((isupper(str[i]) || str[i] == '_' || isdigit(str[i])) && str[i] != '\0') {
384 i++;
385 }
386
387 // Must have at least 2 characters ($X is too short)
388 if (i - pos < 2) {
389 return false;
390 }
391
392 *end_pos = i;
393 return true;
394}
395
402static log_color_t get_value_color(const char *value) {
403 if (!value || *value == '\0') {
404 return LOG_COLOR_FATAL; // Default to magenta for empty/unknown
405 }
406
407 size_t end_pos = 0;
408
409 // Try to detect value type in order of specificity
410 if (is_numeric_pattern(value, 0, &end_pos)) {
411 return LOG_COLOR_GREY; // Grey for numbers (matching line numbers and tid)
412 }
413
414 if (is_url(value, 0, &end_pos)) {
415 return LOG_COLOR_INFO; // Blue for URLs
416 }
417
418 if (is_file_path(value, 0, &end_pos)) {
419 return LOG_COLOR_DEBUG; // Cyan/Blue for paths
420 }
421
422 if (is_env_var(value, 0, &end_pos)) {
423 return LOG_COLOR_GREY; // Grey for environment variables
424 }
425
426 // Check for quoted strings or common value patterns
427 if ((value[0] == '"' || value[0] == '\'' || value[0] == '`') ||
428 strchr(value, ' ') != NULL) { // Space indicates likely string value
429 return LOG_COLOR_FATAL; // Magenta for strings/unknown values
430 }
431
432 // Default: magenta for unrecognized values
433 return LOG_COLOR_FATAL;
434}
435
449static bool is_key_value_pair(const char *str, size_t pos, size_t *key_end, size_t *value_start, size_t *value_end) {
450 // Check if we're at the start of an identifier (key)
451 if (!isalpha(str[pos]) && str[pos] != '_') {
452 return false;
453 }
454
455 // Must not be preceded by alphanumeric or underscore (to avoid matching mid-word)
456 if (pos > 0 && (isalnum(str[pos - 1]) || str[pos - 1] == '_')) {
457 return false;
458 }
459
460 size_t i = pos;
461
462 // Collect the key (alphanumeric and underscores)
463 while ((isalnum(str[i]) || str[i] == '_') && str[i] != '\0') {
464 i++;
465 }
466
467 size_t key_len = i - pos;
468
469 // Check for equals sign
470 if (str[i] != '=') {
471 return false;
472 }
473
474 i++; // Skip the equals sign
475
476 // Skip optional whitespace after equals (though unusual)
477 while (str[i] == ' ' || str[i] == '\t') {
478 i++;
479 }
480
481 // Record where the value actually starts
482 size_t val_start = i;
483
484 // Value continues until we hit whitespace or special ending characters
485 // Stop at: space, tab, newline, comma, semicolon, ), ], }, or null terminator
486 while (str[i] != '\0' && str[i] != ' ' && str[i] != '\t' && str[i] != '\n' && str[i] != ',' && str[i] != ';' &&
487 str[i] != ')' && str[i] != ']' && str[i] != '}') {
488 i++;
489 }
490
491 size_t value_len = i - val_start;
492
493 // Must have a non-empty value
494 if (value_len == 0) {
495 return false;
496 }
497
498 *key_end = pos + key_len; // Position after the key
499 *value_start = val_start; // Position where value begins
500 *value_end = i; // Position after the value
501 return true;
502}
503
513const char *colorize_log_message(const char *message) {
514 if (!message) {
515 return message;
516 }
517
518 // Check if colors should be used based on TTY status (same as ASCII art)
519 // Colors are only applied when output is actually a TTY
520 bool should_colorize = terminal_should_color_output(STDOUT_FILENO);
521
522 if (!should_colorize) {
523 return message;
524 }
525
526 // Use 4 static buffers for rotation (handles nested calls)
527 static char buffers[4][4096];
528 static int buffer_index = 0;
529
530 char *output = buffers[buffer_index];
531 buffer_index = (buffer_index + 1) % 4;
532
533 size_t out_pos = 0;
534 const size_t max_size = sizeof(buffers[0]);
535
536 // Process the input string
537 for (size_t i = 0; message[i] != '\0' && out_pos < max_size - 100; i++) {
538 size_t end_pos = 0;
539
540 // Check if already colorized - only colorize if NOT already colored
541 bool can_colorize = !ansi_is_already_colorized(message, i);
542
543 // Try key=value pair first (highest priority pattern)
544 size_t key_end = 0, value_start = 0, value_end = 0;
545 if (can_colorize && is_key_value_pair(message, i, &key_end, &value_start, &value_end)) {
546 size_t key_len = key_end - i;
547 size_t value_len = value_end - value_start;
548
549 // Extract key
550 char key_buf[256];
551 safe_snprintf(key_buf, sizeof(key_buf), "%.*s", (int)key_len, message + i);
552
553 // Extract value
554 char value_buf[512];
555 safe_snprintf(value_buf, sizeof(value_buf), "%.*s", (int)value_len, message + value_start);
556
557 // Color the key (magenta)
558 const char *colored_key = colored_string(LOG_COLOR_FATAL, key_buf);
559 size_t colored_key_len = strlen(colored_key);
560
561 // Check if we have enough space for key + equals + value
562 // (accounting for ANSI codes which will be added by colored_string)
563 if (out_pos + colored_key_len + 1 + value_len + 100 < max_size) {
564 memcpy(output + out_pos, colored_key, colored_key_len);
565 out_pos += colored_key_len;
566
567 // Add uncolored equals sign
568 output[out_pos++] = '=';
569
570 // Determine value color and add colored value
571 log_color_t value_color = get_value_color(value_buf);
572 const char *colored_value = colored_string(value_color, value_buf);
573 size_t colored_value_len = strlen(colored_value);
574 if (out_pos + colored_value_len < max_size) {
575 memcpy(output + out_pos, colored_value, colored_value_len);
576 out_pos += colored_value_len;
577 }
578 }
579 i = value_end - 1;
580 continue;
581 }
582
583 // Try numeric pattern
584 if (can_colorize && is_numeric_pattern(message, i, &end_pos)) {
585 size_t pattern_len = end_pos - i;
586 char pattern_buf[256];
587 safe_snprintf(pattern_buf, sizeof(pattern_buf), "%.*s", (int)pattern_len, message + i);
588
589 const char *colored = colored_string(LOG_COLOR_GREY, pattern_buf);
590 size_t colored_len = strlen(colored);
591 if (out_pos + colored_len < max_size) {
592 memcpy(output + out_pos, colored, colored_len);
593 out_pos += colored_len;
594 }
595 i = end_pos - 1;
596 continue;
597 }
598
599 // Try file path
600 if (can_colorize && is_file_path(message, i, &end_pos)) {
601 size_t path_len = end_pos - i;
602 char path_buf[512];
603 safe_snprintf(path_buf, sizeof(path_buf), "%.*s", (int)path_len, message + i);
604
605 const char *colored = colored_string(LOG_COLOR_DEBUG, path_buf);
606 size_t colored_len = strlen(colored);
607 if (out_pos + colored_len < max_size) {
608 memcpy(output + out_pos, colored, colored_len);
609 out_pos += colored_len;
610 }
611 i = end_pos - 1;
612 continue;
613 }
614
615 // Try environment variable
616 if (can_colorize && is_env_var(message, i, &end_pos)) {
617 size_t var_len = end_pos - i;
618 char var_buf[256];
619 safe_snprintf(var_buf, sizeof(var_buf), "%.*s", (int)var_len, message + i);
620
621 const char *colored = colored_string(LOG_COLOR_GREY, var_buf);
622 size_t colored_len = strlen(colored);
623 if (out_pos + colored_len < max_size) {
624 memcpy(output + out_pos, colored, colored_len);
625 out_pos += colored_len;
626 }
627 i = end_pos - 1;
628 continue;
629 }
630
631 // Try URL
632 if (can_colorize && is_url(message, i, &end_pos)) {
633 size_t url_len = end_pos - i;
634 char url_buf[2048];
635 safe_snprintf(url_buf, sizeof(url_buf), "%.*s", (int)url_len, message + i);
636
637 const char *colored = colored_string(LOG_COLOR_INFO, url_buf);
638 size_t colored_len = strlen(colored);
639 if (out_pos + colored_len < max_size) {
640 memcpy(output + out_pos, colored, colored_len);
641 out_pos += colored_len;
642 }
643 i = end_pos - 1;
644 continue;
645 }
646
647 // Regular character - just copy
648 if (out_pos < max_size - 1) {
649 output[out_pos++] = message[i];
650 }
651 }
652
653 output[out_pos] = '\0';
654 return output;
655}
bool ansi_is_already_colorized(const char *message, size_t pos)
Definition ansi.c:59
const char * colorize_log_message(const char *message)
Colorize a log message for terminal output.
Definition colorize.c:513
bool terminal_should_color_output(int fd)
Determine if color output should be used.
int platform_strncasecmp(const char *s1, const char *s2, size_t n)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
const char * colored_string(log_color_t color, const char *text)