ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
search.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define LOG_SEARCH_OUTPUT_BUFFER_SIZE 512
4
5#include <ascii-chat/otel.h>
13
16#include "ascii-chat/common.h"
17#include "ascii-chat/log/log.h"
18#include "ascii-chat/log/grep.h"
20#include <libplatform/mutex.h>
21#include <libplatform/terminal.h>
22#include <libplatform/lifecycle.h>
28#include <ascii-chat/ui/input.h>
31
32#include <pcre2.h>
33#include <string.h>
34#include <stdio.h>
35#include <libplatform/atomic.h>
36
37/* ============================================================================
38 * Constants
39 * ========================================================================== */
40
41#define MAX_LOG_SEARCH_PATTERNS 32
42#define LOG_SEARCH_INPUT_BUFFER_SIZE 256
43#define LOG_SEARCH_DEBOUNCE_NS 200000000ULL
44
45/* ============================================================================
46 * State Structure
47 * ========================================================================== */
48
84
85static log_search_state_t g_search_state = {
87 .len = 0,
88 .cursor = 0,
89 .previous_pattern_count = 0,
90 .active_pattern_count = 0,
91 .case_insensitive = false,
92 .fixed_string = false,
93 .global_highlight = false,
94 .invert_match = false,
95 .context_before = 0,
96 .context_after = 0,
97 .lifecycle = LIFECYCLE_INIT,
98 .needs_rerender = {0},
99 .signal_cancelled = {0},
100 .mode_atomic = {0},
101 .cli_pattern_auto_populated = false,
102};
103
104static lifecycle_t g_log_search_lifecycle = LIFECYCLE_INIT;
105
106/* ============================================================================
107 * Pattern Validation
108 * ========================================================================== */
109
115static bool validate_pcre2_pattern(const char *input) {
116 if (!input || strlen(input) == 0) {
117 return true; // Empty is valid (no filtering)
118 }
119
120 // Use the shared parser from filter.c
122
123 if (!parsed.valid) {
124 return false; // Invalid format
125 }
126
127 // If fixed string, always valid
128 if (parsed.is_fixed_string) {
129 return true;
130 }
131
132 // Try to compile pattern with full pcre2_options (includes UTF, UCP, CASELESS, etc.)
133 pcre2_code *code = asciichat_pcre2_compile(parsed.pattern, parsed.pcre2_options);
134 if (!code)
135 return false;
136 pcre2_code_free(code);
137 return true;
138}
139
140/* ============================================================================
141 * Lifecycle Functions
142 * ========================================================================== */
143
145 // Use lifecycle API for unified initialization
146 if (!lifecycle_init_once(&g_log_search_lifecycle)) {
147 // Already initialized.
148 return ASCIICHAT_OK;
149 }
150
151 memset(&g_search_state, 0, sizeof(g_search_state));
152 if (mutex_init(&g_search_state.mutex, "log_search") != 0) {
153 lifecycle_init_abort(&g_log_search_lifecycle);
154 return SET_ERRNO(ERROR_THREAD, "Cannot initialize log search");
155 }
156 mutex_lock(&g_search_state.mutex);
157
158 // Check if there are CLI --grep patterns to use
159 const char *cli_pattern = grep_get_last_pattern();
160
161 // Start in INACTIVE mode if no CLI pattern is provided.
162 // Only use patterns from CLI --grep arguments, not default patterns.
163 // This ensures logs are visible by default and only filtered if explicitly requested.
164 if (!cli_pattern || cli_pattern[0] == '\0') {
165 // No CLI pattern - start inactive
166 g_search_state.mode = LOG_SEARCH_MODE_INACTIVE;
167 atomic_store_int(&g_search_state.mode_atomic, LOG_SEARCH_MODE_INACTIVE);
168 lifecycle_init_commit(&g_log_search_lifecycle);
169 mutex_unlock(&g_search_state.mutex);
170 return ASCIICHAT_OK;
171 }
172
173 // Load CLI pattern into input buffer
174 SAFE_STRNCPY(g_search_state.input_buffer, cli_pattern, LOG_SEARCH_INPUT_BUFFER_SIZE - 1);
175 g_search_state.len = strlen(cli_pattern);
176 g_search_state.cursor = g_search_state.len;
177 g_search_state.mode = LOG_SEARCH_MODE_ACTIVE;
178 atomic_store_int(&g_search_state.mode_atomic, LOG_SEARCH_MODE_ACTIVE);
179
180 // Compile the initial pattern
181 grep_parse_result_t parsed = grep_parse_pattern(cli_pattern);
182 if (parsed.valid) {
183 pcre2_code *compiled = asciichat_pcre2_compile(parsed.pattern, parsed.pcre2_options);
184 if (compiled) {
185 g_search_state.active_patterns[0] = compiled;
186 g_search_state.active_pattern_count = 1;
187 }
188 g_search_state.case_insensitive = parsed.case_insensitive;
189 g_search_state.fixed_string = parsed.is_fixed_string;
190 g_search_state.global_highlight = parsed.global_flag;
191 g_search_state.invert_match = parsed.invert;
192 g_search_state.context_before = parsed.context_before;
193 g_search_state.context_after = parsed.context_after;
194 }
195
196 atomic_store_bool(&g_search_state.needs_rerender, true);
197 lifecycle_init_commit(&g_log_search_lifecycle);
198
199 mutex_unlock(&g_search_state.mutex);
200 return ASCIICHAT_OK;
201}
202
204 if (!lifecycle_shutdown(&g_log_search_lifecycle)) {
205 return; // Already shut down or never initialized
206 }
207
208 mutex_lock(&g_search_state.mutex);
209
210 // Free active patterns
211 for (int i = 0; i < g_search_state.active_pattern_count; i++) {
212 if (g_search_state.active_patterns[i]) {
213 pcre2_code_free(g_search_state.active_patterns[i]);
214 g_search_state.active_patterns[i] = NULL;
215 }
216 }
217
218 mutex_unlock(&g_search_state.mutex);
219 mutex_destroy(&g_search_state.mutex);
220}
221
222/* ============================================================================
223 * Mode Management
224 * ========================================================================== */
225
227 ascii_otel_ui_usage("logs", "search_started", 0);
228 // Ensure cursor is visible for user typing
229 (void)terminal_cursor_show();
230
231 mutex_lock(&g_search_state.mutex);
232
233 // Save current patterns (CLI --grep patterns)
235 if (result != ASCIICHAT_OK) {
236 log_warn("Failed to save filter patterns");
237 }
238
239 // Clear input buffer
240 memset(g_search_state.input_buffer, 0, sizeof(g_search_state.input_buffer));
241 g_search_state.len = 0;
242 g_search_state.cursor = 0;
243 flow_control_debounce_cancel(&g_search_state.debounce);
244
245 // Pre-populate with CLI --grep pattern if available (only once at startup)
246 if (!g_search_state.cli_pattern_auto_populated) {
247 const char *cli_pattern = grep_get_last_pattern();
248 if (cli_pattern && cli_pattern[0] != '\0') {
249 // Skip leading slash if present (prompt already shows "/")
250 const char *pattern_to_use = cli_pattern;
251 if (cli_pattern[0] == '/') {
252 pattern_to_use = cli_pattern + 1;
253 }
254
255 size_t pattern_len = strlen(pattern_to_use);
256 if (pattern_len < sizeof(g_search_state.input_buffer) - 1) {
257 memcpy(g_search_state.input_buffer, pattern_to_use, pattern_len + 1);
258 g_search_state.len = pattern_len;
259 g_search_state.cursor = pattern_len;
260 g_search_state.cli_pattern_auto_populated = true;
261
262 // Compile and apply the pattern immediately so it starts filtering
263 grep_parse_result_t parsed = grep_parse_pattern(g_search_state.input_buffer);
264 if (parsed.valid) {
265 // Store parsed flags
266 g_search_state.case_insensitive = parsed.case_insensitive;
267 g_search_state.fixed_string = parsed.is_fixed_string;
268 g_search_state.global_highlight = parsed.global_flag;
269 g_search_state.invert_match = parsed.invert;
270 g_search_state.context_before = parsed.context_before;
271 g_search_state.context_after = parsed.context_after;
272
273 // Compile pattern if not fixed string
274 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
275 pcre2_code *compiled = asciichat_pcre2_compile(parsed.pattern, parsed.pcre2_options);
276 if (compiled) {
277 pcre2_code *code = compiled;
278 if (code) {
279 if (g_search_state.active_pattern_count > 0)
280 pcre2_code_free(g_search_state.active_patterns[0]);
281 g_search_state.active_patterns[0] = compiled;
282 g_search_state.active_pattern_count = 1;
283 } else {
284 g_search_state.fixed_string = true;
285 }
286 } else {
287 g_search_state.fixed_string = true;
288 }
289 }
290 }
291 }
292 }
293 }
294
295 // Enter input mode
296 g_search_state.mode = LOG_SEARCH_MODE_ENTERING;
297 atomic_store_int(&g_search_state.mode_atomic, LOG_SEARCH_MODE_ENTERING);
298 atomic_store_bool(&g_search_state.needs_rerender, true);
299
300 mutex_unlock(&g_search_state.mutex);
301}
302
303void log_search_exit_mode(bool accept) {
304 mutex_lock(&g_search_state.mutex);
305
306 if (g_search_state.mode == LOG_SEARCH_MODE_INACTIVE || (accept && g_search_state.mode != LOG_SEARCH_MODE_ENTERING)) {
307 mutex_unlock(&g_search_state.mutex);
308 return;
309 }
310
311 if (!accept) {
312 flow_control_debounce_cancel(&g_search_state.debounce);
313 ascii_otel_ui_usage("logs", "search_cancelled", 0);
314 // Cancel - restore previous patterns and reset CLI pattern flag so user can re-populate
316 if (result != ASCIICHAT_OK) {
317 log_warn("Failed to restore filter patterns");
318 }
319
320 // Clear interactive grep patterns
321 for (int i = 0; i < g_search_state.active_pattern_count; i++) {
322 pcre2_code_free(g_search_state.active_patterns[i]);
323 g_search_state.active_patterns[i] = NULL;
324 }
325 g_search_state.active_pattern_count = 0;
326
327 // Reset the CLI pattern auto-populated flag so user can re-enter grep mode with the pattern again
328 g_search_state.cli_pattern_auto_populated = false;
329
330 g_search_state.mode = LOG_SEARCH_MODE_INACTIVE;
331 atomic_store_int(&g_search_state.mode_atomic, LOG_SEARCH_MODE_INACTIVE);
332 atomic_store_bool(&g_search_state.needs_rerender, true);
333 mutex_unlock(&g_search_state.mutex);
334 return;
335 }
336
337 // Accept - parse and compile pattern
338 flow_control_debounce_cancel(&g_search_state.debounce);
339 grep_parse_result_t parsed = grep_parse_pattern(g_search_state.input_buffer);
340
341 if (!parsed.valid) {
342 ascii_otel_ui_usage("logs", "search_rejected", 0);
343 log_error("Invalid pattern format");
344 mutex_unlock(&g_search_state.mutex);
345 return; // Stay in input mode
346 }
347
348 // Clear old regex patterns
349 for (int i = 0; i < g_search_state.active_pattern_count; i++) {
350 pcre2_code_free(g_search_state.active_patterns[i]);
351 g_search_state.active_patterns[i] = NULL;
352 }
353 g_search_state.active_pattern_count = 0;
354
355 // Store parsed flags first
356 g_search_state.case_insensitive = parsed.case_insensitive;
357 g_search_state.fixed_string = parsed.is_fixed_string;
358 g_search_state.global_highlight = parsed.global_flag;
359 g_search_state.invert_match = parsed.invert;
360 g_search_state.context_before = parsed.context_before;
361 g_search_state.context_after = parsed.context_after;
362
363 // Compile and store new pattern (if not fixed string)
364 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
365 pcre2_code *compiled = asciichat_pcre2_compile(parsed.pattern, parsed.pcre2_options);
366 if (compiled) {
367 // Verify the pattern actually compiles
368 pcre2_code *code = compiled;
369 if (code) {
370 g_search_state.active_patterns[0] = compiled;
371 g_search_state.active_pattern_count = 1;
372 } else {
373 // Compilation failed - fall back to fixed string matching
374 g_search_state.fixed_string = true; // Override to use fixed string instead
375 }
376 } else {
377 // Malloc failed - fall back to fixed string
378 g_search_state.fixed_string = true;
379 }
380 }
381 // For fixed strings, pattern stays in input_buffer and fixed_string=true
382
383 ascii_otel_ui_usage("logs", "filter_applied", 0);
384 g_search_state.mode = LOG_SEARCH_MODE_ACTIVE;
385 atomic_store_int(&g_search_state.mode_atomic, LOG_SEARCH_MODE_ACTIVE);
386 atomic_store_bool(&g_search_state.needs_rerender, true);
387
388 mutex_unlock(&g_search_state.mutex);
389}
390
391/* ============================================================================
392 * Signal-Safe Interface
393 * ========================================================================== */
394
396 return atomic_load_int(&g_search_state.mode_atomic) == LOG_SEARCH_MODE_ENTERING;
397}
398
400 atomic_store_bool(&g_search_state.signal_cancelled, true);
401}
402
404 bool expected = true;
405 return atomic_cas_bool(&g_search_state.signal_cancelled, &expected, false);
406}
407
409 // Use atomic read (async-signal-safe) to avoid mutex issues when called from signal handlers
410 return atomic_load_int(&g_search_state.mode_atomic) == LOG_SEARCH_MODE_ENTERING;
411}
412
414 // Use atomic read (async-signal-safe) to avoid mutex issues when called from signal handlers
415 return atomic_load_int(&g_search_state.mode_atomic) != LOG_SEARCH_MODE_INACTIVE;
416}
417
418/* ============================================================================
419 * Keyboard Handling
420 * ========================================================================== */
421
423 mutex_lock(&g_search_state.mutex);
424
425 // If in input mode, handle all keys
426 if (g_search_state.mode == LOG_SEARCH_MODE_ENTERING) {
427 mutex_unlock(&g_search_state.mutex);
428 return true;
429 }
430
431 // If not in input mode, only handle '/' to enter mode
432 bool should_handle = (key == '/');
433 mutex_unlock(&g_search_state.mutex);
434 return should_handle;
435}
436
437static void log_search_update_filter_locked(void) {
438 for (int i = 0; i < g_search_state.active_pattern_count; i++) {
439 pcre2_code_free(g_search_state.active_patterns[i]);
440 g_search_state.active_patterns[i] = NULL;
441 }
442 g_search_state.active_pattern_count = 0;
443 if (g_search_state.len == 0) {
444 g_search_state.active_pattern_count = 0;
445 g_search_state.fixed_string = false;
446 return;
447 }
448 grep_parse_result_t parsed = grep_parse_pattern(g_search_state.input_buffer);
449 if (!parsed.valid)
450 return;
451 g_search_state.active_pattern_count = 0;
452 g_search_state.case_insensitive = parsed.case_insensitive;
453 g_search_state.fixed_string = parsed.is_fixed_string;
454 if (!parsed.is_fixed_string) {
455 pcre2_code *compiled = asciichat_pcre2_compile(parsed.pattern, parsed.pcre2_options);
456 if (compiled) {
457 g_search_state.active_patterns[0] = compiled;
458 g_search_state.active_pattern_count = 1;
459 } else {
460 g_search_state.fixed_string = true;
461 }
462 }
463 g_search_state.global_highlight = parsed.global_flag;
464 g_search_state.invert_match = parsed.invert;
465 g_search_state.context_before = parsed.context_before;
466 g_search_state.context_after = parsed.context_after;
467}
468
470 mutex_lock(&g_search_state.mutex);
471 bool ready = g_search_state.mode == LOG_SEARCH_MODE_ENTERING &&
472 flow_control_debounce_take_ready(&g_search_state.debounce, time_get_ns());
473 if (ready)
474 log_search_update_filter_locked();
475 mutex_unlock(&g_search_state.mutex);
476 if (ready)
477 atomic_store_bool(&g_search_state.needs_rerender, true);
478}
479
481 if (!text || strlen(text) >= LOG_SEARCH_INPUT_BUFFER_SIZE)
482 return SET_ERRNO(ERROR_INVALID_PARAM, "Search paste exceeds input capacity");
484 return SET_ERRNO(ERROR_INVALID_STATE, "Search input is not active");
485 char normalized[LOG_SEARCH_INPUT_BUFFER_SIZE];
486 size_t i;
487 for (i = 0; text[i]; ++i) {
488 unsigned char byte = (unsigned char)text[i];
489 normalized[i] = byte < 32 || byte == 127 ? ' ' : (char)byte;
490 }
491 normalized[i] = '\0';
492 mutex_lock(&g_search_state.mutex);
494 if (g_search_state.mode != LOG_SEARCH_MODE_ENTERING)
495 result = SET_ERRNO(ERROR_INVALID_STATE, "Search input is not active");
496 else {
497 result = ui_input_insert(g_search_state.input_buffer, sizeof(g_search_state.input_buffer), &g_search_state.len,
498 &g_search_state.cursor, normalized);
499 if (result == ASCIICHAT_OK)
500 flow_control_debounce_trigger(&g_search_state.debounce, time_get_ns(), LOG_SEARCH_DEBOUNCE_NS);
501 }
502 mutex_unlock(&g_search_state.mutex);
503 if (result == ASCIICHAT_OK)
504 atomic_store_bool(&g_search_state.needs_rerender, true);
505 return result;
506}
507
509 mutex_lock(&g_search_state.mutex);
510
511 // If not in input mode, check if user pressed '/'
512 if (g_search_state.mode != LOG_SEARCH_MODE_ENTERING) {
513 mutex_unlock(&g_search_state.mutex);
514
515 // Check for '/' key (key already passed as parameter, don't read again!)
516 if (key == '/') {
518 }
519 return ASCIICHAT_OK;
520 }
521
522 // In input mode - use keyboard_read_line_interactive()
523 // Keep the mutex held during editing to prevent races
524 // with the rendering thread. The validator callback and buffer modifications must be
525 // synchronized with readers in log_search_gather_and_filter_logs().
527 .buffer = g_search_state.input_buffer,
528 .max_len = sizeof(g_search_state.input_buffer),
529 .len = &g_search_state.len,
530 .cursor = &g_search_state.cursor,
531 .echo = false, // We handle rendering ourselves
532 .mask_char = 0, // No masking
533 .prefix = NULL, // Don't render prefix - log_search_render_input_line() handles it
534 .validator = validate_pcre2_pattern, // Live validation
535 .key = key // Pass the pre-read key
536 };
537
539
540 switch (result) {
542 // Unlock before exit_mode since it acquires the mutex
543 mutex_unlock(&g_search_state.mutex);
544 log_search_exit_mode(true); // Parse and accept pattern
545 break;
547 // Unlock before exit_mode since it acquires the mutex
548 mutex_unlock(&g_search_state.mutex);
549 log_search_exit_mode(false); // Restore previous
550 break;
552 flow_control_debounce_trigger(&g_search_state.debounce, time_get_ns(), LOG_SEARCH_DEBOUNCE_NS);
553 mutex_unlock(&g_search_state.mutex);
554 atomic_store_bool(&g_search_state.needs_rerender, true);
555 break;
557 // No input available
558 mutex_unlock(&g_search_state.mutex);
559 break;
560 }
561
562 return ASCIICHAT_OK;
563}
564
565/* ============================================================================
566 * Log Filtering
567 * ========================================================================== */
568
571 return log_search_filter_buffer(logs ? logs : log_get_session_log_buffer(), out_entries, out_count);
572}
573
575 size_t *out_count) {
576 if (!out_entries || !out_count) {
577 return SET_ERRNO(ERROR_INVALID_PARAM, "out_entries and out_count must not be NULL");
578 }
579
580 // Get logs from in-memory buffer
581 session_log_entry_t *buffer_entries =
583 if (!buffer_entries) {
584 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate log buffer");
585 }
586
587 size_t buffer_count = session_log_buffer_get_recent(log_buf, buffer_entries, SESSION_LOG_BUFFER_SIZE);
588
589 // Check if filtering is active (either regex patterns or fixed string)
590 mutex_lock(&g_search_state.mutex);
591 bool has_regex_patterns = (g_search_state.active_pattern_count > 0);
592 bool has_fixed_string = (g_search_state.fixed_string && g_search_state.len > 0);
593 bool filtering_active = has_regex_patterns || has_fixed_string;
594
595 if (!filtering_active) {
596 // No filtering active - return all entries
597 mutex_unlock(&g_search_state.mutex);
598 *out_entries = buffer_entries;
599 *out_count = buffer_count;
600 return ASCIICHAT_OK;
601 }
602
603 // Filter entries with PCRE2
604 session_log_entry_t *filtered =
606 if (!filtered) {
607 mutex_unlock(&g_search_state.mutex);
608 SAFE_FREE(buffer_entries);
609 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate filtered buffer");
610 }
611
612 size_t filtered_count = 0;
613
614 // Create match data for PCRE2 matching
615 pcre2_match_data *match_data = NULL;
616 for (int p = 0; p < g_search_state.active_pattern_count; p++) {
617 if (g_search_state.active_patterns[p]) {
618 pcre2_code *code = g_search_state.active_patterns[p];
619 if (code) {
620 match_data = pcre2_match_data_create_from_pattern(code, NULL);
621 break;
622 }
623 }
624 }
625
626 if (!match_data && g_search_state.active_pattern_count > 0) {
627 // Couldn't create match data - fall back to returning all entries
628 mutex_unlock(&g_search_state.mutex);
629 SAFE_FREE(filtered);
630 *out_entries = buffer_entries;
631 *out_count = buffer_count;
632 return ASCIICHAT_OK;
633 }
634
635 // Parse pattern once before loop (for fixed string mode)
636 grep_parse_result_t parsed = {0};
637 if (has_fixed_string) {
638 parsed = grep_parse_pattern(g_search_state.input_buffer);
639 }
640
641 for (size_t i = 0; i < buffer_count; i++) {
642 const char *original_message = buffer_entries[i].message;
643 bool matches = false;
644
645 // Strip ANSI codes to match against plain text (consistent with highlighting)
646 char plain_message[SESSION_LOG_LINE_MAX] = {0};
647 const char *strip_ansi_codes_fn(const char *src, char *dst, size_t dst_size);
648 // Use inline stripping to avoid function call overhead
649 {
650 size_t pos = 0;
651 const char *src = original_message;
652 char *dst = plain_message;
653 size_t dst_size = sizeof(plain_message);
654
655 while (*src && pos < dst_size - 1) {
656 if (*src == '\x1b' && src[1] != '\0' && src[1] == '[') {
657 // CSI sequence - skip until final byte
658 src += 2;
659 while (*src && pos < dst_size - 1 && !(*src >= 0x40 && *src <= 0x7E)) {
660 src++;
661 }
662 if (*src)
663 src++; // Skip final byte
664 } else {
665 dst[pos++] = *src++;
666 }
667 }
668 dst[pos] = '\0';
669 }
670
671 size_t message_len = strlen(plain_message);
672
673 // Fixed string matching
674 if (has_fixed_string) {
675 const char *search_pattern = parsed.pattern;
676
677 if (g_search_state.case_insensitive) {
678 // Unicode-aware case-insensitive fixed string search
679 matches = (utf8_strcasestr(plain_message, search_pattern) != NULL);
680 } else {
681 // Case-sensitive fixed string search
682 matches = (strstr(plain_message, search_pattern) != NULL);
683 }
684 }
685 // Regex pattern matching
686 else if (has_regex_patterns) {
687 // Check against all active patterns (OR logic)
688 for (int p = 0; p < g_search_state.active_pattern_count; p++) {
689 pcre2_code *compiled = g_search_state.active_patterns[p];
690 if (!compiled) {
691 continue;
692 }
693
694 pcre2_code *code = compiled;
695 if (!code) {
696 continue;
697 }
698
699 // Match against plain message (without ANSI codes)
700 int rc = pcre2_match(code, (PCRE2_SPTR)plain_message, message_len, 0, 0, match_data, NULL);
701 if (rc >= 0) {
702 matches = true;
703 break;
704 }
705 }
706 }
707
708 // Apply invert logic
709 if (g_search_state.invert_match) {
710 matches = !matches;
711 }
712
713 if (matches) {
714 filtered[filtered_count++] = buffer_entries[i];
715 }
716 }
717
718 // Free match data
719 if (match_data) {
720 pcre2_match_data_free(match_data);
721 }
722
723 mutex_unlock(&g_search_state.mutex);
724
725 SAFE_FREE(buffer_entries);
726 *out_entries = filtered;
727 *out_count = filtered_count;
728 return ASCIICHAT_OK;
729}
730
731/* ============================================================================
732 * Display Rendering
733 * ========================================================================== */
734
736 (void)width; // Reserved for future use (line truncation)
737
738 mutex_lock(&g_search_state.mutex);
739
740 if (g_search_state.mode != LOG_SEARCH_MODE_ENTERING) {
741 mutex_unlock(&g_search_state.mutex);
742 return;
743 }
744
745 // Render the pattern and position terminal cursor at current edit position
746 char output_buf[LOG_SEARCH_OUTPUT_BUFFER_SIZE];
747
748 // Build output: "/" + pattern
749 int pattern_len =
750 snprintf(output_buf, sizeof(output_buf), "/%.*s", (int)g_search_state.len, g_search_state.input_buffer);
751
752 if (pattern_len > 0 && pattern_len < (int)sizeof(output_buf)) {
753 // Write the pattern to terminal
754 ui_controller_write(STDOUT_FILENO, output_buf, (size_t)pattern_len);
755
756 // After writing, cursor is at: column = 1 (for "/") + len + 1
757 // We want it at: column = 1 (for "/") + cursor_position + 1
758 // So we need to move back by: len - cursor_position columns
759 int cursor_offset = (int)g_search_state.len - (int)g_search_state.cursor;
760 if (cursor_offset > 0) {
761 // Move left by the difference
762 ui_controller_printf(STDOUT_FILENO, "\033[%dD", cursor_offset);
763 }
764 }
765
766 mutex_unlock(&g_search_state.mutex);
767}
768
769/* ============================================================================
770 * Display Highlighting
771 * ========================================================================== */
772
773bool log_search_get_match_info(const char *message, size_t *out_match_start, size_t *out_match_len) {
774 if (!message || !out_match_start || !out_match_len) {
775 return false;
776 }
777
778 *out_match_start = 0;
779 *out_match_len = 0;
780
781 // Use atomic read to avoid mutex contention with keyboard handler
782 int mode = atomic_load_int(&g_search_state.mode_atomic);
783 if (mode == LOG_SEARCH_MODE_INACTIVE) {
784 return false;
785 }
786
787 // Make a safe copy of state under mutex, then release before doing expensive work
788 mutex_lock(&g_search_state.mutex);
789
790 bool has_fixed_string = g_search_state.fixed_string && g_search_state.len > 0;
791 bool case_insensitive = g_search_state.case_insensitive;
792 char pattern_copy[LOG_SEARCH_INPUT_BUFFER_SIZE];
793 size_t pattern_len = g_search_state.len;
794 int pattern_count = g_search_state.active_pattern_count;
795 pcre2_code *patterns_copy[MAX_LOG_SEARCH_PATTERNS] = {0};
796
797 if (has_fixed_string) {
798 // Parse the input buffer to extract just the pattern part (without flags)
799 grep_parse_result_t parsed = grep_parse_pattern(g_search_state.input_buffer);
800 const char *src = parsed.valid ? parsed.pattern : g_search_state.input_buffer;
801 SAFE_STRNCPY(pattern_copy, src, sizeof(pattern_copy) - 1);
802 pattern_copy[sizeof(pattern_copy) - 1] = '\0';
803 pattern_len = strlen(pattern_copy); // Update pattern_len for the actual parsed pattern
804 }
805
806 for (int i = 0; i < pattern_count && i < MAX_LOG_SEARCH_PATTERNS; i++) {
807 if (g_search_state.active_patterns[i])
808 patterns_copy[i] = pcre2_code_copy(g_search_state.active_patterns[i]);
809 }
810
811 mutex_unlock(&g_search_state.mutex);
812
813 // Now do matching without holding mutex
814 size_t message_len = strlen(message);
815 bool matches = false;
816
817 // Fixed string matching
818 if (has_fixed_string) {
819 const char *found = NULL;
820
821 if (case_insensitive) {
822 found = utf8_strcasestr(message, pattern_copy);
823 } else {
824 found = strstr(message, pattern_copy);
825 }
826
827 if (found) {
828 matches = true;
829
830 // Convert byte position to character position (UTF-8 aware)
831 size_t byte_pos = (size_t)(found - message);
832 size_t char_pos = 0;
833 for (size_t i = 0; i < byte_pos && 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_pos++;
840 }
841
842 // Count characters in the match (pattern_len is in bytes for the pattern)
843 size_t match_char_len = 0;
844 for (size_t i = 0; i < pattern_len && found[i] != '\0';) {
845 uint32_t codepoint;
846 int utf8_len = utf8_decode((const uint8_t *)(found + i), &codepoint);
847 if (utf8_len <= 0)
848 utf8_len = 1;
849 i += utf8_len;
850 match_char_len++;
851 }
852
853 *out_match_start = char_pos;
854 *out_match_len = match_char_len;
855 }
856 }
857 // Regex pattern matching
858 else if (pattern_count > 0) {
859 pcre2_match_data *match_data = NULL;
860 for (int p = 0; p < pattern_count; p++) {
861 if (patterns_copy[p]) {
862 pcre2_code *code = patterns_copy[p];
863 if (code) {
864 match_data = pcre2_match_data_create_from_pattern(code, NULL);
865 break;
866 }
867 }
868 }
869
870 if (match_data) {
871 for (int p = 0; p < pattern_count; p++) {
872 pcre2_code *compiled = patterns_copy[p];
873 if (!compiled) {
874 continue;
875 }
876
877 pcre2_code *code = compiled;
878 if (!code) {
879 continue;
880 }
881
882 int rc = pcre2_match(code, (PCRE2_SPTR)message, message_len, 0, 0, match_data, NULL);
883 if (rc >= 0) {
884 matches = true;
885 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
886
887 // Convert byte positions to character positions (UTF-8 aware)
888 size_t byte_start = (size_t)ovector[0];
889 size_t byte_end = (size_t)ovector[1];
890
891 // Count characters up to byte_start
892 size_t char_start = 0;
893 for (size_t i = 0; i < byte_start && message[i] != '\0';) {
894 uint32_t codepoint;
895 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
896 if (utf8_len <= 0)
897 utf8_len = 1;
898 i += utf8_len;
899 char_start++;
900 }
901
902 // Count characters up to byte_end
903 size_t char_end = char_start;
904 for (size_t i = byte_start; i < byte_end && message[i] != '\0';) {
905 uint32_t codepoint;
906 int utf8_len = utf8_decode((const uint8_t *)(message + i), &codepoint);
907 if (utf8_len <= 0)
908 utf8_len = 1;
909 i += utf8_len;
910 char_end++;
911 }
912
913 *out_match_start = char_start;
914 *out_match_len = char_end - char_start;
915 break;
916 }
917 }
918 pcre2_match_data_free(match_data);
919 }
920 }
921
922 for (int i = 0; i < pattern_count && i < MAX_LOG_SEARCH_PATTERNS; i++) {
923 if (patterns_copy[i])
924 pcre2_code_free(patterns_copy[i]);
925 }
926 return matches;
927}
928
929/* ============================================================================
930 * Re-render Notification
931 * ========================================================================== */
932
934 bool needs = atomic_load_bool(&g_search_state.needs_rerender);
935 if (needs) {
936 atomic_store_bool(&g_search_state.needs_rerender, false);
937 }
938 return needs;
939}
940
941/* ============================================================================
942 * Query Functions
943 * ========================================================================== */
944
946 // Use atomic read to avoid mutex contention with keyboard handler
947 int mode = atomic_load_int(&g_search_state.mode_atomic);
948 if (mode == LOG_SEARCH_MODE_INACTIVE) {
949 return false;
950 }
951
952 // Safe read under mutex (quick operation)
953 mutex_lock(&g_search_state.mutex);
954 bool result = g_search_state.global_highlight;
955 mutex_unlock(&g_search_state.mutex);
956
957 return result;
958}
959
961 // Use atomic read to check if active
962 int mode = atomic_load_int(&g_search_state.mode_atomic);
963 if (mode == LOG_SEARCH_MODE_INACTIVE) {
964 return NULL;
965 }
966
967 // Safe read under mutex
968 mutex_lock(&g_search_state.mutex);
969
970 // Return the compiled pattern if we have one (not fixed string)
971 void *result = NULL;
972 if (g_search_state.active_pattern_count > 0 && !g_search_state.fixed_string) {
973 result = (void *)pcre2_code_copy(g_search_state.active_patterns[0]);
974 }
975
976 mutex_unlock(&g_search_state.mutex);
977
978 return result;
979}
980
981/* ============================================================================
982 * Internal Access for Atomic Rendering
983 * ========================================================================== */
984
986 return (void *)&g_search_state.mutex;
987}
988
990 return (int)g_search_state.len;
991}
992
994 return (int)g_search_state.cursor;
995}
996
998 return g_search_state.input_buffer;
999}
1000
1002 return g_search_state.case_insensitive;
1003}
ANSI escape sequence utilities and fast color code generation.
#define SET_ERRNO(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
unsigned int width
Definition fft.c:23
void flow_control_debounce_cancel(flow_control_debounce_t *debounce)
void flow_control_debounce_trigger(flow_control_debounce_t *debounce, uint64_t now_ns, uint64_t delay_ns)
Definition flow_control.c:3
bool flow_control_debounce_take_ready(flow_control_debounce_t *debounce, uint64_t now_ns)
asciichat_error_t grep_restore_patterns(void)
Restore previously saved filter patterns.
Definition grep.c:1108
asciichat_error_t grep_save_patterns(void)
Save current filter patterns for later restoration.
Definition grep.c:1093
int pattern_count
Number of active patterns.
Definition grep.c:60
const char * grep_get_last_pattern(void)
Get the last (most recent) filter pattern string.
Definition grep.c:1135
grep_parse_result_t grep_parse_pattern(const char *input)
Parse pattern in /pattern/flags or pattern/flags format.
Definition grep.c:265
unsigned int uint32_t
Definition common.h:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
unsigned char uint8_t
Definition common.h:60
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_THREAD
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
struct session_log_buffer session_log_buffer_t
Thread-safe fixed-size history ring for session-screen log lines.
session_log_buffer_t * log_get_session_log_buffer(void)
Get the currently registered session log buffer.
Definition log.c:2071
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
keyboard_line_edit_result_t keyboard_read_line_interactive(keyboard_line_edit_opts_t *opts)
Process one keystroke for interactive line editing.
keyboard_key_t
Options for interactive line editing.
Definition keyboard.h:53
keyboard_line_edit_result_t
Result codes for interactive line editing.
Definition keyboard.h:223
@ LINE_EDIT_ACCEPTED
User pressed Enter (accept input).
Definition keyboard.h:225
@ LINE_EDIT_CANCELLED
User pressed Escape/Ctrl+C (cancel input).
Definition keyboard.h:226
@ LINE_EDIT_CONTINUE
Keep editing (more input needed).
Definition keyboard.h:224
@ LINE_EDIT_NO_INPUT
No key available (non-blocking mode).
Definition keyboard.h:227
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
Decode a UTF-8 sequence to a Unicode codepoint.
Definition utf8.c:27
const char * utf8_strcasestr(const char *haystack, const char *needle)
Case-insensitive substring search with full Unicode support.
Definition utf8.c:205
📝 Logging API with multiple log levels and terminal output control
session_log_buffer_t * log_get_history_buffer(void)
Definition log.c:565
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
void ascii_otel_ui_usage(const char *screen, const char *event, uint64_t duration_ns)
Definition otel.c:434
pcre2_code * asciichat_pcre2_compile(const char *pattern, uint32_t flags)
Definition pcre2.c:16
PCRE2 compilation and match helpers.
asciichat_error_t log_search_init(void)
Initialize interactive grep subsystem.
Definition search.c:144
void * log_search_get_mutex(void)
Get the mutex protecting grep state.
Definition search.c:985
bool log_search_needs_rerender(void)
Check if screen needs immediate re-render.
Definition search.c:933
bool log_search_check_signal_cancel(void)
Check and clear the signal-cancel flag.
Definition search.c:403
void log_search_signal_cancel(void)
Cancel grep mode from a signal handler (async-signal-safe).
Definition search.c:399
bool log_search_is_entering(void)
Check if currently in input mode (typing pattern).
Definition search.c:408
void log_search_process_pending(void)
Definition search.c:469
#define MAX_LOG_SEARCH_PATTERNS
Maximum number of patterns to support.
Definition search.c:41
bool log_search_get_global_highlight(void)
Check if global highlighting (/g flag) is enabled in current pattern.
Definition search.c:945
int log_search_get_input_len(void)
Get current input buffer length (must hold mutex).
Definition search.c:989
void log_search_enter_mode(void)
Enter search mode (user pressed '/').
Definition search.c:226
asciichat_error_t log_search_insert_text(const char *text)
Definition search.c:480
asciichat_error_t log_search_filter_buffer(session_log_buffer_t *log_buf, session_log_entry_t **out_entries, size_t *out_count)
Definition search.c:574
const char * log_search_get_input_buffer(void)
Get current input buffer content (must hold mutex).
Definition search.c:997
bool log_search_is_active(void)
Check if filtering is active.
Definition search.c:413
bool log_search_get_case_insensitive(void)
Check if case-insensitive flag is set (must hold mutex).
Definition search.c:1001
void log_search_exit_mode(bool accept)
Exit search mode.
Definition search.c:303
void * log_search_copy_pattern(void)
Get the compiled regex pattern for interactive grep (internal use).
Definition search.c:960
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:773
bool log_search_should_handle(int key)
Check if a key should be handled by grep module.
Definition search.c:422
#define LOG_SEARCH_OUTPUT_BUFFER_SIZE
Definition search.c:3
#define LOG_SEARCH_DEBOUNCE_NS
Definition search.c:43
void log_search_destroy(void)
Clean up interactive grep subsystem.
Definition search.c:203
int log_search_get_cursor_position(void)
Get current cursor position in input buffer (must hold mutex).
Definition search.c:993
asciichat_error_t log_search_handle_key(keyboard_key_t key)
Process keyboard input for grep.
Definition search.c:508
void log_search_render_input_line(int width)
Render grep input line at bottom of screen.
Definition search.c:735
bool log_search_is_entering_atomic(void)
Check if grep is in entering mode (async-signal-safe).
Definition search.c:395
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:569
#define LOG_SEARCH_INPUT_BUFFER_SIZE
Input buffer size.
Definition search.c:42
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
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.
Thread-safe circular log buffer for session screens (status, splash).
#define SESSION_LOG_BUFFER_SIZE
#define SESSION_LOG_LINE_MAX
🔤 String Manipulation and Shell Escaping Utilities
Parsed pattern result from /pattern/flags syntax.
Definition grep.h:22
int context_before
Lines before match (B<n> flag).
Definition grep.h:29
bool global_flag
Highlight all matches (g flag).
Definition grep.h:28
bool is_fixed_string
True if fixed string (not regex).
Definition grep.h:25
bool invert
Invert match (I flag).
Definition grep.h:27
char pattern[4096]
Parsed pattern string.
Definition grep.h:23
bool case_insensitive
Case-insensitive (i flag).
Definition grep.h:26
bool valid
True if parsing succeeded.
Definition grep.h:31
int context_after
Lines after match (A<n> flag).
Definition grep.h:30
uint32_t pcre2_options
PCRE2 compile options.
Definition grep.h:24
Options for interactive line editing.
Definition keyboard.h:203
char previous_patterns[32][256]
Definition search.c:56
atomic_t mode_atomic
Shadow of mode for signal-safe reads.
Definition search.c:75
bool cli_pattern_auto_populated
Track if CLI pattern was already populated.
Definition search.c:76
int previous_pattern_count
Definition search.c:57
int active_pattern_count
Definition search.c:61
flow_control_debounce_t debounce
Definition search.c:77
log_search_mode_t mode
Definition search.c:50
atomic_t needs_rerender
Definition search.c:73
atomic_t signal_cancelled
Set by signal handler, checked by render loop.
Definition search.c:74
pcre2_code * active_patterns[32]
Definition search.c:60
bool global_highlight
Definition search.c:66
bool case_insensitive
Definition search.c:64
mutex_t mutex
Definition search.c:71
char input_buffer[256]
Definition search.c:51
lifecycle_t lifecycle
Definition search.c:72
One formatted session log line and its monotonic history sequence.
char message[1024]
Formatted log message (may include ANSI colors).
⏱️ High-precision timing utilities using sokol_time.h and uthash
asciichat_error_t ui_controller_write(int fd, const char *data, size_t len)
asciichat_error_t ui_controller_printf(int fd, const char *format,...)
asciichat_error_t ui_input_insert(char *buffer, size_t capacity, size_t *length, size_t *cursor, const char *text)
Definition ui/input.c:130
🔤 UTF-8 Encoding and Decoding Utilities