diff --git a/grep.c b/grep.c index fca71ea..5733a04 100644 --- a/grep.c +++ b/grep.c @@ -1,6 +1,6 @@ /** * grep.c - * Simple implementation of text searching for Linux + * Custom implementation of text searching for Linux using Boyer-Moore algorithm */ #include "grep.h" @@ -13,42 +13,1130 @@ #include #include #include +#include +#include +#include // For getting terminal size with TIOCGWINSZ +#include // For terminal control +#include // For malloc, free + +#define MAX_PATTERN_LENGTH 1024 +#define MAX_LINE_LENGTH 4096 +#define MAX_FILE_SIZE (50 * 1024 * 1024) // 50MB max file size + +// Function to determine if a path is a directory +bool is_directory(const char *path) { + struct stat statbuf; + if (stat(path, &statbuf) != 0) + return false; + return S_ISDIR(statbuf.st_mode); +} + +// Function to determine if a file is text file +bool is_text_file(const char *path) { + FILE *file = fopen(path, "r"); + if (!file) return false; + + // Read first 1024 bytes + unsigned char buffer[1024]; + size_t bytes_read = fread(buffer, 1, sizeof(buffer), file); + fclose(file); + + if (bytes_read == 0) return true; // Empty files are considered text + + // Check for binary content + for (size_t i = 0; i < bytes_read; i++) { + // If we find a null byte or too many non-printable characters, consider it binary + if (buffer[i] == 0 || (buffer[i] < 32 && buffer[i] != '\n' && buffer[i] != '\r' && buffer[i] != '\t')) { + return false; + } + } + + return true; +} + +// Function to precompute the Boyer-Moore bad character table +void compute_bad_char_table(const char *pattern, int pattern_len, int bad_char[256]) { + int i; + + // Initialize all occurrences as -1 + for (i = 0; i < 256; i++) + bad_char[i] = -1; + + // Fill the actual value of last occurrence of a character + for (i = 0; i < pattern_len; i++) + bad_char[(unsigned char)pattern[i]] = i; +} + +// Function to precompute the Boyer-Moore good suffix table +void compute_good_suffix_table(const char *pattern, int pattern_len, int suffix[MAX_PATTERN_LENGTH], bool prefix[MAX_PATTERN_LENGTH]) { + int i, j; + + // Initialize suffix and prefix arrays + for (i = 0; i < pattern_len; i++) { + suffix[i] = -1; + prefix[i] = false; + } + + // Case 1: pattern[i+1...m-1] matches suffix of pattern[0...m-1] + for (i = 0; i < pattern_len - 1; i++) { + j = i; + int k = 0; + + // While matching suffix + while (j >= 0 && pattern[j] == pattern[pattern_len - 1 - k]) { + j--; + k++; + suffix[k] = j + 1; + } + + // If we found a suffix that starts at position 0 + if (j == -1) prefix[k] = true; + } +} + +// Function to calculate the shift for the Boyer-Moore algorithm +int get_shift(int bad_char[256], int suffix[MAX_PATTERN_LENGTH], bool prefix[MAX_PATTERN_LENGTH], + int pattern_len, int j, char c) { + // Case 1: Bad character rule + int bad_char_shift = j - bad_char[(unsigned char)c]; + + // Case 2: Good suffix rule + int good_suffix_shift = 0; + + if (j < pattern_len - 1) { + int k = pattern_len - 1 - j; + + if (suffix[k] != -1) { + // Case 2a: Matching suffix + good_suffix_shift = j + 1 - suffix[k]; + } else { + // Case 2b: Matching prefix of pattern + for (int r = j + 2; r <= pattern_len - 1; r++) { + if (prefix[pattern_len - r]) { + good_suffix_shift = r; + break; + } + } + // If no matching prefix found + if (good_suffix_shift == 0) + good_suffix_shift = pattern_len; + } + } + + return (bad_char_shift > good_suffix_shift) ? bad_char_shift : good_suffix_shift; +} + +// Boyer-Moore search in a string +bool boyer_moore_search(const char *text, int text_len, const char *pattern, int pattern_len, + int *match_pos, bool ignore_case) { + if (pattern_len > MAX_PATTERN_LENGTH) { + fprintf(stderr, "Pattern too long (max %d characters)\n", MAX_PATTERN_LENGTH); + return false; + } + + if (pattern_len == 0) return false; + + int bad_char[256]; + int suffix[MAX_PATTERN_LENGTH]; + bool prefix[MAX_PATTERN_LENGTH]; + + // Precompute tables + compute_bad_char_table(pattern, pattern_len, bad_char); + compute_good_suffix_table(pattern, pattern_len, suffix, prefix); + + int s = 0; // Starting position for the current attempt + + while (s <= text_len - pattern_len) { + int j = pattern_len - 1; + + // Compare pattern with text from right to left + while (j >= 0) { + char text_char = text[s + j]; + char pattern_char = pattern[j]; + + if (ignore_case) { + text_char = tolower(text_char); + pattern_char = tolower(pattern_char); + } + + if (text_char != pattern_char) + break; + j--; + } + + // If the pattern was found + if (j < 0) { + *match_pos = s; + return true; + } + + // Shift pattern according to the Boyer-Moore rules + s += get_shift(bad_char, suffix, prefix, pattern_len, j, + ignore_case ? tolower(text[s + j]) : text[s + j]); + } + + return false; // Pattern not found +} + +// Function to process a single file +int process_file(const char *file_path, const char *pattern, bool show_line_numbers, + bool ignore_case, bool fuzzy_match) { + FILE *file = fopen(file_path, "r"); + if (!file) { + fprintf(stderr, "Error: Unable to open file %s\n", file_path); + return 0; + } + + // Check file size + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + rewind(file); + + if (file_size > MAX_FILE_SIZE) { + fprintf(stderr, "Error: File %s is too large (max 50MB)\n", file_path); + fclose(file); + return 0; + } + + // For fuzzy matching, we use a simpler approach + // (in a real implementation, you'd want a proper fuzzy algorithm) + + char line[MAX_LINE_LENGTH]; + int line_number = 0; + int matches_found = 0; + int pattern_len = strlen(pattern); + + while (fgets(line, MAX_LINE_LENGTH, file)) { + line_number++; + int line_len = strlen(line); + + // Remove newline character at the end if present + if (line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + line_len--; + } + + int match_pos = 0; + bool match_found = false; + + if (fuzzy_match) { + // Simple fuzzy matching - allows for character skipping + // For a real implementation, use Levenshtein distance or another algorithm + char *line_ptr = line; + char *pattern_ptr = (char *)pattern; + int matched_chars = 0; + + while (*line_ptr && *pattern_ptr) { + char line_char = ignore_case ? tolower(*line_ptr) : *line_ptr; + char pattern_char = ignore_case ? tolower(*pattern_ptr) : *pattern_ptr; + + if (line_char == pattern_char) { + matched_chars++; + pattern_ptr++; + } + line_ptr++; + } + + // If we've matched at least 70% of the pattern's characters + match_found = (*pattern_ptr == '\0' || + (matched_chars >= pattern_len * 0.7)); + } else { + // Exact matching using Boyer-Moore + match_found = boyer_moore_search(line, line_len, pattern, pattern_len, + &match_pos, ignore_case); + } + + if (match_found) { + matches_found++; + + // Print the file name only once if multiple matches + if (matches_found == 1) { + printf("%s%s%s:\n", ANSI_COLOR_CYAN, file_path, ANSI_COLOR_RESET); + } + + // Print line number if requested + if (show_line_numbers) { + printf(" %s%d:%s ", ANSI_COLOR_GREEN, line_number, ANSI_COLOR_RESET); + } else { + printf(" "); + } + + // Print the matching line, highlighting the match + if (!fuzzy_match) { + // Print the line with the match highlighted + for (int i = 0; i < line_len; i++) { + if (i == match_pos) { + printf("%s", ANSI_COLOR_RED); + for (int j = 0; j < pattern_len; j++) { + putchar(line[i + j]); + } + printf("%s", ANSI_COLOR_RESET); + i += pattern_len - 1; + } else { + putchar(line[i]); + } + } + } else { + // For fuzzy matches, just print the line + printf("%s", line); + } + printf("\n"); + } + } + + fclose(file); + return matches_found; +} + +// Function to recursively search directories +int search_directory(const char *dir_path, const char *pattern, bool show_line_numbers, + bool ignore_case, bool recursive, bool fuzzy_match) { + DIR *dir = opendir(dir_path); + if (!dir) { + fprintf(stderr, "Error: Unable to open directory %s\n", dir_path); + return 0; + } + + struct dirent *entry; + int total_matches = 0; + + while ((entry = readdir(dir)) != NULL) { + // Skip . and .. + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + char path[PATH_MAX]; + snprintf(path, PATH_MAX, "%s/%s", dir_path, entry->d_name); + + if (is_directory(path)) { + // If recursive flag is set, search subdirectories + if (recursive) { + total_matches += search_directory(path, pattern, show_line_numbers, + ignore_case, recursive, fuzzy_match); + } + } else { + // Only process text files + if (is_text_file(path)) { + total_matches += process_file(path, pattern, show_line_numbers, + ignore_case, fuzzy_match); + } + } + } + + closedir(dir); + return total_matches; +} /** - * Simple grep implementation + * Custom grep implementation using Boyer-Moore algorithm */ int lsh_actual_grep(char **args) { if (args[1] == NULL) { - printf("Usage: grep [pattern] [file...]\n"); + printf("Usage: grep [options] pattern [file/directory]\n"); + printf("Options:\n"); + printf(" -n, --line-numbers Show line numbers\n"); + printf(" -i, --ignore-case Ignore case distinctions\n"); + printf(" -r, --recursive Search directories recursively\n"); + printf(" -f, --fuzzy Use fuzzy matching instead of exact\n"); return 1; } + + // Parse options + bool show_line_numbers = false; + bool ignore_case = false; + bool recursive = false; + bool fuzzy_match = false; + + int arg_idx = 1; + const char *pattern = NULL; + + // Parse options + while (args[arg_idx] != NULL && args[arg_idx][0] == '-') { + if (strcmp(args[arg_idx], "-n") == 0 || strcmp(args[arg_idx], "--line-numbers") == 0) { + show_line_numbers = true; + } else if (strcmp(args[arg_idx], "-i") == 0 || strcmp(args[arg_idx], "--ignore-case") == 0) { + ignore_case = true; + } else if (strcmp(args[arg_idx], "-r") == 0 || strcmp(args[arg_idx], "--recursive") == 0) { + recursive = true; + } else if (strcmp(args[arg_idx], "-f") == 0 || strcmp(args[arg_idx], "--fuzzy") == 0) { + fuzzy_match = true; + } else if (strcmp(args[arg_idx], "--help") == 0) { + printf("Usage: grep [options] pattern [file/directory]\n"); + printf("Options:\n"); + printf(" -n, --line-numbers Show line numbers\n"); + printf(" -i, --ignore-case Ignore case distinctions\n"); + printf(" -r, --recursive Search directories recursively\n"); + printf(" -f, --fuzzy Use fuzzy matching instead of exact\n"); + return 1; + } else { + // Unknown option - treat as pattern + pattern = args[arg_idx]; + arg_idx++; + break; + } + arg_idx++; + } + + // The pattern should be the next argument after options + if (pattern == NULL) { + if (args[arg_idx] == NULL) { + fprintf(stderr, "Error: No pattern specified\n"); + return 1; + } + pattern = args[arg_idx++]; + } + + int total_matches = 0; + + // If no files/directories specified, search current directory + if (args[arg_idx] == NULL) { + if (recursive) { + total_matches = search_directory(".", pattern, show_line_numbers, + ignore_case, recursive, fuzzy_match); + } else { + // Just search files in current directory, not recursively + DIR *dir = opendir("."); + if (!dir) { + fprintf(stderr, "Error: Unable to open current directory\n"); + return 1; + } + + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + // Skip directories and non-text files + char path[PATH_MAX]; + snprintf(path, PATH_MAX, "./%s", entry->d_name); + + if (!is_directory(path) && is_text_file(path)) { + total_matches += process_file(path, pattern, show_line_numbers, + ignore_case, fuzzy_match); + } + } + closedir(dir); + } + } else { + // Process each specified file/directory + while (args[arg_idx] != NULL) { + const char *path = args[arg_idx++]; + + if (is_directory(path)) { + total_matches += search_directory(path, pattern, show_line_numbers, + ignore_case, recursive, fuzzy_match); + } else { + if (is_text_file(path)) { + total_matches += process_file(path, pattern, show_line_numbers, + ignore_case, fuzzy_match); + } + } + } + } + + // Print summary + if (total_matches == 0) { + printf("No matches found\n"); + } else { + printf("\nFound %d match%s\n", total_matches, (total_matches == 1 ? "" : "es")); + } + + return 1; +} - // Use the system's grep command - char command[1024] = "grep "; +/** + * Run grep with interactive interface + * + * Shows a split view with files on left, matches on right, and + * updates search results with each keystroke + */ +void run_interactive_grep_session(void) { + // Save original terminal settings to restore later + struct termios old_tio, new_tio; + tcgetattr(STDIN_FILENO, &old_tio); - // Add the pattern (first argument) - strcat(command, "\""); - strcat(command, args[1]); - strcat(command, "\""); + // Copy the old settings and modify for raw input + new_tio = old_tio; + new_tio.c_lflag &= ~(ICANON | ECHO); // Disable canonical mode and echo + new_tio.c_cc[VMIN] = 1; // Read one character at a time + new_tio.c_cc[VTIME] = 0; // No timeout + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + + // Initialize the search query + char search_query[256] = ""; + char last_query[256] = ""; - // Add any additional files specified - for (int i = 2; args[i] != NULL; i++) { - strcat(command, " "); - strcat(command, args[i]); + // Variables for UI state + int running = 1; + int selected_index = 0; + int file_selected_index = 0; + int match_selected_index = 0; + int results_count = 0; + + // Results data structure: + // We'll organize results by file for easier context display + typedef struct { + char *line; // The matching line + int line_number; // Line number in the file + char *context[11]; // 5 lines before, the match line, 5 lines after + int context_line_numbers[11]; // Line numbers for context lines + } MatchLine; + + typedef struct { + char *filename; // Filename + MatchLine *matches; // Array of matches in this file + int match_count; // Number of matches in this file + } FileMatches; + + FileMatches *file_matches = NULL; // Array of file matches + int file_match_count = 0; // Number of files with matches + + char **file_list = NULL; // List of all files + int file_count = 0; // Total number of files + int current_view = 0; // 0 = files, 1 = matches + + // Get terminal size + struct winsize w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + int term_width = w.ws_col; + int term_height = w.ws_row; + int split_point = term_width / 2; + + // Load all files in the current directory to start with + DIR *dir = opendir("."); + if (!dir) { + fprintf(stderr, "Error: Unable to open current directory\n"); + tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); + return; } - // If no files specified, use all files in current directory - if (args[2] == NULL) { - strcat(command, " ."); + // Count files first + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + // Skip . and .. directories + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + // Skip directories, only show files + char path[PATH_MAX]; + snprintf(path, PATH_MAX, "./%s", entry->d_name); + if (!is_directory(path) && is_text_file(path)) { + file_count++; + } } - // Execute the grep command - return system(command); + // Allocate memory for file list + file_list = (char **)malloc(file_count * sizeof(char *)); + if (!file_list) { + closedir(dir); + tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); + return; + } + + // Fill file list + rewinddir(dir); + int file_idx = 0; + while ((entry = readdir(dir)) != NULL && file_idx < file_count) { + // Skip . and .. directories + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + // Skip directories, only show files + char path[PATH_MAX]; + snprintf(path, PATH_MAX, "./%s", entry->d_name); + if (!is_directory(path) && is_text_file(path)) { + file_list[file_idx++] = strdup(entry->d_name); + } + } + closedir(dir); + + // Draw initial UI once + printf(ANSI_CLEAR_SCREEN ANSI_CURSOR_HOME); + printf("--- Interactive Grep Search --- [Type to search | Tab: switch pane | Enter: open file | Ctrl+C: exit]\n\n"); + printf("Search: "); + + // Main interaction loop + while (running) { + // Check terminal size and detect resize + struct winsize new_w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &new_w); + + // If terminal size changed, redraw the screen completely + if (new_w.ws_col != term_width || new_w.ws_row != term_height) { + w = new_w; + term_width = w.ws_col; + term_height = w.ws_row; + split_point = term_width / 2; + + // Redraw the whole screen on resize + printf(ANSI_CLEAR_SCREEN ANSI_CURSOR_HOME); + printf("--- Interactive Grep Search --- [Type to search | Tab: switch pane | Enter: open file | Ctrl+C: exit]\n\n"); + printf("Search: %s", search_query); + } + + // Update just the search query part and clear the rest of the line + printf("\033[3;8H%s\033[K", search_query); + + // Position cursor at the header row + printf("\033[5;1H"); + + // Draw the split view header + printf("Files"); + // Move cursor to split point + printf("\033[%dG", split_point); + printf("| Matches"); + + // Move to the next line for the divider + printf("\n"); + + // Clear the divider line and redraw it + printf("\033[K"); + for (int i = 0; i < split_point - 1; i++) { + printf("-"); + } + printf("+"); + for (int i = split_point + 1; i < term_width; i++) { + printf("-"); + } + + // Check if the query has changed and we need to run search again + if (strcmp(search_query, last_query) != 0) { + // Free previous results if any + if (file_matches) { + for (int i = 0; i < file_match_count; i++) { + for (int j = 0; j < file_matches[i].match_count; j++) { + free(file_matches[i].matches[j].line); + + // Free the context lines + for (int k = 0; k < 11; k++) { + if (file_matches[i].matches[j].context[k]) { + free(file_matches[i].matches[j].context[k]); + } + } + } + + free(file_matches[i].filename); + free(file_matches[i].matches); + } + free(file_matches); + file_matches = NULL; + file_match_count = 0; + } + + results_count = 0; + + if (strlen(search_query) > 0) { + // First pass: count files with matches and allocate the structure + int files_with_matches = 0; + int *matches_per_file = (int *)calloc(file_count, sizeof(int)); + + if (!matches_per_file) { + fprintf(stderr, "Memory allocation error\n"); + continue; + } + + // First, count matches per file + for (int i = 0; i < file_count; i++) { + char filepath[PATH_MAX]; + snprintf(filepath, PATH_MAX, "./%s", file_list[i]); + + // Open the file + FILE *file = fopen(filepath, "r"); + if (!file) continue; + + // Read each line and search for the pattern + char line[MAX_LINE_LENGTH]; + int line_number = 0; + + while (fgets(line, MAX_LINE_LENGTH, file)) { + line_number++; + + // Remove newline if present + int line_len = strlen(line); + if (line_len > 0 && line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + line_len--; + } + + // Check if this line contains the search query + // Using simple case-sensitive search here for simplicity + if (strstr(line, search_query) != NULL) { + if (matches_per_file[i] == 0) { + files_with_matches++; + } + matches_per_file[i]++; + results_count++; + } + } + + fclose(file); + } + + // Allocate the file matches array + if (files_with_matches > 0) { + file_matches = (FileMatches *)malloc(files_with_matches * sizeof(FileMatches)); + if (!file_matches) { + fprintf(stderr, "Memory allocation error\n"); + free(matches_per_file); + continue; + } + + // Initialize the file matches array + file_match_count = 0; + for (int i = 0; i < file_count; i++) { + if (matches_per_file[i] > 0) { + // This file has matches, initialize its entry + file_matches[file_match_count].filename = strdup(file_list[i]); + file_matches[file_match_count].match_count = matches_per_file[i]; + file_matches[file_match_count].matches = + (MatchLine *)malloc(matches_per_file[i] * sizeof(MatchLine)); + + if (!file_matches[file_match_count].matches) { + fprintf(stderr, "Memory allocation error\n"); + // Cleanup what we've allocated so far + free(file_matches[file_match_count].filename); + for (int j = 0; j < file_match_count; j++) { + free(file_matches[j].filename); + free(file_matches[j].matches); + } + free(file_matches); + file_matches = NULL; + file_match_count = 0; + free(matches_per_file); + continue; + } + + // Initialize all matches for this file + for (int j = 0; j < matches_per_file[i]; j++) { + file_matches[file_match_count].matches[j].line = NULL; + for (int k = 0; k < 11; k++) { + file_matches[file_match_count].matches[j].context[k] = NULL; + file_matches[file_match_count].matches[j].context_line_numbers[k] = 0; + } + } + + file_match_count++; + } + } + + // Second pass: fill in the matches with line content and context + for (int file_idx = 0, i = 0; i < file_count && file_idx < file_match_count; i++) { + if (matches_per_file[i] == 0) continue; + + char filepath[PATH_MAX]; + snprintf(filepath, PATH_MAX, "./%s", file_list[i]); + + // Read the whole file into memory to easily extract context + // This is used to provide the surrounding lines + char *file_lines[100000] = {NULL}; // Support for up to 100K lines per file + int total_lines = 0; + + FILE *file = fopen(filepath, "r"); + if (!file) { + file_idx++; + continue; + } + + // Read the file into memory + char line[MAX_LINE_LENGTH]; + while (fgets(line, MAX_LINE_LENGTH, file) && total_lines < 100000) { + // Remove newline if present + int line_len = strlen(line); + if (line_len > 0 && line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + line_len--; + } + + file_lines[total_lines++] = strdup(line); + } + + // Now that we have all lines, look for matches and collect context + int match_idx = 0; + for (int line_idx = 0; line_idx < total_lines && match_idx < matches_per_file[i]; line_idx++) { + // Check if this line contains the search query + if (strstr(file_lines[line_idx], search_query) != NULL) { + // This is a match, save it with context + file_matches[file_idx].matches[match_idx].line = strdup(file_lines[line_idx]); + file_matches[file_idx].matches[match_idx].line_number = line_idx + 1; + + // Get context: 5 lines before and 5 lines after + for (int ctx = 0; ctx < 11; ctx++) { + int target_line = line_idx - 5 + ctx; + + if (target_line >= 0 && target_line < total_lines) { + file_matches[file_idx].matches[match_idx].context[ctx] = + strdup(file_lines[target_line]); + file_matches[file_idx].matches[match_idx].context_line_numbers[ctx] = + target_line + 1; + } else { + file_matches[file_idx].matches[match_idx].context[ctx] = strdup(""); + file_matches[file_idx].matches[match_idx].context_line_numbers[ctx] = 0; + } + } + + match_idx++; + } + } + + // Free the file lines array + for (int line_idx = 0; line_idx < total_lines; line_idx++) { + free(file_lines[line_idx]); + } + + fclose(file); + file_idx++; + } + } + + free(matches_per_file); + } + + // Update last query + strcpy(last_query, search_query); + + // Reset selection + file_selected_index = 0; + match_selected_index = 0; + selected_index = 0; + } + + // Calculate how many items we can display in each pane + int max_display = term_height - 6; // Account for header and other UI elements + + // Draw files in left pane + int display_count = file_count < max_display ? file_count : max_display; + for (int i = 0; i < display_count; i++) { + // Check if this file has matches + int match_idx = -1; + for (int j = 0; j < file_match_count; j++) { + if (strcmp(file_list[i], file_matches[j].filename) == 0) { + match_idx = j; + break; + } + } + + // Highlight if selected and in file view + if (i == selected_index && current_view == 0) { + printf("\033[7m"); // Invert colors + file_selected_index = i; // Track selected file + } + + // Display match count for the file + int match_count = (match_idx >= 0) ? file_matches[match_idx].match_count : 0; + + // Truncate filename if too long + int max_filename_length = split_point - 10; // Leave room for match count + if (strlen(file_list[i]) > max_filename_length) { + printf(" %.%ds… (%d)", max_filename_length - 1, file_list[i], match_count); + } else { + printf(" %s (%d)", file_list[i], match_count); + } + + // Reset colors and clear to end of line section + printf("\033[0m\033[K"); + + // Move cursor to split point + printf("\033[%dG", split_point); + printf("| "); + + // End of line for file entry + printf("\033[K\n"); + } + + // After listing all files, use the rest of the right pane to show context + // for the currently selected file + int context_start_line = display_count + 1; + int selected_file_match_idx = -1; + + // Find the index of the selected file in the file_matches array + for (int i = 0; i < file_match_count; i++) { + if (file_selected_index < file_count && + strcmp(file_list[file_selected_index], file_matches[i].filename) == 0) { + selected_file_match_idx = i; + break; + } + } + + // Set cursor position for context display in the right pane + printf("\033[6;%dH", split_point + 2); + + // Display context for the selected file if it has matches + if (selected_file_match_idx >= 0 && file_matches[selected_file_match_idx].match_count > 0) { + // Title for the context view + printf("%s%s - %d matches%s", + ANSI_COLOR_CYAN, + file_matches[selected_file_match_idx].filename, + file_matches[selected_file_match_idx].match_count, + ANSI_COLOR_RESET); + printf("\033[K\n"); + + // Choose which match to display context for (first one by default) + int match_to_display = 0; + if (match_selected_index < file_matches[selected_file_match_idx].match_count) { + match_to_display = match_selected_index; + } + + // Move cursor to the next line in the right pane + printf("\033[%d;%dH", 7, split_point + 2); + + // Display line number and match info + printf(" %sMatch %d of %d at line %d:%s\033[K\n", + ANSI_COLOR_GREEN, + match_to_display + 1, + file_matches[selected_file_match_idx].match_count, + file_matches[selected_file_match_idx].matches[match_to_display].line_number, + ANSI_COLOR_RESET); + + // Move cursor to the next line in the right pane + printf("\033[%d;%dH", 8, split_point + 2); + + // Display context (5 lines before, the line itself, and 5 lines after) + printf(" %s----- Context -----%s\033[K\n", ANSI_COLOR_YELLOW, ANSI_COLOR_RESET); + + // Display the 11 context lines (5 before, match, 5 after) + for (int ctx = 0; ctx < 11; ctx++) { + // Get the context line + char* ctx_line = file_matches[selected_file_match_idx].matches[match_to_display].context[ctx]; + int ctx_line_number = file_matches[selected_file_match_idx].matches[match_to_display].context_line_numbers[ctx]; + + // Move cursor to the next line in the right pane + printf("\033[%d;%dH", 9 + ctx, split_point + 2); + + if (ctx_line && ctx_line_number > 0) { + // The highlighted line is in position 5 (0-based index) + if (ctx == 5) { + printf(" %s%3d:%s %s%s%s\033[K", + ANSI_COLOR_GREEN, ctx_line_number, ANSI_COLOR_RESET, + ANSI_COLOR_RED, ctx_line, ANSI_COLOR_RESET); + } else { + printf(" %s%3d:%s %s\033[K", + ANSI_COLOR_GREEN, ctx_line_number, ANSI_COLOR_RESET, ctx_line); + } + } else { + // Just print an empty line for padding if we're at file boundaries + printf(" \033[K"); + } + + // Don't add a newline here, as we're using cursor positioning instead + } + } else { + // No matches for this file + if (strlen(search_query) > 0) { + printf("%sNo matches in selected file%s\033[K", ANSI_COLOR_YELLOW, ANSI_COLOR_RESET); + } else { + printf("%sType to search in files%s\033[K", ANSI_COLOR_YELLOW, ANSI_COLOR_RESET); + } + } + + // Clear any remaining lines in the right pane + for (int i = (selected_file_match_idx >= 0 ? 20 : 7); i < term_height - 1; i++) { + printf("\033[%d;%dH\033[K", i, split_point + 2); + } + + // Position cursor at the bottom line + printf("\033[%d;1H", term_height - 1); + + // Show result count at the bottom + printf("\033[K"); // Clear the line + if (strlen(search_query) > 0) { + printf("Found %d match%s in %d file%s", + results_count, + results_count == 1 ? "" : "es", + file_match_count, + file_match_count == 1 ? "" : "s"); + } else { + printf("%d file%s available", + file_count, + file_count == 1 ? "" : "s"); + } + + // Position cursor correctly at the end of the search query + printf("\033[3;%dH", 8 + strlen(search_query)); + fflush(stdout); + + // Allow a small delay to reduce CPU usage and make the UI feel less flickery + struct timespec ts = {0, 5000000}; // 5ms delay + nanosleep(&ts, NULL); + + // Process user input + unsigned char c; + if (read(STDIN_FILENO, &c, 1) > 0) { + if (c == 3) { // Ctrl+C - exit + running = 0; + } else if (c == 9) { // Tab - switch between file and results view + current_view = 1 - current_view; + + // Only switch to results if there are results + if (current_view == 1 && results_count == 0) { + current_view = 0; + } + } else if (c == 10 || c == 13) { // Enter - open file + if (current_view == 0 && selected_index < file_count) { + // Open the selected file in neovim + char filepath[PATH_MAX]; + snprintf(filepath, PATH_MAX, "./%s", file_list[selected_index]); + + // If the file has matches, open at the line of the first match + int line_number = 1; // Default to line 1 if no matches + + // Find if this file has matches + int file_match_idx = -1; + for (int i = 0; i < file_match_count; i++) { + if (strcmp(file_list[selected_index], file_matches[i].filename) == 0) { + file_match_idx = i; + break; + } + } + + // If file has matches, use the line number of the currently selected match + if (file_match_idx >= 0 && file_matches[file_match_idx].match_count > 0) { + int match_idx = 0; + if (match_selected_index < file_matches[file_match_idx].match_count) { + match_idx = match_selected_index; + } + line_number = file_matches[file_match_idx].matches[match_idx].line_number; + } + + // Restore terminal settings before launching editor + tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); + + // Clear the screen + printf(ANSI_CLEAR_SCREEN ANSI_CURSOR_HOME); + + // Check if neovim is available + char command[2048]; + if (system("which nvim >/dev/null 2>&1") == 0) { + snprintf(command, sizeof(command), "nvim +%d \"%s\"", line_number, filepath); + } else if (system("which vim >/dev/null 2>&1") == 0) { + snprintf(command, sizeof(command), "vim +%d \"%s\"", line_number, filepath); + } else { + snprintf(command, sizeof(command), "nano +%d \"%s\"", line_number, filepath); + } + + system(command); + + // Restore our terminal settings + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + + // Redraw UI + printf(ANSI_CLEAR_SCREEN ANSI_CURSOR_HOME); + } + } else if (c == 27) { // Escape sequence for arrow keys + // Read the next two bytes + char seq[2]; + if (read(STDIN_FILENO, &seq[0], 1) > 0 && read(STDIN_FILENO, &seq[1], 1) > 0) { + if (seq[0] == '[') { + switch (seq[1]) { + case 'A': // Up arrow + if (selected_index > 0) selected_index--; + + // Reset match selection when changing files + match_selected_index = 0; + break; + case 'B': // Down arrow + if (current_view == 0 && selected_index < file_count - 1) { + selected_index++; + + // Reset match selection when changing files + match_selected_index = 0; + } + break; + case 'C': // Right arrow - next match in the current file + // Find the current file's match index + int file_match_idx = -1; + for (int i = 0; i < file_match_count; i++) { + if (file_selected_index < file_count && + strcmp(file_list[file_selected_index], file_matches[i].filename) == 0) { + file_match_idx = i; + break; + } + } + + // If file has matches, move to the next match + if (file_match_idx >= 0 && file_matches[file_match_idx].match_count > 0) { + match_selected_index = (match_selected_index + 1) % + file_matches[file_match_idx].match_count; + } + break; + case 'D': // Left arrow - previous match in the current file + // Find the current file's match index + file_match_idx = -1; + for (int i = 0; i < file_match_count; i++) { + if (file_selected_index < file_count && + strcmp(file_list[file_selected_index], file_matches[i].filename) == 0) { + file_match_idx = i; + break; + } + } + + // If file has matches, move to the previous match + if (file_match_idx >= 0 && file_matches[file_match_idx].match_count > 0) { + match_selected_index = (match_selected_index - 1 + + file_matches[file_match_idx].match_count) % + file_matches[file_match_idx].match_count; + } + break; + } + } + } + } else if (c == 127 || c == 8) { // Backspace + // Remove the last character from the search query + size_t len = strlen(search_query); + if (len > 0) { + search_query[len - 1] = '\0'; + } + } else if (isprint(c)) { // Printable character + // Add to the search query + size_t len = strlen(search_query); + if (len < sizeof(search_query) - 1) { + search_query[len] = c; + search_query[len + 1] = '\0'; + } + } + } + } + + // Clean up + if (file_matches) { + for (int i = 0; i < file_match_count; i++) { + for (int j = 0; j < file_matches[i].match_count; j++) { + free(file_matches[i].matches[j].line); + + // Free the context lines + for (int k = 0; k < 11; k++) { + if (file_matches[i].matches[j].context[k]) { + free(file_matches[i].matches[j].context[k]); + } + } + } + + free(file_matches[i].filename); + free(file_matches[i].matches); + } + free(file_matches); + } + + if (file_list) { + for (int i = 0; i < file_count; i++) { + free(file_list[i]); + } + free(file_list); + } + + // Restore original terminal settings + tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); + + // Clear screen + printf(ANSI_CLEAR_SCREEN ANSI_CURSOR_HOME); } /** * Wrapper for grep */ int lsh_grep(char **args) { + // If no arguments provided, run interactive grep + if (args[1] == NULL) { + run_interactive_grep_session(); + return 1; + } + + // Otherwise run the regular grep command return lsh_actual_grep(args); } \ No newline at end of file diff --git a/grep.h b/grep.h index 2dcdda4..8f98789 100644 --- a/grep.h +++ b/grep.h @@ -8,6 +8,14 @@ #include "common.h" +/** + * Run grep with interactive interface + * + * Shows a split view with files on left, matches on right, and + * updates search results with each keystroke + */ +void run_interactive_grep_session(void); + /** * Command handler for the "grep" command * @@ -20,6 +28,9 @@ * --file Specify files/directories to search (otherwise searches * current dir) * + * If called without arguments, opens an interactive grep UI with a split view + * that updates as you type. + * * @param args Command arguments * @return 1 to continue shell execution, 0 to exit shell */ diff --git a/grep.o b/grep.o index 0c17ffc..c46edce 100644 Binary files a/grep.o and b/grep.o differ diff --git a/shell b/shell index da665d7..59a35c8 100755 Binary files a/shell and b/shell differ diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..401006f --- /dev/null +++ b/test.txt @@ -0,0 +1,5 @@ +hello world +This is a test file for the grep command +It contains multiple lines +Boyer-Moore algorithm +testing the grep diff --git a/weather.c b/weather.c index 123f8d0..66e78bc 100644 --- a/weather.c +++ b/weather.c @@ -1,8 +1,7 @@ /** * weather.c * Implementation of weather information fetching and display for Linux - * This is a simplified version that provides basic information about using curl/wget - * instead of implementing full HTTP functionality + * Uses wttr.in service to display weather with its rich UI */ #include "weather.h" @@ -14,62 +13,44 @@ /** * Command handler for the "weather" command - * In this Linux version, we'll provide instructions for using curl/wget + * Executes curl to get weather from wttr.in with full UI */ int lsh_weather(char **args) { - printf("\n"); - printf("╭─────────────────────────────────────────────────────────────╮\n"); - printf("│ Weather Command (Linux Version) │\n"); - printf("├─────────────────────────────────────────────────────────────┤\n"); - printf("│ This is a simplified version of the weather command for │\n"); - printf("│ Linux. The full version would use libcurl for HTTP requests. │\n"); - printf("│ │\n"); - printf("│ To get weather information on Linux: │\n"); - printf("│ │\n"); - printf("│ 1. Using curl: │\n"); - printf("│ curl wttr.in/[city] │\n"); - printf("│ │\n"); - printf("│ 2. Using wget: │\n"); - printf("│ wget -O - wttr.in/[city] │\n"); - printf("│ │\n"); - printf("│ For example: │\n"); - printf("│ curl wttr.in/London │\n"); - printf("│ curl wttr.in/\"New York\" │\n"); - printf("│ │\n"); - printf("│ For a shorter format: │\n"); - printf("│ curl wttr.in/London?format=3 │\n"); - printf("│ │\n"); - printf("├─────────────────────────────────────────────────────────────┤\n"); - - // Check if a city was provided as an argument + char command[256]; + char location[128] = ""; // Default empty location + + // Check if a location was provided as an argument if (args[1]) { - char command[256]; - // Use wttr.in for a simple demonstration - snprintf(command, sizeof(command), "curl -s wttr.in/%s?format=3", args[1]); - printf("│ Running: %-47s │\n", command); - printf("├─────────────────────────────────────────────────────────────┤\n"); - printf("│ Result: │\n"); - printf("│ │\n"); - - // Execute the command - FILE *fp = popen(command, "r"); - if (fp) { - char buffer[256]; - if (fgets(buffer, sizeof(buffer), fp)) { - // Print the result with padding - printf("│ %-53s │\n", buffer); - } else { - printf("│ No result returned │\n"); - } - pclose(fp); - } else { - printf("│ Error executing command │\n"); - } - - printf("│ │\n"); + // Use the provided location + strncpy(location, args[1], sizeof(location) - 1); + location[sizeof(location) - 1] = '\0'; + } else { + // No location provided, use current location (blank parameter to wttr.in) + location[0] = '\0'; } - printf("╰─────────────────────────────────────────────────────────────╯\n\n"); + // Properly URL-encode spaces in the location + char encoded_location[256] = ""; + char *src = location; + char *dst = encoded_location; + + while (*src) { + if (*src == ' ') { + *dst++ = '%'; + *dst++ = '2'; + *dst++ = '0'; + } else { + *dst++ = *src; + } + src++; + } + *dst = '\0'; + + // Format command with the location - using default wttr.in display format + snprintf(command, sizeof(command), "curl -s wttr.in/%s", encoded_location); + + // Execute the command and display the full wttr.in UI + system(command); return 1; } \ No newline at end of file diff --git a/weather.o b/weather.o index 41e4d06..6feccf6 100644 Binary files a/weather.o and b/weather.o differ