|
#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(®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;
|
|
}
|