commit 807ce8cad692a84b92066ea2c76363251a3c929f Author: retoor Date: Sat Aug 2 12:57:44 2025 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..470eff0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +main_* + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a87e61f --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# Compiler +CC = gcc + +# Compiler flags +CFLAGS = -Wall -Wextra -pedantic -L/usr/local/lib -lncurses -lncursesw -ltinfo -ldl -lpthread -static + +# Executable name +TARGET = rzf + +# Source files +SRCS = main.c + +# Object files +OBJS = $(SRCS:.c=.o) + +# Default target +all: $(TARGET) + +# Build the executable +$(TARGET): $(OBJS) + $(CC) -o $@ $^ $(CFLAGS) + +# Clean up build files +clean: + rm -f $(TARGET) $(OBJS) + +.PHONY: all clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..9917bf5 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# RZF - The Ultimate Terminal File Explorer and Search Tool + +Welcome to **RZF**, the most powerful, fast, and versatile terminal-based file explorer and search utility designed for every terminal warrior. Built with performance in mind, leveraging C's maximum efficiency, RZF is your go-to tool for managing and navigating millions of files seamlessly. + +Development is mostly done manually, but later completed and refactored with several LLM's (Gemini Pro, ChatGPT Plus and Claude Pro). + +--- + +## Key Features + +- **Lightning-fast file indexing** capable of handling millions of files without breaking a sweat. +- **Smart directory skipping**: Automatically skips directories like `venv`, `node_modules`, and other common variants to speed up searches. +- **Powerful search modes**: + - Fuzzy search for quick, approximate matching. + - Regex search for precise pattern matching. +- **File filtering**: + - Filter by extension (e.g., `:py`, `:img`, `:doc`). + - Show only bookmarked files or all files. +- **Favorites and Bookmarks**: + - Easily add or remove bookmarks. + - Quick access to favorite files. +- **Preview Mode**: + - Toggle preview panel to view file contents or directory listings without leaving the interface. + - Supports binary and text files, directories, and images. +- **Clipboard Integration**: + - Copy file paths with `Ctrl+X`. + - Copy file contents directly to clipboard with `Ctrl+Y` — perfect for sharing snippets or feeding into an LLM. +- **Backup Files**: + - Quick backups with `Ctrl+B`, appending timestamps for safety. Extension ends with .bak which you can list in your .gitignore. +- **File Operations**: + - Delete files or directories with confirmation. + - Recursive delete for directories. + - Open files or directories with `xdg-open`. +- **Navigation**: + - Go to parent directory (`Ctrl+U`). + - Change sort modes (`Ctrl+V`). (V is Dutch for volgorde) + - Toggle between sorting by name, size, or date. +- **Search History & Command Prompt**: + - Recall previous searches. + - Run custom commands on selected files. +- **User-friendly shortcuts**: + - Help menu with all shortcuts. + - Toggle bookmarks, search modes, and preview with simple keystrokes. +- **Static Compilation**: + - Fully static binary for maximum portability. + - Designed to work out-of-the-box on most Linux systems. +- **Lightweight & Efficient**: + - Minimal dependencies. + - Designed for maximum performance and safety. + +--- + +## Shortcuts & Usage + +| Shortcut | Description | +| --- | --- | +| **Arrow Up / Arrow Down** | Navigate through files and directories | +| **Enter** | Open file or directory | +| **Ctrl+X** | Copy selected file path to clipboard | +| **Ctrl+Y** | Copy file contents to clipboard (great for sharing snippets or LLM input) | +| **Ctrl+B** | Backup selected file with timestamp (safe and quick) | +| **Ctrl+D** | Delete selected file or directory (with confirmation) | +| **Ctrl+U** | Go to parent directory | +| **Ctrl+V** | Toggle sort mode (Name, Size, Date) | +| **Ctrl+P** | Toggle preview mode to view file contents or directory listing | +| **Ctrl+S** | Toggle bookmark for selected file | +| **Ctrl+F** | Show only bookmarked files | +| **Ctrl+R** | Search history navigation | +| **Ctrl+E** | Toggle regex search mode | +| **?** | Show help menu with all shortcuts | +| **Ctrl+C / Esc** | Exit the application | + +--- + +## Tips & Recommendations + +- **Installation**: + - Copy the binary to `/usr/local/bin` for easy global access: + ```bash + sudo cp rzf /usr/local/bin/ + ``` + - Add a bash shortcut for quick launching: + ```bash + bind -x '"\C-f": rzf' + ``` +- **Performance & Compatibility**: + - The binary is statically compiled, ensuring maximum compatibility and performance. + - It should work on most Linux distributions out-of-the-box. + - If you encounter issues or want to compile manually: + - Remove the `-static` parameter in the Makefile. + - Ensure `ncurses-dev` is installed (`sudo apt install libncurses-dev`). +- **Development Setup**: + - Setting up the environment is a breeze and can be done within 5 minutes. + - The code is manually written to optimize performance and usability. +- **Use Cases**: + - Perfect for developers, sysadmins, and terminal enthusiasts. + - Ideal for managing large codebases, quick file access, and integrating with LLMs. + - The copy-to-clipboard feature (`Ctrl+Y`) makes it easy to extract snippets or file contents for sharing or AI processing. + - Backup files instantly with `Ctrl+B` — a lifesaver when experimenting or editing. + +--- + +## Why RZF? + +- **Performance**: Fully utilizes C's speed to handle millions of files effortlessly. +- **Safety**: Recursive delete with confirmation, safe backups, and manual code ensure reliability. +- **Convenience**: All essential features are accessible via intuitive shortcuts. +- **Portability**: Static binary means no dependencies or complex setup. +- **Manual Craftsmanship**: A lot of the code is handcrafted to deliver a polished, efficient experience. Unlike this README.md :P + +--- + +## Final Notes + +This project is the culmination of meticulous manual coding, optimized for speed, safety, and user experience. It’s a big time-saver and a powerful addition to any terminal user’s toolkit. Whether you're navigating vast codebases or managing files on a server, **RZF** is your ultimate terminal companion. + +--- + +## Support & Feedback + +While I believe the application is now feature-complete and polished, I am always available for support and feedback. Feel free to reach out for assistance or suggestions. + +--- + +Enjoy your terminal mastery with **RZF** — the ultimate tool for every terminal warrior! diff --git a/main.c b/main.c new file mode 100644 index 0000000..b476a56 --- /dev/null +++ b/main.c @@ -0,0 +1,1547 @@ +#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_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); + 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) { + *selected_file_path = strdup(files[file_idx].path); + command = strdup(files[file_idx].is_dir ? "xdg-open" : "vim"); + } + pthread_mutex_unlock(&files_mutex); + } + goto end_loop; + 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 (indexing_started && !indexing_complete) { + pthread_cancel(indexing_thread); + } + pthread_join(indexing_thread, NULL); + + 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); + + if (command && file_to_open) { + reset_shell_mode(); + pid_t pid = fork(); + if (pid == 0) { + execlp(command, command, file_to_open, NULL); + perror("execlp failed"); + exit(EXIT_FAILURE); + } else if (pid > 0) { + wait(NULL); + } else { + perror("fork failed"); + } + free(file_to_open); + } + if (command) free(command); + + rzf_free_files(); + rzf_free_bookmarks(); + 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; +} diff --git a/main.o b/main.o new file mode 100644 index 0000000..25c6c9e Binary files /dev/null and b/main.o differ diff --git a/rzf b/rzf new file mode 100755 index 0000000..0f093d4 Binary files /dev/null and b/rzf differ