ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
search.c
Go to the documentation of this file.
1
10#include "ascii-chat/common.h"
11#include "ascii-chat/log/log.h"
12#include "ascii-chat/log/grep.h"
22
23#include <pcre2.h>
24#include <string.h>
25#include <stdio.h>
26#include <ascii-chat/atomic.h>
27
28/* ============================================================================
29 * Constants
30 * ========================================================================== */
31
32#define MAX_LOG_SEARCH_PATTERNS 32
33#define LOG_SEARCH_INPUT_BUFFER_SIZE 256
34
35/* ============================================================================
36 * State Structure
37 * ========================================================================== */
38
39typedef struct {
41 char input_buffer[LOG_SEARCH_INPUT_BUFFER_SIZE];
42 size_t len;
43 size_t cursor;
44
45 // For cancel/restore
48
49 // Compiled patterns for display filtering
52
53 // Parsed flags (from /pattern/flags syntax)
60
68
69static log_search_state_t g_search_state = {
71 .len = 0,
72 .cursor = 0,
73 .previous_pattern_count = 0,
74 .active_pattern_count = 0,
75 .case_insensitive = false,
76 .fixed_string = false,
77 .global_highlight = false,
78 .invert_match = false,
79 .context_before = 0,
80 .context_after = 0,
81 .lifecycle = LIFECYCLE_INIT,
82 .needs_rerender = {0},
83 .signal_cancelled = {0},
84 .mode_atomic = {0},
85 .cli_pattern_auto_populated = false,
86};
87
88static lifecycle_t g_log_search_lifecycle = LIFECYCLE_INIT;
89
90/* ============================================================================
91 * Pattern Validation
92 * ========================================================================== */
93
99static bool validate_pcre2_pattern(const char *input) {
100 if (!input || strlen(input) == 0) {
101 return true; // Empty is valid (no filtering)
102 }
103
104 // Use the shared parser from filter.c
106
107 if (!parsed.valid) {
108 return false; // Invalid format
109 }
110
111 // If fixed string, always valid
112 if (parsed.is_fixed_string) {
113 return true;
114 }
115
116 // Try to compile pattern with full pcre2_options (includes UTF, UCP, CASELESS, etc.)
117 // Pattern is valid if singleton cached and reusable
118 return asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options) != NULL;
119}
120
121/* ============================================================================
122 * Lifecycle Functions
123 * ========================================================================== */
124
126 // Use lifecycle API for unified initialization
127 if (!lifecycle_init_once(&g_log_search_lifecycle)) {
128 // Already initialized, acquire mutex lock and return
129 mutex_lock(&g_search_state.mutex);
130 return ASCIICHAT_OK;
131 }
132
133 // Winner of init_once - initialize mutex and state
134 g_log_search_lifecycle.sync_type = LIFECYCLE_SYNC_MUTEX;
135 g_log_search_lifecycle.sync.mutex = &g_search_state.mutex;
136 lifecycle_init(&g_log_search_lifecycle, "log_search");
137
138 mutex_lock(&g_search_state.mutex);
139
140 // Initialize state (careful not to destroy the mutex!)
141 // Save the mutex before clearing state
142 mutex_t saved_mutex = g_search_state.mutex;
143 memset(&g_search_state, 0, sizeof(g_search_state));
144 // Restore the mutex
145 g_search_state.mutex = saved_mutex;
146
147 // Check if there are CLI --grep patterns to use
148 const char *cli_pattern = grep_get_last_pattern();
149
150 // Start in INACTIVE mode if no CLI pattern is provided.
151 // Only use patterns from CLI --grep arguments, not default patterns.
152 // This ensures logs are visible by default and only filtered if explicitly requested.
153 if (!cli_pattern || cli_pattern[0] == '\0') {
154 // No CLI pattern - start inactive
155 g_search_state.mode = LOG_SEARCH_MODE_INACTIVE;
157 lifecycle_init_commit(&g_log_search_lifecycle);
158 mutex_unlock(&g_search_state.mutex);
159 return ASCIICHAT_OK;
160 }
161
162 // Load CLI pattern into input buffer
163 SAFE_STRNCPY(g_search_state.input_buffer, cli_pattern, LOG_SEARCH_INPUT_BUFFER_SIZE - 1);
164 g_search_state.len = strlen(cli_pattern);
165 g_search_state.cursor = g_search_state.len;
166 g_search_state.mode = LOG_SEARCH_MODE_ACTIVE;
168
169 // Compile the initial pattern
170 grep_parse_result_t parsed = grep_parse_pattern(cli_pattern);
171 if (parsed.valid) {
173 if (singleton) {
174 g_search_state.active_patterns[0] = singleton;
175 g_search_state.active_pattern_count = 1;
176 }
177 g_search_state.case_insensitive = parsed.case_insensitive;
178 g_search_state.fixed_string = parsed.is_fixed_string;
179 g_search_state.global_highlight = parsed.global_flag;
180 g_search_state.invert_match = parsed.invert;
181 g_search_state.context_before = parsed.context_before;
182 g_search_state.context_after = parsed.context_after;
183 }
184
185 atomic_store_bool(&g_search_state.needs_rerender, true);
186 lifecycle_init_commit(&g_log_search_lifecycle);
187
188 mutex_unlock(&g_search_state.mutex);
189 return ASCIICHAT_OK;
190}
191
193 if (!lifecycle_shutdown(&g_log_search_lifecycle)) {
194 return; // Already shut down or never initialized
195 }
196
197 mutex_lock(&g_search_state.mutex);
198
199 // Free active patterns
200 for (int i = 0; i < g_search_state.active_pattern_count; i++) {
201 if (g_search_state.active_patterns[i]) {
202 // Note: pcre2_singleton handles its own reference counting
203 g_search_state.active_patterns[i] = NULL;
204 }
205 }
206
207 mutex_unlock(&g_search_state.mutex);
208}
209
210/* ============================================================================
211 * Mode Management
212 * ========================================================================== */
213
215 // Ensure cursor is visible for user typing
216 (void)terminal_cursor_show();
217
218 mutex_lock(&g_search_state.mutex);
219
220 // Save current patterns (CLI --grep patterns)
222 if (result != ASCIICHAT_OK) {
223 log_warn("Failed to save filter patterns");
224 }
225
226 // Clear input buffer
227 memset(g_search_state.input_buffer, 0, sizeof(g_search_state.input_buffer));
228 g_search_state.len = 0;
229 g_search_state.cursor = 0;
230
231 // Pre-populate with CLI --grep pattern if available (only once at startup)
232 if (!g_search_state.cli_pattern_auto_populated) {
233 const char *cli_pattern = grep_get_last_pattern();
234 if (cli_pattern && cli_pattern[0] != '\0') {
235 // Skip leading slash if present (prompt already shows "/")
236 const char *pattern_to_use = cli_pattern;
237 if (cli_pattern[0] == '/') {
238 pattern_to_use = cli_pattern + 1;
239 }
240
241 size_t pattern_len = strlen(pattern_to_use);
242 if (pattern_len < sizeof(g_search_state.input_buffer) - 1) {
243 memcpy(g_search_state.input_buffer, pattern_to_use, pattern_len + 1);
244 g_search_state.len = pattern_len;
245 g_search_state.cursor = pattern_len;
246 g_search_state.cli_pattern_auto_populated = true;
247
248 // Compile and apply the pattern immediately so it starts filtering
249 grep_parse_result_t parsed = grep_parse_pattern(g_search_state.input_buffer);
250 if (parsed.valid) {
251 // Store parsed flags
252 g_search_state.case_insensitive = parsed.case_insensitive;
253 g_search_state.fixed_string = parsed.is_fixed_string;
254 g_search_state.global_highlight = parsed.global_flag;
255 g_search_state.invert_match = parsed.invert;
256 g_search_state.context_before = parsed.context_before;
257 g_search_state.context_after = parsed.context_after;
258
259 // Compile pattern if not fixed string
260 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
262 if (singleton) {
263 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
264 if (code) {
265 g_search_state.active_patterns[0] = singleton;
266 g_search_state.active_pattern_count = 1;
267 } else {
268 g_search_state.fixed_string = true;
269 }
270 } else {
271 g_search_state.fixed_string = true;
272 }
273 }
274 }
275 }
276 }
277 }
278
279 // Enter input mode
280 g_search_state.mode = LOG_SEARCH_MODE_ENTERING;
282 atomic_store_bool(&g_search_state.needs_rerender, true);
283
284 mutex_unlock(&g_search_state.mutex);
285}
286
287void log_search_exit_mode(bool accept) {
288 mutex_lock(&g_search_state.mutex);
289
290 if (g_search_state.mode != LOG_SEARCH_MODE_ENTERING) {
291 mutex_unlock(&g_search_state.mutex);
292 return;
293 }
294
295 if (!accept) {
296 // Cancel - restore previous patterns and reset CLI pattern flag so user can re-populate
298 if (result != ASCIICHAT_OK) {
299 log_warn("Failed to restore filter patterns");
300 }
301
302 // Clear interactive grep patterns
303 for (int i = 0; i < g_search_state.active_pattern_count; i++) {
304 g_search_state.active_patterns[i] = NULL;
305 }
306 g_search_state.active_pattern_count = 0;
307
308 // Reset the CLI pattern auto-populated flag so user can re-enter grep mode with the pattern again
309 g_search_state.cli_pattern_auto_populated = false;
310
311 g_search_state.mode = LOG_SEARCH_MODE_INACTIVE;
313 atomic_store_bool(&g_search_state.needs_rerender, true);
314 mutex_unlock(&g_search_state.mutex);
315 return;
316 }
317
318 // Accept - parse and compile pattern
319 grep_parse_result_t parsed = grep_parse_pattern(g_search_state.input_buffer);
320
321 if (!parsed.valid) {
322 log_error("Invalid pattern format");
323 mutex_unlock(&g_search_state.mutex);
324 return; // Stay in input mode
325 }
326
327 // Clear old regex patterns
328 for (int i = 0; i < g_search_state.active_pattern_count; i++) {
329 g_search_state.active_patterns[i] = NULL;
330 }
331 g_search_state.active_pattern_count = 0;
332
333 // Store parsed flags first
334 g_search_state.case_insensitive = parsed.case_insensitive;
335 g_search_state.fixed_string = parsed.is_fixed_string;
336 g_search_state.global_highlight = parsed.global_flag;
337 g_search_state.invert_match = parsed.invert;
338 g_search_state.context_before = parsed.context_before;
339 g_search_state.context_after = parsed.context_after;
340
341 // Compile and store new pattern (if not fixed string)
342 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
344 if (singleton) {
345 // Verify the pattern actually compiles
346 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
347 if (code) {
348 g_search_state.active_patterns[0] = singleton;
349 g_search_state.active_pattern_count = 1;
350 } else {
351 // Compilation failed - fall back to fixed string matching
352 g_search_state.fixed_string = true; // Override to use fixed string instead
353 }
354 } else {
355 // Malloc failed - fall back to fixed string
356 g_search_state.fixed_string = true;
357 }
358 }
359 // For fixed strings, pattern stays in input_buffer and fixed_string=true
360
361 g_search_state.mode = LOG_SEARCH_MODE_ACTIVE;
363 atomic_store_bool(&g_search_state.needs_rerender, true);
364
365 mutex_unlock(&g_search_state.mutex);
366}
367
368/* ============================================================================
369 * Signal-Safe Interface
370 * ========================================================================== */
371
375
377 atomic_store_bool(&g_search_state.signal_cancelled, true);
378}
379
381 bool expected = true;
382 return atomic_cas_bool(&g_search_state.signal_cancelled, &expected, false);
383}
384
386 // Use atomic read (async-signal-safe) to avoid mutex issues when called from signal handlers
387 return atomic_load_int(&g_search_state.mode_atomic) == LOG_SEARCH_MODE_ENTERING;
388}
389
391 // Use atomic read (async-signal-safe) to avoid mutex issues when called from signal handlers
392 return atomic_load_int(&g_search_state.mode_atomic) != LOG_SEARCH_MODE_INACTIVE;
393}
394
395/* ============================================================================
396 * Keyboard Handling
397 * ========================================================================== */
398
400 mutex_lock(&g_search_state.mutex);
401
402 // If in input mode, handle all keys
403 if (g_search_state.mode == LOG_SEARCH_MODE_ENTERING) {
404 mutex_unlock(&g_search_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_search_state.mutex);
411 return should_handle;
412}
413
415 mutex_lock(&g_search_state.mutex);
416
417 // If not in input mode, check if user pressed '/'
418 if (g_search_state.mode != LOG_SEARCH_MODE_ENTERING) {
419 mutex_unlock(&g_search_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 log_search_gather_and_filter_logs().
433 .buffer = g_search_state.input_buffer,
434 .max_len = sizeof(g_search_state.input_buffer),
435 .len = &g_search_state.len,
436 .cursor = &g_search_state.cursor,
437 .echo = false, // We handle rendering ourselves
438 .mask_char = 0, // No masking
439 .prefix = NULL, // Don't render prefix - log_search_render_input_line() handles it
440 .validator = validate_pcre2_pattern, // Live validation
441 .key = key // Pass the pre-read key
442 };
443
445
446 switch (result) {
448 // Unlock before exit_mode since it acquires the mutex
449 mutex_unlock(&g_search_state.mutex);
450 log_search_exit_mode(true); // Parse and accept pattern
451 break;
453 // Unlock before exit_mode since it acquires the mutex
454 mutex_unlock(&g_search_state.mutex);
455 log_search_exit_mode(false); // Restore previous
456 break;
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_search_state.len == 0) {
463 g_search_state.active_pattern_count = 0;
464 mutex_unlock(&g_search_state.mutex);
465 atomic_store_bool(&g_search_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_search_state.input_buffer);
471
472 if (!parsed.valid) {
473 // Invalid pattern - keep previous patterns active (don't clear)
474 mutex_unlock(&g_search_state.mutex);
475 atomic_store_bool(&g_search_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_search_state.active_pattern_count = 0;
483 g_search_state.case_insensitive = parsed.case_insensitive;
484 g_search_state.fixed_string = true;
485 } else {
486 // Regex pattern - compile with full pcre2_options
488 if (singleton) {
489 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
490 if (code) {
491 g_search_state.active_patterns[0] = singleton;
492 g_search_state.active_pattern_count = 1;
493 g_search_state.case_insensitive = parsed.case_insensitive;
494 g_search_state.fixed_string = false;
495 } else {
496 // Compilation failed - fall back to fixed string matching
497 g_search_state.active_pattern_count = 0;
498 g_search_state.fixed_string = true;
499 g_search_state.case_insensitive = parsed.case_insensitive;
500 }
501 } else {
502 g_search_state.active_pattern_count = 0;
503 g_search_state.fixed_string = true;
504 g_search_state.case_insensitive = parsed.case_insensitive;
505 }
506 }
507
508 // Store parsed flags
509 g_search_state.global_highlight = parsed.global_flag;
510 g_search_state.invert_match = parsed.invert;
511 g_search_state.context_before = parsed.context_before;
512 g_search_state.context_after = parsed.context_after;
513
514 mutex_unlock(&g_search_state.mutex);
515 atomic_store_bool(&g_search_state.needs_rerender, true);
516 break;
518 // No input available
519 mutex_unlock(&g_search_state.mutex);
520 break;
521 }
522
523 return ASCIICHAT_OK;
524}
525
526/* ============================================================================
527 * Log Filtering
528 * ========================================================================== */
529
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 =
538 if (!buffer_entries) {
539 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate log buffer");
540 }
541
543 size_t buffer_count = session_log_buffer_get_recent(log_buf, buffer_entries, SESSION_LOG_BUFFER_SIZE);
544
545 // Check if filtering is active (either regex patterns or fixed string)
546 mutex_lock(&g_search_state.mutex);
547 bool has_regex_patterns = (g_search_state.active_pattern_count > 0);
548 bool has_fixed_string = (g_search_state.fixed_string && g_search_state.len > 0);
549 bool filtering_active = has_regex_patterns || has_fixed_string;
550
551 if (!filtering_active) {
552 // No filtering active - return all entries
553 mutex_unlock(&g_search_state.mutex);
554 *out_entries = buffer_entries;
555 *out_count = buffer_count;
556 return ASCIICHAT_OK;
557 }
558
559 // Filter entries with PCRE2
560 session_log_entry_t *filtered =
562 if (!filtered) {
563 mutex_unlock(&g_search_state.mutex);
564 SAFE_FREE(buffer_entries);
565 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate filtered buffer");
566 }
567
568 size_t filtered_count = 0;
569
570 // Create match data for PCRE2 matching
571 pcre2_match_data *match_data = NULL;
572 for (int p = 0; p < g_search_state.active_pattern_count; p++) {
573 if (g_search_state.active_patterns[p]) {
574 pcre2_code *code = asciichat_pcre2_singleton_get_code(g_search_state.active_patterns[p]);
575 if (code) {
576 match_data = pcre2_match_data_create_from_pattern(code, NULL);
577 break;
578 }
579 }
580 }
581
582 if (!match_data && g_search_state.active_pattern_count > 0) {
583 // Couldn't create match data - fall back to returning all entries
584 mutex_unlock(&g_search_state.mutex);
585 SAFE_FREE(filtered);
586 *out_entries = buffer_entries;
587 *out_count = buffer_count;
588 return ASCIICHAT_OK;
589 }
590
591 // Parse pattern once before loop (for fixed string mode)
592 grep_parse_result_t parsed = {0};
593 if (has_fixed_string) {
594 parsed = grep_parse_pattern(g_search_state.input_buffer);
595 }
596
597 for (size_t i = 0; i < buffer_count; i++) {
598 const char *original_message = buffer_entries[i].message;
599 bool matches = false;
600
601 // Strip ANSI codes to match against plain text (consistent with highlighting)
602 char plain_message[SESSION_LOG_LINE_MAX] = {0};
603 const char *strip_ansi_codes_fn(const char *src, char *dst, size_t dst_size);
604 // Use inline stripping to avoid function call overhead
605 {
606 size_t pos = 0;
607 const char *src = original_message;
608 char *dst = plain_message;
609 size_t dst_size = sizeof(plain_message);
610
611 while (*src && pos < dst_size - 1) {
612 if (*src == '\x1b' && src[1] != '\0' && src[1] == '[') {
613 // CSI sequence - skip until final byte
614 src += 2;
615 while (*src && pos < dst_size - 1 && !(*src >= 0x40 && *src <= 0x7E)) {
616 src++;
617 }
618 if (*src)
619 src++; // Skip final byte
620 } else {
621 dst[pos++] = *src++;
622 }
623 }
624 dst[pos] = '\0';
625 }
626
627 size_t message_len = strlen(plain_message);
628
629 // Fixed string matching
630 if (has_fixed_string) {
631 const char *search_pattern = parsed.pattern;
632
633 if (g_search_state.case_insensitive) {
634 // Unicode-aware case-insensitive fixed string search
635 matches = (utf8_strcasestr(plain_message, search_pattern) != NULL);
636 } else {
637 // Case-sensitive fixed string search
638 matches = (strstr(plain_message, search_pattern) != NULL);
639 }
640 }
641 // Regex pattern matching
642 else if (has_regex_patterns) {
643 // Check against all active patterns (OR logic)
644 for (int p = 0; p < g_search_state.active_pattern_count; p++) {
645 pcre2_singleton_t *singleton = g_search_state.active_patterns[p];
646 if (!singleton) {
647 continue;
648 }
649
650 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
651 if (!code) {
652 continue;
653 }
654
655 // Match against plain message (without ANSI codes)
656 int rc = pcre2_jit_match(code, (PCRE2_SPTR)plain_message, message_len, 0, 0, match_data, NULL);
657 if (rc >= 0) {
658 matches = true;
659 break;
660 }
661 }
662 }
663
664 // Apply invert logic
665 if (g_search_state.invert_match) {
666 matches = !matches;
667 }
668
669 if (matches) {
670 filtered[filtered_count++] = buffer_entries[i];
671 }
672 }
673
674 // Free match data
675 if (match_data) {
676 pcre2_match_data_free(match_data);
677 }
678
679 mutex_unlock(&g_search_state.mutex);
680
681 SAFE_FREE(buffer_entries);
682 *out_entries = filtered;
683 *out_count = filtered_count;
684 return ASCIICHAT_OK;
685}
686
687/* ============================================================================
688 * Display Rendering
689 * ========================================================================== */
690
692 (void)width; // Reserved for future use (line truncation)
693
694 mutex_lock(&g_search_state.mutex);
695
696 if (g_search_state.mode != LOG_SEARCH_MODE_ENTERING) {
697 mutex_unlock(&g_search_state.mutex);
698 return;
699 }
700
701 // Render the pattern and position terminal cursor at current edit position
702 char output_buf[512];
703
704 // Build output: "/" + pattern
705 int pattern_len =
706 snprintf(output_buf, sizeof(output_buf), "/%.*s", (int)g_search_state.len, g_search_state.input_buffer);
707
708 if (pattern_len > 0 && pattern_len < (int)sizeof(output_buf)) {
709 // Write the pattern to terminal
710 platform_write_all(STDOUT_FILENO, output_buf, pattern_len);
711
712 // After writing, cursor is at: column = 1 (for "/") + len + 1
713 // We want it at: column = 1 (for "/") + cursor_position + 1
714 // So we need to move back by: len - cursor_position columns
715 int cursor_offset = (int)g_search_state.len - (int)g_search_state.cursor;
716 if (cursor_offset > 0) {
717 // Move left by the difference
718 terminal_move_cursor_relative(-cursor_offset);
719 }
720 }
721
722 mutex_unlock(&g_search_state.mutex);
723}
724
725/* ============================================================================
726 * Display Highlighting
727 * ========================================================================== */
728
729bool log_search_get_match_info(const char *message, size_t *out_match_start, size_t *out_match_len) {
730 if (!message || !out_match_start || !out_match_len) {
731 return false;
732 }
733
734 *out_match_start = 0;
735 *out_match_len = 0;
736
737 // Use atomic read to avoid mutex contention with keyboard handler
738 int mode = atomic_load_int(&g_search_state.mode_atomic);
739 if (mode == LOG_SEARCH_MODE_INACTIVE) {
740 return false;
741 }
742
743 // Make a safe copy of state under mutex, then release before doing expensive work
744 mutex_lock(&g_search_state.mutex);
745
746 bool has_fixed_string = g_search_state.fixed_string && g_search_state.len > 0;
747 bool case_insensitive = g_search_state.case_insensitive;
748 char pattern_copy[LOG_SEARCH_INPUT_BUFFER_SIZE];
749 size_t pattern_len = g_search_state.len;
750 int pattern_count = g_search_state.active_pattern_count;
752
753 if (has_fixed_string) {
754 // Parse the input buffer to extract just the pattern part (without flags)
755 grep_parse_result_t parsed = grep_parse_pattern(g_search_state.input_buffer);
756 const char *src = parsed.valid ? parsed.pattern : g_search_state.input_buffer;
757 SAFE_STRNCPY(pattern_copy, src, sizeof(pattern_copy) - 1);
758 pattern_copy[sizeof(pattern_copy) - 1] = '\0';
759 pattern_len = strlen(pattern_copy); // Update pattern_len for the actual parsed pattern
760 }
761
762 for (int i = 0; i < pattern_count && i < MAX_LOG_SEARCH_PATTERNS; i++) {
763 patterns_copy[i] = g_search_state.active_patterns[i];
764 }
765
766 mutex_unlock(&g_search_state.mutex);
767
768 // Now do matching without holding mutex
769 size_t message_len = strlen(message);
770 bool matches = false;
771
772 // Fixed string matching
773 if (has_fixed_string) {
774 const char *found = NULL;
775
776 if (case_insensitive) {
777 found = utf8_strcasestr(message, pattern_copy);
778 } else {
779 found = strstr(message, pattern_copy);
780 }
781
782 if (found) {
783 matches = true;
784
785 // Convert byte position to character position (UTF-8 aware)
786 size_t byte_pos = (size_t)(found - message);
787 size_t char_pos = 0;
788 for (size_t i = 0; i < byte_pos && message[i] != '\0';) {
789 uint32_t codepoint;
790 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
791 if (utf8_len <= 0)
792 utf8_len = 1;
793 i += utf8_len;
794 char_pos++;
795 }
796
797 // Count characters in the match (pattern_len is in bytes for the pattern)
798 size_t match_char_len = 0;
799 for (size_t i = 0; i < pattern_len && found[i] != '\0';) {
800 uint32_t codepoint;
801 int utf8_len = utf8_decode((const uint8_t *)(found + i), &codepoint);
802 if (utf8_len <= 0)
803 utf8_len = 1;
804 i += utf8_len;
805 match_char_len++;
806 }
807
808 *out_match_start = char_pos;
809 *out_match_len = match_char_len;
810 }
811 }
812 // Regex pattern matching
813 else if (pattern_count > 0) {
814 pcre2_match_data *match_data = NULL;
815 for (int p = 0; p < pattern_count; p++) {
816 if (patterns_copy[p]) {
817 pcre2_code *code = asciichat_pcre2_singleton_get_code(patterns_copy[p]);
818 if (code) {
819 match_data = pcre2_match_data_create_from_pattern(code, NULL);
820 break;
821 }
822 }
823 }
824
825 if (match_data) {
826 for (int p = 0; p < pattern_count; p++) {
827 pcre2_singleton_t *singleton = patterns_copy[p];
828 if (!singleton) {
829 continue;
830 }
831
832 pcre2_code *code = asciichat_pcre2_singleton_get_code(singleton);
833 if (!code) {
834 continue;
835 }
836
837 int rc = pcre2_jit_match(code, (PCRE2_SPTR)message, message_len, 0, 0, match_data, NULL);
838 if (rc >= 0) {
839 matches = true;
840 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
841
842 // Convert byte positions to character positions (UTF-8 aware)
843 size_t byte_start = (size_t)ovector[0];
844 size_t byte_end = (size_t)ovector[1];
845
846 // Count characters up to byte_start
847 size_t char_start = 0;
848 for (size_t i = 0; i < byte_start && message[i] != '\0';) {
849 uint32_t codepoint;
850 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
851 if (utf8_len <= 0)
852 utf8_len = 1;
853 i += utf8_len;
854 char_start++;
855 }
856
857 // Count characters up to byte_end
858 size_t char_end = char_start;
859 for (size_t i = byte_start; i < byte_end && message[i] != '\0';) {
860 uint32_t codepoint;
861 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
862 if (utf8_len <= 0)
863 utf8_len = 1;
864 i += utf8_len;
865 char_end++;
866 }
867
868 *out_match_start = char_start;
869 *out_match_len = char_end - char_start;
870 break;
871 }
872 }
873 pcre2_match_data_free(match_data);
874 }
875 }
876
877 return matches;
878}
879
880/* ============================================================================
881 * Re-render Notification
882 * ========================================================================== */
883
885 bool needs = atomic_load_bool(&g_search_state.needs_rerender);
886 if (needs) {
887 atomic_store_bool(&g_search_state.needs_rerender, false);
888 }
889 return needs;
890}
891
892/* ============================================================================
893 * Query Functions
894 * ========================================================================== */
895
897 // Use atomic read to avoid mutex contention with keyboard handler
898 int mode = atomic_load_int(&g_search_state.mode_atomic);
899 if (mode == LOG_SEARCH_MODE_INACTIVE) {
900 return false;
901 }
902
903 // Safe read under mutex (quick operation)
904 mutex_lock(&g_search_state.mutex);
905 bool result = g_search_state.global_highlight;
906 mutex_unlock(&g_search_state.mutex);
907
908 return result;
909}
910
912 // Use atomic read to check if active
913 int mode = atomic_load_int(&g_search_state.mode_atomic);
914 if (mode == LOG_SEARCH_MODE_INACTIVE) {
915 return NULL;
916 }
917
918 // Safe read under mutex
919 mutex_lock(&g_search_state.mutex);
920
921 // Return the compiled pattern if we have one (not fixed string)
922 void *result = NULL;
923 if (g_search_state.active_pattern_count > 0 && !g_search_state.fixed_string) {
924 result = (void *)g_search_state.active_patterns[0];
925 }
926
927 mutex_unlock(&g_search_state.mutex);
928
929 return result;
930}
931
932/* ============================================================================
933 * Internal Access for Atomic Rendering
934 * ========================================================================== */
935
937 return (void *)&g_search_state.mutex;
938}
939
941 return (int)g_search_state.len;
942}
943
945 return (int)g_search_state.cursor;
946}
947
949 return g_search_state.input_buffer;
950}
951
953 return g_search_state.case_insensitive;
954}
ANSI escape sequence utilities and fast color code generation.
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_int(atomic_t *a, int value)
Atomically store an int value.
Definition atomic.c:202
int atomic_load_int(atomic_t *a)
Atomically load an int value.
Definition atomic.c:194
bool atomic_cas_bool(atomic_t *a, bool *expected, bool new_value)
Atomically compare-and-swap a boolean.
Definition atomic.c:184
⚛️ Atomic operations abstraction layer with debug tracking
⚙️ Common definitions, error codes, macros, and types shared throughout the application
asciichat_error_t grep_restore_patterns(void)
Restore previously saved filter patterns.
Definition grep.c:1265
asciichat_error_t grep_save_patterns(void)
Save current filter patterns for later restoration.
Definition grep.c:1215
int pattern_count
Number of active patterns.
Definition grep.c:82
const char * grep_get_last_pattern(void)
Get the last (most recent) filter pattern string.
Definition grep.c:1326
grep_parse_result_t grep_parse_pattern(const char *input)
Parse pattern in /pattern/flags or pattern/flags format.
Definition grep.c:351
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 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_warn(...)
Log a WARN message.
Definition log/log.h:574
session_log_buffer_t * log_get_session_log_buffer(void)
Get the currently registered session log buffer.
Definition log/log.c:1857
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
asciichat_error_t terminal_move_cursor_relative(int offset)
Move cursor relative to current position.
keyboard_line_edit_result_t keyboard_read_line_interactive(keyboard_line_edit_opts_t *opts)
Process one keystroke for interactive line editing.
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.
Definition system.c:224
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
keyboard_key_t
Unified keyboard key code enumeration.
Definition keyboard.h:54
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
asciichat_error_t terminal_cursor_show(void)
Show terminal cursor.
keyboard_line_edit_result_t
Result codes for interactive line editing.
Definition keyboard.h:212
@ LINE_EDIT_ACCEPTED
User pressed Enter (accept input)
Definition keyboard.h:214
@ LINE_EDIT_CANCELLED
User pressed Escape/Ctrl+C (cancel input)
Definition keyboard.h:215
@ LINE_EDIT_CONTINUE
Keep editing (more input needed)
Definition keyboard.h:213
@ LINE_EDIT_NO_INPUT
No key available (non-blocking mode)
Definition keyboard.h:216
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
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Thread-safe circular log buffer for session screens (status, splash)
🎮 Cross-platform keyboard input interface for ascii-chat
void lifecycle_init_commit(lifecycle_t *lc)
Definition lifecycle.c:90
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
bool lifecycle_init_once(lifecycle_t *lc)
Definition lifecycle.c:52
Lock-free module lifecycle state machine using pure stdatomic.
@ LIFECYCLE_SYNC_MUTEX
Contains mutex_t pointer.
Definition lifecycle.h:50
#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
Cross-platform mutex interface for ascii-chat.
size_t session_log_buffer_get_recent(session_log_buffer_t *buf, session_log_entry_t *out_entries, size_t max_count)
Get recent log entries from buffer.
asciichat_error_t log_search_init(void)
Initialize interactive grep subsystem.
Definition search.c:125
void * log_search_get_mutex(void)
Get the mutex protecting grep state.
Definition search.c:936
bool log_search_needs_rerender(void)
Check if screen needs immediate re-render.
Definition search.c:884
bool log_search_check_signal_cancel(void)
Check and clear the signal-cancel flag.
Definition search.c:380
void log_search_signal_cancel(void)
Cancel grep mode from a signal handler (async-signal-safe)
Definition search.c:376
bool log_search_is_entering(void)
Check if currently in input mode (typing pattern)
Definition search.c:385
#define MAX_LOG_SEARCH_PATTERNS
Maximum number of patterns to support.
Definition search.c:32
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
void log_search_enter_mode(void)
Enter search mode (user pressed '/')
Definition search.c:214
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_exit_mode(bool accept)
Exit search mode.
Definition search.c:287
void * log_search_get_pattern_singleton(void)
Get the compiled regex pattern for interactive grep (internal use)
Definition search.c:911
bool log_search_get_match_info(const char *message, size_t *out_match_start, size_t *out_match_len)
Get match info for highlighting a log message.
Definition search.c:729
bool log_search_should_handle(int key)
Check if a key should be handled by grep module.
Definition search.c:399
void log_search_destroy(void)
Clean up interactive grep subsystem.
Definition search.c:192
int log_search_get_cursor_position(void)
Get current cursor position in input buffer (must hold mutex)
Definition search.c:944
asciichat_error_t log_search_handle_key(keyboard_key_t key)
Process keyboard input for grep.
Definition search.c:414
void log_search_render_input_line(int width)
Render grep input line at bottom of screen.
Definition search.c:691
bool log_search_is_entering_atomic(void)
Check if grep is in entering mode (async-signal-safe)
Definition search.c:372
asciichat_error_t log_search_gather_and_filter_logs(session_log_entry_t **out_entries, size_t *out_count)
Gather and filter logs for display.
Definition search.c:530
#define LOG_SEARCH_INPUT_BUFFER_SIZE
Input buffer size.
Definition search.c:33
Interactive search filtering for terminal screens.
log_search_mode_t
Search mode states.
Definition search.h:32
@ LOG_SEARCH_MODE_ACTIVE
Pattern accepted and filtering.
Definition search.h:35
@ LOG_SEARCH_MODE_ENTERING
'/' pressed, typing pattern
Definition search.h:34
@ LOG_SEARCH_MODE_INACTIVE
Not in search mode.
Definition search.h:33
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
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
Options for interactive line editing.
Definition keyboard.h:192
char * buffer
Input buffer (modified in-place)
Definition keyboard.h:193
union lifecycle_t::@24 sync
mutex_t * mutex
Pointer to mutex (if sync_type == LIFECYCLE_SYNC_MUTEX)
Definition lifecycle.h:62
lifecycle_sync_type_t sync_type
Type of sync primitive (if any)
Definition lifecycle.h:60
atomic_t mode_atomic
Shadow of mode for signal-safe reads.
Definition search.c:65
bool cli_pattern_auto_populated
Track if CLI pattern was already populated.
Definition search.c:66
int previous_pattern_count
Definition search.c:47
pcre2_singleton_t * active_patterns[32]
Definition search.c:50
int active_pattern_count
Definition search.c:51
log_search_mode_t mode
Definition search.c:40
atomic_t needs_rerender
Definition search.c:63
atomic_t signal_cancelled
Set by signal handler, checked by render loop.
Definition search.c:64
bool global_highlight
Definition search.c:56
bool case_insensitive
Definition search.c:54
mutex_t mutex
Definition search.c:61
char input_buffer[256]
Definition search.c:41
lifecycle_t lifecycle
Definition search.c:62
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:22
Internal circular buffer structure.
Single log entry with sequence number for ordering.
char message[1024]
Formatted log message (may include ANSI colors)
🖥️ Cross-platform terminal interface for ascii-chat
🔤 UTF-8 Encoding and Decoding Utilities