feat: Files app - add Trash
This commit is contained in:
1 parent
e047c1e0c0
commit
07191b1286
15 files changed
+1044
-150
No files matched your search
@@ -157,6 +157,10 @@ struct DesktopState {
|
||||
SvgIcon icon_folder_new;
|
||||
SvgIcon icon_home_folder;
|
||||
SvgIcon icon_home_folder_lg;
|
||||
SvgIcon icon_trash;
|
||||
SvgIcon icon_trash_lg;
|
||||
SvgIcon icon_trash_full;
|
||||
SvgIcon icon_trash_full_lg;
|
||||
SvgIcon icon_apps;
|
||||
SvgIcon icon_apps_lg;
|
||||
SvgIcon icon_system_configuration_menu;
|
||||
|
||||
@@ -1241,6 +1241,37 @@ inline void svg_parse_css_table(const char* style_data, int style_len, SvgCssTab
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a functional color "rgb(r, g, b)": each channel is 0-255 or a
|
||||
// percentage, decimals allowed (e.g. "rgb(90.196%,90.196%,90.196%)").
|
||||
// Returns false if s is not a well-formed rgb() value.
|
||||
inline bool svg_parse_rgb_color(const char* s, Color* out) {
|
||||
if (!svg_strncmp(s, "rgb(", 4)) return false;
|
||||
s += 4;
|
||||
int channel[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
while (svg_char_is_ws(*s)) ++s;
|
||||
fixed_t value = 0;
|
||||
int consumed = svg_parse_fixed(s, &value);
|
||||
if (consumed <= 0) return false;
|
||||
s += consumed;
|
||||
while (svg_char_is_ws(*s)) ++s;
|
||||
int64_t scaled = value;
|
||||
if (*s == '%') {
|
||||
scaled = scaled * 255 / 100;
|
||||
++s;
|
||||
}
|
||||
int v = (int)((scaled + (1 << 15)) >> 16);
|
||||
channel[i] = v < 0 ? 0 : v > 255 ? 255 : v;
|
||||
while (svg_char_is_ws(*s)) ++s;
|
||||
if (i < 2) {
|
||||
if (*s != ',') return false;
|
||||
++s;
|
||||
}
|
||||
}
|
||||
*out = Color::from_rgb((uint8_t)channel[0], (uint8_t)channel[1], (uint8_t)channel[2]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resolve a fill value that might be url(#id) against gradient table
|
||||
// Returns: 1 = resolved, 0 = not a url() ref, -1 = fill="none"
|
||||
@@ -1248,6 +1279,7 @@ inline void svg_parse_css_table(const char* style_data, int style_len, SvgCssTab
|
||||
inline int svg_resolve_fill_value(const char* val, Color* out_color, const SvgGradientTable* grads) {
|
||||
if (svg_strncmp(val, "none", 4)) return -1;
|
||||
if (*val == '#') { *out_color = svg_parse_hex_color(val); return 1; }
|
||||
if (svg_parse_rgb_color(val, out_color)) return 1;
|
||||
if (grads && svg_strncmp(val, "url(#", 5)) {
|
||||
// Extract id from url(#id)
|
||||
const char* id_start = val + 5;
|
||||
|
||||
@@ -38,10 +38,13 @@ void filemanager_do_copy(FileManagerState* fm) {
|
||||
}
|
||||
|
||||
void filemanager_do_cut(FileManagerState* fm) {
|
||||
// Moving out of the Trash would orphan its restore info; use Restore.
|
||||
if (filemanager_in_trash(fm)) return;
|
||||
filemanager_load_clipboard(fm, true);
|
||||
}
|
||||
|
||||
void filemanager_do_paste(FileManagerState* fm) {
|
||||
if (filemanager_in_trash(fm)) return;
|
||||
// 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);
|
||||
@@ -53,7 +56,7 @@ void filemanager_start_rename(FileManagerState* fm) {
|
||||
// only the focus and renaming it would be a surprise.
|
||||
if (fm->multi_count > 1) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
||||
fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||
filemanager_in_trash(fm) || fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||
|
||||
fm->rename_active = true;
|
||||
fm->rename_idx = fm->selected;
|
||||
@@ -106,6 +109,7 @@ void filemanager_cancel_rename(FileManagerState* fm) {
|
||||
|
||||
void filemanager_new_folder(FileManagerState* fm) {
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
if (filemanager_in_trash(fm)) return;
|
||||
|
||||
// Find a unique name
|
||||
char name[64] = "New Folder";
|
||||
@@ -140,77 +144,179 @@ 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;
|
||||
// Selected entries other than drives, as indices. Caller frees *out.
|
||||
static int filemanager_collect_deletable(FileManagerState* fm, int** out, bool* any_dir) {
|
||||
*out = nullptr;
|
||||
if (any_dir) *any_dir = false;
|
||||
if (fm->entry_count <= 0) return 0;
|
||||
|
||||
int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
|
||||
if (!sel) return;
|
||||
if (!sel) return 0;
|
||||
int n = filemanager_collect_selection(fm, sel, fm->entry_count);
|
||||
if (n <= 0) { montauk::mfree(sel); return; }
|
||||
|
||||
// Filter out drives.
|
||||
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++) {
|
||||
int idx = sel[i];
|
||||
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
|
||||
filtered[fn++] = idx;
|
||||
if (fm->is_dir[idx]) any_dir = true;
|
||||
sel[fn++] = idx;
|
||||
if (any_dir && fm->is_dir[idx]) *any_dir = true;
|
||||
}
|
||||
montauk::mfree(sel);
|
||||
if (fn == 0) { montauk::mfree(filtered); return; }
|
||||
if (fn == 0) { montauk::mfree(sel); return 0; }
|
||||
*out = sel;
|
||||
return fn;
|
||||
}
|
||||
|
||||
// Anything with a directory or multi-item selection: confirm first.
|
||||
if (any_dir || fn > 1) {
|
||||
if (fn == 1 && any_dir) {
|
||||
filemanager_show_directory_delete_confirmation(fm, filtered[0]);
|
||||
} else {
|
||||
filemanager_show_bulk_delete_confirmation(fm, filtered, fn);
|
||||
// Up to this many items are trashed inline (each is a rename plus a small
|
||||
// info file); more go to the worker with a progress dialog.
|
||||
static constexpr int FM_TRASH_INLINE_MAX = 32;
|
||||
|
||||
static void filemanager_trash_selected(FileManagerState* fm, const int* indices, int count) {
|
||||
char (*paths)[512] = (char(*)[512])montauk::malloc((uint64_t)count * 512);
|
||||
if (!paths) return;
|
||||
for (int i = 0; i < count; i++)
|
||||
filemanager_build_fullpath(paths[i], 512, fm->current_path, fm->entry_names[indices[i]]);
|
||||
|
||||
if (count > FM_TRASH_INLINE_MAX) {
|
||||
const char** arr = (const char**)montauk::malloc((uint64_t)count * sizeof(const char*));
|
||||
bool started = false;
|
||||
if (arr) {
|
||||
for (int i = 0; i < count; i++) arr[i] = paths[i];
|
||||
started = filemanager_start_trash_job(fm, arr, count);
|
||||
montauk::mfree(arr);
|
||||
}
|
||||
montauk::mfree(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single plain file. Freeing a large file's blocks is a slow syscall
|
||||
// (the fs driver rewrites allocation bitmaps per block), so deleting it
|
||||
// inline would stall the desktop -- Files runs inside desktop.elf, so a
|
||||
// blocked main thread freezes the compositor and cursor. Hand anything
|
||||
// past the inline threshold to the background worker + progress dialog;
|
||||
// small files still delete inline so there's no dialog flash.
|
||||
int only = filtered[0];
|
||||
char fullpath[512];
|
||||
filemanager_build_fullpath(fullpath, 512,
|
||||
fm->current_path, fm->entry_names[only]);
|
||||
|
||||
if (fm->entry_sizes[only] > FILEMANAGER_DELETE_INLINE_MAX_BYTES) {
|
||||
const char* one = fullpath;
|
||||
if (filemanager_start_delete_job(fm, &one, 1)) {
|
||||
montauk::mfree(filtered);
|
||||
if (started || fm->active_fileop) {
|
||||
// Started, or another operation owns the worker (its dialog says so).
|
||||
montauk::mfree(paths);
|
||||
return;
|
||||
}
|
||||
if (fm->active_fileop) {
|
||||
// Another operation owns the worker. Deleting a big file inline
|
||||
// here would stall the desktop (the exact bug this path avoids),
|
||||
// so refuse; its progress dialog tells the user we're busy.
|
||||
montauk::mfree(filtered);
|
||||
return;
|
||||
}
|
||||
// Job allocation failed (OOM): fall through to the inline delete.
|
||||
}
|
||||
|
||||
montauk::mfree(filtered);
|
||||
montauk::StatusCode status = filemanager_status_from_result(montauk::fdelete(fullpath));
|
||||
int failed = 0;
|
||||
int first_failed = -1;
|
||||
montauk::StatusCode first_error = montauk::Success;
|
||||
for (int i = 0; i < count; i++) {
|
||||
montauk::StatusCode status = filemanager_trash_item(fm->desktop, paths[i]);
|
||||
if (status != montauk::Success && failed++ == 0) {
|
||||
first_failed = i;
|
||||
first_error = status;
|
||||
}
|
||||
}
|
||||
filemanager_clear_multi_selection(fm);
|
||||
filemanager_read_dir(fm);
|
||||
|
||||
if (status != montauk::Success) {
|
||||
if (failed > 0) {
|
||||
char heading[192];
|
||||
snprintf(heading, sizeof(heading), "Couldn't delete \"%s\"", path_basename(fullpath));
|
||||
filemanager_show_error(fm->desktop, fm, heading, fullpath, status);
|
||||
if (failed == 1)
|
||||
snprintf(heading, sizeof(heading), "Couldn't move \"%s\" to the Trash",
|
||||
path_basename(paths[first_failed]));
|
||||
else
|
||||
snprintf(heading, sizeof(heading), "Couldn't move %d of %d items to the Trash",
|
||||
failed, count);
|
||||
filemanager_show_error(fm->desktop, fm, heading, paths[first_failed], first_error);
|
||||
}
|
||||
montauk::mfree(paths);
|
||||
}
|
||||
|
||||
void filemanager_delete_selected(FileManagerState* fm, bool permanent) {
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
|
||||
bool any_dir = false;
|
||||
int* sel = nullptr;
|
||||
int n = filemanager_collect_deletable(fm, &sel, &any_dir);
|
||||
if (n <= 0) return;
|
||||
|
||||
if (!permanent && filemanager_can_trash(fm)) {
|
||||
filemanager_trash_selected(fm, sel, n);
|
||||
} else if (n == 1 && any_dir) {
|
||||
// Permanent deletes cannot be undone: always confirm.
|
||||
filemanager_show_directory_delete_confirmation(fm, sel[0]);
|
||||
} else {
|
||||
filemanager_show_bulk_delete_confirmation(fm, sel, n);
|
||||
}
|
||||
montauk::mfree(sel);
|
||||
}
|
||||
|
||||
void filemanager_restore_selected(FileManagerState* fm) {
|
||||
if (!filemanager_is_trash_view(fm)) return;
|
||||
|
||||
int* sel = nullptr;
|
||||
int n = filemanager_collect_deletable(fm, &sel, nullptr);
|
||||
if (n <= 0) return;
|
||||
|
||||
int failed = 0;
|
||||
char first_failed[512] = "";
|
||||
montauk::StatusCode first_error = montauk::Success;
|
||||
for (int i = 0; i < n; i++) {
|
||||
char trashed[512];
|
||||
filemanager_build_fullpath(trashed, sizeof(trashed), fm->current_path,
|
||||
fm->entry_names[sel[i]]);
|
||||
montauk::StatusCode status = filemanager_trash_restore(trashed, nullptr, 0);
|
||||
if (status != montauk::Success && failed++ == 0) {
|
||||
montauk::strncpy(first_failed, trashed, sizeof(first_failed));
|
||||
first_error = status;
|
||||
}
|
||||
}
|
||||
montauk::mfree(sel);
|
||||
filemanager_clear_multi_selection(fm);
|
||||
filemanager_read_dir(fm);
|
||||
|
||||
if (failed > 0) {
|
||||
// NoSuchPath here means the restore info is missing, not the item.
|
||||
bool unknown_origin = first_error == montauk::NoSuchPath;
|
||||
char heading[192];
|
||||
if (failed == 1 && unknown_origin)
|
||||
snprintf(heading, sizeof(heading), "Couldn't restore \"%s\": its original location is unknown",
|
||||
path_basename(first_failed));
|
||||
else if (failed == 1)
|
||||
snprintf(heading, sizeof(heading), "Couldn't restore \"%s\"", path_basename(first_failed));
|
||||
else
|
||||
snprintf(heading, sizeof(heading), "Couldn't restore %d of %d items", failed, n);
|
||||
filemanager_show_error(fm->desktop, fm, heading, first_failed,
|
||||
unknown_origin ? montauk::Success : first_error);
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_show_empty_trash_confirmation(FileManagerState* fm) {
|
||||
if (!fm || !fm->desktop) return;
|
||||
char files[256];
|
||||
if (!filemanager_trash_files_dir(fm->desktop, files, sizeof(files))) return;
|
||||
|
||||
// Snapshot the Trash's top-level items (paged; the Trash may be large).
|
||||
int cap = 64, count = 0;
|
||||
char (*paths)[512] = (char(*)[512])montauk::malloc((uint64_t)cap * 512);
|
||||
if (!paths) return;
|
||||
static constexpr int BATCH = 128;
|
||||
const char* names[BATCH];
|
||||
for (;;) {
|
||||
int got = montauk::readdir_at(files, names, BATCH, count);
|
||||
if (got <= 0) break;
|
||||
if (count + got > cap) {
|
||||
int new_cap = cap;
|
||||
while (new_cap < count + got) new_cap *= 2;
|
||||
if (new_cap > FM_ENTRY_HARD_CAP) new_cap = FM_ENTRY_HARD_CAP;
|
||||
auto* grown = (char(*)[512])montauk::realloc(paths, (uint64_t)new_cap * 512);
|
||||
if (!grown) break;
|
||||
paths = grown;
|
||||
cap = new_cap;
|
||||
}
|
||||
for (int b = 0; b < got && count < cap; b++) {
|
||||
char name[256];
|
||||
montauk::strncpy(name, path_basename(names[b]), sizeof(name));
|
||||
int len = montauk::slen(name);
|
||||
if (len > 0 && name[len - 1] == '/') name[len - 1] = '\0';
|
||||
filemanager_build_fullpath(paths[count++], 512, files, name);
|
||||
}
|
||||
if (count >= FM_ENTRY_HARD_CAP) break;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
const char** arr = (const char**)montauk::malloc((uint64_t)count * sizeof(const char*));
|
||||
if (arr) {
|
||||
for (int i = 0; i < count; i++) arr[i] = paths[i];
|
||||
filemanager_show_paths_delete_confirmation(fm, "Empty Trash?", arr, count);
|
||||
montauk::mfree(arr);
|
||||
}
|
||||
}
|
||||
montauk::mfree(paths);
|
||||
}
|
||||
|
||||
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx) {
|
||||
@@ -223,7 +329,20 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
|
||||
if (target_idx >= 0 && target_idx < fm->entry_count) {
|
||||
int tt = fm->entry_types[target_idx];
|
||||
if ((filemanager_is_computer_view(fm)
|
||||
if (filemanager_is_computer_view(fm) && tt == FM_ENTRY_TRASH) {
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
if (filemanager_trash_has_items(fm->desktop))
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_EMPTY_TRASH;
|
||||
} else if (filemanager_in_trash(fm)) {
|
||||
// Trash: restore (top level only), copy out, or delete for good.
|
||||
if (filemanager_is_trash_view(fm))
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_RESTORE;
|
||||
else
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_COPY;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||
} else if ((filemanager_is_computer_view(fm)
|
||||
&& (tt == FM_ENTRY_DRIVE || tt == FM_ENTRY_HOME || tt == FM_ENTRY_APPS_ROOT
|
||||
|| tt == FM_ENTRY_SPECIAL_DIR || tt == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT))
|
||||
|| (filemanager_is_virtual_view(fm) && tt == FM_ENTRY_VIRTUAL_ITEM)) {
|
||||
@@ -239,6 +358,8 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_CUT;
|
||||
if (!several)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_RENAME;
|
||||
if (filemanager_can_trash(fm))
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_TRASH;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||
}
|
||||
@@ -251,9 +372,14 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
if (filemanager_is_computer_view(fm)) {
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_REFRESH;
|
||||
} else {
|
||||
if (fm->clipboard_count > 0)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW_FOLDER;
|
||||
if (filemanager_is_trash_view(fm)) {
|
||||
if (fm->entry_count > 0)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_EMPTY_TRASH;
|
||||
} else if (!filemanager_in_trash(fm)) {
|
||||
if (fm->clipboard_count > 0)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW_FOLDER;
|
||||
}
|
||||
if (fm->entry_count > 0)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SELECT_ALL;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_NAME;
|
||||
|
||||
@@ -85,6 +85,10 @@ void ensure_filemanager_icons_loaded(DesktopState* ds) {
|
||||
ds->icon_drive_usb_lg = svg_load("0:/os/icons/drive-harddisk-usb.svg", 48, 48, defColor);
|
||||
ds->icon_home_folder = svg_load("0:/os/icons/folder-blue-home.svg", 16, 16, defColor);
|
||||
ds->icon_home_folder_lg = svg_load("0:/os/icons/folder-blue-home.svg", 48, 48, defColor);
|
||||
ds->icon_trash = svg_load("0:/os/icons/user-trash.svg", 16, 16, defColor);
|
||||
ds->icon_trash_lg = svg_load("0:/os/icons/user-trash.svg", 48, 48, defColor);
|
||||
ds->icon_trash_full = svg_load("0:/os/icons/user-trash-full.svg", 16, 16, defColor);
|
||||
ds->icon_trash_full_lg = svg_load("0:/os/icons/user-trash-full.svg", 48, 48, defColor);
|
||||
ds->icon_apps = svg_load("0:/os/icons/folder-blue-development.svg", 16, 16, defColor);
|
||||
ds->icon_apps_lg = svg_load("0:/os/icons/folder-blue-development.svg", 48, 48, defColor);
|
||||
ds->icon_system_configuration_menu =
|
||||
|
||||
@@ -78,7 +78,8 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
local_x, local_y,
|
||||
ev.buttons, ev.prev_buttons,
|
||||
cw, win->content_h,
|
||||
FM_CTX_MENU_W);
|
||||
filemanager_context_menu_width(
|
||||
items, fm->ctx_item_count));
|
||||
filemanager_store_context_state(fm, state);
|
||||
|
||||
if (action >= 0) {
|
||||
@@ -94,7 +95,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
case CTX_CUT: filemanager_do_cut(fm); break;
|
||||
case CTX_PASTE: filemanager_do_paste(fm); break;
|
||||
case CTX_RENAME: filemanager_start_rename(fm); break;
|
||||
case CTX_DELETE: filemanager_delete_selected(fm); break;
|
||||
case CTX_DELETE: filemanager_delete_selected(fm, true); break;
|
||||
case CTX_TRASH: filemanager_delete_selected(fm); break;
|
||||
case CTX_RESTORE: filemanager_restore_selected(fm); break;
|
||||
case CTX_EMPTY_TRASH: filemanager_show_empty_trash_confirmation(fm); break;
|
||||
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
||||
case CTX_PROPERTIES: filemanager_show_selection_properties(fm, target); break;
|
||||
case CTX_REFRESH: filemanager_refresh(fm); break;
|
||||
@@ -149,7 +153,8 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
else if (local_x >= 244 && local_x < 268) filemanager_do_paste(fm);
|
||||
else if (local_x >= 272 && local_x < 296) filemanager_start_rename(fm);
|
||||
else if (local_x >= 300 && local_x < 324) filemanager_new_folder(fm);
|
||||
else if (local_x >= 328 && local_x < 352) filemanager_delete_selected(fm);
|
||||
else if (local_x >= 328 && local_x < 352)
|
||||
filemanager_delete_selected(fm, fm->desktop && fm->desktop->kbd_shift_down);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -573,8 +578,8 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
if (have_focus && filemanager_is_entry_selected(fm, cur))
|
||||
filemanager_open_entry(fm, cur);
|
||||
} else if (nav == 0x53) {
|
||||
// Delete key
|
||||
filemanager_delete_selected(fm);
|
||||
// Delete key: Move to Trash; Shift+Delete deletes permanently
|
||||
filemanager_delete_selected(fm, key.shift);
|
||||
} else if (key.alt && montauk::nav_scancode(key) == 0x4B) {
|
||||
// Alt+Left: go back
|
||||
filemanager_go_back(fm);
|
||||
|
||||
@@ -25,6 +25,7 @@ enum FileManagerEntryType : int {
|
||||
FM_ENTRY_VIRTUAL_ITEM = 6,
|
||||
FM_ENTRY_SPECIAL_DIR = 7,
|
||||
FM_ENTRY_SYSTEM_CONFIGURATION_ROOT = 8,
|
||||
FM_ENTRY_TRASH = 9,
|
||||
};
|
||||
|
||||
enum FileManagerVirtualViewKind : uint8_t {
|
||||
@@ -161,7 +162,7 @@ inline constexpr int FM_GRID_CELL_H = 80;
|
||||
inline constexpr int FM_GRID_CELL_H_WRAPPED = 92;
|
||||
inline constexpr int FM_GRID_ICON = 48;
|
||||
inline constexpr int FM_GRID_PAD = 4;
|
||||
inline constexpr int FM_CTX_MENU_W = 184;
|
||||
inline constexpr int FM_CTX_MENU_MIN_W = 140;
|
||||
|
||||
// List view column layout. Columns are dropped right to left as the window
|
||||
// narrows (Modified first, then Type, then Size) so the name keeps room.
|
||||
@@ -220,6 +221,9 @@ inline constexpr int CTX_SORT_NAME = 11;
|
||||
inline constexpr int CTX_SORT_SIZE = 12;
|
||||
inline constexpr int CTX_SORT_TYPE = 13;
|
||||
inline constexpr int CTX_SORT_MODIFIED = 14;
|
||||
inline constexpr int CTX_TRASH = 15; // Move to Trash
|
||||
inline constexpr int CTX_RESTORE = 16; // Restore from Trash
|
||||
inline constexpr int CTX_EMPTY_TRASH = 17;
|
||||
|
||||
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
||||
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
||||
@@ -269,6 +273,21 @@ inline void filemanager_build_context_items(const FileManagerState* fm,
|
||||
}
|
||||
}
|
||||
|
||||
// Menu width that fits the widest label plus its shortcut. draw_context_menu
|
||||
// puts labels at x+12 and right-aligns shortcuts 12px from the edge.
|
||||
inline int filemanager_context_menu_width(const mtk::ContextMenuItem* items, int count) {
|
||||
static constexpr int EDGE = 12;
|
||||
static constexpr int GAP = 24; // between label and shortcut
|
||||
int w = FM_CTX_MENU_MIN_W;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int need = EDGE + text_width(items[i].label) + EDGE;
|
||||
if (items[i].shortcut && items[i].shortcut[0])
|
||||
need += GAP + text_width(items[i].shortcut);
|
||||
if (need > w) w = need;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
bool str_ends_with(const char* s, const char* suffix);
|
||||
int detect_file_type(const char* name, bool is_dir);
|
||||
// Lowercased extension after the last '.', without the dot ("" if none).
|
||||
@@ -278,6 +297,14 @@ void file_extension(const char* name, char* out, int out_max);
|
||||
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max);
|
||||
|
||||
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name);
|
||||
// Copy src into out, cut with "..." to fit max_w pixels.
|
||||
void filemanager_fit_text(const char* src, char* out, int out_len, int max_w);
|
||||
bool filemanager_path_exists(const char* path);
|
||||
bool filemanager_path_is_dir(const char* path);
|
||||
// First free "<dir>/<name> (N)<.ext>" path, or with copy_style the
|
||||
// "<name> (copy)" / "<name> (copy N)" form used when duplicating in place.
|
||||
void filemanager_unique_path(const char* dir, const char* name, bool is_dir,
|
||||
bool copy_style, char* out, int out_max);
|
||||
const char* path_basename(const char* path);
|
||||
|
||||
// Grow the per-entry parallel arrays to hold at least `needed` entries.
|
||||
@@ -316,6 +343,9 @@ inline bool filemanager_status_is_access_denied(montauk::StatusCode status) {
|
||||
|
||||
// These return the first failure encountered (Success when everything worked).
|
||||
montauk::StatusCode filemanager_delete_recursive(const char* path);
|
||||
// Permanent delete that also drops the Trash info file when path is a
|
||||
// top-level Trash item. Use this for user-initiated deletes.
|
||||
montauk::StatusCode filemanager_delete_item(const char* path);
|
||||
montauk::StatusCode filemanager_copy_file(const char* src, const char* dst);
|
||||
montauk::StatusCode filemanager_copy_dir_recursive(const char* src, const char* dst);
|
||||
|
||||
@@ -327,7 +357,9 @@ void filemanager_start_rename(FileManagerState* fm);
|
||||
void filemanager_finish_rename(FileManagerState* fm);
|
||||
void filemanager_cancel_rename(FileManagerState* fm);
|
||||
void filemanager_new_folder(FileManagerState* fm);
|
||||
void filemanager_delete_selected(FileManagerState* fm);
|
||||
// Delete key / toolbar: Move to Trash where possible, otherwise (or with
|
||||
// permanent set, i.e. Shift+Delete) delete permanently after confirming.
|
||||
void filemanager_delete_selected(FileManagerState* fm, bool permanent = false);
|
||||
|
||||
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx);
|
||||
void filemanager_close_ctx_menu(FileManagerState* fm);
|
||||
@@ -339,6 +371,9 @@ void filemanager_show_multi_properties(FileManagerState* fm,
|
||||
void filemanager_show_directory_delete_confirmation(FileManagerState* fm, int target_idx);
|
||||
void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
|
||||
const int* indices, int count);
|
||||
// Confirm a permanent delete of arbitrary paths (Empty Trash).
|
||||
void filemanager_show_paths_delete_confirmation(FileManagerState* fm, const char* title,
|
||||
const char* const* paths, int count);
|
||||
// Graphical error popup for a failed file operation. `heading` says what
|
||||
// failed, `path` (optional) names the item, and a non-Success `status` adds
|
||||
// montauk::GetStatusExplanationText() as the cause. `owner_app_data` is only
|
||||
@@ -362,6 +397,29 @@ void filemanager_commit_pathbar(FileManagerState* fm);
|
||||
|
||||
void filemanager_open_entry(FileManagerState* fm, int idx);
|
||||
|
||||
// ---- Trash (trash.cpp) ----
|
||||
// <home>/.trash/files holds trashed items; the view of that folder is the
|
||||
// Trash. Everything below it (trashed folders' contents) is read-only apart
|
||||
// from permanent deletion.
|
||||
bool filemanager_trash_files_dir(const DesktopState* ds, char* out, int out_max);
|
||||
bool filemanager_trash_ensure(const DesktopState* ds);
|
||||
bool filemanager_is_trash_path(const DesktopState* ds, const char* path);
|
||||
bool filemanager_is_trash_view(const FileManagerState* fm); // exactly the Trash
|
||||
bool filemanager_in_trash(const FileManagerState* fm); // Trash or below it
|
||||
bool filemanager_trash_has_items(const DesktopState* ds);
|
||||
// Delete means Move to Trash here: a real folder on the home volume, outside
|
||||
// the Trash itself.
|
||||
bool filemanager_can_trash(const FileManagerState* fm);
|
||||
montauk::StatusCode filemanager_trash_item(const DesktopState* ds, const char* path);
|
||||
// Move a top-level Trash item back to where it came from (recreating missing
|
||||
// parent folders, or picking "name (2)" if the original name is taken).
|
||||
montauk::StatusCode filemanager_trash_restore(const char* trashed, char* restored, int restored_max);
|
||||
bool filemanager_trash_original_path(const char* trashed, char* out, int out_max);
|
||||
void filemanager_trash_forget(const char* trashed);
|
||||
void filemanager_open_trash(FileManagerState* fm);
|
||||
void filemanager_restore_selected(FileManagerState* fm);
|
||||
void filemanager_show_empty_trash_confirmation(FileManagerState* fm);
|
||||
|
||||
// ---- File associations (associations.cpp, openwith.cpp) ----
|
||||
// Default app for a file: the user's "Always use" choice from
|
||||
// <home>/config/associations.toml, else the first app (by name) whose manifest
|
||||
@@ -387,6 +445,7 @@ enum FileOpKind : int {
|
||||
FILE_OP_COPY = 0,
|
||||
FILE_OP_MOVE = 1,
|
||||
FILE_OP_DELETE = 2,
|
||||
FILE_OP_TRASH = 3,
|
||||
};
|
||||
|
||||
// Start a background copy/move of clipboard contents into the current dir,
|
||||
@@ -394,6 +453,7 @@ enum FileOpKind : int {
|
||||
// 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);
|
||||
bool filemanager_start_trash_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);
|
||||
|
||||
@@ -17,6 +17,14 @@ namespace filemanager {
|
||||
// Anything larger -- or any directory / multi-item operation -- runs async.
|
||||
static constexpr uint64_t FILEOP_INLINE_MAX_BYTES = 16ull * 1024 * 1024;
|
||||
|
||||
// Answers to a name conflict. 0 means "not answered yet".
|
||||
enum FileOpChoice : int {
|
||||
FILEOP_CHOICE_NONE = 0,
|
||||
FILEOP_CHOICE_REPLACE, // files: replace; folders: merge
|
||||
FILEOP_CHOICE_SKIP,
|
||||
FILEOP_CHOICE_KEEP_BOTH, // paste as "name (2)"
|
||||
};
|
||||
|
||||
struct FileOpJob {
|
||||
int kind; // FileOpKind: copy / move / delete
|
||||
char (*paths)[256]; // owned: source paths (copy/move) or targets (delete)
|
||||
@@ -39,8 +47,21 @@ struct FileOpJob {
|
||||
montauk::StatusCode first_error;
|
||||
char first_error_path[256];
|
||||
|
||||
// Name-conflict prompt. The worker fills in the conflict_* details, then
|
||||
// publishes conflict_pending (release) and waits; the dialog answers via
|
||||
// conflict_apply_all + conflict_choice (release). Cancel also wakes it.
|
||||
volatile int conflict_pending;
|
||||
volatile int conflict_choice; // FileOpChoice
|
||||
volatile int conflict_apply_all;
|
||||
char conflict_name[128];
|
||||
bool conflict_src_dir;
|
||||
bool conflict_dst_dir;
|
||||
int remembered_choice; // worker only: an "apply to all" answer
|
||||
bool ui_apply_all; // dialog checkbox state
|
||||
|
||||
int tid;
|
||||
int last_drawn_done; // dialog redraw tracking
|
||||
int last_drawn_pending;
|
||||
int mouse_x, mouse_y;
|
||||
};
|
||||
|
||||
@@ -66,16 +87,12 @@ static void fileop_set_current(FileOpJob* job, const char* name) {
|
||||
job->current[i] = '\0';
|
||||
}
|
||||
|
||||
static bool fileop_path_is_dir(const char* path) {
|
||||
const char* probe[1];
|
||||
return montauk::readdir(path, probe, 1) >= 0;
|
||||
}
|
||||
|
||||
static const char* fileop_error_verb(int kind) {
|
||||
switch (kind) {
|
||||
case FILE_OP_COPY: return "copy";
|
||||
case FILE_OP_MOVE: return "move";
|
||||
case FILE_OP_DELETE: return "delete";
|
||||
case FILE_OP_TRASH: return "move";
|
||||
default: return "process";
|
||||
}
|
||||
}
|
||||
@@ -96,12 +113,13 @@ static void fileop_report_errors(FileOpJob* job) {
|
||||
if (!job || job->failed_count <= 0) return;
|
||||
|
||||
char heading[192];
|
||||
const char* tail = job->kind == FILE_OP_TRASH ? " to the Trash" : "";
|
||||
if (job->failed_count == 1) {
|
||||
snprintf(heading, sizeof(heading), "Couldn't %s \"%s\"",
|
||||
fileop_error_verb(job->kind), path_basename(job->first_error_path));
|
||||
snprintf(heading, sizeof(heading), "Couldn't %s \"%s\"%s",
|
||||
fileop_error_verb(job->kind), path_basename(job->first_error_path), tail);
|
||||
} else {
|
||||
snprintf(heading, sizeof(heading), "Couldn't %s %d of %d items",
|
||||
fileop_error_verb(job->kind), job->failed_count, job->count);
|
||||
snprintf(heading, sizeof(heading), "Couldn't %s %d of %d items%s",
|
||||
fileop_error_verb(job->kind), job->failed_count, job->count, tail);
|
||||
}
|
||||
filemanager_show_error(job->desktop, job->owner, heading,
|
||||
job->first_error_path, job->first_error);
|
||||
@@ -111,6 +129,36 @@ static void fileop_report_errors(FileOpJob* job) {
|
||||
// Worker thread
|
||||
// ============================================================================
|
||||
|
||||
// Ask the dialog what to do about an existing destination and wait for the
|
||||
// answer. Runs on the worker thread.
|
||||
static int fileop_resolve_conflict(FileOpJob* job, const char* name,
|
||||
bool src_dir, bool dst_dir) {
|
||||
// A file and a folder cannot replace one another.
|
||||
bool mismatch = src_dir != dst_dir;
|
||||
if (job->remembered_choice != FILEOP_CHOICE_NONE) {
|
||||
if (job->remembered_choice == FILEOP_CHOICE_REPLACE && mismatch)
|
||||
return FILEOP_CHOICE_KEEP_BOTH;
|
||||
return job->remembered_choice;
|
||||
}
|
||||
|
||||
montauk::strncpy(job->conflict_name, name, sizeof(job->conflict_name));
|
||||
job->conflict_src_dir = src_dir;
|
||||
job->conflict_dst_dir = dst_dir;
|
||||
__atomic_store_n(&job->conflict_choice, FILEOP_CHOICE_NONE, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&job->conflict_pending, 1, __ATOMIC_RELEASE);
|
||||
|
||||
int choice = FILEOP_CHOICE_NONE;
|
||||
while (!job->cancel &&
|
||||
(choice = __atomic_load_n(&job->conflict_choice, __ATOMIC_ACQUIRE)) == FILEOP_CHOICE_NONE)
|
||||
montauk::sleep_ms(10);
|
||||
__atomic_store_n(&job->conflict_pending, 0, __ATOMIC_RELEASE);
|
||||
|
||||
if (job->cancel) return FILEOP_CHOICE_SKIP;
|
||||
if (job->conflict_apply_all) job->remembered_choice = choice;
|
||||
if (choice == FILEOP_CHOICE_REPLACE && mismatch) choice = FILEOP_CHOICE_KEEP_BOTH;
|
||||
return choice;
|
||||
}
|
||||
|
||||
static int fileop_worker(void* arg) {
|
||||
FileOpJob* job = (FileOpJob*)arg;
|
||||
|
||||
@@ -120,27 +168,44 @@ static int fileop_worker(void* arg) {
|
||||
fileop_set_current(job, base);
|
||||
|
||||
if (job->kind == FILE_OP_DELETE) {
|
||||
fileop_record_status(job, src, filemanager_delete_recursive(src));
|
||||
fileop_record_status(job, src, filemanager_delete_item(src));
|
||||
job->files_done = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (job->kind == FILE_OP_TRASH) {
|
||||
fileop_record_status(job, src, filemanager_trash_item(job->desktop, 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)) {
|
||||
bool is_dir = filemanager_path_is_dir(src);
|
||||
if (is_dir && fileop_path_same_or_descendant(job->dest_dir, src)) {
|
||||
// Refuse to copy a directory into itself.
|
||||
fileop_record_status(job, src, montauk::InvalidArgument);
|
||||
job->files_done = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
char dst[512];
|
||||
filemanager_build_fullpath(dst, 512, job->dest_dir, base);
|
||||
if (montauk::streq(dst, src)) {
|
||||
// Moving into its own folder is a no-op; copying duplicates it.
|
||||
if (job->kind == FILE_OP_MOVE) { job->files_done = i + 1; continue; }
|
||||
filemanager_unique_path(job->dest_dir, base, is_dir, true, dst, sizeof(dst));
|
||||
} else if (filemanager_path_exists(dst)) {
|
||||
int choice = fileop_resolve_conflict(job, base, is_dir, filemanager_path_is_dir(dst));
|
||||
if (job->cancel || choice == FILEOP_CHOICE_SKIP) { job->files_done = i + 1; continue; }
|
||||
if (choice == FILEOP_CHOICE_KEEP_BOTH)
|
||||
filemanager_unique_path(job->dest_dir, base, is_dir, false, dst, sizeof(dst));
|
||||
// Replace: rename and copy_file swap a file in atomically, and a
|
||||
// folder copy merges into the existing one.
|
||||
}
|
||||
|
||||
montauk::StatusCode status;
|
||||
if (job->kind == FILE_OP_MOVE) {
|
||||
// Try a rename first: an instant same-volume move with no copying.
|
||||
// It fails onto a non-empty folder, which then merges below.
|
||||
status = filemanager_status_from_result(montauk::frename(src, dst));
|
||||
if (status != montauk::Success && !filemanager_status_is_access_denied(status)) {
|
||||
// Cross-volume (or driver without rename): fall back to copy + delete.
|
||||
@@ -173,6 +238,7 @@ static const char* fileop_verb(int kind) {
|
||||
case FILE_OP_COPY: return "Copying";
|
||||
case FILE_OP_MOVE: return "Moving";
|
||||
case FILE_OP_DELETE: return "Deleting";
|
||||
case FILE_OP_TRASH: return "Moving to Trash";
|
||||
default: return "Working";
|
||||
}
|
||||
}
|
||||
@@ -182,6 +248,83 @@ static void fileop_cancel_button_rect(int cw, int ch, Rect* out) {
|
||||
*out = { cw - btn_w - 16, ch - btn_h - 14, btn_w, btn_h };
|
||||
}
|
||||
|
||||
// Conflict view layout: title, explanation, optional "apply to all"
|
||||
// checkbox, then Replace / Skip / Keep Both / Cancel along the bottom.
|
||||
static constexpr int FILEOP_CONFLICT_BTN_COUNT = 4;
|
||||
|
||||
static int fileop_conflict_checkbox_y(int fh) {
|
||||
return 18 + (fh + 8) + (fh + 14);
|
||||
}
|
||||
|
||||
static void fileop_conflict_button_rects(int cw, int ch, Rect* out) {
|
||||
int btn_w = 88, btn_h = 28, gap = 8;
|
||||
int x = cw - 16 - FILEOP_CONFLICT_BTN_COUNT * btn_w - (FILEOP_CONFLICT_BTN_COUNT - 1) * gap;
|
||||
for (int i = 0; i < FILEOP_CONFLICT_BTN_COUNT; i++)
|
||||
out[i] = { x + i * (btn_w + gap), ch - btn_h - 14, btn_w, btn_h };
|
||||
}
|
||||
|
||||
static Rect fileop_conflict_checkbox_rect(int cw) {
|
||||
return { 16, fileop_conflict_checkbox_y(system_font_height()), cw - 32, 22 };
|
||||
}
|
||||
|
||||
static bool fileop_conflict_pending(FileOpJob* job) {
|
||||
return __atomic_load_n(&job->conflict_pending, __ATOMIC_ACQUIRE) != 0;
|
||||
}
|
||||
|
||||
// More items may still collide, so "apply to all" is worth offering.
|
||||
static bool fileop_conflict_offers_apply_all(FileOpJob* job) {
|
||||
return job->files_done + 1 < job->count;
|
||||
}
|
||||
|
||||
static void fileop_answer_conflict(FileOpJob* job, int choice) {
|
||||
job->conflict_apply_all = job->ui_apply_all ? 1 : 0;
|
||||
__atomic_store_n(&job->conflict_choice, choice, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
static void fileop_draw_conflict(Canvas& c, FileOpJob* job, const mtk::Theme& theme) {
|
||||
int pad = 16;
|
||||
int fh = system_font_height();
|
||||
int y = 18;
|
||||
bool src_dir = job->conflict_src_dir;
|
||||
bool dst_dir = job->conflict_dst_dir;
|
||||
bool mismatch = src_dir != dst_dir;
|
||||
|
||||
char title[192];
|
||||
snprintf(title, sizeof(title), dst_dir ? "A folder named \"%s\" already exists"
|
||||
: "\"%s\" already exists", job->conflict_name);
|
||||
char title_fit[192];
|
||||
filemanager_fit_text(title, title_fit, sizeof(title_fit), c.w - pad * 2);
|
||||
c.text(pad, y, title_fit, theme.text);
|
||||
y += fh + 8;
|
||||
|
||||
const char* sub = mismatch ? "A file and a folder can't replace each other."
|
||||
: src_dir ? "Merging replaces files that have the same name."
|
||||
: "Replace it with the one you're pasting?";
|
||||
c.text(pad, y, sub, theme.text_muted);
|
||||
|
||||
if (fileop_conflict_offers_apply_all(job)) {
|
||||
Rect cb = fileop_conflict_checkbox_rect(c.w);
|
||||
mtk::draw_checkbox(c, cb, "Do this for all conflicts", mtk::check_state(job->ui_apply_all),
|
||||
theme, true, cb.contains(job->mouse_x, job->mouse_y));
|
||||
}
|
||||
|
||||
Rect btns[FILEOP_CONFLICT_BTN_COUNT];
|
||||
fileop_conflict_button_rects(c.w, c.h, btns);
|
||||
const char* labels[FILEOP_CONFLICT_BTN_COUNT] = {
|
||||
src_dir && dst_dir ? "Merge" : "Replace", "Skip", "Keep Both", "Cancel"
|
||||
};
|
||||
mtk::ButtonVariant variants[FILEOP_CONFLICT_BTN_COUNT] = {
|
||||
mtk::BUTTON_SECONDARY, mtk::BUTTON_SECONDARY, mtk::BUTTON_PRIMARY, mtk::BUTTON_SECONDARY
|
||||
};
|
||||
for (int i = 0; i < FILEOP_CONFLICT_BTN_COUNT; i++) {
|
||||
bool enabled = !(i == 0 && mismatch);
|
||||
mtk::draw_button(c, btns[i], labels[i], variants[i],
|
||||
mtk::widget_state(false, enabled && btns[i].contains(job->mouse_x, job->mouse_y),
|
||||
enabled),
|
||||
theme);
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
@@ -211,6 +354,11 @@ static void fileop_dialog_on_draw(Window* win, Framebuffer& fb) {
|
||||
mtk::Theme theme = mtk::make_theme(job->accent);
|
||||
c.fill(theme.window_bg);
|
||||
|
||||
if (fileop_conflict_pending(job)) {
|
||||
fileop_draw_conflict(c, job, theme);
|
||||
return;
|
||||
}
|
||||
|
||||
int pad = 16;
|
||||
int fh = system_font_height();
|
||||
int y = 18;
|
||||
@@ -234,9 +382,7 @@ static void fileop_dialog_on_draw(Window* win, Framebuffer& fb) {
|
||||
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';
|
||||
filemanager_fit_text(cur, fit, sizeof(fit), c.w - pad * 2);
|
||||
c.text(pad, y, fit, theme.text_muted);
|
||||
}
|
||||
y += fh + 12;
|
||||
@@ -272,6 +418,29 @@ static void fileop_dialog_on_mouse(Window* win, MouseEvent& ev) {
|
||||
int mx = 0, my = 0;
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
|
||||
if (fileop_conflict_pending(job)) {
|
||||
if (mx != job->mouse_x || my != job->mouse_y) host.invalidate();
|
||||
job->mouse_x = mx;
|
||||
job->mouse_y = my;
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
if (fileop_conflict_offers_apply_all(job) &&
|
||||
fileop_conflict_checkbox_rect(win->content_w).contains(mx, my)) {
|
||||
job->ui_apply_all = !job->ui_apply_all;
|
||||
host.invalidate();
|
||||
return;
|
||||
}
|
||||
Rect btns[FILEOP_CONFLICT_BTN_COUNT];
|
||||
fileop_conflict_button_rects(win->content_w, win->content_h, btns);
|
||||
bool mismatch = job->conflict_src_dir != job->conflict_dst_dir;
|
||||
if (btns[0].contains(mx, my) && !mismatch) fileop_answer_conflict(job, FILEOP_CHOICE_REPLACE);
|
||||
else if (btns[1].contains(mx, my)) fileop_answer_conflict(job, FILEOP_CHOICE_SKIP);
|
||||
else if (btns[2].contains(mx, my)) fileop_answer_conflict(job, FILEOP_CHOICE_KEEP_BOTH);
|
||||
else if (btns[3].contains(mx, my)) job->cancel = 1;
|
||||
host.invalidate();
|
||||
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);
|
||||
@@ -290,7 +459,12 @@ static void fileop_dialog_on_mouse(Window* win, MouseEvent& ev) {
|
||||
static void fileop_dialog_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
FileOpJob* job = (FileOpJob*)win->app_data;
|
||||
if (!job || !key.pressed) return;
|
||||
if (key.scancode == 0x01) job->cancel = 1; // Escape requests cancel
|
||||
if (key.scancode == 0x01) {
|
||||
job->cancel = 1; // Escape requests cancel
|
||||
} else if ((key.ascii == '\n' || key.ascii == '\r') && fileop_conflict_pending(job)) {
|
||||
// Enter takes the non-destructive default.
|
||||
fileop_answer_conflict(job, FILEOP_CHOICE_KEEP_BOTH);
|
||||
}
|
||||
}
|
||||
|
||||
static void fileop_dialog_on_poll(Window* win) {
|
||||
@@ -301,6 +475,11 @@ static void fileop_dialog_on_poll(Window* win) {
|
||||
job->last_drawn_done = job->files_done;
|
||||
win->dirty = true;
|
||||
}
|
||||
int pending = fileop_conflict_pending(job) ? 1 : 0;
|
||||
if (pending != job->last_drawn_pending) {
|
||||
job->last_drawn_pending = pending;
|
||||
win->dirty = true;
|
||||
}
|
||||
|
||||
if (job->finished) {
|
||||
// Hand off to on_close, which joins the worker, refreshes the file
|
||||
@@ -347,14 +526,17 @@ static bool fileop_show_dialog(FileOpJob* job) {
|
||||
int bar_bottom = 18 + (fh + 10) + (fh + 12) + 10; // mirrors on_draw layout
|
||||
int btn_h = 28;
|
||||
int content_h = bar_bottom + 16 /*gap*/ + btn_h + 14 /*bottom margin*/;
|
||||
int w = 380;
|
||||
// Copies and moves can turn into the conflict prompt, which is taller.
|
||||
if (job->kind == FILE_OP_COPY || job->kind == FILE_OP_MOVE) {
|
||||
int conflict_h = fileop_conflict_checkbox_y(fh) + 22 + 16 + btn_h + 14;
|
||||
if (conflict_h > content_h) content_h = conflict_h;
|
||||
}
|
||||
int w = 420;
|
||||
int h = content_h + gui::TITLEBAR_HEIGHT + gui::BORDER_WIDTH;
|
||||
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";
|
||||
const char* dlg_title = fileop_verb(job->kind);
|
||||
int idx = desktop_create_window(ds, dlg_title, wx, wy, w, h);
|
||||
if (idx < 0) return false;
|
||||
|
||||
@@ -435,17 +617,21 @@ bool filemanager_start_paste_job(FileManagerState* fm) {
|
||||
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])) {
|
||||
// snappy (no thread, no dialog flash). A name conflict needs the prompt,
|
||||
// so that case goes through the job instead.
|
||||
if (fm->clipboard_count == 1 && !filemanager_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));
|
||||
char dst[512];
|
||||
filemanager_build_fullpath(dst, 512, fm->current_path, path_basename(src));
|
||||
bool same = montauk::streq(dst, src);
|
||||
if (same && !is_cut)
|
||||
filemanager_unique_path(fm->current_path, path_basename(src), false, true, dst, sizeof(dst));
|
||||
if (sz <= FILEOP_INLINE_MAX_BYTES && (same || !filemanager_path_exists(dst))) {
|
||||
montauk::StatusCode status = montauk::Success;
|
||||
if (!montauk::streq(dst, src)) {
|
||||
if (!(same && is_cut)) {
|
||||
if (is_cut) {
|
||||
status = filemanager_status_from_result(montauk::frename(src, dst));
|
||||
if (status != montauk::Success &&
|
||||
@@ -491,12 +677,12 @@ bool filemanager_start_paste_job(FileManagerState* fm) {
|
||||
return fileop_launch(job);
|
||||
}
|
||||
|
||||
bool filemanager_start_delete_job(FileManagerState* fm,
|
||||
static bool fileop_start_path_job(FileManagerState* fm, int kind,
|
||||
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);
|
||||
FileOpJob* job = fileop_alloc_job(kind, count);
|
||||
if (!job) return false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
montauk::strncpy(job->paths[i], paths[i] ? paths[i] : "", 255);
|
||||
@@ -510,4 +696,14 @@ bool filemanager_start_delete_job(FileManagerState* fm,
|
||||
return fileop_launch(job);
|
||||
}
|
||||
|
||||
bool filemanager_start_delete_job(FileManagerState* fm,
|
||||
const char* const* paths, int count) {
|
||||
return fileop_start_path_job(fm, FILE_OP_DELETE, paths, count);
|
||||
}
|
||||
|
||||
bool filemanager_start_trash_job(FileManagerState* fm,
|
||||
const char* const* paths, int count) {
|
||||
return fileop_start_path_job(fm, FILE_OP_TRASH, paths, count);
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -233,6 +233,12 @@ void filemanager_read_drives(FileManagerState* fm) {
|
||||
// Settings entry
|
||||
filemanager_add_root_entry(fm, "Settings", FM_ENTRY_SYSTEM_CONFIGURATION_ROOT, -1);
|
||||
|
||||
// Trash; drive_indices carries "has items" so the icon shows full/empty.
|
||||
if (ds && ds->home_dir[0] != '\0') {
|
||||
filemanager_add_root_entry(fm, "Trash", FM_ENTRY_TRASH,
|
||||
filemanager_trash_has_items(ds) ? 1 : 0);
|
||||
}
|
||||
|
||||
// Drive entries
|
||||
int drives[FM_MAX_DRIVES];
|
||||
int driveCount = montauk::drivelist(drives, FM_MAX_DRIVES);
|
||||
@@ -610,12 +616,35 @@ montauk::StatusCode filemanager_delete_recursive(const char* path) {
|
||||
montauk::StatusCode filemanager_copy_file(const char* src, const char* dst) {
|
||||
if (!src || !dst || montauk::streq(src, dst)) return montauk::InvalidArgument;
|
||||
|
||||
// A folder cannot be replaced by a file (rename would even delete an empty
|
||||
// one), so refuse rather than guess.
|
||||
bool replacing = filemanager_path_exists(dst);
|
||||
if (replacing && filemanager_path_is_dir(dst)) return montauk::InvalidArgument;
|
||||
|
||||
int sfd = montauk::open(src);
|
||||
if (sfd < 0) return filemanager_status_from_result(sfd);
|
||||
uint64_t size = montauk::getsize(sfd);
|
||||
|
||||
// Create destination
|
||||
int dfd = montauk::fcreate(dst);
|
||||
// fcreate() truncates an existing file, and a failed copy deletes what it
|
||||
// wrote. Writing straight onto an existing destination would therefore
|
||||
// lose the original on any error, so copy beside it and rename over it
|
||||
// only once the data is complete (rename replaces the target).
|
||||
char tmp[512];
|
||||
const char* target = dst;
|
||||
if (replacing) {
|
||||
char dir[512];
|
||||
montauk::strncpy(dir, dst, sizeof(dir));
|
||||
int cut = montauk::slen(dir);
|
||||
while (cut > 0 && dir[cut - 1] != '/') cut--;
|
||||
dir[cut > 0 ? cut - 1 : 0] = '\0';
|
||||
char tmp_name[160];
|
||||
snprintf(tmp_name, sizeof(tmp_name), ".%s.part", path_basename(dst));
|
||||
filemanager_build_fullpath(tmp, sizeof(tmp), dir, tmp_name);
|
||||
montauk::fdelete(tmp); // stale leftover from an interrupted copy
|
||||
target = tmp;
|
||||
}
|
||||
|
||||
int dfd = montauk::fcreate(target);
|
||||
if (dfd < 0) {
|
||||
montauk::close(sfd);
|
||||
return filemanager_status_from_result(dfd);
|
||||
@@ -629,7 +658,7 @@ montauk::StatusCode filemanager_copy_file(const char* src, const char* dst) {
|
||||
if (!buf) {
|
||||
montauk::close(sfd);
|
||||
montauk::close(dfd);
|
||||
montauk::fdelete(dst);
|
||||
montauk::fdelete(target);
|
||||
return montauk::OutOfMemory;
|
||||
}
|
||||
|
||||
@@ -656,7 +685,10 @@ montauk::StatusCode filemanager_copy_file(const char* src, const char* dst) {
|
||||
|
||||
montauk::close(sfd);
|
||||
montauk::close(dfd);
|
||||
if (status != montauk::Success) montauk::fdelete(dst);
|
||||
if (status == montauk::Success && replacing) {
|
||||
status = filemanager_status_from_result(montauk::frename(target, dst));
|
||||
}
|
||||
if (status != montauk::Success) montauk::fdelete(target);
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const char* ctx_label(int action) {
|
||||
case CTX_CUT: return "Cut";
|
||||
case CTX_PASTE: return "Paste";
|
||||
case CTX_RENAME: return "Rename";
|
||||
case CTX_DELETE: return "Delete";
|
||||
case CTX_DELETE: return "Delete Permanently";
|
||||
case CTX_NEW_FOLDER: return "New Folder";
|
||||
case CTX_PROPERTIES: return "Properties";
|
||||
case CTX_REFRESH: return "Refresh";
|
||||
@@ -137,6 +137,9 @@ const char* ctx_label(int action) {
|
||||
case CTX_SORT_SIZE: return "Sort by Size";
|
||||
case CTX_SORT_TYPE: return "Sort by Type";
|
||||
case CTX_SORT_MODIFIED: return "Sort by Modified";
|
||||
case CTX_TRASH: return "Move to Trash";
|
||||
case CTX_RESTORE: return "Restore";
|
||||
case CTX_EMPTY_TRASH: return "Empty Trash";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
@@ -160,7 +163,10 @@ const char* ctx_shortcut(const FileManagerState* fm, int action) {
|
||||
case CTX_CUT: return "Ctrl+X";
|
||||
case CTX_PASTE: return "Ctrl+V";
|
||||
case CTX_RENAME: return "F2";
|
||||
case CTX_DELETE: return "Del";
|
||||
case CTX_TRASH: return "Del";
|
||||
// Delete is permanent; Del alone means that only where nothing can be
|
||||
// trashed (inside the Trash, other volumes).
|
||||
case CTX_DELETE: return filemanager_can_trash(fm) ? "Shift+Del" : "Del";
|
||||
case CTX_NEW_FOLDER: return "Ctrl+Shift+N";
|
||||
case CTX_PROPERTIES: return "Alt+Enter";
|
||||
case CTX_REFRESH: return "F5";
|
||||
@@ -172,12 +178,15 @@ const char* ctx_shortcut(const FileManagerState* fm, int action) {
|
||||
int ctx_group(int action) {
|
||||
switch (action) {
|
||||
case CTX_OPEN:
|
||||
case CTX_OPEN_WITH: return 0;
|
||||
case CTX_OPEN_WITH:
|
||||
case CTX_RESTORE: return 0;
|
||||
case CTX_COPY:
|
||||
case CTX_CUT:
|
||||
case CTX_PASTE: return 1;
|
||||
case CTX_RENAME:
|
||||
case CTX_TRASH:
|
||||
case CTX_DELETE:
|
||||
case CTX_EMPTY_TRASH:
|
||||
case CTX_NEW_FOLDER: return 2;
|
||||
case CTX_SELECT_ALL: return 3;
|
||||
case CTX_SORT_NAME:
|
||||
@@ -239,6 +248,7 @@ void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int
|
||||
case FM_ENTRY_SYSTEM_CONFIGURATION_ROOT: fixed = "Settings"; break;
|
||||
case FM_ENTRY_HOME: fixed = "Home"; break;
|
||||
case FM_ENTRY_DRIVE: fixed = "Drive"; break;
|
||||
case FM_ENTRY_TRASH: fixed = "Trash"; break;
|
||||
case FM_ENTRY_SPECIAL_DIR:
|
||||
case FM_ENTRY_DIR: fixed = "Folder"; break;
|
||||
case FM_ENTRY_EXEC: fixed = "Program"; break;
|
||||
@@ -268,6 +278,85 @@ void filemanager_build_fullpath(char* out, int out_max, const char* dir, const c
|
||||
str_append(out, name, out_max);
|
||||
}
|
||||
|
||||
bool filemanager_path_exists(const char* path) {
|
||||
// open() succeeds for files and directories alike on every driver, which
|
||||
// stat() does not guarantee.
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) return false;
|
||||
montauk::close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool filemanager_path_is_dir(const char* path) {
|
||||
const char* probe[1];
|
||||
return montauk::readdir(path, probe, 1) >= 0;
|
||||
}
|
||||
|
||||
void filemanager_unique_path(const char* dir, const char* name, bool is_dir,
|
||||
bool copy_style, char* out, int out_max) {
|
||||
// Split "report.txt" into stem "report" + ext ".txt". Folders and
|
||||
// leading-dot names keep the whole name as the stem.
|
||||
int nlen = montauk::slen(name);
|
||||
int ext_at = nlen;
|
||||
if (!is_dir) {
|
||||
for (int i = nlen - 1; i > 0; i--) {
|
||||
if (name[i] == '.') { ext_at = i; break; }
|
||||
}
|
||||
}
|
||||
|
||||
for (int n = copy_style ? 1 : 2; n < 10000; n++) {
|
||||
char suffix[24];
|
||||
if (copy_style) {
|
||||
if (n == 1) snprintf(suffix, sizeof(suffix), " (copy)");
|
||||
else snprintf(suffix, sizeof(suffix), " (copy %d)", n);
|
||||
} else {
|
||||
snprintf(suffix, sizeof(suffix), " (%d)", n);
|
||||
}
|
||||
|
||||
// Entry names hold 127 bytes; shorten the stem (on a UTF-8 boundary)
|
||||
// so the suffix and extension always fit.
|
||||
int ext_len = nlen - ext_at;
|
||||
int suf_len = montauk::slen(suffix);
|
||||
int stem_len = ext_at;
|
||||
if (stem_len + suf_len + ext_len > 127) {
|
||||
int max_stem = 127 - suf_len - ext_len;
|
||||
if (max_stem < 1) max_stem = 1;
|
||||
stem_len = montauk::utf8::floor_boundary(name, ext_at, max_stem);
|
||||
}
|
||||
|
||||
char candidate[128];
|
||||
int p = 0;
|
||||
for (int i = 0; i < stem_len; i++) candidate[p++] = name[i];
|
||||
for (int i = 0; i < suf_len; i++) candidate[p++] = suffix[i];
|
||||
for (int i = ext_at; i < nlen && p < 127; i++) candidate[p++] = name[i];
|
||||
candidate[p] = '\0';
|
||||
|
||||
filemanager_build_fullpath(out, out_max, dir, candidate);
|
||||
if (!filemanager_path_exists(out)) return;
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_fit_text(const char* src, char* out, int out_len, int max_w) {
|
||||
if (!out || out_len <= 0) return;
|
||||
montauk::strncpy(out, src ? src : "", out_len);
|
||||
if (out_len <= 4 || text_width(out) <= max_w) return;
|
||||
|
||||
int ellipsis_w = text_width("...");
|
||||
int len = montauk::slen(out);
|
||||
while (len > 0) {
|
||||
out[len] = '\0';
|
||||
if (text_width(out) + ellipsis_w <= max_w) break;
|
||||
len = montauk::utf8::prev_boundary(out, len);
|
||||
}
|
||||
|
||||
if (len > out_len - 4) len = montauk::utf8::floor_boundary(out, len, out_len - 4);
|
||||
if (len < 0) len = 0;
|
||||
out[len] = '.';
|
||||
out[len + 1] = '.';
|
||||
out[len + 2] = '.';
|
||||
out[len + 3] = '\0';
|
||||
}
|
||||
|
||||
// Extract basename from a path (pointer into the path string)
|
||||
const char* path_basename(const char* path) {
|
||||
const char* last = path;
|
||||
|
||||
@@ -72,7 +72,9 @@ void filemanager_navigate(FileManagerState* fm, const char* name) {
|
||||
void filemanager_go_up(FileManagerState* fm) {
|
||||
if (filemanager_is_computer_view(fm)) return;
|
||||
|
||||
if (filemanager_is_virtual_view(fm)) {
|
||||
// The Trash lives under a hidden folder of home; its parent is Computer,
|
||||
// where its icon is.
|
||||
if (filemanager_is_virtual_view(fm) || filemanager_is_trash_view(fm)) {
|
||||
filemanager_read_drives(fm);
|
||||
filemanager_push_history(fm);
|
||||
return;
|
||||
@@ -132,6 +134,14 @@ void filemanager_go_forward(FileManagerState* fm) {
|
||||
filemanager_navigate_to_history(fm);
|
||||
}
|
||||
|
||||
void filemanager_open_trash(FileManagerState* fm) {
|
||||
if (!fm || !filemanager_trash_ensure(fm->desktop)) return;
|
||||
if (!filemanager_trash_files_dir(fm->desktop, fm->current_path, sizeof(fm->current_path)))
|
||||
return;
|
||||
filemanager_read_dir(fm);
|
||||
filemanager_push_history(fm);
|
||||
}
|
||||
|
||||
void filemanager_go_home(FileManagerState* fm) {
|
||||
filemanager_read_drives(fm);
|
||||
filemanager_push_history(fm);
|
||||
@@ -262,6 +272,11 @@ void filemanager_open_entry(FileManagerState* fm, int idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filemanager_is_computer_view(fm) && fm->entry_types[idx] == FM_ENTRY_TRASH) {
|
||||
filemanager_open_trash(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
if (filemanager_is_computer_view(fm) && fm->entry_types[idx] == FM_ENTRY_DRIVE) {
|
||||
int d = fm->drive_indices[idx];
|
||||
char dpath[8];
|
||||
|
||||
@@ -41,6 +41,8 @@ struct FileDeleteConfirmDialogState {
|
||||
|
||||
char name[128]; // display name (single item) or summary ("3 items")
|
||||
char path[512]; // primary path (used for single-item subtitle)
|
||||
char title[96]; // custom heading (Empty Trash); empty = derived
|
||||
bool is_dir; // single item: folder or file wording
|
||||
SvgIcon icon;
|
||||
|
||||
// Multi-item targets (dynamically allocated). count == 1 keeps the original
|
||||
@@ -56,26 +58,6 @@ static void props_copy(char* dst, int dst_len, const char* src) {
|
||||
dst[dst_len - 1] = '\0';
|
||||
}
|
||||
|
||||
static void props_fit_text(const char* src, char* out, int out_len, int max_w) {
|
||||
props_copy(out, out_len, src);
|
||||
if (out_len <= 4 || text_width(out) <= max_w) return;
|
||||
|
||||
int ellipsis_w = text_width("...");
|
||||
int len = montauk::slen(out);
|
||||
while (len > 0) {
|
||||
out[len] = '\0';
|
||||
if (text_width(out) + ellipsis_w <= max_w) break;
|
||||
len = montauk::utf8::prev_boundary(out, len);
|
||||
}
|
||||
|
||||
if (len > out_len - 4) len = montauk::utf8::floor_boundary(out, len, out_len - 4);
|
||||
if (len < 0) len = 0;
|
||||
out[len] = '.';
|
||||
out[len + 1] = '.';
|
||||
out[len + 2] = '.';
|
||||
out[len + 3] = '\0';
|
||||
}
|
||||
|
||||
static Rect props_close_button_rect(int content_w, int content_h) {
|
||||
return {content_w - 96, content_h - 42, 80, 28};
|
||||
}
|
||||
@@ -342,7 +324,7 @@ static int props_draw_row(Canvas& c,
|
||||
c.text(x, y, label, theme.text_muted);
|
||||
|
||||
char fit[512];
|
||||
props_fit_text(value, fit, sizeof(fit), c.w - x - label_w - 16);
|
||||
filemanager_fit_text(value, fit, sizeof(fit), c.w - x - label_w - 16);
|
||||
c.text(x + label_w, y, fit, theme.text);
|
||||
return y + fh + 8;
|
||||
}
|
||||
@@ -361,7 +343,7 @@ static void props_on_draw(Window* win, Framebuffer& fb) {
|
||||
props_draw_icon(c, st, pad, pad + 2);
|
||||
|
||||
char title[128];
|
||||
props_fit_text(st->name, title, sizeof(title), c.w - 88);
|
||||
filemanager_fit_text(st->name, title, sizeof(title), c.w - 88);
|
||||
c.text(80, pad + 4, title, theme.text);
|
||||
c.text(80, pad + 4 + fh + 4, st->type, theme.text_muted);
|
||||
|
||||
@@ -422,9 +404,16 @@ static void props_on_close(Window* win) {
|
||||
// Subtitle wording, shared between the width calculation at creation time and
|
||||
// the draw path so the two never drift out of sync.
|
||||
static const char* const kDeleteSubMulti =
|
||||
"This will delete all selected items and their contents.";
|
||||
"They will be deleted for good, folders with their contents.";
|
||||
static const char* const kDeleteSubSingle =
|
||||
"This will delete all of its contents.";
|
||||
"The folder and all of its contents will be deleted for good.";
|
||||
static const char* const kDeleteSubFile =
|
||||
"It will be deleted for good. This can't be undone.";
|
||||
|
||||
static const char* delete_confirm_subtitle(const FileDeleteConfirmDialogState* st) {
|
||||
if (st->count > 1) return kDeleteSubMulti;
|
||||
return st->is_dir ? kDeleteSubSingle : kDeleteSubFile;
|
||||
}
|
||||
|
||||
// Pick a dialog width wide enough that the subtitle (drawn at x=80 with a 16px
|
||||
// right margin) fits without being clipped. Falls back to the original 380px
|
||||
@@ -474,10 +463,19 @@ static void delete_confirm_apply(FileDeleteConfirmDialogState* st) {
|
||||
FileManagerState* owner = delete_confirm_live_owner(st);
|
||||
int n = st->count > 0 ? st->count : (st->path[0] ? 1 : 0);
|
||||
|
||||
// A single small file deletes inline, without a progress dialog flash.
|
||||
// Anything bigger frees many blocks, which can stall the desktop.
|
||||
bool inline_ok = false;
|
||||
if (n == 1 && !st->is_dir) {
|
||||
montauk::abi::FileStat stt;
|
||||
const char* p = st->count > 0 ? st->paths[0] : st->path;
|
||||
inline_ok = montauk::stat(p, &stt) == 0 && stt.size <= FILEMANAGER_DELETE_INLINE_MAX_BYTES;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (owner && n > 0 && !inline_ok) {
|
||||
const char** arr = (const char**)montauk::malloc((uint64_t)n * sizeof(const char*));
|
||||
if (arr) {
|
||||
for (int i = 0; i < n; i++)
|
||||
@@ -496,7 +494,7 @@ static void delete_confirm_apply(FileDeleteConfirmDialogState* st) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char* p = (st->count > 0) ? st->paths[i] : st->path;
|
||||
if (!p || !p[0]) continue;
|
||||
montauk::StatusCode status = filemanager_delete_recursive(p);
|
||||
montauk::StatusCode status = filemanager_delete_item(p);
|
||||
if (status == montauk::Success) {
|
||||
any = true;
|
||||
} else if (failed++ == 0) {
|
||||
@@ -546,22 +544,26 @@ static void delete_confirm_on_draw(Window* win, Framebuffer& fb) {
|
||||
}
|
||||
|
||||
char title[160];
|
||||
if (st->count > 1)
|
||||
snprintf(title, sizeof(title), "Delete %d items?", st->count);
|
||||
if (st->title[0])
|
||||
snprintf(title, sizeof(title), "%s", st->title);
|
||||
else if (st->count > 1)
|
||||
snprintf(title, sizeof(title), "Permanently delete %d items?", st->count);
|
||||
else if (st->is_dir)
|
||||
snprintf(title, sizeof(title), "Permanently delete folder \"%s\"?", st->name);
|
||||
else
|
||||
snprintf(title, sizeof(title), "Delete folder \"%s\"?", st->name);
|
||||
snprintf(title, sizeof(title), "Permanently delete \"%s\"?", st->name);
|
||||
char title_fit[160];
|
||||
props_fit_text(title, title_fit, sizeof(title_fit), c.w - 88);
|
||||
filemanager_fit_text(title, title_fit, sizeof(title_fit), c.w - 88);
|
||||
c.text(80, 22, title_fit, theme.text);
|
||||
|
||||
const char* sub = (st->count > 1) ? kDeleteSubMulti : kDeleteSubSingle;
|
||||
const char* sub = delete_confirm_subtitle(st);
|
||||
char sub_fit[160];
|
||||
props_fit_text(sub, sub_fit, sizeof(sub_fit), c.w - 96);
|
||||
filemanager_fit_text(sub, sub_fit, sizeof(sub_fit), c.w - 96);
|
||||
c.text(80, 22 + fh + 8, sub_fit, theme.text_muted);
|
||||
|
||||
if (st->count <= 1) {
|
||||
char path_fit[512];
|
||||
props_fit_text(st->path, path_fit, sizeof(path_fit), c.w - 96);
|
||||
filemanager_fit_text(st->path, path_fit, sizeof(path_fit), c.w - 96);
|
||||
c.text(80, 22 + fh * 2 + 16, path_fit, theme.text_muted);
|
||||
}
|
||||
|
||||
@@ -659,6 +661,7 @@ void filemanager_show_directory_delete_confirmation(FileManagerState* fm, int ta
|
||||
props_copy(st->name, sizeof(st->name), fm->entry_names[target_idx]);
|
||||
filemanager_build_fullpath(st->path, sizeof(st->path),
|
||||
fm->current_path, fm->entry_names[target_idx]);
|
||||
st->is_dir = true;
|
||||
|
||||
int sfi = -1;
|
||||
if (fm->entry_types[target_idx] == FM_ENTRY_SPECIAL_DIR)
|
||||
@@ -745,20 +748,82 @@ void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
|
||||
int idx = indices[0];
|
||||
props_copy(st->name, sizeof(st->name), fm->entry_names[idx]);
|
||||
montauk::strncpy(st->path, st->paths[0], sizeof(st->path) - 1);
|
||||
st->is_dir = fm->is_dir[idx];
|
||||
} else {
|
||||
snprintf(st->name, sizeof(st->name), "%d items", out);
|
||||
}
|
||||
|
||||
if (ds->icon_folder_lg.pixels) st->icon = ds->icon_folder_lg;
|
||||
const SvgIcon& icon = (out == 1 && !st->is_dir) ? ds->icon_file_lg : ds->icon_folder_lg;
|
||||
if (icon.pixels) st->icon = icon;
|
||||
|
||||
int w = delete_confirm_dialog_width(out > 1 ? kDeleteSubMulti : kDeleteSubSingle);
|
||||
int w = delete_confirm_dialog_width(delete_confirm_subtitle(st));
|
||||
int h = 180;
|
||||
int wx = 0;
|
||||
int wy = 0;
|
||||
props_center_dialog(ds, fm, w, h, &wx, &wy);
|
||||
|
||||
int idx = desktop_create_window(ds, "Delete Items", wx, wy, w, h);
|
||||
const char* win_title = out > 1 ? "Delete Items" : st->is_dir ? "Delete Folder" : "Delete File";
|
||||
int idx = desktop_create_window(ds, win_title, wx, wy, w, h);
|
||||
if (idx < 0) {
|
||||
montauk::mfree(st->paths);
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
fm->delete_confirm_dialog = st;
|
||||
win->app_data = st;
|
||||
win->on_draw = delete_confirm_on_draw;
|
||||
win->on_mouse = delete_confirm_on_mouse;
|
||||
win->on_key = delete_confirm_on_key;
|
||||
win->on_close = delete_confirm_on_close;
|
||||
}
|
||||
|
||||
void filemanager_show_paths_delete_confirmation(FileManagerState* fm, const char* title,
|
||||
const char* const* paths, int count) {
|
||||
if (!fm || !fm->desktop || !paths || count <= 0) return;
|
||||
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (fm->delete_confirm_dialog) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == fm->delete_confirm_dialog) {
|
||||
desktop_raise_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fm->delete_confirm_dialog = nullptr;
|
||||
}
|
||||
|
||||
FileDeleteConfirmDialogState* st =
|
||||
(FileDeleteConfirmDialogState*)montauk::malloc(sizeof(FileDeleteConfirmDialogState));
|
||||
if (!st) return;
|
||||
montauk::memset(st, 0, sizeof(FileDeleteConfirmDialogState));
|
||||
st->desktop = ds;
|
||||
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;
|
||||
}
|
||||
for (int i = 0; i < count; i++) montauk::strncpy(st->paths[i], paths[i], 512);
|
||||
st->count = count;
|
||||
props_copy(st->title, sizeof(st->title), title);
|
||||
props_copy(st->name, sizeof(st->name), path_basename(paths[0]));
|
||||
montauk::strncpy(st->path, paths[0], sizeof(st->path));
|
||||
// One item: describe it as a folder or a file.
|
||||
st->is_dir = count == 1 && filemanager_path_is_dir(paths[0]);
|
||||
if (ds->icon_trash_full_lg.pixels) st->icon = ds->icon_trash_full_lg;
|
||||
|
||||
int w = delete_confirm_dialog_width(delete_confirm_subtitle(st));
|
||||
int h = 180;
|
||||
int wx = 0;
|
||||
int wy = 0;
|
||||
props_center_dialog(ds, fm, w, h, &wx, &wy);
|
||||
|
||||
int idx = desktop_create_window(ds, title, wx, wy, w, h);
|
||||
if (idx < 0) {
|
||||
montauk::mfree(st->paths);
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
@@ -802,7 +867,7 @@ static int error_wrap_text(const char* src, char (*lines)[160], int max_lines, i
|
||||
if (!src[pos]) break;
|
||||
|
||||
if (count == max_lines - 1) {
|
||||
props_fit_text(src + pos, lines[count++], 160, max_w);
|
||||
filemanager_fit_text(src + pos, lines[count++], 160, max_w);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -830,7 +895,7 @@ static int error_wrap_text(const char* src, char (*lines)[160], int max_lines, i
|
||||
if (n > 159) n = 159;
|
||||
montauk::memcpy(buf, src + pos, n);
|
||||
buf[n] = '\0';
|
||||
props_fit_text(buf, lines[count++], 160, max_w);
|
||||
filemanager_fit_text(buf, lines[count++], 160, max_w);
|
||||
pos = word_end;
|
||||
} else {
|
||||
int n = best - pos;
|
||||
@@ -877,11 +942,11 @@ static void error_on_draw(Window* win, Framebuffer& fb) {
|
||||
}
|
||||
|
||||
char fit[192];
|
||||
props_fit_text(st->heading, fit, sizeof(fit), text_w);
|
||||
filemanager_fit_text(st->heading, fit, sizeof(fit), text_w);
|
||||
c.text(kErrorTextX, kErrorTop, fit, theme.text);
|
||||
|
||||
if (st->path[0]) {
|
||||
props_fit_text(st->path, fit, sizeof(fit), text_w);
|
||||
filemanager_fit_text(st->path, fit, sizeof(fit), text_w);
|
||||
c.text(kErrorTextX, error_path_y(fh), fit, theme.text_muted);
|
||||
}
|
||||
|
||||
|
||||
@@ -257,21 +257,23 @@ void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_colo
|
||||
bool multi_sel = fm->multi_count > 0
|
||||
&& !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
|
||||
bool has_sel = single_sel || multi_sel;
|
||||
// The Trash is read-only apart from Copy, Restore and permanent delete.
|
||||
bool in_trash = filemanager_in_trash(fm);
|
||||
// Rename is single-target only.
|
||||
bool has_rename = single_sel && !multi_sel;
|
||||
bool has_rename = single_sel && !multi_sel && !in_trash;
|
||||
bool has_clip = fm->clipboard_count > 0 && !filemanager_is_computer_view(fm)
|
||||
&& !filemanager_is_virtual_view(fm);
|
||||
&& !filemanager_is_virtual_view(fm) && !in_trash;
|
||||
Color dim_bg = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||
|
||||
// Icon toolbar buttons: Copy, Cut, Paste, Rename, New Folder, Delete
|
||||
struct ActionBtn { int x; SvgIcon* icon; bool enabled; };
|
||||
ActionBtn action_btns[] = {
|
||||
{ 188, ds ? &ds->icon_copy : nullptr, has_sel },
|
||||
{ 216, ds ? &ds->icon_cut : nullptr, has_sel },
|
||||
{ 216, ds ? &ds->icon_cut : nullptr, has_sel && !in_trash },
|
||||
{ 244, ds ? &ds->icon_paste : nullptr, has_clip },
|
||||
{ 272, ds ? &ds->icon_rename : nullptr, has_rename },
|
||||
{ 300, ds ? &ds->icon_folder_new : nullptr,
|
||||
!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm) },
|
||||
!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm) && !in_trash },
|
||||
{ 328, ds ? &ds->icon_delete : nullptr, has_sel },
|
||||
};
|
||||
|
||||
@@ -309,8 +311,17 @@ void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_colo
|
||||
} else {
|
||||
c.fill_rect(0, pathbar_y, c.w, FM_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
|
||||
const char* pathbar_text = fm->current_path;
|
||||
char trash_label[256];
|
||||
if (filemanager_is_computer_view(fm)) pathbar_text = "Computer";
|
||||
else if (filemanager_is_virtual_view(fm)) pathbar_text = filemanager_virtual_view_label(fm->virtual_view);
|
||||
else if (in_trash) {
|
||||
// "Trash", or "Trash/<folder>" inside a trashed folder.
|
||||
char files[256];
|
||||
filemanager_trash_files_dir(ds, files, sizeof(files));
|
||||
snprintf(trash_label, sizeof(trash_label), "Trash%s",
|
||||
fm->current_path + montauk::slen(files));
|
||||
pathbar_text = trash_label;
|
||||
}
|
||||
c.text(8, pathbar_y + 4, pathbar_text, colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
@@ -387,6 +398,9 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
c.icon(icon_x, icon_y, ds->icon_apps_lg);
|
||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_HOME && ds->icon_home_folder_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, ds->icon_home_folder_lg);
|
||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_TRASH && ds->icon_trash_lg.pixels) {
|
||||
c.icon(icon_x, icon_y, (fm->drive_indices[i] && ds->icon_trash_full_lg.pixels)
|
||||
? ds->icon_trash_full_lg : ds->icon_trash_lg);
|
||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_DRIVE && ds->icon_drive_lg.pixels) {
|
||||
// Kind 4 = USB MSC (kernel BlockDeviceKind enum)
|
||||
const SvgIcon& di = (fm->drive_kinds[i] == 4 && ds->icon_drive_usb_lg.pixels)
|
||||
@@ -535,6 +549,9 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
c.icon(ico_x, ico_y, ds->icon_apps);
|
||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_HOME && ds->icon_home_folder.pixels) {
|
||||
c.icon(ico_x, ico_y, ds->icon_home_folder);
|
||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_TRASH && ds->icon_trash.pixels) {
|
||||
c.icon(ico_x, ico_y, (fm->drive_indices[i] && ds->icon_trash_full.pixels)
|
||||
? ds->icon_trash_full : ds->icon_trash);
|
||||
} else if (ds && fm->entry_types[i] == FM_ENTRY_DRIVE && ds->icon_drive.pixels) {
|
||||
const SvgIcon& di = (fm->drive_kinds[i] == 4 && ds->icon_drive_usb.pixels)
|
||||
? ds->icon_drive_usb : ds->icon_drive;
|
||||
@@ -663,7 +680,8 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
||||
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
||||
mtk::draw_context_menu(c, filemanager_context_state(fm), items,
|
||||
fm->ctx_item_count, theme, FM_CTX_MENU_W);
|
||||
fm->ctx_item_count, theme,
|
||||
filemanager_context_menu_width(items, fm->ctx_item_count));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* trash.cpp
|
||||
* Per-user Trash for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
#include <montauk/config.h>
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
// Layout, modelled on the freedesktop.org trash:
|
||||
// <home>/.trash/files/<name> the trashed item itself
|
||||
// <home>/.trash/info/<name>.trashinfo TOML: [trash] path = "<original>"
|
||||
// Items are moved in with rename(), so only items on the home volume can be
|
||||
// trashed; anything else is deleted permanently after a confirmation.
|
||||
|
||||
static constexpr const char* TRASH_INFO_SUFFIX = ".trashinfo";
|
||||
|
||||
static bool trash_root(const DesktopState* ds, char* out, int out_max) {
|
||||
if (!ds || !ds->home_dir[0]) return false;
|
||||
filemanager_build_fullpath(out, out_max, ds->home_dir, ".trash");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool filemanager_trash_files_dir(const DesktopState* ds, char* out, int out_max) {
|
||||
char root[256];
|
||||
if (!trash_root(ds, root, sizeof(root))) return false;
|
||||
filemanager_build_fullpath(out, out_max, root, "files");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool trash_info_dir(const DesktopState* ds, char* out, int out_max) {
|
||||
char root[256];
|
||||
if (!trash_root(ds, root, sizeof(root))) return false;
|
||||
filemanager_build_fullpath(out, out_max, root, "info");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool filemanager_trash_ensure(const DesktopState* ds) {
|
||||
char path[256];
|
||||
if (!trash_root(ds, path, sizeof(path))) return false;
|
||||
montauk::fmkdir(path);
|
||||
if (!filemanager_trash_files_dir(ds, path, sizeof(path))) return false;
|
||||
montauk::fmkdir(path);
|
||||
if (!trash_info_dir(ds, path, sizeof(path))) return false;
|
||||
montauk::fmkdir(path);
|
||||
return filemanager_path_is_dir(path);
|
||||
}
|
||||
|
||||
// Same leading-path test as fileop's, without trailing-slash surprises.
|
||||
static bool trash_path_within(const char* path, const char* root) {
|
||||
int n = montauk::slen(root);
|
||||
while (n > 0 && root[n - 1] == '/') n--;
|
||||
if (n <= 0) return false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (path[i] != root[i]) return false;
|
||||
}
|
||||
return path[n] == '\0' || path[n] == '/';
|
||||
}
|
||||
|
||||
bool filemanager_is_trash_path(const DesktopState* ds, const char* path) {
|
||||
char files[256];
|
||||
return path && filemanager_trash_files_dir(ds, files, sizeof(files))
|
||||
&& trash_path_within(path, files);
|
||||
}
|
||||
|
||||
bool filemanager_is_trash_view(const FileManagerState* fm) {
|
||||
char files[256];
|
||||
if (!fm || filemanager_is_virtual_view(fm) || filemanager_is_computer_view(fm)) return false;
|
||||
if (!filemanager_trash_files_dir(fm->desktop, files, sizeof(files))) return false;
|
||||
int n = montauk::slen(files);
|
||||
int m = montauk::slen(fm->current_path);
|
||||
while (m > n && fm->current_path[m - 1] == '/') m--;
|
||||
if (m != n) return false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (fm->current_path[i] != files[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool filemanager_in_trash(const FileManagerState* fm) {
|
||||
return fm && !filemanager_is_virtual_view(fm) && !filemanager_is_computer_view(fm)
|
||||
&& filemanager_is_trash_path(fm->desktop, fm->current_path);
|
||||
}
|
||||
|
||||
bool filemanager_trash_has_items(const DesktopState* ds) {
|
||||
char files[256];
|
||||
if (!filemanager_trash_files_dir(ds, files, sizeof(files))) return false;
|
||||
const char* names[1];
|
||||
return montauk::readdir_at(files, names, 1, 0) > 0;
|
||||
}
|
||||
|
||||
// "N:" prefix comparison: rename() only works within one volume.
|
||||
static bool trash_same_volume(const char* a, const char* b) {
|
||||
for (int i = 0; a[i] && b[i]; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
if (a[i] == ':') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool filemanager_can_trash(const FileManagerState* fm) {
|
||||
if (!fm || !fm->desktop || !fm->desktop->home_dir[0]) return false;
|
||||
if (filemanager_is_virtual_view(fm) || filemanager_is_computer_view(fm)) return false;
|
||||
if (filemanager_in_trash(fm)) return false;
|
||||
return trash_same_volume(fm->current_path, fm->desktop->home_dir);
|
||||
}
|
||||
|
||||
static void trash_info_path_for(const char* info_dir, const char* trashed_name,
|
||||
char* out, int out_max) {
|
||||
char name[160];
|
||||
snprintf(name, sizeof(name), "%s%s", trashed_name, TRASH_INFO_SUFFIX);
|
||||
filemanager_build_fullpath(out, out_max, info_dir, name);
|
||||
}
|
||||
|
||||
// Info file of an item directly inside a .../.trash/files directory; false
|
||||
// for anything else (including entries nested deeper in a trashed folder).
|
||||
static bool trash_info_path_of(const char* trashed, char* out, int out_max) {
|
||||
int len = montauk::slen(trashed);
|
||||
int slash = len - 1;
|
||||
while (slash > 0 && trashed[slash] != '/') slash--;
|
||||
if (slash <= 0) return false;
|
||||
|
||||
static constexpr const char* FILES_TAIL = "/.trash/files";
|
||||
int tail_len = montauk::slen(FILES_TAIL);
|
||||
if (slash < tail_len) return false;
|
||||
for (int i = 0; i < tail_len; i++) {
|
||||
if (trashed[slash - tail_len + i] != FILES_TAIL[i]) return false;
|
||||
}
|
||||
|
||||
char info_dir[512];
|
||||
int root_len = slash - tail_len + (int)montauk::slen("/.trash");
|
||||
if (root_len >= (int)sizeof(info_dir) - 6) return false;
|
||||
for (int i = 0; i < root_len; i++) info_dir[i] = trashed[i];
|
||||
info_dir[root_len] = '\0';
|
||||
str_append(info_dir, "/info", sizeof(info_dir));
|
||||
|
||||
trash_info_path_for(info_dir, trashed + slash + 1, out, out_max);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool filemanager_trash_original_path(const char* trashed, char* out, int out_max) {
|
||||
char info[512];
|
||||
if (!out || out_max <= 0 || !trash_info_path_of(trashed, info, sizeof(info))) return false;
|
||||
auto doc = montauk::config::load_path(info);
|
||||
const char* orig = doc.get_string("trash.path", "");
|
||||
bool ok = orig[0] != '\0';
|
||||
if (ok) montauk::strncpy(out, orig, out_max);
|
||||
doc.destroy();
|
||||
return ok;
|
||||
}
|
||||
|
||||
void filemanager_trash_forget(const char* trashed) {
|
||||
char info[512];
|
||||
if (trash_info_path_of(trashed, info, sizeof(info))) montauk::fdelete(info);
|
||||
}
|
||||
|
||||
montauk::StatusCode filemanager_trash_item(const DesktopState* ds, const char* path) {
|
||||
char files[256], info_dir[256];
|
||||
if (!path || !path[0] || !filemanager_trash_files_dir(ds, files, sizeof(files)) ||
|
||||
!trash_info_dir(ds, info_dir, sizeof(info_dir)))
|
||||
return montauk::InvalidArgument;
|
||||
if (!trash_same_volume(path, files)) return montauk::UnsupportedOp;
|
||||
// Trashing the trash (or the home folder holding it) would move it into
|
||||
// itself.
|
||||
if (trash_path_within(files, path)) return montauk::InvalidArgument;
|
||||
if (!filemanager_trash_ensure(ds)) return montauk::IoFailure;
|
||||
|
||||
const char* base = path_basename(path);
|
||||
char dest[512];
|
||||
filemanager_build_fullpath(dest, sizeof(dest), files, base);
|
||||
if (filemanager_path_exists(dest))
|
||||
filemanager_unique_path(files, base, filemanager_path_is_dir(path), false,
|
||||
dest, sizeof(dest));
|
||||
|
||||
// Write the info file first: an item in files/ without one could not be
|
||||
// restored. An orphaned info file under the same name is simply replaced.
|
||||
char info[512];
|
||||
trash_info_path_for(info_dir, path_basename(dest), info, sizeof(info));
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
montauk::config::set_string(&doc, "trash.path", path);
|
||||
int rc = montauk::config::save_path(info, &doc);
|
||||
doc.destroy();
|
||||
if (rc < 0) return montauk::IoFailure;
|
||||
|
||||
montauk::StatusCode status = filemanager_status_from_result(montauk::frename(path, dest));
|
||||
if (status != montauk::Success) montauk::fdelete(info);
|
||||
return status;
|
||||
}
|
||||
|
||||
// mkdir -p for the parent of path, so a restore can recreate a deleted
|
||||
// original folder.
|
||||
static void trash_make_parents(const char* path) {
|
||||
char buf[512];
|
||||
montauk::strncpy(buf, path, sizeof(buf));
|
||||
int len = montauk::slen(buf);
|
||||
int start = 0;
|
||||
for (int i = 0; i + 1 < len; i++) {
|
||||
if (buf[i] == ':' && buf[i + 1] == '/') { start = i + 2; break; }
|
||||
}
|
||||
int last = len - 1;
|
||||
while (last > 0 && buf[last] != '/') last--;
|
||||
for (int i = start; i <= last; i++) {
|
||||
if (buf[i] != '/') continue;
|
||||
buf[i] = '\0';
|
||||
if (i > start) montauk::fmkdir(buf);
|
||||
buf[i] = '/';
|
||||
}
|
||||
}
|
||||
|
||||
montauk::StatusCode filemanager_trash_restore(const char* trashed, char* restored, int restored_max) {
|
||||
char orig[512];
|
||||
if (!filemanager_trash_original_path(trashed, orig, sizeof(orig))) return montauk::NoSuchPath;
|
||||
|
||||
trash_make_parents(orig);
|
||||
|
||||
// Something new took the original name meanwhile: restore beside it.
|
||||
char target[512];
|
||||
montauk::strncpy(target, orig, sizeof(target));
|
||||
if (filemanager_path_exists(target)) {
|
||||
char dir[512];
|
||||
montauk::strncpy(dir, orig, sizeof(dir));
|
||||
int cut = montauk::slen(dir);
|
||||
while (cut > 0 && dir[cut - 1] != '/') cut--;
|
||||
dir[cut > 0 ? cut - 1 : 0] = '\0';
|
||||
filemanager_unique_path(dir, path_basename(orig), filemanager_path_is_dir(trashed),
|
||||
false, target, sizeof(target));
|
||||
}
|
||||
|
||||
montauk::StatusCode status = filemanager_status_from_result(montauk::frename(trashed, target));
|
||||
if (status == montauk::Success) {
|
||||
filemanager_trash_forget(trashed);
|
||||
if (restored && restored_max > 0) montauk::strncpy(restored, target, restored_max);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
montauk::StatusCode filemanager_delete_item(const char* path) {
|
||||
montauk::StatusCode status = filemanager_delete_recursive(path);
|
||||
if (status == montauk::Success) filemanager_trash_forget(path);
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
Reference in new issue
Block a user