feat: filesystem and Files app support for >64 file entries, 64-bit sizes, async ops, GUI toolkit improvements to devexplorer app
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
/*
|
||||
* fileop.cpp
|
||||
* Background copy/move/delete worker + progress dialog for the embedded
|
||||
* desktop file manager. Long file operations run on a worker thread so the
|
||||
* desktop UI never freezes; a modal progress dialog shows the current item,
|
||||
* a determinate bar, and a Cancel button.
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
#include <gui/mtk/hosts.hpp>
|
||||
#include <montauk/thread.h>
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
// Inline (synchronous, no dialog) threshold for a single plain file paste.
|
||||
// Anything larger -- or any directory / multi-item operation -- runs async.
|
||||
static constexpr uint64_t FILEOP_INLINE_MAX_BYTES = 16ull * 1024 * 1024;
|
||||
|
||||
struct FileOpJob {
|
||||
int kind; // FileOpKind: copy / move / delete
|
||||
char (*paths)[256]; // owned: source paths (copy/move) or targets (delete)
|
||||
int count;
|
||||
char dest_dir[256]; // destination directory (copy/move); empty for delete
|
||||
|
||||
DesktopState* desktop;
|
||||
FileManagerState* owner; // for the post-op refresh (liveness-checked)
|
||||
Color accent;
|
||||
|
||||
volatile int files_total;
|
||||
volatile int files_done;
|
||||
volatile int cancel; // UI -> worker
|
||||
volatile int finished; // worker -> UI
|
||||
volatile char current[128]; // basename of the item in progress (worker writes)
|
||||
|
||||
int tid;
|
||||
int last_drawn_done; // dialog redraw tracking
|
||||
int mouse_x, mouse_y;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
static bool fileop_path_same_or_descendant(const char* path, const char* root) {
|
||||
if (!path || !root) return false;
|
||||
int root_len = montauk::slen(root);
|
||||
while (root_len > 0 && root[root_len - 1] == '/') root_len--;
|
||||
if (root_len <= 0) return false;
|
||||
for (int i = 0; i < root_len; i++) {
|
||||
if (path[i] == '\0' || path[i] != root[i]) return false;
|
||||
}
|
||||
return path[root_len] == '\0' || path[root_len] == '/';
|
||||
}
|
||||
|
||||
static void fileop_set_current(FileOpJob* job, const char* name) {
|
||||
if (!job || !name) return;
|
||||
int i = 0;
|
||||
for (; name[i] && i < 127; i++) job->current[i] = name[i];
|
||||
job->current[i] = '\0';
|
||||
}
|
||||
|
||||
static bool fileop_path_is_dir(const char* path) {
|
||||
const char* probe[1];
|
||||
return montauk::readdir(path, probe, 1) >= 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Worker thread
|
||||
// ============================================================================
|
||||
|
||||
static int fileop_worker(void* arg) {
|
||||
FileOpJob* job = (FileOpJob*)arg;
|
||||
|
||||
for (int i = 0; i < job->count && !job->cancel; i++) {
|
||||
const char* src = job->paths[i];
|
||||
const char* base = path_basename(src);
|
||||
fileop_set_current(job, base);
|
||||
|
||||
if (job->kind == FILE_OP_DELETE) {
|
||||
filemanager_delete_recursive(src);
|
||||
job->files_done = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy or move into dest_dir.
|
||||
char dst[512];
|
||||
filemanager_build_fullpath(dst, 512, job->dest_dir, base);
|
||||
if (montauk::streq(dst, src)) { job->files_done = i + 1; continue; }
|
||||
|
||||
bool is_dir = fileop_path_is_dir(src);
|
||||
if (is_dir && fileop_path_same_or_descendant(dst, src)) {
|
||||
job->files_done = i + 1;
|
||||
continue; // refuse to copy a directory into itself
|
||||
}
|
||||
|
||||
if (job->kind == FILE_OP_MOVE) {
|
||||
// Try a rename first: an instant same-volume move with no copying.
|
||||
if (montauk::frename(src, dst) == 0) { job->files_done = i + 1; continue; }
|
||||
// Cross-volume (or driver without rename): fall back to copy + delete.
|
||||
bool ok = is_dir ? filemanager_copy_dir_recursive(src, dst)
|
||||
: filemanager_copy_file(src, dst);
|
||||
if (ok) {
|
||||
if (is_dir) filemanager_delete_recursive(src);
|
||||
else montauk::fdelete(src);
|
||||
}
|
||||
} else { // FILE_OP_COPY
|
||||
if (is_dir) filemanager_copy_dir_recursive(src, dst);
|
||||
else filemanager_copy_file(src, dst);
|
||||
}
|
||||
job->files_done = i + 1;
|
||||
}
|
||||
|
||||
job->finished = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Progress dialog
|
||||
// ============================================================================
|
||||
|
||||
static const char* fileop_verb(int kind) {
|
||||
switch (kind) {
|
||||
case FILE_OP_COPY: return "Copying";
|
||||
case FILE_OP_MOVE: return "Moving";
|
||||
case FILE_OP_DELETE: return "Deleting";
|
||||
default: return "Working";
|
||||
}
|
||||
}
|
||||
|
||||
static void fileop_cancel_button_rect(int cw, int ch, Rect* out) {
|
||||
int btn_w = 88, btn_h = 28;
|
||||
*out = { cw - btn_w - 16, ch - btn_h - 14, btn_w, btn_h };
|
||||
}
|
||||
|
||||
static FileManagerState* fileop_live_owner(FileOpJob* job) {
|
||||
if (!job || !job->desktop || !job->owner) return nullptr;
|
||||
for (int i = 0; i < job->desktop->window_count; i++) {
|
||||
Window& win = job->desktop->windows[i];
|
||||
if (win.app_data == job->owner && win.on_draw == filemanager_on_draw)
|
||||
return job->owner;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void fileop_close_self(DesktopState* ds, FileOpJob* job) {
|
||||
if (!ds) return;
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == job) {
|
||||
desktop_close_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fileop_dialog_on_draw(Window* win, Framebuffer& fb) {
|
||||
(void)fb;
|
||||
FileOpJob* job = (FileOpJob*)win->app_data;
|
||||
if (!job) return;
|
||||
|
||||
Canvas c(win);
|
||||
mtk::Theme theme = mtk::make_theme(job->accent);
|
||||
c.fill(theme.window_bg);
|
||||
|
||||
int pad = 16;
|
||||
int fh = system_font_height();
|
||||
int y = 18;
|
||||
|
||||
// Title: "Copying 3 of 12"
|
||||
char title[96];
|
||||
int done = job->files_done;
|
||||
int total = job->files_total;
|
||||
if (total > 1)
|
||||
snprintf(title, sizeof(title), "%s %d of %d", fileop_verb(job->kind),
|
||||
done < total ? done + 1 : total, total);
|
||||
else
|
||||
snprintf(title, sizeof(title), "%s...", fileop_verb(job->kind));
|
||||
c.text(pad, y, title, theme.text);
|
||||
y += fh + 10;
|
||||
|
||||
// Current item name (copied out of the volatile buffer).
|
||||
char cur[128];
|
||||
int k = 0;
|
||||
for (; k < 127 && job->current[k]; k++) cur[k] = job->current[k];
|
||||
cur[k] = '\0';
|
||||
if (cur[0]) {
|
||||
char fit[128];
|
||||
// Trim to width using the shared helper if available; otherwise show raw.
|
||||
montauk::strncpy(fit, cur, sizeof(fit) - 1);
|
||||
fit[sizeof(fit) - 1] = '\0';
|
||||
c.text(pad, y, fit, theme.text_muted);
|
||||
}
|
||||
y += fh + 12;
|
||||
|
||||
// Progress bar.
|
||||
int bar_x = pad;
|
||||
int bar_w = c.w - pad * 2;
|
||||
int bar_h = 10;
|
||||
c.fill_rounded_rect(bar_x, y, bar_w, bar_h, 5,
|
||||
Color::from_rgb(0xE0, 0xE0, 0xE0));
|
||||
int frac_w = 0;
|
||||
if (total > 0) {
|
||||
if (done > total) done = total;
|
||||
frac_w = (int)(((int64_t)bar_w * done) / total);
|
||||
}
|
||||
if (frac_w > 0)
|
||||
c.fill_rounded_rect(bar_x, y, frac_w, bar_h, 5, job->accent);
|
||||
|
||||
// Cancel button.
|
||||
Rect cancel_btn;
|
||||
fileop_cancel_button_rect(c.w, c.h, &cancel_btn);
|
||||
bool hover = cancel_btn.contains(job->mouse_x, job->mouse_y);
|
||||
mtk::draw_button(c, cancel_btn, job->finished ? "Close" : "Cancel",
|
||||
mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, hover, true), theme);
|
||||
}
|
||||
|
||||
static void fileop_dialog_on_mouse(Window* win, MouseEvent& ev) {
|
||||
FileOpJob* job = (FileOpJob*)win->app_data;
|
||||
if (!job) return;
|
||||
|
||||
mtk::DesktopHost host(win);
|
||||
int mx = 0, my = 0;
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
|
||||
Rect cancel_btn;
|
||||
fileop_cancel_button_rect(win->content_w, win->content_h, &cancel_btn);
|
||||
bool old_hover = cancel_btn.contains(job->mouse_x, job->mouse_y);
|
||||
bool new_hover = cancel_btn.contains(mx, my);
|
||||
job->mouse_x = mx;
|
||||
job->mouse_y = my;
|
||||
if (old_hover != new_hover) host.invalidate();
|
||||
|
||||
if (ev.left_pressed() && new_hover) {
|
||||
// Request cancel; the dialog closes once the worker observes it.
|
||||
job->cancel = 1;
|
||||
if (job->finished) fileop_close_self(job->desktop, job);
|
||||
}
|
||||
}
|
||||
|
||||
static void fileop_dialog_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
FileOpJob* job = (FileOpJob*)win->app_data;
|
||||
if (!job || !key.pressed) return;
|
||||
if (key.scancode == 0x01) job->cancel = 1; // Escape requests cancel
|
||||
}
|
||||
|
||||
static void fileop_dialog_on_poll(Window* win) {
|
||||
FileOpJob* job = (FileOpJob*)win->app_data;
|
||||
if (!job) return;
|
||||
|
||||
if (job->files_done != job->last_drawn_done) {
|
||||
job->last_drawn_done = job->files_done;
|
||||
win->dirty = true;
|
||||
}
|
||||
|
||||
if (job->finished) {
|
||||
// Hand off to on_close, which joins the worker, refreshes the file
|
||||
// manager, and frees the job exactly once.
|
||||
fileop_close_self(job->desktop, job);
|
||||
}
|
||||
}
|
||||
|
||||
static void fileop_dialog_on_close(Window* win) {
|
||||
FileOpJob* job = (FileOpJob*)win->app_data;
|
||||
if (!job) return;
|
||||
|
||||
// Make sure the worker has stopped before we free anything it touches.
|
||||
if (job->tid > 0) {
|
||||
if (!job->finished) job->cancel = 1;
|
||||
montauk::thread_join(job->tid, nullptr);
|
||||
job->tid = 0;
|
||||
}
|
||||
|
||||
FileManagerState* owner = fileop_live_owner(job);
|
||||
if (owner) {
|
||||
if (owner->active_fileop == job) owner->active_fileop = nullptr;
|
||||
filemanager_clear_multi_selection(owner);
|
||||
filemanager_read_dir(owner);
|
||||
}
|
||||
|
||||
if (job->paths) montauk::mfree(job->paths);
|
||||
montauk::mfree(job);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
|
||||
static bool fileop_show_dialog(FileOpJob* job) {
|
||||
DesktopState* ds = job->desktop;
|
||||
int w = 380, h = 150;
|
||||
int wx = 0, wy = 0;
|
||||
props_center_dialog(ds, job->owner, w, h, &wx, &wy);
|
||||
|
||||
const char* dlg_title = (job->kind == FILE_OP_DELETE) ? "Deleting"
|
||||
: (job->kind == FILE_OP_MOVE) ? "Moving"
|
||||
: "Copying";
|
||||
int idx = desktop_create_window(ds, dlg_title, wx, wy, w, h);
|
||||
if (idx < 0) return false;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
win->app_data = job;
|
||||
win->on_draw = fileop_dialog_on_draw;
|
||||
win->on_mouse = fileop_dialog_on_mouse;
|
||||
win->on_key = fileop_dialog_on_key;
|
||||
win->on_close = fileop_dialog_on_close;
|
||||
win->on_poll = fileop_dialog_on_poll;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Spawn the worker + dialog. Takes ownership of `job` on success. On failure
|
||||
// (no thread / no window) runs the work synchronously and frees the job.
|
||||
static bool fileop_launch(FileOpJob* job) {
|
||||
if (job->owner) job->owner->active_fileop = job;
|
||||
|
||||
// Generous stack: copy/delete recurse with ~2.5 KiB of buffers per directory
|
||||
// level, so this comfortably handles deeply nested trees.
|
||||
int tid = montauk::thread_spawn(fileop_worker, job, 256 * 1024);
|
||||
if (tid <= 0) {
|
||||
// Degraded path: do it inline (UI blocks), then refresh.
|
||||
fileop_worker(job);
|
||||
FileManagerState* owner = fileop_live_owner(job);
|
||||
if (owner) {
|
||||
owner->active_fileop = nullptr;
|
||||
filemanager_clear_multi_selection(owner);
|
||||
filemanager_read_dir(owner);
|
||||
}
|
||||
if (job->paths) montauk::mfree(job->paths);
|
||||
montauk::mfree(job);
|
||||
return true;
|
||||
}
|
||||
job->tid = tid;
|
||||
|
||||
if (!fileop_show_dialog(job)) {
|
||||
// Window creation failed: let the worker finish, then reap + refresh
|
||||
// without a dialog.
|
||||
montauk::thread_join(job->tid, nullptr);
|
||||
FileManagerState* owner = fileop_live_owner(job);
|
||||
if (owner) {
|
||||
owner->active_fileop = nullptr;
|
||||
filemanager_clear_multi_selection(owner);
|
||||
filemanager_read_dir(owner);
|
||||
}
|
||||
if (job->paths) montauk::mfree(job->paths);
|
||||
montauk::mfree(job);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static FileOpJob* fileop_alloc_job(int kind, int count) {
|
||||
if (count <= 0) return nullptr;
|
||||
FileOpJob* job = (FileOpJob*)montauk::malloc(sizeof(FileOpJob));
|
||||
if (!job) return nullptr;
|
||||
montauk::memset(job, 0, sizeof(FileOpJob));
|
||||
job->paths = (char(*)[256])montauk::malloc((uint64_t)count * 256);
|
||||
if (!job->paths) { montauk::mfree(job); return nullptr; }
|
||||
job->kind = kind;
|
||||
job->count = count;
|
||||
job->files_total = count;
|
||||
return job;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public entry points
|
||||
// ============================================================================
|
||||
|
||||
bool filemanager_start_paste_job(FileManagerState* fm) {
|
||||
if (!fm || fm->active_fileop) return false;
|
||||
if (fm->clipboard_count <= 0) return false;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return false;
|
||||
|
||||
bool is_cut = fm->clipboard_is_cut;
|
||||
|
||||
// Fast path: a single small plain file pastes inline so quick copies stay
|
||||
// snappy (no thread, no dialog flash).
|
||||
if (fm->clipboard_count == 1 && !fileop_path_is_dir(fm->clipboard_paths[0])) {
|
||||
const char* src = fm->clipboard_paths[0];
|
||||
uint64_t sz = 0;
|
||||
int fd = montauk::open(src);
|
||||
if (fd >= 0) { sz = montauk::getsize(fd); montauk::close(fd); }
|
||||
if (sz <= FILEOP_INLINE_MAX_BYTES) {
|
||||
char dst[512];
|
||||
filemanager_build_fullpath(dst, 512, fm->current_path, path_basename(src));
|
||||
if (!montauk::streq(dst, src)) {
|
||||
if (is_cut) {
|
||||
if (montauk::frename(src, dst) != 0) {
|
||||
if (filemanager_copy_file(src, dst)) montauk::fdelete(src);
|
||||
}
|
||||
} else {
|
||||
filemanager_copy_file(src, dst);
|
||||
}
|
||||
}
|
||||
if (is_cut) fm->clipboard_count = 0;
|
||||
filemanager_read_dir(fm);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
FileOpJob* job = fileop_alloc_job(is_cut ? FILE_OP_MOVE : FILE_OP_COPY,
|
||||
fm->clipboard_count);
|
||||
if (!job) return false;
|
||||
for (int i = 0; i < fm->clipboard_count; i++) {
|
||||
montauk::strncpy(job->paths[i], fm->clipboard_paths[i], 255);
|
||||
job->paths[i][255] = '\0';
|
||||
}
|
||||
montauk::strncpy(job->dest_dir, fm->current_path, sizeof(job->dest_dir) - 1);
|
||||
job->dest_dir[sizeof(job->dest_dir) - 1] = '\0';
|
||||
job->desktop = fm->desktop;
|
||||
job->owner = fm;
|
||||
job->accent = fm->desktop ? fm->desktop->settings.accent_color : Color{};
|
||||
|
||||
// A cut is consumed when the paste starts (paths are already copied in).
|
||||
if (is_cut) fm->clipboard_count = 0;
|
||||
|
||||
return fileop_launch(job);
|
||||
}
|
||||
|
||||
bool filemanager_start_delete_job(FileManagerState* fm,
|
||||
const char* const* paths, int count) {
|
||||
if (!fm || fm->active_fileop) return false;
|
||||
if (!paths || count <= 0) return false;
|
||||
|
||||
FileOpJob* job = fileop_alloc_job(FILE_OP_DELETE, count);
|
||||
if (!job) return false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
montauk::strncpy(job->paths[i], paths[i] ? paths[i] : "", 255);
|
||||
job->paths[i][255] = '\0';
|
||||
}
|
||||
job->dest_dir[0] = '\0';
|
||||
job->desktop = fm->desktop;
|
||||
job->owner = fm;
|
||||
job->accent = fm->desktop ? fm->desktop->settings.accent_color : Color{};
|
||||
|
||||
return fileop_launch(job);
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -8,6 +8,92 @@
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user