diff --git a/include/justui/jfileselect.h b/include/justui/jfileselect.h index ea63cf5..bddfe0a 100644 --- a/include/justui/jfileselect.h +++ b/include/justui/jfileselect.h @@ -27,13 +27,14 @@ typedef struct { /* Folder currently being browsed */ char *path; - /* Corresponding directory stream */ - DIR *dp; - /* Entry previously validated (with EXE) */ + /* List of entries */ + void *entries; + /* Number of entries in the current folder */ + int entry_count; + + /* Full path to file last selected with EXE */ char *selected_file; - /* Number of entries in the current folder */ - int folder_entries; /* Current cursor position (0 .. folder_entries-1) */ int16_t cursor; /* Current scroll position */ @@ -43,6 +44,8 @@ typedef struct { /* Additional pixels of spacing per line (base is font->height) */ int8_t line_spacing; + /* Whether to show the file size on the right */ + bool show_file_size; /* Rendering font */ font_t const *font; @@ -84,5 +87,6 @@ char const *jfileselect_current_folder(jfileselect *fs); /* Trivial properties */ void jfileselect_set_font(jfileselect *fs, font_t const *font); void jfileselect_set_line_spacing(jfileselect *fs, int line_spacing); +void jfileselect_set_show_file_size(jfileselect *fs, bool show_file_size); #endif /* _J_JFILESELECT */ diff --git a/src/jfileselect.c b/src/jfileselect.c index 4a8fec8..526cc17 100644 --- a/src/jfileselect.c +++ b/src/jfileselect.c @@ -8,6 +8,7 @@ #include #include #include +#include /* Type identifier for jfileselect */ static int jfileselect_type_id = -1; @@ -17,6 +18,20 @@ uint16_t JFILESELECT_LOADED; uint16_t JFILESELECT_VALIDATED; uint16_t JFILESELECT_CANCELED; +/* We can try pretty hard to not duplicate the information held by the + directory descriptor, which is already a full array of all entries on gint. + However, the standard API behind readdir(3) does not allow us any complex + operations; no filtering, no sorting, no accessing auxiliary data such as + the file size. So we do this manually. */ +struct fileinfo { + /* Entry name (owned by the structure) */ + char *name; + /* File size in bytes if file, number of entries if folder */ + int size; + /* Type from [struct dirent] */ + int type; +}; + jfileselect *jfileselect_create(void *parent) { if(jfileselect_type_id < 0) return NULL; @@ -27,16 +42,18 @@ jfileselect *jfileselect_create(void *parent) jwidget_init(&fs->widget, jfileselect_type_id, parent); fs->path = NULL; - fs->dp = NULL; + fs->entries = NULL; + fs->entry_count = 0; + fs->selected_file = NULL; - fs->folder_entries = -1; fs->cursor = -1; fs->scroll = 0; fs->visible_lines = 0; fs->line_spacing = 4; fs->font = dfont_default(); + fs->show_file_size = false; return fs; } @@ -48,6 +65,18 @@ static void count_visible_lines(jfileselect *fs) fs->visible_lines = ch / line_height; } +static void set_finfo(jfileselect *fs, struct fileinfo *finfo, int n) +{ + struct fileinfo *old = fs->entries; + if(old) { + for(int i = 0; i < fs->entry_count; i++) + free(old[i].name); + free(old); + } + fs->entries = finfo; + fs->entry_count = n; +} + //--- // Getters and setters //--- @@ -64,6 +93,12 @@ void jfileselect_set_line_spacing(jfileselect *fs, int line_spacing) count_visible_lines(fs); } +void jfileselect_set_show_file_size(jfileselect *fs, bool show_file_size) +{ + fs->show_file_size = show_file_size; + fs->widget.update = true; +} + //--- // Path and folder manipulation //--- @@ -97,58 +132,118 @@ static char *path_up(char const *path) return parent; } -static int accept_entry(struct dirent *ent) +static bool accept_entry(struct dirent *ent) { /* TODO: jfileselect: Programmable filter */ if(!strcmp(ent->d_name, "@MainMem")) - return 0; + return false; if(!strcmp(ent->d_name, "SAVE-F")) - return 0; + return false; if(!strcmp(ent->d_name, ".")) - return 0; + return false; if(!strcmp(ent->d_name, "..")) - return 0; - return 1; + return false; + return true; } -struct dirent *read_nth_dir_entry(DIR *dp, int n) +static int count_accepted_entries(DIR *dp) { - struct dirent *entry = NULL; + int n = 0; + struct dirent *ent; + rewinddir(dp); + while((ent = readdir(dp))) + n += accept_entry(ent); - for(int i = 0; i <= n;) { - entry = readdir(dp); - if(!entry) - return NULL; - i += accept_entry(entry); - } - - return entry; + return n; } -static bool load_folder(jfileselect *fs, char *path) +static int compare_entries(void const *i1_0, void const *i2_0) { - if(fs->dp) - closedir(fs->dp); + struct fileinfo const *i1 = i1_0, *i2 = i2_0; + int d1 = (i1->type == DT_DIR); + int d2 = (i2->type == DT_DIR); - fs->dp = (DIR *)gint_world_switch(GINT_CALL(opendir, path)); - if(!fs->dp) + /* Group directories first */ + if(d1 != d2) + return d2 - d1; + /* Then group by name */ + return strcmp(i1->name, i2->name); +} + +static bool load_folder_switch(jfileselect *fs, char *path) +{ + set_finfo(fs, NULL, 0); + + struct dirent *ent; + DIR *dp = opendir(path); + if(!dp) return false; + /* Count entries */ + int n = count_accepted_entries(dp); + + /* Allocate memory for the fileinfo structures */ + struct fileinfo *finfo = malloc(n * sizeof *finfo); + if(!finfo) { + closedir(dp); + return false; + } + + /* Read the fileinfo structures */ + rewinddir(dp); + for(int i = 0; i < n && (ent = readdir(dp));) { + if(!accept_entry(ent)) + continue; + + finfo[i].name = strdup(ent->d_name); + finfo[i].type = ent->d_type; + finfo[i].size = -1; + + if(!finfo[i].name) { + /* Profesionnal unwinding isn't it? */ + for(int j = 0; j < i; j++) free(finfo[j].name); + free(finfo); + closedir(dp); + return false; + } + + char *full_path = path_down(path, ent->d_name); + if(full_path) { + if(ent->d_type == DT_DIR) { + DIR *sub = opendir(full_path); + if(sub) { + finfo[i].size = count_accepted_entries(sub); + closedir(sub); + } + } + else { + struct stat st; + if(stat(full_path, &st) >= 0) + finfo[i].size = st.st_size; + } + } + + i++; + } + + qsort(finfo, n, sizeof *finfo, compare_entries); + + closedir(dp); free(fs->path); fs->path = path; - - fs->folder_entries = 0; - struct dirent *ent; - while ((ent = readdir(fs->dp))) - fs->folder_entries += accept_entry(ent); - + set_finfo(fs, finfo, n); fs->widget.update = true; jwidget_emit(fs, (jevent){ .type = JFILESELECT_LOADED }); return true; } +static bool load_folder(jfileselect *fs, char *path) +{ + return gint_world_switch(GINT_CALL(load_folder_switch, (void *)fs, path)); +} + bool jfileselect_browse(jfileselect *fs, char const *path) { char *path_copy = strdup(path); @@ -198,42 +293,32 @@ static void jfileselect_poly_layout(void *fs0) static void jfileselect_poly_render(void *fs0, int x, int y) { jfileselect *fs = fs0; - if(!fs->path || !fs->dp) + if(!fs->path || !fs->entries) return; font_t const *old_font = dfont(fs->font); int line_height = fs->font->line_height + fs->line_spacing; int cw = jwidget_content_width(fs); + struct fileinfo *finfo = fs->entries; - rewinddir(fs->dp); - struct dirent *ent; - char const *entry_name; - bool isfolder; - - for(int i = -fs->scroll; i < fs->visible_lines;) { + for(int i = 0; i < fs->visible_lines && i < fs->entry_count; i++) { bool selected = (fs->cursor == fs->scroll + i); - - ent = readdir(fs->dp); - if(!ent) break; - if(!accept_entry(ent)) continue; - - entry_name = ent->d_name; - isfolder = (ent->d_type == DT_DIR); - - if(i < 0) { - i++; - continue; - } + struct fileinfo *info = &finfo[fs->scroll + i]; + bool isfolder = (info->type == DT_DIR); int line_y = y + line_height * i; if(selected) drect(x, line_y, x + cw - 1, line_y + line_height - 1, C_BLACK); /* Round `line_spacing / 2` down so there is more spacing below */ - dprint(x+2, line_y + (fs->line_spacing + 0) / 2, - selected ? C_WHITE : C_BLACK, - "%s%s", entry_name, isfolder ? "/" : ""); - i++; + int text_y = line_y + (fs->line_spacing + 0) / 2; + int fg = selected ? C_WHITE : C_BLACK; + + dprint(x+2, text_y, fg, "%s%s", info->name, isfolder ? "/" : ""); + if(fs->show_file_size && info->size >= 0) { + dprint_opt(x + cw - 3, text_y, fg, C_NONE, DTEXT_RIGHT, DTEXT_TOP, + isfolder ? "%d entries" : "%d B", info->size); + } } dfont(old_font); @@ -242,7 +327,7 @@ static void jfileselect_poly_render(void *fs0, int x, int y) static bool jfileselect_poly_event(void *fs0, jevent e) { jfileselect *fs = fs0; - if(!fs->path || !fs->dp) + if(!fs->path || !fs->entries) return false; if(e.type == JWIDGET_KEY) { @@ -257,17 +342,17 @@ static bool jfileselect_poly_event(void *fs0, jevent e) fs->cursor = ev.shift ? 0 : fs->cursor - 1; moved = true; } - if(key == KEY_DOWN && fs->cursor < fs->folder_entries - 1) { - fs->cursor = ev.shift ? fs->folder_entries - 1 : fs->cursor + 1; + if(key == KEY_DOWN && fs->cursor < fs->entry_count - 1) { + fs->cursor = ev.shift ? fs->entry_count - 1 : fs->cursor + 1; moved = true; } if(fs->scroll > 0 && fs->cursor <= fs->scroll) fs->scroll = max(fs->cursor - 1, 0); - if(fs->scroll + fs->visible_lines < fs->folder_entries + if(fs->scroll + fs->visible_lines < fs->entry_count && fs->cursor >= fs->scroll + fs->visible_lines - 2) { fs->scroll = min(fs->cursor - fs->visible_lines + 2, - fs->folder_entries - fs->visible_lines); + fs->entry_count - fs->visible_lines); } if(moved) { @@ -289,9 +374,10 @@ static bool jfileselect_poly_event(void *fs0, jevent e) } } else if(key == KEY_EXE) { - struct dirent *ent = read_nth_dir_entry(fs->dp, fs->cursor); - if(ent->d_type == DT_DIR) { - char *child = path_down(fs->path, ent->d_name); + struct fileinfo *finfo = fs->entries; + struct fileinfo *i = &finfo[fs->cursor]; + if(i->type == DT_DIR) { + char *child = path_down(fs->path, i->name); if(child) { load_folder(fs, child); fs->cursor = 0; @@ -300,7 +386,7 @@ static bool jfileselect_poly_event(void *fs0, jevent e) } } else { - fs->selected_file = path_down(fs->path, ent->d_name); + fs->selected_file = path_down(fs->path, i->name); if(fs->selected_file) { jwidget_emit(fs,(jevent){ .type = JFILESELECT_VALIDATED }); return true; @@ -317,8 +403,7 @@ static void jfileselect_poly_destroy(void *fs0) jfileselect *fs = fs0; free(fs->path); - if(fs->dp) - closedir(fs->dp); + set_finfo(fs, NULL, 0); free(fs->selected_file); }