feat: expand Files features; add file associations

This commit is contained in:
daniel committed 2026-10-24 07:26:50 +02:00
1 parent 23cc56b933
commit 61d3b368f3
20 files changed
+781 -240

No files matched your search

+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 4
#define MONTAUK_BUILD_NUMBER 5
+7
View File
@@ -46,6 +46,9 @@ struct DesktopItem {
bool menu_visible;
bool launch_with_home;
bool admin_only;
// File extensions the app opens (manifest files.types): lowercase, no dot,
// space-separated with a leading and trailing space, e.g. " jpg jpeg ".
char file_types[160];
};
enum LauncherItemKind : uint8_t {
@@ -121,6 +124,10 @@ struct DesktopState {
int launcher_scroll;
bool super_left_down;
bool super_right_down;
// Live modifier state, tracked from every key event (modifier presses and
// releases are delivered too) so mouse handlers can see Ctrl/Shift.
bool kbd_shift_down;
bool kbd_ctrl_down;
SvgIcon icon_terminal;
SvgIcon icon_filemanager;
@@ -49,6 +49,9 @@ void filemanager_do_paste(FileManagerState* fm) {
void filemanager_start_rename(FileManagerState* fm) {
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
// Rename is single-target; with several entries selected, `selected` is
// 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;
@@ -228,9 +231,13 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
} else {
bool several = fm->multi_count > 1;
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
if (!several && !fm->is_dir[target_idx])
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN_WITH;
fm->ctx_items[fm->ctx_item_count++] = CTX_COPY;
fm->ctx_items[fm->ctx_item_count++] = CTX_CUT;
if (!several)
fm->ctx_items[fm->ctx_item_count++] = CTX_RENAME;
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
@@ -247,6 +254,13 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
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;
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_SIZE;
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_TYPE;
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_MODIFIED;
fm->ctx_items[fm->ctx_item_count++] = CTX_REFRESH;
}
}
}
@@ -37,6 +37,7 @@ static void open_filemanager_internal(DesktopState* ds, const char* initial_path
FileManagerState* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState));
montauk::memset(fm, 0, sizeof(FileManagerState));
fm->selected = -1;
fm->sel_anchor = -1;
fm->last_click_item = -1;
fm->history_pos = -1;
fm->history_count = 0;
+215 -106
View File
@@ -8,6 +8,51 @@
namespace filemanager {
static void filemanager_show_selection_properties(FileManagerState* fm, int target) {
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);
}
}
static bool filemanager_is_listing(const FileManagerState* fm) {
return !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
}
// Left click on an entry. Ctrl toggles it, Shift selects the range from the
// anchor (Ctrl+Shift adds that range); a plain click selects it alone, and a
// second plain click within 400 ms opens it.
static void filemanager_click_entry(FileManagerState* fm, int idx) {
DesktopState* ds = fm->desktop;
bool ctrl = ds && ds->kbd_ctrl_down;
bool shift = ds && ds->kbd_shift_down;
if (ctrl || shift) {
if (shift) filemanager_select_range(fm, idx, ctrl);
else filemanager_toggle_selection(fm, idx);
fm->last_click_item = -1;
fm->last_click_time = 0;
return;
}
uint64_t now = montauk::get_milliseconds();
if (fm->last_click_item == idx && (now - fm->last_click_time) < 400) {
filemanager_open_entry(fm, idx);
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
filemanager_select_single(fm, idx);
fm->last_click_item = idx;
fm->last_click_time = now;
}
}
void filemanager_on_mouse(Window* win, MouseEvent& ev) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm) return;
@@ -26,13 +71,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
// ---- Context menu interaction ----
if (fm->ctx_open) {
mtk::ContextMenuState state = filemanager_context_state(fm);
mtk::ContextMenuItem items[8];
filemanager_build_context_items(fm, items, 8);
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
int action = mtk::context_menu_handle_mouse(state, items,
fm->ctx_item_count,
local_x, local_y,
ev.buttons, ev.prev_buttons,
cw, win->content_h);
cw, win->content_h,
FM_CTX_MENU_W);
filemanager_store_context_state(fm, state);
if (action >= 0) {
@@ -50,20 +96,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
case CTX_RENAME: filemanager_start_rename(fm); break;
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 && 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);
}
break;
}
case CTX_PROPERTIES: filemanager_show_selection_properties(fm, target); break;
case CTX_REFRESH: filemanager_refresh(fm); break;
case CTX_OPEN_WITH: filemanager_show_open_with(fm, target); break;
case CTX_SELECT_ALL: filemanager_select_all(fm); break;
case CTX_SORT_NAME: filemanager_set_sort(fm, FM_SORT_NAME); break;
case CTX_SORT_SIZE: filemanager_set_sort(fm, FM_SORT_SIZE); break;
case CTX_SORT_TYPE: filemanager_set_sort(fm, FM_SORT_TYPE); break;
case CTX_SORT_MODIFIED: filemanager_set_sort(fm, FM_SORT_MODIFIED); break;
}
return;
}
@@ -123,23 +163,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
int clicked_idx = filemanager_grid_index_at(fm, cols, local_x, rel_y);
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
uint64_t now = montauk::get_milliseconds();
filemanager_clear_multi_selection(fm);
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
filemanager_open_entry(fm, clicked_idx);
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
fm->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
filemanager_click_entry(fm, clicked_idx);
} else {
// Background click in grid area: start marquee drag
fm->selected = -1;
filemanager_clear_multi_selection(fm);
filemanager_select_single(fm, -1);
fm->marquee_active = true;
fm->marquee_start_x = local_x;
fm->marquee_start_y = rel_y;
@@ -149,30 +176,30 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
}
} else {
// List view clicks
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
int list_y = header_y + FM_HEADER_H;
if (local_y >= header_y && local_y < list_y && filemanager_is_listing(fm)) {
// Column header click: sort by that column (again = reverse).
FileManagerListColumns cols = filemanager_list_columns(cw);
if (cols.type_x >= 0 && local_x >= cols.type_x)
filemanager_set_sort(fm, FM_SORT_TYPE);
else if (cols.size_x >= 0 && local_x >= cols.size_x)
filemanager_set_sort(fm, FM_SORT_SIZE);
else if (cols.modified_x >= 0 && local_x >= cols.modified_x - 4)
filemanager_set_sort(fm, FM_SORT_MODIFIED);
else
filemanager_set_sort(fm, FM_SORT_NAME);
return;
}
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
int clicked_idx = rel_y / FM_ITEM_H;
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
uint64_t now = montauk::get_milliseconds();
filemanager_clear_multi_selection(fm);
// Double-click detection
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
filemanager_open_entry(fm, clicked_idx);
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
fm->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
filemanager_click_entry(fm, clicked_idx);
} else {
// Background click in list area: start marquee drag
fm->selected = -1;
filemanager_clear_multi_selection(fm);
filemanager_select_single(fm, -1);
fm->marquee_active = true;
fm->marquee_start_x = local_x;
fm->marquee_start_y = rel_y;
@@ -271,13 +298,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
target_idx = idx;
// Preserve multi-selection if right-clicking inside it,
// otherwise replace it with a single selection.
if (!(fm->multi_count > 0 && fm->multi_selected[idx])) {
filemanager_clear_multi_selection(fm);
fm->selected = idx;
}
if (!filemanager_is_entry_selected(fm, idx))
filemanager_select_single(fm, idx);
} else {
filemanager_clear_multi_selection(fm);
fm->selected = -1;
filemanager_select_single(fm, -1);
}
}
} else {
@@ -289,13 +313,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
target_idx = idx;
// Preserve multi-selection if right-clicking inside it,
// otherwise replace it with a single selection.
if (!(fm->multi_count > 0 && fm->multi_selected[idx])) {
filemanager_clear_multi_selection(fm);
fm->selected = idx;
}
if (!filemanager_is_entry_selected(fm, idx))
filemanager_select_single(fm, idx);
} else {
filemanager_clear_multi_selection(fm);
fm->selected = -1;
filemanager_select_single(fm, -1);
}
}
}
@@ -319,27 +340,103 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
}
}
// Keep the selected row inside the visible list area so arrow-key navigation
static int filemanager_grid_cols(Window* win) {
int cols = (win->content_rect().w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
return cols < 1 ? 1 : cols;
}
// Keep the focused entry inside the visible area so keyboard navigation
// scrolls the view along with the selection.
static void filemanager_scroll_to_selected(Window* win, FileManagerState* fm) {
if (fm->grid_view) return;
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int list_y = fm->grid_view ? FM_TOOLBAR_H + FM_PATHBAR_H
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int list_h = win->content_h - list_y;
if (list_h <= 0) return;
int top = fm->selected * FM_ITEM_H;
int top, item_h, content_h;
if (fm->grid_view) {
int cols = filemanager_grid_cols(win);
int row = fm->selected / cols;
top = filemanager_grid_row_y(fm, row, cols);
item_h = filemanager_grid_row_height(fm, row, cols);
content_h = filemanager_grid_content_height(fm, cols);
} else {
top = fm->selected * FM_ITEM_H;
item_h = FM_ITEM_H;
content_h = fm->entry_count * FM_ITEM_H;
}
int off = fm->scrollbar.scroll_offset;
if (top < off) off = top;
else if (top + FM_ITEM_H > off + list_h) off = top + FM_ITEM_H - list_h;
else if (top + item_h > off + list_h) off = top + item_h - list_h;
int max_scroll = fm->entry_count * FM_ITEM_H - list_h;
int max_scroll = content_h - list_h;
if (off > max_scroll) off = max_scroll;
if (off < 0) off = 0;
fm->scrollbar.scroll_offset = off;
}
// Move keyboard focus to target (clamped). With extend (Shift held) the
// selection grows from the anchor instead of moving with the focus.
static void filemanager_move_focus(Window* win, FileManagerState* fm, int target, bool extend) {
if (fm->entry_count <= 0) return;
if (target < 0) target = 0;
if (target >= fm->entry_count) target = fm->entry_count - 1;
if (extend) filemanager_select_range(fm, target, false);
else filemanager_select_single(fm, target);
filemanager_scroll_to_selected(win, fm);
}
static bool filemanager_prefix_match_ci(const char* name, const char* prefix, int len) {
for (int i = 0; i < len; i++) {
char a = name[i];
char b = prefix[i];
if (!a) return false;
if (a >= 'A' && a <= 'Z') a += 32;
if (b >= 'A' && b <= 'Z') b += 32;
if (a != b) return false;
}
return true;
}
// Type-to-jump. Keys typed within a second of each other build up a prefix;
// repeating a single character instead cycles through the names starting
// with it.
static void filemanager_typeahead(Window* win, FileManagerState* fm,
const montauk::abi::KeyEvent& key) {
char ch[4];
int n = montauk::utf8::key_utf8(key, ch);
if (n <= 0 || fm->entry_count <= 0) return;
uint64_t now = montauk::get_milliseconds();
if (now - fm->typeahead_time > 1000) fm->typeahead_len = 0;
fm->typeahead_time = now;
if (fm->typeahead_len == 0 && ch[0] == ' ') return;
if (fm->typeahead_len + n >= (int)sizeof(fm->typeahead)) return;
montauk::memcpy(fm->typeahead + fm->typeahead_len, ch, n);
fm->typeahead_len += n;
fm->typeahead[fm->typeahead_len] = '\0';
bool cycle = fm->typeahead_len % n == 0;
for (int off = n; cycle && off < fm->typeahead_len; off += n) {
for (int k = 0; k < n; k++) {
if (fm->typeahead[off + k] != ch[k]) { cycle = false; break; }
}
}
int prefix_len = cycle ? n : fm->typeahead_len;
int start = fm->selected < 0 ? 0 : fm->selected + (cycle ? 1 : 0);
for (int k = 0; k < fm->entry_count; k++) {
int i = (start + k) % fm->entry_count;
if (filemanager_prefix_match_ci(fm->entry_names[i], fm->typeahead, prefix_len)) {
filemanager_move_focus(win, fm, i, false);
return;
}
}
}
void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
FileManagerState* fm = (FileManagerState*)win->app_data;
if (!fm || !key.pressed) return;
@@ -411,61 +508,71 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
}
// ---- Keyboard shortcuts ----
if (key.ctrl) {
if (key.ascii == 'c' || key.ascii == 'C') {
filemanager_do_copy(fm);
if (key.ctrl && !key.alt) {
char ch = key.ascii;
if (ch >= 'A' && ch <= 'Z') ch += 32;
switch (ch) {
case 'c': filemanager_do_copy(fm); return;
case 'x': filemanager_do_cut(fm); return;
case 'v': filemanager_do_paste(fm); return;
case 'a':
if (filemanager_is_listing(fm)) filemanager_select_all(fm);
return;
}
if (key.ascii == 'x' || key.ascii == 'X') {
filemanager_do_cut(fm);
return;
}
if (key.ascii == 'v' || key.ascii == 'V') {
filemanager_do_paste(fm);
case 'l': filemanager_start_pathbar(fm); return;
case 'n':
if (key.shift) filemanager_new_folder(fm);
return;
default: break;
}
}
// F2 = rename
// F2 = rename, F5 = refresh
if (key.scancode == 0x3C) {
filemanager_start_rename(fm);
return;
}
if (key.scancode == 0x3F) {
filemanager_refresh(fm);
return;
}
uint8_t nav = montauk::nav_scancode(key);
int cur = fm->selected;
bool have_focus = cur >= 0 && cur < fm->entry_count;
int cols = fm->grid_view ? filemanager_grid_cols(win) : 1;
if (key.ascii == '\b' || key.scancode == 0x0E) {
filemanager_go_up(fm);
} else if (montauk::nav_scancode(key) == 0x48) {
// Up arrow
if (fm->grid_view) {
Rect cr_r = win->content_rect();
int cols = (cr_r.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected >= cols) fm->selected -= cols;
} else {
if (fm->selected > 0) fm->selected--;
}
filemanager_scroll_to_selected(win, fm);
} else if (montauk::nav_scancode(key) == 0x50) {
// Down arrow
if (fm->grid_view) {
Rect cr_r = win->content_rect();
int cols = (cr_r.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
} else {
if (fm->selected < fm->entry_count - 1) fm->selected++;
}
filemanager_scroll_to_selected(win, fm);
} else if (montauk::nav_scancode(key) == 0x4B && !key.alt && fm->grid_view) {
} else if (key.scancode == 0x01) {
// Escape: drop the selection
filemanager_select_single(fm, -1);
} else if (nav == 0x48 && !key.alt) {
// Up arrow (one row up in the grid)
int target = !have_focus ? 0 : (cur >= cols ? cur - cols : cur);
filemanager_move_focus(win, fm, target, key.shift);
} else if (nav == 0x50 && !key.alt) {
// Down arrow (one row down in the grid)
int target = !have_focus ? 0 : (cur + cols < fm->entry_count ? cur + cols : cur);
filemanager_move_focus(win, fm, target, key.shift);
} else if (nav == 0x4B && !key.alt && fm->grid_view) {
// Left arrow (grid view only)
if (fm->selected > 0) fm->selected--;
} else if (montauk::nav_scancode(key) == 0x4D && !key.alt && fm->grid_view) {
filemanager_move_focus(win, fm, have_focus ? cur - 1 : 0, key.shift);
} else if (nav == 0x4D && !key.alt && fm->grid_view) {
// Right arrow (grid view only)
if (fm->selected < fm->entry_count - 1) fm->selected++;
filemanager_move_focus(win, fm, have_focus ? cur + 1 : 0, key.shift);
} else if (nav == 0x47) {
// Home
filemanager_move_focus(win, fm, 0, key.shift);
} else if (nav == 0x4F) {
// End
filemanager_move_focus(win, fm, fm->entry_count - 1, key.shift);
} else if ((key.ascii == '\n' || key.ascii == '\r') && key.alt) {
// Alt+Enter: properties
if (have_focus) filemanager_show_selection_properties(fm, cur);
} else if (key.ascii == '\n' || key.ascii == '\r') {
if (fm->selected >= 0 && fm->selected < fm->entry_count)
filemanager_open_entry(fm, fm->selected);
} else if (montauk::nav_scancode(key) == 0x53) {
if (have_focus && filemanager_is_entry_selected(fm, cur))
filemanager_open_entry(fm, cur);
} else if (nav == 0x53) {
// Delete key
filemanager_delete_selected(fm);
} else if (key.alt && montauk::nav_scancode(key) == 0x4B) {
@@ -474,6 +581,8 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
} else if (key.alt && montauk::nav_scancode(key) == 0x4D) {
// Alt+Right: go forward
filemanager_go_forward(fm);
} else if (!key.ctrl && !key.alt) {
filemanager_typeahead(win, fm, key);
}
}
@@ -48,12 +48,21 @@ struct FileManagerVirtualEntry {
// misbehaving driver and bounds memory. Far above any realistic directory.
inline constexpr int FM_ENTRY_HARD_CAP = 8192;
inline constexpr int FM_CTX_MAX_ITEMS = 10;
// Single-file deletes at or below this size are freed inline; larger files go
// to the background worker + progress dialog. Freeing a big file rewrites the
// filesystem allocation bitmaps and can take seconds on real disks, which would
// otherwise stall the desktop (Files runs inside desktop.elf).
inline constexpr uint64_t FILEMANAGER_DELETE_INLINE_MAX_BYTES = 4ull * 1024 * 1024;
enum FileManagerSortKey : uint8_t {
FM_SORT_NAME = 0,
FM_SORT_SIZE,
FM_SORT_TYPE,
FM_SORT_MODIFIED,
};
struct FileManagerState {
char current_path[256];
FileManagerLocation history[16];
@@ -67,7 +76,10 @@ struct FileManagerState {
int64_t* entry_mtimes; // last-modified time, UTC unix seconds (0 = unknown)
int entry_count;
int entry_cap; // allocated capacity of the per-entry arrays
// Focused entry. With no multi-selection it is also the selection; while
// multi_count > 0 only multi_selected marks what is selected.
int selected;
int sel_anchor; // fixed end of a Shift range selection (-1 = none)
int scroll_offset;
bool* is_dir;
int last_click_item;
@@ -84,7 +96,7 @@ struct FileManagerState {
int clipboard_count;
bool clipboard_is_cut;
// Multi-selection (marquee selection), sized with the entry arrays.
// Multi-selection (marquee, Ctrl/Shift-click), sized with the entry arrays.
bool* multi_selected;
int multi_count;
@@ -99,7 +111,7 @@ struct FileManagerState {
bool ctx_open;
int ctx_x, ctx_y;
int ctx_target_idx; // -1 = background
int ctx_items[8];
int ctx_items[FM_CTX_MAX_ITEMS];
int ctx_item_count;
int ctx_hover;
@@ -121,11 +133,22 @@ struct FileManagerState {
FileManagerVirtualEntry* virtual_entries;
int virtual_entry_count;
// Sort order of real directory listings (folders always come first).
FileManagerSortKey sort_key;
bool sort_desc;
// Type-to-jump: characters typed in quick succession select the first
// entry whose name starts with them.
char typeahead[32];
int typeahead_len;
uint64_t typeahead_time;
// 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;
void* open_with_dialog;
};
inline constexpr int FM_TOOLBAR_H = 32;
@@ -138,6 +161,37 @@ 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;
// 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.
struct FileManagerListColumns {
int name_x;
int name_right; // right edge available to the name text
int modified_x; // -1 = hidden
int size_x; // -1 = hidden
int type_x; // -1 = hidden
};
inline FileManagerListColumns filemanager_list_columns(int content_w) {
static constexpr int MIN_NAME_W = 160;
static constexpr int TYPE_W = 96;
static constexpr int SIZE_W = 76;
static constexpr int MODIFIED_W = 132;
FileManagerListColumns cols = {8, 0, -1, -1, -1};
int right = content_w - FM_SCROLLBAR_W;
if (right - SIZE_W >= MIN_NAME_W) { right -= SIZE_W; cols.size_x = right; }
if (right - TYPE_W >= MIN_NAME_W) {
// Type sits right of Size; shift Size left to make room.
cols.type_x = content_w - FM_SCROLLBAR_W - TYPE_W;
if (cols.size_x >= 0) cols.size_x -= TYPE_W;
right -= TYPE_W;
}
if (right - MODIFIED_W >= MIN_NAME_W) { right -= MODIFIED_W; cols.modified_x = right; }
cols.name_right = right - 8;
return cols;
}
// Special user folders: name, icon filename, index into DesktopState arrays
inline constexpr int SF_COUNT = DesktopState::SPECIAL_FOLDER_COUNT;
@@ -160,6 +214,12 @@ inline constexpr int CTX_DELETE = 5;
inline constexpr int CTX_NEW_FOLDER = 6;
inline constexpr int CTX_PROPERTIES = 7;
inline constexpr int CTX_REFRESH = 8;
inline constexpr int CTX_OPEN_WITH = 9;
inline constexpr int CTX_SELECT_ALL = 10;
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 bool filemanager_is_virtual_view(const FileManagerState* fm) {
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
@@ -179,6 +239,9 @@ inline const char* filemanager_virtual_view_label(FileManagerVirtualViewKind vie
int special_folder_index(const char* name);
const char* ctx_label(int action);
const char* ctx_shortcut(const FileManagerState* fm, int action);
// Menu items of different groups are split by a separator line.
int ctx_group(int action);
inline mtk::ContextMenuState filemanager_context_state(const FileManagerState* fm) {
if (!fm) return {};
@@ -199,19 +262,20 @@ inline void filemanager_build_context_items(const FileManagerState* fm,
int max_items) {
if (!fm || !items || max_items <= 0) return;
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
for (int i = 0; i < count; i++)
items[i] = {ctx_label(fm->ctx_items[i]), fm->ctx_items[i], true};
for (int i = 0; i < count; i++) {
int action = fm->ctx_items[i];
items[i] = {ctx_label(action), action, true, ctx_shortcut(fm, action),
i + 1 < count && ctx_group(action) != ctx_group(fm->ctx_items[i + 1])};
}
}
bool str_ends_with(const char* s, const char* suffix);
bool is_image_file(const char* name);
bool is_font_file(const char* name);
bool is_pdf_file(const char* name);
bool is_spreadsheet_file(const char* name);
bool is_wordprocessor_file(const char* name);
bool is_audio_file(const char* name);
bool is_video_file(const char* name);
int detect_file_type(const char* name, bool is_dir);
// Lowercased extension after the last '.', without the dot ("" if none).
// Leading-dot names (".profile") count as having no extension.
void file_extension(const char* name, char* out, int out_max);
// Human-readable type for the list view's Type column ("PDF file", "Folder").
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);
const char* path_basename(const char* path);
@@ -226,6 +290,11 @@ void filemanager_free_arrays(FileManagerState* fm);
void filemanager_read_drives(FileManagerState* fm);
void filemanager_read_dir(FileManagerState* fm);
// Re-sort a real directory listing by sort_key/sort_desc, keeping the
// selection on the same entries. No-op for Computer and virtual views.
void filemanager_sort_entries(FileManagerState* fm);
// Pick a sort column: a new key sorts ascending, the current key flips order.
void filemanager_set_sort(FileManagerState* fm, FileManagerSortKey key);
void filemanager_free_app_icons(FileManagerState* fm);
void filemanager_read_virtual_view(FileManagerState* fm, FileManagerVirtualViewKind view);
void filemanager_read_apps(FileManagerState* fm);
@@ -293,6 +362,26 @@ void filemanager_commit_pathbar(FileManagerState* fm);
void filemanager_open_entry(FileManagerState* fm, int idx);
// ---- 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
// lists the extension in files.types. Returns a desktop_items index or -1.
int filemanager_assoc_default(DesktopState* ds, const char* ext);
// Apps whose manifest claims ext come first ("recommended"); every other
// launchable app follows. Returns the total count, *recommended gets how many
// of those lead the list. Indices refer to ds->desktop_items.
int filemanager_assoc_candidates(DesktopState* ds, const char* ext,
int* out, int max, int* recommended);
// Remember item as the user's default app for ext. Returns false if ext cannot
// be stored as a config key or the save failed.
bool filemanager_assoc_set_default(DesktopState* ds, const char* ext, int item_index);
// True if the file looks like text (no NULs / stray control bytes in its
// first KiB). Empty files count as text.
bool filemanager_file_looks_textual(const char* path);
// Launch a desktop item with a file path argument.
bool filemanager_launch_with(DesktopState* ds, int item_index, const char* path);
void filemanager_show_open_with(FileManagerState* fm, int target_idx);
// ---- Async file operations (copy/move/delete on a worker thread) ----
enum FileOpKind : int {
FILE_OP_COPY = 0,
@@ -313,6 +402,15 @@ int filemanager_grid_index_at(FileManagerState* fm, int cols, int x, int rel_y);
// Selection helpers
void filemanager_clear_multi_selection(FileManagerState* fm);
// Replace the selection with a single entry (or none for idx < 0).
void filemanager_select_single(FileManagerState* fm, int idx);
// Ctrl-click: add idx to or remove it from the selection.
void filemanager_toggle_selection(FileManagerState* fm, int idx);
// Shift-click / Shift+arrow: select the range from sel_anchor to idx. With
// additive set (Ctrl+Shift) the range is added to the existing selection.
void filemanager_select_range(FileManagerState* fm, int idx, bool additive);
void filemanager_select_all(FileManagerState* fm);
bool filemanager_is_entry_selected(const FileManagerState* fm, int idx);
// Fill out with selected entry indices. Returns count. Uses multi_selected if
// non-empty, otherwise falls back to fm->selected (0 or 1 results).
int filemanager_collect_selection(const FileManagerState* fm, int* out, int max);
@@ -100,6 +100,8 @@ void filemanager_free_arrays(FileManagerState* fm) {
static void filemanager_reset_list_state(FileManagerState* fm) {
fm->selected = -1;
fm->sel_anchor = -1;
fm->typeahead_len = 0;
fm->scroll_offset = 0;
fm->scrollbar.scroll_offset = 0;
fm->last_click_item = -1;
@@ -344,42 +346,113 @@ void filemanager_read_dir(FileManagerState* fm) {
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];
uint64_t tmp_size = fm->entry_sizes[i];
int64_t tmp_mtime = fm->entry_mtimes[i];
bool tmp_isdir = fm->is_dir[i];
montauk::strcpy(tmp_name, fm->entry_names[i]);
int j = i - 1;
while (j >= 0) {
bool swap = false;
if (tmp_isdir && !fm->is_dir[j]) {
swap = true;
} else if (tmp_isdir == fm->is_dir[j]) {
if (str_compare_ci(tmp_name, fm->entry_names[j]) < 0) {
swap = true;
}
}
if (!swap) break;
montauk::strcpy(fm->entry_names[j + 1], fm->entry_names[j]);
fm->entry_types[j + 1] = fm->entry_types[j];
fm->entry_sizes[j + 1] = fm->entry_sizes[j];
fm->entry_mtimes[j + 1] = fm->entry_mtimes[j];
fm->is_dir[j + 1] = fm->is_dir[j];
j--;
}
montauk::strcpy(fm->entry_names[j + 1], tmp_name);
fm->entry_types[j + 1] = tmp_type;
fm->entry_sizes[j + 1] = tmp_size;
fm->entry_mtimes[j + 1] = tmp_mtime;
fm->is_dir[j + 1] = tmp_isdir;
}
filemanager_reset_list_state(fm);
filemanager_sort_entries(fm);
}
// Folders always lead; within each group the active column decides, with the
// name as tie-breaker so equal sizes/types/times still list predictably.
static int filemanager_compare_entries(const FileManagerState* fm, int a, int b) {
if (fm->is_dir[a] != fm->is_dir[b]) return fm->is_dir[a] ? -1 : 1;
int c = 0;
switch (fm->sort_key) {
case FM_SORT_SIZE:
if (fm->entry_sizes[a] != fm->entry_sizes[b])
c = fm->entry_sizes[a] < fm->entry_sizes[b] ? -1 : 1;
break;
case FM_SORT_MODIFIED:
if (fm->entry_mtimes[a] != fm->entry_mtimes[b])
c = fm->entry_mtimes[a] < fm->entry_mtimes[b] ? -1 : 1;
break;
case FM_SORT_TYPE: {
char ea[16], eb[16];
file_extension(fm->entry_names[a], ea, sizeof(ea));
file_extension(fm->entry_names[b], eb, sizeof(eb));
c = str_compare_ci(ea, eb);
break;
}
default:
break;
}
if (c == 0) c = str_compare_ci(fm->entry_names[a], fm->entry_names[b]);
return fm->sort_desc ? -c : c;
}
template <typename T>
static void filemanager_permute(T* arr, const int* perm, int n, void* scratch) {
T* tmp = (T*)scratch;
for (int j = 0; j < n; j++) tmp[j] = arr[perm[j]];
montauk::memcpy(arr, tmp, (uint64_t)n * sizeof(T));
}
static int filemanager_remap_index(int idx, const int* inv, int n) {
return (idx >= 0 && idx < n) ? inv[idx] : idx;
}
void filemanager_sort_entries(FileManagerState* fm) {
if (!fm || filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
int n = fm->entry_count;
if (n < 2) return;
// Sort an index permutation (stable bottom-up merge sort), then apply it
// to every parallel array once instead of swapping 128-byte names around.
int* perm = (int*)montauk::malloc((uint64_t)n * sizeof(int));
int* tmp = (int*)montauk::malloc((uint64_t)n * sizeof(int));
void* scratch = montauk::malloc((uint64_t)n * 128);
if (!perm || !tmp || !scratch) {
montauk::mfree(perm);
montauk::mfree(tmp);
montauk::mfree(scratch);
return;
}
for (int i = 0; i < n; i++) perm[i] = i;
for (int width = 1; width < n; width *= 2) {
for (int lo = 0; lo < n; lo += 2 * width) {
int mid = lo + width < n ? lo + width : n;
int hi = lo + 2 * width < n ? lo + 2 * width : n;
int i = lo, j = mid, k = lo;
while (i < mid && j < hi)
tmp[k++] = filemanager_compare_entries(fm, perm[j], perm[i]) < 0 ? perm[j++] : perm[i++];
while (i < mid) tmp[k++] = perm[i++];
while (j < hi) tmp[k++] = perm[j++];
}
int* swap = perm; perm = tmp; tmp = swap;
}
auto* names = (char(*)[128])scratch;
for (int j = 0; j < n; j++) montauk::memcpy(names[j], fm->entry_names[perm[j]], 128);
montauk::memcpy(fm->entry_names, names, (uint64_t)n * 128);
filemanager_permute(fm->entry_types, perm, n, scratch);
filemanager_permute(fm->entry_sizes, perm, n, scratch);
filemanager_permute(fm->entry_mtimes, perm, n, scratch);
filemanager_permute(fm->is_dir, perm, n, scratch);
filemanager_permute(fm->multi_selected, perm, n, scratch);
// Keep focus, anchor and click tracking on the same entries.
int* inv = tmp;
for (int j = 0; j < n; j++) inv[perm[j]] = j;
fm->selected = filemanager_remap_index(fm->selected, inv, n);
fm->sel_anchor = filemanager_remap_index(fm->sel_anchor, inv, n);
fm->last_click_item = filemanager_remap_index(fm->last_click_item, inv, n);
fm->rename_idx = filemanager_remap_index(fm->rename_idx, inv, n);
montauk::mfree(perm);
montauk::mfree(tmp);
montauk::mfree(scratch);
}
void filemanager_set_sort(FileManagerState* fm, FileManagerSortKey key) {
if (!fm) return;
if (fm->sort_key == key) {
fm->sort_desc = !fm->sort_desc;
} else {
fm->sort_key = key;
// Largest and newest first is what one usually wants from these.
fm->sort_desc = key == FM_SORT_SIZE || key == FM_SORT_MODIFIED;
}
filemanager_sort_entries(fm);
}
void filemanager_free_app_icons(FileManagerState* fm) {
+191 -30
View File
@@ -15,6 +15,91 @@ void filemanager_clear_multi_selection(FileManagerState* fm) {
fm->multi_count = 0;
}
bool filemanager_is_entry_selected(const FileManagerState* fm, int idx) {
if (!fm || idx < 0 || idx >= fm->entry_count) return false;
if (fm->multi_count > 0) return fm->multi_selected[idx];
return idx == fm->selected;
}
void filemanager_select_single(FileManagerState* fm, int idx) {
if (!fm) return;
filemanager_clear_multi_selection(fm);
fm->selected = (idx >= 0 && idx < fm->entry_count) ? idx : -1;
fm->sel_anchor = fm->selected;
}
// A multi-selection of one entry collapses back to the plain single-selection
// form, which the rename/toolbar logic treats as "one item selected".
static void filemanager_normalize_selection(FileManagerState* fm) {
if (fm->multi_count != 1) return;
for (int i = 0; i < fm->entry_count; i++) {
if (fm->multi_selected[i]) {
filemanager_clear_multi_selection(fm);
fm->selected = i;
return;
}
}
}
// Turn a plain single selection into the multi form before extending it.
static void filemanager_seed_multi_selection(FileManagerState* fm) {
if (fm->multi_count > 0) return;
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
fm->multi_selected[fm->selected] = true;
fm->multi_count = 1;
}
}
void filemanager_toggle_selection(FileManagerState* fm, int idx) {
if (!fm || idx < 0 || idx >= fm->entry_count) return;
filemanager_seed_multi_selection(fm);
if (fm->multi_selected[idx]) {
fm->multi_selected[idx] = false;
fm->multi_count--;
} else {
fm->multi_selected[idx] = true;
fm->multi_count++;
}
fm->selected = fm->multi_count > 0 ? idx : -1;
fm->sel_anchor = idx;
filemanager_normalize_selection(fm);
}
void filemanager_select_range(FileManagerState* fm, int idx, bool additive) {
if (!fm || idx < 0 || idx >= fm->entry_count) return;
int anchor = fm->sel_anchor;
if (anchor < 0 || anchor >= fm->entry_count)
anchor = (fm->selected >= 0 && fm->selected < fm->entry_count) ? fm->selected : idx;
if (additive) filemanager_seed_multi_selection(fm);
else filemanager_clear_multi_selection(fm);
int lo = anchor < idx ? anchor : idx;
int hi = anchor < idx ? idx : anchor;
for (int i = lo; i <= hi; i++) {
if (!fm->multi_selected[i]) {
fm->multi_selected[i] = true;
fm->multi_count++;
}
}
fm->selected = idx;
fm->sel_anchor = anchor;
filemanager_normalize_selection(fm);
}
void filemanager_select_all(FileManagerState* fm) {
if (!fm || fm->entry_count <= 0) return;
if (fm->entry_count == 1) {
filemanager_select_single(fm, 0);
return;
}
for (int i = 0; i < fm->entry_count; i++) fm->multi_selected[i] = true;
fm->multi_count = fm->entry_count;
if (fm->selected < 0 || fm->selected >= fm->entry_count) fm->selected = 0;
}
int filemanager_collect_selection(const FileManagerState* fm, int* out, int max) {
if (!fm || !out || max <= 0) return 0;
int n = 0;
@@ -46,10 +131,64 @@ const char* ctx_label(int action) {
case CTX_NEW_FOLDER: return "New Folder";
case CTX_PROPERTIES: return "Properties";
case CTX_REFRESH: return "Refresh";
case CTX_OPEN_WITH: return "Open With...";
case CTX_SELECT_ALL: return "Select All";
case CTX_SORT_NAME: return "Sort by Name";
case CTX_SORT_SIZE: return "Sort by Size";
case CTX_SORT_TYPE: return "Sort by Type";
case CTX_SORT_MODIFIED: return "Sort by Modified";
default: return "?";
}
}
const char* ctx_shortcut(const FileManagerState* fm, int action) {
// The active sort column shows its direction in the shortcut slot.
int sort_action = -1;
if (fm) {
switch (fm->sort_key) {
case FM_SORT_NAME: sort_action = CTX_SORT_NAME; break;
case FM_SORT_SIZE: sort_action = CTX_SORT_SIZE; break;
case FM_SORT_TYPE: sort_action = CTX_SORT_TYPE; break;
case FM_SORT_MODIFIED: sort_action = CTX_SORT_MODIFIED; break;
}
}
if (action == sort_action) return fm->sort_desc ? "Desc" : "Asc";
switch (action) {
case CTX_OPEN: return "Enter";
case CTX_COPY: return "Ctrl+C";
case CTX_CUT: return "Ctrl+X";
case CTX_PASTE: return "Ctrl+V";
case CTX_RENAME: return "F2";
case CTX_DELETE: return "Del";
case CTX_NEW_FOLDER: return "Ctrl+Shift+N";
case CTX_PROPERTIES: return "Alt+Enter";
case CTX_REFRESH: return "F5";
case CTX_SELECT_ALL: return "Ctrl+A";
default: return nullptr;
}
}
int ctx_group(int action) {
switch (action) {
case CTX_OPEN:
case CTX_OPEN_WITH: return 0;
case CTX_COPY:
case CTX_CUT:
case CTX_PASTE: return 1;
case CTX_RENAME:
case CTX_DELETE:
case CTX_NEW_FOLDER: return 2;
case CTX_SELECT_ALL: return 3;
case CTX_SORT_NAME:
case CTX_SORT_SIZE:
case CTX_SORT_TYPE:
case CTX_SORT_MODIFIED: return 4;
case CTX_REFRESH: return 5;
default: return 6; // Properties
}
}
bool str_ends_with(const char* s, const char* suffix) {
int slen = montauk::slen(s);
int suflen = montauk::slen(suffix);
@@ -64,42 +203,64 @@ bool str_ends_with(const char* s, const char* suffix) {
return true;
}
bool is_image_file(const char* name) {
return str_ends_with(name, ".jpg")
|| str_ends_with(name, ".jpeg")
|| str_ends_with(name, ".svg");
}
bool is_font_file(const char* name) {
return str_ends_with(name, ".ttf");
}
bool is_pdf_file(const char* name) {
return str_ends_with(name, ".pdf");
}
bool is_spreadsheet_file(const char* name) {
return str_ends_with(name, ".mss");
}
bool is_wordprocessor_file(const char* name) {
return str_ends_with(name, ".mwp");
}
bool is_audio_file(const char* name) {
return str_ends_with(name, ".mp3") || str_ends_with(name, ".wav");
}
bool is_video_file(const char* name) {
return str_ends_with(name, ".avi");
}
int detect_file_type(const char* name, bool is_dir) {
if (is_dir) return 1;
if (str_ends_with(name, ".elf")) return 2;
return 0;
}
void file_extension(const char* name, char* out, int out_max) {
if (!out || out_max <= 0) return;
out[0] = '\0';
if (!name) return;
const char* dot = nullptr;
for (const char* p = name; *p; p++) {
if (*p == '.') dot = p;
}
if (!dot || dot == name || !dot[1]) return;
int n = 0;
for (const char* p = dot + 1; *p && n < out_max - 1; p++) {
char ch = *p;
out[n++] = (ch >= 'A' && ch <= 'Z') ? ch + 32 : ch;
}
out[n] = '\0';
}
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max) {
if (!out || out_max <= 0) return;
const char* fixed = nullptr;
switch (fm->entry_types[idx]) {
case FM_ENTRY_VIRTUAL_ITEM:
fixed = fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION ? "Setting" : "App";
break;
case FM_ENTRY_APPS_ROOT: fixed = "Apps"; break;
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_SPECIAL_DIR:
case FM_ENTRY_DIR: fixed = "Folder"; break;
case FM_ENTRY_EXEC: fixed = "Program"; break;
default: break;
}
if (fixed) {
montauk::strncpy(out, fixed, out_max);
return;
}
char ext[16];
file_extension(fm->entry_names[idx], ext, 9);
if (!ext[0]) {
montauk::strncpy(out, "File", out_max);
return;
}
for (int k = 0; ext[k]; k++) {
if (ext[k] >= 'a' && ext[k] <= 'z') ext[k] -= 32;
}
snprintf(out, out_max, "%s file", ext);
}
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name) {
montauk::strcpy(out, dir);
int plen = montauk::slen(out);
@@ -281,28 +281,28 @@ void filemanager_open_entry(FileManagerState* fm, int idx) {
return;
}
// Open file with appropriate app
// Open the file with its default app (see associations.cpp).
DesktopState* ds = fm->desktop;
const char* name = fm->entry_names[idx];
char fullpath[512];
filemanager_build_fullpath(fullpath, 512, fm->current_path, fm->entry_names[idx]);
if (is_image_file(fm->entry_names[idx])) {
montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath);
} else if (is_font_file(fm->entry_names[idx])) {
montauk::spawn("0:/apps/fontpreview/fontpreview.elf", fullpath);
} else if (is_pdf_file(fm->entry_names[idx])) {
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
} else if (is_spreadsheet_file(fm->entry_names[idx])) {
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
} else if (is_wordprocessor_file(fm->entry_names[idx])) {
montauk::spawn("0:/apps/wordprocessor/wordprocessor.elf", fullpath);
} else if (is_audio_file(fm->entry_names[idx])) {
montauk::spawn("0:/apps/music/music.elf", fullpath);
} else if (is_video_file(fm->entry_names[idx])) {
montauk::spawn("0:/apps/video/video.elf", fullpath);
} else if (str_ends_with(fm->entry_names[idx], ".elf")) {
montauk::spawn(fullpath);
} else {
montauk::spawn("0:/apps/texteditor/texteditor.elf", fullpath);
filemanager_build_fullpath(fullpath, 512, fm->current_path, name);
if (str_ends_with(name, ".elf")) {
desktop_spawn_app(ds, fullpath);
return;
}
char ext[16];
file_extension(name, ext, sizeof(ext));
int app = filemanager_assoc_default(ds, ext);
if (app >= 0 && filemanager_launch_with(ds, app, fullpath)) return;
// Nothing claims this type: text still goes straight to the Text Editor
// (READMEs, Makefiles, odd extensions); anything else asks.
if (filemanager_file_looks_textual(fullpath)) {
desktop_spawn_app(ds, "0:/apps/texteditor/texteditor.elf", fullpath);
return;
}
filemanager_show_open_with(fm, idx);
}
} // namespace filemanager
@@ -135,6 +135,21 @@ static void draw_marquee(Canvas& c, FileManagerState* fm, int list_y) {
}
}
// Column title, plus a small arrow when it is the active sort column.
static void draw_column_header(Canvas& c, int x, int header_y, const char* label,
bool active, bool desc, Color color) {
c.text(x, header_y + 2, label, active ? colors::TEXT_COLOR : color);
if (!active) return;
int ax = x + text_width(label) + 6;
int ay = header_y + FM_HEADER_H / 2 - 2;
for (int r = 0; r < 4; r++) {
// Ascending points up, descending points down.
int row = desc ? 3 - r : r;
c.hline(ax + 3 - r, ay + row, r * 2 + 1, colors::TEXT_COLOR);
}
}
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols) {
if (!fm || cols <= 0 || row < 0) return FM_GRID_CELL_H;
@@ -342,8 +357,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
if (cell_y + row_h <= list_y || cell_y >= c.h) continue;
// Selection highlight (single or multi)
bool cell_selected = (i == fm->selected) ||
(fm->multi_selected && fm->multi_selected[i]);
bool cell_selected = filemanager_is_entry_selected(fm, i);
if (cell_selected) {
int sy = gui_max(cell_y, list_y);
int sh = gui_min(cell_y + row_h, c.h) - sy;
@@ -446,23 +460,32 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
int name_col_x = 8;
int size_col_x = c.w - FM_SCROLLBAR_W - 120;
int type_col_x = c.w - FM_SCROLLBAR_W - 60;
FileManagerListColumns cols = filemanager_list_columns(c.w);
// Sorting only applies to real directories; Computer and the virtual
// views keep their curated order and show no sort arrow.
bool sortable = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
c.text(name_col_x, header_y + 2, "Name", dim);
if (size_col_x > 100)
c.text(size_col_x, header_y + 2, "Size", dim);
if (type_col_x > 160)
c.text(type_col_x, header_y + 2, "Type", dim);
draw_column_header(c, cols.name_x, header_y, "Name",
sortable && fm->sort_key == FM_SORT_NAME, fm->sort_desc, dim);
if (cols.modified_x >= 0)
draw_column_header(c, cols.modified_x, header_y, "Modified",
sortable && fm->sort_key == FM_SORT_MODIFIED, fm->sort_desc, dim);
if (cols.size_x >= 0)
draw_column_header(c, cols.size_x, header_y, "Size",
sortable && fm->sort_key == FM_SORT_SIZE, fm->sort_desc, dim);
if (cols.type_x >= 0)
draw_column_header(c, cols.type_x, header_y, "Type",
sortable && fm->sort_key == FM_SORT_TYPE, fm->sort_desc, dim);
// Header separator
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
// Column separator lines
if (size_col_x > 100) {
c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER);
}
int first_col_x = cols.modified_x >= 0 ? cols.modified_x
: cols.size_x >= 0 ? cols.size_x : cols.type_x;
if (first_col_x >= 0)
c.vline(first_col_x - 4, header_y, c.h - header_y, colors::BORDER);
int tz = montauk::gettz();
// ---- File entries ----
int list_y = header_y + FM_HEADER_H;
@@ -482,8 +505,7 @@ 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) ||
(fm->multi_selected && fm->multi_selected[i]);
bool row_selected = filemanager_is_entry_selected(fm, i);
if (row_selected) {
int sy = gui_max(iy, list_y);
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
@@ -540,7 +562,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
if (fm->rename_active && fm->rename_idx == i && ty >= list_y && ty + fm_sfh <= c.h) {
// Rename textbox inline
int rw = (size_col_x > 100 ? size_col_x - 8 : c.w - FM_SCROLLBAR_W - 8) - tx;
int rw = cols.name_right - tx;
c.fill_rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::WHITE);
c.rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::ACCENT);
c.text(tx, ty, fm->rename_buf, colors::TEXT_COLOR);
@@ -556,8 +578,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
if (ty >= list_y && ty + fm_sfh <= c.h) {
// Truncate name to fit the name column width
const char* name = fm->entry_names[i];
int name_max_w = size_col_x - tx - 8;
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
int name_max_w = cols.name_right - tx;
char label[64];
montauk::utf8::copy(label, 64, name);
@@ -581,28 +602,30 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
}
}
bool row_text_visible = ty >= list_y && ty + fm_sfh <= c.h;
// Modified
if (cols.modified_x >= 0 && row_text_visible && fm->entry_mtimes[i] > 0) {
char date_str[24];
format_datetime(date_str, sizeof(date_str), fm->entry_mtimes[i], tz);
c.text(cols.modified_x, ty, date_str, dim);
}
// Size
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + fm_sfh <= c.h) {
if (cols.size_x >= 0 && !fm->is_dir[i] && row_text_visible) {
char size_str[16];
format_size(size_str, fm->entry_sizes[i]);
c.text(size_col_x, ty, size_str, dim);
c.text(cols.size_x, ty, size_str, dim);
}
// Type
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
const char* type_str = "File";
if (fm->entry_types[i] == FM_ENTRY_VIRTUAL_ITEM) {
type_str = fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION
? "Setting"
: "App";
}
else if (fm->entry_types[i] == FM_ENTRY_APPS_ROOT) type_str = "Apps";
else if (fm->entry_types[i] == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT) type_str = "Settings";
else if (fm->entry_types[i] == FM_ENTRY_HOME) type_str = "Home";
else if (fm->entry_types[i] == FM_ENTRY_DRIVE) type_str = "Drive";
else if (fm->entry_types[i] == FM_ENTRY_DIR) type_str = "Dir";
else if (fm->entry_types[i] == FM_ENTRY_EXEC) type_str = "Exec";
c.text(type_col_x, ty, type_str, dim);
if (cols.type_x >= 0 && row_text_visible) {
char type_str[24];
filemanager_type_label(fm, i, type_str, sizeof(type_str));
char type_fit[24];
fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str,
c.w - FM_SCROLLBAR_W - cols.type_x - 4);
c.text(cols.type_x, ty, type_fit, dim);
}
}
@@ -637,10 +660,10 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
theme.border = colors::BORDER;
theme.text = colors::TEXT_COLOR;
mtk::ContextMenuItem items[8];
filemanager_build_context_items(fm, items, 8);
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_item_count, theme, FM_CTX_MENU_W);
}
}
+2
View File
@@ -887,6 +887,8 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
montauk::abi::KeyEvent translated_key = key;
montauk::keyboard::translate(ds->keyboard, &translated_key);
const montauk::abi::KeyEvent& routed_key = translated_key;
ds->kbd_shift_down = routed_key.shift;
ds->kbd_ctrl_down = routed_key.ctrl;
if (ds->screen_locked) {
handle_lock_keyboard(ds, routed_key);
+21
View File
@@ -181,6 +181,27 @@ void desktop_scan_apps(DesktopState* ds) {
snprintf(item->binary_path, sizeof(item->binary_path),
"0:/apps/%s/%s", dirname, binary);
// File types the app opens, used by Files for default apps and
// "Open With". Stored lowercased as " ext1 ext2 " for whole-word
// matching; entries that would overflow the buffer are dropped.
if (auto* types = doc.get_array("files.types")) {
int len = 0;
item->file_types[len++] = ' ';
for (int t = 0; t < types->array.count; t++) {
auto* v = types->array.items[t];
if (v->type != montauk::toml::Type::String || !v->str[0]) continue;
const char* ext = v->str[0] == '.' ? v->str + 1 : v->str;
int elen = montauk::slen(ext);
if (elen == 0 || len + elen + 2 > (int)sizeof(item->file_types)) continue;
for (int k = 0; k < elen; k++) {
char ch = ext[k];
item->file_types[len++] = (ch >= 'A' && ch <= 'Z') ? ch + 32 : ch;
}
item->file_types[len++] = ' ';
}
item->file_types[len < 2 ? 0 : len] = '\0';
}
// Load icon from app directory
item->icon = {};
item->icon_path[0] = '\0';
+4
View File
@@ -12,3 +12,7 @@ visible = false
# starting directory. Don't hand it home_dir when launched from the Apps
# folder (it would try to parse the directory path as a font).
pass_home_dir = false
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["ttf"]
+4
View File
@@ -6,3 +6,7 @@ icon = "image-viewer.svg"
[menu]
category = "Media"
visible = false
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["jpg", "jpeg", "png", "bmp", "gif", "tga", "svg"]
+4
View File
@@ -9,3 +9,7 @@ visible = true
[launch]
pass_home_dir = false
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["mp3", "wav"]
+4
View File
@@ -6,3 +6,7 @@ icon = "pdf-viewer.svg"
[menu]
category = "Productivity"
visible = false
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["pdf"]
+4
View File
@@ -6,3 +6,7 @@ icon = "spreadsheet.svg"
[menu]
category = "Productivity"
visible = true
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["mss"]
+4
View File
@@ -9,3 +9,7 @@ visible = true
[launch]
pass_home_dir = false
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["txt", "md", "log", "toml", "ini", "cfg", "conf", "json", "xml", "csv", "c", "h", "cc", "cpp", "hpp", "rs", "s", "asm", "ld", "mk", "sh", "py", "js", "css", "html"]
+4
View File
@@ -6,3 +6,7 @@ icon = "video.svg"
[menu]
category = "Media"
visible = true
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["avi"]
+4
View File
@@ -6,3 +6,7 @@ icon = "wordprocessor.svg"
[menu]
category = "Productivity"
visible = true
[files]
# Extensions Files opens with this app (lowercase, no dot)
types = ["mwp"]