Simple File Selector in Curses
Here is a simple program that lets you select individual files from a tree view, and then the paths of the selected files are output to a file. The resulting file will typically be used as input for a completely different program.
Here is a screenshot:
Here is the code, which is based on the Directory Tree Diff Front End that I presented earlier:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <curses.h> typedef enum { FILE_NODE_TYPE_ROOT, FILE_NODE_TYPE_DIR, FILE_NODE_TYPE_FILE, } file_node_type_t; typedef struct file_node_s { int marked; char *name; file_node_type_t type; struct file_node_s *parent; unsigned int no_of_subnodes; struct file_node_s **subnode; } file_node_t; static int curses_scroll_offset = 0; static int curses_selected_entry = 0; static file_node_t *file_node_new(file_node_t *parent, char *name, file_node_type_t type) { int len; file_node_t *new; new = (file_node_t *)malloc(sizeof(file_node_t)); if (new == NULL) return NULL; if (name == NULL) { new->name = NULL; } else { len = strlen(name) + 1; new->name = (char *)malloc(len); strncpy(new->name, name, len); } new->type = type; new->parent = parent; new->no_of_subnodes = 0; new->subnode = NULL; new->marked = 0; return new; } static file_node_t *file_node_add(file_node_t *current, char *name, file_node_type_t type) { file_node_t *new; new = file_node_new(current, name, type); if (new == NULL) return NULL; if (current->subnode == NULL) { current->subnode = malloc(sizeof(file_node_t *)); } else { current->subnode = realloc(current->subnode, sizeof(file_node_t *) * (current->no_of_subnodes + 1)); } current->subnode[current->no_of_subnodes] = new; current->no_of_subnodes++; return new; } static void file_node_remove(file_node_t *node) { int i; free(node->name); for (i = 0; i < node->no_of_subnodes; i++) { file_node_remove(node->subnode[i]); } if (node->subnode != NULL) { free(node->subnode); } free(node); } static int file_node_depth(file_node_t *node) { int depth; depth = 0; do { if (node->type == FILE_NODE_TYPE_ROOT) { break; } node = node->parent; depth++; } while (node != NULL); return depth; } static int file_node_list_size(file_node_t *node) { int i, size; if (node->type == FILE_NODE_TYPE_ROOT) { size = 0; } else { size = 1; } for (i = 0; i < node->no_of_subnodes; i++) { size += file_node_list_size(node->subnode[i]); } return size; } static char *file_node_path(file_node_t *node, char *path, int path_len) { char temp[PATH_MAX]; strncpy(path, node->name, path_len); node = node->parent; while (node != NULL) { if (node->type == FILE_NODE_TYPE_ROOT) { break; } strncpy(temp, path, PATH_MAX); snprintf(path, path_len, "%s/%s", node->name, temp); node = node->parent; } return path; } static int file_node_compare(const void *p1, const void *p2) { file_node_t *p1p, *p2p; p1p = *((file_node_t **)p1); p2p = *((file_node_t **)p2); return strcmp(((file_node_t *)p1p)->name, ((file_node_t *)p2p)->name); } static void file_node_sort(file_node_t *node) { int i; qsort(node->subnode, node->no_of_subnodes, sizeof(file_node_t *), file_node_compare); for (i = 0; i < node->no_of_subnodes; i++) { file_node_sort(node->subnode[i]); } } static void file_node_dump(file_node_t *node) { int i, depth; depth = file_node_depth(node); while (depth-- > 1) printf(" "); if (node->type == FILE_NODE_TYPE_ROOT) printf("/"); else { printf("%s", node->name); if (node->type == FILE_NODE_TYPE_DIR) printf("/"); } printf("\n"); for (i = 0; i < node->no_of_subnodes; i++) { file_node_dump(node->subnode[i]); } } static void file_node_print_marked(file_node_t *node, char *root_dir, FILE *fh) { int i; char path[PATH_MAX]; if (node->marked) { fprintf(fh, "%s/%s\n", root_dir, file_node_path(node, path, PATH_MAX)); } for (i = 0; i < node->no_of_subnodes; i++) { file_node_print_marked(node->subnode[i], root_dir, fh); } } static int file_node_scan(file_node_t *current, char *path) { DIR *dh; struct dirent *entry; struct stat st; char fullpath[PATH_MAX]; file_node_t *subnode; dh = opendir(path); if (dh == NULL) { fprintf(stderr, "Error: Unable to open directory: %s\n", path); return -1; } while ((entry = readdir(dh))) { if (entry->d_name[0] == '.') continue; /* Ignore files with leading dot. */ snprintf(fullpath, PATH_MAX, "%s/%s", path, entry->d_name); if (stat(fullpath, &st) == -1) { fprintf(stderr, "Warning: Unable to stat() path: %s\n", fullpath); continue; } if (S_ISDIR(st.st_mode)) { subnode = file_node_add(current, entry->d_name, FILE_NODE_TYPE_DIR); file_node_scan(subnode, fullpath); } else if (S_ISREG(st.st_mode)) { file_node_add(current, entry->d_name, FILE_NODE_TYPE_FILE); } } closedir(dh); return 0; } static file_node_t *file_node_get_by_node_no(file_node_t *node, int node_no, int *node_count) { file_node_t *found; int i; if (node->type != FILE_NODE_TYPE_ROOT) { *node_count = *node_count + 1; } if (*node_count == node_no) { return node; /* Match found, return self. */ } for (i = 0; i < node->no_of_subnodes; i++) { found = file_node_get_by_node_no(node->subnode[i], node_no, node_count); if (found != NULL) { return found; } } return NULL; } static void file_node_mark(file_node_t *node, int node_no) { int node_count; file_node_t *found; node_count = 0; found = file_node_get_by_node_no(node, node_no, &node_count); if (found == NULL) return; if (found->type != FILE_NODE_TYPE_FILE) return; /* Only files, not directories, can be marked. */ if (found->marked == 0) { found->marked = 1; } else { found->marked = 0; } } static void curses_list_draw(file_node_t *node, int line_no, int node_no, int selected) { int node_count, maxy, maxx, pos, depth; file_node_t *found; node_count = 0; found = file_node_get_by_node_no(node, node_no, &node_count); if (found == NULL) return; getmaxyx(stdscr, maxy, maxx); if (selected) attron(A_REVERSE); if (found->name == NULL) { for (pos = 0; pos < maxx - 2; pos++) mvaddch(line_no, pos, ' '); } else { pos = 0; /* Depth indicator. */ depth = file_node_depth(found); while (depth-- > 1) { mvaddch(line_no, pos++, ' '); mvaddch(line_no, pos++, ' '); } /* File/directory name. */ if (found->marked) attron(A_BOLD); mvaddstr(line_no, pos, found->name); pos += strlen(found->name); if (found->marked) attroff(A_BOLD); /* Slash for directory. */ if (found->type == FILE_NODE_TYPE_DIR) mvaddch(line_no, pos++, '/'); /* Padding. */ for (; pos < maxx - 2; pos++) mvaddch(line_no, pos, ' '); } if (selected) attroff(A_REVERSE); } static void curses_update_screen(file_node_t *node) { int n, i, maxy, maxx; int scrollbar_size, scrollbar_pos; int list_size; list_size = file_node_list_size(node); getmaxyx(stdscr, maxy, maxx); erase(); /* Draw text lines. */ for (n = 0; n < maxy; n++) { if ((n + curses_scroll_offset) >= list_size) break; if (n == (curses_selected_entry - curses_scroll_offset)) { curses_list_draw(node, n, n + curses_scroll_offset + 1, 1); } else { curses_list_draw(node, n, n + curses_scroll_offset + 1, 0); } } /* Draw scrollbar. */ if (list_size <= maxy) scrollbar_size = maxy; else scrollbar_size = maxy / (list_size / (double)maxy); scrollbar_pos = curses_selected_entry / (double)list_size * (maxy - scrollbar_size); attron(A_REVERSE); for (i = 0; i <= scrollbar_size; i++) mvaddch(i + scrollbar_pos, maxx - 1, ' '); attroff(A_REVERSE); mvvline(0, maxx - 2, 0, maxy); /* Place cursor at end of selected line. */ move(curses_selected_entry - curses_scroll_offset, maxx - 3); } static void curses_exit_handler(void) { endwin(); } static void curses_winch_handler(file_node_t *node) { endwin(); /* To get new window limits. */ curses_update_screen(node); flushinp(); keypad(stdscr, TRUE); } static void file_node_curses_loop(file_node_t *node) { int c, maxy, maxx, list_size; initscr(); atexit(curses_exit_handler); noecho(); keypad(stdscr, TRUE); while (1) { list_size = file_node_list_size(node); curses_update_screen(node); getmaxyx(stdscr, maxy, maxx); c = getch(); switch (c) { case KEY_RESIZE: curses_winch_handler(node); break; case KEY_UP: curses_selected_entry--; if (curses_selected_entry < 0) curses_selected_entry++; if (curses_scroll_offset > curses_selected_entry) { curses_scroll_offset--; if (curses_scroll_offset < 0) curses_scroll_offset = 0; } break; case KEY_NPAGE: curses_scroll_offset += maxy / 2; while (maxy + curses_scroll_offset > list_size) curses_scroll_offset--; if (curses_scroll_offset < 0) curses_scroll_offset = 0; if (curses_selected_entry < curses_scroll_offset) curses_selected_entry = curses_scroll_offset; break; case KEY_PPAGE: curses_scroll_offset -= maxy / 2; if (curses_scroll_offset < 0) curses_scroll_offset = 0; if (curses_selected_entry > maxy + curses_scroll_offset - 1) curses_selected_entry = maxy + curses_scroll_offset - 1; break; case ' ': case KEY_IC: file_node_mark(node, curses_selected_entry + 1); /* Move cursor to next line automatically. */ case KEY_ENTER: case '\n': case '\r': case KEY_DOWN: curses_selected_entry++; if (curses_selected_entry >= list_size) curses_selected_entry--; if (curses_selected_entry > maxy - 1) { curses_scroll_offset++; if (curses_scroll_offset > curses_selected_entry - maxy + 1) curses_scroll_offset--; } break; case '\e': /* Escape */ case 'Q': case 'q': return; } } } int main(int argc, char *argv[]) { file_node_t *root; char *root_dir; FILE *fh; if (argc < 2) { fprintf(stderr, "Usage: %s <output file> [directory]\n", argv[0]); return 1; } fh = fopen(argv[1], "wx"); if (fh == NULL) { fprintf(stderr, "Error: Cannot open file, or it exists already: %s\n", argv[1]); return 1; } if (argc > 2) { root_dir = argv[2]; } else { root_dir = "."; } root = file_node_new(NULL, NULL, FILE_NODE_TYPE_ROOT); if (root == NULL) { fclose(fh); return 1; } if (file_node_scan(root, root_dir) != 0) { file_node_remove(root); fclose(fh); return 1; } file_node_sort(root); if (isatty(STDOUT_FILENO)) { file_node_curses_loop(root); file_node_print_marked(root, root_dir, fh); } else { /* Mostly for debugging. */ file_node_dump(root); } file_node_remove(root); fclose(fh); return 0; }