Files
MontaukOS/programs/src/desktop/apps/filemanager/filesystem.cpp
T

619 lines
22 KiB
C++

/*
* 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