ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
grep.c
Go to the documentation of this file.
1
16#include <ascii-chat/log/grep.h>
17#include <ascii-chat/common.h>
18#include <ascii-chat/log/log.h>
26
27#include <string.h>
28#include <stdint.h>
29
30#ifdef _WIN32
31#include <windows.h>
32#else
33#include <sched.h>
34#endif
35
43#define HIGHLIGHT_DARK_BG 70 // Dark grey for dark theme backgrounds
44#define HIGHLIGHT_LIGHT_BG 200 // Light grey for light theme backgrounds
45
52#define MIN_HIGHLIGHT_DISTANCE 40
53
58static inline float calculate_luminance(uint8_t r, uint8_t g, uint8_t b) {
59 return (0.2126f * r + 0.7152f * g + 0.0722f * b) / 255.0f;
60}
61
76
80static struct {
84 bool enabled;
87
88 // Context line buffering
89 char **line_buffer;
94
95 // Save/restore for interactive grep
100
101 // Cached highlight color (avoid querying terminal every render)
104} g_filter_state = {
105 .patterns = NULL,
106 .pattern_count = 0,
107 .pattern_capacity = 0,
108 .enabled = false,
109 .match_data_lifecycle = LIFECYCLE_INIT,
110 .line_buffer = NULL,
111 .buffer_size = 0,
112 .buffer_pos = 0,
113 .lines_after_match = 0,
114 .max_context_after = 0,
115 .saved_patterns = NULL,
116 .saved_pattern_count = 0,
117 .saved_pattern_capacity = 0,
118 .saved_enabled = false,
119 .cached_highlight_r = 70,
120 .cached_highlight_g = 70,
121 .cached_highlight_b = 70,
122 .last_color_query_us = 0,
123};
124
128static void destroy_match_data(void *data) {
129 if (data) {
130 pcre2_match_data_free((pcre2_match_data *)data);
131 }
132}
133
137static void create_match_data_key(void) {
138 ascii_tls_key_create(&g_filter_state.match_data_key, destroy_match_data);
139}
140
149static void get_highlight_color(uint8_t *r, uint8_t *g, uint8_t *b) {
150 // Get current time for cache validation
152 uint64_t cache_age_us = now_us - g_filter_state.last_color_query_us;
153
154 // Use cached color if recent (refresh every 2 seconds)
155 if (cache_age_us < 2 * US_PER_SEC_INT) {
156 *r = g_filter_state.cached_highlight_r;
157 *g = g_filter_state.cached_highlight_g;
158 *b = g_filter_state.cached_highlight_b;
159 return;
160 }
161
162 // Query terminal color (only every 2 seconds)
163 // Note: Skip querying during interactive grep mode to avoid PTY state issues
164 // that can cause spurious ESC bytes in stdin
165 uint8_t bg_r = 0, bg_g = 0, bg_b = 0;
166 bool has_bg_color = log_search_is_active() ? false : terminal_query_background_color(&bg_r, &bg_g, &bg_b);
167
168 bool is_dark;
169 if (has_bg_color) {
170 // Calculate luminance from actual background color
171 float luminance = calculate_luminance(bg_r, bg_g, bg_b);
172 is_dark = (luminance < 0.5f); // Dark if luminance < 50%
173 } else {
174 // Fall back to heuristic detection
176 }
177
178 // Choose highlight: dark bg = dark highlight, light bg = light highlight
180
181 // If we have the actual background color, check if it's too close to our grey
182 if (has_bg_color) {
183 uint8_t bg_grey = (uint8_t)((bg_r + bg_g + bg_b) / 3);
184 int distance = abs((int)bg_grey - (int)grey);
185
186 // If background is too close to our grey, use black/white for maximum contrast
187 if (distance < MIN_HIGHLIGHT_DISTANCE) {
188 grey = is_dark ? 0 : 255; // Black for dark terminals, white for light
189 }
190 }
191
192 // Cache the result
193 g_filter_state.cached_highlight_r = grey;
194 g_filter_state.cached_highlight_g = grey;
195 g_filter_state.cached_highlight_b = grey;
196 g_filter_state.last_color_query_us = now_us;
197
198 *r = grey;
199 *g = grey;
200 *b = grey;
201}
202
210static size_t map_plain_to_colored_pos(const char *colored_text, size_t char_pos) {
211 size_t byte_pos = 0;
212 size_t chars_seen = 0;
213
214 while (colored_text[byte_pos] != '\0' && chars_seen < char_pos) {
215 // Check for ANSI escape sequence (handle all escape types, not just CSI)
216 if (colored_text[byte_pos] == '\x1b') {
217 byte_pos++;
218 // Check if there's a next byte before reading it
219 if (colored_text[byte_pos] == '\0') {
220 // Incomplete escape sequence at end of string, just break
221 break;
222 }
223 unsigned char next = (unsigned char)colored_text[byte_pos];
224 if (next == '[') {
225 // CSI sequence: \x1b[...final_byte (where final byte is 0x40-0x7E)
226 byte_pos++;
227 while (colored_text[byte_pos] != '\0') {
228 unsigned char c = (unsigned char)colored_text[byte_pos];
229 byte_pos++;
230 // Final byte ends the sequence (0x40-0x7E)
231 if (c >= 0x40 && c <= 0x7E) {
232 break;
233 }
234 }
235 } else if (next >= 0x40 && next <= 0x7E) {
236 // 2-byte Fe sequence: \x1b + final_byte (e.g., \x1b7, \x1b8)
237 byte_pos++;
238 } else if (next == '(' || next == ')' || next == '*' || next == '+') {
239 // Designate character set sequences: \x1b( + charset (3 bytes total)
240 byte_pos++; // skip designator
241 if (colored_text[byte_pos] != '\0') {
242 byte_pos++; // skip charset ID
243 }
244 } else {
245 // Unknown escape sequence type, try to skip conservatively
246 if (colored_text[byte_pos] != '\0') {
247 byte_pos++;
248 }
249 }
250 } else {
251 // Regular character - decode UTF-8 to get byte length
252 uint32_t codepoint;
253 int utf8_len = utf8_decode((const uint8_t *)(colored_text + byte_pos), &codepoint);
254 if (utf8_len < 0) {
255 utf8_len = 1; // Invalid UTF-8, treat as single byte
256 }
257
258 // Advance by all bytes of this UTF-8 character
259 byte_pos += utf8_len;
260
261 // Only increment character count once per character (not per byte)
262 chars_seen++;
263 }
264 }
265
266 return byte_pos;
267}
268
272static void ensure_match_data_key_initialized(void) {
273 if (!lifecycle_init_once(&g_filter_state.match_data_lifecycle)) {
274 return; // Already initialized, spin-wait completed
275 }
276 // Winner of init_once - do the initialization work
277 create_match_data_key();
278 lifecycle_init_commit(&g_filter_state.match_data_lifecycle);
279}
280
284static pcre2_match_data *get_thread_match_data(void) {
285 ensure_match_data_key_initialized();
286
287 pcre2_match_data *data = ascii_tls_get(g_filter_state.match_data_key);
288 if (!data) {
289 // Find any regex pattern to create match data from
290 for (int i = 0; i < g_filter_state.pattern_count; i++) {
291 if (g_filter_state.patterns[i].singleton) {
292 pcre2_code *code = asciichat_pcre2_singleton_get_code(g_filter_state.patterns[i].singleton);
293 if (code) {
294 data = pcre2_match_data_create_from_pattern(code, NULL);
295 if (data) {
296 ascii_tls_set(g_filter_state.match_data_key, data);
297 break;
298 }
299 }
300 }
301 }
302 }
303
304 return data;
305}
306
307// Use the shared parse_result_t from filter.h
309
352 parse_result_t result = {0};
353 result.pcre2_options = PCRE2_UTF | PCRE2_UCP; // Default: UTF-8 mode
354
355 if (!input || strlen(input) == 0) {
356 return result; // Invalid: empty pattern
357 }
358
359 size_t len = strlen(input);
360 const char *closing_slash = NULL;
361 const char *pattern_start = input;
362
363 // Check if pattern uses /pattern/flags format (explicit)
364 if (input[0] == '/') {
365 // Format 1: /pattern/flags
366 if (len < 3) {
367 return result; // Invalid: too short for /pattern/ format
368 }
369
370 // Find closing slash - don't treat backslash as an escape character
371 // (backslashes are regex escapes, not delimiter escapes)
372 closing_slash = strchr(input + 1, '/');
373 if (!closing_slash) {
374 return result; // Invalid: missing closing /
375 }
376
377 pattern_start = input + 1;
378 } else {
379 // Check for pattern/flags format (implicit, no leading slash)
380 // Find the last forward slash to use as delimiter
381 closing_slash = strrchr(input, '/');
382
383 const char *implicit_flags = closing_slash ? closing_slash + 1 : NULL;
384 bool implicit_flags_valid = closing_slash && closing_slash > input && *implicit_flags != '\0';
385 if (implicit_flags_valid) {
386 for (const char *flag = implicit_flags; *flag; flag++) {
387 if (!strchr("imsxgIF", *flag) && *flag != 'A' && *flag != 'B' && *flag != 'C' &&
388 !(*flag >= '0' && *flag <= '9')) {
389 implicit_flags_valid = false;
390 break;
391 }
392 }
393 }
394
395 if (implicit_flags_valid) {
396 // There's at least one slash (not at the beginning)
397 // Try to parse as pattern/flags format
398 pattern_start = input;
399 } else {
400 // No slash or slash at beginning - treat as plain pattern
401 closing_slash = NULL;
402 }
403 }
404
405 if (closing_slash) {
406 // Extract pattern between start and closing slash
407 size_t pattern_len = (size_t)(closing_slash - pattern_start);
408 if (pattern_len == 0) {
409 return result; // Invalid: empty pattern
410 }
411 if (pattern_len >= sizeof(result.pattern)) {
412 pattern_len = sizeof(result.pattern) - 1;
413 }
414 memcpy(result.pattern, pattern_start, pattern_len);
415 result.pattern[pattern_len] = '\0';
416
417 // Parse flags after closing slash
418 // First pass: check for F flag
419 const char *flags = closing_slash + 1;
420 bool has_F_flag = (strchr(flags, 'F') != NULL);
421
422 // Parse all flags
423 for (const char *p = flags; *p; p++) {
424 char c = *p;
425
426 // Single-character flags
427 if (c == 'i') {
428 result.pcre2_options |= PCRE2_CASELESS;
429 result.case_insensitive = true;
430 } else if (c == 'm') {
431 result.pcre2_options |= PCRE2_MULTILINE;
432 } else if (c == 's') {
433 result.pcre2_options |= PCRE2_DOTALL;
434 } else if (c == 'x') {
435 result.pcre2_options |= PCRE2_EXTENDED;
436 } else if (c == 'g') {
437 result.global_flag = true;
438 } else if (c == 'I') {
439 result.invert = true;
440 } else if (c == 'F') {
441 result.is_fixed_string = true;
442 }
443 // Multi-character flags with integers
444 else if (c == 'A') {
445 p++; // Move to digits
446 int num = 0;
447 while (*p >= '0' && *p <= '9') {
448 num = num * 10 + (*p - '0');
449 p++;
450 }
451 p--; // Back up one (for loop will increment)
452 result.context_after = (num > 0) ? num : 1; // Default to 1 if no number
453 } else if (c == 'B') {
454 p++;
455 int num = 0;
456 while (*p >= '0' && *p <= '9') {
457 num = num * 10 + (*p - '0');
458 p++;
459 }
460 p--;
461 result.context_before = (num > 0) ? num : 1;
462 } else if (c == 'C') {
463 p++;
464 int num = 0;
465 while (*p >= '0' && *p <= '9') {
466 num = num * 10 + (*p - '0');
467 p++;
468 }
469 p--;
470 int ctx = (num > 0) ? num : 1;
471 result.context_before = ctx;
472 result.context_after = ctx;
473 } else {
474 // Invalid flag character - only error if not using F flag
475 if (!has_F_flag) {
476 return result; // Invalid
477 }
478 // Otherwise ignore invalid flags (they're part of the fixed string context)
479 }
480 }
481 } else {
482 // Format 3: Plain pattern without slashes (treat as regex, no flags)
483 size_t pattern_len = len;
484 if (pattern_len >= sizeof(result.pattern)) {
485 pattern_len = sizeof(result.pattern) - 1;
486 }
487 memcpy(result.pattern, input, pattern_len);
488 result.pattern[pattern_len] = '\0';
489
490 // No flags for plain format - just use default options
491 // (PCRE2_UTF | PCRE2_UCP already set at the top)
492 }
493
494 result.valid = true;
495 return result;
496}
497
498asciichat_error_t grep_init(const char *pattern) {
499 // Reject NULL or empty patterns
500 if (!pattern) {
501 return SET_ERRNO(ERROR_INVALID_PARAM, "Pattern cannot be NULL");
502 }
503 if (strlen(pattern) == 0) {
504 return SET_ERRNO(ERROR_INVALID_PARAM, "Pattern cannot be empty");
505 }
506
507 // Parse pattern
508 parse_result_t parsed = grep_parse_pattern(pattern);
509 if (!parsed.valid) {
510 log_error("Invalid --grep pattern format: \"%s\"", pattern);
511 log_error("Use /pattern/flags format (e.g., \"/query/ig\") or plain regex (e.g., \"query\")");
512 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid --grep pattern format");
513 }
514
515 // Allocate or expand pattern array
516 if (g_filter_state.pattern_count >= g_filter_state.pattern_capacity) {
517 int new_capacity = (g_filter_state.pattern_capacity == 0) ? 4 : g_filter_state.pattern_capacity * 2;
518 grep_pattern_t *new_patterns =
519 SAFE_REALLOC(g_filter_state.patterns, new_capacity * sizeof(grep_pattern_t), grep_pattern_t *);
520 if (!new_patterns) {
521 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate pattern array");
522 }
523 g_filter_state.patterns = new_patterns;
524 g_filter_state.pattern_capacity = new_capacity;
525 }
526
527 // Add new pattern
528 grep_pattern_t *new_pat = &g_filter_state.patterns[g_filter_state.pattern_count];
529 memset(new_pat, 0, sizeof(*new_pat));
530
531 // Make a copy of the original pattern string (argv pointers may be deallocated)
532 char *original_copy = SAFE_MALLOC(strlen(pattern) + 1, char *);
533 if (!original_copy) {
534 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate original pattern string");
535 }
536 memcpy(original_copy, pattern, strlen(pattern) + 1);
537 new_pat->original = original_copy;
538
539 new_pat->parsed_pattern = SAFE_MALLOC(strlen(parsed.pattern) + 1, char *);
540 if (!new_pat->parsed_pattern) {
541 SAFE_FREE(original_copy);
542 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate pattern string");
543 }
544 memcpy(new_pat->parsed_pattern, parsed.pattern, strlen(parsed.pattern) + 1);
545
546 new_pat->is_fixed_string = parsed.is_fixed_string;
547 new_pat->case_insensitive = parsed.case_insensitive;
548 new_pat->invert = parsed.invert;
549 new_pat->global_flag = parsed.global_flag;
550 new_pat->context_before = parsed.context_before;
551 new_pat->context_after = parsed.context_after;
552
553 // Compile regex (skip for fixed strings)
554 if (!parsed.is_fixed_string) {
556 if (!new_pat->singleton) {
557 SAFE_FREE(new_pat->parsed_pattern);
558 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to compile regex pattern");
559 }
560
561 // Validate compilation
562 pcre2_code *code = asciichat_pcre2_singleton_get_code(new_pat->singleton);
563 if (!code) {
564 SAFE_FREE(new_pat->parsed_pattern);
565 return SET_ERRNO(ERROR_INVALID_PARAM, "Regex compilation failed");
566 }
567 }
568
569 g_filter_state.pattern_count++;
570
571 // Update context buffer size if needed
572 if (parsed.context_before > g_filter_state.buffer_size) {
573 // Reallocate line buffer
574 char **new_buffer = SAFE_REALLOC(g_filter_state.line_buffer, parsed.context_before * sizeof(char *), char **);
575 if (!new_buffer) {
576 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate context line buffer");
577 }
578 // Initialize new slots
579 for (int i = g_filter_state.buffer_size; i < parsed.context_before; i++) {
580 new_buffer[i] = NULL;
581 }
582 g_filter_state.line_buffer = new_buffer;
583 g_filter_state.buffer_size = parsed.context_before;
584 }
585
586 // Update max context_after
587 if (parsed.context_after > g_filter_state.max_context_after) {
588 g_filter_state.max_context_after = parsed.context_after;
589 }
590
591 g_filter_state.enabled = true;
592 log_info("Added --grep pattern #%d: %s", g_filter_state.pattern_count, pattern);
593
594 return ASCIICHAT_OK;
595}
596
597bool grep_should_output(const char *log_line, size_t *match_start, size_t *match_len) {
598 // Reject NULL lines
599 if (!log_line) {
600 if (match_start)
601 *match_start = 0;
602 if (match_len)
603 *match_len = 0;
604 return false;
605 }
606
607 // If filtering disabled, output everything
608 if (!g_filter_state.enabled) {
609 return true;
610 }
611
612 // Check if we're in "context after" mode (outputting lines after a match)
613 if (g_filter_state.lines_after_match > 0) {
614 g_filter_state.lines_after_match--;
615 *match_start = 0;
616 *match_len = 0;
617
618 // Store in buffer even during context-after mode
619 if (g_filter_state.buffer_size > 0) {
620 SAFE_FREE(g_filter_state.line_buffer[g_filter_state.buffer_pos]);
621 size_t line_len = strlen(log_line) + 1;
622 g_filter_state.line_buffer[g_filter_state.buffer_pos] = SAFE_MALLOC(line_len, char *);
623 if (g_filter_state.line_buffer[g_filter_state.buffer_pos]) {
624 memcpy(g_filter_state.line_buffer[g_filter_state.buffer_pos], log_line, line_len);
625 }
626 g_filter_state.buffer_pos = (g_filter_state.buffer_pos + 1) % g_filter_state.buffer_size;
627 }
628
629 return true; // Output context line
630 }
631
632 pcre2_match_data *match_data = get_thread_match_data();
633 size_t line_len = strlen(log_line);
634
635 // Try each pattern (OR logic)
636 bool any_match = false;
637 grep_pattern_t *matched_pattern = NULL;
638
639 for (int i = 0; i < g_filter_state.pattern_count; i++) {
640 grep_pattern_t *pat = &g_filter_state.patterns[i];
641 bool this_match = false;
642
643 if (pat->is_fixed_string) {
644 // Fixed string matching (with optional case-insensitive support)
645 const char *found;
646 if (pat->case_insensitive) {
647 // Unicode-aware case-insensitive search
648 found = utf8_strcasestr(log_line, pat->parsed_pattern);
649 } else {
650 // Case-sensitive search
651 found = strstr(log_line, pat->parsed_pattern);
652 }
653
654 if (found) {
655 this_match = true;
656 if (!matched_pattern) {
657 *match_start = (size_t)(found - log_line);
658 *match_len = strlen(pat->parsed_pattern);
659 matched_pattern = pat;
660 }
661 }
662 } else {
663 // Regex matching
664 if (!match_data || !pat->singleton) {
665 continue;
666 }
667
668 pcre2_code *code = asciichat_pcre2_singleton_get_code(pat->singleton);
669 if (!code) {
670 continue;
671 }
672
673 int rc = pcre2_jit_match(code, (PCRE2_SPTR)log_line, line_len, 0, 0, match_data, NULL);
674 if (rc >= 0) {
675 this_match = true;
676 if (!matched_pattern) {
677 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
678 *match_start = (size_t)ovector[0];
679 *match_len = (size_t)(ovector[1] - ovector[0]);
680 matched_pattern = pat;
681 }
682 }
683 }
684
685 // Apply invert flag
686 if (pat->invert) {
687 this_match = !this_match;
688 }
689
690 if (this_match) {
691 any_match = true;
692 // Update matched_pattern if this one has more context
693 if (!matched_pattern || (pat->context_before + pat->context_after) >
694 (matched_pattern->context_before + matched_pattern->context_after)) {
695 matched_pattern = pat;
696 }
697 }
698 }
699
700 // If any pattern matched
701 if (any_match && matched_pattern) {
702 // Output context_before lines from circular buffer
703 if (matched_pattern->context_before > 0 && g_filter_state.buffer_size > 0) {
704 int num_lines = (matched_pattern->context_before < g_filter_state.buffer_size) ? matched_pattern->context_before
705 : g_filter_state.buffer_size;
706
707 // Calculate starting position in circular buffer
708 int start_pos = (g_filter_state.buffer_pos - num_lines + g_filter_state.buffer_size) % g_filter_state.buffer_size;
709
710 // Output buffered lines
711 for (int i = 0; i < num_lines; i++) {
712 int pos = (start_pos + i) % g_filter_state.buffer_size;
713 if (g_filter_state.line_buffer[pos]) {
714 fprintf(stderr, "%s\n", g_filter_state.line_buffer[pos]);
715 }
716 }
717 }
718
719 // Set up context_after counter
720 if (matched_pattern->context_after > 0) {
721 g_filter_state.lines_after_match = matched_pattern->context_after;
722 }
723
724 return true; // Match found, output line
725 }
726
727 // No match - store in context buffer for potential future use
728 if (g_filter_state.buffer_size > 0) {
729 SAFE_FREE(g_filter_state.line_buffer[g_filter_state.buffer_pos]);
730 size_t line_len_copy = strlen(log_line) + 1;
731 g_filter_state.line_buffer[g_filter_state.buffer_pos] = SAFE_MALLOC(line_len_copy, char *);
732 if (g_filter_state.line_buffer[g_filter_state.buffer_pos]) {
733 memcpy(g_filter_state.line_buffer[g_filter_state.buffer_pos], log_line, line_len_copy);
734 }
735 g_filter_state.buffer_pos = (g_filter_state.buffer_pos + 1) % g_filter_state.buffer_size;
736 }
737
738 return false; // No match, suppress line
739}
740
741char *grep_highlight_colored_copy(const char *colored_text, const char *plain_text, size_t match_start,
742 size_t match_len) {
743 const char *result = grep_highlight_colored(colored_text, plain_text, match_start, match_len);
744 if (result) {
745 size_t result_len = strlen(result) + 1;
746 char *copy = SAFE_MALLOC(result_len, char *);
747 if (copy) {
748 memcpy(copy, result, result_len);
749 return copy;
750 }
751 }
752 return NULL;
753}
754
755const char *grep_highlight_colored(const char *colored_text, const char *plain_text, size_t match_start,
756 size_t match_len) {
757 static __thread char highlight_buffer[16384];
758
759 if (!colored_text || !plain_text || match_len == 0) {
760 return colored_text;
761 }
762
763 // When interactive grep is active, check if it has global flag
764 bool is_log_search = log_search_is_active();
765 bool should_use_global_pattern = false;
766 grep_pattern_t *global_pat = NULL;
767
768 if (!is_log_search) {
769 // If any pattern has global flag, highlight ALL matches for that pattern
770 // Find first pattern with global flag
771 for (int i = 0; i < g_filter_state.pattern_count; i++) {
772 if (g_filter_state.patterns[i].global_flag && !g_filter_state.patterns[i].is_fixed_string) {
773 global_pat = &g_filter_state.patterns[i];
774 should_use_global_pattern = true;
775 break;
776 }
777 }
778 } else if (log_search_get_global_highlight()) {
779 // Interactive grep is active and has /g flag - do global matching on the provided pattern
780 // We'll do all-match highlighting using the match position to extract the pattern
781 // and re-match for all occurrences
782 should_use_global_pattern = true;
783 // global_pat stays NULL, but we set the flag to enable global matching below
784 }
785
786 if (should_use_global_pattern) {
787 pcre2_code *code = NULL;
788 pcre2_match_data *match_data = NULL;
789 char fixed_string_pattern[256] = {0};
790 bool is_fixed_string_pattern = false;
791 bool case_insensitive_fixed = false;
792
793 // Get code from CLI pattern or interactive grep pattern
794 if (global_pat && global_pat->singleton) {
796 match_data = get_thread_match_data();
797 } else if (is_log_search) {
798 // Interactive grep global highlighting - get pattern from grep state
799 void *grep_singleton_void = log_search_get_pattern_singleton();
800 if (grep_singleton_void) {
801 pcre2_singleton_t *grep_singleton = (pcre2_singleton_t *)grep_singleton_void;
802 code = asciichat_pcre2_singleton_get_code(grep_singleton);
803 // Create match data for this pattern
804 if (code) {
805 match_data = pcre2_match_data_create_from_pattern(code, NULL);
806 }
807 } else {
808 // No regex pattern - check if it's a fixed string pattern in interactive grep
809 int pattern_len = log_search_get_input_len();
810 const char *pattern = log_search_get_input_buffer();
811 if (pattern_len > 0 && pattern && pattern_len < (int)sizeof(fixed_string_pattern)) {
812 SAFE_STRNCPY(fixed_string_pattern, pattern, sizeof(fixed_string_pattern) - 1);
813 is_fixed_string_pattern = true;
814 case_insensitive_fixed = log_search_get_case_insensitive();
815 }
816 }
817 }
818
819 if (!code && !is_fixed_string_pattern) {
820 if (match_data && is_log_search) {
821 pcre2_match_data_free(match_data); // Free if we created it
822 }
823 return colored_text; // Fall back to no highlighting
824 }
825
826 size_t plain_len = strlen(plain_text);
827 size_t colored_len = strlen(colored_text);
828 char *dst = highlight_buffer;
829 size_t plain_offset = 0;
830 size_t colored_pos = 0;
831
832 // Handle fixed string global matching
833 if (is_fixed_string_pattern) {
834 while (plain_offset < plain_len) {
835 // Find next occurrence of fixed string
836 const char *found = NULL;
837 if (case_insensitive_fixed) {
838 found = utf8_strcasestr(plain_text + plain_offset, fixed_string_pattern);
839 } else {
840 found = strstr(plain_text + plain_offset, fixed_string_pattern);
841 }
842
843 if (!found) {
844 break; // No more matches
845 }
846
847 size_t plain_match_start = (size_t)(found - plain_text);
848 size_t plain_match_end = plain_match_start + strlen(fixed_string_pattern);
849
850 // Map to colored text positions (same as single-match path)
851 size_t colored_match_start = map_plain_to_colored_pos(colored_text, plain_match_start);
852 size_t colored_match_end = map_plain_to_colored_pos(colored_text, plain_match_end);
853
854 // Copy text before match
855 if (colored_match_start > colored_pos) {
856 memcpy(dst, colored_text + colored_pos, colored_match_start - colored_pos);
857 dst += (colored_match_start - colored_pos);
858 }
859
860 // Add highlight background
861 uint8_t r, g, b;
862 get_highlight_color(&r, &g, &b);
863 dst = append_truecolor_bg(dst, r, g, b);
864
865 // Copy matched text with background
866 size_t match_byte_len = colored_match_end - colored_match_start;
867 memcpy(dst, colored_text + colored_match_start, match_byte_len);
868 dst += match_byte_len;
869
870 // Reset background
871 memcpy(dst, "\x1b[49m", 5);
872 dst += 5;
873
874 colored_pos = colored_match_end;
875 plain_offset = plain_match_end;
876 }
877
878 // Copy remaining text
879 if (colored_pos < colored_len) {
880 memcpy(dst, colored_text + colored_pos, colored_len - colored_pos);
881 dst += (colored_len - colored_pos);
882 }
883
884 // Reset both background and foreground at end
885 *dst = '\0';
886 return highlight_buffer;
887 }
888
889 // Find all matches in plain text and highlight in colored text (regex matching)
890 while (plain_offset < plain_len) {
891 int rc = pcre2_jit_match(code, (PCRE2_SPTR)plain_text, plain_len, plain_offset, 0, match_data, NULL);
892 if (rc < 0) {
893 break; // No more matches
894 }
895
896 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
897 size_t plain_match_start = (size_t)ovector[0];
898 size_t plain_match_end = (size_t)ovector[1];
899
900 // Map to colored text positions
901 size_t colored_match_start = map_plain_to_colored_pos(colored_text, plain_match_start);
902 size_t colored_match_end = map_plain_to_colored_pos(colored_text, plain_match_end);
903
904 // Copy text before match
905 if (colored_match_start > colored_pos) {
906 memcpy(dst, colored_text + colored_pos, colored_match_start - colored_pos);
907 dst += (colored_match_start - colored_pos);
908 }
909
910 // Add highlight background
911 uint8_t r, g, b;
912 get_highlight_color(&r, &g, &b);
913 dst = append_truecolor_bg(dst, r, g, b);
914
915 // Copy matched text, re-applying background after any [0m or [00m reset codes
916 size_t match_byte_len = colored_match_end - colored_match_start;
917 const char *match_src = colored_text + colored_match_start;
918 char *dst_end = highlight_buffer + sizeof(highlight_buffer) - 1; // Absolute buffer end
919 size_t i = 0;
920 while (i < match_byte_len) {
921 // Check for [0m or [00m reset codes
922 if (i + 4 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' && match_src[i + 2] == '0' &&
923 match_src[i + 3] == 'm') {
924 // Found [0m - copy it and re-apply background (if room)
925 if (dst + 4 >= dst_end)
926 break; // Not enough space for the reset code itself
927 *dst++ = match_src[i++]; // ESC
928 *dst++ = match_src[i++]; // '['
929 *dst++ = match_src[i++]; // '0'
930 *dst++ = match_src[i++]; // 'm'
931 // Re-apply highlight background after reset (only if room)
932 if (dst + 20 < dst_end) {
933 uint8_t r2, g2, b2;
934 get_highlight_color(&r2, &g2, &b2);
935 dst = append_truecolor_bg(dst, r2, g2, b2);
936 }
937 } else if (i + 5 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' &&
938 match_src[i + 2] == '0' && match_src[i + 3] == '0' && match_src[i + 4] == 'm') {
939 // Found [00m - copy it and re-apply background (if room)
940 if (dst + 5 >= dst_end)
941 break; // Not enough space for the reset code itself
942 *dst++ = match_src[i++]; // ESC
943 *dst++ = match_src[i++]; // '['
944 *dst++ = match_src[i++]; // '0'
945 *dst++ = match_src[i++]; // '0'
946 *dst++ = match_src[i++]; // 'm'
947 // Re-apply highlight background after reset (only if room)
948 if (dst + 20 < dst_end) {
949 uint8_t r2, g2, b2;
950 get_highlight_color(&r2, &g2, &b2);
951 dst = append_truecolor_bg(dst, r2, g2, b2);
952 }
953 } else {
954 // Regular character - just copy
955 if (dst + 1 >= dst_end)
956 break;
957 *dst++ = match_src[i++];
958 }
959 }
960
961 // Reset background only
962 memcpy(dst, "\x1b[49m", 5);
963 dst += 5;
964
965 colored_pos = colored_match_end;
966 plain_offset = plain_match_end;
967
968 // Prevent infinite loop on zero-length matches
969 if (plain_match_end == plain_match_start) {
970 plain_offset++;
971 }
972 }
973
974 // Copy remaining text
975 if (colored_pos < colored_len) {
976 memcpy(dst, colored_text + colored_pos, colored_len - colored_pos);
977 dst += (colored_len - colored_pos);
978 }
979
980 // Reset both background and foreground at end to prevent color bleeding
981 *dst = '\0';
982
983 // Clean up match data if we created it for interactive grep
984 if (is_log_search && match_data && !global_pat) {
985 pcre2_match_data_free(match_data);
986 }
987
988 return highlight_buffer;
989 }
990
991 // Single match highlighting (same logic as global path)
992 // map_plain_to_colored_pos(N) returns byte position after counting N characters
993 size_t colored_start = map_plain_to_colored_pos(colored_text, match_start);
994 size_t colored_end = map_plain_to_colored_pos(colored_text, match_start + match_len);
995
996 size_t colored_len = strlen(colored_text);
997 char *dst = highlight_buffer;
998
999 // Copy everything before the match
1000 if (colored_start > 0) {
1001 memcpy(dst, colored_text, colored_start);
1002 dst += colored_start;
1003 }
1004
1005 // Add highlight background
1006 uint8_t r, g, b;
1007 get_highlight_color(&r, &g, &b);
1008 dst = append_truecolor_bg(dst, r, g, b);
1009
1010 // Copy matched text, re-applying background after any [0m or [00m reset codes
1011 size_t match_byte_len = colored_end - colored_start;
1012 const char *match_src = colored_text + colored_start;
1013 char *dst_end = highlight_buffer + sizeof(highlight_buffer) - 1; // Absolute buffer end
1014 size_t i = 0;
1015
1016 while (i < match_byte_len) {
1017 // Check for [0m or [00m reset codes
1018 if (i + 4 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' && match_src[i + 2] == '0' &&
1019 match_src[i + 3] == 'm') {
1020 // Found [0m - copy it and re-apply background (if room)
1021 if (dst + 4 >= dst_end)
1022 break; // Not enough space for even the reset code
1023 *dst++ = match_src[i++]; // ESC
1024 *dst++ = match_src[i++]; // '['
1025 *dst++ = match_src[i++]; // '0'
1026 *dst++ = match_src[i++]; // 'm'
1027 // Re-apply highlight background after reset (only if room)
1028 if (dst + 20 < dst_end) {
1029 uint8_t r2, g2, b2;
1030 get_highlight_color(&r2, &g2, &b2);
1031 dst = append_truecolor_bg(dst, r2, g2, b2);
1032 }
1033 } else if (i + 5 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' &&
1034 match_src[i + 2] == '0' && match_src[i + 3] == '0' && match_src[i + 4] == 'm') {
1035 // Found [00m - copy it and re-apply background (if room)
1036 if (dst + 5 >= dst_end)
1037 break; // Not enough space for even the reset code
1038 *dst++ = match_src[i++]; // ESC
1039 *dst++ = match_src[i++]; // '['
1040 *dst++ = match_src[i++]; // '0'
1041 *dst++ = match_src[i++]; // '0'
1042 *dst++ = match_src[i++]; // 'm'
1043 // Re-apply highlight background after reset (only if room)
1044 if (dst + 20 < dst_end) {
1045 uint8_t r3, g3, b3;
1046 get_highlight_color(&r3, &g3, &b3);
1047 dst = append_truecolor_bg(dst, r3, g3, b3);
1048 }
1049 } else {
1050 // Regular character - just copy
1051 if (dst + 1 >= dst_end) {
1052 break;
1053 }
1054 *dst++ = match_src[i++];
1055 }
1056 }
1057
1058 // Reset only the background color to preserve foreground colors from headers
1059 // Using \x1b[49m (reset background only) instead of \x1b[0m (reset all)
1060 memcpy(dst, "\x1b[49m", 5);
1061 dst += 5;
1062
1063 // Copy remaining text
1064 size_t remaining = colored_len - colored_end;
1065 if (remaining > 0) {
1066 memcpy(dst, colored_text + colored_end, remaining);
1067 dst += remaining;
1068 }
1069
1070 *dst = '\0';
1071 return highlight_buffer;
1072}
1073
1074const char *grep_highlight(const char *log_line, size_t match_start, size_t match_len) {
1075 static __thread char highlight_buffer[16384];
1076
1077 if (!log_line || match_len == 0) {
1078 return log_line; // No highlighting needed
1079 }
1080
1081 size_t line_len = strlen(log_line);
1082 if (match_start + match_len > line_len) {
1083 return log_line; // Invalid range
1084 }
1085
1086 char *dst = highlight_buffer;
1087 const char *src = log_line;
1088 size_t pos = 0;
1089
1090 // If any pattern has global flag, highlight ALL matches
1091 grep_pattern_t *global_pat = NULL;
1092 for (int i = 0; i < g_filter_state.pattern_count; i++) {
1093 if (g_filter_state.patterns[i].global_flag && !g_filter_state.patterns[i].is_fixed_string) {
1094 global_pat = &g_filter_state.patterns[i];
1095 break;
1096 }
1097 }
1098
1099 if (global_pat && global_pat->singleton) {
1100 pcre2_code *code = asciichat_pcre2_singleton_get_code(global_pat->singleton);
1101 pcre2_match_data *match_data = get_thread_match_data();
1102
1103 if (!code || !match_data) {
1104 return log_line; // Fall back to no highlighting
1105 }
1106
1107 // Find all matches in the line
1108 size_t offset = 0;
1109 while (offset < line_len) {
1110 int rc = pcre2_jit_match(code, (PCRE2_SPTR)log_line, line_len, offset, 0, match_data, NULL);
1111 if (rc < 0) {
1112 break; // No more matches
1113 }
1114
1115 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
1116 size_t match_pos = (size_t)ovector[0];
1117 size_t match_end = (size_t)ovector[1];
1118 size_t len = match_end - match_pos;
1119
1120 // Copy text before match
1121 if (match_pos > pos) {
1122 memcpy(dst, src + pos, match_pos - pos);
1123 dst += (match_pos - pos);
1124 }
1125
1126 // Highlight the match (background only, preserve foreground color)
1127 uint8_t r, g, b;
1128 get_highlight_color(&r, &g, &b);
1129 dst = append_truecolor_bg(dst, r, g, b);
1130 memcpy(dst, src + match_pos, len);
1131 dst += len;
1132
1133 pos = match_end;
1134 offset = match_end;
1135
1136 // Prevent infinite loop on zero-length matches
1137 if (len == 0) {
1138 offset++;
1139 }
1140 }
1141
1142 // Copy remaining text after last match
1143 if (pos < line_len) {
1144 memcpy(dst, src + pos, line_len - pos);
1145 dst += (line_len - pos);
1146 }
1147
1148 } else {
1149 // Single match highlighting (original behavior)
1150 // Copy text before match
1151 if (match_start > 0) {
1152 memcpy(dst, log_line, match_start);
1153 dst += match_start;
1154 }
1155
1156 // Add background color for match (preserve foreground color)
1157 uint8_t r, g, b;
1158 get_highlight_color(&r, &g, &b);
1159 dst = append_truecolor_bg(dst, r, g, b);
1160
1161 // Copy matched text
1162 memcpy(dst, log_line + match_start, match_len);
1163 dst += match_len;
1164
1165 // Copy text after match
1166 size_t remaining = line_len - (match_start + match_len);
1167 if (remaining > 0) {
1168 memcpy(dst, log_line + match_start + match_len, remaining);
1169 dst += remaining;
1170 }
1171 }
1172
1173 *dst = '\0';
1174 return highlight_buffer;
1175}
1176
1177void grep_destroy(void) {
1178 // Free all patterns
1179 for (int i = 0; i < g_filter_state.pattern_count; i++) {
1180 grep_pattern_t *pat = &g_filter_state.patterns[i];
1181 // Free the copied original string (need temp variable due to const qualifier)
1182 char *original_to_free = (char *)pat->original;
1183 SAFE_FREE(original_to_free);
1185 // Singletons are auto-cleaned by asciichat_pcre2_cleanup_all()
1186 pat->singleton = NULL;
1187 }
1188 SAFE_FREE(g_filter_state.patterns);
1189 g_filter_state.patterns = NULL; // Make idempotent
1190
1191 // Free context buffer
1192 if (g_filter_state.line_buffer) {
1193 for (int i = 0; i < g_filter_state.buffer_size; i++) {
1194 SAFE_FREE(g_filter_state.line_buffer[i]);
1195 }
1196 SAFE_FREE(g_filter_state.line_buffer);
1197 g_filter_state.line_buffer = NULL; // Make idempotent
1198 }
1199
1200 g_filter_state.pattern_count = 0;
1201 g_filter_state.pattern_capacity = 0;
1202 g_filter_state.buffer_size = 0;
1203 g_filter_state.buffer_pos = 0;
1204 g_filter_state.lines_after_match = 0;
1205 g_filter_state.max_context_after = 0;
1206 g_filter_state.enabled = false;
1207
1208 // Note: Thread-local match_data is cleaned up via pthread_key destructor
1209}
1210
1211/* ============================================================================
1212 * Save/Restore Functions for Interactive Grep
1213 * ========================================================================== */
1214
1216 // Free previous saved patterns if any
1217 if (g_filter_state.saved_patterns) {
1218 for (int i = 0; i < g_filter_state.saved_pattern_count; i++) {
1219 SAFE_FREE(g_filter_state.saved_patterns[i].parsed_pattern);
1220 }
1221 SAFE_FREE(g_filter_state.saved_patterns);
1222 }
1223
1224 // Allocate saved pattern array
1225 if (g_filter_state.pattern_count > 0) {
1226 g_filter_state.saved_patterns =
1227 SAFE_MALLOC(g_filter_state.pattern_count * sizeof(grep_pattern_t), grep_pattern_t *);
1228 if (!g_filter_state.saved_patterns) {
1229 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate saved pattern array");
1230 }
1231
1232 // Deep copy all patterns
1233 for (int i = 0; i < g_filter_state.pattern_count; i++) {
1234 grep_pattern_t *src = &g_filter_state.patterns[i];
1235 grep_pattern_t *dst = &g_filter_state.saved_patterns[i];
1236
1237 // Copy pattern structure
1238 *dst = *src;
1239
1240 // Deep copy parsed_pattern string
1241 dst->parsed_pattern = SAFE_MALLOC(strlen(src->parsed_pattern) + 1, char *);
1242 if (!dst->parsed_pattern) {
1243 // Clean up partial save
1244 for (int j = 0; j < i; j++) {
1245 SAFE_FREE(g_filter_state.saved_patterns[j].parsed_pattern);
1246 }
1247 SAFE_FREE(g_filter_state.saved_patterns);
1248 g_filter_state.saved_patterns = NULL;
1249 return SET_ERRNO(ERROR_MEMORY, "Failed to save pattern string");
1250 }
1251 memcpy(dst->parsed_pattern, src->parsed_pattern, strlen(src->parsed_pattern) + 1);
1252
1253 // Singleton pointers are shared (reference counted)
1254 dst->singleton = src->singleton;
1255 }
1256 }
1257
1258 g_filter_state.saved_pattern_count = g_filter_state.pattern_count;
1259 g_filter_state.saved_pattern_capacity = g_filter_state.pattern_count;
1260 g_filter_state.saved_enabled = g_filter_state.enabled;
1261
1262 return ASCIICHAT_OK;
1263}
1264
1266 // Clear current patterns (but don't free the array)
1267 for (int i = 0; i < g_filter_state.pattern_count; i++) {
1268 SAFE_FREE(g_filter_state.patterns[i].parsed_pattern);
1269 g_filter_state.patterns[i].singleton = NULL;
1270 }
1271
1272 // Ensure we have capacity for saved patterns
1273 if (g_filter_state.saved_pattern_count > g_filter_state.pattern_capacity) {
1274 grep_pattern_t *new_patterns = SAFE_REALLOC(
1275 g_filter_state.patterns, g_filter_state.saved_pattern_count * sizeof(grep_pattern_t), grep_pattern_t *);
1276 if (!new_patterns) {
1277 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate pattern array for restore");
1278 }
1279 g_filter_state.patterns = new_patterns;
1280 g_filter_state.pattern_capacity = g_filter_state.saved_pattern_count;
1281 }
1282
1283 // Restore patterns
1284 if (g_filter_state.saved_patterns) {
1285 for (int i = 0; i < g_filter_state.saved_pattern_count; i++) {
1286 grep_pattern_t *src = &g_filter_state.saved_patterns[i];
1287 grep_pattern_t *dst = &g_filter_state.patterns[i];
1288
1289 // Copy pattern structure
1290 *dst = *src;
1291
1292 // Deep copy parsed_pattern string
1293 dst->parsed_pattern = SAFE_MALLOC(strlen(src->parsed_pattern) + 1, char *);
1294 if (!dst->parsed_pattern) {
1295 // Clean up partial restore
1296 for (int j = 0; j < i; j++) {
1297 SAFE_FREE(g_filter_state.patterns[j].parsed_pattern);
1298 }
1299 g_filter_state.pattern_count = 0;
1300 return SET_ERRNO(ERROR_MEMORY, "Failed to restore pattern string");
1301 }
1302 memcpy(dst->parsed_pattern, src->parsed_pattern, strlen(src->parsed_pattern) + 1);
1303
1304 // Singleton pointers are shared (reference counted)
1305 dst->singleton = src->singleton;
1306 }
1307 }
1308
1309 g_filter_state.pattern_count = g_filter_state.saved_pattern_count;
1310 g_filter_state.enabled = g_filter_state.saved_enabled;
1311
1312 return ASCIICHAT_OK;
1313}
1314
1316 // Don't free patterns, just set count to 0
1317 // This allows quick restore without reallocation
1318 g_filter_state.pattern_count = 0;
1319 g_filter_state.enabled = false;
1320}
1321
1323 return g_filter_state.pattern_count;
1324}
1325
1326const char *grep_get_last_pattern(void) {
1327 if (g_filter_state.pattern_count == 0) {
1328 return NULL;
1329 }
1330
1331 // Return the original pattern string of the last pattern
1332 return g_filter_state.patterns[g_filter_state.pattern_count - 1].original;
1333}
ANSI escape sequence utilities and fast color code generation.
โš™๏ธ Common definitions, error codes, macros, and types shared throughout the application
lifecycle_t match_data_lifecycle
Lifecycle for TLS key initialization.
Definition grep.c:86
asciichat_error_t grep_restore_patterns(void)
Restore previously saved filter patterns.
Definition grep.c:1265
#define HIGHLIGHT_LIGHT_BG
Definition grep.c:44
asciichat_error_t grep_save_patterns(void)
Save current filter patterns for later restoration.
Definition grep.c:1215
uint8_t cached_highlight_b
Definition grep.c:102
char * grep_highlight_colored_copy(const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
Highlight matches in colored text, returning a new allocated copy.
Definition grep.c:741
int pattern_count
Number of active patterns.
Definition grep.c:82
int saved_pattern_count
Number of saved patterns.
Definition grep.c:97
int lines_after_match
Counter for context_after lines.
Definition grep.c:92
const char * grep_get_last_pattern(void)
Get the last (most recent) filter pattern string.
Definition grep.c:1326
const char * grep_highlight(const char *log_line, size_t match_start, size_t match_len)
Highlight matched portion with background color.
Definition grep.c:1074
void grep_clear_patterns(void)
Clear all filter patterns.
Definition grep.c:1315
bool saved_enabled
Saved enabled state.
Definition grep.c:99
int grep_get_pattern_count(void)
Get number of active filter patterns.
Definition grep.c:1322
asciichat_error_t grep_init(const char *pattern)
Initialize log filtering with PCRE2 regex pattern.
Definition grep.c:498
tls_key_t match_data_key
Thread-local match_data key.
Definition grep.c:85
uint8_t cached_highlight_r
Definition grep.c:102
void grep_destroy(void)
Clean up filter resources.
Definition grep.c:1177
bool grep_should_output(const char *log_line, size_t *match_start, size_t *match_len)
Check if log line should be output (matches filter)
Definition grep.c:597
grep_parse_result_t grep_parse_pattern(const char *input)
Parse pattern in /pattern/flags or pattern/flags format.
Definition grep.c:351
#define HIGHLIGHT_DARK_BG
Highlight colors that adapt to terminal theme.
Definition grep.c:43
bool enabled
Is filtering active?
Definition grep.c:84
int buffer_pos
Current position in buffer.
Definition grep.c:91
grep_parse_result_t parse_result_t
Definition grep.c:308
uint8_t cached_highlight_g
Definition grep.c:102
int pattern_capacity
Allocated capacity.
Definition grep.c:83
uint64_t last_color_query_us
Definition grep.c:103
#define MIN_HIGHLIGHT_DISTANCE
Minimum color difference threshold (0-255 scale)
Definition grep.c:52
int saved_pattern_capacity
Allocated capacity for saved.
Definition grep.c:98
grep_pattern_t * saved_patterns
Backup of patterns for restore.
Definition grep.c:96
int buffer_size
Size of circular buffer.
Definition grep.c:90
grep_pattern_t * patterns
Array of patterns.
Definition grep.c:81
char ** line_buffer
Circular buffer for context_before.
Definition grep.c:89
const char * grep_highlight_colored(const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
Highlight matches in colored text while preserving original colors.
Definition grep.c:755
int max_context_after
Maximum context_after across all patterns.
Definition grep.c:93
char * append_truecolor_bg(char *dst, uint8_t r, uint8_t g, uint8_t b)
Definition ansi.c:173
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:284
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define US_PER_SEC_INT
Definition time.h:161
int ascii_tls_key_create(tls_key_t *key, void(*destructor)(void *))
Create a thread-local storage key.
Definition threading.c:97
int ascii_tls_set(tls_key_t key, void *value)
Set thread-local value for a key.
Definition threading.c:109
uint64_t platform_get_monotonic_time_us(void)
Get monotonic time in microseconds.
void * ascii_tls_get(tls_key_t key)
Get thread-local value for a key.
Definition threading.c:105
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
Decode a UTF-8 sequence to a Unicode codepoint.
Definition utf8.c:18
const char * utf8_strcasestr(const char *haystack, const char *needle)
Case-insensitive substring search with full Unicode support.
Definition utf8.c:272
๐Ÿงต Cross-platform thread interface for ascii-chat
void lifecycle_init_commit(lifecycle_t *lc)
Definition lifecycle.c:90
bool lifecycle_init_once(lifecycle_t *lc)
Definition lifecycle.c:52
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
๐Ÿ“ Logging API with multiple log levels and terminal output control
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Definition pcre2.c:98
pcre2_singleton_t * asciichat_pcre2_singleton_compile(const char *pattern, uint32_t flags)
Compile and cache a PCRE2 regex pattern with thread-safe singleton semantics.
Definition pcre2.c:47
๐Ÿ” Centralized PCRE2 singleton pattern for efficient regex compilation
bool terminal_has_dark_background(void)
Detect if terminal theme is dark.
bool terminal_query_background_color(uint8_t *bg_r, uint8_t *bg_g, uint8_t *bg_b)
Query terminal background color using OSC 11 escape sequence.
bool log_search_get_global_highlight(void)
Check if global highlighting (/g flag) is enabled in current pattern.
Definition search.c:896
int log_search_get_input_len(void)
Get current input buffer length (must hold mutex)
Definition search.c:940
const char * log_search_get_input_buffer(void)
Get current input buffer content (must hold mutex)
Definition search.c:948
bool log_search_is_active(void)
Check if filtering is active.
Definition search.c:390
bool log_search_get_case_insensitive(void)
Check if case-insensitive flag is set (must hold mutex)
Definition search.c:952
void * log_search_get_pattern_singleton(void)
Get the compiled regex pattern for interactive grep (internal use)
Definition search.c:911
Interactive search filtering for terminal screens.
#define false
Definition stdbool.h:63
Parsed pattern result from /pattern/flags syntax.
Definition grep.h:18
int context_before
Lines before match (B<n> flag)
Definition grep.h:25
bool global_flag
Highlight all matches (g flag)
Definition grep.h:24
bool is_fixed_string
True if fixed string (not regex)
Definition grep.h:21
bool invert
Invert match (I flag)
Definition grep.h:23
char pattern[4096]
Parsed pattern string.
Definition grep.h:19
bool case_insensitive
Case-insensitive (i flag)
Definition grep.h:22
bool valid
True if parsing succeeded.
Definition grep.h:27
int context_after
Lines after match (A<n> flag)
Definition grep.h:26
uint32_t pcre2_options
PCRE2 compile options.
Definition grep.h:20
Single filter pattern with all its settings.
Definition grep.c:65
int context_before
Lines before match (B flag)
Definition grep.c:73
int context_after
Lines after match (A flag)
Definition grep.c:74
bool case_insensitive
Case-insensitive matching (i flag)
Definition grep.c:70
bool global_flag
Highlight all matches (g flag)
Definition grep.c:72
bool is_fixed_string
True for fixed string matching (no regex)
Definition grep.c:69
const char * original
Original pattern string (for display)
Definition grep.c:66
bool invert
Invert match (I flag)
Definition grep.c:71
pcre2_singleton_t * singleton
PCRE2 singleton (NULL if fixed string)
Definition grep.c:68
char * parsed_pattern
Parsed pattern (without delimiters/flags)
Definition grep.c:67
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:22
๐Ÿ–ฅ๏ธ Cross-platform terminal interface for ascii-chat
๐Ÿ”ค UTF-8 Encoding and Decoding Utilities