From 3d620673c07017f54577641d59345e201a40749b Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 7 Jun 2026 09:33:46 +0200 Subject: [PATCH] feat: filesystem and Files app support for >64 file entries, 64-bit sizes, async ops, GUI toolkit improvements to devexplorer app --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Filesystem.hpp | 11 +- kernel/src/Api/Syscall.cpp | 13 + kernel/src/Api/Syscall.hpp | 3 + kernel/src/Fs/Boot.cpp | 1 + kernel/src/Fs/Ext2.cpp | 17 +- kernel/src/Fs/Fat32.cpp | 13 +- kernel/src/Fs/Ramdisk.cpp | 22 +- kernel/src/Fs/Ramdisk.hpp | 1 + kernel/src/Fs/Vfs.cpp | 19 +- kernel/src/Fs/Vfs.hpp | 4 + programs/include/Api/Syscall.hpp | 3 + programs/include/montauk/syscall.h | 7 + .../obj/src/libprintersapplet.o | Bin 18464 -> 18520 bytes .../obj/src/libtimezonesapplet.o | Bin 18600 -> 18656 bytes programs/src/desktop/apps/apps_common.hpp | 37 +- .../src/desktop/apps/filemanager/actions.cpp | 73 +-- .../src/desktop/apps/filemanager/events.cpp | 15 +- .../apps/filemanager/filemanager_internal.hpp | 58 ++- .../src/desktop/apps/filemanager/fileop.cpp | 435 ++++++++++++++++++ .../desktop/apps/filemanager/filesystem.cpp | 311 +++++++++---- .../src/desktop/apps/filemanager/helpers.cpp | 6 +- .../desktop/apps/filemanager/properties.cpp | 63 ++- .../src/desktop/apps/filemanager/render.cpp | 5 +- programs/src/devexplorer/devexplorer.h | 33 ++ programs/src/devexplorer/main.cpp | 189 ++++++-- programs/src/devexplorer/render.cpp | 62 +-- 27 files changed, 1110 insertions(+), 293 deletions(-) create mode 100644 programs/src/desktop/apps/filemanager/fileop.cpp diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 9deb047..910f63d 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 66 +#define MONTAUK_BUILD_NUMBER 68 diff --git a/kernel/src/Api/Filesystem.hpp b/kernel/src/Api/Filesystem.hpp index 9e1892c..616ab9e 100644 --- a/kernel/src/Api/Filesystem.hpp +++ b/kernel/src/Api/Filesystem.hpp @@ -36,15 +36,24 @@ namespace Montauk { 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) { + return Sys_ReadDirAt(path, outNames, maxEntries, 0); + } + + static int Sys_ReadDirAt(const char* path, const char** outNames, int maxEntries, + int startIndex) { char resolved[256]; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; + if (startIndex < 0) return -1; // Get entries from VFS into a kernel-local array const char* kernelNames[256]; int max = maxEntries; 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; // Allocate a user-accessible page for string data via process heap diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index ed760f0..72e7efb 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -97,6 +97,19 @@ namespace Montauk { return (int64_t)Sys_ReadDir((const char*)frame->arg1, (const char**)frame->arg2, (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: return (int64_t)Sys_Alloc(frame->arg1); case SYS_FREE: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 6afe04a..ec8d527 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -255,6 +255,9 @@ namespace Montauk { /* Power.hpp -- cross-process graceful power-off request channel */ 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 // a pending action and exits; login.elf reads it, runs the shutdown stages, // then issues the matching SYS_SHUTDOWN / SYS_RESET. diff --git a/kernel/src/Fs/Boot.cpp b/kernel/src/Fs/Boot.cpp index f67bf8b..a30a7d9 100644 --- a/kernel/src/Fs/Boot.cpp +++ b/kernel/src/Fs/Boot.cpp @@ -66,6 +66,7 @@ namespace Fs { Ramdisk::Mkdir, Ramdisk::Rename, Ramdisk::GetLabel, + Ramdisk::ReadDirAt, }; } diff --git a/kernel/src/Fs/Ext2.cpp b/kernel/src/Fs/Ext2.cpp index 74d7d3c..e37b750 100644 --- a/kernel/src/Fs/Ext2.cpp +++ b/kernel/src/Fs/Ext2.cpp @@ -806,11 +806,12 @@ namespace Fs::Ext2 { static int ReadDirectoryNames(Ext2Instance& inst, const Inode& dirInode, char outNames[MaxDirEntries][MaxNameLen], - int maxEntries) { + int maxEntries, int startOffset = 0) { uint32_t dirSize = dirInode.i_size; uint32_t blockSize = inst.blockSize; uint32_t numBlocks = (dirSize + blockSize - 1) / blockSize; int count = 0; + int skipped = 0; // valid entries skipped to reach startOffset uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed(); if (!dirBuf) return 0; @@ -846,6 +847,14 @@ namespace Fs::Ext2 { 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++; } @@ -1146,7 +1155,7 @@ namespace Fs::Ext2 { } 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; auto& self = g_instances[inst]; @@ -1156,7 +1165,7 @@ namespace Fs::Ext2 { if ((inode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1; 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; 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 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 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 Create(const char* p) { return CreateImpl(N, p); } static int Delete(const char* p) { return DeleteImpl(N, p); } @@ -1625,6 +1635,7 @@ namespace Fs::Ext2 { Thunks::Mkdir, Thunks::Rename, Thunks::GetLabel, + Thunks::ReadDirAt, }; } diff --git a/kernel/src/Fs/Fat32.cpp b/kernel/src/Fs/Fat32.cpp index bc81efb..042deaf 100644 --- a/kernel/src/Fs/Fat32.cpp +++ b/kernel/src/Fs/Fat32.cpp @@ -674,11 +674,12 @@ namespace Fs::Fat32 { static int ReadDirectoryNames(int inst, uint32_t dirCluster, char outNames[MaxDirEntries][MaxNameLen], - int maxEntries) { + int maxEntries, int startOffset = 0) { auto& self = g_instances[inst]; uint16_t lfnBuf[MaxNameLen]; bool hasLfn = false; int count = 0; + int skipped = 0; // valid entries skipped to reach startOffset uint32_t cluster = dirCluster; while (!IsEndOfChain(cluster) && count < maxEntries) { @@ -736,6 +737,10 @@ namespace Fs::Fat32 { 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++; } @@ -997,7 +1002,7 @@ namespace Fs::Fat32 { } 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; auto& self = g_instances[inst]; @@ -1006,7 +1011,7 @@ namespace Fs::Fat32 { if (!(dirEntry.attributes & ATTR_DIRECTORY)) return -1; 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; 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 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 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 Create(const char* p) { return CreateImpl(N, p); } static int Delete(const char* p) { return DeleteImpl(N, p); } @@ -1784,6 +1790,7 @@ namespace Fs::Fat32 { Thunks::Mkdir, Thunks::Rename, Thunks::GetLabel, + Thunks::ReadDirAt, }; } diff --git a/kernel/src/Fs/Ramdisk.cpp b/kernel/src/Fs/Ramdisk.cpp index 167153b..3956e3b 100644 --- a/kernel/src/Fs/Ramdisk.cpp +++ b/kernel/src/Fs/Ramdisk.cpp @@ -217,15 +217,17 @@ namespace Fs::Ramdisk { (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 '/' if (path[0] == '/') 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++) { const char* entryName = fileTable[i].name; + bool isChild = false; if (pathLen == 0) { // Root directory: find entries without '/' in them (or only trailing '/') @@ -237,9 +239,7 @@ namespace Fs::Ramdisk { break; } } - if (!hasSlash) { - outNames[count++] = entryName; - } + isChild = !hasSlash; } else { // Subdirectory: match entries starting with "path/" // and that are direct children (no additional '/' beyond the prefix) @@ -259,15 +259,21 @@ namespace Fs::Ramdisk { break; } } - if (!hasDeepSlash && restLen > 0) { - outNames[count++] = entryName; - } + isChild = (!hasDeepSlash && restLen > 0); } + + if (!isChild) continue; + if (seen++ < startIndex) continue; // skip entries before the requested page + outNames[count++] = entryName; } 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) { if (handle < 0 || handle >= fileCount) return -1; if (buffer == nullptr || size == 0) return 0; diff --git a/kernel/src/Fs/Ramdisk.hpp b/kernel/src/Fs/Ramdisk.hpp index 1f4f0f6..b435c00 100644 --- a/kernel/src/Fs/Ramdisk.hpp +++ b/kernel/src/Fs/Ramdisk.hpp @@ -32,6 +32,7 @@ namespace Fs::Ramdisk { void Close(int handle); 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 Mkdir(const char* path); int Rename(const char* oldPath, const char* newPath); diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp index e3985dd..79c75bc 100644 --- a/kernel/src/Fs/Vfs.cpp +++ b/kernel/src/Fs/Vfs.cpp @@ -374,17 +374,30 @@ namespace Fs::Vfs { } 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; const char* localPath; + if (startIndex < 0) return -1; if (!ParsePath(path, drive, localPath)) return -1; if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; vfsLock.Acquire(); FsDriver* driver = driveTable[drive]; - int result = (driveActive[drive] && driver && driver->ReadDir) - ? driver->ReadDir(localPath, outNames, maxEntries) - : -1; + int result; + if (!driveActive[drive] || !driver) { + 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(); return result; } diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp index 67763e2..bbd0669 100644 --- a/kernel/src/Fs/Vfs.hpp +++ b/kernel/src/Fs/Vfs.hpp @@ -30,6 +30,9 @@ namespace Fs::Vfs { int (*Mkdir)(const char* path); int (*Rename)(const char* oldPath, const char* newPath); 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(); @@ -49,6 +52,7 @@ namespace Fs::Vfs { int VfsDelete(const char* path); 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 VfsRename(const char* oldPath, const char* newPath); diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 591d38b..aa16747 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -179,6 +179,9 @@ namespace Montauk { // Cross-process graceful power-off request channel 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 // a pending action and exits; login.elf reads it, runs the shutdown stages, // then issues the matching SYS_SHUTDOWN / SYS_RESET. diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 15509a7..96b2cb2 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -141,6 +141,13 @@ namespace montauk { 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); } + // 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 inline int fwrite(int handle, const uint8_t* buf, uint64_t off, uint64_t size) { diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index b876a53078b763ca9bd2ce3aa34899ca25d9243d..d00bbb9ee4a2f11e22a6c91214c6a3c2e885d898 100644 GIT binary patch delta 2766 zcmZ8jYiv|S6uxJH?UT|?CBrR|vS4Yq(B767+NH|UmgS~2B2bivkF{1FMWR*>u$Z{P z7lJYE=1WAaCK8AO{-B#GxHi&Mq<}w=iXb2g)gJ^KB2@{LYU0e?S=h`Zd+v9>@64Gq zGiUGa$L(~ko!SfOy{PXHt%(hsCwif)+tuujxC}lH<-N)6TayG%`6ll<@iayoQu|iA zB=J#+T@v?6JX+#o5~oUhT;gnrPv!2C4+vD*hzX?;xUr{ zQDV2mXCzLM_^iav!aqrzCi!y`r%QZZ;tYu|NSrD0MS)8Ur=nk^;K~r?Hjvx`NvDmZ z;yCSHbJGvHXd!N7uFF`MW*D?wn5WS^){eY1)-b$cKA9B)Cx+)rk~2!MG%?qGJ&AOW zBsrdxti(*i7$cx7e6Kq@iFBLE&J^VoiqI*-Or4&j3nJ|bH}?rd<0tLJk}U6dQB%HP zT${7Sql$m}uq~_9{V7*cvha0Q?wCFftGX^`te*3%lsf!WHF4D&cjIOPqC>F;ho0lS@@^fveS|w++Gb3@%d*CVFL=R(ou}!*P zHkG+si(iGN5@vR&U7yy}35b?3S9&J)mu#f1@LCoXV2Q=36fKrz&QL`(%f`QZSUHGU zwksnAx2+O$zuNILR_at`H1}bB81ZJ50$VO|qWg3~zsjk&x zUO~Lvrhh;cjQ6iu5u#RvD{QJpZ-qrmF~n^Xyt8cY<7({&)uV}3o>w>fC|YMRbFHS9 zsr6pOF0Mv2^>vl=ZS>D#^*c3ni}H7C{C8@<2Q>bOTB{pnvzd8JQ}dO78cnlVq;#WI-(VbV2TNS-mHA!MKH&lavfmkIo4{PdwD*XuzaP_jLex=%C5S}V# z{+XnT?8-H6iMbvUZ)!e-B1*|btcrPkg5txIq>6|F$r?s3cJa)UG*$lKizxJ0F;8id zDl$(&jh`XgpX!;Hq=>8!p_ME1{Hf-Hni5j$J%T1bGgoQqCZ&cDcZc^ke@|9($pSxf_pXonmYFb8vmQR-eVfyq4<*;uiC&Ea(HZ;<1zo$Vs@yx z)rieuX07^N7w07ce^+0MdJJ%Nv8GN?dtVMuHS;~8sSru{SgrAgyOvb1b<=UgYkK(a zXSlY9Zoyw?(|T;Jt4y0G|1=oJV)WKk@;^eubpiSu{`moFN0{4%h;zFR!`$wGKNz4+ zgt@(jcrf59d;qtDbJK3;IBPFM&4Nl=gz$m@1rX zNE%CMePT5jLxYJDHRMu0)0k4f2&j~!2owm|s6oCA_YVXi(V5wIuuS4hcJ}vvzxUp} z*`3+DQ=PD_6FLjw#jc{g@JxJU7+^_npQlBSP1LxJ>Yuby+_iMgBW?rI1&P0v*dy_G z5~oZ2y~JLLe~@^L#6JZhqJ+dhOYGG7MPjGU5sA}yOia@TrQ;MmDshHPPDrdvd`#j| z5)Vo2Onh8opX7g)I8)*i5@$(#QsU7P4@>McbXwp%&66eCeGKFhKsp`Zgh^*p@8g+! zJTMO-`(=HOPt#x#uFke&E!mouD^kIz5;$f46D}uBurNNLpLdb^C70vB?s76jxTkG` z{;!MFy*Yw*O!t%Xg7EXiFgf7T1>w8Ehld4%2PVGjs^k?MH~7R?4VpGaU{9v#H($ba zk_mCV8$Zga&_CqL^=v$n6Uf-jVL|WJoaI@)O9HQF!2%q~XD}CU=9|WQRJTzS;g-oS zG3zFbPGQi5jZmL!;cQB9U7vHv^lw`n>M2I`39kO5K57 z1q^23j)ErGh`z$Lumv|1+He{V@$nV(7cod+QBe~d!}cQ6I4Tmkwa86yx6T<%@W^7k z$YT~#C#b|k(JE#zfDOeqlwemeGydS4K-d{2at{vh$b&e{BfXeg!i;W;oGkaj-Fd5U zeu)WR;RYTPq23lKF$TuDIw<#`rmaxaG92ORa>{*MQ9ICI%8cq!PM=#9^>>Vx+HeWm z`RK<)scj6zmDsK*^;Hhh4|G6wgfue`}vPnG+W_&KLEq- zVZ5AqZe`)&@-^@(MokkQ#8#6*F1DMdF@c(Bk*DD9!%D}5X?t$#7^o;Ww$h5fRDyoQ z<_ZR9v8`gW;ic}fX!oR6vmSFT2D>n9nJ@;ISvD-gIA=!VA&%cff2GYUG*+6h5ZgKK z$3!JF_EMEwO0^z@ATw4`KVxam)LcDi1)Cs*T|v`G(!!Q1F)ML6Xv3eFTgCYP4Ocb6 zOZah>4W&3zWkN0br<(8-MyJ}iZK^l+B<;`C3|ndUEmrtEnq{fNZ>L?bO5tClb;T4u zO!et>$L<`y6%^R5B=pgv*sk!qsG*$-f0zz+pTdtL|KL6RbpA=Bj)v|LH0^Vm_`Jej zA^$RtRI`Q~iuxA4l{DI|spr~5?cS&GkI;@AkI@=t6ewzt{2;d0u!e}DUZI*#DEw^V z8x{Trt*9FlyxM1@ol_~E7bQsIYvNO#|4mi)x#Fp589#_th#99{St3(@^ZET8HiuZl zh5yOD=E@Qej$G}wix<_3_$3VoLd@?IKLgx^BCaU*gxLFGuQ84Uu_SkklQ1{Tj1nbn zK93S+;`}i4hZMCLtuVvRu-9LwC~erxl>>afc`ik?FMi{MEyNK%-(r_4+T18LO!5S2()1< zAJ<`=k7qGi7x7%1ftB?$eHX_%Me4Avz6Q!MULS!;nB-#!Ejt4Hu$7OU80X_GOxlsm Ozh*iaYXJY5HSs^!o@9Lh diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index 01604850217f243bf44b12041adf1d597fc7c577..f83bbdafa709e4f07d7cb7c78e71f43cb4afc3a0 100644 GIT binary patch delta 2994 zcmZ`*U2IfE6uxJHUACnQ7057401KAgrKNYbEw-h~x~258G@2GrC@M=;ek24^HMa2J zX8lK_r8$YH)o23|`{qoEJ-xHG$+t1qP}*0j z3kn}o*rjlv!ebTgS9rX_#}%HW@F{;(MpF2+!gic93d=Z2!x&JVG0N#{h3%kc6&|Pf za|*i^KCiGx;R_1e6JJ!=tN3pe&Qkc2!r2OctMCMczn8ekurs=>1Xs39w-(eAKy_L{ zDRyU9_u8yOE?9kYgBmay;--i8C$tx}!5jvM}B6{?$Rc zZ#g=-9$#jpVT_Z|wS9(rl7n<}XJ_V%AE2g z<7$~CU*+QI!+pN6d!JA|KD_MnkLwe#y!*CqbGC0q!-~Z6C9&%2`0_+-W%qhNX7i`i z1{p@F3?N#Tc;w7aVD4NKKE&8u#y^szXzP?6_1TI|b1hyWc2BluB~*G@morIM3n>NjznS*&n324Ps5#wvLJAv9lLu%SQS~z#-KH>h zm?jkBfXQGo4w;rK+k?g7A~=AJVaCtX-W=A34P#rl6&U747;mNd8SO=;vW{YX#DrOx zidejbw1nm}9S0*8yn~@)3x=_&Sk_BNvBgi(sov21`w&VP?>?*aO5^t88upC1ROlZW01(x?o8dZyJ z3s~}TT74#mcB;~i8XuuKp4a$2R5?kFf0br=TjMJz{%)I3-nl4tkYlExG6|iOK(vEe zpre$~5sg1d7ev3t=TraeU3{guO(~-*cL|1Zh$bG^_*>Nf69?~S@iBCknRMIej+?0Q zos{l$jen3TGk~!&#!ED{iuzG(Dr50lO}#}iS8MzV;@4}un{wNa10vcNR9kYtXh~|L zdx+1h=pTx5!r@OzEB~t)DQA4xF-lIOK1Jev0~^a({ICBR3-q;=b4eF+Gjbffj204) zY`K^gbNMwjW{bv591&`ORJkr^o?*N0OjzCExG z62Q|urs@kC7- lY(=o5;@@IS*cMD#QCBdEz1AY}g@mP1cpqcJuEx~Tz<&}mVW$89 delta 2894 zcmZ8jYiv|S6u#%uZhKn_EJ21@0=N}vx3Al!r7sAZh0;rdK#C6_MAu3pwEid+wX7yC zYZOB$LOoz(G@6YjYN7#BC{&`&ib{-+vVaf@0U9+VK#=|r1c^E`_blDaBzx|6zVFPL zGmkraFZaNwJ+Ql2*uYtMybs_Uz_Wel&GnwxOhdGs!C4*hX=E9ud?b|%3V)-psqis{ zvlRYT;ar8kQ+SfXKl(y4g~C56?B@AdVK>h&3Xc;pF~b;Aj+^v^!r7{GN@0(}Cl$_7 z_>{u#z^4_q6#uKjc?zFVIA7tj3XfO#oWgEF!x9%7X1*+UDX1xca=O6{Q_jx5_Pl-* zR-nINqi30A7_bKS6gaW<1%~05U4d6ioNnGT8O}J#!gQbKQU>X{n&EhFWjNU~+>Flf z{Fgy`awkaIZ5}TqN60oSRU}AfI-!|Xd`TS9}Hil6xJBpSkezJ_^7@T9n z4s4yn_)bZR_O!BNF%j&U)ck}1x+|o?(jV{yI7VxPI_OaV7MXzTQO0=_%hnr z<62BNrYe}vq70Wb^`Cq+?U&$rjM_GzM$-tXcWP{z{Ft~7~ia^ zFVcuH4A-#c9h&+a`Mnx{l=lB4jXy|Z?Zbo^>!_wqCI2|4YFO-)rhY;>FKPUB@~>(9 zemY&ZHU2eP;c>Jl>2*j@Z4+G{H`ID#)3ul$x{Hc5zKZo6$PwhUGNQ4Z5lcmZ1*Sgc!9 z2Wf44HU1*)$OjsKiZ;GqlPJeOS_Z#Q`PH7({9>MQ=%<})6okBs!mIZdxc z9kvKHq^Z+r?oAk<&jQOe^>sRpk7@kAzU2Hak9m1EHZ1zu{ChUuUG%TGPwy;tU@e9l zYppfvJH;^SFy2@zxPitH9K$=p?#B8hA;Cn2-G~FizKeH+-H-K6A@km3e7R{M6yrcs z$g-xm#VtW_YQ=Y5y%T~)j0(F32b_?3b0OY!7RsJWL$C#-OGDO!bi&@ic+;%^0j@q_ A00000 diff --git a/programs/src/desktop/apps/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp index bd77d9d..4af4527 100644 --- a/programs/src/desktop/apps/apps_common.hpp +++ b/programs/src/desktop/apps/apps_common.hpp @@ -133,25 +133,28 @@ inline void format_mac(char* buf, const uint8_t* mac) { // 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) { - snprintf(buf, 16, "%d B", size); - } else if (size < 1024 * 1024) { - 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); - } + snprintf(buf, 16, "%d B", (int)size); + return; + } + int unit = 0; + uint64_t scaled = size; + uint64_t rem = 0; + while (scaled >= 1024 && unit < 5) { + rem = scaled % 1024; + scaled /= 1024; + 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 { - int mb = size / (1024 * 1024); - int frac = ((size % (1024 * 1024)) * 10) / (1024 * 1024); - if (mb < 10) { - snprintf(buf, 16, "%d.%d MB", mb, frac); - } else { - snprintf(buf, 16, "%d MB", mb); - } + snprintf(buf, 16, "%d %s", whole, units[unit]); } } diff --git a/programs/src/desktop/apps/filemanager/actions.cpp b/programs/src/desktop/apps/filemanager/actions.cpp index d6bea98..b330a5f 100644 --- a/programs/src/desktop/apps/filemanager/actions.cpp +++ b/programs/src/desktop/apps/filemanager/actions.cpp @@ -8,34 +8,25 @@ 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) { if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return; + if (fm->entry_count <= 0) return; - int sel[64]; - int n = filemanager_collect_selection(fm, sel, 64); - if (n <= 0) return; + int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int)); + if (!sel) 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; - for (int i = 0; i < n && out < 64; i++) { + for (int i = 0; i < n && out < fm->clipboard_cap; i++) { int idx = sel[i]; if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue; filemanager_build_fullpath(fm->clipboard_paths[out], 256, fm->current_path, fm->entry_names[idx]); out++; } + montauk::mfree(sel); if (out == 0) return; fm->clipboard_count = out; @@ -51,35 +42,9 @@ void filemanager_do_cut(FileManagerState* fm) { } void filemanager_do_paste(FileManagerState* fm) { - if (fm->clipboard_count <= 0 || - filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return; - - 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); + // Copy/move runs on a worker thread with a progress dialog (a small single + // file pastes inline). Same-volume moves use rename (instant, no copy). + filemanager_start_paste_job(fm); } void filemanager_start_rename(FileManagerState* fm) { @@ -157,13 +122,16 @@ void filemanager_new_folder(FileManagerState* fm) { void filemanager_delete_selected(FileManagerState* fm) { if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return; + if (fm->entry_count <= 0) return; - int sel[64]; - int n = filemanager_collect_selection(fm, sel, 64); - if (n <= 0) return; + int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int)); + if (!sel) return; + int n = filemanager_collect_selection(fm, sel, fm->entry_count); + if (n <= 0) { montauk::mfree(sel); return; } // 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; bool any_dir = false; for (int i = 0; i < n; i++) { @@ -172,7 +140,8 @@ void filemanager_delete_selected(FileManagerState* fm) { filtered[fn++] = idx; 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. if (any_dir || fn > 1) { @@ -181,6 +150,7 @@ void filemanager_delete_selected(FileManagerState* fm) { } else { filemanager_show_bulk_delete_confirmation(fm, filtered, fn); } + montauk::mfree(filtered); return; } @@ -188,6 +158,7 @@ void filemanager_delete_selected(FileManagerState* fm) { char fullpath[512]; filemanager_build_fullpath(fullpath, 512, fm->current_path, fm->entry_names[filtered[0]]); + montauk::mfree(filtered); montauk::fdelete(fullpath); filemanager_clear_multi_selection(fm); filemanager_read_dir(fm); diff --git a/programs/src/desktop/apps/filemanager/events.cpp b/programs/src/desktop/apps/filemanager/events.cpp index 2792591..2fd52f7 100644 --- a/programs/src/desktop/apps/filemanager/events.cpp +++ b/programs/src/desktop/apps/filemanager/events.cpp @@ -51,10 +51,13 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) { case CTX_DELETE: filemanager_delete_selected(fm); break; case CTX_NEW_FOLDER: filemanager_new_folder(fm); break; case CTX_PROPERTIES: { - if (fm->multi_count > 1) { - int sel[64]; - int n = filemanager_collect_selection(fm, sel, 64); - if (n > 0) filemanager_show_multi_properties(fm, sel, n); + if (fm->multi_count > 1 && fm->entry_count > 0) { + int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int)); + if (sel) { + int n = filemanager_collect_selection(fm, sel, fm->entry_count); + if (n > 0) filemanager_show_multi_properties(fm, sel, n); + montauk::mfree(sel); + } } else { 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. filemanager_clear_multi_selection(fm); - int limit = fm->entry_count < 64 ? fm->entry_count : 64; + int limit = fm->entry_count; if (fm->grid_view) { int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W; 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) { if (win->app_data) { FileManagerState* fm = (FileManagerState*)win->app_data; - filemanager_free_app_icons(fm); + filemanager_free_arrays(fm); montauk::mfree(win->app_data); win->app_data = nullptr; } diff --git a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp index 6790fff..a1fc228 100644 --- a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp +++ b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp @@ -50,33 +50,41 @@ struct FileManagerVirtualEntry { 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 { char current_path[256]; FileManagerLocation history[16]; int history_pos; int history_count; - char entry_names[64][128]; - int entry_types[64]; - int entry_sizes[64]; + // Parallel per-entry arrays, dynamically grown together to entry_cap. + // Allocated lazily via filemanager_ensure_entry_capacity(). + char (*entry_names)[128]; + int* entry_types; + uint64_t* entry_sizes; // 64-bit: avoids overflow on files >= 2 GiB int entry_count; + int entry_cap; // allocated capacity of the per-entry arrays int selected; int scroll_offset; - bool is_dir[64]; + bool* is_dir; int last_click_item; uint64_t last_click_time; Scrollbar scrollbar; DesktopState* desktop; bool grid_view; - int drive_indices[64]; // 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_indices; // drive number or special-folder index for Computer view entries + int* drive_kinds; // kernel BlockDeviceKind for FM_ENTRY_DRIVE entries (4 = USB MSC) - // Clipboard (supports multiple paths from a marquee selection) - char clipboard_paths[64][256]; + // Clipboard (supports multiple paths from a selection), grown to clipboard_cap. + char (*clipboard_paths)[256]; + int clipboard_cap; int clipboard_count; bool clipboard_is_cut; - // Multi-selection (grid view marquee selection) - bool multi_selected[64]; + // Multi-selection (marquee selection), sized with the entry arrays. + bool* multi_selected; int multi_count; // Marquee drag state. Coordinates are stored in content space: @@ -107,11 +115,14 @@ struct FileManagerState { int pathbar_cursor; int pathbar_len; - // Virtual views + // Virtual views (sized with the entry arrays) FileManagerVirtualViewKind virtual_view; - FileManagerVirtualEntry virtual_entries[64]; + FileManagerVirtualEntry* virtual_entries; 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 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); 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_dir(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_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_multi_properties(FileManagerState* fm, const int* indices, int count); @@ -249,6 +270,19 @@ void filemanager_commit_pathbar(FileManagerState* fm); 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_y(FileManagerState* fm, int row, int cols); int filemanager_grid_content_height(FileManagerState* fm, int cols); diff --git a/programs/src/desktop/apps/filemanager/fileop.cpp b/programs/src/desktop/apps/filemanager/fileop.cpp new file mode 100644 index 0000000..765c9ba --- /dev/null +++ b/programs/src/desktop/apps/filemanager/fileop.cpp @@ -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 +#include + +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 diff --git a/programs/src/desktop/apps/filemanager/filesystem.cpp b/programs/src/desktop/apps/filemanager/filesystem.cpp index 4b31c7e..3e087e9 100644 --- a/programs/src/desktop/apps/filemanager/filesystem.cpp +++ b/programs/src/desktop/apps/filemanager/filesystem.cpp @@ -8,6 +8,92 @@ 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) { fm->selected = -1; fm->scroll_offset = 0; @@ -33,7 +119,7 @@ static void filemanager_add_root_entry(FileManagerState* fm, const char* name, FileManagerEntryType type, 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++; 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* icon_path, 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; 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 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]; montauk::strcpy(probe_path, ds->home_dir); int plen = montauk::slen(probe_path); @@ -167,7 +253,6 @@ void filemanager_read_drives(FileManagerState* fm) { str_append(label, ")", 64); } filemanager_add_root_entry(fm, label, FM_ENTRY_DRIVE, d); - if (fm->entry_count >= 64) break; } filemanager_reset_list_state(fm); @@ -181,10 +266,7 @@ void filemanager_read_dir(FileManagerState* fm) { filemanager_free_app_icons(fm); fm->virtual_view = FM_VIRTUAL_VIEW_NONE; - - const char* names[64]; - fm->entry_count = montauk::readdir(fm->current_path, names, 64); - if (fm->entry_count < 0) fm->entry_count = 0; + 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. @@ -206,52 +288,67 @@ void filemanager_read_dir(FileManagerState* fm) { } } - for (int i = 0; i < fm->entry_count; i++) { - const char* raw = names[i]; - // 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; } + // 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; } - if (match) raw += prefix_len; - } - montauk::strncpy(fm->entry_names[i], raw, 128); - int len = montauk::slen(fm->entry_names[i]); + 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]); - - // Get file size - fm->entry_sizes[i] = 0; - if (!fm->is_dir[i]) { - char fullpath[512]; - montauk::strcpy(fullpath, fm->current_path); - int plen = montauk::slen(fullpath); - if (plen > 0 && fullpath[plen - 1] != '/') { - str_append(fullpath, "/", 512); + // 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; } - str_append(fullpath, fm->entry_names[i], 512); - int fd = montauk::open(fullpath); - if (fd >= 0) { - fm->entry_sizes[i] = (int)montauk::getsize(fd); - montauk::close(fd); + + fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]); + + // Get file size (64-bit: no truncation for files >= 2 GiB) + fm->entry_sizes[i] = 0; + if (!fm->is_dir[i]) { + 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); + int fd = montauk::open(fullpath); + if (fd >= 0) { + fm->entry_sizes[i] = montauk::getsize(fd); + montauk::close(fd); + } } } + + 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]; - int tmp_size = fm->entry_sizes[i]; + uint64_t tmp_size = fm->entry_sizes[i]; bool tmp_isdir = fm->is_dir[i]; montauk::strcpy(tmp_name, fm->entry_names[i]); @@ -315,7 +412,7 @@ void filemanager_read_apps(FileManagerState* fm) { DesktopState* ds = fm->desktop; 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); } @@ -327,24 +424,23 @@ void filemanager_read_system_configuration(FileManagerState* fm) { DesktopAppletEntry 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_reset_list_state(fm); } -bool filemanager_delete_recursive(const char* path) { - // Try deleting as a file (or empty directory) first - if (montauk::fdelete(path) == 0) return true; +// 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; - // If that failed, it may be a non-empty directory -- enumerate and delete children - const char* names[64]; - 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; + // 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; @@ -362,22 +458,68 @@ bool filemanager_delete_recursive(const char* path) { } } - 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; + 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, raw, 512); + str_append(child, children[i], 512); // Strip trailing slash if present (directory marker) int clen = montauk::slen(child); @@ -386,6 +528,8 @@ bool filemanager_delete_recursive(const char* path) { filemanager_delete_recursive(child); } + if (children) montauk::mfree(children); + // Now the directory should be empty -- delete it return montauk::fdelete(path) == 0; } @@ -446,42 +590,15 @@ bool filemanager_copy_dir_recursive(const char* src, const char* dst) { montauk::fmkdir(dst); - const char* names[64]; - int count = montauk::readdir(src, names, 64); + char (*children)[256] = nullptr; + int count = filemanager_read_all_children(src, &children); 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++) { - 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; - char basename[64]; - montauk::strncpy(basename, raw, 63); + 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; @@ -498,6 +615,8 @@ bool filemanager_copy_dir_recursive(const char* src, const char* dst) { filemanager_copy_file(src_child, dst_child); } + if (children) montauk::mfree(children); + return true; } diff --git a/programs/src/desktop/apps/filemanager/helpers.cpp b/programs/src/desktop/apps/filemanager/helpers.cpp index ba01f7d..4bf51f7 100644 --- a/programs/src/desktop/apps/filemanager/helpers.cpp +++ b/programs/src/desktop/apps/filemanager/helpers.cpp @@ -10,7 +10,8 @@ namespace filemanager { void filemanager_clear_multi_selection(FileManagerState* fm) { 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; } @@ -18,8 +19,7 @@ int filemanager_collect_selection(const FileManagerState* fm, int* out, int max) if (!fm || !out || max <= 0) return 0; int n = 0; if (fm->multi_count > 0) { - int limit = fm->entry_count < 64 ? fm->entry_count : 64; - for (int i = 0; i < limit && n < max; i++) { + for (int i = 0; i < fm->entry_count && n < max; i++) { if (fm->multi_selected[i]) out[n++] = i; } } else if (fm->selected >= 0 && fm->selected < fm->entry_count) { diff --git a/programs/src/desktop/apps/filemanager/properties.cpp b/programs/src/desktop/apps/filemanager/properties.cpp index c82579e..183885e 100644 --- a/programs/src/desktop/apps/filemanager/properties.cpp +++ b/programs/src/desktop/apps/filemanager/properties.cpp @@ -41,8 +41,9 @@ struct FileDeleteConfirmDialogState { char path[512]; // primary path (used for single-item subtitle) SvgIcon icon; - // Multi-item targets. count == 1 keeps the original single-folder UX. - char paths[64][512]; + // Multi-item targets (dynamically allocated). count == 1 keeps the original + // single-folder UX. + char (*paths)[512]; int count; }; @@ -87,12 +88,12 @@ static void props_close_self(DesktopState* ds, void* app_data) { } } -static void props_center_dialog(DesktopState* ds, - const void* owner_app_data, - int w, - int h, - int* out_x, - int* out_y) { +void props_center_dialog(DesktopState* ds, + const void* owner_app_data, + int w, + int h, + int* out_x, + int* out_y) { if (!out_x || !out_y) return; int wx = 180; @@ -441,22 +442,38 @@ static void delete_confirm_apply(FileDeleteConfirmDialogState* st) { if (!st) return; FileManagerState* owner = delete_confirm_live_owner(st); - bool any = false; int n = st->count > 0 ? st->count : (st->path[0] ? 1 : 0); - for (int i = 0; i < n; i++) { - const char* p = (st->count > 0) ? st->paths[i] : st->path; - if (!p || !p[0]) continue; - if (filemanager_delete_recursive(p)) any = true; + + // 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 (owner) { - if (owner->delete_confirm_dialog == st) - owner->delete_confirm_dialog = nullptr; - if (any) { + + if (!started) { + // Fallback: delete synchronously (e.g. another operation is running). + bool any = false; + for (int i = 0; i < n; i++) { + const char* p = (st->count > 0) ? st->paths[i] : st->path; + if (!p || !p[0]) continue; + if (filemanager_delete_recursive(p)) any = true; + } + if (owner && any) { filemanager_clear_multi_selection(owner); filemanager_read_dir(owner); } } + if (owner && owner->delete_confirm_dialog == st) + owner->delete_confirm_dialog = nullptr; + delete_confirm_close(st); } @@ -560,6 +577,7 @@ static void delete_confirm_on_close(Window* win) { if (owner && owner->delete_confirm_dialog == st) owner->delete_confirm_dialog = nullptr; + if (st->paths) montauk::mfree(st->paths); montauk::mfree(st); win->app_data = nullptr; } @@ -650,8 +668,14 @@ void filemanager_show_bulk_delete_confirmation(FileManagerState* fm, st->owner = fm; 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; - for (int i = 0; i < count && out < 64; i++) { + for (int i = 0; i < count; i++) { int idx = indices[i]; if (idx < 0 || idx >= fm->entry_count) continue; if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue; @@ -660,6 +684,7 @@ void filemanager_show_bulk_delete_confirmation(FileManagerState* fm, out++; } if (out == 0) { + montauk::mfree(st->paths); montauk::mfree(st); return; } @@ -803,7 +828,7 @@ void filemanager_show_multi_properties(FileManagerState* fm, st->desktop = ds; st->accent = ds->settings.accent_color; - int total_size = 0; + uint64_t total_size = 0; bool any_file = false; for (int i = 0; i < count; i++) { int idx = indices[i]; diff --git a/programs/src/desktop/apps/filemanager/render.cpp b/programs/src/desktop/apps/filemanager/render.cpp index 4f144ac..9e1b383 100644 --- a/programs/src/desktop/apps/filemanager/render.cpp +++ b/programs/src/desktop/apps/filemanager/render.cpp @@ -341,7 +341,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) { // Selection highlight (single or multi) bool cell_selected = (i == fm->selected) || - (i < 64 && fm->multi_selected[i]); + (fm->multi_selected && fm->multi_selected[i]); if (cell_selected) { int sy = gui_max(cell_y, list_y); 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; // 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) { int sy = gui_max(iy, list_y); int sh = gui_min(iy + FM_ITEM_H, c.h) - sy; diff --git a/programs/src/devexplorer/devexplorer.h b/programs/src/devexplorer/devexplorer.h index fb586db..435d4e5 100644 --- a/programs/src/devexplorer/devexplorer.h +++ b/programs/src/devexplorer/devexplorer.h @@ -11,6 +11,7 @@ #include #include #include +#include extern "C" { #include @@ -107,6 +108,25 @@ struct DisplayRow { 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 // ============================================================================ @@ -142,6 +162,14 @@ struct DevExplorerState { int last_click_row; 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; }; @@ -152,6 +180,7 @@ struct DevExplorerState { extern int g_win_w, g_win_h; extern DevExplorerState g_state; extern TrueTypeFont* g_font; +extern mtk::Theme g_theme; // ============================================================================ // Function declarations — render.cpp @@ -165,6 +194,10 @@ void render(uint32_t* pixels); int build_display_rows(DevExplorerState* de, DisplayRow* rows); 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 diff --git a/programs/src/devexplorer/main.cpp b/programs/src/devexplorer/main.cpp index 5ee1e54..5b4bc3b 100644 --- a/programs/src/devexplorer/main.cpp +++ b/programs/src/devexplorer/main.cpp @@ -16,6 +16,7 @@ int g_win_h = INIT_H; DevExplorerState g_state; TrueTypeFont* g_font = nullptr; +mtk::Theme g_theme; static void refresh_devices(DevExplorerState* de) { if (de == nullptr) return; @@ -63,6 +64,71 @@ int build_display_rows(DevExplorerState* de, DisplayRow* rows) { 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 // ============================================================================ @@ -70,10 +136,7 @@ int build_display_rows(DevExplorerState* de, DisplayRow* rows) { static bool handle_toolbar_click(int mx, int my) { if (my >= TOOLBAR_H) return false; - int btn_w = 80, btn_h = 26; - 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) { + if (refresh_button_rect().contains(mx, my)) { g_state.last_poll_ms = 0; // force refresh return true; } @@ -127,6 +190,92 @@ static bool handle_list_click(int mx, int my) { 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 // ============================================================================ @@ -205,15 +354,15 @@ extern "C" void _start() { for (int i = 0; i < NUM_CATEGORIES; i++) g_state.collapsed[i] = false; - // Load font - { - TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); - if (f) { - montauk::memset(f, 0, sizeof(TrueTypeFont)); - if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; } - } - g_font = f; - } + // 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 + // (button, scrollbar) render with, so point g_font at it to avoid a second + // copy of the same face. + gui::fonts::init(); + g_font = gui::fonts::system_font; + + // Theme for MTK widgets, built once from the user's accent color. + g_theme = mtk::make_theme(); // Initial device poll refresh_devices(&g_state); @@ -290,20 +439,8 @@ extern "C" void _start() { // Mouse if (ev.type == 1) { - int mx = ev.mouse.x; - 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; + if (handle_main_mouse(ev)) 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(); } diff --git a/programs/src/devexplorer/render.cpp b/programs/src/devexplorer/render.cpp index 0d0d39d..b23058f 100644 --- a/programs/src/devexplorer/render.cpp +++ b/programs/src/devexplorer/render.cpp @@ -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_hline(px, g_win_w, g_win_h, 0, TOOLBAR_H - 1, g_win_w, BORDER_COLOR); - // "Refresh" button - int btn_w = 80, btn_h = 26; - int btn_x = 8; - int btn_y = (TOOLBAR_H - btn_h) / 2; - px_button(px, g_win_w, g_win_h, btn_x, btn_y, btn_w, btn_h, - "Refresh", ACCENT_COLOR, WHITE, 4); + // "Refresh" button (MTK primary button) + Canvas canvas(px, g_win_w, g_win_h); + mtk::WidgetState btn_state = mtk::widget_state(false, de.btn_hover, true); + mtk::draw_button(canvas, refresh_button_rect(), "Refresh", + mtk::BUTTON_PRIMARY, btn_state, g_theme); // Device count on right char count_str[24]; @@ -71,34 +70,11 @@ static void render_list(uint32_t* px) { DisplayRow rows[MAX_DISPLAY_ROWS]; int row_count = build_display_rows(&de, rows); - int list_y = TOOLBAR_H; - int list_h = g_win_h - list_y; + ScrollMetrics m = compute_scroll_metrics(&de, rows, row_count); + int list_y = m.list_y; + int list_h = m.list_h; 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; // Draw rows @@ -175,16 +151,18 @@ static void render_list(uint32_t* px) { cur_y += row_h; } - // Scrollbar - if (total_h > list_h) { - int sb_x = g_win_w - 6; - int thumb_h = (list_h * list_h) / total_h; - if (thumb_h < 20) thumb_h = 20; - int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px; - px_fill(px, g_win_w, g_win_h, sb_x, list_y, 4, list_h, - Color::from_rgb(0xE0, 0xE0, 0xE0)); - px_fill(px, g_win_w, g_win_h, sb_x, thumb_y, 4, thumb_h, - Color::from_rgb(0xAA, 0xAA, 0xAA)); + // Draggable MTK scrollbar + Rect viewport = { 0, list_y, g_win_w, list_h }; + Rect track = mtk::scrollbar_track_rect(viewport); + Rect thumb = mtk::scrollbar_thumb_rect(track, m.total_h, list_h, m.scroll_px); + if (!track.empty() && !thumb.empty()) { + bool hovered = track.contains(de.mouse_x, de.mouse_y); + mtk::Theme theme{}; + Color thumb_col = mtk::scrollbar_thumb_color(theme, hovered, de.sb_dragging); + px_fill(px, g_win_w, g_win_h, track.x, track.y, track.w, track.h, + colors::SCROLLBAR_BG); + px_fill(px, g_win_w, g_win_h, thumb.x, thumb.y, thumb.w, thumb.h, + thumb_col); } }