ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
interactive_grep.c File Reference

Go to the source code of this file.

Data Structures

struct  interactive_grep_state_t
 

Macros

#define MAX_GREP_PATTERNS   32
 Maximum number of patterns to support.
 
#define GREP_INPUT_BUFFER_SIZE   256
 Input buffer size.
 

Functions

asciichat_error_t interactive_grep_init (void)
 
void interactive_grep_destroy (void)
 
void interactive_grep_enter_mode (void)
 
void interactive_grep_exit_mode (bool accept)
 
bool interactive_grep_is_entering_atomic (void)
 
void interactive_grep_signal_cancel (void)
 
bool interactive_grep_check_signal_cancel (void)
 
bool interactive_grep_is_entering (void)
 
bool interactive_grep_is_active (void)
 
bool interactive_grep_should_handle (int key)
 
asciichat_error_t interactive_grep_handle_key (keyboard_key_t key)
 
asciichat_error_t interactive_grep_gather_and_filter_logs (session_log_entry_t **out_entries, size_t *out_count)
 
void interactive_grep_render_input_line (int width)
 
bool interactive_grep_get_match_info (const char *message, size_t *out_match_start, size_t *out_match_len)
 
bool interactive_grep_needs_rerender (void)
 
bool interactive_grep_get_global_highlight (void)
 
void * interactive_grep_get_pattern_singleton (void)
 
void * interactive_grep_get_mutex (void)
 
int interactive_grep_get_input_len (void)
 
const char * interactive_grep_get_input_buffer (void)
 
bool interactive_grep_get_case_insensitive (void)
 

Macro Definition Documentation

◆ GREP_INPUT_BUFFER_SIZE

#define GREP_INPUT_BUFFER_SIZE   256

Input buffer size.

Definition at line 31 of file interactive_grep.c.

◆ MAX_GREP_PATTERNS

#define MAX_GREP_PATTERNS   32

Maximum number of patterns to support.

Definition at line 30 of file interactive_grep.c.

Function Documentation

◆ interactive_grep_check_signal_cancel()

bool interactive_grep_check_signal_cancel ( void  )

Definition at line 381 of file interactive_grep.c.

381 {
382 return atomic_exchange(&g_grep_state.signal_cancelled, false);
383}
_Atomic bool signal_cancelled
Set by signal handler, checked by render loop.

References interactive_grep_state_t::signal_cancelled.

◆ interactive_grep_destroy()

void interactive_grep_destroy ( void  )

Definition at line 193 of file interactive_grep.c.

193 {
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}
pcre2_singleton_t * active_patterns[32]

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, interactive_grep_state_t::initialized, and interactive_grep_state_t::mutex.

◆ interactive_grep_enter_mode()

void interactive_grep_enter_mode ( void  )

Definition at line 218 of file interactive_grep.c.

218 {
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}
asciichat_error_t grep_save_patterns(void)
Definition grep.c:1278
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
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Definition pcre2.c:95
bool 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

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, asciichat_pcre2_singleton_get_code(), interactive_grep_state_t::case_insensitive, interactive_grep_state_t::cli_pattern_auto_populated, interactive_grep_state_t::context_after, interactive_grep_state_t::context_before, interactive_grep_state_t::cursor, interactive_grep_state_t::fixed_string, interactive_grep_state_t::global_highlight, grep_get_last_pattern(), grep_parse_pattern(), grep_save_patterns(), interactive_grep_state_t::input_buffer, interactive_grep_state_t::invert_match, interactive_grep_state_t::len, interactive_grep_state_t::mode, interactive_grep_state_t::mode_atomic, interactive_grep_state_t::mutex, and interactive_grep_state_t::needs_rerender.

Referenced by interactive_grep_handle_key(), server_status_display(), and server_status_display_interactive().

◆ interactive_grep_exit_mode()

void interactive_grep_exit_mode ( bool  accept)

Definition at line 288 of file interactive_grep.c.

