// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // fileMgr.c -- File Manager // // A thin user interface over the file operations library (fileops.lib): // drive bar, path bar, folder tree, sortable file list and status bar, // with open/run, copy, move, rename, delete, new folder, properties, // selection and filter commands. // // One window per instance (multiInstance: File > New Window starts another // instance). Cut/Copy/Paste of files between File Manager windows goes // through the DVX clipboard as text: a header line (FM_CLIP_COPY or // FM_CLIP_CUT) followed by one full path per line. DVX has no // drag-and-drop of files between programs, so this is the way to move // files between windows; pasting the same text into a text editor gives // the list of paths. // // Copy, move and delete run as cooperative tasks (tsCreate), each with its // own progress window and Cancel button, so the File Manager (and the rest // of DVX) stays usable. The task advances the job in time slices and // yields in between; questions ("file exists", "read-only", errors) are // asked from the task with a small modal dialog. // // The tree shows the path from the drive root to the current folder, the // folders beside each step, and the folders inside the current one. // Choosing a folder in the tree lists it and adds its sub-folders under it // in place; navigating any other way rebuilds the tree. #include "dvxApp.h" #include "dvxDlg.h" #include "dvxWgt.h" #include "dvxWgtP.h" #include "dvxWm.h" #include "dvxPlat.h" #include "shellApp.h" #include "shellAssoc.h" #include "taskSwch.h" #include "stb_ds_wrap.h" #include "box/box.h" #include "button/button.h" #include "checkbox/checkbox.h" #include "label/label.h" #include "listView/listView.h" #include "progressBar/progress.h" #include "splitter/splitter.h" #include "statusBar/statBar.h" #include "textInput/textInpt.h" #include "toolbar/toolbar.h" #include "treeView/treeView.h" #include "fileops/fileOps.h" #include #include #include #include #include #include #include #include // ============================================================ // Constants // ============================================================ #define FM_TITLE "File Manager" #define FM_WIN_W 600 #define FM_WIN_H 400 #define FM_WIN_X 40 #define FM_WIN_Y 30 #define FM_CASCADE 20 // offset between successive File Manager windows #define FM_TREE_W 170 // initial width of the folder tree pane #define FM_TITLE_MAX (DVX_MAX_PATH + 32) #define FM_STATUS_MAX 128 #define FM_PROMPT_MAX (DVX_MAX_PATH + 96) #define FM_NAME_MAX DVX_MAX_PATH #define FM_DEFAULT_FOLDER "NEWFOLDR" // Widget names (for scripts and tests) #define FM_NAME_PATH "fmPath" #define FM_NAME_TREE "fmTree" #define FM_NAME_LIST "fmList" #define FM_NAME_STATUS "fmStatus" // File list columns #define FM_COL_NAME 0 #define FM_COL_SIZE 1 #define FM_COL_TYPE 2 #define FM_COL_DATE 3 #define FM_COL_COUNT 4 #define FM_COL_NAME_CHARS 14 #define FM_COL_SIZE_CHARS 12 #define FM_COL_TYPE_CHARS 10 #define FM_COL_DATE_CHARS 16 #define FM_PATH_CHARS 40 // visible width of the path bar // Clipboard format for files #define FM_CLIP_COPY "DVX-FILES COPY" #define FM_CLIP_CUT "DVX-FILES CUT" #define FM_CLIP_EOL '\n' // Job tasks: stack for the task (modal dialogs run on it) and the time // slice between yields #define FM_JOB_STACK (TS_DEFAULT_STACK_SIZE * 2) #define FM_SLICE_MS 60 #define FM_JOB_WIN_W 360 #define FM_JOB_WIN_H 130 // Question dialog // Properties dialog #define FM_PROPS_W 340 // Menu command IDs #define CMD_FILE_OPEN 100 #define CMD_FILE_NEW_FOLDER 101 #define CMD_FILE_COPY_TO 102 #define CMD_FILE_MOVE_TO 103 #define CMD_FILE_RENAME 104 #define CMD_FILE_DELETE 105 #define CMD_FILE_PROPERTIES 106 #define CMD_FILE_NEW_WINDOW 107 #define CMD_FILE_EXIT 108 #define CMD_EDIT_CUT 200 #define CMD_EDIT_COPY 201 #define CMD_EDIT_PASTE 202 #define CMD_EDIT_SELECT_ALL 203 #define CMD_EDIT_INVERT 204 #define CMD_VIEW_REFRESH 300 #define CMD_VIEW_FILTER 301 #define CMD_VIEW_UP 302 #define CMD_VIEW_SORT_NAME 310 #define CMD_VIEW_SORT_SIZE 311 #define CMD_VIEW_SORT_TYPE 312 #define CMD_VIEW_SORT_DATE 313 #define CMD_VIEW_SORT_DESC 314 #define CMD_HELP_CONTENTS 400 #define CMD_HELP_ABOUT 401 // Deepest folder chain the tree shows (each level is at least two // characters of the path) #define FM_TREE_DEPTH_MAX (DVX_MAX_PATH / 2) #define FM_CUR_DIR "." // ============================================================ // Types // ============================================================ // Formatted text for the non-name columns of one row typedef struct { char size[FILEOPS_SIZE_TEXT_MAX]; char type[FILEOPS_TYPE_TEXT_MAX]; char date[FILEOPS_DATE_TEXT_MAX]; } FmRowTextT; // One running copy/move/delete and its progress window typedef struct { FileOpsJobT *job; WindowT *win; WidgetT *what; WidgetT *counts; WidgetT *bar; const char *verb; // "Copying" / "Moving" / "Deleting" int32_t taskId; } FmJobUiT; // The Properties dialog's attribute check boxes and what OK read from them typedef struct { WidgetT *readOnly; WidgetT *hidden; WidgetT *system; WidgetT *archive; uint8_t chosen; } FmPropsDlgT; typedef struct { WindowT *win; WidgetT *driveBar; WidgetT *pathInput; WidgetT *tree; WidgetT *list; WidgetT *statusSel; WidgetT *statusFree; AccelTableT *accel; char curDir[DVX_MAX_PATH]; char filter[FILEOPS_FILTER_MAX]; FileOpsEntryT *entries; // current listing int32_t entryCount; const char **cells; // entryCount * FM_COL_COUNT, handed to the list view FmRowTextT *rowText; char **treePaths; // stb_ds: path of every tree item (item->userData) FileOpsSortE sortCol; bool sortDesc; bool treeActive; // the tree (not the list) has focus bool syncingTree; // tree selection is being set by the program } FmStateT; // ============================================================ // Prototypes // ============================================================ static void addDriveButtons(void); void appShutdown(void); static void buildMenus(void); static void buildWindow(void); static void clipboardPut(bool cut); static void clipboardSetPaths(const char *header, char **paths); static void cmdDelete(void); static void cmdFilter(void); static void cmdInvert(void); static void cmdNewFolder(void); static void cmdOpen(void); static void cmdPaste(void); static void cmdProperties(void); static void cmdRename(void); static void cmdSelectAll(void); static void cmdSort(FileOpsSortE col, bool descending); static void cmdTransferTo(FileOpsOpE op); static void cmdUp(void); static int32_t countFileMgrWindows(const AppContextT *ac); static void freeListing(void); static void freeSelection(char **paths); static char **getSelection(void); static bool jobAddSources(FileOpsJobT *job, char **paths); static void jobAsk(FmJobUiT *j, const char * const *labels, const FileOpsAnswerE *answers, int32_t count); static void jobFinish(FmJobUiT *j, FileOpsStepE result); static void jobOnCancel(WidgetT *w); static void jobOnClose(WindowT *win); static void jobShowProgress(FmJobUiT *j); static void jobStart(FileOpsOpE op, char **paths, const char *destDir); static void jobTask(void *arg); static void listOnDblClick(WidgetT *w); static void listOnFocus(WidgetT *w); static void listOnHeader(WidgetT *w, int32_t col, ListViewSortE dir); static void listOnKey(WidgetT *w, int32_t keyCode, int32_t shift); static void listOnSelChange(WidgetT *w); static bool loadListing(const char *dir); static bool navigate(const char *path, bool fromTree); static void onClose(WindowT *win); static void onDriveClick(WidgetT *w); static void onMenu(WindowT *win, int32_t menuId); static void onPathKey(WidgetT *w, int32_t keyCode, int32_t shift); static void openEntry(const FileOpsEntryT *e); static bool propsOnOk(WindowT *dlg, void *userCtx); static void refresh(void); static void selectByName(const char *name); static void selectionAdd(char ***paths, const char *path); static void setCells(void); static void showError(const char *title, const char *message); static void treeAddChildren(WidgetT *parentItem, const char *dir, const char *expandPath, WidgetT **outNext); static WidgetT *treeAddItem(WidgetT *parent, const char *path, const char *label); static void treeFreePaths(void); static void treeOnChange(WidgetT *w); static void treeOnFocus(WidgetT *w); static void treeOnKey(WidgetT *w, int32_t keyCode, int32_t shift); static void treeRebuild(void); static void updateStatus(void); static void updateTitle(void); int32_t appMain(DxeAppContextT *ctx); // ============================================================ // App descriptor and module state // ============================================================ SHELL_APP_DESCRIPTOR( .name = FM_TITLE, .hasMainLoop = false, .multiInstance = true, .stackSize = SHELL_STACK_DEFAULT, .priority = TS_PRIORITY_NORMAL ); static DxeAppContextT *sCtx = NULL; static FmStateT sState; static FmJobUiT **sJobs = NULL; // stb_ds: running jobs static const char * const sOverwriteLabels[] = { "&Yes", "&No", "Yes to &All", "&Cancel" }; static const char * const sErrorLabels[] = { "&Retry", "&Skip", "&Cancel" }; static const FileOpsAnswerE sOverwriteAnswers[] = { FileOpsAnswerYesE, FileOpsAnswerNoE, FileOpsAnswerAllE, FileOpsAnswerCancelE }; static const FileOpsAnswerE sErrorAnswers[] = { FileOpsAnswerRetryE, FileOpsAnswerSkipE, FileOpsAnswerCancelE }; // The list view keeps a pointer to the column array. static const ListViewColT sColumns[FM_COL_COUNT] = { { "Name", wgtChars(FM_COL_NAME_CHARS), ListViewAlignLeftE }, { "Size", wgtChars(FM_COL_SIZE_CHARS), ListViewAlignRightE }, { "Type", wgtChars(FM_COL_TYPE_CHARS), ListViewAlignLeftE }, { "Modified", wgtChars(FM_COL_DATE_CHARS), ListViewAlignLeftE } }; // One button per drive that exists. The DOS probe never touches the // media, so empty floppy and CD-ROM drives cost nothing here. static void addDriveButtons(void) { PlatformDriveT drives[PLATFORM_DRIVE_MAX]; int32_t count = platformDriveList(drives, PLATFORM_DRIVE_MAX); for (int32_t i = 0; i < count; i++) { char label[PLATFORM_DRIVE_ROOT_MAX]; // "C:" for a DOS drive, "/" for a single-root file system snprintf(label, sizeof(label), "%s", drives[i].root); if (label[1] == ':') { label[2] = '\0'; } WidgetT *b = wgtButton(sState.driveBar, label); b->userData = (void *)(intptr_t)drives[i].root[0]; b->onClick = onDriveClick; } } // Force-kill (Task Manager, shell shutdown): stop every job task and // release its files; the shell destroys the windows. void appShutdown(void) { for (int32_t i = 0; i < (int32_t)arrlen(sJobs); i++) { tsKill((uint32_t)sJobs[i]->taskId); fileOpsJobFree(sJobs[i]->job); free(sJobs[i]); } arrfree(sJobs); freeListing(); treeFreePaths(); if (sState.accel) { dvxFreeAccelTable(sState.accel); sState.accel = NULL; } } static void buildMenus(void) { WindowT *win = sState.win; MenuBarT *menuBar = wmAddMenuBar(win); if (!menuBar) { return; } MenuT *file = wmAddMenu(menuBar, "&File"); wmAddMenuItem(file, "&Open\tEnter", CMD_FILE_OPEN); wmAddMenuItem(file, "New &Folder...", CMD_FILE_NEW_FOLDER); wmAddMenuSeparator(file); wmAddMenuItem(file, "&Copy To...\tF8", CMD_FILE_COPY_TO); wmAddMenuItem(file, "&Move To...\tF7", CMD_FILE_MOVE_TO); wmAddMenuItem(file, "&Rename...\tF2", CMD_FILE_RENAME); wmAddMenuItem(file, "&Delete\tDel", CMD_FILE_DELETE); wmAddMenuItem(file, "P&roperties\tAlt+Enter", CMD_FILE_PROPERTIES); wmAddMenuSeparator(file); wmAddMenuItem(file, "New &Window", CMD_FILE_NEW_WINDOW); wmAddMenuSeparator(file); wmAddMenuItem(file, "E&xit", CMD_FILE_EXIT); MenuT *edit = wmAddMenu(menuBar, "&Edit"); wmAddMenuItem(edit, "Cu&t\tCtrl+X", CMD_EDIT_CUT); wmAddMenuItem(edit, "&Copy\tCtrl+C", CMD_EDIT_COPY); wmAddMenuItem(edit, "&Paste\tCtrl+V", CMD_EDIT_PASTE); wmAddMenuSeparator(edit); wmAddMenuItem(edit, "Select &All\tCtrl+A", CMD_EDIT_SELECT_ALL); wmAddMenuItem(edit, "&Invert Selection", CMD_EDIT_INVERT); MenuT *view = wmAddMenu(menuBar, "&View"); wmAddMenuItem(view, "&Refresh\tF5", CMD_VIEW_REFRESH); wmAddMenuItem(view, "&Filter...", CMD_VIEW_FILTER); wmAddMenuItem(view, "&Up One Level\tBackspace", CMD_VIEW_UP); wmAddMenuSeparator(view); wmAddMenuRadioItem(view, "Sort by &Name", CMD_VIEW_SORT_NAME, true); wmAddMenuRadioItem(view, "Sort by &Size", CMD_VIEW_SORT_SIZE, false); wmAddMenuRadioItem(view, "Sort by &Type", CMD_VIEW_SORT_TYPE, false); wmAddMenuRadioItem(view, "Sort by &Date", CMD_VIEW_SORT_DATE, false); wmAddMenuSeparator(view); wmAddMenuCheckItem(view, "D&escending", CMD_VIEW_SORT_DESC, false); MenuT *help = wmAddMenu(menuBar, "&Help"); wmAddMenuItem(help, "&Contents\tF1", CMD_HELP_CONTENTS); wmAddMenuSeparator(help); wmAddMenuItem(help, "&About File Manager...", CMD_HELP_ABOUT); // Keys that never mean anything to the path bar go in the accelerator // table; Del, Backspace and Ctrl+A/C/V/X belong to the text box when // it has focus, so the tree and list handle those themselves. sState.accel = dvxCreateAccelTable(); dvxAddAccel(sState.accel, KEY_F2, 0, CMD_FILE_RENAME); dvxAddAccel(sState.accel, KEY_F5, 0, CMD_VIEW_REFRESH); dvxAddAccel(sState.accel, KEY_F7, 0, CMD_FILE_MOVE_TO); dvxAddAccel(sState.accel, KEY_F8, 0, CMD_FILE_COPY_TO); dvxAddAccel(sState.accel, KEY_ENTER, ACCEL_ALT, CMD_FILE_PROPERTIES); dvxAddAccel(sState.accel, KEY_SCAN_ENTER | KEY_EXT_FLAG, ACCEL_ALT, CMD_FILE_PROPERTIES); win->accelTable = sState.accel; } static void buildWindow(void) { AppContextT *ac = sCtx->shellCtx; WidgetT *root = wgtInitWindow(ac, sState.win); sState.driveBar = wgtToolbar(root); addDriveButtons(); WidgetT *pathRow = wgtHBox(root); wgtLabel(pathRow, "&Path:"); sState.pathInput = wgtTextInput(pathRow, DVX_MAX_PATH); sState.pathInput->weight = WGT_WEIGHT_FILL; sState.pathInput->prefW = wgtChars(FM_PATH_CHARS); sState.pathInput->onKeyDown = onPathKey; wgtSetName(sState.pathInput, FM_NAME_PATH); WidgetT *split = wgtSplitter(root, true); split->weight = WGT_WEIGHT_FILL; sState.tree = wgtTreeView(split); sState.tree->onChange = treeOnChange; sState.tree->onFocus = treeOnFocus; sState.tree->onKeyDown = treeOnKey; wgtSetName(sState.tree, FM_NAME_TREE); sState.list = wgtListView(split); sState.list->onDblClick = listOnDblClick; sState.list->onChange = listOnSelChange; sState.list->onFocus = listOnFocus; sState.list->onKeyDown = listOnKey; wgtSetName(sState.list, FM_NAME_LIST); wgtListViewSetColumns(sState.list, sColumns, FM_COL_COUNT); wgtListViewSetMultiSelect(sState.list, true); wgtListViewSetHeaderClickCallback(sState.list, listOnHeader); wgtSplitterSetPos(split, FM_TREE_W); WidgetT *status = wgtStatusBar(root); sState.statusSel = wgtLabel(status, ""); sState.statusSel->weight = WGT_WEIGHT_FILL; sState.statusFree = wgtLabel(status, ""); wgtSetName(sState.statusSel, FM_NAME_STATUS); } // Put the selection on the DVX clipboard as a file list. static void clipboardPut(bool cut) { char **paths = getSelection(); if (arrlen(paths) > 0) { clipboardSetPaths(cut ? FM_CLIP_CUT : FM_CLIP_COPY, paths); } freeSelection(paths); } // Clipboard text: the header line, then one path per line. static void clipboardSetPaths(const char *header, char **paths) { char *text = NULL; for (const char *p = header; *p; p++) { arrput(text, *p); } for (int32_t i = 0; i < (int32_t)arrlen(paths); i++) { arrput(text, FM_CLIP_EOL); for (const char *p = paths[i]; *p; p++) { arrput(text, *p); } } dvxClipboardCopy(text, (int32_t)arrlen(text)); arrfree(text); } static void cmdDelete(void) { char **paths = getSelection(); int32_t count = (int32_t)arrlen(paths); char msg[FM_PROMPT_MAX]; if (count == 0) { freeSelection(paths); return; } if (count == 1) { snprintf(msg, sizeof(msg), "Delete %s?", paths[0]); } else { snprintf(msg, sizeof(msg), "Delete these %" PRId32 " items?", count); } if (dvxMessageBox(sCtx->shellCtx, "Confirm Delete", msg, MB_YESNO | MB_ICONQUESTION) == ID_YES) { jobStart(FileOpsDeleteE, paths, NULL); } freeSelection(paths); } static void cmdFilter(void) { char buf[FILEOPS_FILTER_MAX]; if (!dvxInputBox(sCtx->shellCtx, "Filter", "Show only files matching:", sState.filter, buf, sizeof(buf))) { return; } snprintf(sState.filter, sizeof(sState.filter), "%s", buf[0] ? buf : PLATFORM_FILTER_ALL); refresh(); } static void cmdInvert(void) { for (int32_t i = 0; i < sState.entryCount; i++) { wgtListViewSetItemSelected(sState.list, i, !wgtListViewIsItemSelected(sState.list, i)); } updateStatus(); } static void cmdNewFolder(void) { char name[FM_NAME_MAX]; char err[FILEOPS_MSG_MAX]; if (!dvxInputBox(sCtx->shellCtx, "New Folder", "Name of the new folder:", FM_DEFAULT_FOLDER, name, sizeof(name))) { return; } if (!fileOpsMakeDir(sState.curDir, name, err, sizeof(err))) { showError("New Folder", err); return; } dvxLog("FileMgr: created folder %s", name); refresh(); selectByName(name); } // Enter / double-click / File > Open: a folder is opened in place, a // program is started, a document opens in its associated program. static void cmdOpen(void) { if (sState.treeActive) { return; } int32_t row = wgtListViewGetSelected(sState.list); if (row >= 0 && row < sState.entryCount) { openEntry(&sState.entries[row]); } } // Paste the clipboard's file list into the current folder. static void cmdPaste(void) { int32_t len = 0; const char *clip = dvxClipboardGet(&len); bool cut = false; size_t headLen; if (!clip) { return; } if (strncmp(clip, FM_CLIP_CUT, strlen(FM_CLIP_CUT)) == 0 && clip[strlen(FM_CLIP_CUT)] == FM_CLIP_EOL) { cut = true; headLen = strlen(FM_CLIP_CUT); } else if (strncmp(clip, FM_CLIP_COPY, strlen(FM_CLIP_COPY)) == 0 && clip[strlen(FM_CLIP_COPY)] == FM_CLIP_EOL) { headLen = strlen(FM_CLIP_COPY); } else { showError("Paste", "The clipboard holds no files. Use Copy or Cut in a File Manager window first."); return; } char **paths = NULL; const char *p = clip + headLen + 1; const char *end = clip + len; while (p < end) { const char *eol = memchr(p, FM_CLIP_EOL, (size_t)(end - p)); size_t n = eol ? (size_t)(eol - p) : (size_t)(end - p); char *path = (char *)malloc(n + 1); if (path) { memcpy(path, p, n); path[n] = '\0'; arrput(paths, path); } p += n + 1; } jobStart(cut ? FileOpsMoveE : FileOpsCopyE, paths, sState.curDir); // A cut is used up by its paste. if (cut) { dvxClipboardClear(); } freeSelection(paths); } // Size, dates and editable attributes of the selection. Attribute changes // apply to every selected item. static void cmdProperties(void) { AppContextT *ac = sCtx->shellCtx; char **paths = getSelection(); int32_t count = (int32_t)arrlen(paths); if (count == 0) { freeSelection(paths); return; } uint64_t bytes = 0; int32_t files = 0; int32_t folders = 0; bool allRead = true; for (int32_t i = 0; i < count; i++) { uint64_t b; int32_t f; int32_t d; struct stat st; allRead = fileOpsTreeSize(paths[i], &b, &f, &d) && allRead; bytes += b; files += f; folders += d + (stat(paths[i], &st) == 0 && S_ISDIR(st.st_mode) ? 1 : 0); } uint8_t attr = 0; struct stat st; char line[FM_PROMPT_MAX]; char sizeText[FILEOPS_SIZE_TEXT_MAX]; char dateText[FILEOPS_DATE_TEXT_MAX]; char location[DVX_MAX_PATH]; bool haveStat = stat(paths[0], &st) == 0; fileOpsGetAttr(paths[0], &attr); if (!fileOpsPathParent(paths[0], location, sizeof(location))) { snprintf(location, sizeof(location), "%s", paths[0]); } WindowT *win = dvxDialogCreate(ac, "Properties", FM_PROPS_W); if (!win) { freeSelection(paths); return; } WidgetT *root = win->widgetRoot; if (count == 1) { snprintf(line, sizeof(line), "Name: %s", platformPathBaseName(paths[0])); } else { snprintf(line, sizeof(line), "Name: %" PRId32 " items", count); } wgtLabel(root, line); snprintf(line, sizeof(line), "Location: %s", location); wgtLabel(root, line); fileOpsFormatSize(bytes, sizeText, sizeof(sizeText)); snprintf(line, sizeof(line), "Size: %s bytes%s", sizeText, allRead ? "" : " (some items could not be read)"); wgtLabel(root, line); snprintf(line, sizeof(line), "Contains: %" PRId32 " files, %" PRId32 " folders", files, folders); wgtLabel(root, line); if (count == 1 && haveStat) { fileOpsFormatDate(st.st_mtime, dateText, sizeof(dateText)); snprintf(line, sizeof(line), "Modified: %s", dateText); wgtLabel(root, line); } WidgetT *frame = wgtFrame(root, "Attributes"); FmPropsDlgT pd = {0}; pd.readOnly = wgtCheckbox(frame, "&Read-only"); pd.hidden = wgtCheckbox(frame, "&Hidden"); pd.system = wgtCheckbox(frame, "&System"); pd.archive = wgtCheckbox(frame, "&Archive"); wgtCheckboxSetChecked(pd.readOnly, (attr & FILEOPS_ATTR_READONLY) != 0); wgtCheckboxSetChecked(pd.hidden, (attr & FILEOPS_ATTR_HIDDEN) != 0); wgtCheckboxSetChecked(pd.system, (attr & FILEOPS_ATTR_SYSTEM) != 0); wgtCheckboxSetChecked(pd.archive, (attr & FILEOPS_ATTR_ARCHIVE) != 0); dvxDialogAddOkCancel(win, "&OK", propsOnOk, &pd); bool apply = dvxDialogRun(ac, win); uint8_t chosen = pd.chosen; if (apply) { bool failed = false; for (int32_t i = 0; i < count; i++) { uint8_t old = 0; if (!fileOpsGetAttr(paths[i], &old) || !fileOpsSetAttr(paths[i], (uint8_t)((old & ~FILEOPS_ATTR_EDITABLE) | chosen))) { failed = true; } } if (failed) { showError("Properties", "The attributes of some items could not be changed."); } refresh(); } freeSelection(paths); } static void cmdRename(void) { char **paths = getSelection(); char name[FM_NAME_MAX]; char err[FILEOPS_MSG_MAX]; char dir[DVX_MAX_PATH]; char prompt[FM_PROMPT_MAX]; if (arrlen(paths) != 1) { if (arrlen(paths) > 1) { showError("Rename", "Select one file or folder to rename."); } freeSelection(paths); return; } const char *oldName = platformPathBaseName(paths[0]); snprintf(prompt, sizeof(prompt), "New name for %s:", oldName); if (fileOpsPathParent(paths[0], dir, sizeof(dir)) && dvxInputBox(sCtx->shellCtx, "Rename", prompt, oldName, name, sizeof(name))) { bool renamedCurrent = fileOpsPathSame(paths[0], sState.curDir); if (!fileOpsRename(dir, oldName, name, err, sizeof(err))) { showError("Rename", err); } else if (renamedCurrent) { // The tree's current folder itself was renamed. char newPath[DVX_MAX_PATH]; if (fileOpsPathJoin(newPath, sizeof(newPath), dir, name)) { snprintf(sState.curDir, sizeof(sState.curDir), "%s", newPath); } } refresh(); } freeSelection(paths); } static void cmdSelectAll(void) { wgtListViewSelectAll(sState.list); updateStatus(); } static void cmdSort(FileOpsSortE col, bool descending) { static const int32_t sortCmds[] = { CMD_VIEW_SORT_NAME, CMD_VIEW_SORT_SIZE, CMD_VIEW_SORT_TYPE, CMD_VIEW_SORT_DATE }; sState.sortCol = col; sState.sortDesc = descending; if (sState.win->menuBar) { for (int32_t i = 0; i < DVX_ARRAY_LEN(sortCmds); i++) { wmMenuItemSetChecked(sState.win->menuBar, sortCmds[i], i == (int32_t)col); } wmMenuItemSetChecked(sState.win->menuBar, CMD_VIEW_SORT_DESC, descending); } fileOpsListSort(sState.entries, sState.entryCount, col, descending); setCells(); } // Copy To / Move To: ask for the destination folder. static void cmdTransferTo(FileOpsOpE op) { char **paths = getSelection(); int32_t count = (int32_t)arrlen(paths); const char *verb = op == FileOpsMoveE ? "Move" : "Copy"; char prompt[FM_PROMPT_MAX]; char dest[DVX_MAX_PATH]; char norm[DVX_MAX_PATH]; if (count == 0) { freeSelection(paths); return; } if (count == 1) { snprintf(prompt, sizeof(prompt), "%s %s to folder:", verb, platformPathBaseName(paths[0])); } else { snprintf(prompt, sizeof(prompt), "%s %" PRId32 " items to folder:", verb, count); } if (dvxInputBox(sCtx->shellCtx, verb, prompt, sState.curDir, dest, sizeof(dest))) { if (fileOpsPathNormalize(dest, norm, sizeof(norm))) { jobStart(op, paths, norm); } else { snprintf(prompt, sizeof(prompt), "%s is not an existing folder, or its drive is not ready.", dest); showError(verb, prompt); } } freeSelection(paths); } static void cmdUp(void) { char parent[DVX_MAX_PATH]; if (fileOpsPathParent(sState.curDir, parent, sizeof(parent))) { navigate(parent, false); } } // File Manager windows already open (other instances), used to cascade. static int32_t countFileMgrWindows(const AppContextT *ac) { int32_t n = 0; for (int32_t i = 0; i < ac->stack.count; i++) { if (strncmp(ac->stack.windows[i]->title, FM_TITLE, strlen(FM_TITLE)) == 0) { n++; } } return n; } // Drop the list view's reference first: it keeps the cell array. static void freeListing(void) { if (sState.list) { wgtListViewSetData(sState.list, NULL, 0); } fileOpsListFree(sState.entries, sState.entryCount); free(sState.cells); free(sState.rowText); sState.entries = NULL; sState.entryCount = 0; sState.cells = NULL; sState.rowText = NULL; } static void freeSelection(char **paths) { for (int32_t i = 0; i < (int32_t)arrlen(paths); i++) { free(paths[i]); } arrfree(paths); } // Full paths the file commands act on: the tree's folder when the tree has // focus, else the selected list rows (or the row under the cursor). static char **getSelection(void) { char **paths = NULL; char path[DVX_MAX_PATH]; if (sState.treeActive) { if (!fileOpsPathIsRoot(sState.curDir)) { selectionAdd(&paths, sState.curDir); } return paths; } for (int32_t i = 0; i < sState.entryCount; i++) { if (wgtListViewIsItemSelected(sState.list, i) && fileOpsPathJoin(path, sizeof(path), sState.curDir, sState.entries[i].name)) { selectionAdd(&paths, path); } } int32_t row = wgtListViewGetSelected(sState.list); if (arrlen(paths) == 0 && row >= 0 && row < sState.entryCount && fileOpsPathJoin(path, sizeof(path), sState.curDir, sState.entries[row].name)) { selectionAdd(&paths, path); } return paths; } // Add every path to the job; report the ones that cannot be added. static bool jobAddSources(FileOpsJobT *job, char **paths) { bool any = false; for (int32_t i = 0; i < (int32_t)arrlen(paths); i++) { if (fileOpsJobAddSource(job, paths[i])) { any = true; } else { char msg[FM_PROMPT_MAX]; snprintf(msg, sizeof(msg), "%s cannot be found, or the destination path would be too long. It is skipped.", paths[i]); showError(FM_TITLE, msg); } } return any; } // Put the job's question to the user (last button = Cancel) and hand the // answer back. static void jobAsk(FmJobUiT *j, const char * const *labels, const FileOpsAnswerE *answers, int32_t count) { jobShowProgress(j); int32_t pick = dvxButtonBox(sCtx->shellCtx, FM_TITLE, fileOpsJobMessage(j->job), MB_ICONQUESTION, labels, count); fileOpsJobAnswer(j->job, answers[pick]); } // Runs on the job's task: close its window, free it, refresh the listing. static void jobFinish(FmJobUiT *j, FileOpsStepE result) { AppContextT *ac = sCtx->shellCtx; dvxLog("FileMgr: %s %s", j->verb, result == FileOpsStepDoneE ? "finished" : "cancelled"); for (int32_t i = 0; i < (int32_t)arrlen(sJobs); i++) { if (sJobs[i] == j) { arrdel(sJobs, i); break; } } if (j->win) { dvxDestroyWindow(ac, j->win); } fileOpsJobFree(j->job); free(j); if (sState.win) { refresh(); } } static void jobOnCancel(WidgetT *w) { FmJobUiT *j = (FmJobUiT *)w->userData; fileOpsJobCancel(j->job); } // The close gadget cancels; the task closes the window when it stops. static void jobOnClose(WindowT *win) { FmJobUiT *j = (FmJobUiT *)win->userData; fileOpsJobCancel(j->job); } static void jobShowProgress(FmJobUiT *j) { FileOpsProgressT p; char line[FM_PROMPT_MAX]; if (!j->win) { return; } fileOpsJobProgress(j->job, &p); snprintf(line, sizeof(line), "%s %s", j->verb, p.current); wgtSetText(j->what, line); snprintf(line, sizeof(line), "%" PRId32 " of %" PRId32 " files", p.filesDone, p.filesTotal); wgtSetText(j->counts, line); wgtProgressBarSetValue(j->bar, p.percent); } // Start a copy, move or delete of paths (into destDir) on its own task. static void jobStart(FileOpsOpE op, char **paths, const char *destDir) { AppContextT *ac = sCtx->shellCtx; FileOpsJobT *job = fileOpsJobCreate(op, destDir); FmJobUiT *j = (FmJobUiT *)calloc(1, sizeof(FmJobUiT)); if (!job || !j) { fileOpsJobFree(job); free(j); showError(FM_TITLE, "Not enough memory to start the operation."); return; } if (!jobAddSources(job, paths)) { fileOpsJobFree(job); free(j); return; } j->job = job; j->verb = op == FileOpsCopyE ? "Copying" : op == FileOpsMoveE ? "Moving" : "Deleting"; j->win = dvxCreateWindowCentered(ac, j->verb, FM_JOB_WIN_W, FM_JOB_WIN_H, false); if (j->win) { WidgetT *root = wgtInitWindow(ac, j->win); j->what = wgtLabel(root, ""); j->bar = wgtProgressBar(root); j->counts = wgtLabel(root, ""); WidgetT *row = wgtHBox(root); WidgetT *cancel = wgtButton(row, "&Cancel"); row->align = AlignCenterE; j->what->weight = WGT_WEIGHT_FILL; cancel->userData = j; cancel->onClick = jobOnCancel; j->win->userData = j; j->win->onClose = jobOnClose; } jobShowProgress(j); j->taskId = tsCreate(j->verb, jobTask, j, FM_JOB_STACK, TS_PRIORITY_NORMAL); if (j->taskId < 0) { if (j->win) { dvxDestroyWindow(ac, j->win); } fileOpsJobFree(job); free(j); showError(FM_TITLE, "Not enough memory to start the operation."); return; } arrput(sJobs, j); } // The job's task: step through the job in time slices, yielding between // them, and ask the user when the job needs an answer. static void jobTask(void *arg) { FmJobUiT *j = (FmJobUiT *)arg; PlatformTicksT slice = PLATFORM_MS_TO_TICKS(FM_SLICE_MS); PlatformTicksT sliceEnd; FileOpsStepE step; sCtx->shellCtx->currentAppId = sCtx->appId; sliceEnd = platformClock() + slice; for (step = fileOpsJobStep(j->job); step != FileOpsStepDoneE && step != FileOpsStepCancelledE; step = fileOpsJobStep(j->job)) { if (step == FileOpsStepAskE) { jobAsk(j, sOverwriteLabels, sOverwriteAnswers, DVX_ARRAY_LEN(sOverwriteLabels)); sliceEnd = platformClock() + slice; } else if (step == FileOpsStepErrorE) { jobAsk(j, sErrorLabels, sErrorAnswers, DVX_ARRAY_LEN(sErrorLabels)); sliceEnd = platformClock() + slice; } else if (platformClock() >= sliceEnd) { jobShowProgress(j); tsYield(); sliceEnd = platformClock() + slice; } } jobFinish(j, step); } static void listOnDblClick(WidgetT *w) { (void)w; cmdOpen(); } static void listOnFocus(WidgetT *w) { (void)w; sState.treeActive = false; } // Header click: sort by that column; the same column again reverses. The // list's own text sort is cleared (sizes and dates need real ordering). static void listOnHeader(WidgetT *w, int32_t col, ListViewSortE dir) { static const FileOpsSortE colSort[FM_COL_COUNT] = { FileOpsSortNameE, FileOpsSortSizeE, FileOpsSortTypeE, FileOpsSortDateE }; (void)dir; wgtListViewSetSort(w, -1, ListViewSortNoneE); if (col < 0 || col >= FM_COL_COUNT) { return; } FileOpsSortE sort = colSort[col]; cmdSort(sort, sort == sState.sortCol ? !sState.sortDesc : false); } static void listOnKey(WidgetT *w, int32_t keyCode, int32_t shift) { (void)w; (void)shift; switch (keyCode) { case KEY_DELETE: cmdDelete(); break; case KEY_BACKSPACE: cmdUp(); break; case KEY_CTRL_A: cmdSelectAll(); break; case KEY_CTRL_C: clipboardPut(false); break; case KEY_CTRL_X: clipboardPut(true); break; case KEY_CTRL_V: cmdPaste(); break; default: break; } } static void listOnSelChange(WidgetT *w) { (void)w; updateStatus(); } // Read dir into the list (sorted), replacing the current listing. False // (listing unchanged) when it cannot be read. static bool loadListing(const char *dir) { FileOpsEntryT *entries = NULL; int32_t count = 0; if (!fileOpsListDir(dir, sState.filter, &entries, &count)) { return false; } freeListing(); sState.entries = entries; sState.entryCount = count; sState.cells = (const char **)calloc((size_t)(count > 0 ? count : 1) * FM_COL_COUNT, sizeof(const char *)); sState.rowText = (FmRowTextT *)calloc((size_t)(count > 0 ? count : 1), sizeof(FmRowTextT)); if (!sState.cells || !sState.rowText) { freeListing(); return false; } fileOpsListSort(sState.entries, sState.entryCount, sState.sortCol, sState.sortDesc); setCells(); return true; } // Go to a folder. fromTree: the user chose it in the tree, which then // only grows its sub-folders; otherwise the tree is rebuilt. static bool navigate(const char *path, bool fromTree) { char norm[DVX_MAX_PATH]; char msg[FM_PROMPT_MAX]; if (!fileOpsPathNormalize(path, norm, sizeof(norm)) || !loadListing(norm)) { snprintf(msg, sizeof(msg), "Cannot open %s.\nThe folder does not exist or the drive is not ready.", path); showError(FM_TITLE, msg); return false; } snprintf(sState.curDir, sizeof(sState.curDir), "%s", norm); wgtSetText(sState.pathInput, sState.curDir); updateTitle(); updateStatus(); if (fromTree) { WidgetT *item = wgtTreeViewGetSelected(sState.tree); if (item && !item->firstChild) { treeAddChildren(item, sState.curDir, NULL, NULL); wgtTreeItemSetExpanded(item, true); } } else { treeRebuild(); } return true; } // Closing with operations still running asks first; the jobs then stop // and close their own windows (the program ends with the last one). static void onClose(WindowT *win) { AppContextT *ac = sCtx->shellCtx; if (arrlen(sJobs) > 0) { if (dvxMessageBox(ac, FM_TITLE, "File operations are still running.\nStop them and close?", MB_YESNO | MB_ICONQUESTION) != ID_YES) { return; } for (int32_t i = 0; i < (int32_t)arrlen(sJobs); i++) { fileOpsJobCancel(sJobs[i]->job); } } freeListing(); treeFreePaths(); sState.list = NULL; sState.win = NULL; dvxDestroyWindow(ac, win); if (sState.accel) { dvxFreeAccelTable(sState.accel); sState.accel = NULL; } } // Drive button: that drive's current folder ("C:" resolves to it). static void onDriveClick(WidgetT *w) { char spec[PLATFORM_DRIVE_ROOT_MAX]; char letter = (char)(intptr_t)w->userData; if (letter == '/') { snprintf(spec, sizeof(spec), "/"); } else { snprintf(spec, sizeof(spec), "%c:", letter); } navigate(spec, false); } static void onMenu(WindowT *win, int32_t menuId) { (void)win; switch (menuId) { case CMD_FILE_OPEN: cmdOpen(); break; case CMD_FILE_NEW_FOLDER: cmdNewFolder(); break; case CMD_FILE_COPY_TO: cmdTransferTo(FileOpsCopyE); break; case CMD_FILE_MOVE_TO: cmdTransferTo(FileOpsMoveE); break; case CMD_FILE_RENAME: cmdRename(); break; case CMD_FILE_DELETE: cmdDelete(); break; case CMD_FILE_PROPERTIES: cmdProperties(); break; case CMD_FILE_NEW_WINDOW: shellLoadAppWithArgs(sCtx->shellCtx, sCtx->appPath, sState.curDir); break; case CMD_FILE_EXIT: onClose(sState.win); break; case CMD_EDIT_CUT: clipboardPut(true); break; case CMD_EDIT_COPY: clipboardPut(false); break; case CMD_EDIT_PASTE: cmdPaste(); break; case CMD_EDIT_SELECT_ALL: cmdSelectAll(); break; case CMD_EDIT_INVERT: cmdInvert(); break; case CMD_VIEW_REFRESH: refresh(); break; case CMD_VIEW_FILTER: cmdFilter(); break; case CMD_VIEW_UP: cmdUp(); break; case CMD_VIEW_SORT_NAME: cmdSort(FileOpsSortNameE, sState.sortDesc); break; case CMD_VIEW_SORT_SIZE: cmdSort(FileOpsSortSizeE, sState.sortDesc); break; case CMD_VIEW_SORT_TYPE: cmdSort(FileOpsSortTypeE, sState.sortDesc); break; case CMD_VIEW_SORT_DATE: cmdSort(FileOpsSortDateE, sState.sortDesc); break; case CMD_VIEW_SORT_DESC: cmdSort(sState.sortCol, !sState.sortDesc); break; case CMD_HELP_CONTENTS: shellOpenHelp(sCtx->shellCtx, sCtx->helpFile); break; case CMD_HELP_ABOUT: dvxInfoBox(sCtx->shellCtx, "About File Manager", "File Manager\n\nBrowse, copy, move, rename and delete files and folders.\n\nCopyright 2026 Scott Duensing"); break; default: break; } } // Enter in the path bar goes to the typed folder. static void onPathKey(WidgetT *w, int32_t keyCode, int32_t shift) { (void)shift; if (keyCode == KEY_ENTER) { navigate(wgtGetText(w), false); } } static void openEntry(const FileOpsEntryT *e) { AppContextT *ac = sCtx->shellCtx; char path[DVX_MAX_PATH]; char program[DVX_MAX_PATH]; char msg[FM_PROMPT_MAX]; if (!fileOpsPathJoin(path, sizeof(path), sState.curDir, e->name)) { return; } if (e->isDir) { navigate(path, false); return; } if (dvxHasExt(e->name, ".app")) { shellLoadApp(ac, path); return; } if (!shellAssocLookup(fileOpsExtension(e->name), program, sizeof(program))) { snprintf(msg, sizeof(msg), "No program is set to open %s files.\nFile types are set in the Control Panel.", fileOpsExtension(e->name)[0] ? fileOpsExtension(e->name) : "these"); showError(FM_TITLE, msg); return; } if (!shellAssocOpenDocument(ac, path)) { snprintf(msg, sizeof(msg), "%s could not be opened.", e->name); showError(FM_TITLE, msg); } } // Properties OK: read the check boxes while they still exist. static bool propsOnOk(WindowT *dlg, void *userCtx) { FmPropsDlgT *pd = (FmPropsDlgT *)userCtx; (void)dlg; pd->chosen = wgtCheckboxIsChecked(pd->readOnly) ? FILEOPS_ATTR_READONLY : 0; pd->chosen |= wgtCheckboxIsChecked(pd->hidden) ? FILEOPS_ATTR_HIDDEN : 0; pd->chosen |= wgtCheckboxIsChecked(pd->system) ? FILEOPS_ATTR_SYSTEM : 0; pd->chosen |= wgtCheckboxIsChecked(pd->archive) ? FILEOPS_ATTR_ARCHIVE : 0; return true; } // Re-read the current folder, keeping the row under the cursor. static void refresh(void) { char keep[FM_NAME_MAX]; int32_t row = wgtListViewGetSelected(sState.list); keep[0] = '\0'; if (row >= 0 && row < sState.entryCount) { snprintf(keep, sizeof(keep), "%s", sState.entries[row].name); } if (!loadListing(sState.curDir)) { // The folder is gone (deleted, renamed, disk removed): go up. char parent[DVX_MAX_PATH]; if (fileOpsPathParent(sState.curDir, parent, sizeof(parent)) && navigate(parent, false)) { return; } freeListing(); } selectByName(keep); treeRebuild(); updateTitle(); updateStatus(); } // Put the list cursor (and selection) on the entry called name, if any. static void selectByName(const char *name) { for (int32_t i = 0; i < sState.entryCount; i++) { if (strcasecmp(sState.entries[i].name, name) == 0) { wgtListViewSetSelected(sState.list, i); wgtListViewSetItemSelected(sState.list, i, true); updateStatus(); return; } } } // Adds a copy of path to a selection list; a copy that cannot be made is // left out, so every entry is a string. static void selectionAdd(char ***paths, const char *path) { char *copy = strdup(path); if (copy) { arrput(*paths, copy); } } // Fill the cell array from the listing and hand it to the list view. static void setCells(void) { for (int32_t i = 0; i < sState.entryCount; i++) { const FileOpsEntryT *e = &sState.entries[i]; FmRowTextT *t = &sState.rowText[i]; const char **cell = &sState.cells[i * FM_COL_COUNT]; t->size[0] = '\0'; if (!e->isDir) { fileOpsFormatSize(e->size, t->size, sizeof(t->size)); } fileOpsTypeName(e, t->type, sizeof(t->type)); fileOpsFormatDate(e->mtime, t->date, sizeof(t->date)); cell[FM_COL_NAME] = e->name; cell[FM_COL_SIZE] = t->size; cell[FM_COL_TYPE] = t->type; cell[FM_COL_DATE] = t->date; } wgtListViewSetData(sState.list, sState.cells, sState.entryCount); wgtListViewClearSelection(sState.list); updateStatus(); } static void showError(const char *title, const char *message) { dvxMessageBox(sCtx->shellCtx, title, message, MB_OK | MB_ICONERROR); } // Add dir's sub-folders under parentItem. When expandPath is one of them, // *outNext receives its item. static void treeAddChildren(WidgetT *parentItem, const char *dir, const char *expandPath, WidgetT **outNext) { FileOpsEntryT *entries = NULL; int32_t count = 0; char path[DVX_MAX_PATH]; if (!fileOpsListDir(dir, NULL, &entries, &count)) { return; } fileOpsListSort(entries, count, FileOpsSortNameE, false); for (int32_t i = 0; i < count && entries[i].isDir; i++) { if (!fileOpsPathJoin(path, sizeof(path), dir, entries[i].name)) { continue; } WidgetT *item = treeAddItem(parentItem, path, entries[i].name); if (item && outNext && expandPath && fileOpsPathSame(path, expandPath)) { *outNext = item; } } fileOpsListFree(entries, count); } // One tree item remembering its folder path. static WidgetT *treeAddItem(WidgetT *parent, const char *path, const char *label) { char *copy = strdup(path); if (!copy) { return NULL; } WidgetT *item = wgtTreeItem(parent, label); if (!item) { free(copy); return NULL; } arrput(sState.treePaths, copy); item->userData = copy; return item; } static void treeFreePaths(void) { for (int32_t i = 0; i < (int32_t)arrlen(sState.treePaths); i++) { free(sState.treePaths[i]); } arrfree(sState.treePaths); sState.treePaths = NULL; } static void treeOnChange(WidgetT *w) { if (sState.syncingTree) { return; } WidgetT *item = wgtTreeViewGetSelected(w); if (item && item->userData && !fileOpsPathSame((const char *)item->userData, sState.curDir)) { navigate((const char *)item->userData, true); } } static void treeOnFocus(WidgetT *w) { (void)w; sState.treeActive = true; } static void treeOnKey(WidgetT *w, int32_t keyCode, int32_t shift) { (void)w; (void)shift; if (keyCode == KEY_DELETE) { cmdDelete(); } else if (keyCode == KEY_BACKSPACE) { cmdUp(); } } // Root, then each folder on the way to the current one with its sibling // folders, then the current folder's own sub-folders. static void treeRebuild(void) { sState.syncingTree = true; while (sState.tree->firstChild) { wgtDestroy(sState.tree->firstChild); } treeFreePaths(); // Walk up from the current folder to the root, remembering each step // (one block: the chain is too big for a task stack). char (*chain)[DVX_MAX_PATH] = malloc(sizeof(*chain) * FM_TREE_DEPTH_MAX); int32_t depth = 0; if (chain) { snprintf(chain[0], sizeof(chain[0]), "%s", sState.curDir); depth = 1; while (depth < FM_TREE_DEPTH_MAX && fileOpsPathParent(chain[depth - 1], chain[depth], sizeof(chain[depth]))) { depth++; } } WidgetT *item = depth > 0 ? treeAddItem(sState.tree, chain[depth - 1], chain[depth - 1]) : NULL; for (int32_t level = depth - 1; item && level > 0; level--) { WidgetT *next = NULL; treeAddChildren(item, chain[level], chain[level - 1], &next); wgtTreeItemSetExpanded(item, true); item = next; } if (item) { treeAddChildren(item, sState.curDir, NULL, NULL); wgtTreeItemSetExpanded(item, true); wgtTreeViewSetSelected(sState.tree, item); } free(chain); sState.syncingTree = false; } static void updateStatus(void) { char text[FM_STATUS_MAX]; char sizeText[FILEOPS_SIZE_TEXT_MAX]; int32_t selected = 0; uint64_t bytes = 0; uint64_t totalBytes; uint64_t freeBytes; for (int32_t i = 0; i < sState.entryCount; i++) { if (wgtListViewIsItemSelected(sState.list, i)) { selected++; bytes += sState.entries[i].size; } } fileOpsFormatSize(bytes, sizeText, sizeof(sizeText)); if (selected > 0) { snprintf(text, sizeof(text), "%" PRId32 " of %" PRId32 " selected (%s bytes)", selected, sState.entryCount, sizeText); } else { snprintf(text, sizeof(text), "%" PRId32 " items", sState.entryCount); } wgtSetText(sState.statusSel, text); if (platformDiskSpace(sState.curDir, &totalBytes, &freeBytes)) { fileOpsFormatSize(freeBytes, sizeText, sizeof(sizeText)); snprintf(text, sizeof(text), "%s bytes free", sizeText); } else { text[0] = '\0'; } wgtSetText(sState.statusFree, text); } static void updateTitle(void) { char title[FM_TITLE_MAX]; bool filtered = strcmp(sState.filter, PLATFORM_FILTER_ALL) != 0; snprintf(title, sizeof(title), FM_TITLE " - %s%s%s", sState.curDir, filtered ? DVX_PATH_SEP : "", filtered ? sState.filter : ""); dvxSetTitle(sCtx->shellCtx, sState.win, title); } // Arguments: an optional starting folder (default: the current one). int32_t appMain(DxeAppContextT *ctx) { AppContextT *ac = ctx->shellCtx; int32_t offset = countFileMgrWindows(ac) * FM_CASCADE; char start[DVX_MAX_PATH]; sCtx = ctx; memset(&sState, 0, sizeof(sState)); snprintf(sState.filter, sizeof(sState.filter), "%s", PLATFORM_FILTER_ALL); sState.sortCol = FileOpsSortNameE; int32_t winX = FM_WIN_X + offset; int32_t winY = FM_WIN_Y + offset; if (winX + FM_WIN_W > ac->display.width || winY + FM_WIN_H > ac->display.height) { winX = FM_WIN_X; winY = FM_WIN_Y; } sState.win = dvxCreateWindow(ac, FM_TITLE, winX, winY, FM_WIN_W, FM_WIN_H, true); if (!sState.win) { return -1; } sState.win->onClose = onClose; sState.win->onMenu = onMenu; buildMenus(); buildWindow(); if (ctx->args[0]) { snprintf(start, sizeof(start), "%s", ctx->args); } else if (!getcwd(start, sizeof(start))) { snprintf(start, sizeof(start), "%s", FM_CUR_DIR); } if (!navigate(start, false)) { PlatformDriveT drive; if (platformDriveList(&drive, 1) == 1) { navigate(drive.root, false); } } wgtSetFocused(sState.list); sState.treeActive = false; return 0; }