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