288 {
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}
asciichat_error_t grep_restore_patterns(void)
Definition grep.c:1328

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, asciichat_pcre2_singleton_get_code(), interactive_grep_state_t::case_insensitive, interactive_grep_state_t::cli_pattern_auto_populated, interactive_grep_state_t::context_after, interactive_grep_state_t::context_before, interactive_grep_state_t::fixed_string, interactive_grep_state_t::global_highlight, grep_parse_pattern(), grep_restore_patterns(), interactive_grep_state_t::input_buffer, interactive_grep_state_t::invert_match, interactive_grep_state_t::mode, interactive_grep_state_t::mode_atomic, interactive_grep_state_t::mutex, and interactive_grep_state_t::needs_rerender.

Referenced by interactive_grep_handle_key(), and server_status_display_interactive().

◆ interactive_grep_gather_and_filter_logs()

asciichat_error_t interactive_grep_gather_and_filter_logs ( session_log_entry_t **  out_entries,
size_t *  out_count 
)

Definition at line 530 of file interactive_grep.c.

530 {
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}
size_t session_log_buffer_get_recent(session_log_entry_t *out_entries, size_t max_count)
const char * utf8_strcasestr(const char *haystack, const char *needle)
Case-insensitive substring search with full Unicode support.
Definition utf8.c:274

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, asciichat_pcre2_singleton_get_code(), interactive_grep_state_t::case_insensitive, interactive_grep_state_t::fixed_string, grep_parse_pattern(), interactive_grep_state_t::input_buffer, interactive_grep_state_t::invert_match, interactive_grep_state_t::len, interactive_grep_state_t::mutex, session_log_buffer_get_recent(), and utf8_strcasestr().

Referenced by terminal_screen_render().

◆ interactive_grep_get_case_insensitive()

bool interactive_grep_get_case_insensitive ( void  )

Definition at line 933 of file interactive_grep.c.

933 {
934 return g_grep_state.case_insensitive;
935}

References interactive_grep_state_t::case_insensitive.

Referenced by grep_highlight_colored().

◆ interactive_grep_get_global_highlight()

bool interactive_grep_get_global_highlight ( void  )

Definition at line 881 of file interactive_grep.c.

881 {
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}

References interactive_grep_state_t::global_highlight, interactive_grep_state_t::mode_atomic, and interactive_grep_state_t::mutex.

Referenced by grep_highlight_colored().

◆ interactive_grep_get_input_buffer()

const char * interactive_grep_get_input_buffer ( void  )

Definition at line 929 of file interactive_grep.c.

929 {
930 return g_grep_state.input_buffer;
931}

References interactive_grep_state_t::input_buffer.

Referenced by grep_highlight_colored(), and terminal_screen_render().

◆ interactive_grep_get_input_len()

int interactive_grep_get_input_len ( void  )

Definition at line 925 of file interactive_grep.c.

925 {
926 return (int)g_grep_state.len;
927}

References interactive_grep_state_t::len.

Referenced by grep_highlight_colored(), and terminal_screen_render().

◆ interactive_grep_get_match_info()

bool interactive_grep_get_match_info ( const char *  message,
size_t *  out_match_start,
size_t *  out_match_len 
)

Definition at line 714 of file interactive_grep.c.

714 {
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}
int pattern_count
Number of active patterns.
Definition grep.c:76
#define GREP_INPUT_BUFFER_SIZE
Input buffer size.
#define MAX_GREP_PATTERNS
Maximum number of patterns to support.
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
Definition utf8.c:18

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, asciichat_pcre2_singleton_get_code(), interactive_grep_state_t::case_insensitive, interactive_grep_state_t::fixed_string, GREP_INPUT_BUFFER_SIZE, grep_parse_pattern(), interactive_grep_state_t::input_buffer, interactive_grep_state_t::len, MAX_GREP_PATTERNS, interactive_grep_state_t::mode_atomic, interactive_grep_state_t::mutex, pattern_count, utf8_decode(), and utf8_strcasestr().

Referenced by terminal_screen_render().

◆ interactive_grep_get_mutex()

void * interactive_grep_get_mutex ( void  )

Definition at line 921 of file interactive_grep.c.

921 {
922 return (void *)&g_grep_state.mutex;
923}

References interactive_grep_state_t::mutex.

Referenced by terminal_screen_render().

◆ interactive_grep_get_pattern_singleton()

void * interactive_grep_get_pattern_singleton ( void  )

Definition at line 896 of file interactive_grep.c.

