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