#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <ncurses.h>
#include <pthread.h>
#include <regex.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
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(&regex, pattern, REG_EXTENDED | REG_ICASE);
if (ret != 0)
return false;
ret = regexec(&regex, text, 0, NULL, 0);
regfree(&regex);
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;
}