896 {
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}

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, interactive_grep_state_t::fixed_string, interactive_grep_state_t::mode_atomic, and interactive_grep_state_t::mutex.

Referenced by grep_highlight_colored().

◆ interactive_grep_handle_key()

asciichat_error_t interactive_grep_handle_key ( keyboard_key_t  key)

Definition at line 414 of file interactive_grep.c.

414 {
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}
void interactive_grep_exit_mode(bool accept)
void interactive_grep_enter_mode(void)

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, asciichat_pcre2_singleton_get_code(), interactive_grep_state_t::case_insensitive, interactive_grep_state_t::context_after, interactive_grep_state_t::context_before, interactive_grep_state_t::cursor, interactive_grep_state_t::fixed_string, interactive_grep_state_t::global_highlight, grep_parse_pattern(), interactive_grep_state_t::input_buffer, interactive_grep_enter_mode(), interactive_grep_exit_mode(), interactive_grep_state_t::invert_match, interactive_grep_state_t::len, interactive_grep_state_t::mode, interactive_grep_state_t::mutex, and interactive_grep_state_t::needs_rerender.

Referenced by server_status_display_interactive(), and session_render_loop().

◆ interactive_grep_init()

asciichat_error_t interactive_grep_init ( void  )

Definition at line 126 of file interactive_grep.c.

126 {
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}
int mutex_init(mutex_t *mutex)
Definition threading.c:16

References interactive_grep_state_t::active_pattern_count, interactive_grep_state_t::active_patterns, interactive_grep_state_t::case_insensitive, interactive_grep_state_t::context_after, interactive_grep_state_t::context_before, interactive_grep_state_t::cursor, interactive_grep_state_t::fixed_string, interactive_grep_state_t::global_highlight, grep_get_last_pattern(), GREP_INPUT_BUFFER_SIZE, grep_parse_pattern(), interactive_grep_state_t::initialized, interactive_grep_state_t::input_buffer, interactive_grep_state_t::invert_match, interactive_grep_state_t::len, interactive_grep_state_t::mode, interactive_grep_state_t::mode_atomic, interactive_grep_state_t::mutex, mutex_init(), and interactive_grep_state_t::needs_rerender.

Referenced by server_main().

◆ interactive_grep_is_active()

bool interactive_grep_is_active ( void  )

Definition at line 390 of file interactive_grep.c.

390 {
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}

References interactive_grep_state_t::mode_atomic.

Referenced by grep_highlight_colored(), server_status_display_interactive(), and terminal_screen_render().

◆ interactive_grep_is_entering()

bool interactive_grep_is_entering ( void  )

Definition at line 385 of file interactive_grep.c.

385 {
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}

References interactive_grep_state_t::mode_atomic.

Referenced by terminal_screen_render().

◆ interactive_grep_is_entering_atomic()

bool interactive_grep_is_entering_atomic ( void  )

Definition at line 373 of file interactive_grep.c.

373 {
374 return atomic_load(&g_grep_state.mode_atomic) == GREP_MODE_ENTERING;
375}

References interactive_grep_state_t::mode_atomic.

◆ interactive_grep_needs_rerender()

bool interactive_grep_needs_rerender ( void  )

Definition at line 869 of file interactive_grep.c.

869 {
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}

References interactive_grep_state_t::needs_rerender.

Referenced by terminal_screen_render().

◆ interactive_grep_render_input_line()

void interactive_grep_render_input_line ( int  width)

Definition at line 690 of file interactive_grep.c.

690 {
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}
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

References interactive_grep_state_t::input_buffer, interactive_grep_state_t::len, interactive_grep_state_t::mode, interactive_grep_state_t::mutex, and platform_write_all().

Referenced by terminal_screen_render().

◆ interactive_grep_should_handle()

bool interactive_grep_should_handle ( int  key)

Definition at line 399 of file interactive_grep.c.

399 {
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}

References interactive_grep_state_t::mode, and interactive_grep_state_t::mutex.

Referenced by server_status_display_interactive(), and session_render_loop().

◆ interactive_grep_signal_cancel()

void interactive_grep_signal_cancel ( void  )

Definition at line 377 of file interactive_grep.c.

377 {
378 atomic_store(&g_grep_state.signal_cancelled, true);
379}

References interactive_grep_state_t::signal_cancelled.