ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
interactive_grep.c
Go to the documentation of this file.
1
9#include "ascii-chat/log/interactive_grep.h"
10#include "ascii-chat/common.h"
11#include "ascii-chat/log/logging.h"
12#include "ascii-chat/log/grep.h"
13#include "ascii-chat/platform/keyboard.h"
14#include "ascii-chat/platform/mutex.h"
15#include "ascii-chat/util/pcre2.h"
16#include "ascii-chat/util/utf8.h"
17#include "ascii-chat/session/session_log_buffer.h"
18#include "ascii-chat/options/options.h"
19#include "ascii-chat/video/ansi.h"
20
21#include <pcre2.h>
22#include <string.h>
23#include <stdio.h>
24#include <stdatomic.h>
25
26/* ============================================================================
27 * Constants
28 * ========================================================================== */
29
30#define MAX_GREP_PATTERNS 32
31#define GREP_INPUT_BUFFER_SIZE 256
32
33/* ============================================================================
34 * State Structure
35 * ========================================================================== */
36
37typedef struct {
38 grep_mode_t mode;
39 char input_buffer[GREP_INPUT_BUFFER_SIZE];
40 size_t len;
41 size_t cursor;
42
43 // For cancel/restore
44 char previous_patterns[MAX_GREP_PATTERNS][GREP_INPUT_BUFFER_SIZE];
46
47 // Compiled patterns for display filtering
50
51 // Parsed flags (from /pattern/flags syntax)
58
59 mutex_t mutex;
60 _Atomic bool needs_rerender;
61 _Atomic bool signal_cancelled;
62 _Atomic int mode_atomic;
66
67static interactive_grep_state_t g_grep_state = {
68 .mode = GREP_MODE_INACTIVE,
69 .len = 0,
70 .cursor = 0,
71 .previous_pattern_count = 0,
72 .active_pattern_count = 0,
73 .case_insensitive = false,
74 .fixed_string = false,
75 .global_highlight = false,
76 .invert_match = false,
77 .context_before = 0,
78 .context_after = 0,
79 .needs_rerender = false,
80 .signal_cancelled = false,
81 .mode_atomic = GREP_MODE_INACTIVE,
82 .initialized = false,
83 .cli_pattern_auto_populated = false,
84};
85
86/* ============================================================================
87 * Pattern Validation
88 * ========================================================================== */
89
95static bool validate_pcre2_pattern(const char *input) {
96 if (!input || strlen(input) == 0) {
97 return true; // Empty is valid (no filtering)
98 }
99
100 // Use the shared parser from filter.c
101 grep_parse_result_t parsed = grep_parse_pattern(input);
102
103 if (!parsed.valid) {
104 return false; // Invalid format
105 }
106
107 // If fixed string, always valid
108 if (parsed.is_fixed_string) {
109 return true;
110 }
111
112 // Try to compile pattern with full pcre2_options (includes UTF, UCP, CASELESS, etc.)
113 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
114 if (!singleton) {
115 return false; // Compilation failed
116 }
117
118 // Pattern is valid (singleton cached, will be reused later)
119 return true;
120}
121
122/* ============================================================================
123 * Lifecycle Functions
124 * ========================================================================== */
125
126asciichat_error_t interactive_grep_init(void) {
127 // Initialize mutex first (before any locking!)
128 static bool mutex_inited = false;
129 if (!mutex_inited) {
130 mutex_init(&g_grep_state.mutex);
131 mutex_inited = true;
132 }
133
134 mutex_lock(&g_grep_state.mutex);
135
136 if (g_grep_state.initialized) {
137 mutex_unlock(&g_grep_state.mutex);
138 return ASCIICHAT_OK;
139 }
140
141 // Initialize state (careful not to destroy the mutex!)
142 // Save the mutex before clearing state
143 mutex_t saved_mutex = g_grep_state.mutex;
144 memset(&g_grep_state, 0, sizeof(g_grep_state));
145 // Restore the mutex
146 g_grep_state.mutex = saved_mutex;
147
148 // Check if there are CLI --grep patterns to use
149 const char *cli_pattern = grep_get_last_pattern();
150
151 // Start in INACTIVE mode if no CLI pattern is provided.
152 // Only use patterns from CLI --grep arguments, not default patterns.
153 // This ensures logs are visible by default and only filtered if explicitly requested.
154 if (!cli_pattern || cli_pattern[0] == '\0') {
155 // No CLI pattern - start inactive
156 g_grep_state.mode = GREP_MODE_INACTIVE;
157 atomic_store(&g_grep_state.mode_atomic, GREP_MODE_INACTIVE);
158 g_grep_state.initialized = true;
159 mutex_unlock(&g_grep_state.mutex);
160 return ASCIICHAT_OK;
161 }
162
163 // Load CLI pattern into input buffer
164 SAFE_STRNCPY(g_grep_state.input_buffer, cli_pattern, GREP_INPUT_BUFFER_SIZE - 1);
165 g_grep_state.len = strlen(cli_pattern);
166 g_grep_state.cursor = g_grep_state.len;
167 g_grep_state.mode = GREP_MODE_ACTIVE;
168 atomic_store(&g_grep_state.mode_atomic, GREP_MODE_ACTIVE);
169
170 // Compile the initial pattern
171 grep_parse_result_t parsed = grep_parse_pattern(cli_pattern);
172 if (parsed.valid) {
173 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
174 if (singleton) {
175 g_grep_state.active_patterns[0] = singleton;
176 g_grep_state.active_pattern_count = 1;
177 }
178 g_grep_state.case_insensitive = parsed.case_insensitive;
179 g_grep_state.fixed_string = parsed.is_fixed_string;
180 g_grep_state.global_highlight = parsed.global_flag;
181 g_grep_state.invert_match = parsed.invert;
182 g_grep_state.context_before = parsed.context_before;
183 g_grep_state.context_after = parsed.context_after;
184 }
185
186 atomic_store(&g_grep_state.needs_rerender, true);
187 g_grep_state.initialized = true;
188
189 mutex_unlock(&g_grep_state.mutex);
190 return ASCIICHAT_OK;
191}
192
194 mutex_lock(&g_grep_state.mutex);
195
196 if (!g_grep_state.initialized) {
197 mutex_unlock(&g_grep_state.mutex);
198 return;
199 }
200
201 // Free active patterns
202 for (int i = 0; i < g_grep_state.active_pattern_count; i++) {
203 if (g_grep_state.active_patterns[i]) {
204 // Note: pcre2_singleton handles its own reference counting
205 g_grep_state.active_patterns[i] = NULL;
206 }
207 }
208
209 g_grep_state.initialized = false;
210
211 mutex_unlock(&g_grep_state.mutex);
212}
213
214/* ============================================================================
215 * Mode Management
216 * ========================================================================== */
217
219 mutex_lock(&g_grep_state.mutex);
220
221 // Save current patterns (CLI --grep patterns)
222 asciichat_error_t result = grep_save_patterns();
223 if (result != ASCIICHAT_OK) {
224 log_warn("Failed to save filter patterns");
225 }
226
227 // Clear input buffer
228 memset(g_grep_state.input_buffer, 0, sizeof(g_grep_state.input_buffer));
229 g_grep_state.len = 0;
230 g_grep_state.cursor = 0;
231
232 // Pre-populate with CLI --grep pattern if available (only once at startup)
233 if (!g_grep_state.cli_pattern_auto_populated) {
234 const char *cli_pattern = grep_get_last_pattern();
235 if (cli_pattern && cli_pattern[0] != '\0') {
236 // Skip leading slash if present (prompt already shows "/")
237 const char *pattern_to_use = cli_pattern;
238 if (cli_pattern[0] == '/') {
239 pattern_to_use = cli_pattern + 1;
240 }
241
242 size_t pattern_len = strlen(pattern_to_use);
243 if (pattern_len < sizeof(g_grep_state.input_buffer) - 1) {
244 memcpy(g_grep_state.input_buffer, pattern_to_use, pattern_len + 1);
245 g_grep_state.len = pattern_len;
246 g_grep_state.cursor = pattern_len;
247 g_grep_state.cli_pattern_auto_populated = true;
248
249 // Compile and apply the pattern immediately so it starts filtering
250 grep_parse_result_t parsed = grep_parse_pattern(g_grep_state.input_buffer);
251 if (parsed.valid) {
252 // Store parsed flags
253 g_grep_state.case_insensitive = parsed.case_insensitive;
254 g_grep_state.fixed_string = parsed.is_fixed_string;
255 g_grep_state.global_highlight = parsed.global_flag;
256 g_grep_state.invert_match = parsed.invert;
257 g_grep_state.context_before = parsed.context_before;
258 g_grep_state.context_after = parsed.context_after;
259
260 // Compile pattern if not fixed string
261 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
262 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
263 if (singleton) {
264 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
265 if (code) {
266 g_grep_state.active_patterns[0] = singleton;
267 g_grep_state.active_pattern_count = 1;
268 } else {
269 g_grep_state.fixed_string = true;
270 }
271 } else {
272 g_grep_state.fixed_string = true;
273 }
274 }
275 }
276 }
277 }
278 }
279
280 // Enter input mode
281 g_grep_state.mode = GREP_MODE_ENTERING;
282 atomic_store(&g_grep_state.mode_atomic, GREP_MODE_ENTERING);
283 atomic_store(&g_grep_state.needs_rerender, true);
284
285 mutex_unlock(&g_grep_state.mutex);
286}
287
288void interactive_grep_exit_mode(bool accept) {
289 mutex_lock(&g_grep_state.mutex);
290
291 if (g_grep_state.mode != GREP_MODE_ENTERING) {
292 mutex_unlock(&g_grep_state.mutex);
293 return;
294 }
295
296 if (!accept) {
297 // Cancel - restore previous patterns and reset CLI pattern flag so user can re-populate
298 asciichat_error_t result = grep_restore_patterns();
299 if (result != ASCIICHAT_OK) {
300 log_warn("Failed to restore filter patterns");
301 }
302
303 // Clear interactive grep patterns
304 for (int i = 0; i < g_grep_state.active_pattern_count; i++) {
305 g_grep_state.active_patterns[i] = NULL;
306 }
307 g_grep_state.active_pattern_count = 0;
308
309 // Reset the CLI pattern auto-populated flag so user can re-enter grep mode with the pattern again
310 g_grep_state.cli_pattern_auto_populated = false;
311
312 g_grep_state.mode = GREP_MODE_INACTIVE;
313 atomic_store(&g_grep_state.mode_atomic, GREP_MODE_INACTIVE);
314 atomic_store(&g_grep_state.needs_rerender, true);
315 mutex_unlock(&g_grep_state.mutex);
316 return;
317 }
318
319 // Accept - parse and compile pattern
320 grep_parse_result_t parsed = grep_parse_pattern(g_grep_state.input_buffer);
321
322 if (!parsed.valid) {
323 log_error("Invalid pattern format");
324 mutex_unlock(&g_grep_state.mutex);
325 return; // Stay in input mode
326 }
327
328 // Clear old regex patterns
329 for (int i = 0; i < g_grep_state.active_pattern_count; i++) {
330 g_grep_state.active_patterns[i] = NULL;
331 }
332 g_grep_state.active_pattern_count = 0;
333
334 // Store parsed flags first
335 g_grep_state.case_insensitive = parsed.case_insensitive;
336 g_grep_state.fixed_string = parsed.is_fixed_string;
337 g_grep_state.global_highlight = parsed.global_flag;
338 g_grep_state.invert_match = parsed.invert;
339 g_grep_state.context_before = parsed.context_before;
340 g_grep_state.context_after = parsed.context_after;
341
342 // Compile and store new pattern (if not fixed string)
343 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
344 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
345 if (singleton) {
346 // Verify the pattern actually compiles
347 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
348 if (code) {
349 g_grep_state.active_patterns[0] = singleton;
350 g_grep_state.active_pattern_count = 1;
351 } else {
352 // Compilation failed - fall back to fixed string matching
353 g_grep_state.fixed_string = true; // Override to use fixed string instead
354 }
355 } else {
356 // Malloc failed - fall back to fixed string
357 g_grep_state.fixed_string = true;
358 }
359 }
360 // For fixed strings, pattern stays in input_buffer and fixed_string=true
361
362 g_grep_state.mode = GREP_MODE_ACTIVE;
363 atomic_store(&g_grep_state.mode_atomic, GREP_MODE_ACTIVE);
364 atomic_store(&g_grep_state.needs_rerender, true);
365
366 mutex_unlock(&g_grep_state.mutex);
367}
368
369/* ============================================================================
370 * Signal-Safe Interface
371 * ========================================================================== */
372
374 return atomic_load(&g_grep_state.mode_atomic) == GREP_MODE_ENTERING;
375}
376
378 atomic_store(&g_grep_state.signal_cancelled, true);
379}
380
382 return atomic_exchange(&g_grep_state.signal_cancelled, false);
383}
384
386 // Use atomic read (async-signal-safe) to avoid mutex issues when called from signal handlers
387 return atomic_load(&g_grep_state.mode_atomic) == GREP_MODE_ENTERING;
388}
389
391 // Use atomic read (async-signal-safe) to avoid mutex issues when called from signal handlers
392 return atomic_load(&g_grep_state.mode_atomic) != GREP_MODE_INACTIVE;
393}
394
395/* ============================================================================
396 * Keyboard Handling
397 * ========================================================================== */
398
400 mutex_lock(&g_grep_state.mutex);
401
402 // If in input mode, handle all keys
403 if (g_grep_state.mode == GREP_MODE_ENTERING) {
404 mutex_unlock(&g_grep_state.mutex);
405 return true;
406 }
407
408 // If not in input mode, only handle '/' to enter mode
409 bool should_handle = (key == '/');
410 mutex_unlock(&g_grep_state.mutex);
411 return should_handle;
412}
413
414asciichat_error_t interactive_grep_handle_key(keyboard_key_t key) {
415 mutex_lock(&g_grep_state.mutex);
416
417 // If not in input mode, check if user pressed '/'
418 if (g_grep_state.mode != GREP_MODE_ENTERING) {
419 mutex_unlock(&g_grep_state.mutex);
420
421 // Check for '/' key (key already passed as parameter, don't read again!)
422 if (key == '/') {
424 }
425 return ASCIICHAT_OK;
426 }
427
428 // In input mode - use keyboard_read_line_interactive()
429 // CRITICAL: Keep mutex held during keyboard_read_line_interactive() to prevent races
430 // with the rendering thread. The validator callback and buffer modifications must be
431 // synchronized with readers in interactive_grep_gather_and_filter_logs().
432 keyboard_line_edit_opts_t opts = {
433 .buffer = g_grep_state.input_buffer,
434 .max_len = sizeof(g_grep_state.input_buffer),
435 .len = &g_grep_state.len,
436 .cursor = &g_grep_state.cursor,
437 .echo = false, // We handle rendering ourselves
438 .mask_char = 0, // No masking
439 .prefix = NULL, // Don't render prefix - interactive_grep_render_input_line() handles it
440 .validator = validate_pcre2_pattern, // Live validation
441 .key = key // Pass the pre-read key
442 };
443
444 keyboard_line_edit_result_t result = keyboard_read_line_interactive(&opts);
445
446 switch (result) {
447 case LINE_EDIT_ACCEPTED:
448 // Unlock before exit_mode since it acquires the mutex
449 mutex_unlock(&g_grep_state.mutex);
450 interactive_grep_exit_mode(true); // Parse and accept pattern
451 break;
452 case LINE_EDIT_CANCELLED:
453 // Unlock before exit_mode since it acquires the mutex
454 mutex_unlock(&g_grep_state.mutex);
455 interactive_grep_exit_mode(false); // Restore previous
456 break;
457 case LINE_EDIT_CONTINUE:
458 // Still editing - compile pattern for live filtering
459 // Mutex is still held here
460
461 // If buffer is empty, clear patterns to show all logs (don't exit grep mode)
462 if (g_grep_state.len == 0) {
463 g_grep_state.active_pattern_count = 0;
464 mutex_unlock(&g_grep_state.mutex);
465 atomic_store(&g_grep_state.needs_rerender, true);
466 break;
467 }
468
469 // Parse and compile pattern for live filtering
470 grep_parse_result_t parsed = grep_parse_pattern(g_grep_state.input_buffer);
471
472 if (!parsed.valid) {
473 // Invalid pattern - keep previous patterns active (don't clear)
474 mutex_unlock(&g_grep_state.mutex);
475 atomic_store(&g_grep_state.needs_rerender, true);
476 break;
477 }
478
479 // Valid pattern - update filtering state
480 if (parsed.is_fixed_string) {
481 // Fixed string matching - no regex compilation needed
482 g_grep_state.active_pattern_count = 0;
483 g_grep_state.case_insensitive = parsed.case_insensitive;
484 g_grep_state.fixed_string = true;
485 } else {
486 // Regex pattern - compile with full pcre2_options
487 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
488 if (singleton) {
489 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
490 if (code) {
491 g_grep_state.active_patterns[0] = singleton;
492 g_grep_state.active_pattern_count = 1;
493 g_grep_state.case_insensitive = parsed.case_insensitive;
494 g_grep_state.fixed_string = false;
495 } else {
496 // Compilation failed - fall back to fixed string matching
497 g_grep_state.active_pattern_count = 0;
498 g_grep_state.fixed_string = true;
499 g_grep_state.case_insensitive = parsed.case_insensitive;
500 }
501 } else {
502 g_grep_state.active_pattern_count = 0;
503 g_grep_state.fixed_string = true;
504 g_grep_state.case_insensitive = parsed.case_insensitive;
505 }
506 }
507
508 // Store parsed flags
509 g_grep_state.global_highlight = parsed.global_flag;
510 g_grep_state.invert_match = parsed.invert;
511 g_grep_state.context_before = parsed.context_before;
512 g_grep_state.context_after = parsed.context_after;
513
514 mutex_unlock(&g_grep_state.mutex);
515 atomic_store(&g_grep_state.needs_rerender, true);
516 break;
517 case LINE_EDIT_NO_INPUT:
518 // No input available
519 mutex_unlock(&g_grep_state.mutex);
520 break;
521 }
522
523 return ASCIICHAT_OK;
524}
525
526/* ============================================================================
527 * Log Filtering
528 * ========================================================================== */
529
530asciichat_error_t interactive_grep_gather_and_filter_logs(session_log_entry_t **out_entries, size_t *out_count) {
531 if (!out_entries || !out_count) {
532 return SET_ERRNO(ERROR_INVALID_PARAM, "out_entries and out_count must not be NULL");
533 }
534
535 // Get logs from in-memory buffer
536 session_log_entry_t *buffer_entries =
537 SAFE_MALLOC(SESSION_LOG_BUFFER_SIZE * sizeof(session_log_entry_t), session_log_entry_t *);
538 if (!buffer_entries) {
539 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate log buffer");
540 }
541
542 size_t buffer_count = session_log_buffer_get_recent(buffer_entries, SESSION_LOG_BUFFER_SIZE);
543
544 // Check if filtering is active (either regex patterns or fixed string)
545 mutex_lock(&g_grep_state.mutex);
546 bool has_regex_patterns = (g_grep_state.active_pattern_count > 0);
547 bool has_fixed_string = (g_grep_state.fixed_string && g_grep_state.len > 0);
548 bool filtering_active = has_regex_patterns || has_fixed_string;
549
550 if (!filtering_active) {
551 // No filtering active - return all entries
552 mutex_unlock(&g_grep_state.mutex);
553 *out_entries = buffer_entries;
554 *out_count = buffer_count;
555 return ASCIICHAT_OK;
556 }
557
558 // Filter entries with PCRE2
559 session_log_entry_t *filtered =
560 SAFE_MALLOC(SESSION_LOG_BUFFER_SIZE * sizeof(session_log_entry_t), session_log_entry_t *);
561 if (!filtered) {
562 mutex_unlock(&g_grep_state.mutex);
563 SAFE_FREE(buffer_entries);
564 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate filtered buffer");
565 }
566
567 size_t filtered_count = 0;
568
569 // Create match data for PCRE2 matching
570 pcre2_match_data *match_data = NULL;
571 for (int p = 0; p < g_grep_state.active_pattern_count; p++) {
572 if (g_grep_state.active_patterns[p]) {
573 pcre2_code *code = asciichat_pcre2_singleton_get_code(g_grep_state.active_patterns[p]);
574 if (code) {
575 match_data = pcre2_match_data_create_from_pattern(code, NULL);
576 break;
577 }
578 }
579 }
580
581 if (!match_data && g_grep_state.active_pattern_count > 0) {
582 // Couldn't create match data - fall back to returning all entries
583 mutex_unlock(&g_grep_state.mutex);
584 SAFE_FREE(filtered);
585 *out_entries = buffer_entries;
586 *out_count = buffer_count;
587 return ASCIICHAT_OK;
588 }
589
590 // Parse pattern once before loop (for fixed string mode)
591 grep_parse_result_t parsed = {0};
592 if (has_fixed_string) {
593 parsed = grep_parse_pattern(g_grep_state.input_buffer);
594 }
595
596 for (size_t i = 0; i < buffer_count; i++) {
597 const char *original_message = buffer_entries[i].message;
598 bool matches = false;
599
600 // Strip ANSI codes to match against plain text (consistent with highlighting)
601 char plain_message[SESSION_LOG_LINE_MAX] = {0};
602 const char *strip_ansi_codes_fn(const char *src, char *dst, size_t dst_size);
603 // Use inline stripping to avoid function call overhead
604 {
605 size_t pos = 0;
606 const char *src = original_message;
607 char *dst = plain_message;
608 size_t dst_size = sizeof(plain_message);
609
610 while (*src && pos < dst_size - 1) {
611 if (*src == '\x1b' && src[1] != '\0' && src[1] == '[') {
612 // CSI sequence - skip until final byte
613 src += 2;
614 while (*src && pos < dst_size - 1 && !(*src >= 0x40 && *src <= 0x7E)) {
615 src++;
616 }
617 if (*src)
618 src++; // Skip final byte
619 } else {
620 dst[pos++] = *src++;
621 }
622 }
623 dst[pos] = '\0';
624 }
625
626 size_t message_len = strlen(plain_message);
627
628 // Fixed string matching
629 if (has_fixed_string) {
630 const char *search_pattern = parsed.pattern;
631
632 if (g_grep_state.case_insensitive) {
633 // Unicode-aware case-insensitive fixed string search
634 matches = (utf8_strcasestr(plain_message, search_pattern) != NULL);
635 } else {
636 // Case-sensitive fixed string search
637 matches = (strstr(plain_message, search_pattern) != NULL);
638 }
639 }
640 // Regex pattern matching
641 else if (has_regex_patterns) {
642 // Check against all active patterns (OR logic)
643 for (int p = 0; p < g_grep_state.active_pattern_count; p++) {
644 pcre2_singleton_t *singleton = g_grep_state.active_patterns[p];
645 if (!singleton) {
646 continue;
647 }
648
649 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
650 if (!code) {
651 continue;
652 }
653
654 // Match against plain message (without ANSI codes)
655 int rc = pcre2_jit_match(code, (PCRE2_SPTR)plain_message, message_len, 0, 0, match_data, NULL);
656 if (rc >= 0) {
657 matches = true;
658 break;
659 }
660 }
661 }
662
663 // Apply invert logic
664 if (g_grep_state.invert_match) {
665 matches = !matches;
666 }
667
668 if (matches) {
669 filtered[filtered_count++] = buffer_entries[i];
670 }
671 }
672
673 // Free match data
674 if (match_data) {
675 pcre2_match_data_free(match_data);
676 }
677
678 mutex_unlock(&g_grep_state.mutex);
679
680 SAFE_FREE(buffer_entries);
681 *out_entries = filtered;
682 *out_count = filtered_count;
683 return ASCIICHAT_OK;
684}
685
686/* ============================================================================
687 * Display Rendering
688 * ========================================================================== */
689
691 (void)width; // Reserved for future use (line truncation)
692
693 mutex_lock(&g_grep_state.mutex);
694
695 if (g_grep_state.mode != GREP_MODE_ENTERING) {
696 mutex_unlock(&g_grep_state.mutex);
697 return;
698 }
699
700 // Just show slash and pattern (cursor already positioned by caller)
701 char output_buf[256];
702 int len = snprintf(output_buf, sizeof(output_buf), "/%.*s", (int)g_grep_state.len, g_grep_state.input_buffer);
703 if (len > 0) {
704 platform_write_all(STDOUT_FILENO, output_buf, len);
705 }
706
707 mutex_unlock(&g_grep_state.mutex);
708}
709
710/* ============================================================================
711 * Display Highlighting
712 * ========================================================================== */
713
714bool interactive_grep_get_match_info(const char *message, size_t *out_match_start, size_t *out_match_len) {
715 if (!message || !out_match_start || !out_match_len) {
716 return false;
717 }
718
719 *out_match_start = 0;
720 *out_match_len = 0;
721
722 // Use atomic read to avoid mutex contention with keyboard handler
723 int mode = atomic_load(&g_grep_state.mode_atomic);
724 if (mode == GREP_MODE_INACTIVE) {
725 return false;
726 }
727
728 // Make a safe copy of state under mutex, then release before doing expensive work
729 mutex_lock(&g_grep_state.mutex);
730
731 bool has_fixed_string = g_grep_state.fixed_string && g_grep_state.len > 0;
732 bool case_insensitive = g_grep_state.case_insensitive;
733 char pattern_copy[GREP_INPUT_BUFFER_SIZE];
734 size_t pattern_len = g_grep_state.len;
735 int pattern_count = g_grep_state.active_pattern_count;
736 pcre2_singleton_t *patterns_copy[MAX_GREP_PATTERNS];
737
738 if (has_fixed_string) {
739 // Parse the input buffer to extract just the pattern part (without flags)
740 grep_parse_result_t parsed = grep_parse_pattern(g_grep_state.input_buffer);
741 const char *src = parsed.valid ? parsed.pattern : g_grep_state.input_buffer;
742 SAFE_STRNCPY(pattern_copy, src, sizeof(pattern_copy) - 1);
743 pattern_copy[sizeof(pattern_copy) - 1] = '\0';
744 pattern_len = strlen(pattern_copy); // Update pattern_len for the actual parsed pattern
745 }
746
747 for (int i = 0; i < pattern_count && i < MAX_GREP_PATTERNS; i++) {
748 patterns_copy[i] = g_grep_state.active_patterns[i];
749 }
750
751 mutex_unlock(&g_grep_state.mutex);
752
753 // Now do matching without holding mutex
754 size_t message_len = strlen(message);
755 bool matches = false;
756
757 // Fixed string matching
758 if (has_fixed_string) {
759 const char *found = NULL;
760
761 if (case_insensitive) {
762 found = utf8_strcasestr(message, pattern_copy);
763 } else {
764 found = strstr(message, pattern_copy);
765 }
766
767 if (found) {
768 matches = true;
769
770 // Convert byte position to character position (UTF-8 aware)
771 size_t byte_pos = (size_t)(found - message);
772 size_t char_pos = 0;
773 for (size_t i = 0; i < byte_pos && message[i] != '\0';) {
774 uint32_t codepoint;
775 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
776 if (utf8_len <= 0)
777 utf8_len = 1;
778 i += utf8_len;
779 char_pos++;
780 }
781
782 // Count characters in the match (pattern_len is in bytes for the pattern)
783 size_t match_char_len = 0;
784 for (size_t i = 0; i < pattern_len && found[i] != '\0';) {
785 uint32_t codepoint;
786 int utf8_len = utf8_decode((const uint8_t *)(found + i), &codepoint);
787 if (utf8_len <= 0)
788 utf8_len = 1;
789 i += utf8_len;
790 match_char_len++;
791 }
792
793 *out_match_start = char_pos;
794 *out_match_len = match_char_len;
795 }
796 }
797 // Regex pattern matching
798 else if (pattern_count > 0) {
799 pcre2_match_data *match_data = NULL;
800 for (int p = 0; p < pattern_count; p++) {
801 if (patterns_copy[p]) {
802 pcre2_code *code = asciichat_pcre2_singleton_get_code(patterns_copy[p]);
803 if (code) {
804 match_data = pcre2_match_data_create_from_pattern(code, NULL);
805 break;
806 }
807 }
808 }
809
810 if (match_data) {
811 for (int p = 0; p < pattern_count; p++) {
812 pcre2_singleton_t *singleton = patterns_copy[p];
813 if (!singleton) {
814 continue;
815 }
816
817 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
818 if (!code) {
819 continue;
820 }
821
822 int rc = pcre2_jit_match(code, (PCRE2_SPTR)message, message_len, 0, 0, match_data, NULL);
823 if (rc >= 0) {
824 matches = true;
825 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
826
827 // Convert byte positions to character positions (UTF-8 aware)
828 size_t byte_start = (size_t)ovector[0];
829 size_t byte_end = (size_t)ovector[1];
830
831 // Count characters up to byte_start
832 size_t char_start = 0;
833 for (size_t i = 0; i < byte_start && message[i] != '\0';) {
834 uint32_t codepoint;
835 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
836 if (utf8_len <= 0)
837 utf8_len = 1;
838 i += utf8_len;
839 char_start++;
840 }
841
842 // Count characters up to byte_end
843 size_t char_end = char_start;
844 for (size_t i = byte_start; i < byte_end && message[i] != '\0';) {
845 uint32_t codepoint;
846 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
847 if (utf8_len <= 0)
848 utf8_len = 1;
849 i += utf8_len;
850 char_end++;
851 }
852
853 *out_match_start = char_start;
854 *out_match_len = char_end - char_start;
855 break;
856 }
857 }
858 pcre2_match_data_free(match_data);
859 }
860 }
861
862 return matches;
863}
864
865/* ============================================================================
866 * Re-render Notification
867 * ========================================================================== */
868
870 bool needs = atomic_load(&g_grep_state.needs_rerender);
871 if (needs) {
872 atomic_store(&g_grep_state.needs_rerender, false);
873 }
874 return needs;
875}
876
877/* ============================================================================
878 * Query Functions
879 * ========================================================================== */
880
882 // Use atomic read to avoid mutex contention with keyboard handler
883 int mode = atomic_load(&g_grep_state.mode_atomic);
884 if (mode == GREP_MODE_INACTIVE) {
885 return false;
886 }
887
888 // Safe read under mutex (quick operation)
889 mutex_lock(&g_grep_state.mutex);
890 bool result = g_grep_state.global_highlight;
891 mutex_unlock(&g_grep_state.mutex);
892
893 return result;
894}
895
897 // Use atomic read to check if active
898 int mode = atomic_load(&g_grep_state.mode_atomic);
899 if (mode == GREP_MODE_INACTIVE) {
900 return NULL;
901 }
902
903 // Safe read under mutex
904 mutex_lock(&g_grep_state.mutex);
905
906 // Return the compiled pattern if we have one (not fixed string)
907 void *result = NULL;
908 if (g_grep_state.active_pattern_count > 0 && !g_grep_state.fixed_string) {
909 result = (void *)g_grep_state.active_patterns[0];
910 }
911
912 mutex_unlock(&g_grep_state.mutex);
913
914 return result;
915}
916
917/* ============================================================================
918 * Internal Access for Atomic Rendering
919 * ========================================================================== */
920
922 return (void *)&g_grep_state.mutex;
923}
924
926 return (int)g_grep_state.len;
927}
928
930 return g_grep_state.input_buffer;
931}
932
934 return g_grep_state.case_insensitive;
935}
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all data to file descriptor with automatic retry on transient errors.
Definition abstraction.c:39
asciichat_error_t grep_restore_patterns(void)
Definition grep.c:1328
asciichat_error_t grep_save_patterns(void)
Definition grep.c:1278
int pattern_count
Number of active patterns.
Definition grep.c:76
const char * grep_get_last_pattern(void)
Definition grep.c:1389
grep_parse_result_t grep_parse_pattern(const char *input)
Parse pattern in /pattern/flags or pattern/flags format.
Definition grep.c:414
void interactive_grep_exit_mode(bool accept)
bool interactive_grep_is_entering(void)
bool interactive_grep_check_signal_cancel(void)
bool interactive_grep_is_entering_atomic(void)
bool interactive_grep_get_case_insensitive(void)
#define GREP_INPUT_BUFFER_SIZE
Input buffer size.
const char * interactive_grep_get_input_buffer(void)
void interactive_grep_signal_cancel(void)
void interactive_grep_enter_mode(void)
asciichat_error_t interactive_grep_gather_and_filter_logs(session_log_entry_t **out_entries, size_t *out_count)
bool interactive_grep_is_active(void)
asciichat_error_t interactive_grep_init(void)
#define MAX_GREP_PATTERNS
Maximum number of patterns to support.
bool interactive_grep_needs_rerender(void)
bool interactive_grep_get_global_highlight(void)
void * interactive_grep_get_mutex(void)
asciichat_error_t interactive_grep_handle_key(keyboard_key_t key)
void interactive_grep_render_input_line(int width)
int interactive_grep_get_input_len(void)
bool interactive_grep_get_match_info(const char *message, size_t *out_match_start, size_t *out_match_len)
bool interactive_grep_should_handle(int key)
void interactive_grep_destroy(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
size_t session_log_buffer_get_recent(session_log_entry_t *out_entries, size_t max_count)
_Atomic bool signal_cancelled
Set by signal handler, checked by render loop.
pcre2_singleton_t * active_patterns[32]
bool cli_pattern_auto_populated
Track if CLI pattern was already populated.
_Atomic int mode_atomic
Shadow of mode for signal-safe reads.
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:21
int mutex_init(mutex_t *mutex)
Definition threading.c:16
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