/* * filesystem.cpp * Filesystem enumeration and copy/delete helpers for the embedded desktop file manager * Copyright (c) 2026 Daniel Hammer */ #include "filemanager_internal.hpp" namespace filemanager { bool filemanager_ensure_entry_capacity(FileManagerState* fm, int needed) { if (!fm) return false; if (needed <= fm->entry_cap) return true; if (needed > FM_ENTRY_HARD_CAP) needed = FM_ENTRY_HARD_CAP; if (needed <= fm->entry_cap) return true; // already at the hard cap int new_cap = fm->entry_cap ? fm->entry_cap : 64; while (new_cap < needed) new_cap *= 2; if (new_cap > FM_ENTRY_HARD_CAP) new_cap = FM_ENTRY_HARD_CAP; // Grow each parallel array. On any failure we leave already-grown arrays in // place (they only get bigger) and keep entry_cap at the old value, so all // arrays still hold at least entry_count entries. auto* names = (char(*)[128])montauk::realloc(fm->entry_names, (uint64_t)new_cap * 128); if (!names) return false; fm->entry_names = names; int* types = (int*)montauk::realloc(fm->entry_types, (uint64_t)new_cap * sizeof(int)); if (!types) return false; fm->entry_types = types; uint64_t* sizes = (uint64_t*)montauk::realloc(fm->entry_sizes, (uint64_t)new_cap * sizeof(uint64_t)); if (!sizes) return false; fm->entry_sizes = sizes; int64_t* mtimes = (int64_t*)montauk::realloc(fm->entry_mtimes, (uint64_t)new_cap * sizeof(int64_t)); if (!mtimes) return false; fm->entry_mtimes = mtimes; bool* isdir = (bool*)montauk::realloc(fm->is_dir, (uint64_t)new_cap * sizeof(bool)); if (!isdir) return false; fm->is_dir = isdir; int* di = (int*)montauk::realloc(fm->drive_indices, (uint64_t)new_cap * sizeof(int)); if (!di) return false; fm->drive_indices = di; int* dk = (int*)montauk::realloc(fm->drive_kinds, (uint64_t)new_cap * sizeof(int)); if (!dk) return false; fm->drive_kinds = dk; bool* msel = (bool*)montauk::realloc(fm->multi_selected, (uint64_t)new_cap * sizeof(bool)); if (!msel) return false; fm->multi_selected = msel; auto* ve = (FileManagerVirtualEntry*)montauk::realloc( fm->virtual_entries, (uint64_t)new_cap * sizeof(FileManagerVirtualEntry)); if (!ve) return false; fm->virtual_entries = ve; // Zero the newly added tail: virtual_entries icon pointers must start null // (free_app_icons walks them) and multi_selected flags must start false. int old_cap = fm->entry_cap; int added = new_cap - old_cap; montauk::memset(&fm->multi_selected[old_cap], 0, (uint64_t)added * sizeof(bool)); montauk::memset(&fm->virtual_entries[old_cap], 0, (uint64_t)added * sizeof(FileManagerVirtualEntry)); fm->entry_cap = new_cap; return true; } bool filemanager_ensure_clipboard_capacity(FileManagerState* fm, int needed) { if (!fm) return false; if (needed <= fm->clipboard_cap) return true; if (needed > FM_ENTRY_HARD_CAP) needed = FM_ENTRY_HARD_CAP; if (needed <= fm->clipboard_cap) return true; int new_cap = fm->clipboard_cap ? fm->clipboard_cap : 64; while (new_cap < needed) new_cap *= 2; if (new_cap > FM_ENTRY_HARD_CAP) new_cap = FM_ENTRY_HARD_CAP; auto* paths = (char(*)[256])montauk::realloc(fm->clipboard_paths, (uint64_t)new_cap * 256); if (!paths) return false; fm->clipboard_paths = paths; fm->clipboard_cap = new_cap; return true; } void filemanager_free_arrays(FileManagerState* fm) { if (!fm) return; filemanager_free_app_icons(fm); montauk::mfree(fm->entry_names); fm->entry_names = nullptr; montauk::mfree(fm->entry_types); fm->entry_types = nullptr; montauk::mfree(fm->entry_sizes); fm->entry_sizes = nullptr; montauk::mfree(fm->entry_mtimes); fm->entry_mtimes = nullptr; montauk::mfree(fm->is_dir); fm->is_dir = nullptr; montauk::mfree(fm->drive_indices); fm->drive_indices = nullptr; montauk::mfree(fm->drive_kinds); fm->drive_kinds = nullptr; montauk::mfree(fm->multi_selected); fm->multi_selected = nullptr; montauk::mfree(fm->virtual_entries); fm->virtual_entries = nullptr; montauk::mfree(fm->clipboard_paths); fm->clipboard_paths = nullptr; fm->entry_cap = 0; fm->clipboard_cap = 0; fm->entry_count = 0; fm->virtual_entry_count = 0; fm->clipboard_count = 0; } static void filemanager_reset_list_state(FileManagerState* fm) { fm->selected = -1; fm->scroll_offset = 0; fm->scrollbar.scroll_offset = 0; fm->last_click_item = -1; fm->last_click_time = 0; fm->marquee_active = false; filemanager_clear_multi_selection(fm); } static void filemanager_reset_virtual_entry(FileManagerVirtualEntry* entry) { if (!entry) return; entry->desktop_item_index = -1; entry->icon_lg = {}; entry->icon_sm = {}; } static void filemanager_add_root_entry(FileManagerState* fm, const char* name, FileManagerEntryType type, int drive_index) { if (!fm || !filemanager_ensure_entry_capacity(fm, fm->entry_count + 1)) return; int idx = fm->entry_count++; montauk::strncpy(fm->entry_names[idx], name, 63); fm->entry_types[idx] = type; fm->entry_sizes[idx] = 0; fm->entry_mtimes[idx] = 0; fm->is_dir[idx] = true; fm->drive_indices[idx] = drive_index; fm->drive_kinds[idx] = 0; if (type == FM_ENTRY_DRIVE && drive_index >= 0) { fm->drive_kinds[idx] = montauk::drivekind(drive_index); } } static int filemanager_add_virtual_entry(FileManagerState* fm, const char* name, const char* icon_path) { if (!fm || !filemanager_ensure_entry_capacity(fm, fm->entry_count + 1)) return -1; int idx = fm->entry_count; montauk::strncpy(fm->entry_names[idx], name, 63); fm->entry_types[idx] = FM_ENTRY_VIRTUAL_ITEM; fm->entry_sizes[idx] = 0; fm->entry_mtimes[idx] = 0; fm->is_dir[idx] = false; fm->drive_indices[idx] = -1; filemanager_reset_virtual_entry(&fm->virtual_entries[idx]); if (icon_path && icon_path[0]) { Color defColor = colors::ICON_COLOR; fm->virtual_entries[idx].icon_lg = svg_load(icon_path, 48, 48, defColor); fm->virtual_entries[idx].icon_sm = svg_load(icon_path, 16, 16, defColor); } fm->entry_count++; fm->virtual_entry_count = fm->entry_count; return idx; } static void filemanager_add_desktop_item_entry(FileManagerState* fm, const DesktopItem& item, int desktop_item_index) { int idx = filemanager_add_virtual_entry(fm, item.name, item.icon_path); if (idx >= 0) { fm->virtual_entries[idx].desktop_item_index = desktop_item_index; } } static void filemanager_add_catalog_section(FileManagerState* fm, DesktopItemSection section) { if (!fm || !fm->desktop || fm->desktop->desktop_item_count <= 0) return; int capacity = fm->desktop->desktop_item_count; int* item_indices = (int*)montauk::malloc((uint64_t)capacity * sizeof(int)); if (!item_indices) return; int item_count = desktop_list_item_indices( fm->desktop, section, item_indices, capacity); for (int i = 0; i < item_count; i++) { int item_index = item_indices[i]; filemanager_add_desktop_item_entry( fm, fm->desktop->desktop_items[item_index], item_index); } montauk::mfree(item_indices); } static void filemanager_begin_virtual_view(FileManagerState* fm, FileManagerVirtualViewKind view) { filemanager_free_app_icons(fm); fm->current_path[0] = '\0'; fm->virtual_view = view; fm->entry_count = 0; } void filemanager_read_drives(FileManagerState* fm) { filemanager_free_app_icons(fm); fm->entry_count = 0; fm->virtual_view = FM_VIRTUAL_VIEW_NONE; fm->current_path[0] = '\0'; // Home folder entry (if we have a valid home directory) DesktopState* ds = fm->desktop; if (ds && ds->home_dir[0] != '\0') { filemanager_add_root_entry(fm, "Home", FM_ENTRY_HOME, -1); } // Apps entry filemanager_add_root_entry(fm, "Apps", FM_ENTRY_APPS_ROOT, -1); // Special user folders (Documents, Desktop, Music, etc.) - only if they exist if (ds && ds->home_dir[0] != '\0') { for (int sf = 0; sf < SF_COUNT; sf++) { char probe_path[256]; montauk::strcpy(probe_path, ds->home_dir); int plen = montauk::slen(probe_path); if (plen > 0 && probe_path[plen - 1] != '/') str_append(probe_path, "/", 256); str_append(probe_path, sf_names[sf], 256); // Probe: try open to check if the directory exists int probe_fd = montauk::open(probe_path); if (probe_fd >= 0) { montauk::close(probe_fd); filemanager_add_root_entry(fm, sf_names[sf], FM_ENTRY_SPECIAL_DIR, sf); } } } // Settings entry filemanager_add_root_entry(fm, "Settings", FM_ENTRY_SYSTEM_CONFIGURATION_ROOT, -1); // Drive entries int drives[FM_MAX_DRIVES]; int driveCount = montauk::drivelist(drives, FM_MAX_DRIVES); for (int di = 0; di < driveCount; di++) { int d = drives[di]; char probe[8]; int pi = 0; if (d >= 10) probe[pi++] = (char)('0' + (d / 10)); probe[pi++] = (char)('0' + (d % 10)); probe[pi++] = ':'; probe[pi++] = '/'; probe[pi] = '\0'; char label[64]; montauk::strcpy(label, "Drive "); str_append(label, probe, 64); char volume_label[64]; if (montauk::drivelabel(d, volume_label, 64) > 0 && volume_label[0] != '\0') { montauk::strncpy(label, volume_label, 64); str_append(label, " (", 64); str_append(label, probe, 64); str_append(label, ")", 64); } filemanager_add_root_entry(fm, label, FM_ENTRY_DRIVE, d); } filemanager_reset_list_state(fm); } void filemanager_read_dir(FileManagerState* fm) { if (fm->current_path[0] == '\0') { filemanager_read_drives(fm); return; } filemanager_free_app_icons(fm); fm->virtual_view = FM_VIRTUAL_VIEW_NONE; fm->entry_count = 0; // readdir returns full paths from the VFS (e.g. "man/fetch.1" instead // of just "fetch.1"). Compute the prefix to strip so we get basenames. const char* after_drive = fm->current_path; for (int k = 0; after_drive[k]; k++) { if (after_drive[k] == ':' && after_drive[k + 1] == '/') { after_drive += k + 2; break; } } char prefix[256] = {0}; int prefix_len = 0; if (after_drive[0] != '\0') { montauk::strcpy(prefix, after_drive); prefix_len = montauk::slen(prefix); if (prefix_len > 0 && prefix[prefix_len - 1] != '/') { prefix[prefix_len++] = '/'; prefix[prefix_len] = '\0'; } } // Page through the directory. The kernel returns a bounded batch per call, // so keep asking from the running offset until a call returns nothing. This // removes the old fixed cap -- directories of any size are fully listed. static constexpr int BATCH = 128; const char* names[BATCH]; for (;;) { int got = montauk::readdir_at(fm->current_path, names, BATCH, fm->entry_count); if (got <= 0) break; if (!filemanager_ensure_entry_capacity(fm, fm->entry_count + got)) break; for (int b = 0; b < got; b++) { int i = fm->entry_count + b; const char* raw = names[b]; // Strip directory prefix if it matches if (prefix_len > 0) { bool match = true; for (int k = 0; k < prefix_len; k++) { if (raw[k] != prefix[k]) { match = false; break; } } if (match) raw += prefix_len; } montauk::strncpy(fm->entry_names[i], raw, 128); int len = montauk::slen(fm->entry_names[i]); // Detect directory if (len > 0 && fm->entry_names[i][len - 1] == '/') { fm->is_dir[i] = true; fm->entry_names[i][len - 1] = '\0'; } else { fm->is_dir[i] = false; } fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]); // Fetch size + modified time in one stat call (works for files and // directories). Filesystems without stat support leave both 0. fm->entry_sizes[i] = 0; fm->entry_mtimes[i] = 0; char fullpath[512]; montauk::strcpy(fullpath, fm->current_path); int plen = montauk::slen(fullpath); if (plen > 0 && fullpath[plen - 1] != '/') { str_append(fullpath, "/", 512); } str_append(fullpath, fm->entry_names[i], 512); montauk::abi::FileStat stt; if (montauk::stat(fullpath, &stt) == 0) { if (!fm->is_dir[i]) fm->entry_sizes[i] = stt.size; fm->entry_mtimes[i] = stt.mtime; } } fm->entry_count += got; if (fm->entry_count >= FM_ENTRY_HARD_CAP) break; } // Sort: directories first, then alphabetical (case-insensitive) for (int i = 1; i < fm->entry_count; i++) { char tmp_name[128]; int tmp_type = fm->entry_types[i]; uint64_t tmp_size = fm->entry_sizes[i]; int64_t tmp_mtime = fm->entry_mtimes[i]; bool tmp_isdir = fm->is_dir[i]; montauk::strcpy(tmp_name, fm->entry_names[i]); int j = i - 1; while (j >= 0) { bool swap = false; if (tmp_isdir && !fm->is_dir[j]) { swap = true; } else if (tmp_isdir == fm->is_dir[j]) { if (str_compare_ci(tmp_name, fm->entry_names[j]) < 0) { swap = true; } } if (!swap) break; montauk::strcpy(fm->entry_names[j + 1], fm->entry_names[j]); fm->entry_types[j + 1] = fm->entry_types[j]; fm->entry_sizes[j + 1] = fm->entry_sizes[j]; fm->entry_mtimes[j + 1] = fm->entry_mtimes[j]; fm->is_dir[j + 1] = fm->is_dir[j]; j--; } montauk::strcpy(fm->entry_names[j + 1], tmp_name); fm->entry_types[j + 1] = tmp_type; fm->entry_sizes[j + 1] = tmp_size; fm->entry_mtimes[j + 1] = tmp_mtime; fm->is_dir[j + 1] = tmp_isdir; } filemanager_reset_list_state(fm); } void filemanager_free_app_icons(FileManagerState* fm) { if (!fm) return; for (int i = 0; i < fm->virtual_entry_count; i++) { if (fm->virtual_entries[i].icon_lg.pixels) { montauk::mfree(fm->virtual_entries[i].icon_lg.pixels); fm->virtual_entries[i].icon_lg.pixels = nullptr; } if (fm->virtual_entries[i].icon_sm.pixels) { montauk::mfree(fm->virtual_entries[i].icon_sm.pixels); fm->virtual_entries[i].icon_sm.pixels = nullptr; } filemanager_reset_virtual_entry(&fm->virtual_entries[i]); } fm->virtual_entry_count = 0; } void filemanager_read_virtual_view(FileManagerState* fm, FileManagerVirtualViewKind view) { switch (view) { case FM_VIRTUAL_VIEW_APPS: filemanager_read_apps(fm); break; case FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION: filemanager_read_system_configuration(fm); break; default: filemanager_read_drives(fm); break; } } void filemanager_read_apps(FileManagerState* fm) { filemanager_begin_virtual_view(fm, FM_VIRTUAL_VIEW_APPS); filemanager_add_catalog_section(fm, DESKTOP_ITEM_SECTION_APPS); filemanager_reset_list_state(fm); } void filemanager_read_system_configuration(FileManagerState* fm) { filemanager_begin_virtual_view(fm, FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION); filemanager_add_catalog_section(fm, DESKTOP_ITEM_SECTION_SETTINGS); filemanager_reset_list_state(fm); } // Read ALL direct children of `dir` into a freshly-allocated array of basenames // (each up to 255 chars, directory entries keep their trailing '/'). Pages the // directory fully so there is no fixed entry cap. Returns the child count and // stores the malloc'd buffer in *out (caller frees with montauk::mfree); the // buffer is null when the count is 0. Returns -1 if the path is not readable. static int filemanager_read_all_children(const char* dir, char (**out)[256]) { *out = nullptr; // Compute the prefix to strip (readdir returns drive-root-relative paths). const char* after_drive = dir; for (int k = 0; after_drive[k]; k++) { if (after_drive[k] == ':' && after_drive[k + 1] == '/') { after_drive += k + 2; break; } } char prefix[256] = {0}; int prefix_len = 0; if (after_drive[0] != '\0') { montauk::strcpy(prefix, after_drive); prefix_len = montauk::slen(prefix); if (prefix_len > 0 && prefix[prefix_len - 1] != '/') { prefix[prefix_len++] = '/'; prefix[prefix_len] = '\0'; } } static constexpr int BATCH = 128; const char* names[BATCH]; char (*list)[256] = nullptr; int cap = 0; int count = 0; bool any = false; for (;;) { int got = montauk::readdir_at(dir, names, BATCH, count); if (got < 0) { if (!any) return -1; break; } any = true; if (got == 0) break; if (count + got > cap) { int new_cap = cap ? cap * 2 : 128; while (new_cap < count + got) new_cap *= 2; if (new_cap > FM_ENTRY_HARD_CAP) new_cap = FM_ENTRY_HARD_CAP; auto* grown = (char(*)[256])montauk::realloc(list, (uint64_t)new_cap * 256); if (!grown) break; list = grown; cap = new_cap; } for (int b = 0; b < got && count < cap; b++) { const char* raw = names[b]; if (prefix_len > 0) { bool match = true; for (int k = 0; k < prefix_len; k++) { if (raw[k] != prefix[k]) { match = false; break; } } if (match) raw += prefix_len; } montauk::strncpy(list[count], raw, 255); list[count][255] = '\0'; count++; } if (count >= FM_ENTRY_HARD_CAP) break; } *out = list; return count; } bool filemanager_delete_recursive(const char* path) { // Try deleting as a file (or empty directory) first if (montauk::fdelete(path) == 0) return true; // If that failed, it may be a non-empty directory -- snapshot all children // first (the directory shrinks as we delete, so we cannot page while // deleting), then delete each. char (*children)[256] = nullptr; int count = filemanager_read_all_children(path, &children); if (count < 0) return false; for (int i = 0; i < count; i++) { char child[512]; montauk::strcpy(child, path); int plen = montauk::slen(child); if (plen > 0 && child[plen - 1] != '/') str_append(child, "/", 512); str_append(child, children[i], 512); // Strip trailing slash if present (directory marker) int clen = montauk::slen(child); if (clen > 0 && child[clen - 1] == '/') child[clen - 1] = '\0'; filemanager_delete_recursive(child); } if (children) montauk::mfree(children); // Now the directory should be empty -- delete it return montauk::fdelete(path) == 0; } bool filemanager_copy_file(const char* src, const char* dst) { if (!src || !dst || montauk::streq(src, dst)) return false; int sfd = montauk::open(src); if (sfd < 0) return false; uint64_t size = montauk::getsize(sfd); // Create destination int dfd = montauk::fcreate(dst); if (dfd < 0) { montauk::close(sfd); return false; } bool ok = true; if (size > 0) { static constexpr uint64_t COPY_CHUNK = 64 * 1024; uint64_t buf_size = size < COPY_CHUNK ? size : COPY_CHUNK; uint8_t* buf = (uint8_t*)montauk::malloc(buf_size); if (!buf) { montauk::close(sfd); montauk::close(dfd); montauk::fdelete(dst); return false; } uint64_t off = 0; while (off < size) { uint64_t todo64 = size - off; if (todo64 > buf_size) todo64 = buf_size; int todo = (int)todo64; int got = montauk::read(sfd, buf, off, todo64); if (got != todo) { ok = false; break; } int wrote = montauk::fwrite(dfd, buf, off, todo64); if (wrote != todo) { ok = false; break; } off += todo64; } montauk::mfree(buf); } montauk::close(sfd); montauk::close(dfd); if (!ok) { montauk::fdelete(dst); return false; } return true; } // Recursive directory copy bool filemanager_copy_dir_recursive(const char* src, const char* dst) { if (!src || !dst || montauk::streq(src, dst)) return false; montauk::fmkdir(dst); char (*children)[256] = nullptr; int count = filemanager_read_all_children(src, &children); if (count < 0) return false; for (int i = 0; i < count; i++) { bool child_is_dir = false; char basename[256]; montauk::strncpy(basename, children[i], 255); basename[255] = '\0'; int blen = montauk::slen(basename); if (blen > 0 && basename[blen - 1] == '/') { child_is_dir = true; basename[blen - 1] = '\0'; } char src_child[512], dst_child[512]; filemanager_build_fullpath(src_child, 512, src, basename); filemanager_build_fullpath(dst_child, 512, dst, basename); if (child_is_dir) filemanager_copy_dir_recursive(src_child, dst_child); else filemanager_copy_file(src_child, dst_child); } if (children) montauk::mfree(children); return true; } } // namespace filemanager