/******************************************************************************* * * nseeker - Configurable ncurses file seeker / command launcher * * Usage: nseeker [args...] * * Navigate up and down through the options using "j" and "k". * go back to parent dir with "h". To run the command on a dir, * use the "e" key. * * Config file: ~/.config/nseeker/nseeker.cfg * * Supported options: * show_hidden = true|false * normal_fg = black|red|green|yellow|blue|magenta|cyan|white * normal_bg = black|red|green|yellow|blue|magenta|cyan|white * selected_fg = ... * selected_bg = ... * use_bold = true|false * * Author: yetimach * Date : 30 Dec 2025 * ******************************************************************************/ #include #include #include #include #include #include #include #include #include #include #define MAX_ENTRIES 1024 #define MAX_PATH 4096 #define MAX_CMD 1024 #define MAX_LINE 256 typedef struct { char name[256]; int is_dir; } Entry; Entry entries[MAX_ENTRIES]; int num_entries = 0; char current_path[MAX_PATH]; char full_command[MAX_CMD]; // Configurable options with defaults int show_hidden = 0; short normal_fg = COLOR_YELLOW; short normal_bg = COLOR_BLACK; short selected_fg = COLOR_GREEN; short selected_bg = COLOR_BLACK; int use_bold = 1; const char *color_names[] = { "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white" }; const int num_colors = 8; // Map color name to ncurses color code short name_to_color(const char *name) { for (int i = 0; i < num_colors; i++) { if (strcasecmp(name, color_names[i]) == 0) { return i; } } return -1; // invalid } // Trim whitespace and comments void trim_line(char *line) { // Remove trailing newline/comment char *comment = strchr(line, '#'); if (comment) *comment = '\0'; // Trim trailing whitespace char *end = line + strlen(line) - 1; while (end >= line && isspace(*end)) { *end = '\0'; end--; } // Trim leading whitespace char *start = line; while (isspace(*start)) start++; memmove(line, start, strlen(start) + 1); } // Load config from ~/.config/nseeker/nseeker.cfg // create default file if none exists void load_config() { const char *home = getenv("HOME"); if (!home) return; char config_dir[MAX_PATH]; char config_path[MAX_PATH]; snprintf(config_dir, sizeof(config_dir), "%s/.config/nseeker", home); if ((size_t)snprintf(config_path, sizeof(config_path), "%s/nseeker.cfg", config_dir) >= sizeof(config_path)) { // Extremely unlikely — path too long, fall back to defaults return; } FILE *fp = fopen(config_path, "r"); if (fp) { // Config exists — read it char line[MAX_LINE]; while (fgets(line, sizeof(line), fp)) { trim_line(line); if (line[0] == '\0') continue; char key[64], value[64]; if (sscanf(line, "%63[^= ] = %63s", key, value) == 2) { if (strcasecmp(key, "show_hidden") == 0) { show_hidden = (strcasecmp(value, "true") == 0 || atoi(value) != 0); } else if (strcasecmp(key, "use_bold") == 0) { use_bold = (strcasecmp(value, "true") == 0 || atoi(value) != 0); } else if (strcasecmp(key, "normal_fg") == 0) { short c = name_to_color(value); if (c != -1) normal_fg = c; } else if (strcasecmp(key, "normal_bg") == 0) { short c = name_to_color(value); if (c != -1) normal_bg = c; } else if (strcasecmp(key, "selected_fg") == 0) { short c = name_to_color(value); if (c != -1) selected_fg = c; } else if (strcasecmp(key, "selected_bg") == 0) { short c = name_to_color(value); if (c != -1) selected_bg = c; } } } fclose(fp); return; } // No config file — create directory and default config mkdir(config_dir, 0755); // Create ~/.config/nseeker if needed fp = fopen(config_path, "w"); if (!fp) return; // Can't write — oh well, use defaults silently fprintf(fp, "# nseeker configuration file\n" "# Created automatically on first run\n" "# Edit and restart nseeker to apply changes\n" "\n" "show_hidden = false\n" "use_bold = true\n" "\n" "normal_fg = yellow\n" "normal_bg = black\n" "selected_fg = green\n" "selected_bg = black\n" "\n" "# Available colors: black, red, green, yellow, blue, magenta, cyan, white\n" "\n" "# Example alternative themes (uncomment one block at a time):\n" "\n" "# Retro green terminal\n" "# normal_fg = green\n" "# normal_bg = black\n" "# selected_fg = black\n" "# selected_bg = green\n" "\n" "# High contrast\n" "# normal_fg = white\n" "# normal_bg = black\n" "# selected_fg = black\n" "# selected_bg = white\n" "\n" "# Solarized dark vibe\n" "# normal_fg = cyan\n" "# normal_bg = black\n" "# selected_fg = yellow\n" "# selected_bg = blue\n" ); fclose(fp); // Now that default config exists, we can just use the built-in defaults // (they match what's written to the file) } int entry_compare(const void *a, const void *b) { const Entry *ea = (const Entry *)a; const Entry *eb = (const Entry *)b; if (ea->is_dir != eb->is_dir) { return eb->is_dir - ea->is_dir; } return strcasecmp(ea->name, eb->name); } void load_directory(const char *path) { DIR *dir = opendir(path); if (!dir) { mvprintw(LINES-2, 0, "Cannot open directory: %s", strerror(errno)); refresh(); getch(); return; } num_entries = 0; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { if (!show_hidden && ent->d_name[0] == '.') { continue; } if (num_entries >= MAX_ENTRIES) break; strncpy(entries[num_entries].name, ent->d_name, sizeof(entries[num_entries].name) - 1); entries[num_entries].name[sizeof(entries[num_entries].name) - 1] = '\0'; char fullpath[MAX_PATH]; snprintf(fullpath, sizeof(fullpath), "%s/%s", path, ent->d_name); struct stat st; if (stat(fullpath, &st) == 0 && S_ISDIR(st.st_mode)) { entries[num_entries].is_dir = 1; } else { entries[num_entries].is_dir = 0; } num_entries++; } closedir(dir); qsort(entries, num_entries, sizeof(Entry), entry_compare); } void draw_list(int selected) { erase(); mvprintw(0, 0, "nseeker: %s", current_path); mvprintw(1, 0, "Command: %s", full_command); int max_display = LINES - 5; if (max_display < 1) max_display = 1; int start = selected - max_display / 2; if (start < 0) start = 0; if (start > num_entries - max_display) start = num_entries - max_display; if (start < 0) start = 0; for (int i = 0; i < max_display && (start + i) < num_entries; i++) { int idx = start + i; const char *name = entries[idx].name; const char *suffix = entries[idx].is_dir ? "/" : ""; if (idx == selected) { attron(COLOR_PAIR(2) | (use_bold ? A_BOLD : 0)); mvprintw(3 + i, 0, "> %s%s", name, suffix); attroff(COLOR_PAIR(2) | (use_bold ? A_BOLD : 0)); } else { attron(COLOR_PAIR(1) | (use_bold ? A_BOLD : 0)); mvprintw(3 + i, 0, " %s%s", name, suffix); attroff(COLOR_PAIR(1) | (use_bold ? A_BOLD : 0)); } } attron(use_bold ? A_BOLD : 0); mvprintw(LINES-2, 0, "j/k ↑↓: nav | Enter: open/run | e: run | h: back | q: quit"); attroff(use_bold ? A_BOLD : 0); clrtoeol(); refresh(); } // ... [get_selected_path, execute_command, go_back unchanged from previous version] ... void get_selected_path(int idx, char *out, size_t outsize) { snprintf(out, outsize, "%s/%s", current_path, entries[idx].name); } void execute_command(const char *target) { char cmd[MAX_CMD + MAX_PATH + 32]; snprintf(cmd, sizeof(cmd), "%s \"%s\"", full_command, target); endwin(); int ret = system(cmd); initscr(); erase(); if (ret == -1) { mvprintw(LINES/2, 0, "Failed to execute: %s", strerror(errno)); } else if (WEXITSTATUS(ret) != 0) { mvprintw(LINES/2, 0, "Command failed with status: %d", WEXITSTATUS(ret)); } else { mvprintw(LINES/2, 0, "Success!"); } mvprintw(LINES/2 + 2, 0, "Press any key..."); refresh(); getch(); } void go_back() { if (strcmp(current_path, "/") == 0) return; char *tmp = strdup(current_path); if (!tmp) return; char *parent = dirname(tmp); strncpy(current_path, parent, sizeof(current_path) - 1); current_path[sizeof(current_path) - 1] = '\0'; if (current_path[0] == '\0') strcpy(current_path, "/"); free(tmp); load_directory(current_path); } int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s [args...]\n", argv[0]); return 1; } // Build command full_command[0] = '\0'; for (int i = 1; i < argc; i++) { if (i > 1) strcat(full_command, " "); strncat(full_command, argv[i], MAX_CMD - strlen(full_command) - 1); } if (getcwd(current_path, sizeof(current_path)) == NULL) { perror("getcwd"); return 1; } // Load user config first load_config(); initscr(); curs_set(0); start_color(); use_default_colors(); // Allows -1 for default terminal colors if needed init_pair(1, normal_fg, normal_bg); init_pair(2, selected_fg, selected_bg); bkgd(COLOR_PAIR(1) | (use_bold ? A_BOLD : 0)); cbreak(); noecho(); keypad(stdscr, TRUE); load_directory(current_path); int selected = 0; int quit = 0; while (!quit && num_entries > 0) { draw_list(selected); int ch = getch(); switch (ch) { case 'q': case 'Q': quit = 1; break; case KEY_DOWN: case 'j': selected = (selected + 1) % num_entries; break; case KEY_UP: case 'k': selected = (selected + num_entries - 1) % num_entries; break; case 'h': go_back(); selected = 0; break; case 'e': case 'E': { char t[MAX_PATH]; get_selected_path(selected, t, sizeof(t)); execute_command(t); } break; case '\n': #ifdef KEY_ENTER case KEY_ENTER: #endif { char next[MAX_PATH]; get_selected_path(selected, next, sizeof(next)); struct stat st; int is_dir = (stat(next, &st) == 0 && S_ISDIR(st.st_mode)); if (!is_dir && lstat(next, &st) == 0 && S_ISLNK(st.st_mode)) is_dir = (stat(next, &st) == 0 && S_ISDIR(st.st_mode)); if (is_dir) { strcpy(current_path, next); load_directory(current_path); selected = 0; } else { endwin(); char cmd[MAX_CMD + MAX_PATH + 32]; snprintf(cmd, sizeof(cmd), "%s \"%s\"", full_command, next); system(cmd); return 0; } } break; default: break; } } endwin(); return 0; }