#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct { char *path; char *lower_path; off_t size; time_t mtime; int is_dir; char git_status; } FileInfo; typedef enum { SORT_NAME, SORT_SIZE, SORT_DATE } SortMode; typedef enum { SEARCH_FUZZY, SEARCH_REGEX } SearchMode; FileInfo *files = NULL; int file_count = 0; int file_capacity = 0; pthread_mutex_t files_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_t indexing_thread; volatile bool indexing_complete = false; volatile bool indexing_started = false; #define MAX_INDEXING_THREADS 8 #define DIR_QUEUE_CAPACITY 2048 #define FILE_BATCH_SIZE 128 char *dir_queue[DIR_QUEUE_CAPACITY]; int queue_head = 0; int queue_tail = 0; int queue_count = 0; pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t queue_cond = PTHREAD_COND_INITIALIZER; volatile int active_workers = 0; volatile bool producer_finished = false; pthread_mutex_t git_queue_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t git_root_mutex = PTHREAD_MUTEX_INITIALIZER; int *git_queue = NULL; volatile int git_queue_count = 0; #define MAX_HISTORY 10 char search_history[MAX_HISTORY][256]; int history_count = 0; int history_index = -1; #define MAX_BOOKMARKS 100 char *bookmarks[MAX_BOOKMARKS]; int bookmark_count = 0; bool show_bookmarks_only = false; #define MAX_SELECTED 1000 int selected_indices[MAX_SELECTED]; int selected_count = 0; bool show_preview = true; SortMode sort_mode = SORT_NAME; SearchMode search_mode = SEARCH_FUZZY; pthread_mutex_t preview_mutex = PTHREAD_MUTEX_INITIALIZER; char last_preview_path[PATH_MAX] = {0}; void rzf_cleanup_terminal(int sig) { (void)sig; endwin(); exit(0); } char *rzf_get_file_extension(const char *filename) { const char *dot = strrchr(filename, '.'); if (!dot || dot == filename) return ""; return (char *)(dot + 1); } bool rzf_is_text_file(const char *path) { const char *ext = rzf_get_file_extension(path); // Common text file extensions const char *text_exts[] = { "txt", "c", "h", "cpp", "hpp", "cc", "cxx", "py", "js", "ts", "java", "rb", "go", "rs", "sh", "bash", "zsh", "fish", "pl", "php", "html", "htm", "css", "xml", "json", "yaml", "yml", "toml", "ini", "conf", "cfg", "log", "md", "markdown", "rst", "tex", "vim", "el", "lisp", "scm", "clj", "lua", "r", "m", "swift", "kt", "scala", "hs", "ml", "fs", "pas", "d", "nim", "cr", "jl", "dart", "ex", "exs", "erl", "hrl", "zig", "v", "sql", "cmake", "make", "dockerfile", "gitignore", NULL }; // Check if it has no extension (often scripts or config files) if (strlen(ext) == 0) { return true; } // Check against known text extensions for (int i = 0; text_exts[i] != NULL; i++) { if (strcasecmp(ext, text_exts[i]) == 0) { return true; } } return false; } bool rzf_matches_file_type_filter(const char *path, const char *filter) { if (!filter || strlen(filter) < 2 || filter[0] != ':') return true; const char *ext = rzf_get_file_extension(path); const char *filter_ext = filter + 1; if (strcmp(filter_ext, "img") == 0) { return strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0 || strcasecmp(ext, "png") == 0 || strcasecmp(ext, "gif") == 0 || strcasecmp(ext, "bmp") == 0 || strcasecmp(ext, "svg") == 0; } else if (strcmp(filter_ext, "doc") == 0) { return strcasecmp(ext, "txt") == 0 || strcasecmp(ext, "md") == 0 || strcasecmp(ext, "pdf") == 0 || strcasecmp(ext, "doc") == 0 || strcasecmp(ext, "docx") == 0; } return strcasecmp(ext, filter_ext) == 0; } bool rzf_regex_match(const char *text, const char *pattern) { regex_t regex; int ret = regcomp(®ex, pattern, REG_EXTENDED | REG_ICASE); if (ret != 0) return false; ret = regexec(®ex, text, 0, NULL, 0); regfree(®ex); return ret == 0; } char *rzf_to_lower(const char *str) { if (!str) return NULL; char *lower_str = strdup(str); if (!lower_str) { perror("strdup"); exit(EXIT_FAILURE); } for (int i = 0; lower_str[i]; i++) { lower_str[i] = tolower((unsigned char)lower_str[i]); } return lower_str; } void rzf_format_size(off_t size, char *buf) { const char *units[] = {"B", "KB", "MB", "GB", "TB"}; int i = 0; double d_size = size; if (d_size < 1024) { sprintf(buf, "%ldB", (long)d_size); return; } while (d_size >= 1024 && i < 4) { d_size /= 1024.0; i++; } sprintf(buf, "%.1f%s", d_size, units[i]); } char rzf_get_git_status(const char *filepath) { static time_t last_check = 0; static char git_root[PATH_MAX] = {0}; time_t now = time(NULL); pthread_mutex_lock(&git_root_mutex); if (now - last_check > 5 || git_root[0] == 0) { last_check = now; FILE *fp = popen("git rev-parse --show-toplevel 2>/dev/null", "r"); if (fp) { if (fgets(git_root, sizeof(git_root), fp) != NULL) { git_root[strcspn(git_root, "\n")] = 0; } else { git_root[0] = 0; } pclose(fp); } } if (git_root[0] == 0) { pthread_mutex_unlock(&git_root_mutex); return ' '; } char cmd[PATH_MAX + 100]; snprintf(cmd, sizeof(cmd), "cd '%s' && git status --porcelain '%s' 2>/dev/null", git_root, filepath); pthread_mutex_unlock(&git_root_mutex); FILE *fp = popen(cmd, "r"); if (!fp) return ' '; char line[256]; char status = ' '; if (fgets(line, sizeof(line), fp) != NULL) { if (line[0] == 'M' || line[1] == 'M') status = 'M'; else if (line[0] == '?' && line[1] == '?') status = '?'; else if (line[0] == 'A') status = 'A'; else if (line[0] == 'D') status = 'D'; } pclose(fp); return status; } void rzf_load_history(void) { char history_file[PATH_MAX]; snprintf(history_file, sizeof(history_file), "%s/.rzf_history", getenv("HOME")); FILE *fp = fopen(history_file, "r"); if (!fp) return; char line[256]; history_count = 0; while (fgets(line, sizeof(line), fp) && history_count < MAX_HISTORY) { line[strcspn(line, "\n")] = 0; strcpy(search_history[history_count++], line); } fclose(fp); } void rzf_save_history(void) { char history_file[PATH_MAX]; const char *home = getenv("HOME"); if (!home) return; snprintf(history_file, sizeof(history_file), "%s/.rzf_history", home); FILE *fp = fopen(history_file, "w"); if (!fp) return; for (int i = 0; i < history_count; i++) { fprintf(fp, "%s\n", search_history[i]); } fclose(fp); } void rzf_add_to_history(const char *query) { if (strlen(query) == 0) return; for (int i = 0; i < history_count; i++) { if (strcmp(search_history[i], query) == 0) { char temp[256]; strcpy(temp, search_history[i]); for (int j = i; j > 0; j--) { strcpy(search_history[j], search_history[j - 1]); } strcpy(search_history[0], temp); return; } } if (history_count == MAX_HISTORY) { history_count--; } for (int i = history_count; i > 0; i--) { strcpy(search_history[i], search_history[i - 1]); } strcpy(search_history[0], query); history_count++; } void rzf_load_bookmarks(void) { char bookmark_file[PATH_MAX]; const char *home = getenv("HOME"); if (!home) return; snprintf(bookmark_file, sizeof(bookmark_file), "%s/.rzf_bookmarks", home); FILE *fp = fopen(bookmark_file, "r"); if (!fp) return; char line[PATH_MAX]; bookmark_count = 0; while (fgets(line, sizeof(line), fp) && bookmark_count < MAX_BOOKMARKS) { line[strcspn(line, "\n")] = 0; bookmarks[bookmark_count] = strdup(line); if (bookmarks[bookmark_count]) bookmark_count++; } fclose(fp); } void rzf_save_bookmarks(void) { char bookmark_file[PATH_MAX]; const char *home = getenv("HOME"); if (!home) return; snprintf(bookmark_file, sizeof(bookmark_file), "%s/.rzf_bookmarks", home); FILE *fp = fopen(bookmark_file, "w"); if (!fp) return; for (int i = 0; i < bookmark_count; i++) { fprintf(fp, "%s\n", bookmarks[i]); } fclose(fp); } bool rzf_is_bookmarked(const char *path) { char abs_path[PATH_MAX]; if (realpath(path, abs_path) == NULL) return false; for (int i = 0; i < bookmark_count; i++) { if (strcmp(bookmarks[i], abs_path) == 0) return true; } return false; } void rzf_add_bookmark(const char *path) { if (bookmark_count >= MAX_BOOKMARKS) return; char abs_path[PATH_MAX]; if (realpath(path, abs_path) == NULL) return; if (rzf_is_bookmarked(path)) return; bookmarks[bookmark_count] = strdup(abs_path); if (bookmarks[bookmark_count]) { bookmark_count++; rzf_save_bookmarks(); } } void rzf_remove_bookmark(const char *path) { char abs_path[PATH_MAX]; if (realpath(path, abs_path) == NULL) return; for (int i = 0; i < bookmark_count; i++) { if (strcmp(bookmarks[i], abs_path) == 0) { free(bookmarks[i]); for (int j = i; j < bookmark_count - 1; j++) { bookmarks[j] = bookmarks[j + 1]; } bookmark_count--; rzf_save_bookmarks(); return; } } } bool rzf_is_selected(int index) { for (int i = 0; i < selected_count; i++) { if (selected_indices[i] == index) return true; } return false; } void rzf_toggle_selection(int index) { if (rzf_is_selected(index)) { for (int i = 0; i < selected_count; i++) { if (selected_indices[i] == index) { for (int j = i; j < selected_count - 1; j++) { selected_indices[j] = selected_indices[j + 1]; } selected_count--; return; } } } else { if (selected_count < MAX_SELECTED) { selected_indices[selected_count++] = index; } } } void rzf_clear_selections(void) { selected_count = 0; } int rzf_compare_files(const void *a, const void *b) { FileInfo *fa = (FileInfo *)a; FileInfo *fb = (FileInfo *)b; if (fa->is_dir && !fb->is_dir) return -1; if (!fa->is_dir && fb->is_dir) return 1; switch (sort_mode) { case SORT_SIZE: if (fa->size < fb->size) return -1; if (fa->size > fb->size) return 1; return strcasecmp(fa->path, fb->path); case SORT_DATE: if (fa->mtime < fb->mtime) return 1; if (fa->mtime > fb->mtime) return -1; return strcasecmp(fa->path, fb->path); case SORT_NAME: default: return strcasecmp(fa->path, fb->path); } } void rzf_sort_files(void) { pthread_mutex_lock(&files_mutex); if (file_count > 0) { qsort(files, file_count, sizeof(FileInfo), rzf_compare_files); } pthread_mutex_unlock(&files_mutex); } void rzf_draw_file_preview(WINDOW *win, const char *filepath) { if (!win) return; werase(win); box(win, 0, 0); if (!filepath) { mvwprintw(win, 1, 1, "No file selected"); return; } struct stat st; if (stat(filepath, &st) != 0) { mvwprintw(win, 1, 1, "Cannot stat file"); return; } if (S_ISDIR(st.st_mode)) { mvwprintw(win, 1, 1, "[Directory]"); DIR *dir = opendir(filepath); if (dir) { struct dirent *entry; int line = 3; int max_lines = getmaxy(win) - 2; while ((entry = readdir(dir)) != NULL && line < max_lines) { if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) { mvwprintw(win, line++, 2, "%s", entry->d_name); } } closedir(dir); } } else { FILE *fp = fopen(filepath, "rb"); if (!fp) { mvwprintw(win, 1, 1, "Cannot open file"); return; } unsigned char buffer[512]; size_t bytes_read = fread(buffer, 1, sizeof(buffer), fp); bool is_binary = false; for (size_t i = 0; i < bytes_read; i++) { if (buffer[i] == 0 || (buffer[i] < 32 && buffer[i] != '\n' && buffer[i] != '\r' && buffer[i] != '\t')) { is_binary = true; break; } } fclose(fp); if (is_binary) { mvwprintw(win, 1, 1, "[Binary file]"); char size_str[32]; rzf_format_size(st.st_size, size_str); mvwprintw(win, 3, 2, "Size: %s", size_str); } else { fp = fopen(filepath, "r"); if (fp) { char line[1024]; int line_num = 1; int max_lines = getmaxy(win) - 2; int max_cols = getmaxx(win) - 2; while (fgets(line, sizeof(line), fp) && line_num < max_lines) { line[strcspn(line, "\n")] = 0; mvwprintw(win, line_num, 1, "%.*s", max_cols, line); line_num++; } fclose(fp); } } } } void rzf_add_files_batch(FileInfo *batch, int count) { if (count == 0) return; pthread_mutex_lock(&files_mutex); if (file_count + count > file_capacity) { file_capacity = (file_count + count) * 2; if (file_capacity == 0) file_capacity = 256; FileInfo *new_files = realloc(files, file_capacity * sizeof(FileInfo)); if (!new_files) { pthread_mutex_unlock(&files_mutex); perror("realloc failed"); exit(EXIT_FAILURE); } files = new_files; } memcpy(&files[file_count], batch, count * sizeof(FileInfo)); file_count += count; pthread_mutex_unlock(&files_mutex); } void rzf_enqueue_dir(const char *path) { pthread_mutex_lock(&queue_mutex); while (queue_count >= DIR_QUEUE_CAPACITY) { pthread_cond_wait(&queue_cond, &queue_mutex); } dir_queue[queue_tail] = strdup(path); if (!dir_queue[queue_tail]) { pthread_mutex_unlock(&queue_mutex); return; } queue_tail = (queue_tail + 1) % DIR_QUEUE_CAPACITY; queue_count++; active_workers++; pthread_cond_signal(&queue_cond); pthread_mutex_unlock(&queue_mutex); } char *rzf_dequeue_dir() { pthread_mutex_lock(&queue_mutex); while (queue_count == 0 && !producer_finished) { pthread_cond_wait(&queue_cond, &queue_mutex); } if (queue_count == 0) { pthread_mutex_unlock(&queue_mutex); return NULL; } char *path = dir_queue[queue_head]; queue_head = (queue_head + 1) % DIR_QUEUE_CAPACITY; queue_count--; pthread_cond_signal(&queue_cond); pthread_mutex_unlock(&queue_mutex); return path; } void *rzf_indexing_worker_func(void *arg) { (void)arg; const char *ignore_list[] = {".git", "node_modules", ".venv", "venv", "env", NULL}; FileInfo local_batch[FILE_BATCH_SIZE]; int local_count = 0; while (1) { char *base_path = rzf_dequeue_dir(); if (base_path == NULL) break; DIR *dir = opendir(base_path); if (dir) { struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; bool should_ignore = false; for (int i = 0; ignore_list[i] != NULL; i++) { if (strcmp(entry->d_name, ignore_list[i]) == 0) { should_ignore = true; break; } } if (should_ignore) continue; char path[PATH_MAX]; snprintf(path, sizeof(path), "%s/%s", base_path, entry->d_name); struct stat statbuf; if (lstat(path, &statbuf) == 0) { int is_dir = S_ISDIR(statbuf.st_mode); char real[PATH_MAX]; if (realpath(path, real) == NULL) continue; local_batch[local_count].path = strdup(path); local_batch[local_count].lower_path = rzf_to_lower(path); if (!local_batch[local_count].path || !local_batch[local_count].lower_path) { if (local_batch[local_count].path) free(local_batch[local_count].path); if (local_batch[local_count].lower_path) free(local_batch[local_count].lower_path); continue; } local_batch[local_count].size = statbuf.st_size; local_batch[local_count].mtime = statbuf.st_mtime; local_batch[local_count].is_dir = is_dir; local_batch[local_count].git_status = ' '; local_count++; if (local_count == FILE_BATCH_SIZE) { rzf_add_files_batch(local_batch, local_count); local_count = 0; } if (is_dir && !S_ISLNK(statbuf.st_mode)) { rzf_enqueue_dir(path); } } } closedir(dir); } free(base_path); pthread_mutex_lock(&queue_mutex); active_workers--; if (active_workers == 0 && producer_finished) { pthread_cond_broadcast(&queue_cond); } pthread_mutex_unlock(&queue_mutex); } if (local_count > 0) { rzf_add_files_batch(local_batch, local_count); } return NULL; } void *rzf_git_worker_func(void *arg) { (void)arg; while (1) { int i; pthread_mutex_lock(&git_queue_mutex); if (git_queue_count == 0) { pthread_mutex_unlock(&git_queue_mutex); break; } i = git_queue[--git_queue_count]; pthread_mutex_unlock(&git_queue_mutex); char *path_copy; pthread_mutex_lock(&files_mutex); path_copy = (i < file_count) ? strdup(files[i].path) : NULL; pthread_mutex_unlock(&files_mutex); if (path_copy) { char status = rzf_get_git_status(path_copy); free(path_copy); pthread_mutex_lock(&files_mutex); if (i < file_count) files[i].git_status = status; pthread_mutex_unlock(&files_mutex); } } return NULL; } void *rzf_indexing_thread_func(void *arg) { (void)arg; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); indexing_started = true; long num_cores = sysconf(_SC_NPROCESSORS_ONLN); int num_threads = (num_cores > 1 && num_cores <= MAX_INDEXING_THREADS) ? num_cores : 4; pthread_t threads[num_threads]; for (int i = 0; i < num_threads; i++) { pthread_create(&threads[i], NULL, rzf_indexing_worker_func, NULL); } rzf_enqueue_dir("."); pthread_mutex_lock(&queue_mutex); producer_finished = true; while (active_workers > 0) { pthread_cond_broadcast(&queue_cond); pthread_cond_wait(&queue_cond, &queue_mutex); } pthread_mutex_unlock(&queue_mutex); for (int i = 0; i < num_threads; i++) { pthread_join(threads[i], NULL); } rzf_sort_files(); int count = 0; pthread_mutex_lock(&files_mutex); count = file_count < 1000 ? file_count : 1000; pthread_mutex_unlock(&files_mutex); if (count > 0) { git_queue = malloc(count * sizeof(int)); if (git_queue) { for (int i = 0; i < count; i++) git_queue[i] = i; git_queue_count = count; pthread_t git_threads[num_threads]; for (int i = 0; i < num_threads; i++) { pthread_create(&git_threads[i], NULL, rzf_git_worker_func, NULL); } for (int i = 0; i < num_threads; i++) { pthread_join(git_threads[i], NULL); } free(git_queue); git_queue = NULL; git_queue_count = 0; } } indexing_complete = true; return NULL; } int rzf_recursive_delete(const char *path) { struct stat path_stat; if (lstat(path, &path_stat) != 0) return -1; if (!S_ISDIR(path_stat.st_mode)) { return remove(path); } DIR *d = opendir(path); if (!d) return -1; struct dirent *p; int ret = 0; while (ret == 0 && (p = readdir(d))) { if (!strcmp(p->d_name, ".") || !strcmp(p->d_name, "..")) continue; char *buf; size_t len = strlen(path) + strlen(p->d_name) + 2; buf = malloc(len); if (!buf) { closedir(d); return -1; } snprintf(buf, len, "%s/%s", path, p->d_name); struct stat statbuf; if (lstat(buf, &statbuf) == 0) { if (S_ISDIR(statbuf.st_mode) && !S_ISLNK(statbuf.st_mode)) { ret = rzf_recursive_delete(buf); } else { ret = remove(buf); } } free(buf); } closedir(d); if (ret == 0) { ret = rmdir(path); } return ret; } void rzf_draw_help_window(int height, int width) { int h = 20, w = 55; int start_y = (height - h) / 2; int start_x = (width - w) / 2; WINDOW *win = newwin(h, w, start_y, start_x); if (!win) return; box(win, 0, 0); wattron(win, A_BOLD); mvwprintw(win, 1, (w - 10) / 2, "Shortcuts"); wattroff(win, A_BOLD); int line = 3; mvwprintw(win, line++, 2, "Up/Down : Navigate"); mvwprintw(win, line++, 2, "Enter : Open file/directory"); mvwprintw(win, line++, 2, "Ctrl-X : Copy path to clipboard"); mvwprintw(win, line++, 2, "Ctrl-Y : Yank (copy) file contents"); mvwprintw(win, line++, 2, "Ctrl-B : Backup file with timestamp"); mvwprintw(win, line++, 2, "Ctrl-D : Delete selected item(s)"); mvwprintw(win, line++, 2, "Ctrl-P : Toggle preview panel"); mvwprintw(win, line++, 2, "Ctrl-R : Search history"); mvwprintw(win, line++, 2, "Ctrl-S : Toggle bookmark"); mvwprintw(win, line++, 2, "Ctrl-F : Show bookmarks only"); mvwprintw(win, line++, 2, "Ctrl-Space : Multi-select"); mvwprintw(win, line++, 2, "Ctrl-E : Toggle regex search"); mvwprintw(win, line++, 2, "Ctrl-V : Change sort mode"); mvwprintw(win, line++, 2, "Ctrl-U : Go to parent directory"); mvwprintw(win, line++, 2, "Ctrl-K : Run command on file"); mvwprintw(win, line++, 2, ":ext : Filter by extension (e.g. :py)"); mvwprintw(win, line++, 2, "Ctrl-C/Esc : Quit"); mvwprintw(win, line++, 2, "? : Toggle this help"); wrefresh(win); wgetch(win); delwin(win); } bool rzf_show_confirmation(int height, int width, const char *message) { int h = 3, w = strlen(message) + 8; if (w > width - 4) w = width - 4; int start_y = (height - h) / 2; int start_x = (width - w) / 2; WINDOW *win = newwin(h, w, start_y, start_x); if (!win) return false; box(win, 0, 0); mvwprintw(win, 1, 2, "%.*s (y/n)", w - 8, message); wrefresh(win); int confirm_ch; while (1) { confirm_ch = wgetch(win); if (confirm_ch == 'y' || confirm_ch == 'Y') { delwin(win); return true; } if (confirm_ch == 'n' || confirm_ch == 'N' || confirm_ch == 27) { delwin(win); return false; } } } char *rzf_prompt_for_command(int height, int width) { int h = 3, w = 60; if (w > width - 4) w = width - 4; int start_y = (height - h) / 2; int start_x = (width - w) / 2; WINDOW *win = newwin(h, w, start_y, start_x); if (!win) return NULL; box(win, 0, 0); mvwprintw(win, 1, 2, "Command: "); wrefresh(win); echo(); char *command = malloc(256); if (!command) { noecho(); delwin(win); return NULL; } command[0] = '\0'; mvwgetnstr(win, 1, 11, command, 255); noecho(); delwin(win); if (strlen(command) == 0) { free(command); return NULL; } return command; } char *rzf_run_interface(char **selected_file_path) { initscr(); raw(); noecho(); keypad(stdscr, TRUE); curs_set(0); timeout(100); start_color(); use_default_colors(); init_pair(1, COLOR_WHITE, COLOR_CYAN); init_pair(2, COLOR_BLUE, -1); init_pair(3, COLOR_WHITE, COLOR_BLUE); init_pair(4, COLOR_YELLOW, -1); init_pair(5, COLOR_GREEN, -1); init_pair(6, COLOR_RED, -1); init_pair(7, COLOR_MAGENTA, -1); init_pair(8, COLOR_YELLOW, COLOR_BLACK); char search_query[256] = {0}; int selected_index = 0; int scroll_offset = 0; char *command = NULL; int filtered_capacity = 256; rzf_load_history(); rzf_load_bookmarks(); int *filtered_indices = malloc(filtered_capacity * sizeof(int)); if (!filtered_indices) { endwin(); perror("malloc"); exit(EXIT_FAILURE); } int ch; int current_file_count = 0; int filtered_count = 0; WINDOW *preview_win = NULL; int last_height = 0, last_width = 0; WINDOW *file_list_win = NULL; while (1) { int height, width; getmaxyx(stdscr, height, width); bool resized = (height != last_height || width != last_width); if (resized) { if (file_list_win) { delwin(file_list_win); file_list_win = NULL; } if (preview_win) { delwin(preview_win); preview_win = NULL; } clear(); last_height = height; last_width = width; } int list_height = height - 2; int list_width = show_preview ? (width / 2) : width; if (!file_list_win) { file_list_win = newwin(list_height, list_width, 1, 0); } else { wresize(file_list_win, list_height, list_width); } if (show_preview) { int preview_width = width - list_width; if (!preview_win) { preview_win = newwin(list_height, preview_width, 1, list_width); } else { wresize(preview_win, list_height, preview_width); mvwin(preview_win, 1, list_width); } } else { if (preview_win) { delwin(preview_win); preview_win = NULL; } } ch = getch(); switch (ch) { case KEY_UP: if (selected_index > 0) selected_index--; if (selected_index < scroll_offset) scroll_offset = selected_index; break; case KEY_DOWN: if (selected_index < filtered_count - 1) selected_index++; if (selected_index >= scroll_offset + list_height) scroll_offset++; break; case 10: if (filtered_count > 0) { pthread_mutex_lock(&files_mutex); int file_idx = filtered_indices[selected_index]; if (file_idx < file_count) { if (files[file_idx].is_dir) { // Navigate into directory char *dir_path = strdup(files[file_idx].path); pthread_mutex_unlock(&files_mutex); if (dir_path && chdir(dir_path) == 0) { // Clear the queue pthread_mutex_lock(&queue_mutex); for (int i = 0; i < queue_count; i++) { int idx = (queue_head + i) % DIR_QUEUE_CAPACITY; if (dir_queue[idx]) { free(dir_queue[idx]); dir_queue[idx] = NULL; } } queue_head = 0; queue_tail = 0; queue_count = 0; pthread_mutex_unlock(&queue_mutex); // Clear files pthread_mutex_lock(&files_mutex); if (files) { for (int i = 0; i < file_count; i++) { free(files[i].path); free(files[i].lower_path); } free(files); } files = NULL; file_count = 0; file_capacity = 0; pthread_mutex_unlock(&files_mutex); // Cancel and restart indexing if (indexing_started && !indexing_complete) { pthread_cancel(indexing_thread); pthread_join(indexing_thread, NULL); } indexing_complete = false; indexing_started = false; producer_finished = false; active_workers = 0; pthread_create(&indexing_thread, NULL, rzf_indexing_thread_func, NULL); search_query[0] = '\0'; selected_index = 0; scroll_offset = 0; } if (dir_path) free(dir_path); } else { // Open file char abs[PATH_MAX]; if (realpath(files[file_idx].path, abs)) *selected_file_path = strdup(abs); else *selected_file_path = strdup(files[file_idx].path); // Determine whether to use vim or xdg-open if (rzf_is_text_file(*selected_file_path)) { command = strdup("vim"); } else { command = strdup("xdg-open"); } pthread_mutex_unlock(&files_mutex); goto end_loop; } } else { pthread_mutex_unlock(&files_mutex); } } break; case 27: case 3: goto end_loop; case 2: if (filtered_count > 0) { pthread_mutex_lock(&files_mutex); int file_idx = filtered_indices[selected_index]; char *path = NULL; int is_dir = 0; if (file_idx < file_count) { path = strdup(files[file_idx].path); is_dir = files[file_idx].is_dir; } pthread_mutex_unlock(&files_mutex); if (path && !is_dir) { time_t t = time(NULL); struct tm *tm = localtime(&t); char dt[32]; strftime(dt, sizeof(dt), "_%Y%m%d_%H%M%S", tm); /* hidden backup path in the same directory */ char backup_path[PATH_MAX]; const char *slash = strrchr(path, '/'); const char *fname = slash ? slash + 1 : path; size_t dir_len = slash ? (size_t)(slash - path) : 0; const char *dot = strrchr(fname, '.'); if (dot) { snprintf(backup_path, sizeof(backup_path), "%.*s/.%.*s%s%s.bak", (int)dir_len, path, (int)(dot - fname), fname, dt, dot); } else { snprintf(backup_path, sizeof(backup_path), "%.*s/.%s%s.bak", (int)dir_len, path, fname, dt); } char cmd[PATH_MAX * 2 + 10]; snprintf(cmd, sizeof(cmd), "cp -- '%s' '%s'", path, backup_path); system(cmd); } if (path) free(path); } break; case 24: if (filtered_count > 0) { char copy_command[PATH_MAX * 10 + 100] = {0}; pthread_mutex_lock(&files_mutex); if (selected_count > 0) { char all_paths[PATH_MAX * 10] = {0}; size_t all_paths_used = 0; for (int i = 0; i < selected_count && i < 10; i++) { if (selected_indices[i] < file_count) { char abs_path[PATH_MAX]; if (realpath(files[selected_indices[i]].path, abs_path)) { size_t path_len = strlen(abs_path); if (all_paths_used + path_len + 1 < sizeof(all_paths)) { strcat(all_paths, abs_path); strcat(all_paths, "\n"); all_paths_used += path_len + 1; } } } } pthread_mutex_unlock(&files_mutex); if (strlen(all_paths) > 0) { all_paths[strlen(all_paths) - 1] = '\0'; snprintf(copy_command, sizeof(copy_command), "echo -n '%s' | xclip -selection clipboard", all_paths); } } else { int file_idx = filtered_indices[selected_index]; char *path = (file_idx < file_count) ? strdup(files[file_idx].path) : NULL; pthread_mutex_unlock(&files_mutex); if (path) { char abs_path[PATH_MAX]; if (realpath(path, abs_path)) { snprintf(copy_command, sizeof(copy_command), "echo -n '%s' | xclip -selection clipboard", abs_path); } free(path); } } if (strlen(copy_command) > 0) { FILE *pipe = popen(copy_command, "w"); if (pipe) pclose(pipe); } } break; case 25: if (filtered_count > 0) { pthread_mutex_lock(&files_mutex); int file_idx = filtered_indices[selected_index]; char *path = (file_idx < file_count && !files[file_idx].is_dir) ? strdup(files[file_idx].path) : NULL; pthread_mutex_unlock(&files_mutex); if (path) { char cmd[2048]; snprintf(cmd, sizeof(cmd), "cat '%s' | xclip -selection clipboard", path); FILE *p = popen(cmd, "w"); if (p) pclose(p); free(path); } } break; case 4: if (selected_count > 0 || filtered_count > 0) { char confirm_msg[1024] = {0}; bool confirmed = false; if (selected_count > 0) { confirmed = rzf_show_confirmation(height, width, "Delete selected files?"); } else { pthread_mutex_lock(&files_mutex); int idx = filtered_indices[selected_index]; if (idx < file_count) snprintf(confirm_msg, sizeof(confirm_msg), "Delete '%s'?", files[idx].path); pthread_mutex_unlock(&files_mutex); if (strlen(confirm_msg) > 0) confirmed = rzf_show_confirmation(height, width, confirm_msg); } if (confirmed) { if (selected_count > 0) { char **paths_to_delete = malloc(selected_count * sizeof(char *)); int *is_dir_flags = malloc(selected_count * sizeof(int)); int delete_count = 0; pthread_mutex_lock(&files_mutex); for (int i = 0; i < selected_count; i++) { if (selected_indices[i] < file_count) { paths_to_delete[delete_count] = strdup(files[selected_indices[i]].path); is_dir_flags[delete_count] = files[selected_indices[i]].is_dir; if (paths_to_delete[delete_count]) delete_count++; } } pthread_mutex_unlock(&files_mutex); for (int i = 0; i < delete_count; i++) { if (is_dir_flags[i]) rzf_recursive_delete(paths_to_delete[i]); else remove(paths_to_delete[i]); free(paths_to_delete[i]); } free(paths_to_delete); free(is_dir_flags); rzf_clear_selections(); } else { pthread_mutex_lock(&files_mutex); int idx = filtered_indices[selected_index]; char *p = (idx < file_count) ? strdup(files[idx].path) : NULL; int d = (idx < file_count) ? files[idx].is_dir : 0; pthread_mutex_unlock(&files_mutex); if (p) { if (d) rzf_recursive_delete(p); else remove(p); free(p); } } pthread_mutex_lock(&queue_mutex); for (int i = 0; i < queue_count; i++) { int idx = (queue_head + i) % DIR_QUEUE_CAPACITY; if (dir_queue[idx]) { free(dir_queue[idx]); dir_queue[idx] = NULL; } } queue_head = 0; queue_tail = 0; queue_count = 0; pthread_mutex_unlock(&queue_mutex); pthread_mutex_lock(&files_mutex); if (files) { for (int i = 0; i < file_count; i++) { free(files[i].path); free(files[i].lower_path); } free(files); } files = NULL; file_count = 0; file_capacity = 0; pthread_mutex_unlock(&files_mutex); if (indexing_started && !indexing_complete) { pthread_cancel(indexing_thread); pthread_join(indexing_thread, NULL); } indexing_complete = false; indexing_started = false; producer_finished = false; active_workers = 0; pthread_create(&indexing_thread, NULL, rzf_indexing_thread_func, NULL); } } break; case 16: show_preview = !show_preview; break; case 18: if (history_count > 0) { history_index = (history_index + 1) % history_count; strcpy(search_query, search_history[history_index]); } break; case 19: if (filtered_count > 0) { pthread_mutex_lock(&files_mutex); int idx = filtered_indices[selected_index]; if (idx < file_count) { char *p = files[idx].path; if (rzf_is_bookmarked(p)) rzf_remove_bookmark(p); else rzf_add_bookmark(p); } pthread_mutex_unlock(&files_mutex); } break; case 6: show_bookmarks_only = !show_bookmarks_only; break; case 0: if (filtered_count > 0 && selected_count < MAX_SELECTED) { pthread_mutex_lock(&files_mutex); int idx = filtered_indices[selected_index]; pthread_mutex_unlock(&files_mutex); rzf_toggle_selection(idx); } break; case 5: search_mode = (search_mode == SEARCH_FUZZY) ? SEARCH_REGEX : SEARCH_FUZZY; break; case 21: { char cwd[PATH_MAX]; if (getcwd(cwd, sizeof(cwd))) { if (strcmp(cwd, "/") != 0) { if (chdir("..") == 0) { pthread_mutex_lock(&queue_mutex); for (int i = 0; i < queue_count; i++) { int idx = (queue_head + i) % DIR_QUEUE_CAPACITY; if (dir_queue[idx]) { free(dir_queue[idx]); dir_queue[idx] = NULL; } } queue_head = 0; queue_tail = 0; queue_count = 0; pthread_mutex_unlock(&queue_mutex); pthread_mutex_lock(&files_mutex); if (files) { for (int i = 0; i < file_count; i++) { free(files[i].path); free(files[i].lower_path); } free(files); } files = NULL; file_count = 0; file_capacity = 0; pthread_mutex_unlock(&files_mutex); if (indexing_started && !indexing_complete) { pthread_cancel(indexing_thread); pthread_join(indexing_thread, NULL); } indexing_complete = false; indexing_started = false; producer_finished = false; active_workers = 0; pthread_create(&indexing_thread, NULL, rzf_indexing_thread_func, NULL); search_query[0] = '\0'; selected_index = 0; scroll_offset = 0; } } } } break; case 11: if (filtered_count > 0) { char *cmd = rzf_prompt_for_command(height, width); if (cmd && strlen(cmd) > 0) { pthread_mutex_lock(&files_mutex); int idx = filtered_indices[selected_index]; char *fp = (idx < file_count) ? strdup(files[idx].path) : NULL; pthread_mutex_unlock(&files_mutex); if (fp) { size_t fc_size = strlen(cmd) + strlen(fp) + 10; char *fc = malloc(fc_size); if (fc) { snprintf(fc, fc_size, "%s '%s'", cmd, fp); endwin(); system(fc); printf("\nPress any key to continue..."); getchar(); initscr(); raw(); noecho(); keypad(stdscr, TRUE); curs_set(0); timeout(100); free(fc); } free(fp); } } if (cmd) free(cmd); } break; case 22: sort_mode = (sort_mode + 1) % 3; rzf_sort_files(); break; case '?': rzf_draw_help_window(height, width); break; case KEY_BACKSPACE: case 127: if (strlen(search_query) > 0) { search_query[strlen(search_query) - 1] = '\0'; } break; default: if (isprint(ch) && strlen(search_query) < sizeof(search_query) - 1) { strncat(search_query, (char *)&ch, 1); } break; } char *filter_start = strchr(search_query, ':'); char file_type_filter[32] = {0}; if (filter_start && strlen(filter_start) < sizeof(file_type_filter)) { strcpy(file_type_filter, filter_start); } pthread_mutex_lock(&files_mutex); current_file_count = file_count; if (current_file_count > filtered_capacity) { filtered_capacity = current_file_count * 2; int *new_filtered = realloc(filtered_indices, filtered_capacity * sizeof(int)); if (!new_filtered) { pthread_mutex_unlock(&files_mutex); continue; } filtered_indices = new_filtered; } filtered_count = 0; if (current_file_count > 0) { char search_pattern[256]; if (filter_start) { size_t len = filter_start - search_query; if (len >= sizeof(search_pattern)) len = sizeof(search_pattern) - 1; strncpy(search_pattern, search_query, len); search_pattern[len] = '\0'; } else { strncpy(search_pattern, search_query, sizeof(search_pattern) - 1); search_pattern[sizeof(search_pattern) - 1] = '\0'; } char *lower_query = rzf_to_lower(search_pattern); if (lower_query) { for (int i = 0; i < current_file_count; i++) { if (show_bookmarks_only && !rzf_is_bookmarked(files[i].path)) continue; if (file_type_filter[0] && !files[i].is_dir && !rzf_matches_file_type_filter(files[i].path, file_type_filter)) continue; bool m = (search_mode == SEARCH_REGEX && strlen(search_pattern) > 0) ? rzf_regex_match(files[i].path, search_pattern) : (strstr(files[i].lower_path, lower_query) != NULL); if (m) filtered_indices[filtered_count++] = i; } free(lower_query); } } pthread_mutex_unlock(&files_mutex); if (selected_index >= filtered_count) { selected_index = filtered_count > 0 ? filtered_count - 1 : 0; } move(0, 0); clrtoeol(); attron(A_BOLD); int prompt_end; if (!indexing_complete) { prompt_end = mvprintw(0, 0, "%s%sSearch: %s (indexing... %d files)", search_mode == SEARCH_REGEX ? "[REGEX] " : "", show_bookmarks_only ? "[*] " : "", search_query, current_file_count); } else { const char *sort_str = sort_mode == SORT_SIZE ? "[SIZE] " : (sort_mode == SORT_DATE ? "[DATE] " : ""); prompt_end = mvprintw(0, 0, "%s%s%sSearch: %s", sort_str, search_mode == SEARCH_REGEX ? "[REGEX] " : "", show_bookmarks_only ? "[*] " : "", search_query); } if (selected_count > 0) mvprintw(0, prompt_end + 2, "(%d selected)", selected_count); attroff(A_BOLD); werase(file_list_win); pthread_mutex_lock(&files_mutex); for (int i = 0; i < list_height && (i + scroll_offset) < filtered_count; ++i) { int current_index = i + scroll_offset; if (current_index >= filtered_count) continue; int file_idx = filtered_indices[current_index]; if (file_idx >= file_count) continue; FileInfo *file = &files[file_idx]; char size_buf[10]; if (!file->is_dir) rzf_format_size(file->size, size_buf); else strcpy(size_buf, ""); char time_buf[20]; strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M", localtime(&file->mtime)); char git_char = file->git_status; char display_line[list_width + 1]; snprintf(display_line, sizeof(display_line), "%c%s %s %s", git_char != ' ' ? git_char : ' ', rzf_is_bookmarked(file->path) ? "*" : " ", file->is_dir ? "[D]" : " ", file->path); int path_len = strlen(display_line); if (list_width - path_len > 30) { snprintf(display_line + path_len, list_width - path_len, "%*s %s", list_width - path_len - 20, size_buf, time_buf); } if (rzf_is_selected(file_idx)) wattron(file_list_win, COLOR_PAIR(8)); else if (current_index == selected_index) wattron(file_list_win, COLOR_PAIR(1) | A_BOLD); else if (rzf_is_bookmarked(file->path)) wattron(file_list_win, COLOR_PAIR(7)); else if (file->git_status == 'M') wattron(file_list_win, COLOR_PAIR(5)); else if (file->git_status == '?') wattron(file_list_win, COLOR_PAIR(6)); else if (file->is_dir) wattron(file_list_win, COLOR_PAIR(2) | A_BOLD); mvwprintw(file_list_win, i, 0, "%.*s", list_width, display_line); wattroff(file_list_win, A_REVERSE | A_BOLD | COLOR_PAIR(1) | COLOR_PAIR(2) | COLOR_PAIR(5) | COLOR_PAIR(6) | COLOR_PAIR(7) | COLOR_PAIR(8)); } pthread_mutex_unlock(&files_mutex); char *preview_path = NULL; if (show_preview && filtered_count > 0) { pthread_mutex_lock(&files_mutex); int idx = filtered_indices[selected_index]; if (idx < file_count) preview_path = files[idx].path; pthread_mutex_unlock(&files_mutex); } pthread_mutex_lock(&preview_mutex); if (preview_path && (resized || strcmp(preview_path, last_preview_path) != 0)) { rzf_draw_file_preview(preview_win, preview_path); strncpy(last_preview_path, preview_path, PATH_MAX - 1); last_preview_path[PATH_MAX - 1] = '\0'; } if (!show_preview) last_preview_path[0] = '\0'; pthread_mutex_unlock(&preview_mutex); move(height - 1, 0); clrtoeol(); char footer_text[256]; snprintf(footer_text, sizeof(footer_text), "^V:Sort %s | ^P:Preview | ^S:Bookmark | ^F:%s | ^E:%s | ?:Help | " "^C:Quit", sort_mode == SORT_NAME ? "NAME" : (sort_mode == SORT_SIZE ? "SIZE" : "DATE"), show_bookmarks_only ? "ALL" : "*ONLY", search_mode == SEARCH_FUZZY ? "REGEX" : "FUZZY"); attron(COLOR_PAIR(3) | A_BOLD); mvprintw(height - 1, 0, "%-*s", width, footer_text); attroff(COLOR_PAIR(3) | A_BOLD); wnoutrefresh(stdscr); wnoutrefresh(file_list_win); if (preview_win) wnoutrefresh(preview_win); doupdate(); } end_loop: if (strlen(search_query) > 0) { rzf_add_to_history(search_query); rzf_save_history(); } if (preview_win) delwin(preview_win); if (file_list_win) delwin(file_list_win); endwin(); free(filtered_indices); return command; } void rzf_free_files() { pthread_mutex_lock(&files_mutex); if (files) { for (int i = 0; i < file_count; i++) { free(files[i].path); free(files[i].lower_path); } free(files); files = NULL; } pthread_mutex_unlock(&files_mutex); } void rzf_free_bookmarks() { for (int i = 0; i < bookmark_count; i++) { free(bookmarks[i]); } } int main() { signal(SIGINT, rzf_cleanup_terminal); signal(SIGTERM, rzf_cleanup_terminal); signal(SIGHUP, rzf_cleanup_terminal); pthread_create(&indexing_thread, NULL, rzf_indexing_thread_func, NULL); char *file_to_open = NULL; char *command = rzf_run_interface(&file_to_open); if (command && file_to_open) { // Fork and execute pid_t pid = fork(); if (pid == 0) { // Child process if (strcmp(command, "xdg-open") == 0) { // For xdg-open, we need to detach from the terminal setsid(); // Redirect stdout/stderr to /dev/null to avoid terminal corruption freopen("/dev/null", "w", stdout); freopen("/dev/null", "w", stderr); } execlp(command, command, file_to_open, (char *)NULL); perror("execlp failed"); exit(EXIT_FAILURE); } else if (pid > 0) { // Parent process if (strcmp(command, "vim") == 0) { // Wait for vim to finish wait(NULL); } // For xdg-open, don't wait - let it run in background } else { perror("fork failed"); } } return 0; // Clean up indexing thread if (indexing_started && !indexing_complete) { pthread_cancel(indexing_thread); } pthread_join(indexing_thread, NULL); // Clean up queue pthread_mutex_lock(&queue_mutex); for (int i = 0; i < queue_count; i++) { int idx = (queue_head + i) % DIR_QUEUE_CAPACITY; if (dir_queue[idx]) { free(dir_queue[idx]); dir_queue[idx] = NULL; } } pthread_mutex_unlock(&queue_mutex); // Execute the command if we have one // Clean up memory if (file_to_open) free(file_to_open); if (command) free(command); rzf_free_files(); rzf_free_bookmarks(); // Destroy mutexes pthread_mutex_destroy(&files_mutex); pthread_mutex_destroy(&queue_mutex); pthread_mutex_destroy(&git_queue_mutex); pthread_mutex_destroy(&git_root_mutex); pthread_mutex_destroy(&preview_mutex); pthread_cond_destroy(&queue_cond); return 0; }