feat: expand Files features; add file associations
This commit is contained in:
1 parent
23cc56b933
commit
61d3b368f3
20 files changed
+779
-238
No files matched your search
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 4
|
#define MONTAUK_BUILD_NUMBER 5
|
||||||
@@ -46,6 +46,9 @@ struct DesktopItem {
|
|||||||
bool menu_visible;
|
bool menu_visible;
|
||||||
bool launch_with_home;
|
bool launch_with_home;
|
||||||
bool admin_only;
|
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 {
|
enum LauncherItemKind : uint8_t {
|
||||||
@@ -121,6 +124,10 @@ struct DesktopState {
|
|||||||
int launcher_scroll;
|
int launcher_scroll;
|
||||||
bool super_left_down;
|
bool super_left_down;
|
||||||
bool super_right_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_terminal;
|
||||||
SvgIcon icon_filemanager;
|
SvgIcon icon_filemanager;
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ void filemanager_do_paste(FileManagerState* fm) {
|
|||||||
|
|
||||||
void filemanager_start_rename(FileManagerState* fm) {
|
void filemanager_start_rename(FileManagerState* fm) {
|
||||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
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) ||
|
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
||||||
fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||||
|
|
||||||
@@ -228,10 +231,14 @@ 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_OPEN;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||||
} else {
|
} else {
|
||||||
|
bool several = fm->multi_count > 1;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
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_COPY;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_CUT;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_CUT;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_RENAME;
|
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_DELETE;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
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)
|
if (fm->clipboard_count > 0)
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
||||||
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW_FOLDER;
|
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));
|
FileManagerState* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState));
|
||||||
montauk::memset(fm, 0, sizeof(FileManagerState));
|
montauk::memset(fm, 0, sizeof(FileManagerState));
|
||||||
fm->selected = -1;
|
fm->selected = -1;
|
||||||
|
fm->sel_anchor = -1;
|
||||||
fm->last_click_item = -1;
|
fm->last_click_item = -1;
|
||||||
fm->history_pos = -1;
|
fm->history_pos = -1;
|
||||||
fm->history_count = 0;
|
fm->history_count = 0;
|
||||||
|
|||||||
@@ -8,6 +8,51 @@
|
|||||||
|
|
||||||
namespace filemanager {
|
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) {
|
void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||||
if (!fm) return;
|
if (!fm) return;
|
||||||
@@ -26,13 +71,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
// ---- Context menu interaction ----
|
// ---- Context menu interaction ----
|
||||||
if (fm->ctx_open) {
|
if (fm->ctx_open) {
|
||||||
mtk::ContextMenuState state = filemanager_context_state(fm);
|
mtk::ContextMenuState state = filemanager_context_state(fm);
|
||||||
mtk::ContextMenuItem items[8];
|
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
||||||
filemanager_build_context_items(fm, items, 8);
|
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
||||||
int action = mtk::context_menu_handle_mouse(state, items,
|
int action = mtk::context_menu_handle_mouse(state, items,
|
||||||
fm->ctx_item_count,
|
fm->ctx_item_count,
|
||||||
local_x, local_y,
|
local_x, local_y,
|
||||||
ev.buttons, ev.prev_buttons,
|
ev.buttons, ev.prev_buttons,
|
||||||
cw, win->content_h);
|
cw, win->content_h,
|
||||||
|
FM_CTX_MENU_W);
|
||||||
filemanager_store_context_state(fm, state);
|
filemanager_store_context_state(fm, state);
|
||||||
|
|
||||||
if (action >= 0) {
|
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_RENAME: filemanager_start_rename(fm); break;
|
||||||
case CTX_DELETE: filemanager_delete_selected(fm); break;
|
case CTX_DELETE: filemanager_delete_selected(fm); break;
|
||||||
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
||||||
case CTX_PROPERTIES: {
|
case CTX_PROPERTIES: filemanager_show_selection_properties(fm, target); break;
|
||||||
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_REFRESH: filemanager_refresh(fm); 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;
|
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);
|
int clicked_idx = filemanager_grid_index_at(fm, cols, local_x, rel_y);
|
||||||
|
|
||||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||||
uint64_t now = montauk::get_milliseconds();
|
filemanager_click_entry(fm, clicked_idx);
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Background click in grid area: start marquee drag
|
// Background click in grid area: start marquee drag
|
||||||
fm->selected = -1;
|
filemanager_select_single(fm, -1);
|
||||||
filemanager_clear_multi_selection(fm);
|
|
||||||
fm->marquee_active = true;
|
fm->marquee_active = true;
|
||||||
fm->marquee_start_x = local_x;
|
fm->marquee_start_x = local_x;
|
||||||
fm->marquee_start_y = rel_y;
|
fm->marquee_start_y = rel_y;
|
||||||
@@ -149,30 +176,30 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// List view clicks
|
// 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) {
|
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
||||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
||||||
int clicked_idx = rel_y / FM_ITEM_H;
|
int clicked_idx = rel_y / FM_ITEM_H;
|
||||||
|
|
||||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||||
uint64_t now = montauk::get_milliseconds();
|
filemanager_click_entry(fm, clicked_idx);
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Background click in list area: start marquee drag
|
// Background click in list area: start marquee drag
|
||||||
fm->selected = -1;
|
filemanager_select_single(fm, -1);
|
||||||
filemanager_clear_multi_selection(fm);
|
|
||||||
fm->marquee_active = true;
|
fm->marquee_active = true;
|
||||||
fm->marquee_start_x = local_x;
|
fm->marquee_start_x = local_x;
|
||||||
fm->marquee_start_y = rel_y;
|
fm->marquee_start_y = rel_y;
|
||||||
@@ -271,13 +298,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
target_idx = idx;
|
target_idx = idx;
|
||||||
// Preserve multi-selection if right-clicking inside it,
|
// Preserve multi-selection if right-clicking inside it,
|
||||||
// otherwise replace it with a single selection.
|
// otherwise replace it with a single selection.
|
||||||
if (!(fm->multi_count > 0 && fm->multi_selected[idx])) {
|
if (!filemanager_is_entry_selected(fm, idx))
|
||||||
filemanager_clear_multi_selection(fm);
|
filemanager_select_single(fm, idx);
|
||||||
fm->selected = idx;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
filemanager_clear_multi_selection(fm);
|
filemanager_select_single(fm, -1);
|
||||||
fm->selected = -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -289,13 +313,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
|||||||
target_idx = idx;
|
target_idx = idx;
|
||||||
// Preserve multi-selection if right-clicking inside it,
|
// Preserve multi-selection if right-clicking inside it,
|
||||||
// otherwise replace it with a single selection.
|
// otherwise replace it with a single selection.
|
||||||
if (!(fm->multi_count > 0 && fm->multi_selected[idx])) {
|
if (!filemanager_is_entry_selected(fm, idx))
|
||||||
filemanager_clear_multi_selection(fm);
|
filemanager_select_single(fm, idx);
|
||||||
fm->selected = idx;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
filemanager_clear_multi_selection(fm);
|
filemanager_select_single(fm, -1);
|
||||||
fm->selected = -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.
|
// scrolls the view along with the selection.
|
||||||
static void filemanager_scroll_to_selected(Window* win, FileManagerState* fm) {
|
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;
|
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;
|
int list_h = win->content_h - list_y;
|
||||||
if (list_h <= 0) return;
|
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;
|
int off = fm->scrollbar.scroll_offset;
|
||||||
if (top < off) off = top;
|
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 > max_scroll) off = max_scroll;
|
||||||
if (off < 0) off = 0;
|
if (off < 0) off = 0;
|
||||||
fm->scrollbar.scroll_offset = off;
|
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) {
|
void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||||
if (!fm || !key.pressed) return;
|
if (!fm || !key.pressed) return;
|
||||||
@@ -411,61 +508,71 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Keyboard shortcuts ----
|
// ---- Keyboard shortcuts ----
|
||||||
if (key.ctrl) {
|
if (key.ctrl && !key.alt) {
|
||||||
if (key.ascii == 'c' || key.ascii == 'C') {
|
char ch = key.ascii;
|
||||||
filemanager_do_copy(fm);
|
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;
|
return;
|
||||||
}
|
case 'l': filemanager_start_pathbar(fm); return;
|
||||||
if (key.ascii == 'x' || key.ascii == 'X') {
|
case 'n':
|
||||||
filemanager_do_cut(fm);
|
if (key.shift) filemanager_new_folder(fm);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key.ascii == 'v' || key.ascii == 'V') {
|
|
||||||
filemanager_do_paste(fm);
|
|
||||||
return;
|
return;
|
||||||
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// F2 = rename
|
// F2 = rename, F5 = refresh
|
||||||
if (key.scancode == 0x3C) {
|
if (key.scancode == 0x3C) {
|
||||||
filemanager_start_rename(fm);
|
filemanager_start_rename(fm);
|
||||||
return;
|
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) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
filemanager_go_up(fm);
|
filemanager_go_up(fm);
|
||||||
} else if (montauk::nav_scancode(key) == 0x48) {
|
} else if (key.scancode == 0x01) {
|
||||||
// Up arrow
|
// Escape: drop the selection
|
||||||
if (fm->grid_view) {
|
filemanager_select_single(fm, -1);
|
||||||
Rect cr_r = win->content_rect();
|
} else if (nav == 0x48 && !key.alt) {
|
||||||
int cols = (cr_r.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
// Up arrow (one row up in the grid)
|
||||||
if (cols < 1) cols = 1;
|
int target = !have_focus ? 0 : (cur >= cols ? cur - cols : cur);
|
||||||
if (fm->selected >= cols) fm->selected -= cols;
|
filemanager_move_focus(win, fm, target, key.shift);
|
||||||
} else {
|
} else if (nav == 0x50 && !key.alt) {
|
||||||
if (fm->selected > 0) fm->selected--;
|
// Down arrow (one row down in the grid)
|
||||||
}
|
int target = !have_focus ? 0 : (cur + cols < fm->entry_count ? cur + cols : cur);
|
||||||
filemanager_scroll_to_selected(win, fm);
|
filemanager_move_focus(win, fm, target, key.shift);
|
||||||
} else if (montauk::nav_scancode(key) == 0x50) {
|
} else if (nav == 0x4B && !key.alt && fm->grid_view) {
|
||||||
// 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) {
|
|
||||||
// Left arrow (grid view only)
|
// Left arrow (grid view only)
|
||||||
if (fm->selected > 0) fm->selected--;
|
filemanager_move_focus(win, fm, have_focus ? cur - 1 : 0, key.shift);
|
||||||
} else if (montauk::nav_scancode(key) == 0x4D && !key.alt && fm->grid_view) {
|
} else if (nav == 0x4D && !key.alt && fm->grid_view) {
|
||||||
// Right arrow (grid view only)
|
// 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') {
|
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||||
if (fm->selected >= 0 && fm->selected < fm->entry_count)
|
if (have_focus && filemanager_is_entry_selected(fm, cur))
|
||||||
filemanager_open_entry(fm, fm->selected);
|
filemanager_open_entry(fm, cur);
|
||||||
} else if (montauk::nav_scancode(key) == 0x53) {
|
} else if (nav == 0x53) {
|
||||||
// Delete key
|
// Delete key
|
||||||
filemanager_delete_selected(fm);
|
filemanager_delete_selected(fm);
|
||||||
} else if (key.alt && montauk::nav_scancode(key) == 0x4B) {
|
} 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) {
|
} else if (key.alt && montauk::nav_scancode(key) == 0x4D) {
|
||||||
// Alt+Right: go forward
|
// Alt+Right: go forward
|
||||||
filemanager_go_forward(fm);
|
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.
|
// misbehaving driver and bounds memory. Far above any realistic directory.
|
||||||
inline constexpr int FM_ENTRY_HARD_CAP = 8192;
|
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
|
// 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
|
// to the background worker + progress dialog. Freeing a big file rewrites the
|
||||||
// filesystem allocation bitmaps and can take seconds on real disks, which would
|
// filesystem allocation bitmaps and can take seconds on real disks, which would
|
||||||
// otherwise stall the desktop (Files runs inside desktop.elf).
|
// otherwise stall the desktop (Files runs inside desktop.elf).
|
||||||
inline constexpr uint64_t FILEMANAGER_DELETE_INLINE_MAX_BYTES = 4ull * 1024 * 1024;
|
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 {
|
struct FileManagerState {
|
||||||
char current_path[256];
|
char current_path[256];
|
||||||
FileManagerLocation history[16];
|
FileManagerLocation history[16];
|
||||||
@@ -67,7 +76,10 @@ struct FileManagerState {
|
|||||||
int64_t* entry_mtimes; // last-modified time, UTC unix seconds (0 = unknown)
|
int64_t* entry_mtimes; // last-modified time, UTC unix seconds (0 = unknown)
|
||||||
int entry_count;
|
int entry_count;
|
||||||
int entry_cap; // allocated capacity of the per-entry arrays
|
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 selected;
|
||||||
|
int sel_anchor; // fixed end of a Shift range selection (-1 = none)
|
||||||
int scroll_offset;
|
int scroll_offset;
|
||||||
bool* is_dir;
|
bool* is_dir;
|
||||||
int last_click_item;
|
int last_click_item;
|
||||||
@@ -84,7 +96,7 @@ struct FileManagerState {
|
|||||||
int clipboard_count;
|
int clipboard_count;
|
||||||
bool clipboard_is_cut;
|
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;
|
bool* multi_selected;
|
||||||
int multi_count;
|
int multi_count;
|
||||||
|
|
||||||
@@ -99,7 +111,7 @@ struct FileManagerState {
|
|||||||
bool ctx_open;
|
bool ctx_open;
|
||||||
int ctx_x, ctx_y;
|
int ctx_x, ctx_y;
|
||||||
int ctx_target_idx; // -1 = background
|
int ctx_target_idx; // -1 = background
|
||||||
int ctx_items[8];
|
int ctx_items[FM_CTX_MAX_ITEMS];
|
||||||
int ctx_item_count;
|
int ctx_item_count;
|
||||||
int ctx_hover;
|
int ctx_hover;
|
||||||
|
|
||||||
@@ -121,11 +133,22 @@ struct FileManagerState {
|
|||||||
FileManagerVirtualEntry* virtual_entries;
|
FileManagerVirtualEntry* virtual_entries;
|
||||||
int virtual_entry_count;
|
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.
|
// Background file operation (copy/move/delete) + its progress dialog.
|
||||||
void* active_fileop; // FileOpJob*, non-null while an operation runs
|
void* active_fileop; // FileOpJob*, non-null while an operation runs
|
||||||
|
|
||||||
// File Manager-owned dialogs
|
// File Manager-owned dialogs
|
||||||
void* delete_confirm_dialog;
|
void* delete_confirm_dialog;
|
||||||
|
void* open_with_dialog;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr int FM_TOOLBAR_H = 32;
|
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_CELL_H_WRAPPED = 92;
|
||||||
inline constexpr int FM_GRID_ICON = 48;
|
inline constexpr int FM_GRID_ICON = 48;
|
||||||
inline constexpr int FM_GRID_PAD = 4;
|
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
|
// Special user folders: name, icon filename, index into DesktopState arrays
|
||||||
inline constexpr int SF_COUNT = DesktopState::SPECIAL_FOLDER_COUNT;
|
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_NEW_FOLDER = 6;
|
||||||
inline constexpr int CTX_PROPERTIES = 7;
|
inline constexpr int CTX_PROPERTIES = 7;
|
||||||
inline constexpr int CTX_REFRESH = 8;
|
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) {
|
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
||||||
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
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);
|
int special_folder_index(const char* name);
|
||||||
const char* ctx_label(int action);
|
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) {
|
inline mtk::ContextMenuState filemanager_context_state(const FileManagerState* fm) {
|
||||||
if (!fm) return {};
|
if (!fm) return {};
|
||||||
@@ -199,19 +262,20 @@ inline void filemanager_build_context_items(const FileManagerState* fm,
|
|||||||
int max_items) {
|
int max_items) {
|
||||||
if (!fm || !items || max_items <= 0) return;
|
if (!fm || !items || max_items <= 0) return;
|
||||||
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
|
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++) {
|
||||||
items[i] = {ctx_label(fm->ctx_items[i]), fm->ctx_items[i], true};
|
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 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);
|
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);
|
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name);
|
||||||
const char* path_basename(const char* path);
|
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_drives(FileManagerState* fm);
|
||||||
void filemanager_read_dir(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_free_app_icons(FileManagerState* fm);
|
||||||
void filemanager_read_virtual_view(FileManagerState* fm, FileManagerVirtualViewKind view);
|
void filemanager_read_virtual_view(FileManagerState* fm, FileManagerVirtualViewKind view);
|
||||||
void filemanager_read_apps(FileManagerState* fm);
|
void filemanager_read_apps(FileManagerState* fm);
|
||||||
@@ -293,6 +362,26 @@ void filemanager_commit_pathbar(FileManagerState* fm);
|
|||||||
|
|
||||||
void filemanager_open_entry(FileManagerState* fm, int idx);
|
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) ----
|
// ---- Async file operations (copy/move/delete on a worker thread) ----
|
||||||
enum FileOpKind : int {
|
enum FileOpKind : int {
|
||||||
FILE_OP_COPY = 0,
|
FILE_OP_COPY = 0,
|
||||||
@@ -313,6 +402,15 @@ int filemanager_grid_index_at(FileManagerState* fm, int cols, int x, int rel_y);
|
|||||||
|
|
||||||
// Selection helpers
|
// Selection helpers
|
||||||
void filemanager_clear_multi_selection(FileManagerState* fm);
|
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
|
// Fill out with selected entry indices. Returns count. Uses multi_selected if
|
||||||
// non-empty, otherwise falls back to fm->selected (0 or 1 results).
|
// non-empty, otherwise falls back to fm->selected (0 or 1 results).
|
||||||
int filemanager_collect_selection(const FileManagerState* fm, int* out, int max);
|
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) {
|
static void filemanager_reset_list_state(FileManagerState* fm) {
|
||||||
fm->selected = -1;
|
fm->selected = -1;
|
||||||
|
fm->sel_anchor = -1;
|
||||||
|
fm->typeahead_len = 0;
|
||||||
fm->scroll_offset = 0;
|
fm->scroll_offset = 0;
|
||||||
fm->scrollbar.scroll_offset = 0;
|
fm->scrollbar.scroll_offset = 0;
|
||||||
fm->last_click_item = -1;
|
fm->last_click_item = -1;
|
||||||
@@ -344,42 +346,113 @@ void filemanager_read_dir(FileManagerState* fm) {
|
|||||||
if (fm->entry_count >= FM_ENTRY_HARD_CAP) break;
|
if (fm->entry_count >= FM_ENTRY_HARD_CAP) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort: directories first, then alphabetical (case-insensitive)
|
filemanager_reset_list_state(fm);
|
||||||
for (int i = 1; i < fm->entry_count; i++) {
|
filemanager_sort_entries(fm);
|
||||||
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;
|
// Folders always lead; within each group the active column decides, with the
|
||||||
while (j >= 0) {
|
// name as tie-breaker so equal sizes/types/times still list predictably.
|
||||||
bool swap = false;
|
static int filemanager_compare_entries(const FileManagerState* fm, int a, int b) {
|
||||||
if (tmp_isdir && !fm->is_dir[j]) {
|
if (fm->is_dir[a] != fm->is_dir[b]) return fm->is_dir[a] ? -1 : 1;
|
||||||
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]);
|
int c = 0;
|
||||||
fm->entry_types[j + 1] = fm->entry_types[j];
|
switch (fm->sort_key) {
|
||||||
fm->entry_sizes[j + 1] = fm->entry_sizes[j];
|
case FM_SORT_SIZE:
|
||||||
fm->entry_mtimes[j + 1] = fm->entry_mtimes[j];
|
if (fm->entry_sizes[a] != fm->entry_sizes[b])
|
||||||
fm->is_dir[j + 1] = fm->is_dir[j];
|
c = fm->entry_sizes[a] < fm->entry_sizes[b] ? -1 : 1;
|
||||||
j--;
|
break;
|
||||||
}
|
case FM_SORT_MODIFIED:
|
||||||
montauk::strcpy(fm->entry_names[j + 1], tmp_name);
|
if (fm->entry_mtimes[a] != fm->entry_mtimes[b])
|
||||||
fm->entry_types[j + 1] = tmp_type;
|
c = fm->entry_mtimes[a] < fm->entry_mtimes[b] ? -1 : 1;
|
||||||
fm->entry_sizes[j + 1] = tmp_size;
|
break;
|
||||||
fm->entry_mtimes[j + 1] = tmp_mtime;
|
case FM_SORT_TYPE: {
|
||||||
fm->is_dir[j + 1] = tmp_isdir;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
filemanager_reset_list_state(fm);
|
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) {
|
void filemanager_free_app_icons(FileManagerState* fm) {
|
||||||
|
|||||||
@@ -15,6 +15,91 @@ void filemanager_clear_multi_selection(FileManagerState* fm) {
|
|||||||
fm->multi_count = 0;
|
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) {
|
int filemanager_collect_selection(const FileManagerState* fm, int* out, int max) {
|
||||||
if (!fm || !out || max <= 0) return 0;
|
if (!fm || !out || max <= 0) return 0;
|
||||||
int n = 0;
|
int n = 0;
|
||||||
@@ -46,10 +131,64 @@ const char* ctx_label(int action) {
|
|||||||
case CTX_NEW_FOLDER: return "New Folder";
|
case CTX_NEW_FOLDER: return "New Folder";
|
||||||
case CTX_PROPERTIES: return "Properties";
|
case CTX_PROPERTIES: return "Properties";
|
||||||
case CTX_REFRESH: return "Refresh";
|
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 "?";
|
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) {
|
bool str_ends_with(const char* s, const char* suffix) {
|
||||||
int slen = montauk::slen(s);
|
int slen = montauk::slen(s);
|
||||||
int suflen = montauk::slen(suffix);
|
int suflen = montauk::slen(suffix);
|
||||||
@@ -64,42 +203,64 @@ bool str_ends_with(const char* s, const char* suffix) {
|
|||||||
return true;
|
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) {
|
int detect_file_type(const char* name, bool is_dir) {
|
||||||
if (is_dir) return 1;
|
if (is_dir) return 1;
|
||||||
if (str_ends_with(name, ".elf")) return 2;
|
if (str_ends_with(name, ".elf")) return 2;
|
||||||
return 0;
|
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) {
|
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name) {
|
||||||
montauk::strcpy(out, dir);
|
montauk::strcpy(out, dir);
|
||||||
int plen = montauk::slen(out);
|
int plen = montauk::slen(out);
|
||||||
|
|||||||
@@ -281,28 +281,28 @@ void filemanager_open_entry(FileManagerState* fm, int idx) {
|
|||||||
return;
|
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];
|
char fullpath[512];
|
||||||
filemanager_build_fullpath(fullpath, 512, fm->current_path, fm->entry_names[idx]);
|
filemanager_build_fullpath(fullpath, 512, fm->current_path, name);
|
||||||
if (is_image_file(fm->entry_names[idx])) {
|
if (str_ends_with(name, ".elf")) {
|
||||||
montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath);
|
desktop_spawn_app(ds, fullpath);
|
||||||
} else if (is_font_file(fm->entry_names[idx])) {
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
} // 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) {
|
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols) {
|
||||||
if (!fm || cols <= 0 || row < 0) return FM_GRID_CELL_H;
|
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;
|
if (cell_y + row_h <= list_y || cell_y >= c.h) continue;
|
||||||
|
|
||||||
// Selection highlight (single or multi)
|
// Selection highlight (single or multi)
|
||||||
bool cell_selected = (i == fm->selected) ||
|
bool cell_selected = filemanager_is_entry_selected(fm, i);
|
||||||
(fm->multi_selected && fm->multi_selected[i]);
|
|
||||||
if (cell_selected) {
|
if (cell_selected) {
|
||||||
int sy = gui_max(cell_y, list_y);
|
int sy = gui_max(cell_y, list_y);
|
||||||
int sh = gui_min(cell_y + row_h, c.h) - sy;
|
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;
|
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));
|
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
||||||
|
|
||||||
int name_col_x = 8;
|
FileManagerListColumns cols = filemanager_list_columns(c.w);
|
||||||
int size_col_x = c.w - FM_SCROLLBAR_W - 120;
|
// Sorting only applies to real directories; Computer and the virtual
|
||||||
int type_col_x = c.w - FM_SCROLLBAR_W - 60;
|
// 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);
|
draw_column_header(c, cols.name_x, header_y, "Name",
|
||||||
if (size_col_x > 100)
|
sortable && fm->sort_key == FM_SORT_NAME, fm->sort_desc, dim);
|
||||||
c.text(size_col_x, header_y + 2, "Size", dim);
|
if (cols.modified_x >= 0)
|
||||||
if (type_col_x > 160)
|
draw_column_header(c, cols.modified_x, header_y, "Modified",
|
||||||
c.text(type_col_x, header_y + 2, "Type", dim);
|
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
|
// Header separator
|
||||||
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
|
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
|
||||||
|
|
||||||
// Column separator lines
|
// Column separator lines
|
||||||
if (size_col_x > 100) {
|
int first_col_x = cols.modified_x >= 0 ? cols.modified_x
|
||||||
c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER);
|
: 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 ----
|
// ---- File entries ----
|
||||||
int list_y = header_y + FM_HEADER_H;
|
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;
|
if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue;
|
||||||
|
|
||||||
// Highlight selected (single or marquee multi-selection)
|
// Highlight selected (single or marquee multi-selection)
|
||||||
bool row_selected = (i == fm->selected) ||
|
bool row_selected = filemanager_is_entry_selected(fm, i);
|
||||||
(fm->multi_selected && fm->multi_selected[i]);
|
|
||||||
if (row_selected) {
|
if (row_selected) {
|
||||||
int sy = gui_max(iy, list_y);
|
int sy = gui_max(iy, list_y);
|
||||||
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
|
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) {
|
if (fm->rename_active && fm->rename_idx == i && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||||
// Rename textbox inline
|
// 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.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.rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::ACCENT);
|
||||||
c.text(tx, ty, fm->rename_buf, colors::TEXT_COLOR);
|
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) {
|
if (ty >= list_y && ty + fm_sfh <= c.h) {
|
||||||
// Truncate name to fit the name column width
|
// Truncate name to fit the name column width
|
||||||
const char* name = fm->entry_names[i];
|
const char* name = fm->entry_names[i];
|
||||||
int name_max_w = size_col_x - tx - 8;
|
int name_max_w = cols.name_right - tx;
|
||||||
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
|
|
||||||
char label[64];
|
char label[64];
|
||||||
montauk::utf8::copy(label, 64, name);
|
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
|
// 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];
|
char size_str[16];
|
||||||
format_size(size_str, fm->entry_sizes[i]);
|
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
|
// Type
|
||||||
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
|
if (cols.type_x >= 0 && row_text_visible) {
|
||||||
const char* type_str = "File";
|
char type_str[24];
|
||||||
if (fm->entry_types[i] == FM_ENTRY_VIRTUAL_ITEM) {
|
filemanager_type_label(fm, i, type_str, sizeof(type_str));
|
||||||
type_str = fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION
|
char type_fit[24];
|
||||||
? "Setting"
|
fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str,
|
||||||
: "App";
|
c.w - FM_SCROLLBAR_W - cols.type_x - 4);
|
||||||
}
|
c.text(cols.type_x, ty, type_fit, dim);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,10 +660,10 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
theme.border = colors::BORDER;
|
theme.border = colors::BORDER;
|
||||||
theme.text = colors::TEXT_COLOR;
|
theme.text = colors::TEXT_COLOR;
|
||||||
|
|
||||||
mtk::ContextMenuItem items[8];
|
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
||||||
filemanager_build_context_items(fm, items, 8);
|
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
||||||
mtk::draw_context_menu(c, filemanager_context_state(fm), 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -887,6 +887,8 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
|||||||
montauk::abi::KeyEvent translated_key = key;
|
montauk::abi::KeyEvent translated_key = key;
|
||||||
montauk::keyboard::translate(ds->keyboard, &translated_key);
|
montauk::keyboard::translate(ds->keyboard, &translated_key);
|
||||||
const montauk::abi::KeyEvent& routed_key = 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) {
|
if (ds->screen_locked) {
|
||||||
handle_lock_keyboard(ds, routed_key);
|
handle_lock_keyboard(ds, routed_key);
|
||||||
|
|||||||
@@ -181,6 +181,27 @@ void desktop_scan_apps(DesktopState* ds) {
|
|||||||
snprintf(item->binary_path, sizeof(item->binary_path),
|
snprintf(item->binary_path, sizeof(item->binary_path),
|
||||||
"0:/apps/%s/%s", dirname, binary);
|
"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
|
// Load icon from app directory
|
||||||
item->icon = {};
|
item->icon = {};
|
||||||
item->icon_path[0] = '\0';
|
item->icon_path[0] = '\0';
|
||||||
|
|||||||
@@ -12,3 +12,7 @@ visible = false
|
|||||||
# starting directory. Don't hand it home_dir when launched from the Apps
|
# 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).
|
# folder (it would try to parse the directory path as a font).
|
||||||
pass_home_dir = false
|
pass_home_dir = false
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
|
types = ["ttf"]
|
||||||
@@ -6,3 +6,7 @@ icon = "image-viewer.svg"
|
|||||||
[menu]
|
[menu]
|
||||||
category = "Media"
|
category = "Media"
|
||||||
visible = false
|
visible = false
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
|
types = ["jpg", "jpeg", "png", "bmp", "gif", "tga", "svg"]
|
||||||
@@ -9,3 +9,7 @@ visible = true
|
|||||||
|
|
||||||
[launch]
|
[launch]
|
||||||
pass_home_dir = false
|
pass_home_dir = false
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
|
types = ["mp3", "wav"]
|
||||||
@@ -6,3 +6,7 @@ icon = "pdf-viewer.svg"
|
|||||||
[menu]
|
[menu]
|
||||||
category = "Productivity"
|
category = "Productivity"
|
||||||
visible = false
|
visible = false
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
|
types = ["pdf"]
|
||||||
@@ -6,3 +6,7 @@ icon = "spreadsheet.svg"
|
|||||||
[menu]
|
[menu]
|
||||||
category = "Productivity"
|
category = "Productivity"
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
|
types = ["mss"]
|
||||||
@@ -9,3 +9,7 @@ visible = true
|
|||||||
|
|
||||||
[launch]
|
[launch]
|
||||||
pass_home_dir = false
|
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"]
|
||||||
@@ -6,3 +6,7 @@ icon = "video.svg"
|
|||||||
[menu]
|
[menu]
|
||||||
category = "Media"
|
category = "Media"
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
|
types = ["avi"]
|
||||||
@@ -6,3 +6,7 @@ icon = "wordprocessor.svg"
|
|||||||
[menu]
|
[menu]
|
||||||
category = "Productivity"
|
category = "Productivity"
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Extensions Files opens with this app (lowercase, no dot)
|
||||||
|
types = ["mwp"]
|
||||||
Reference in new issue
Block a user