feat: filesystem and Files app support for >64 file entries, 64-bit sizes, async ops, GUI toolkit improvements to devexplorer app

This commit is contained in:
2026-06-07 09:33:46 +02:00
parent 6554ef7e15
commit 3d620673c0
27 changed files with 1110 additions and 293 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 66 #define MONTAUK_BUILD_NUMBER 68
+10 -1
View File
@@ -36,15 +36,24 @@ namespace Montauk {
Ipc::CloseHandle(handle); Ipc::CloseHandle(handle);
} }
static int Sys_ReadDirAt(const char* path, const char** outNames, int maxEntries,
int startIndex);
static int Sys_ReadDir(const char* path, const char** outNames, int maxEntries) { static int Sys_ReadDir(const char* path, const char** outNames, int maxEntries) {
return Sys_ReadDirAt(path, outNames, maxEntries, 0);
}
static int Sys_ReadDirAt(const char* path, const char** outNames, int maxEntries,
int startIndex) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (startIndex < 0) return -1;
// Get entries from VFS into a kernel-local array // Get entries from VFS into a kernel-local array
const char* kernelNames[256]; const char* kernelNames[256];
int max = maxEntries; int max = maxEntries;
if (max > 256) max = 256; if (max > 256) max = 256;
int count = Fs::Vfs::VfsReadDir(resolved, kernelNames, max); int count = Fs::Vfs::VfsReadDirAt(resolved, kernelNames, max, startIndex);
if (count <= 0) return count; if (count <= 0) return count;
// Allocate a user-accessible page for string data via process heap // Allocate a user-accessible page for string data via process heap
+13
View File
@@ -97,6 +97,19 @@ namespace Montauk {
return (int64_t)Sys_ReadDir((const char*)frame->arg1, return (int64_t)Sys_ReadDir((const char*)frame->arg1,
(const char**)frame->arg2, (const char**)frame->arg2,
(int)frame->arg3); (int)frame->arg3);
case SYS_READDIR_AT:
if ((int64_t)frame->arg3 < 0) return -1;
if ((int64_t)frame->arg4 < 0) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::Range(frame->arg2,
(uint64_t)((frame->arg3 > 256) ? 256 : frame->arg3) * sizeof(const char*),
true)) {
return -1;
}
return (int64_t)Sys_ReadDirAt((const char*)frame->arg1,
(const char**)frame->arg2,
(int)frame->arg3,
(int)frame->arg4);
case SYS_ALLOC: case SYS_ALLOC:
return (int64_t)Sys_Alloc(frame->arg1); return (int64_t)Sys_Alloc(frame->arg1);
case SYS_FREE: case SYS_FREE:
+3
View File
@@ -255,6 +255,9 @@ namespace Montauk {
/* Power.hpp -- cross-process graceful power-off request channel */ /* Power.hpp -- cross-process graceful power-off request channel */
static constexpr uint64_t SYS_POWER_REQUEST = 135; static constexpr uint64_t SYS_POWER_REQUEST = 135;
/* Filesystem.hpp -- paginated directory read (path, names, max, startIndex) */
static constexpr uint64_t SYS_READDIR_AT = 136;
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
// a pending action and exits; login.elf reads it, runs the shutdown stages, // a pending action and exits; login.elf reads it, runs the shutdown stages,
// then issues the matching SYS_SHUTDOWN / SYS_RESET. // then issues the matching SYS_SHUTDOWN / SYS_RESET.
+1
View File
@@ -66,6 +66,7 @@ namespace Fs {
Ramdisk::Mkdir, Ramdisk::Mkdir,
Ramdisk::Rename, Ramdisk::Rename,
Ramdisk::GetLabel, Ramdisk::GetLabel,
Ramdisk::ReadDirAt,
}; };
} }
+14 -3
View File
@@ -806,11 +806,12 @@ namespace Fs::Ext2 {
static int ReadDirectoryNames(Ext2Instance& inst, const Inode& dirInode, static int ReadDirectoryNames(Ext2Instance& inst, const Inode& dirInode,
char outNames[MaxDirEntries][MaxNameLen], char outNames[MaxDirEntries][MaxNameLen],
int maxEntries) { int maxEntries, int startOffset = 0) {
uint32_t dirSize = dirInode.i_size; uint32_t dirSize = dirInode.i_size;
uint32_t blockSize = inst.blockSize; uint32_t blockSize = inst.blockSize;
uint32_t numBlocks = (dirSize + blockSize - 1) / blockSize; uint32_t numBlocks = (dirSize + blockSize - 1) / blockSize;
int count = 0; int count = 0;
int skipped = 0; // valid entries skipped to reach startOffset
uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed(); uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dirBuf) return 0; if (!dirBuf) return 0;
@@ -846,6 +847,14 @@ namespace Fs::Ext2 {
outNames[count][nameLen] = '\0'; outNames[count][nameLen] = '\0';
} }
// Skip entries before the requested page. The name above was
// written into outNames[count] as scratch and will be reused.
if (skipped < startOffset) {
skipped++;
pos += de->rec_len;
continue;
}
count++; count++;
} }
@@ -1146,7 +1155,7 @@ namespace Fs::Ext2 {
} }
static int ReadDirImpl(int inst, const char* path, static int ReadDirImpl(int inst, const char* path,
const char** outNames, int maxEntries) { const char** outNames, int maxEntries, int startIndex = 0) {
if (inst < 0 || inst >= g_instanceCount) return -1; if (inst < 0 || inst >= g_instanceCount) return -1;
auto& self = g_instances[inst]; auto& self = g_instances[inst];
@@ -1156,7 +1165,7 @@ namespace Fs::Ext2 {
if ((inode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1; if ((inode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1;
int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries; int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries;
int count = ReadDirectoryNames(self, inode, self.dirNames, limit); int count = ReadDirectoryNames(self, inode, self.dirNames, limit, startIndex);
self.dirNameCount = count; self.dirNameCount = count;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -1603,6 +1612,7 @@ namespace Fs::Ext2 {
static uint64_t GetSize(int h) { return GetSizeImpl(N, h); } static uint64_t GetSize(int h) { return GetSizeImpl(N, h); }
static void Close(int h) { CloseImpl(N, h); } static void Close(int h) { CloseImpl(N, h); }
static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); } static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); }
static int ReadDirAt(const char* p, const char** o, int m, int s) { return ReadDirImpl(N, p, o, m, s); }
static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); } static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); }
static int Create(const char* p) { return CreateImpl(N, p); } static int Create(const char* p) { return CreateImpl(N, p); }
static int Delete(const char* p) { return DeleteImpl(N, p); } static int Delete(const char* p) { return DeleteImpl(N, p); }
@@ -1625,6 +1635,7 @@ namespace Fs::Ext2 {
Thunks<N>::Mkdir, Thunks<N>::Mkdir,
Thunks<N>::Rename, Thunks<N>::Rename,
Thunks<N>::GetLabel, Thunks<N>::GetLabel,
Thunks<N>::ReadDirAt,
}; };
} }
+10 -3
View File
@@ -674,11 +674,12 @@ namespace Fs::Fat32 {
static int ReadDirectoryNames(int inst, uint32_t dirCluster, static int ReadDirectoryNames(int inst, uint32_t dirCluster,
char outNames[MaxDirEntries][MaxNameLen], char outNames[MaxDirEntries][MaxNameLen],
int maxEntries) { int maxEntries, int startOffset = 0) {
auto& self = g_instances[inst]; auto& self = g_instances[inst];
uint16_t lfnBuf[MaxNameLen]; uint16_t lfnBuf[MaxNameLen];
bool hasLfn = false; bool hasLfn = false;
int count = 0; int count = 0;
int skipped = 0; // valid entries skipped to reach startOffset
uint32_t cluster = dirCluster; uint32_t cluster = dirCluster;
while (!IsEndOfChain(cluster) && count < maxEntries) { while (!IsEndOfChain(cluster) && count < maxEntries) {
@@ -736,6 +737,10 @@ namespace Fs::Fat32 {
outNames[count][len] = '\0'; outNames[count][len] = '\0';
} }
// Skip entries before the requested page. The name above was
// written into outNames[count] as scratch and will be reused.
if (skipped < startOffset) { skipped++; continue; }
count++; count++;
} }
@@ -997,7 +1002,7 @@ namespace Fs::Fat32 {
} }
static int ReadDirImpl(int inst, const char* path, static int ReadDirImpl(int inst, const char* path,
const char** outNames, int maxEntries) { const char** outNames, int maxEntries, int startIndex = 0) {
if (inst < 0 || inst >= g_instanceCount) return -1; if (inst < 0 || inst >= g_instanceCount) return -1;
auto& self = g_instances[inst]; auto& self = g_instances[inst];
@@ -1006,7 +1011,7 @@ namespace Fs::Fat32 {
if (!(dirEntry.attributes & ATTR_DIRECTORY)) return -1; if (!(dirEntry.attributes & ATTR_DIRECTORY)) return -1;
int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries; int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries;
int count = ReadDirectoryNames(inst, dirEntry.firstCluster, self.dirNames, limit); int count = ReadDirectoryNames(inst, dirEntry.firstCluster, self.dirNames, limit, startIndex);
self.dirNameCount = count; self.dirNameCount = count;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -1762,6 +1767,7 @@ namespace Fs::Fat32 {
static uint64_t GetSize(int h) { return GetSizeImpl(N, h); } static uint64_t GetSize(int h) { return GetSizeImpl(N, h); }
static void Close(int h) { CloseImpl(N, h); } static void Close(int h) { CloseImpl(N, h); }
static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); } static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); }
static int ReadDirAt(const char* p, const char** o, int m, int s) { return ReadDirImpl(N, p, o, m, s); }
static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); } static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); }
static int Create(const char* p) { return CreateImpl(N, p); } static int Create(const char* p) { return CreateImpl(N, p); }
static int Delete(const char* p) { return DeleteImpl(N, p); } static int Delete(const char* p) { return DeleteImpl(N, p); }
@@ -1784,6 +1790,7 @@ namespace Fs::Fat32 {
Thunks<N>::Mkdir, Thunks<N>::Mkdir,
Thunks<N>::Rename, Thunks<N>::Rename,
Thunks<N>::GetLabel, Thunks<N>::GetLabel,
Thunks<N>::ReadDirAt,
}; };
} }
+14 -8
View File
@@ -217,15 +217,17 @@ namespace Fs::Ramdisk {
(void)handle; (void)handle;
} }
int ReadDir(const char* path, const char** outNames, int maxEntries) { int ReadDirAt(const char* path, const char** outNames, int maxEntries, int startIndex) {
// Normalize path: skip leading '/' // Normalize path: skip leading '/'
if (path[0] == '/') path++; if (path[0] == '/') path++;
int pathLen = StrLen(path); int pathLen = StrLen(path);
int count = 0; int count = 0; // entries written to outNames
int seen = 0; // matching direct children scanned so far (for startIndex skip)
for (int i = 0; i < fileCount && count < maxEntries; i++) { for (int i = 0; i < fileCount && count < maxEntries; i++) {
const char* entryName = fileTable[i].name; const char* entryName = fileTable[i].name;
bool isChild = false;
if (pathLen == 0) { if (pathLen == 0) {
// Root directory: find entries without '/' in them (or only trailing '/') // Root directory: find entries without '/' in them (or only trailing '/')
@@ -237,9 +239,7 @@ namespace Fs::Ramdisk {
break; break;
} }
} }
if (!hasSlash) { isChild = !hasSlash;
outNames[count++] = entryName;
}
} else { } else {
// Subdirectory: match entries starting with "path/" // Subdirectory: match entries starting with "path/"
// and that are direct children (no additional '/' beyond the prefix) // and that are direct children (no additional '/' beyond the prefix)
@@ -259,15 +259,21 @@ namespace Fs::Ramdisk {
break; break;
} }
} }
if (!hasDeepSlash && restLen > 0) { isChild = (!hasDeepSlash && restLen > 0);
}
if (!isChild) continue;
if (seen++ < startIndex) continue; // skip entries before the requested page
outNames[count++] = entryName; outNames[count++] = entryName;
} }
}
}
return count; return count;
} }
int ReadDir(const char* path, const char** outNames, int maxEntries) {
return ReadDirAt(path, outNames, maxEntries, 0);
}
int Write(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) { int Write(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) {
if (handle < 0 || handle >= fileCount) return -1; if (handle < 0 || handle >= fileCount) return -1;
if (buffer == nullptr || size == 0) return 0; if (buffer == nullptr || size == 0) return 0;
+1
View File
@@ -32,6 +32,7 @@ namespace Fs::Ramdisk {
void Close(int handle); void Close(int handle);
int ReadDir(const char* path, const char** outNames, int maxEntries); int ReadDir(const char* path, const char** outNames, int maxEntries);
int ReadDirAt(const char* path, const char** outNames, int maxEntries, int startIndex);
int Delete(const char* path); int Delete(const char* path);
int Mkdir(const char* path); int Mkdir(const char* path);
int Rename(const char* oldPath, const char* newPath); int Rename(const char* oldPath, const char* newPath);
+16 -3
View File
@@ -374,17 +374,30 @@ namespace Fs::Vfs {
} }
int VfsReadDir(const char* path, const char** outNames, int maxEntries) { int VfsReadDir(const char* path, const char** outNames, int maxEntries) {
return VfsReadDirAt(path, outNames, maxEntries, 0);
}
int VfsReadDirAt(const char* path, const char** outNames, int maxEntries, int startIndex) {
int drive; int drive;
const char* localPath; const char* localPath;
if (startIndex < 0) return -1;
if (!ParsePath(path, drive, localPath)) return -1; if (!ParsePath(path, drive, localPath)) return -1;
if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1;
vfsLock.Acquire(); vfsLock.Acquire();
FsDriver* driver = driveTable[drive]; FsDriver* driver = driveTable[drive];
int result = (driveActive[drive] && driver && driver->ReadDir) int result;
? driver->ReadDir(localPath, outNames, maxEntries) if (!driveActive[drive] || !driver) {
: -1; result = -1;
} else if (driver->ReadDirAt) {
result = driver->ReadDirAt(localPath, outNames, maxEntries, startIndex);
} else if (driver->ReadDir) {
// Driver without pagination support: only the first page is reachable.
result = (startIndex == 0) ? driver->ReadDir(localPath, outNames, maxEntries) : 0;
} else {
result = -1;
}
vfsLock.Release(); vfsLock.Release();
return result; return result;
} }
+4
View File
@@ -30,6 +30,9 @@ namespace Fs::Vfs {
int (*Mkdir)(const char* path); int (*Mkdir)(const char* path);
int (*Rename)(const char* oldPath, const char* newPath); int (*Rename)(const char* oldPath, const char* newPath);
const char* (*GetLabel)(); const char* (*GetLabel)();
// Optional: paginated directory read returning entries [startIndex, startIndex+maxEntries).
// Drivers that leave this null are read via ReadDir at startIndex 0 only.
int (*ReadDirAt)(const char* path, const char** outNames, int maxEntries, int startIndex);
}; };
void Initialize(); void Initialize();
@@ -49,6 +52,7 @@ namespace Fs::Vfs {
int VfsDelete(const char* path); int VfsDelete(const char* path);
int VfsReadDir(const char* path, const char** outNames, int maxEntries); int VfsReadDir(const char* path, const char** outNames, int maxEntries);
int VfsReadDirAt(const char* path, const char** outNames, int maxEntries, int startIndex);
int VfsMkdir(const char* path); int VfsMkdir(const char* path);
int VfsRename(const char* oldPath, const char* newPath); int VfsRename(const char* oldPath, const char* newPath);
+3
View File
@@ -179,6 +179,9 @@ namespace Montauk {
// Cross-process graceful power-off request channel // Cross-process graceful power-off request channel
static constexpr uint64_t SYS_POWER_REQUEST = 135; static constexpr uint64_t SYS_POWER_REQUEST = 135;
// Paginated directory read (path, names, max, startIndex)
static constexpr uint64_t SYS_READDIR_AT = 136;
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
// a pending action and exits; login.elf reads it, runs the shutdown stages, // a pending action and exits; login.elf reads it, runs the shutdown stages,
// then issues the matching SYS_SHUTDOWN / SYS_RESET. // then issues the matching SYS_SHUTDOWN / SYS_RESET.
+7
View File
@@ -141,6 +141,13 @@ namespace montauk {
inline int readdir(const char* path, const char** names, int max) { inline int readdir(const char* path, const char** names, int max) {
return (int)syscall3(Montauk::SYS_READDIR, (uint64_t)path, (uint64_t)names, (uint64_t)max); return (int)syscall3(Montauk::SYS_READDIR, (uint64_t)path, (uint64_t)names, (uint64_t)max);
} }
// Paginated directory read: returns entries [startIndex, startIndex+max).
// Call repeatedly with increasing startIndex (advancing by the returned
// count) until it returns 0 to enumerate directories of any size.
inline int readdir_at(const char* path, const char** names, int max, int startIndex) {
return (int)syscall4(Montauk::SYS_READDIR_AT, (uint64_t)path, (uint64_t)names,
(uint64_t)max, (uint64_t)startIndex);
}
// File write/create // File write/create
inline int fwrite(int handle, const uint8_t* buf, uint64_t off, uint64_t size) { inline int fwrite(int handle, const uint8_t* buf, uint64_t off, uint64_t size) {
+19 -16
View File
@@ -133,25 +133,28 @@ inline void format_mac(char* buf, const uint8_t* mac) {
// File size formatting // File size formatting
// ============================================================================ // ============================================================================
inline void format_size(char* buf, int size) { inline void format_size(char* buf, uint64_t size) {
// Pick the largest unit that keeps the integer part below 1024, then show
// one decimal place for small values. 64-bit throughout: no 2 GiB overflow.
static const char* const units[] = { "B", "KB", "MB", "GB", "TB", "PB" };
if (size < 1024) { if (size < 1024) {
snprintf(buf, 16, "%d B", size); snprintf(buf, 16, "%d B", (int)size);
} else if (size < 1024 * 1024) { return;
int kb = size / 1024;
int frac = ((size % 1024) * 10) / 1024;
if (kb < 10) {
snprintf(buf, 16, "%d.%d KB", kb, frac);
} else {
snprintf(buf, 16, "%d KB", kb);
} }
} else { int unit = 0;
int mb = size / (1024 * 1024); uint64_t scaled = size;
int frac = ((size % (1024 * 1024)) * 10) / (1024 * 1024); uint64_t rem = 0;
if (mb < 10) { while (scaled >= 1024 && unit < 5) {
snprintf(buf, 16, "%d.%d MB", mb, frac); rem = scaled % 1024;
} else { scaled /= 1024;
snprintf(buf, 16, "%d MB", mb); unit++;
} }
int whole = (int)scaled;
int frac = (int)((rem * 10) / 1024);
if (whole < 10) {
snprintf(buf, 16, "%d.%d %s", whole, frac, units[unit]);
} else {
snprintf(buf, 16, "%d %s", whole, units[unit]);
} }
} }
@@ -8,34 +8,25 @@
namespace filemanager { namespace filemanager {
static bool path_is_same_or_descendant(const char* path, const char* root) {
if (!path || !root) return false;
int root_len = montauk::slen(root);
while (root_len > 0 && root[root_len - 1] == '/') root_len--;
if (root_len <= 0) return false;
for (int i = 0; i < root_len; i++) {
if (path[i] == '\0' || path[i] != root[i]) return false;
}
return path[root_len] == '\0' || path[root_len] == '/';
}
static void filemanager_load_clipboard(FileManagerState* fm, bool is_cut) { static void filemanager_load_clipboard(FileManagerState* fm, bool is_cut) {
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return; if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
if (fm->entry_count <= 0) return;
int sel[64]; int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
int n = filemanager_collect_selection(fm, sel, 64); if (!sel) return;
if (n <= 0) return; int n = filemanager_collect_selection(fm, sel, fm->entry_count);
if (n <= 0) { montauk::mfree(sel); return; }
if (!filemanager_ensure_clipboard_capacity(fm, n)) { montauk::mfree(sel); return; }
int out = 0; int out = 0;
for (int i = 0; i < n && out < 64; i++) { for (int i = 0; i < n && out < fm->clipboard_cap; i++) {
int idx = sel[i]; int idx = sel[i];
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue; if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
filemanager_build_fullpath(fm->clipboard_paths[out], 256, filemanager_build_fullpath(fm->clipboard_paths[out], 256,
fm->current_path, fm->entry_names[idx]); fm->current_path, fm->entry_names[idx]);
out++; out++;
} }
montauk::mfree(sel);
if (out == 0) return; if (out == 0) return;
fm->clipboard_count = out; fm->clipboard_count = out;
@@ -51,35 +42,9 @@ void filemanager_do_cut(FileManagerState* fm) {
} }
void filemanager_do_paste(FileManagerState* fm) { void filemanager_do_paste(FileManagerState* fm) {
if (fm->clipboard_count <= 0 || // Copy/move runs on a worker thread with a progress dialog (a small single
filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return; // file pastes inline). Same-volume moves use rename (instant, no copy).
filemanager_start_paste_job(fm);
bool any_ok = false;
bool clear_clipboard = fm->clipboard_is_cut;
for (int i = 0; i < fm->clipboard_count; i++) {
const char* src = fm->clipboard_paths[i];
const char* basename = path_basename(src);
char dst[512];
filemanager_build_fullpath(dst, 512, fm->current_path, basename);
if (montauk::streq(dst, src)) continue;
const char* probe[1];
int probe_count = montauk::readdir(src, probe, 1);
bool src_is_dir = (probe_count >= 0);
if (src_is_dir && path_is_same_or_descendant(dst, src)) continue;
bool ok = src_is_dir ? filemanager_copy_dir_recursive(src, dst)
: filemanager_copy_file(src, dst);
any_ok = any_ok || ok;
if (ok && fm->clipboard_is_cut) {
if (src_is_dir) filemanager_delete_recursive(src);
else montauk::fdelete(src);
}
}
if (clear_clipboard) fm->clipboard_count = 0;
if (any_ok) filemanager_read_dir(fm);
} }
void filemanager_start_rename(FileManagerState* fm) { void filemanager_start_rename(FileManagerState* fm) {
@@ -157,13 +122,16 @@ void filemanager_new_folder(FileManagerState* fm) {
void filemanager_delete_selected(FileManagerState* fm) { void filemanager_delete_selected(FileManagerState* fm) {
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return; if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
if (fm->entry_count <= 0) return;
int sel[64]; int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
int n = filemanager_collect_selection(fm, sel, 64); if (!sel) return;
if (n <= 0) return; int n = filemanager_collect_selection(fm, sel, fm->entry_count);
if (n <= 0) { montauk::mfree(sel); return; }
// Filter out drives. // Filter out drives.
int filtered[64]; int* filtered = (int*)montauk::malloc((uint64_t)n * sizeof(int));
if (!filtered) { montauk::mfree(sel); return; }
int fn = 0; int fn = 0;
bool any_dir = false; bool any_dir = false;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
@@ -172,7 +140,8 @@ void filemanager_delete_selected(FileManagerState* fm) {
filtered[fn++] = idx; filtered[fn++] = idx;
if (fm->is_dir[idx]) any_dir = true; if (fm->is_dir[idx]) any_dir = true;
} }
if (fn == 0) return; montauk::mfree(sel);
if (fn == 0) { montauk::mfree(filtered); return; }
// Anything with a directory or multi-item selection: confirm first. // Anything with a directory or multi-item selection: confirm first.
if (any_dir || fn > 1) { if (any_dir || fn > 1) {
@@ -181,6 +150,7 @@ void filemanager_delete_selected(FileManagerState* fm) {
} else { } else {
filemanager_show_bulk_delete_confirmation(fm, filtered, fn); filemanager_show_bulk_delete_confirmation(fm, filtered, fn);
} }
montauk::mfree(filtered);
return; return;
} }
@@ -188,6 +158,7 @@ void filemanager_delete_selected(FileManagerState* fm) {
char fullpath[512]; char fullpath[512];
filemanager_build_fullpath(fullpath, 512, filemanager_build_fullpath(fullpath, 512,
fm->current_path, fm->entry_names[filtered[0]]); fm->current_path, fm->entry_names[filtered[0]]);
montauk::mfree(filtered);
montauk::fdelete(fullpath); montauk::fdelete(fullpath);
filemanager_clear_multi_selection(fm); filemanager_clear_multi_selection(fm);
filemanager_read_dir(fm); filemanager_read_dir(fm);
@@ -51,10 +51,13 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
case CTX_DELETE: filemanager_delete_selected(fm); break; case CTX_DELETE: filemanager_delete_selected(fm); break;
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break; case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
case CTX_PROPERTIES: { case CTX_PROPERTIES: {
if (fm->multi_count > 1) { if (fm->multi_count > 1 && fm->entry_count > 0) {
int sel[64]; int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
int n = filemanager_collect_selection(fm, sel, 64); if (sel) {
int n = filemanager_collect_selection(fm, sel, fm->entry_count);
if (n > 0) filemanager_show_multi_properties(fm, sel, n); if (n > 0) filemanager_show_multi_properties(fm, sel, n);
montauk::mfree(sel);
}
} else { } else {
filemanager_show_properties(fm, target); filemanager_show_properties(fm, target);
} }
@@ -204,7 +207,7 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
// Recompute hit-set every frame so entries highlight live during the drag. // Recompute hit-set every frame so entries highlight live during the drag.
filemanager_clear_multi_selection(fm); filemanager_clear_multi_selection(fm);
int limit = fm->entry_count < 64 ? fm->entry_count : 64; int limit = fm->entry_count;
if (fm->grid_view) { if (fm->grid_view) {
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W; int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1; if (cols < 1) cols = 1;
@@ -484,7 +487,7 @@ void filemanager_on_key(Window* win, const Montauk::KeyEvent& key) {
void filemanager_on_close(Window* win) { void filemanager_on_close(Window* win) {
if (win->app_data) { if (win->app_data) {
FileManagerState* fm = (FileManagerState*)win->app_data; FileManagerState* fm = (FileManagerState*)win->app_data;
filemanager_free_app_icons(fm); filemanager_free_arrays(fm);
montauk::mfree(win->app_data); montauk::mfree(win->app_data);
win->app_data = nullptr; win->app_data = nullptr;
} }
@@ -50,33 +50,41 @@ struct FileManagerVirtualEntry {
SvgIcon icon_sm; SvgIcon icon_sm;
}; };
// Hard backstop on entries per directory: protects the pagination loop from a
// misbehaving driver and bounds memory. Far above any realistic directory.
inline constexpr int FM_ENTRY_HARD_CAP = 8192;
struct FileManagerState { struct FileManagerState {
char current_path[256]; char current_path[256];
FileManagerLocation history[16]; FileManagerLocation history[16];
int history_pos; int history_pos;
int history_count; int history_count;
char entry_names[64][128]; // Parallel per-entry arrays, dynamically grown together to entry_cap.
int entry_types[64]; // Allocated lazily via filemanager_ensure_entry_capacity().
int entry_sizes[64]; char (*entry_names)[128];
int* entry_types;
uint64_t* entry_sizes; // 64-bit: avoids overflow on files >= 2 GiB
int entry_count; int entry_count;
int entry_cap; // allocated capacity of the per-entry arrays
int selected; int selected;
int scroll_offset; int scroll_offset;
bool is_dir[64]; bool* is_dir;
int last_click_item; int last_click_item;
uint64_t last_click_time; uint64_t last_click_time;
Scrollbar scrollbar; Scrollbar scrollbar;
DesktopState* desktop; DesktopState* desktop;
bool grid_view; bool grid_view;
int drive_indices[64]; // drive number or special-folder index for Computer view entries int* drive_indices; // drive number or special-folder index for Computer view entries
int drive_kinds[64]; // kernel BlockDeviceKind for FM_ENTRY_DRIVE entries (4 = USB MSC) int* drive_kinds; // kernel BlockDeviceKind for FM_ENTRY_DRIVE entries (4 = USB MSC)
// Clipboard (supports multiple paths from a marquee selection) // Clipboard (supports multiple paths from a selection), grown to clipboard_cap.
char clipboard_paths[64][256]; char (*clipboard_paths)[256];
int clipboard_cap;
int clipboard_count; int clipboard_count;
bool clipboard_is_cut; bool clipboard_is_cut;
// Multi-selection (grid view marquee selection) // Multi-selection (marquee selection), sized with the entry arrays.
bool multi_selected[64]; bool* multi_selected;
int multi_count; int multi_count;
// Marquee drag state. Coordinates are stored in content space: // Marquee drag state. Coordinates are stored in content space:
@@ -107,11 +115,14 @@ struct FileManagerState {
int pathbar_cursor; int pathbar_cursor;
int pathbar_len; int pathbar_len;
// Virtual views // Virtual views (sized with the entry arrays)
FileManagerVirtualViewKind virtual_view; FileManagerVirtualViewKind virtual_view;
FileManagerVirtualEntry virtual_entries[64]; FileManagerVirtualEntry* virtual_entries;
int virtual_entry_count; int virtual_entry_count;
// Background file operation (copy/move/delete) + its progress dialog.
void* active_fileop; // FileOpJob*, non-null while an operation runs
// File Manager-owned dialogs // File Manager-owned dialogs
void* delete_confirm_dialog; void* delete_confirm_dialog;
}; };
@@ -204,6 +215,14 @@ int detect_file_type(const char* name, bool is_dir);
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name); void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name);
const char* path_basename(const char* path); const char* path_basename(const char* path);
// Grow the per-entry parallel arrays to hold at least `needed` entries.
// Returns false if allocation fails (callers should stop adding entries).
bool filemanager_ensure_entry_capacity(FileManagerState* fm, int needed);
// Grow the clipboard path array to hold at least `needed` paths.
bool filemanager_ensure_clipboard_capacity(FileManagerState* fm, int needed);
// Free all dynamically-allocated arrays (called on window close).
void filemanager_free_arrays(FileManagerState* fm);
void filemanager_read_drives(FileManagerState* fm); void filemanager_read_drives(FileManagerState* fm);
void filemanager_read_dir(FileManagerState* fm); void filemanager_read_dir(FileManagerState* fm);
void filemanager_free_app_icons(FileManagerState* fm); void filemanager_free_app_icons(FileManagerState* fm);
@@ -227,6 +246,8 @@ void filemanager_delete_selected(FileManagerState* fm);
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx); void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx);
void filemanager_close_ctx_menu(FileManagerState* fm); void filemanager_close_ctx_menu(FileManagerState* fm);
void props_center_dialog(DesktopState* ds, const void* owner_app_data,
int w, int h, int* out_x, int* out_y);
void filemanager_show_properties(FileManagerState* fm, int target_idx); void filemanager_show_properties(FileManagerState* fm, int target_idx);
void filemanager_show_multi_properties(FileManagerState* fm, void filemanager_show_multi_properties(FileManagerState* fm,
const int* indices, int count); const int* indices, int count);
@@ -249,6 +270,19 @@ void filemanager_commit_pathbar(FileManagerState* fm);
void filemanager_open_entry(FileManagerState* fm, int idx); void filemanager_open_entry(FileManagerState* fm, int idx);
// ---- Async file operations (copy/move/delete on a worker thread) ----
enum FileOpKind : int {
FILE_OP_COPY = 0,
FILE_OP_MOVE = 1,
FILE_OP_DELETE = 2,
};
// Start a background copy/move of clipboard contents into the current dir,
// or a background delete of the given paths, showing a progress dialog.
// Returns true if a job was started (caller should not also do it inline).
bool filemanager_start_paste_job(FileManagerState* fm);
bool filemanager_start_delete_job(FileManagerState* fm, const char* const* paths, int count);
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols); int filemanager_grid_row_height(FileManagerState* fm, int row, int cols);
int filemanager_grid_row_y(FileManagerState* fm, int row, int cols); int filemanager_grid_row_y(FileManagerState* fm, int row, int cols);
int filemanager_grid_content_height(FileManagerState* fm, int cols); int filemanager_grid_content_height(FileManagerState* fm, int cols);
@@ -0,0 +1,435 @@
/*
* fileop.cpp
* Background copy/move/delete worker + progress dialog for the embedded
* desktop file manager. Long file operations run on a worker thread so the
* desktop UI never freezes; a modal progress dialog shows the current item,
* a determinate bar, and a Cancel button.
* Copyright (c) 2026 Daniel Hammer
*/
#include "filemanager_internal.hpp"
#include <gui/mtk/hosts.hpp>
#include <montauk/thread.h>
namespace filemanager {
// Inline (synchronous, no dialog) threshold for a single plain file paste.
// Anything larger -- or any directory / multi-item operation -- runs async.
static constexpr uint64_t FILEOP_INLINE_MAX_BYTES = 16ull * 1024 * 1024;
struct FileOpJob {
int kind; // FileOpKind: copy / move / delete
char (*paths)[256]; // owned: source paths (copy/move) or targets (delete)
int count;
char dest_dir[256]; // destination directory (copy/move); empty for delete
DesktopState* desktop;
FileManagerState* owner; // for the post-op refresh (liveness-checked)
Color accent;
volatile int files_total;
volatile int files_done;
volatile int cancel; // UI -> worker
volatile int finished; // worker -> UI
volatile char current[128]; // basename of the item in progress (worker writes)
int tid;
int last_drawn_done; // dialog redraw tracking
int mouse_x, mouse_y;
};
// ============================================================================
// Helpers
// ============================================================================
static bool fileop_path_same_or_descendant(const char* path, const char* root) {
if (!path || !root) return false;
int root_len = montauk::slen(root);
while (root_len > 0 && root[root_len - 1] == '/') root_len--;
if (root_len <= 0) return false;
for (int i = 0; i < root_len; i++) {
if (path[i] == '\0' || path[i] != root[i]) return false;
}
return path[root_len] == '\0' || path[root_len] == '/';
}
static void fileop_set_current(FileOpJob* job, const char* name) {
if (!job || !name) return;
int i = 0;
for (; name[i] && i < 127; i++) job->current[i] = name[i];
job->current[i] = '\0';
}
static bool fileop_path_is_dir(const char* path) {
const char* probe[1];
return montauk::readdir(path, probe, 1) >= 0;
}
// ============================================================================
// Worker thread
// ============================================================================
static int fileop_worker(void* arg) {
FileOpJob* job = (FileOpJob*)arg;
for (int i = 0; i < job->count && !job->cancel; i++) {
const char* src = job->paths[i];
const char* base = path_basename(src);
fileop_set_current(job, base);
if (job->kind == FILE_OP_DELETE) {
filemanager_delete_recursive(src);
job->files_done = i + 1;
continue;
}
// Copy or move into dest_dir.
char dst[512];
filemanager_build_fullpath(dst, 512, job->dest_dir, base);
if (montauk::streq(dst, src)) { job->files_done = i + 1; continue; }
bool is_dir = fileop_path_is_dir(src);
if (is_dir && fileop_path_same_or_descendant(dst, src)) {
job->files_done = i + 1;
continue; // refuse to copy a directory into itself
}
if (job->kind == FILE_OP_MOVE) {
// Try a rename first: an instant same-volume move with no copying.
if (montauk::frename(src, dst) == 0) { job->files_done = i + 1; continue; }
// Cross-volume (or driver without rename): fall back to copy + delete.
bool ok = is_dir ? filemanager_copy_dir_recursive(src, dst)
: filemanager_copy_file(src, dst);
if (ok) {
if (is_dir) filemanager_delete_recursive(src);
else montauk::fdelete(src);
}
} else { // FILE_OP_COPY
if (is_dir) filemanager_copy_dir_recursive(src, dst);
else filemanager_copy_file(src, dst);
}
job->files_done = i + 1;
}
job->finished = 1;
return 0;
}
// ============================================================================
// Progress dialog
// ============================================================================
static const char* fileop_verb(int kind) {
switch (kind) {
case FILE_OP_COPY: return "Copying";
case FILE_OP_MOVE: return "Moving";
case FILE_OP_DELETE: return "Deleting";
default: return "Working";
}
}
static void fileop_cancel_button_rect(int cw, int ch, Rect* out) {
int btn_w = 88, btn_h = 28;
*out = { cw - btn_w - 16, ch - btn_h - 14, btn_w, btn_h };
}
static FileManagerState* fileop_live_owner(FileOpJob* job) {
if (!job || !job->desktop || !job->owner) return nullptr;
for (int i = 0; i < job->desktop->window_count; i++) {
Window& win = job->desktop->windows[i];
if (win.app_data == job->owner && win.on_draw == filemanager_on_draw)
return job->owner;
}
return nullptr;
}
static void fileop_close_self(DesktopState* ds, FileOpJob* job) {
if (!ds) return;
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].app_data == job) {
desktop_close_window(ds, i);
return;
}
}
}
static void fileop_dialog_on_draw(Window* win, Framebuffer& fb) {
(void)fb;
FileOpJob* job = (FileOpJob*)win->app_data;
if (!job) return;
Canvas c(win);
mtk::Theme theme = mtk::make_theme(job->accent);
c.fill(theme.window_bg);
int pad = 16;
int fh = system_font_height();
int y = 18;
// Title: "Copying 3 of 12"
char title[96];
int done = job->files_done;
int total = job->files_total;
if (total > 1)
snprintf(title, sizeof(title), "%s %d of %d", fileop_verb(job->kind),
done < total ? done + 1 : total, total);
else
snprintf(title, sizeof(title), "%s...", fileop_verb(job->kind));
c.text(pad, y, title, theme.text);
y += fh + 10;
// Current item name (copied out of the volatile buffer).
char cur[128];
int k = 0;
for (; k < 127 && job->current[k]; k++) cur[k] = job->current[k];
cur[k] = '\0';
if (cur[0]) {
char fit[128];
// Trim to width using the shared helper if available; otherwise show raw.
montauk::strncpy(fit, cur, sizeof(fit) - 1);
fit[sizeof(fit) - 1] = '\0';
c.text(pad, y, fit, theme.text_muted);
}
y += fh + 12;
// Progress bar.
int bar_x = pad;
int bar_w = c.w - pad * 2;
int bar_h = 10;
c.fill_rounded_rect(bar_x, y, bar_w, bar_h, 5,
Color::from_rgb(0xE0, 0xE0, 0xE0));
int frac_w = 0;
if (total > 0) {
if (done > total) done = total;
frac_w = (int)(((int64_t)bar_w * done) / total);
}
if (frac_w > 0)
c.fill_rounded_rect(bar_x, y, frac_w, bar_h, 5, job->accent);
// Cancel button.
Rect cancel_btn;
fileop_cancel_button_rect(c.w, c.h, &cancel_btn);
bool hover = cancel_btn.contains(job->mouse_x, job->mouse_y);
mtk::draw_button(c, cancel_btn, job->finished ? "Close" : "Cancel",
mtk::BUTTON_SECONDARY,
mtk::widget_state(false, hover, true), theme);
}
static void fileop_dialog_on_mouse(Window* win, MouseEvent& ev) {
FileOpJob* job = (FileOpJob*)win->app_data;
if (!job) return;
mtk::DesktopHost host(win);
int mx = 0, my = 0;
if (!host.map_mouse(ev, &mx, &my)) return;
Rect cancel_btn;
fileop_cancel_button_rect(win->content_w, win->content_h, &cancel_btn);
bool old_hover = cancel_btn.contains(job->mouse_x, job->mouse_y);
bool new_hover = cancel_btn.contains(mx, my);
job->mouse_x = mx;
job->mouse_y = my;
if (old_hover != new_hover) host.invalidate();
if (ev.left_pressed() && new_hover) {
// Request cancel; the dialog closes once the worker observes it.
job->cancel = 1;
if (job->finished) fileop_close_self(job->desktop, job);
}
}
static void fileop_dialog_on_key(Window* win, const Montauk::KeyEvent& key) {
FileOpJob* job = (FileOpJob*)win->app_data;
if (!job || !key.pressed) return;
if (key.scancode == 0x01) job->cancel = 1; // Escape requests cancel
}
static void fileop_dialog_on_poll(Window* win) {
FileOpJob* job = (FileOpJob*)win->app_data;
if (!job) return;
if (job->files_done != job->last_drawn_done) {
job->last_drawn_done = job->files_done;
win->dirty = true;
}
if (job->finished) {
// Hand off to on_close, which joins the worker, refreshes the file
// manager, and frees the job exactly once.
fileop_close_self(job->desktop, job);
}
}
static void fileop_dialog_on_close(Window* win) {
FileOpJob* job = (FileOpJob*)win->app_data;
if (!job) return;
// Make sure the worker has stopped before we free anything it touches.
if (job->tid > 0) {
if (!job->finished) job->cancel = 1;
montauk::thread_join(job->tid, nullptr);
job->tid = 0;
}
FileManagerState* owner = fileop_live_owner(job);
if (owner) {
if (owner->active_fileop == job) owner->active_fileop = nullptr;
filemanager_clear_multi_selection(owner);
filemanager_read_dir(owner);
}
if (job->paths) montauk::mfree(job->paths);
montauk::mfree(job);
win->app_data = nullptr;
}
static bool fileop_show_dialog(FileOpJob* job) {
DesktopState* ds = job->desktop;
int w = 380, h = 150;
int wx = 0, wy = 0;
props_center_dialog(ds, job->owner, w, h, &wx, &wy);
const char* dlg_title = (job->kind == FILE_OP_DELETE) ? "Deleting"
: (job->kind == FILE_OP_MOVE) ? "Moving"
: "Copying";
int idx = desktop_create_window(ds, dlg_title, wx, wy, w, h);
if (idx < 0) return false;
Window* win = &ds->windows[idx];
win->app_data = job;
win->on_draw = fileop_dialog_on_draw;
win->on_mouse = fileop_dialog_on_mouse;
win->on_key = fileop_dialog_on_key;
win->on_close = fileop_dialog_on_close;
win->on_poll = fileop_dialog_on_poll;
return true;
}
// Spawn the worker + dialog. Takes ownership of `job` on success. On failure
// (no thread / no window) runs the work synchronously and frees the job.
static bool fileop_launch(FileOpJob* job) {
if (job->owner) job->owner->active_fileop = job;
// Generous stack: copy/delete recurse with ~2.5 KiB of buffers per directory
// level, so this comfortably handles deeply nested trees.
int tid = montauk::thread_spawn(fileop_worker, job, 256 * 1024);
if (tid <= 0) {
// Degraded path: do it inline (UI blocks), then refresh.
fileop_worker(job);
FileManagerState* owner = fileop_live_owner(job);
if (owner) {
owner->active_fileop = nullptr;
filemanager_clear_multi_selection(owner);
filemanager_read_dir(owner);
}
if (job->paths) montauk::mfree(job->paths);
montauk::mfree(job);
return true;
}
job->tid = tid;
if (!fileop_show_dialog(job)) {
// Window creation failed: let the worker finish, then reap + refresh
// without a dialog.
montauk::thread_join(job->tid, nullptr);
FileManagerState* owner = fileop_live_owner(job);
if (owner) {
owner->active_fileop = nullptr;
filemanager_clear_multi_selection(owner);
filemanager_read_dir(owner);
}
if (job->paths) montauk::mfree(job->paths);
montauk::mfree(job);
return true;
}
return true;
}
static FileOpJob* fileop_alloc_job(int kind, int count) {
if (count <= 0) return nullptr;
FileOpJob* job = (FileOpJob*)montauk::malloc(sizeof(FileOpJob));
if (!job) return nullptr;
montauk::memset(job, 0, sizeof(FileOpJob));
job->paths = (char(*)[256])montauk::malloc((uint64_t)count * 256);
if (!job->paths) { montauk::mfree(job); return nullptr; }
job->kind = kind;
job->count = count;
job->files_total = count;
return job;
}
// ============================================================================
// Public entry points
// ============================================================================
bool filemanager_start_paste_job(FileManagerState* fm) {
if (!fm || fm->active_fileop) return false;
if (fm->clipboard_count <= 0) return false;
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return false;
bool is_cut = fm->clipboard_is_cut;
// Fast path: a single small plain file pastes inline so quick copies stay
// snappy (no thread, no dialog flash).
if (fm->clipboard_count == 1 && !fileop_path_is_dir(fm->clipboard_paths[0])) {
const char* src = fm->clipboard_paths[0];
uint64_t sz = 0;
int fd = montauk::open(src);
if (fd >= 0) { sz = montauk::getsize(fd); montauk::close(fd); }
if (sz <= FILEOP_INLINE_MAX_BYTES) {
char dst[512];
filemanager_build_fullpath(dst, 512, fm->current_path, path_basename(src));
if (!montauk::streq(dst, src)) {
if (is_cut) {
if (montauk::frename(src, dst) != 0) {
if (filemanager_copy_file(src, dst)) montauk::fdelete(src);
}
} else {
filemanager_copy_file(src, dst);
}
}
if (is_cut) fm->clipboard_count = 0;
filemanager_read_dir(fm);
return true;
}
}
FileOpJob* job = fileop_alloc_job(is_cut ? FILE_OP_MOVE : FILE_OP_COPY,
fm->clipboard_count);
if (!job) return false;
for (int i = 0; i < fm->clipboard_count; i++) {
montauk::strncpy(job->paths[i], fm->clipboard_paths[i], 255);
job->paths[i][255] = '\0';
}
montauk::strncpy(job->dest_dir, fm->current_path, sizeof(job->dest_dir) - 1);
job->dest_dir[sizeof(job->dest_dir) - 1] = '\0';
job->desktop = fm->desktop;
job->owner = fm;
job->accent = fm->desktop ? fm->desktop->settings.accent_color : Color{};
// A cut is consumed when the paste starts (paths are already copied in).
if (is_cut) fm->clipboard_count = 0;
return fileop_launch(job);
}
bool filemanager_start_delete_job(FileManagerState* fm,
const char* const* paths, int count) {
if (!fm || fm->active_fileop) return false;
if (!paths || count <= 0) return false;
FileOpJob* job = fileop_alloc_job(FILE_OP_DELETE, count);
if (!job) return false;
for (int i = 0; i < count; i++) {
montauk::strncpy(job->paths[i], paths[i] ? paths[i] : "", 255);
job->paths[i][255] = '\0';
}
job->dest_dir[0] = '\0';
job->desktop = fm->desktop;
job->owner = fm;
job->accent = fm->desktop ? fm->desktop->settings.accent_color : Color{};
return fileop_launch(job);
}
} // namespace filemanager
@@ -8,6 +8,92 @@
namespace filemanager { 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;
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->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) { static void filemanager_reset_list_state(FileManagerState* fm) {
fm->selected = -1; fm->selected = -1;
fm->scroll_offset = 0; fm->scroll_offset = 0;
@@ -33,7 +119,7 @@ static void filemanager_add_root_entry(FileManagerState* fm,
const char* name, const char* name,
FileManagerEntryType type, FileManagerEntryType type,
int drive_index) { int drive_index) {
if (!fm || fm->entry_count >= 64) return; if (!fm || !filemanager_ensure_entry_capacity(fm, fm->entry_count + 1)) return;
int idx = fm->entry_count++; int idx = fm->entry_count++;
montauk::strncpy(fm->entry_names[idx], name, 63); montauk::strncpy(fm->entry_names[idx], name, 63);
@@ -51,7 +137,7 @@ static int filemanager_add_virtual_entry(FileManagerState* fm,
const char* name, const char* name,
const char* icon_path, const char* icon_path,
FileManagerVirtualEntryKind kind) { FileManagerVirtualEntryKind kind) {
if (!fm || fm->entry_count >= 64) return -1; if (!fm || !filemanager_ensure_entry_capacity(fm, fm->entry_count + 1)) return -1;
int idx = fm->entry_count; int idx = fm->entry_count;
montauk::strncpy(fm->entry_names[idx], name, 63); montauk::strncpy(fm->entry_names[idx], name, 63);
@@ -117,7 +203,7 @@ void filemanager_read_drives(FileManagerState* fm) {
// Special user folders (Documents, Desktop, Music, etc.) - only if they exist // Special user folders (Documents, Desktop, Music, etc.) - only if they exist
if (ds && ds->home_dir[0] != '\0') { if (ds && ds->home_dir[0] != '\0') {
for (int sf = 0; sf < SF_COUNT && fm->entry_count < 64; sf++) { for (int sf = 0; sf < SF_COUNT; sf++) {
char probe_path[256]; char probe_path[256];
montauk::strcpy(probe_path, ds->home_dir); montauk::strcpy(probe_path, ds->home_dir);
int plen = montauk::slen(probe_path); int plen = montauk::slen(probe_path);
@@ -167,7 +253,6 @@ void filemanager_read_drives(FileManagerState* fm) {
str_append(label, ")", 64); str_append(label, ")", 64);
} }
filemanager_add_root_entry(fm, label, FM_ENTRY_DRIVE, d); filemanager_add_root_entry(fm, label, FM_ENTRY_DRIVE, d);
if (fm->entry_count >= 64) break;
} }
filemanager_reset_list_state(fm); filemanager_reset_list_state(fm);
@@ -181,10 +266,7 @@ void filemanager_read_dir(FileManagerState* fm) {
filemanager_free_app_icons(fm); filemanager_free_app_icons(fm);
fm->virtual_view = FM_VIRTUAL_VIEW_NONE; fm->virtual_view = FM_VIRTUAL_VIEW_NONE;
fm->entry_count = 0;
const char* names[64];
fm->entry_count = montauk::readdir(fm->current_path, names, 64);
if (fm->entry_count < 0) fm->entry_count = 0;
// readdir returns full paths from the VFS (e.g. "man/fetch.1" instead // 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. // of just "fetch.1"). Compute the prefix to strip so we get basenames.
@@ -206,8 +288,19 @@ void filemanager_read_dir(FileManagerState* fm) {
} }
} }
for (int i = 0; i < fm->entry_count; i++) { // Page through the directory. The kernel returns a bounded batch per call,
const char* raw = names[i]; // 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 // Strip directory prefix if it matches
if (prefix_len > 0) { if (prefix_len > 0) {
bool match = true; bool match = true;
@@ -229,7 +322,7 @@ void filemanager_read_dir(FileManagerState* fm) {
fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]); fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]);
// Get file size // Get file size (64-bit: no truncation for files >= 2 GiB)
fm->entry_sizes[i] = 0; fm->entry_sizes[i] = 0;
if (!fm->is_dir[i]) { if (!fm->is_dir[i]) {
char fullpath[512]; char fullpath[512];
@@ -241,17 +334,21 @@ void filemanager_read_dir(FileManagerState* fm) {
str_append(fullpath, fm->entry_names[i], 512); str_append(fullpath, fm->entry_names[i], 512);
int fd = montauk::open(fullpath); int fd = montauk::open(fullpath);
if (fd >= 0) { if (fd >= 0) {
fm->entry_sizes[i] = (int)montauk::getsize(fd); fm->entry_sizes[i] = montauk::getsize(fd);
montauk::close(fd); montauk::close(fd);
} }
} }
} }
fm->entry_count += got;
if (fm->entry_count >= FM_ENTRY_HARD_CAP) break;
}
// Sort: directories first, then alphabetical (case-insensitive) // Sort: directories first, then alphabetical (case-insensitive)
for (int i = 1; i < fm->entry_count; i++) { for (int i = 1; i < fm->entry_count; i++) {
char tmp_name[128]; char tmp_name[128];
int tmp_type = fm->entry_types[i]; int tmp_type = fm->entry_types[i];
int tmp_size = fm->entry_sizes[i]; uint64_t tmp_size = fm->entry_sizes[i];
bool tmp_isdir = fm->is_dir[i]; bool tmp_isdir = fm->is_dir[i];
montauk::strcpy(tmp_name, fm->entry_names[i]); montauk::strcpy(tmp_name, fm->entry_names[i]);
@@ -315,7 +412,7 @@ void filemanager_read_apps(FileManagerState* fm) {
DesktopState* ds = fm->desktop; DesktopState* ds = fm->desktop;
if (!ds) return; if (!ds) return;
for (int i = 0; i < ds->external_app_count && fm->entry_count < 64; i++) { for (int i = 0; i < ds->external_app_count; i++) {
filemanager_add_external_app_entry(fm, ds->external_apps[i], i); filemanager_add_external_app_entry(fm, ds->external_apps[i], i);
} }
@@ -327,24 +424,23 @@ void filemanager_read_system_configuration(FileManagerState* fm) {
DesktopAppletEntry applets[32]; DesktopAppletEntry applets[32];
int applet_count = desktop_list_system_configuration_applets(fm->desktop, applets, 32); int applet_count = desktop_list_system_configuration_applets(fm->desktop, applets, 32);
for (int i = 0; i < applet_count && fm->entry_count < 64; i++) { for (int i = 0; i < applet_count; i++) {
filemanager_add_system_configuration_applet_entry(fm, applets[i]); filemanager_add_system_configuration_applet_entry(fm, applets[i]);
} }
filemanager_reset_list_state(fm); filemanager_reset_list_state(fm);
} }
bool filemanager_delete_recursive(const char* path) { // Read ALL direct children of `dir` into a freshly-allocated array of basenames
// Try deleting as a file (or empty directory) first // (each up to 255 chars, directory entries keep their trailing '/'). Pages the
if (montauk::fdelete(path) == 0) return true; // 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;
// If that failed, it may be a non-empty directory -- enumerate and delete children // Compute the prefix to strip (readdir returns drive-root-relative paths).
const char* names[64]; const char* after_drive = dir;
int count = montauk::readdir(path, names, 64);
if (count < 0) return false;
// Compute the prefix to strip (readdir returns paths relative to drive root)
const char* after_drive = path;
for (int k = 0; after_drive[k]; k++) { for (int k = 0; after_drive[k]; k++) {
if (after_drive[k] == ':' && after_drive[k + 1] == '/') { if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
after_drive += k + 2; after_drive += k + 2;
@@ -362,8 +458,31 @@ bool filemanager_delete_recursive(const char* path) {
} }
} }
for (int i = 0; i < count; i++) { static constexpr int BATCH = 128;
const char* raw = names[i]; 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) { if (prefix_len > 0) {
bool match = true; bool match = true;
for (int k = 0; k < prefix_len; k++) { for (int k = 0; k < prefix_len; k++) {
@@ -371,13 +490,36 @@ bool filemanager_delete_recursive(const char* path) {
} }
if (match) raw += prefix_len; 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]; char child[512];
montauk::strcpy(child, path); montauk::strcpy(child, path);
int plen = montauk::slen(child); int plen = montauk::slen(child);
if (plen > 0 && child[plen - 1] != '/') if (plen > 0 && child[plen - 1] != '/')
str_append(child, "/", 512); str_append(child, "/", 512);
str_append(child, raw, 512); str_append(child, children[i], 512);
// Strip trailing slash if present (directory marker) // Strip trailing slash if present (directory marker)
int clen = montauk::slen(child); int clen = montauk::slen(child);
@@ -386,6 +528,8 @@ bool filemanager_delete_recursive(const char* path) {
filemanager_delete_recursive(child); filemanager_delete_recursive(child);
} }
if (children) montauk::mfree(children);
// Now the directory should be empty -- delete it // Now the directory should be empty -- delete it
return montauk::fdelete(path) == 0; return montauk::fdelete(path) == 0;
} }
@@ -446,42 +590,15 @@ bool filemanager_copy_dir_recursive(const char* src, const char* dst) {
montauk::fmkdir(dst); montauk::fmkdir(dst);
const char* names[64]; char (*children)[256] = nullptr;
int count = montauk::readdir(src, names, 64); int count = filemanager_read_all_children(src, &children);
if (count < 0) return false; if (count < 0) return false;
// Compute prefix to strip
const char* after_drive = src;
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';
}
}
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
const char* raw = names[i];
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;
}
bool child_is_dir = false; bool child_is_dir = false;
char basename[64]; char basename[256];
montauk::strncpy(basename, raw, 63); montauk::strncpy(basename, children[i], 255);
basename[255] = '\0';
int blen = montauk::slen(basename); int blen = montauk::slen(basename);
if (blen > 0 && basename[blen - 1] == '/') { if (blen > 0 && basename[blen - 1] == '/') {
child_is_dir = true; child_is_dir = true;
@@ -498,6 +615,8 @@ bool filemanager_copy_dir_recursive(const char* src, const char* dst) {
filemanager_copy_file(src_child, dst_child); filemanager_copy_file(src_child, dst_child);
} }
if (children) montauk::mfree(children);
return true; return true;
} }
@@ -10,7 +10,8 @@ namespace filemanager {
void filemanager_clear_multi_selection(FileManagerState* fm) { void filemanager_clear_multi_selection(FileManagerState* fm) {
if (!fm) return; if (!fm) return;
for (int i = 0; i < 64; i++) fm->multi_selected[i] = false; if (fm->multi_selected && fm->entry_cap > 0)
montauk::memset(fm->multi_selected, 0, (uint64_t)fm->entry_cap * sizeof(bool));
fm->multi_count = 0; fm->multi_count = 0;
} }
@@ -18,8 +19,7 @@ int filemanager_collect_selection(const FileManagerState* fm, int* out, int max)
if (!fm || !out || max <= 0) return 0; if (!fm || !out || max <= 0) return 0;
int n = 0; int n = 0;
if (fm->multi_count > 0) { if (fm->multi_count > 0) {
int limit = fm->entry_count < 64 ? fm->entry_count : 64; for (int i = 0; i < fm->entry_count && n < max; i++) {
for (int i = 0; i < limit && n < max; i++) {
if (fm->multi_selected[i]) out[n++] = i; if (fm->multi_selected[i]) out[n++] = i;
} }
} else if (fm->selected >= 0 && fm->selected < fm->entry_count) { } else if (fm->selected >= 0 && fm->selected < fm->entry_count) {
@@ -41,8 +41,9 @@ struct FileDeleteConfirmDialogState {
char path[512]; // primary path (used for single-item subtitle) char path[512]; // primary path (used for single-item subtitle)
SvgIcon icon; SvgIcon icon;
// Multi-item targets. count == 1 keeps the original single-folder UX. // Multi-item targets (dynamically allocated). count == 1 keeps the original
char paths[64][512]; // single-folder UX.
char (*paths)[512];
int count; int count;
}; };
@@ -87,7 +88,7 @@ static void props_close_self(DesktopState* ds, void* app_data) {
} }
} }
static void props_center_dialog(DesktopState* ds, void props_center_dialog(DesktopState* ds,
const void* owner_app_data, const void* owner_app_data,
int w, int w,
int h, int h,
@@ -441,22 +442,38 @@ static void delete_confirm_apply(FileDeleteConfirmDialogState* st) {
if (!st) return; if (!st) return;
FileManagerState* owner = delete_confirm_live_owner(st); FileManagerState* owner = delete_confirm_live_owner(st);
bool any = false;
int n = st->count > 0 ? st->count : (st->path[0] ? 1 : 0); int n = st->count > 0 ? st->count : (st->path[0] ? 1 : 0);
// Delete on a worker thread with a progress dialog. The job copies the path
// list, so it stays valid after this confirm dialog is freed.
bool started = false;
if (owner && n > 0) {
const char** arr = (const char**)montauk::malloc((uint64_t)n * sizeof(const char*));
if (arr) {
for (int i = 0; i < n; i++)
arr[i] = (st->count > 0) ? st->paths[i] : st->path;
started = filemanager_start_delete_job(owner, arr, n);
montauk::mfree(arr);
}
}
if (!started) {
// Fallback: delete synchronously (e.g. another operation is running).
bool any = false;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
const char* p = (st->count > 0) ? st->paths[i] : st->path; const char* p = (st->count > 0) ? st->paths[i] : st->path;
if (!p || !p[0]) continue; if (!p || !p[0]) continue;
if (filemanager_delete_recursive(p)) any = true; if (filemanager_delete_recursive(p)) any = true;
} }
if (owner) { if (owner && any) {
if (owner->delete_confirm_dialog == st)
owner->delete_confirm_dialog = nullptr;
if (any) {
filemanager_clear_multi_selection(owner); filemanager_clear_multi_selection(owner);
filemanager_read_dir(owner); filemanager_read_dir(owner);
} }
} }
if (owner && owner->delete_confirm_dialog == st)
owner->delete_confirm_dialog = nullptr;
delete_confirm_close(st); delete_confirm_close(st);
} }
@@ -560,6 +577,7 @@ static void delete_confirm_on_close(Window* win) {
if (owner && owner->delete_confirm_dialog == st) if (owner && owner->delete_confirm_dialog == st)
owner->delete_confirm_dialog = nullptr; owner->delete_confirm_dialog = nullptr;
if (st->paths) montauk::mfree(st->paths);
montauk::mfree(st); montauk::mfree(st);
win->app_data = nullptr; win->app_data = nullptr;
} }
@@ -650,8 +668,14 @@ void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
st->owner = fm; st->owner = fm;
st->accent = ds->settings.accent_color; st->accent = ds->settings.accent_color;
st->paths = (char(*)[512])montauk::malloc((uint64_t)count * 512);
if (!st->paths) {
montauk::mfree(st);
return;
}
int out = 0; int out = 0;
for (int i = 0; i < count && out < 64; i++) { for (int i = 0; i < count; i++) {
int idx = indices[i]; int idx = indices[i];
if (idx < 0 || idx >= fm->entry_count) continue; if (idx < 0 || idx >= fm->entry_count) continue;
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue; if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
@@ -660,6 +684,7 @@ void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
out++; out++;
} }
if (out == 0) { if (out == 0) {
montauk::mfree(st->paths);
montauk::mfree(st); montauk::mfree(st);
return; return;
} }
@@ -803,7 +828,7 @@ void filemanager_show_multi_properties(FileManagerState* fm,
st->desktop = ds; st->desktop = ds;
st->accent = ds->settings.accent_color; st->accent = ds->settings.accent_color;
int total_size = 0; uint64_t total_size = 0;
bool any_file = false; bool any_file = false;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
int idx = indices[i]; int idx = indices[i];
@@ -341,7 +341,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
// Selection highlight (single or multi) // Selection highlight (single or multi)
bool cell_selected = (i == fm->selected) || bool cell_selected = (i == fm->selected) ||
(i < 64 && fm->multi_selected[i]); (fm->multi_selected && fm->multi_selected[i]);
if (cell_selected) { if (cell_selected) {
int sy = gui_max(cell_y, list_y); int sy = gui_max(cell_y, list_y);
int sh = gui_min(cell_y + row_h, c.h) - sy; int sh = gui_min(cell_y + row_h, c.h) - sy;
@@ -480,7 +480,8 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue; if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue;
// Highlight selected (single or marquee multi-selection) // Highlight selected (single or marquee multi-selection)
bool row_selected = (i == fm->selected) || (i < 64 && fm->multi_selected[i]); bool row_selected = (i == fm->selected) ||
(fm->multi_selected && fm->multi_selected[i]);
if (row_selected) { if (row_selected) {
int sy = gui_max(iy, list_y); int sy = gui_max(iy, list_y);
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy; int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
+33
View File
@@ -11,6 +11,7 @@
#include <montauk/heap.h> #include <montauk/heap.h>
#include <gui/gui.hpp> #include <gui/gui.hpp>
#include <gui/truetype.hpp> #include <gui/truetype.hpp>
#include <gui/mtk/widgets.hpp>
extern "C" { extern "C" {
#include <string.h> #include <string.h>
@@ -107,6 +108,25 @@ struct DisplayRow {
static constexpr int MAX_DISPLAY_ROWS = MAX_TOTAL_DEVS + NUM_CATEGORIES; static constexpr int MAX_DISPLAY_ROWS = MAX_TOTAL_DEVS + NUM_CATEGORIES;
// ============================================================================
// Scroll metrics (pixel-space) — shared by render and input handling so the
// draggable MTK scrollbar and the row layout stay in sync.
// ============================================================================
struct ScrollMetrics {
int list_y; // top of the scrollable list area
int list_h; // viewport height (view extent)
int total_h; // total content height (content extent)
int max_scroll_px; // maximum pixel scroll offset
int scroll_px; // current pixel scroll offset (derived from scroll_y)
};
// Bounds of the toolbar "Refresh" button — shared by render and hit-testing.
inline Rect refresh_button_rect() {
constexpr int btn_w = 80, btn_h = 26;
return { 8, (TOOLBAR_H - btn_h) / 2, btn_w, btn_h };
}
// ============================================================================ // ============================================================================
// Disk detail window // Disk detail window
// ============================================================================ // ============================================================================
@@ -142,6 +162,14 @@ struct DevExplorerState {
int last_click_row; int last_click_row;
uint64_t last_click_ms; uint64_t last_click_ms;
// Draggable MTK scrollbar interaction state
int mouse_x, mouse_y; // last known pointer position (for hover)
bool sb_hover; // pointer is over the scrollbar track
bool sb_dragging; // thumb is being dragged
int sb_drag_offset; // pointer offset from thumb top when drag began
bool btn_hover; // pointer is over the Refresh button
DiskDetailState detail; DiskDetailState detail;
}; };
@@ -152,6 +180,7 @@ struct DevExplorerState {
extern int g_win_w, g_win_h; extern int g_win_w, g_win_h;
extern DevExplorerState g_state; extern DevExplorerState g_state;
extern TrueTypeFont* g_font; extern TrueTypeFont* g_font;
extern mtk::Theme g_theme;
// ============================================================================ // ============================================================================
// Function declarations — render.cpp // Function declarations — render.cpp
@@ -165,6 +194,10 @@ void render(uint32_t* pixels);
int build_display_rows(DevExplorerState* de, DisplayRow* rows); int build_display_rows(DevExplorerState* de, DisplayRow* rows);
int append_printer_devices(Montauk::DevInfo* out, int max_count); int append_printer_devices(Montauk::DevInfo* out, int max_count);
ScrollMetrics compute_scroll_metrics(DevExplorerState* de,
const DisplayRow* rows, int row_count);
void set_scroll_from_px(DevExplorerState* de, const DisplayRow* rows,
int row_count, int target_px);
// ============================================================================ // ============================================================================
// Function declarations — diskdetail.cpp // Function declarations — diskdetail.cpp
+163 -26
View File
@@ -16,6 +16,7 @@ int g_win_h = INIT_H;
DevExplorerState g_state; DevExplorerState g_state;
TrueTypeFont* g_font = nullptr; TrueTypeFont* g_font = nullptr;
mtk::Theme g_theme;
static void refresh_devices(DevExplorerState* de) { static void refresh_devices(DevExplorerState* de) {
if (de == nullptr) return; if (de == nullptr) return;
@@ -63,6 +64,71 @@ int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
return count; return count;
} }
// ============================================================================
// Scroll metrics
// ============================================================================
// Computes pixel-space scroll geometry and clamps de->scroll_y so the list can
// never scroll past its content. Shared by render.cpp and the input handlers.
ScrollMetrics compute_scroll_metrics(DevExplorerState* de,
const DisplayRow* rows, int row_count) {
ScrollMetrics m;
m.list_y = TOOLBAR_H;
m.list_h = g_win_h - m.list_y;
if (m.list_h < 0) m.list_h = 0;
int total_h = 0;
for (int i = 0; i < row_count; i++)
total_h += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
m.total_h = total_h;
int max_scroll_px = total_h - m.list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
m.max_scroll_px = max_scroll_px;
if (de->scroll_y < 0) de->scroll_y = 0;
if (de->scroll_y > row_count) de->scroll_y = row_count;
int scroll_px = 0;
for (int i = 0; i < de->scroll_y && i < row_count; i++)
scroll_px += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
// If the first visible row would push content past the end, snap scroll_y
// back to the furthest row that keeps the list fully populated.
if (scroll_px > max_scroll_px) {
de->scroll_y = 0;
int acc = 0;
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (acc + rh > max_scroll_px) break;
acc += rh;
de->scroll_y = i + 1;
}
scroll_px = acc;
}
m.scroll_px = scroll_px;
return m;
}
// Maps a target pixel offset (from the dragged thumb) back to the nearest row
// index, keeping scroll_y row-aligned like the keyboard and wheel navigation.
void set_scroll_from_px(DevExplorerState* de, const DisplayRow* rows,
int row_count, int target_px) {
if (target_px < 0) target_px = 0;
int acc = 0;
int best_k = 0;
int best_diff = target_px; // distance for scroll_y == 0
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
acc += rh;
int diff = gui_abs(acc - target_px);
if (diff < best_diff) { best_diff = diff; best_k = i + 1; }
}
de->scroll_y = best_k;
}
// ============================================================================ // ============================================================================
// Toolbar hit testing // Toolbar hit testing
// ============================================================================ // ============================================================================
@@ -70,10 +136,7 @@ int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
static bool handle_toolbar_click(int mx, int my) { static bool handle_toolbar_click(int mx, int my) {
if (my >= TOOLBAR_H) return false; if (my >= TOOLBAR_H) return false;
int btn_w = 80, btn_h = 26; if (refresh_button_rect().contains(mx, my)) {
int btn_x = 8;
int btn_y = (TOOLBAR_H - btn_h) / 2;
if (mx >= btn_x && mx < btn_x + btn_w && my >= btn_y && my < btn_y + btn_h) {
g_state.last_poll_ms = 0; // force refresh g_state.last_poll_ms = 0; // force refresh
return true; return true;
} }
@@ -127,6 +190,92 @@ static bool handle_list_click(int mx, int my) {
return true; return true;
} }
// ============================================================================
// Mouse handling (wheel, draggable scrollbar, clicks)
// ============================================================================
static bool handle_main_mouse(const Montauk::WinEvent& ev) {
auto& de = g_state;
int mx = ev.mouse.x;
int my = ev.mouse.y;
bool changed = false;
bool left_down = (ev.mouse.buttons & 1) != 0;
bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);
de.mouse_x = mx;
de.mouse_y = my;
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(&de, rows);
ScrollMetrics m = compute_scroll_metrics(&de, rows, row_count);
Rect viewport = { 0, m.list_y, g_win_w, m.list_h };
Rect track = mtk::scrollbar_track_rect(viewport);
Rect thumb = mtk::scrollbar_thumb_rect(track, m.total_h, m.list_h, m.scroll_px);
// Mouse wheel scrolls by rows.
if (ev.mouse.scroll != 0) {
de.scroll_y += ev.mouse.scroll;
if (de.scroll_y < 0) de.scroll_y = 0;
changed = true;
}
// Release ends any active drag.
if (just_released && de.sb_dragging) {
de.sb_dragging = false;
changed = true;
}
// Active drag follows the pointer even if it leaves the track horizontally.
if (de.sb_dragging && left_down && !track.empty() && !thumb.empty()) {
int new_top = my - de.sb_drag_offset;
int target_px = mtk::scrollbar_offset_from_thumb_top(
track, m.total_h, m.list_h, thumb.h, new_top);
set_scroll_from_px(&de, rows, row_count, target_px);
changed = true;
}
// Hover highlight on the thumb; only redraw when it changes. The track is
// only interactive when the list actually scrolls (thumb is present).
bool scrollable = !track.empty() && !thumb.empty();
bool hover_now = scrollable && track.contains(mx, my);
if (hover_now != de.sb_hover) {
de.sb_hover = hover_now;
changed = true;
}
// Hover highlight on the Refresh button.
bool btn_hover_now = refresh_button_rect().contains(mx, my);
if (btn_hover_now != de.btn_hover) {
de.btn_hover = btn_hover_now;
changed = true;
}
if (just_clicked) {
if (scrollable && thumb.contains(mx, my)) {
// Grab the thumb.
de.sb_dragging = true;
de.sb_drag_offset = my - thumb.y;
changed = true;
} else if (scrollable && track.contains(mx, my)) {
// Click on the track: center the thumb under the pointer and drag.
de.sb_dragging = true;
de.sb_drag_offset = thumb.h / 2;
int new_top = my - de.sb_drag_offset;
int target_px = mtk::scrollbar_offset_from_thumb_top(
track, m.total_h, m.list_h, thumb.h, new_top);
set_scroll_from_px(&de, rows, row_count, target_px);
changed = true;
} else if (handle_toolbar_click(mx, my) || handle_list_click(mx, my)) {
changed = true;
}
}
return changed;
}
// ============================================================================ // ============================================================================
// Key handling // Key handling
// ============================================================================ // ============================================================================
@@ -205,15 +354,15 @@ extern "C" void _start() {
for (int i = 0; i < NUM_CATEGORIES; i++) for (int i = 0; i < NUM_CATEGORIES; i++)
g_state.collapsed[i] = false; g_state.collapsed[i] = false;
// Load font // Load fonts. fonts::init() loads the shared system font (Roboto-Medium at
{ // UI_SIZE 18) which both the device list (via g_font) and the MTK widgets
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); // (button, scrollbar) render with, so point g_font at it to avoid a second
if (f) { // copy of the same face.
montauk::memset(f, 0, sizeof(TrueTypeFont)); gui::fonts::init();
if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; } g_font = gui::fonts::system_font;
}
g_font = f; // Theme for MTK widgets, built once from the user's accent color.
} g_theme = mtk::make_theme();
// Initial device poll // Initial device poll
refresh_devices(&g_state); refresh_devices(&g_state);
@@ -290,22 +439,10 @@ extern "C" void _start() {
// Mouse // Mouse
if (ev.type == 1) { if (ev.type == 1) {
int mx = ev.mouse.x; if (handle_main_mouse(ev))
int my = ev.mouse.y;
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (ev.mouse.scroll != 0) {
g_state.scroll_y += ev.mouse.scroll;
if (g_state.scroll_y < 0) g_state.scroll_y = 0;
redraw_main = true; redraw_main = true;
} }
if (clicked) {
if (handle_toolbar_click(mx, my) || handle_list_click(mx, my))
redraw_main = true;
}
}
if (redraw_main) { render(pixels); win.present(); } if (redraw_main) { render(pixels); win.present(); }
if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); } if (redraw_detail) { render_disk_detail(); montauk::win_present(g_state.detail.win_id); }
} }
+20 -42
View File
@@ -49,12 +49,11 @@ static void render_toolbar(uint32_t* px) {
px_fill(px, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG); px_fill(px, g_win_w, g_win_h, 0, 0, g_win_w, TOOLBAR_H, TOOLBAR_BG);
px_hline(px, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, BORDER_COLOR); px_hline(px, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, BORDER_COLOR);
// "Refresh" button // "Refresh" button (MTK primary button)
int btn_w = 80, btn_h = 26; Canvas canvas(px, g_win_w, g_win_h);
int btn_x = 8; mtk::WidgetState btn_state = mtk::widget_state(false, de.btn_hover, true);
int btn_y = (TOOLBAR_H - btn_h) / 2; mtk::draw_button(canvas, refresh_button_rect(), "Refresh",
px_button(px, g_win_w, g_win_h, btn_x, btn_y, btn_w, btn_h, mtk::BUTTON_PRIMARY, btn_state, g_theme);
"Refresh", ACCENT_COLOR, WHITE, 4);
// Device count on right // Device count on right
char count_str[24]; char count_str[24];
@@ -71,34 +70,11 @@ static void render_list(uint32_t* px) {
DisplayRow rows[MAX_DISPLAY_ROWS]; DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(&de, rows); int row_count = build_display_rows(&de, rows);
int list_y = TOOLBAR_H; ScrollMetrics m = compute_scroll_metrics(&de, rows, row_count);
int list_h = g_win_h - list_y; int list_y = m.list_y;
int list_h = m.list_h;
if (list_h < 1) return; if (list_h < 1) return;
// Compute total content height
int total_h = 0;
for (int i = 0; i < row_count; i++)
total_h += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
int max_scroll_px = total_h - list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
// Compute scroll pixel offset
int scroll_px = 0;
for (int i = 0; i < de.scroll_y && i < row_count; i++)
scroll_px += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (scroll_px > max_scroll_px) {
scroll_px = max_scroll_px;
de.scroll_y = 0;
int acc = 0;
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (acc + rh > max_scroll_px) break;
acc += rh;
de.scroll_y = i + 1;
}
}
if (de.scroll_y < 0) de.scroll_y = 0;
if (de.selected_row >= row_count) de.selected_row = row_count - 1; if (de.selected_row >= row_count) de.selected_row = row_count - 1;
// Draw rows // Draw rows
@@ -175,16 +151,18 @@ static void render_list(uint32_t* px) {
cur_y += row_h; cur_y += row_h;
} }
// Scrollbar // Draggable MTK scrollbar
if (total_h > list_h) { Rect viewport = { 0, list_y, g_win_w, list_h };
int sb_x = g_win_w - 6; Rect track = mtk::scrollbar_track_rect(viewport);
int thumb_h = (list_h * list_h) / total_h; Rect thumb = mtk::scrollbar_thumb_rect(track, m.total_h, list_h, m.scroll_px);
if (thumb_h < 20) thumb_h = 20; if (!track.empty() && !thumb.empty()) {
int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px; bool hovered = track.contains(de.mouse_x, de.mouse_y);
px_fill(px, g_win_w, g_win_h, sb_x, list_y, 4, list_h, mtk::Theme theme{};
Color::from_rgb(0xE0, 0xE0, 0xE0)); Color thumb_col = mtk::scrollbar_thumb_color(theme, hovered, de.sb_dragging);
px_fill(px, g_win_w, g_win_h, sb_x, thumb_y, 4, thumb_h, px_fill(px, g_win_w, g_win_h, track.x, track.y, track.w, track.h,
Color::from_rgb(0xAA, 0xAA, 0xAA)); colors::SCROLLBAR_BG);
px_fill(px, g_win_w, g_win_h, thumb.x, thumb.y, thumb.w, thumb.h,
thumb_col);
} }
} }