#include "config.h"
#include <ctype.h>
#include <fcntl.h>
#include <ncurses.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define key_ctrl(x) ((x) & 0x1f)
#define LAST_ACTION_SIZE 2048
#define PROMPT_WIN_HEIGHT 10
#define PROMPT_WIN_WIDTH 80
#define MSG_WIN_HEIGHT 5
#define MSG_WIN_WIDTH 60
#define MAX_LINE 2048
#define INFO_BAR_PADDING 20
#define ENTRIES_PER_PAGE 20
typedef enum {
file_reg,
file_dir,
file_exec,
file_link,
} file_type;
typedef struct ls_entry {
char *full_line;
char *prefix;
char *fname;
file_type type;
} ls_entry;
void trim_newline(char *s);
void trim_executable_mark(char *s);
int parse_ls_line(char *line, ls_entry *entry);
int load_ls_entries(const char *path, ls_entry ***entries_out);
void free_ls_entry(ls_entry *entry);
void free_ls_entries(ls_entry **entries, int count);
const char *file_type_str(int type);
void show_help(void);
int confirm_box(const char *msg);
int prompt_input(const char *prompt, char *buffer, int buf_size);
void show_message(const char *msg);
void run_executable(char *file_path);
void run_silent(char *file_path);
static char last_action[LAST_ACTION_SIZE] = "";
void trim_newline(char *s) {
char *p = strchr(s, '\n');
if (p) {
*p = '\0';
}
}
void trim_executable_mark(char *s) {
char *p = strchr(s, '*');
if (p) {
*p = '\0';
}
}
int parse_ls_line(char *line, ls_entry *entry) {
entry->full_line = strdup(line);
char perm[32], links[32], owner[64], group[64], size[64], month[32], day[32], time_year[32];
int offset = 0;
int n = sscanf(line, "%31s %31s %63s %63s %63s %31s %31s %31s %n",
perm, links, owner, group, size, month, day, time_year, &offset);
if (n < 8) {
return -1;
}
while (line[offset] && isspace((unsigned char)line[offset])) {
offset++;
}
entry->fname = strdup(line + offset);
int prefix_len = offset;
entry->prefix = malloc(prefix_len + 1);
strncpy(entry->prefix, line, prefix_len);
entry->prefix[prefix_len] = '\0';
if (perm[0] == 'd') {
entry->type = file_dir;
} else if (perm[0] == 'l') {
entry->type = file_link;
} else if (perm[0] == '-' && strchr(perm, 'x') != NULL) {
entry->type = file_exec;
} else {
entry->type = file_reg;
}
return 0;
}
int load_ls_entries(const char *path, ls_entry ***entries_out) {
char command[256];
int ret = snprintf(command, sizeof(command), LS_COMMAND " %s", path);
if (ret < 0 || (size_t)ret >= sizeof(command)) {
fprintf(stderr, "Command buffer too small.\n");
return -1;
}
FILE *fp = popen(command, "r");
if (!fp) {
return -1;
}
ls_entry **entries = NULL;
int capacity = 20, count = 0;
entries = malloc(capacity * sizeof(ls_entry *));
char line[MAX_LINE];
if (fgets(line, sizeof(line), fp) != NULL) {
if (strncmp(line, "total", 5) != 0) {
trim_newline(line);
ls_entry *entry = malloc(sizeof(ls_entry));
if (parse_ls_line(line, entry) == 0) {
entries[count++] = entry;
} else {
free(entry);
}
}
}
while (fgets(line, sizeof(line), fp) != NULL) {
trim_newline(line);
if (strlen(line) == 0) {
continue;
}
if (count >= capacity) {
capacity *= 2;
entries = realloc(entries, capacity * sizeof(ls_entry *));
}
ls_entry *entry = malloc(sizeof(ls_entry));
if (parse_ls_line(line, entry) == 0) {
entries[count++] = entry;
} else {
free(entry);
}
}
pclose(fp);
*entries_out = entries;
return count;
}
void free_ls_entry(ls_entry *entry) {
if (entry) {
free(entry->full_line);
free(entry->prefix);
free(entry->fname);
free(entry);
}
}
void free_ls_entries(ls_entry **entries, int count) {
for (int i = 0; i < count; i++) {
free_ls_entry(entries[i]);
}
free(entries);
}
const char *file_type_str(int type) {
switch (type) {
case file_dir:
return "DIRECTORY";
break;
case file_exec:
return "EXECUTABLE";
break;
case file_link:
return "SYMLINK";
break;
default:
return "REGULAR";
}
}
void show_help(void) {
clear();
mvprintw(1, 2, "Key Bindings:");
mvprintw(3, 4, "%c : Jump to a line", KEY_JUMP);
mvprintw(4, 4, "%c : Next page", KEY_NEXT_PAGE);
mvprintw(5, 4, "%c : Previous page", KEY_PREV_PAGE);
mvprintw(6, 4, "%c : Rename file", KEY_RENAME_2);
mvprintw(7, 4, "%c : Delete file", KEY_DELETE_2);
mvprintw(8, 4, "%c : Search file", KEY_SEARCH_1);
mvprintw(10, 4, "%c : Run command", KEY_RUN_CMD);
mvprintw(11, 4, "%c : mkdir", KEY_MKDIR);
mvprintw(12, 4, "%c : create file", KEY_TOUCH);
mvprintw(13, 4, "%c : open in a new terminal", KEY_TERM_OPEN);
mvprintw(14, 4, "%c : open location in a new terminal window", KEY_OPEN_LOCATION);
mvprintw(15, 4, "%c : Show help", KEY_SHOW_HELP);
mvprintw(16, 4, "%c : Quit", KEY_QUIT);
mvprintw(LINES - 2, 2, "Press any key to return.");
refresh();
getch();
}
int confirm_box(const char *msg) {
int height = 10, width = 60;
int starty = (LINES - height) / 2, startx = (COLS - width) / 2;
WINDOW *win = newwin(height, width, starty, startx);
box(win, 0, 0);
mvwprintw(win, 2, 2, "%s (y/n)", msg);
wrefresh(win);
int ch, confirmed = 0;
while ((ch = wgetch(win))) {
if (ch == 'y' || ch == 'Y') {
confirmed = 1;
break;
} else if (ch == 'n' || ch == 'N') {
break;
}
}
delwin(win);
return confirmed;
}
void show_message(const char *msg) {
int height = MSG_WIN_HEIGHT, width = MSG_WIN_WIDTH;
int starty = (LINES - height) / 2, startx = (COLS - width) / 2;
WINDOW *win = newwin(height, width, starty, startx);
box(win, 0, 0);
mvwprintw(win, 2, 2, "%s", msg);
wrefresh(win);
wgetch(win);
delwin(win);
}
int prompt_input(const char *prompt, char *buffer, int buf_size) {
int height = PROMPT_WIN_HEIGHT, width = PROMPT_WIN_WIDTH;
int starty = (LINES - height) / 2, startx = (COLS - width) / 2;
WINDOW *win = newwin(height, width, starty, startx);
box(win, 0, 0);
mvwprintw(win, 1, 2, "%s", prompt);
mvwprintw(win, 3, 2, "Entry: ");
wrefresh(win);
curs_set(1);
int ch, pos = 0;
memset(buffer, 0, buf_size);
while (1) {
ch = wgetch(win);
if (ch == 27) {
buffer[0] = '\0';
break;
} else if (ch == '\n') {
break;
} else if (ch == KEY_BACKSPACE || ch == 127) {
if (pos > 0) {
pos--;
buffer[pos] = '\0';
mvwprintw(win, 3, 12, "%-*s", buf_size - 12, " ");
mvwprintw(win, 3, 12, "%s", buffer);
box(win, 0, 0);
wrefresh(win);
}
} else if (pos < buf_size - 1 && isprint(ch)) {
buffer[pos++] = ch;
buffer[pos] = '\0';
mvwprintw(win, 3, 12, "%s", buffer);
wrefresh(win);
}
}
curs_set(0);
delwin(win);
return 0;
}
void run_executable(char *file_path) {
if (confirm_box("Open this file?")) {
clear();
refresh();
endwin();
trim_executable_mark(file_path);
printf("Running: %s\n", file_path);
int status = system(file_path);
printf("Process exited with status %d\n", status);
printf("Press Enter to return...\n");
getchar();
initscr();
cbreak();
noecho();
keypad(stdscr, TRUE);
curs_set(0);
}
}
void run_silent(char *file_path) {
trim_executable_mark(file_path);
system(file_path);
}
int main(void) {
char current_path[1024] = ".";
int ch, selected = 0, page = 0;
int num_entries = 0;
ls_entry **entries = NULL;
initscr();
cbreak();
noecho();
keypad(stdscr, TRUE);
curs_set(0);
start_color();
use_default_colors();
init_pair(1, COLOR_DIRECTORY, COLOR_BLACK);
init_pair(2, COLOR_EXECUTABLE, COLOR_BLACK);
init_pair(3, COLOR_REGULAR, COLOR_BLACK);
init_pair(4, COLOR_SYMLINK, COLOR_BLACK);
num_entries = load_ls_entries(current_path, &entries);
if (num_entries < 0) {
endwin();
fprintf(stderr, "Failed to load directory entries.\n");
exit(EXIT_FAILURE);
}
while (1) {
clear();
mvprintw(0, 0, "%s", last_action);
int total_pages = (num_entries + ENTRIES_PER_PAGE - 1) / ENTRIES_PER_PAGE;
int start_index = page * ENTRIES_PER_PAGE;
int end_index = start_index + ENTRIES_PER_PAGE;
if (end_index > num_entries) {
end_index = num_entries;
}
for (int i = start_index; i < end_index; i++) {
if (i == selected) {
attron(A_REVERSE);
}
mvprintw(i - start_index + 1, 0, "[%2d]", i);
int col = 4 + snprintf(NULL, 0, "[%2d]", i);
mvprintw(i - start_index + 1, col, "%s", entries[i]->prefix);
col += strlen(entries[i]->prefix);
switch (entries[i]->type) {
case file_dir: {
attron(COLOR_PAIR(1));
break;
}
case file_exec: {
attron(COLOR_PAIR(2));
break;
}
case file_link: {
attron(COLOR_PAIR(4));
break;
}
default: {
attron(COLOR_PAIR(3));
break;
}
}
mvprintw(i - start_index + 1, col, "%s", entries[i]->fname);
attroff(COLOR_PAIR(1));
attroff(COLOR_PAIR(2));
attroff(COLOR_PAIR(3));
attroff(COLOR_PAIR(4));
if (i == selected) {
attroff(A_REVERSE);
}
}
char info_bar[100];
int ret = snprintf(info_bar, sizeof(info_bar), "INFO: %-*s | Page (%d/%d)",
INFO_BAR_PADDING, file_type_str(entries[selected]->type),
page + 1, total_pages);
if (ret < 0 || (size_t)ret >= sizeof(info_bar)) {
info_bar[sizeof(info_bar) - 1] = '\0';
}
mvprintw(LINES - 2, 0, "%s", info_bar);
mvprintw(LINES - 1, 0, "q: Quit | h: Help | r: Rename | d: Delete | n: Next | p: Prev "
"| m: mkdir | t: touch | x: Run Command | z: Run in a new window");
refresh();
ch = getch();
if (ch == KEY_QUIT) {
if (confirm_box("Are you sure you want to quit?")) {
break;
}
} else if (ch == KEY_UP && selected > 0) {
selected--;
if (selected < start_index) {
page--;
}
} else if (ch == KEY_DOWN && selected < num_entries - 1) {
selected++;
if (selected >= end_index) {
page++;
}
} else if (ch == KEY_NEXT_PAGE) {
if (page < total_pages - 1) {
page++;
selected = page * ENTRIES_PER_PAGE;
}
} else if (ch == KEY_PREV_PAGE) {
if (page > 0) {
page--;
selected = page * ENTRIES_PER_PAGE;
}
} else if (ch == KEY_JUMP) {
echo();
char num_str[10];
mvprintw(LINES - 3, 0, "Jump to line: ");
getnstr(num_str, sizeof(num_str) - 1);
noecho();
int jump = atoi(num_str);
if (jump >= 0 && jump < num_entries) {
selected = jump;
page = jump / ENTRIES_PER_PAGE;
}
} else if (ch == KEY_SEARCH_1 || ch == KEY_SEARCH_2) {
char search_query[256] = {0};
prompt_input("Search: ", search_query, sizeof(search_query));
if (strlen(search_query) > 0) {
int found = 0;
for (int i = 0; i < num_entries; i++) {
if (strcasestr(entries[i]->fname, search_query) != NULL) {
selected = i;
page = i / ENTRIES_PER_PAGE;
found = 1;
break;
}
}
if (!found) {
show_message("No matching file found. Press any key.");
}
}
} else if (ch == '\n') {
if ((entries[selected]->type == file_dir) ||
(strcmp(entries[selected]->fname, "../") == 0)) {
char new_path[1024];
int ret2 = snprintf(new_path, sizeof(new_path), "%s/%s", current_path, entries[selected]->fname);
if (ret2 < 0 || (size_t)ret2 >= sizeof(new_path)) {
new_path[sizeof(new_path) - 1] = '\0';
}
if (chdir(new_path) == 0) {
if (realpath(".", current_path) == NULL) {
perror("realpath");
break;
}
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries) {
selected = num_entries - 1;
}
page = selected / ENTRIES_PER_PAGE;
}
} else {
char *ext = strrchr(entries[selected]->fname, '.');
char command[1024];
if (entries[selected]->type == file_exec) {
char exec_path[2048];
snprintf(exec_path, sizeof(exec_path), "%s/%s", current_path, entries[selected]->fname);
run_executable(exec_path);
} else if ((ext && strcasecmp(ext, ".png") == 0) ||
(ext && strcasecmp(ext, ".jpeg") == 0) ||
(ext && strcasecmp(ext, ".jpg") == 0) ||
(ext && strcasecmp(ext, ".gif") == 0)) {
snprintf(command, sizeof(command), IMAGE_VIEWER_COMMAND, entries[selected]->fname);
run_executable(command);
} else if ((ext && strcasecmp(ext, ".mp4") == 0) ||
(ext && strcasecmp(ext, ".mov")) == 0) {
snprintf(command, sizeof(command), VIDEO_PLAYER_COMMAND, entries[selected]->fname);
run_executable(command);
} else if ((ext && strcasecmp(ext, ".mp3")) == 0 ||
(ext && strcasecmp(ext, ".ogg")) == 0 ||
(ext && strcasecmp(ext, ".wav")) == 0) {
snprintf(command, sizeof(command), AUDIO_PLAYER_COMMAND, entries[selected]->fname);
run_executable(command);
} else {
snprintf(command, sizeof(command), "xdg-open %s", entries[selected]->fname);
run_executable(command);
}
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
}
} else if (ch == KEY_RENAME_1 || ch == KEY_RENAME_2) {
char new_name[256] = {0};
prompt_input("Rename file: ", new_name, sizeof(new_name));
if (strlen(new_name) > 0) {
char old_filename[512];
strncpy(old_filename, entries[selected]->fname, sizeof(old_filename));
old_filename[sizeof(old_filename) - 1] = '\0';
if (entries[selected]->type == file_exec) {
size_t len = strlen(old_filename);
if (len > 0 && old_filename[len - 1] == '*') {
old_filename[len - 1] = '\0';
}
}
char confirm_msg[512];
int ret = snprintf(confirm_msg, sizeof(confirm_msg),
"Rename '%.50s' to '%.50s'?", old_filename, new_name);
if (ret < 0 || (size_t)ret >= sizeof(confirm_msg)) {
confirm_msg[sizeof(confirm_msg) - 1] = '\0';
}
if (confirm_box(confirm_msg)) {
char old_path[1024], new_path[1024];
int ret4 = snprintf(old_path, sizeof(old_path), "%s/%s", current_path, old_filename);
if (ret4 < 0 || (size_t)ret4 >= sizeof(old_path)) {
old_path[sizeof(old_path) - 1] = '\0';
}
int ret5 = snprintf(new_path, sizeof(new_path), "%s/%s", current_path, new_name);
if (ret5 < 0 || (size_t)ret5 >= sizeof(new_path)) {
new_path[sizeof(new_path) - 1] = '\0';
}
if (rename(old_path, new_path) == 0) {
snprintf(last_action, LAST_ACTION_SIZE, "Renamed '%.50s' to '%.50s'", old_filename, new_name);
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
}
}
}
} else if (ch == KEY_DELETE_1 || ch == KEY_DELETE_2) {
if (confirm_box("Confirm delete?")) {
char del_path[2048];
snprintf(del_path, sizeof(del_path), "%s/%s", current_path, entries[selected]->fname);
trim_executable_mark(del_path);
if (remove(del_path) == 0) {
snprintf(last_action, LAST_ACTION_SIZE, "Deleted '%.50s'", entries[selected]->fname);
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
} else {
endwin();
printf("path sent%s\n", del_path);
perror("remove()\n");
}
}
} else if (ch == KEY_RUN_CMD) {
char cmd[256] = {0};
prompt_input("Run command: ", cmd, sizeof(cmd));
if (strlen(cmd) > 0) {
int status = system(cmd);
snprintf(last_action, LAST_ACTION_SIZE, "Ran '%.50s' (status %d)", cmd, status);
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
}
} else if (ch == KEY_MKDIR) {
char dir_name[256] = {0};
prompt_input("mkdir: ", dir_name, sizeof(dir_name));
if (strlen(dir_name) > 0) {
if (mkdir(dir_name, 0755) == 0) {
snprintf(last_action, LAST_ACTION_SIZE, "Created directory '%s'", dir_name);
} else {
snprintf(last_action, LAST_ACTION_SIZE, "mkdir failed for '%s'", dir_name);
}
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
}
} else if (ch == KEY_TOUCH) {
char file_name[256] = {0};
prompt_input("Touch: ", file_name, sizeof(file_name));
if (strlen(file_name) > 0) {
int fd = open(file_name, O_CREAT | O_WRONLY, 0644);
if (fd != -1) {
close(fd);
snprintf(last_action, LAST_ACTION_SIZE, "Created file '%s'", file_name);
} else {
snprintf(last_action, LAST_ACTION_SIZE, "Touch failed for '%s'", file_name);
}
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
}
} else if (ch == KEY_RELOAD) {
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
} else if (ch == KEY_GO_UP) {
if (chdir("..") == 0) {
if (realpath(".", current_path) == NULL) {
perror("realpath");
break;
}
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
}
} else if (ch == KEY_TERM_OPEN) {
if (entries[selected]->type == file_exec) {
char command[2048];
snprintf(command, sizeof(command), TERM_OPEN_COMMAND, entries[selected]->fname);
run_executable(command);
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
if (selected >= num_entries)
selected = num_entries - 1;
page = selected / ENTRIES_PER_PAGE;
}
else {
show_message("File must be executable.");
}
} else if (ch == KEY_OPEN_LOCATION) {
char command[2048];
snprintf(command, sizeof(command), TERM_OPEN_LOCATION_COMMAND " > /dev/null 2>&1 &", current_path);
run_silent(command);
} else if (ch == KEY_SHOW_HELP) {
show_help();
}
else if (ch == KEY_COPY_PATH) {
char command[2048];
snprintf(command, sizeof(command), CLIPBOARD_COMMAND, current_path);
run_silent(command);
snprintf(last_action, LAST_ACTION_SIZE, "Copied '%s' (current path) in to clipboard.", current_path);
}
else if (ch == KEY_GOTO_PATH) {
char new_path[1024];
prompt_input("Go to: ", new_path, sizeof(new_path));
if (strcmp(new_path, "~") == 0) {
snprintf(new_path, sizeof(new_path), CUSTOM_HOME_PATH);
}
if (chdir(new_path) == 0) {
if (realpath(".", current_path) == NULL) {
perror("realpath");
break;
}
free_ls_entries(entries, num_entries);
num_entries = load_ls_entries(current_path, &entries);
}
snprintf(last_action, LAST_ACTION_SIZE, "Moved to %s", new_path);
}
}
free_ls_entries(entries, num_entries);
endwin();
return 0;
}