9#include "ascii-chat/log/interactive_grep.h"
10#include "ascii-chat/common.h"
11#include "ascii-chat/log/logging.h"
12#include "ascii-chat/log/grep.h"
13#include "ascii-chat/platform/keyboard.h"
14#include "ascii-chat/platform/mutex.h"
15#include "ascii-chat/util/pcre2.h"
16#include "ascii-chat/util/utf8.h"
17#include "ascii-chat/session/session_log_buffer.h"
18#include "ascii-chat/options/options.h"
19#include "ascii-chat/video/ansi.h"
30#define MAX_GREP_PATTERNS 32
31#define GREP_INPUT_BUFFER_SIZE 256
68 .
mode = GREP_MODE_INACTIVE,
71 .previous_pattern_count = 0,
72 .active_pattern_count = 0,
73 .case_insensitive =
false,
74 .fixed_string =
false,
75 .global_highlight =
false,
76 .invert_match =
false,
79 .needs_rerender =
false,
80 .signal_cancelled =
false,
81 .mode_atomic = GREP_MODE_INACTIVE,
83 .cli_pattern_auto_populated =
false,
95static bool validate_pcre2_pattern(
const char *input) {
96 if (!input || strlen(input) == 0) {
108 if (parsed.is_fixed_string) {
113 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
128 static bool mutex_inited =
false;
134 mutex_lock(&g_grep_state.
mutex);
137 mutex_unlock(&g_grep_state.
mutex);
143 mutex_t saved_mutex = g_grep_state.
mutex;
144 memset(&g_grep_state, 0,
sizeof(g_grep_state));
146 g_grep_state.
mutex = saved_mutex;
154 if (!cli_pattern || cli_pattern[0] ==
'\0') {
156 g_grep_state.
mode = GREP_MODE_INACTIVE;
157 atomic_store(&g_grep_state.
mode_atomic, GREP_MODE_INACTIVE);
159 mutex_unlock(&g_grep_state.
mutex);
165 g_grep_state.
len = strlen(cli_pattern);
167 g_grep_state.
mode = GREP_MODE_ACTIVE;
168 atomic_store(&g_grep_state.
mode_atomic, GREP_MODE_ACTIVE);
173 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
189 mutex_unlock(&g_grep_state.
mutex);
194 mutex_lock(&g_grep_state.
mutex);
197 mutex_unlock(&g_grep_state.
mutex);
211 mutex_unlock(&g_grep_state.
mutex);
219 mutex_lock(&g_grep_state.
mutex);
223 if (result != ASCIICHAT_OK) {
224 log_warn(
"Failed to save filter patterns");
229 g_grep_state.
len = 0;
235 if (cli_pattern && cli_pattern[0] !=
'\0') {
237 const char *pattern_to_use = cli_pattern;
238 if (cli_pattern[0] ==
'/') {
239 pattern_to_use = cli_pattern + 1;
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;
261 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
262 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
281 g_grep_state.
mode = GREP_MODE_ENTERING;
282 atomic_store(&g_grep_state.
mode_atomic, GREP_MODE_ENTERING);
285 mutex_unlock(&g_grep_state.
mutex);
289 mutex_lock(&g_grep_state.
mutex);
291 if (g_grep_state.
mode != GREP_MODE_ENTERING) {
292 mutex_unlock(&g_grep_state.
mutex);
299 if (result != ASCIICHAT_OK) {
300 log_warn(
"Failed to restore filter patterns");
312 g_grep_state.
mode = GREP_MODE_INACTIVE;
313 atomic_store(&g_grep_state.
mode_atomic, GREP_MODE_INACTIVE);
315 mutex_unlock(&g_grep_state.
mutex);
323 log_error(
"Invalid pattern format");
324 mutex_unlock(&g_grep_state.
mutex);
343 if (strlen(parsed.pattern) > 0 && !parsed.is_fixed_string) {
344 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
362 g_grep_state.
mode = GREP_MODE_ACTIVE;
363 atomic_store(&g_grep_state.
mode_atomic, GREP_MODE_ACTIVE);
366 mutex_unlock(&g_grep_state.
mutex);
374 return atomic_load(&g_grep_state.
mode_atomic) == GREP_MODE_ENTERING;
387 return atomic_load(&g_grep_state.
mode_atomic) == GREP_MODE_ENTERING;
392 return atomic_load(&g_grep_state.
mode_atomic) != GREP_MODE_INACTIVE;
400 mutex_lock(&g_grep_state.
mutex);
403 if (g_grep_state.
mode == GREP_MODE_ENTERING) {
404 mutex_unlock(&g_grep_state.
mutex);
409 bool should_handle = (key ==
'/');
410 mutex_unlock(&g_grep_state.
mutex);
411 return should_handle;
415 mutex_lock(&g_grep_state.
mutex);
418 if (g_grep_state.
mode != GREP_MODE_ENTERING) {
419 mutex_unlock(&g_grep_state.
mutex);
432 keyboard_line_edit_opts_t opts = {
435 .len = &g_grep_state.
len,
436 .cursor = &g_grep_state.
cursor,
440 .validator = validate_pcre2_pattern,
444 keyboard_line_edit_result_t result = keyboard_read_line_interactive(&opts);
447 case LINE_EDIT_ACCEPTED:
449 mutex_unlock(&g_grep_state.
mutex);
452 case LINE_EDIT_CANCELLED:
454 mutex_unlock(&g_grep_state.
mutex);
457 case LINE_EDIT_CONTINUE:
462 if (g_grep_state.
len == 0) {
464 mutex_unlock(&g_grep_state.
mutex);
474 mutex_unlock(&g_grep_state.
mutex);
480 if (parsed.is_fixed_string) {
487 pcre2_singleton_t *singleton = asciichat_pcre2_singleton_compile(parsed.pattern, parsed.pcre2_options);
514 mutex_unlock(&g_grep_state.
mutex);
517 case LINE_EDIT_NO_INPUT:
519 mutex_unlock(&g_grep_state.
mutex);
531 if (!out_entries || !out_count) {
532 return SET_ERRNO(ERROR_INVALID_PARAM,
"out_entries and out_count must not be NULL");
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");
545 mutex_lock(&g_grep_state.
mutex);
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;
550 if (!filtering_active) {
552 mutex_unlock(&g_grep_state.
mutex);
553 *out_entries = buffer_entries;
554 *out_count = buffer_count;
559 session_log_entry_t *filtered =
560 SAFE_MALLOC(SESSION_LOG_BUFFER_SIZE *
sizeof(session_log_entry_t), session_log_entry_t *);
562 mutex_unlock(&g_grep_state.
mutex);
563 SAFE_FREE(buffer_entries);
564 return SET_ERRNO(ERROR_MEMORY,
"Failed to allocate filtered buffer");
567 size_t filtered_count = 0;
570 pcre2_match_data *match_data = NULL;
575 match_data = pcre2_match_data_create_from_pattern(code, NULL);
583 mutex_unlock(&g_grep_state.
mutex);
585 *out_entries = buffer_entries;
586 *out_count = buffer_count;
591 grep_parse_result_t parsed = {0};
592 if (has_fixed_string) {
596 for (
size_t i = 0; i < buffer_count; i++) {
597 const char *original_message = buffer_entries[i].message;
598 bool matches =
false;
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);
606 const char *src = original_message;
607 char *dst = plain_message;
608 size_t dst_size =
sizeof(plain_message);
610 while (*src && pos < dst_size - 1) {
611 if (*src ==
'\x1b' && src[1] !=
'\0' && src[1] ==
'[') {
614 while (*src && pos < dst_size - 1 && !(*src >= 0x40 && *src <= 0x7E)) {
626 size_t message_len = strlen(plain_message);
629 if (has_fixed_string) {
630 const char *search_pattern = parsed.pattern;
637 matches = (strstr(plain_message, search_pattern) != NULL);
641 else if (has_regex_patterns) {
655 int rc = pcre2_jit_match(code, (PCRE2_SPTR)plain_message, message_len, 0, 0, match_data, NULL);
669 filtered[filtered_count++] = buffer_entries[i];
675 pcre2_match_data_free(match_data);
678 mutex_unlock(&g_grep_state.
mutex);
680 SAFE_FREE(buffer_entries);
681 *out_entries = filtered;
682 *out_count = filtered_count;
693 mutex_lock(&g_grep_state.
mutex);
695 if (g_grep_state.
mode != GREP_MODE_ENTERING) {
696 mutex_unlock(&g_grep_state.
mutex);
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);
707 mutex_unlock(&g_grep_state.
mutex);
715 if (!message || !out_match_start || !out_match_len) {
719 *out_match_start = 0;
724 if (mode == GREP_MODE_INACTIVE) {
729 mutex_lock(&g_grep_state.
mutex);
731 bool has_fixed_string = g_grep_state.
fixed_string && g_grep_state.
len > 0;
734 size_t pattern_len = g_grep_state.
len;
738 if (has_fixed_string) {
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);
751 mutex_unlock(&g_grep_state.
mutex);
754 size_t message_len = strlen(message);
755 bool matches =
false;
758 if (has_fixed_string) {
759 const char *found = NULL;
761 if (case_insensitive) {
764 found = strstr(message, pattern_copy);
771 size_t byte_pos = (size_t)(found - message);
773 for (
size_t i = 0; i < byte_pos && message[i] !=
'\0';) {
775 int utf8_len =
utf8_decode((
const uint8_t *)(message + i), &codepoint);
783 size_t match_char_len = 0;
784 for (
size_t i = 0; i < pattern_len && found[i] !=
'\0';) {
786 int utf8_len =
utf8_decode((
const uint8_t *)(found + i), &codepoint);
793 *out_match_start = char_pos;
794 *out_match_len = match_char_len;
799 pcre2_match_data *match_data = NULL;
801 if (patterns_copy[p]) {
804 match_data = pcre2_match_data_create_from_pattern(code, NULL);
822 int rc = pcre2_jit_match(code, (PCRE2_SPTR)message, message_len, 0, 0, match_data, NULL);
825 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
828 size_t byte_start = (size_t)ovector[0];
829 size_t byte_end = (size_t)ovector[1];
832 size_t char_start = 0;
833 for (
size_t i = 0; i < byte_start && message[i] !=
'\0';) {
835 int utf8_len =
utf8_decode((
const uint8_t *)(message + i), &codepoint);
843 size_t char_end = char_start;
844 for (
size_t i = byte_start; i < byte_end && message[i] !=
'\0';) {
846 int utf8_len =
utf8_decode((
const uint8_t *)(message + i), &codepoint);
853 *out_match_start = char_start;
854 *out_match_len = char_end - char_start;
858 pcre2_match_data_free(match_data);
884 if (mode == GREP_MODE_INACTIVE) {
889 mutex_lock(&g_grep_state.
mutex);
891 mutex_unlock(&g_grep_state.
mutex);
899 if (mode == GREP_MODE_INACTIVE) {
904 mutex_lock(&g_grep_state.
mutex);
912 mutex_unlock(&g_grep_state.
mutex);
922 return (
void *)&g_grep_state.
mutex;
926 return (
int)g_grep_state.
len;
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.
asciichat_error_t grep_restore_patterns(void)
asciichat_error_t grep_save_patterns(void)
int pattern_count
Number of active patterns.
const char * grep_get_last_pattern(void)
grep_parse_result_t grep_parse_pattern(const char *input)
Parse pattern in /pattern/flags or pattern/flags format.
void interactive_grep_exit_mode(bool accept)
bool interactive_grep_is_entering(void)
bool interactive_grep_check_signal_cancel(void)
bool interactive_grep_is_entering_atomic(void)
bool interactive_grep_get_case_insensitive(void)
#define GREP_INPUT_BUFFER_SIZE
Input buffer size.
const char * interactive_grep_get_input_buffer(void)
void interactive_grep_signal_cancel(void)
void interactive_grep_enter_mode(void)
asciichat_error_t interactive_grep_gather_and_filter_logs(session_log_entry_t **out_entries, size_t *out_count)
bool interactive_grep_is_active(void)
asciichat_error_t interactive_grep_init(void)
#define MAX_GREP_PATTERNS
Maximum number of patterns to support.
bool interactive_grep_needs_rerender(void)
bool interactive_grep_get_global_highlight(void)
void * interactive_grep_get_mutex(void)
asciichat_error_t interactive_grep_handle_key(keyboard_key_t key)
void interactive_grep_render_input_line(int width)
int interactive_grep_get_input_len(void)
bool interactive_grep_get_match_info(const char *message, size_t *out_match_start, size_t *out_match_len)
bool interactive_grep_should_handle(int key)
void interactive_grep_destroy(void)
void * interactive_grep_get_pattern_singleton(void)
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
size_t session_log_buffer_get_recent(session_log_entry_t *out_entries, size_t max_count)
_Atomic bool needs_rerender
_Atomic bool signal_cancelled
Set by signal handler, checked by render loop.
int previous_pattern_count
pcre2_singleton_t * active_patterns[32]
bool cli_pattern_auto_populated
Track if CLI pattern was already populated.
_Atomic int mode_atomic
Shadow of mode for signal-safe reads.
Represents a thread-safe compiled PCRE2 regex singleton.
int mutex_init(mutex_t *mutex)
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
const char * utf8_strcasestr(const char *haystack, const char *needle)
Case-insensitive substring search with full Unicode support.