From bb176f21939fc2f96d72e722265411921a93bcd3 Mon Sep 17 00:00:00 2001 From: boreddevnl Date: Sun, 12 Apr 2026 21:46:28 +0200 Subject: [PATCH] FEAT: Lumos file searcher --- src/dev/ps2.c | 4 + src/dev/ps2.h | 2 + src/sys/file_index.c | 477 +++++++++++++++++++++++++++++++++++++++++++ src/sys/file_index.h | 50 +++++ src/wm/cmd.c | 11 - src/wm/explorer.c | 2 +- src/wm/explorer.h | 9 +- src/wm/graphics.c | 3 + src/wm/wm.c | 337 +++++++++++++++++++++++++++++- src/wm/wm.h | 18 ++ 10 files changed, 891 insertions(+), 22 deletions(-) create mode 100644 src/sys/file_index.c create mode 100644 src/sys/file_index.h diff --git a/src/dev/ps2.c b/src/dev/ps2.c index 727d4a2..356a363 100644 --- a/src/dev/ps2.c +++ b/src/dev/ps2.c @@ -269,3 +269,7 @@ uint64_t mouse_handler(registers_t *regs) { void ps2_init(void) { mouse_init(); } + +bool ps2_shift_pressed(void) { + return shift_pressed; +} diff --git a/src/dev/ps2.h b/src/dev/ps2.h index f8926cc..01a0a88 100644 --- a/src/dev/ps2.h +++ b/src/dev/ps2.h @@ -13,4 +13,6 @@ uint64_t timer_handler(registers_t *regs); uint64_t keyboard_handler(registers_t *regs); uint64_t mouse_handler(registers_t *regs); +bool ps2_shift_pressed(void); + #endif diff --git a/src/sys/file_index.c b/src/sys/file_index.c new file mode 100644 index 0000000..42c8979 --- /dev/null +++ b/src/sys/file_index.c @@ -0,0 +1,477 @@ +// Copyright (c) 2023-2026 Chris (boreddevnl) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// This header needs to maintain in any file it is present in, as per the GPL license terms. +#include "file_index.h" +#include "vfs.h" +#include "memory_manager.h" +#include "spinlock.h" +#include + +static file_index_t g_file_index = {0}; +static spinlock_t g_index_lock = SPINLOCK_INIT; +static bool g_index_valid = false; + +static int str_len(const char *s) { + int n = 0; + while (s[n]) n++; + return n; +} + +static void str_copy(char *d, const char *s) { + while ((*d++ = *s++)); +} + +static int str_cmp(const char *a, const char *b) { + while (*a && *a == *b) { a++; b++; } + return (unsigned char)*a - (unsigned char)*b; +} + +static void str_cat(char *d, const char *s) { + while (*d) d++; + str_copy(d, s); +} + +static bool str_starts_with(const char *str, const char *prefix) { + while (*prefix) { + if (*str++ != *prefix++) return false; + } + return true; +} + +static int fuzzy_match_score(const char *query, const char *filename) { + if (!query || !filename) return 0; + + int score = 0; + int query_idx = 0; + int consecutive = 0; + + for (int i = 0; filename[i] && query_idx < 256; i++) { + char fc = filename[i]; + char qc = query[query_idx]; + + if (fc >= 'A' && fc <= 'Z') fc += 32; + if (qc >= 'A' && qc <= 'Z') qc += 32; + + if (fc == qc) { + score += 10; + consecutive++; + if (consecutive > 1) score += 5; + query_idx++; + } else { + consecutive = 0; + } + } + + if (query_idx < str_len(query)) { + return 0; + } + + if (str_starts_with(filename, query)) { + score += 20; + } + + return score; +} + +static void index_walk_directory(const char *path, int depth) { + if (depth > 16 || g_file_index.count >= FILE_INDEX_MAX_ENTRIES) { + return; + } + + if (str_starts_with(path, "/proc") || + str_starts_with(path, "/sys") || + str_starts_with(path, "/dev")) { + return; + } + + vfs_dirent_t *entries = (vfs_dirent_t *)kmalloc(sizeof(vfs_dirent_t) * 1024); + if (!entries) { + return; + } + + int count = vfs_list_directory(path, entries, 1024); + + if (count <= 0 || count > 1024) { + kfree(entries); + return; + } + + for (int i = 0; i < count; i++) { + if (g_file_index.count >= FILE_INDEX_MAX_ENTRIES) { + break; + } + + vfs_dirent_t *entry = &entries[i]; + if (!entry) continue; + + + if (!entry->name || entry->name[0] == 0) { + continue; + } + + + if (str_cmp(entry->name, ".color") == 0 || + str_cmp(entry->name, ".origin") == 0 || + str_cmp(entry->name, ".") == 0 || + str_cmp(entry->name, "..") == 0) { + continue; + } + + char full_path[FILE_INDEX_MAX_PATH]; + int path_len = 0; + + + for (int j = 0; path[j] && path_len < FILE_INDEX_MAX_PATH - 1; j++) { + full_path[path_len++] = path[j]; + } + + + if (path_len > 0 && full_path[path_len - 1] != '/' && path_len < FILE_INDEX_MAX_PATH - 1) { + full_path[path_len++] = '/'; + } + + + for (int j = 0; entry->name[j] && path_len < FILE_INDEX_MAX_PATH - 1; j++) { + full_path[path_len++] = entry->name[j]; + } + full_path[path_len] = 0; + + + if (path_len >= FILE_INDEX_MAX_PATH - 1) { + continue; + } + + + file_index_entry_t *idx_entry = &g_file_index.entries[g_file_index.count]; + str_copy(idx_entry->path, full_path); + idx_entry->size = entry->size; + idx_entry->mod_time_low = entry->write_date; + idx_entry->mod_time_high = entry->write_time; + idx_entry->is_directory = entry->is_directory; + + g_file_index.count++; + + + if (entry->is_directory && !str_starts_with(full_path, "/proc") && + !str_starts_with(full_path, "/sys") && !str_starts_with(full_path, "/dev")) { + index_walk_directory(full_path, depth + 1); + } + } + + + kfree(entries); +} + + +void file_index_init(void) { + g_file_index.count = 0; + g_file_index.capacity = FILE_INDEX_MAX_ENTRIES; + g_index_valid = false; +} + + +bool file_index_build(void) { + + + + + g_file_index.count = 0; + + + const char *safe_paths[] = {"/root", "/bin", "/Library", "/docs", NULL}; + + + for (int p = 0; safe_paths[p] != NULL && g_file_index.count < FILE_INDEX_MAX_ENTRIES; p++) { + index_walk_directory(safe_paths[p], 0); + } + + + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + g_index_valid = true; + spinlock_release_irqrestore(&g_index_lock, flags); + + + file_index_save(); + + return true; +} + + +bool file_index_load(void) { + vfs_file_t *file = vfs_open(FILE_INDEX_CACHE_PATH, "r"); + if (!file) { + return false; + } + + + uint32_t version = 0; + if (vfs_read(file, &version, sizeof(version)) != sizeof(version)) { + vfs_close(file); + return false; + } + + + if (version != FILE_INDEX_VERSION) { + vfs_close(file); + return false; + } + + + int count = 0; + if (vfs_read(file, &count, sizeof(count)) != sizeof(count)) { + vfs_close(file); + return false; + } + + + if (count < 0 || count > FILE_INDEX_MAX_ENTRIES) { + vfs_close(file); + return false; + } + + + file_index_entry_t temp_entries[FILE_INDEX_MAX_ENTRIES]; + for (int i = 0; i < count; i++) { + file_index_entry_t *entry = &temp_entries[i]; + + int bytes_read = vfs_read(file, entry->path, FILE_INDEX_MAX_PATH); + if (bytes_read != FILE_INDEX_MAX_PATH) { + vfs_close(file); + return false; + } + + if (vfs_read(file, &entry->size, sizeof(entry->size)) != sizeof(entry->size)) { + vfs_close(file); + return false; + } + + if (vfs_read(file, &entry->mod_time_low, sizeof(entry->mod_time_low)) != sizeof(entry->mod_time_low)) { + vfs_close(file); + return false; + } + + if (vfs_read(file, &entry->mod_time_high, sizeof(entry->mod_time_high)) != sizeof(entry->mod_time_high)) { + vfs_close(file); + return false; + } + + if (vfs_read(file, &entry->is_directory, sizeof(entry->is_directory)) != sizeof(entry->is_directory)) { + vfs_close(file); + return false; + } + } + + vfs_close(file); + + + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + g_file_index.count = 0; + for (int i = 0; i < count; i++) { + g_file_index.entries[i] = temp_entries[i]; + g_file_index.count++; + } + g_index_valid = true; + spinlock_release_irqrestore(&g_index_lock, flags); + + return true; +} + +bool file_index_save(void) { + if (!vfs_mkdir("/Library")) { + } + if (!vfs_mkdir("/Library/Index")) { + } + + vfs_file_t *file = vfs_open(FILE_INDEX_CACHE_PATH, "w"); + if (!file) { + return false; + } + + + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + int count = g_file_index.count; + file_index_entry_t entries[FILE_INDEX_MAX_ENTRIES]; + for (int i = 0; i < count; i++) { + entries[i] = g_file_index.entries[i]; + } + spinlock_release_irqrestore(&g_index_lock, flags); + + + uint32_t version = FILE_INDEX_VERSION; + if (vfs_write(file, &version, sizeof(version)) != sizeof(version)) { + vfs_close(file); + return false; + } + + if (vfs_write(file, &count, sizeof(count)) != sizeof(count)) { + vfs_close(file); + return false; + } + + for (int i = 0; i < count; i++) { + file_index_entry_t *entry = &entries[i]; + + if (vfs_write(file, entry->path, FILE_INDEX_MAX_PATH) != FILE_INDEX_MAX_PATH) { + vfs_close(file); + return false; + } + + if (vfs_write(file, &entry->size, sizeof(entry->size)) != sizeof(entry->size)) { + vfs_close(file); + return false; + } + + if (vfs_write(file, &entry->mod_time_low, sizeof(entry->mod_time_low)) != sizeof(entry->mod_time_low)) { + vfs_close(file); + return false; + } + + if (vfs_write(file, &entry->mod_time_high, sizeof(entry->mod_time_high)) != sizeof(entry->mod_time_high)) { + vfs_close(file); + return false; + } + + if (vfs_write(file, &entry->is_directory, sizeof(entry->is_directory)) != sizeof(entry->is_directory)) { + vfs_close(file); + return false; + } + } + + vfs_close(file); + return true; +} + +int file_index_find_fuzzy(const char *query, file_index_result_t *results, int max_results) { + if (!query || !results || max_results <= 0) { + return 0; + } + + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + + int result_count = 0; + + for (int i = 0; i < g_file_index.count && result_count < max_results; i++) { + if (i < 0 || i >= FILE_INDEX_MAX_ENTRIES) { + break; + } + + const char *path = g_file_index.entries[i].path; + if (!path || path[0] == 0) { + continue; + } + + const char *filename = path; + + for (int j = 0; path[j]; j++) { + if (path[j] == '/') { + filename = &path[j + 1]; + } + } + + if (!filename || filename[0] == 0) { + continue; + } + + int score = fuzzy_match_score(query, filename); + + if (score > 0) { + results[result_count].entry = g_file_index.entries[i]; + results[result_count].match_score = score; + result_count++; + } + } + + spinlock_release_irqrestore(&g_index_lock, flags); + + for (int i = 0; i < result_count; i++) { + for (int j = i + 1; j < result_count; j++) { + if (results[j].match_score > results[i].match_score) { + file_index_result_t tmp = results[i]; + results[i] = results[j]; + results[j] = tmp; + } + } + } + + return result_count; +} + +bool file_index_add_entry(const char *path, uint32_t size, uint32_t mod_time_low, + uint32_t mod_time_high, bool is_dir) { + if (!path || g_file_index.count >= FILE_INDEX_MAX_ENTRIES) { + return false; + } + + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + + for (int i = 0; i < g_file_index.count; i++) { + if (str_cmp(g_file_index.entries[i].path, path) == 0) { + g_file_index.entries[i].size = size; + g_file_index.entries[i].mod_time_low = mod_time_low; + g_file_index.entries[i].mod_time_high = mod_time_high; + spinlock_release_irqrestore(&g_index_lock, flags); + return true; + } + } + + file_index_entry_t *entry = &g_file_index.entries[g_file_index.count]; + str_copy(entry->path, path); + entry->size = size; + entry->mod_time_low = mod_time_low; + entry->mod_time_high = mod_time_high; + entry->is_directory = is_dir; + + g_file_index.count++; + + spinlock_release_irqrestore(&g_index_lock, flags); + return true; +} + +bool file_index_remove_entry(const char *path) { + if (!path) { + return false; + } + + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + + for (int i = 0; i < g_file_index.count; i++) { + if (str_cmp(g_file_index.entries[i].path, path) == 0) { + for (int j = i; j < g_file_index.count - 1; j++) { + g_file_index.entries[j] = g_file_index.entries[j + 1]; + } + g_file_index.count--; + spinlock_release_irqrestore(&g_index_lock, flags); + return true; + } + } + + spinlock_release_irqrestore(&g_index_lock, flags); + return false; +} + +int file_index_get_entry_count(void) { + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + int count = g_file_index.count; + spinlock_release_irqrestore(&g_index_lock, flags); + return count; +} + +void file_index_clear(void) { + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + g_file_index.count = 0; + g_index_valid = false; + spinlock_release_irqrestore(&g_index_lock, flags); +} + +void file_index_invalidate_cache(void) { + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + g_index_valid = false; + spinlock_release_irqrestore(&g_index_lock, flags); +} + +bool file_index_is_valid(void) { + uint64_t flags = spinlock_acquire_irqsave(&g_index_lock); + bool valid = g_index_valid; + spinlock_release_irqrestore(&g_index_lock, flags); + return valid; +} diff --git a/src/sys/file_index.h b/src/sys/file_index.h new file mode 100644 index 0000000..8e10edc --- /dev/null +++ b/src/sys/file_index.h @@ -0,0 +1,50 @@ +// Copyright (c) 2023-2026 Chris (boreddevnl) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// This header needs to maintain in any file it is present in, as per the GPL license terms. +#ifndef FILE_INDEX_H +#define FILE_INDEX_H + +#include +#include +#include + +#define FILE_INDEX_MAX_ENTRIES 50000 +#define FILE_INDEX_MAX_PATH 1024 +#define FILE_INDEX_CACHE_PATH "/Library/Index/file_index.dat" +#define FILE_INDEX_VERSION 1 + +typedef struct { + char path[FILE_INDEX_MAX_PATH]; + uint32_t size; + uint32_t mod_time_low; + uint32_t mod_time_high; + bool is_directory; +} file_index_entry_t; + +typedef struct { + file_index_entry_t entry; + int match_score; +} file_index_result_t; + +typedef struct { + file_index_entry_t entries[FILE_INDEX_MAX_ENTRIES]; + int count; + int capacity; +} file_index_t; + +void file_index_init(void); +bool file_index_build(void); +bool file_index_load(void); +bool file_index_save(void); + +int file_index_find_fuzzy(const char *query, file_index_result_t *results, int max_results); + +bool file_index_add_entry(const char *path, uint32_t size, uint32_t mod_time_low, uint32_t mod_time_high, bool is_dir); +bool file_index_remove_entry(const char *path); + +int file_index_get_entry_count(void); +void file_index_clear(void); +void file_index_invalidate_cache(void); +bool file_index_is_valid(void); + +#endif diff --git a/src/wm/cmd.c b/src/wm/cmd.c index eba084a..1e945b8 100644 --- a/src/wm/cmd.c +++ b/src/wm/cmd.c @@ -2273,17 +2273,6 @@ static void create_ramfs_files(void) { } fat32_close(fh); } - - fh = fat32_open("root/Apps/DOOM.c", "w"); - if (fh) { - const char *content = - "int main(){\n" - " puts(\"To DOOM, or not to DOOM.\\n\");\n" - " puts(\"-Me\\n\");\n" - "}\n"; - fat32_write(fh, (void *)content, cmd_strlen(content)); - fat32_close(fh); - } } diff --git a/src/wm/explorer.c b/src/wm/explorer.c index b2a21c7..6ba831a 100644 --- a/src/wm/explorer.c +++ b/src/wm/explorer.c @@ -805,7 +805,7 @@ void explorer_open_directory(const char *path) { explorer_create_window(path); } -static void explorer_open_target(const char *path) { +void explorer_open_target(const char *path) { if (vfs_is_directory(path)) { explorer_open_directory(path); } else { diff --git a/src/wm/explorer.h b/src/wm/explorer.h index 835183f..e6a1997 100644 --- a/src/wm/explorer.h +++ b/src/wm/explorer.h @@ -70,10 +70,10 @@ typedef struct { void explorer_init(void); void explorer_reset(void); -void explorer_open_directory(const char *path); // Creates a NEW window +void explorer_open_directory(const char *path); +void explorer_open_target(const char *path); + -// Drag and Drop support -// This now needs to find WHICH explorer window is under the mouse bool explorer_get_file_at(int screen_x, int screen_y, char *out_path, bool *is_dir); void explorer_import_file(Window *win, const char *source_path); // To focused or default void explorer_import_file_to(Window *win, const char *source_path, const char *dest_dir); @@ -81,18 +81,15 @@ void explorer_refresh(Window *win); void explorer_refresh_all(void); void explorer_clear_click_state(Window *win); -// String Helpers size_t explorer_strlen(const char *str); void explorer_strcpy(char *dest, const char *src); void explorer_strcat(char *dest, const char *src); -// Clipboard (System-wide) void explorer_clipboard_copy(const char *path); void explorer_clipboard_cut(const char *path); void explorer_clipboard_paste(Window *win, const char *dest_dir); bool explorer_clipboard_has_content(void); -// File Operations bool explorer_delete_permanently(const char *path); bool explorer_delete_recursive(const char *path); void explorer_create_shortcut(Window *win, const char *target_path); diff --git a/src/wm/graphics.c b/src/wm/graphics.c index 3e2f45f..6986a28 100644 --- a/src/wm/graphics.c +++ b/src/wm/graphics.c @@ -402,8 +402,11 @@ void draw_rounded_rect_blurred(int x, int y, int w, int h, int radius, uint32_t for (int r = 0; r < h; r++) { int g_y = y + r; + if (g_y < 0 || g_y >= sh) continue; + for (int c = 0; c < w; c++) { int g_x = x + c; + if (g_x < 0 || g_x >= sw) continue; int r_sum = 0, g_sum = 0, b_sum = 0, count = 0; int start_kx = g_x - blur_radius; diff --git a/src/wm/wm.c b/src/wm/wm.c index 16764e4..d3dfa18 100644 --- a/src/wm/wm.c +++ b/src/wm/wm.c @@ -13,6 +13,8 @@ #include #include "wallpaper.h" #include "fat32.h" +#include "file_index.h" +#include "../dev/ps2.h" #define STBI_NO_STDIO #include "userland/stb_image.h" #include "memory_manager.h" @@ -87,6 +89,125 @@ static int notif_x_offset = 420; // Starts offscreen static bool notif_active = false; extern bool ps2_ctrl_pressed; +static spotlight_state_t spotlight_state = {0}; +static bool spotlight_index_built = false; // Track if index has been built + +static bool force_redraw = true; + +static void spotlight_update_search(void) { + // Note: Index must already be loaded/built before searching + // Don't build here - this runs on every keystroke! + + int query_hash = 0; + for (int i = 0; spotlight_state.search_query[i] && i < 256; i++) { + query_hash = (query_hash * 31) + spotlight_state.search_query[i]; + } + + if (query_hash == spotlight_state.last_query_hash) { + return; + } + + spotlight_state.last_query_hash = query_hash; + spotlight_state.result_count = 0; + spotlight_state.selected_index = 0; + + if (spotlight_state.search_len == 0) { + return; + } + + file_index_result_t results[SPOTLIGHT_MAX_RESULTS]; + int count = file_index_find_fuzzy(spotlight_state.search_query, results, SPOTLIGHT_MAX_RESULTS); + + spotlight_state.result_count = count; + for (int i = 0; i < count && i < SPOTLIGHT_MAX_RESULTS; i++) { + spotlight_state.results[i] = results[i]; + } + + int sw = get_screen_width(); + int sh = get_screen_height(); + int modal_height = SPOTLIGHT_SEARCH_HEIGHT + (spotlight_state.result_count * SPOTLIGHT_RESULT_HEIGHT) + 10; + int modal_y = (sh * 2 / 5) - (modal_height / 2); + + graphics_mark_dirty(0, 0, sw, sh); + force_redraw = true; +} + +static void wm_spotlight_handle_key(char c) { + if (c == 27) { + spotlight_state.visible = false; + force_redraw = true; + return; + } + + if (c == '\n') { + if (spotlight_state.result_count > 0 && spotlight_state.selected_index < spotlight_state.result_count) { + const char *file_path = spotlight_state.results[spotlight_state.selected_index].entry.path; + explorer_open_target(file_path); + spotlight_state.visible = false; + force_redraw = true; + } + return; + } + + if (c == 17) { + if (spotlight_state.selected_index > 0) { + spotlight_state.selected_index--; + force_redraw = true; + } + return; + } + + if (c == 18) { + if (spotlight_state.selected_index < spotlight_state.result_count - 1) { + spotlight_state.selected_index++; + force_redraw = true; + } + return; + } + + if (c == '\b' || c == 127) { + if (spotlight_state.cursor_pos > 0) { + for (int i = spotlight_state.cursor_pos - 1; i < spotlight_state.search_len; i++) { + spotlight_state.search_query[i] = spotlight_state.search_query[i + 1]; + } + spotlight_state.search_len--; + spotlight_state.cursor_pos--; + spotlight_state.search_query[spotlight_state.search_len] = 0; + spotlight_update_search(); + force_redraw = true; + } + return; + } + + if (c == 19) { + if (spotlight_state.cursor_pos > 0) { + spotlight_state.cursor_pos--; + force_redraw = true; + } + return; + } + + if (c == 20) { + if (spotlight_state.cursor_pos < spotlight_state.search_len) { + spotlight_state.cursor_pos++; + force_redraw = true; + } + return; + } + + if (c >= 32 && c <= 126 && spotlight_state.search_len < 255) { + for (int i = spotlight_state.search_len; i >= spotlight_state.cursor_pos; i--) { + spotlight_state.search_query[i + 1] = spotlight_state.search_query[i]; + } + spotlight_state.search_query[spotlight_state.cursor_pos] = c; + spotlight_state.search_len++; + spotlight_state.cursor_pos++; + spotlight_state.search_query[spotlight_state.search_len] = 0; + spotlight_update_search(); + force_redraw = true; + } +} + // Dragging State static bool is_dragging = false; static bool is_resizing = false; @@ -118,8 +239,8 @@ static Window *drag_src_win = NULL; static Window *all_windows[32]; static int window_count = 0; -// Redraw system -static bool force_redraw = true; +// Redraw system (moved to be with spotlight state) +// static bool force_redraw = true; (moved earlier) static uint32_t timer_ticks = 0; // Cursor state @@ -1309,6 +1430,144 @@ bool rect_contains(int x, int y, int w, int h, int px, int py) { return px >= x && px < x + w && py >= y && py < y + h; } +static void wm_render_spotlight(int y_start, int y_end, DirtyRect dirty) { + if (!spotlight_state.visible) { + return; + } + + int sw = get_screen_width(); + int sh = get_screen_height(); + + int modal_width = SPOTLIGHT_MODAL_WIDTH; + int modal_height = SPOTLIGHT_SEARCH_HEIGHT + (spotlight_state.result_count * SPOTLIGHT_RESULT_HEIGHT) + 10; + + if (spotlight_state.result_count == 0 && spotlight_state.search_len > 0) { + modal_height = SPOTLIGHT_SEARCH_HEIGHT + SPOTLIGHT_RESULT_HEIGHT + 20; + } + + int modal_x = (sw - modal_width) / 2; + int modal_y = (sh * 2 / 5) - (modal_height / 2); + + if (modal_y + modal_height <= y_start || modal_y >= y_end) { + return; + } + + // Draw modal background - with subtle blur effect + draw_rounded_rect_blurred(modal_x, modal_y, modal_width, modal_height, 12, COLOR_DARK_PANEL, 1, 220); + + int search_x = modal_x + 8; + int search_y = modal_y + 8; + int search_width = modal_width - 16; + int search_height = 32; + + draw_rounded_rect_filled(search_x, search_y, search_width, search_height, 6, COLOR_DARK_BG); + + if (spotlight_state.search_len > 0) { + draw_string(search_x + 8, search_y + 8, spotlight_state.search_query, COLOR_DARK_TEXT); + } else { + draw_string(search_x + 8, search_y + 8, "Search files...", COLOR_DKGRAY); + } + + if (spotlight_state.cursor_pos <= spotlight_state.search_len && spotlight_state.search_len > 0) { + ttf_font_t *ttf = graphics_get_current_ttf(); + char temp_query[256]; + for (int i = 0; i < spotlight_state.cursor_pos && i < 255; i++) { + temp_query[i] = spotlight_state.search_query[i]; + } + temp_query[spotlight_state.cursor_pos] = 0; + int cursor_x_offset = (ttf) ? font_manager_get_string_width(ttf, temp_query) : (spotlight_state.cursor_pos * 6); + draw_rect(search_x + 8 + cursor_x_offset, search_y + 8, 1, 16, COLOR_DARK_TEXT); + } + + for (int i = 0; i < spotlight_state.result_count && i < SPOTLIGHT_MAX_RESULTS; i++) { + if (i < 0 || i >= SPOTLIGHT_MAX_RESULTS) { + break; + } + + int result_x = modal_x + 4; + int result_y = modal_y + SPOTLIGHT_SEARCH_HEIGHT + 4 + (i * SPOTLIGHT_RESULT_HEIGHT); + int result_width = modal_width - 8; + + if (i == spotlight_state.selected_index) { + draw_rounded_rect_filled(result_x, result_y, result_width, SPOTLIGHT_RESULT_HEIGHT - 2, 6, COLOR_DARK_BORDER); + } + + const char *full_path = spotlight_state.results[i].entry.path; + if (!full_path || full_path[0] == 0) { + continue; + } + + const char *filename = full_path; + + for (int j = 0; full_path[j]; j++) { + if (full_path[j] == '/') { + filename = &full_path[j + 1]; + } + } + + if (!filename || filename[0] == 0) { + continue; + } + + draw_string(result_x + 8, result_y + 8, filename, COLOR_DARK_TEXT); + + if (!spotlight_state.results[i].entry.is_directory) { + char size_str[32]; + uint32_t size = spotlight_state.results[i].entry.size; + + if (size < 1024) { + // Bytes + size_str[0] = '0' + ((size / 1) % 10); + size_str[1] = 'B'; + size_str[2] = 0; + } else if (size < 1024 * 1024) { + // Kilobytes - properly format for values up to 1023 KB + int kb = size / 1024; + if (kb >= 100) { + size_str[0] = '0' + (kb / 100); + size_str[1] = '0' + ((kb / 10) % 10); + size_str[2] = '0' + (kb % 10); + size_str[3] = 'K'; + size_str[4] = 'B'; + size_str[5] = 0; + } else { + size_str[0] = '0' + (kb / 10); + size_str[1] = '0' + (kb % 10); + size_str[2] = 'K'; + size_str[3] = 'B'; + size_str[4] = 0; + } + } else { + // Megabytes - properly format for any MB value + int mb = size / (1024 * 1024); + if (mb >= 100) { + size_str[0] = '0' + (mb / 100); + size_str[1] = '0' + ((mb / 10) % 10); + size_str[2] = '0' + (mb % 10); + size_str[3] = 'M'; + size_str[4] = 'B'; + size_str[5] = 0; + } else { + size_str[0] = '0' + (mb / 10); + size_str[1] = '0' + (mb % 10); + size_str[2] = 'M'; + size_str[3] = 'B'; + size_str[4] = 0; + } + } + + int size_x = result_x + result_width - 8 - 32; // Account for wider size strings + draw_string(size_x, result_y + 8, size_str, COLOR_DKGRAY); + } + } + + // Draw "No results" message if needed + if (spotlight_state.search_len > 0 && spotlight_state.result_count == 0) { + int msg_y = modal_y + SPOTLIGHT_SEARCH_HEIGHT + 10; + draw_string(modal_x + 20, msg_y, "No results found", COLOR_DKGRAY); + } +} + static Window *sorted_windows_cache[32]; static int sorted_window_count_cache = 0; @@ -1393,7 +1652,7 @@ static void wm_paint_region(int y_start, int y_end, DirtyRect dirty, int pass) { if (dock_y < cy + ch && dock_y + dock_h > cy) { int d_item_sz = 48, d_space = 10, d_total_w = 12 * (d_item_sz + d_space); int d_bg_x = (sw - d_total_w) / 2 - 12, d_bg_w = d_total_w + 24; - draw_rounded_rect_blurred(d_bg_x, dock_y, d_bg_w, dock_h, 18, COLOR_DOCK_BG, 5, 140); + draw_rounded_rect_blurred(d_bg_x, dock_y, d_bg_w, dock_h, 18, COLOR_DOCK_BG, 1, 180); int dx = (sw - d_total_w) / 2, dy = dock_y + 6; draw_dock_files(dx, dy); dx += d_item_sz+d_space; draw_dock_settings(dx, dy); dx += d_item_sz+d_space; @@ -1466,6 +1725,9 @@ static void wm_paint_region(int y_start, int y_end, DirtyRect dirty, int pass) { } } + // Render spotlight modal + wm_render_spotlight(cy, cy + ch, dirty); + if (wm_custom_paint_hook) wm_custom_paint_hook(); if (is_dragging_file) { @@ -1679,6 +1941,29 @@ void wm_handle_click(int x, int y) { int sh = get_screen_height(); int sw = get_screen_width(); + if (spotlight_state.visible) { + int modal_width = SPOTLIGHT_MODAL_WIDTH; + int modal_height = SPOTLIGHT_SEARCH_HEIGHT + (spotlight_state.result_count * SPOTLIGHT_RESULT_HEIGHT) + 10; + int modal_x = (sw - modal_width) / 2; + int modal_y = (sh * 2 / 5) - (modal_height / 2); + + if (rect_contains(modal_x, modal_y, modal_width, modal_height, x, y)) { + int result_click_y = y - (modal_y + SPOTLIGHT_SEARCH_HEIGHT + 4); + if (result_click_y >= 0 && result_click_y < spotlight_state.result_count * SPOTLIGHT_RESULT_HEIGHT) { + int result_idx = result_click_y / SPOTLIGHT_RESULT_HEIGHT; + if (result_idx >= 0 && result_idx < spotlight_state.result_count) { + spotlight_state.selected_index = result_idx; + const char *file_path = spotlight_state.results[result_idx].entry.path; + spotlight_state.visible = false; + } + } + } else { + spotlight_state.visible = false; + } + force_redraw = true; + return; + } + if (msg_box_visible) { int mw = 320; int mh = 100; @@ -2712,12 +2997,42 @@ void wm_show_notification(const char *msg) { force_redraw = true; } +// Wrapper for work queue - builds file index asynchronously +static void build_file_index_async(void *arg) { + (void)arg; // Unused + file_index_build(); +} + void wm_handle_key(char c, bool pressed) { if (pressed && c == 'p' && ps2_ctrl_pressed) { process_create_elf("/bin/screenshot.elf", NULL); return; } + if (pressed && c == ' ' && ps2_ctrl_pressed && ps2_shift_pressed()) { + spotlight_state.visible = !spotlight_state.visible; + if (spotlight_state.visible) { + // Check current index status - it may still be building in background + spotlight_index_built = file_index_is_valid(); + // Clear search state when opening + spotlight_state.search_len = 0; + spotlight_state.search_query[0] = 0; + spotlight_state.cursor_pos = 0; + spotlight_state.result_count = 0; + spotlight_state.selected_index = 0; + } + int sw = get_screen_width(); + int sh = get_screen_height(); + graphics_mark_dirty(0, 0, sw, sh); + force_redraw = true; + return; + } + + if (spotlight_state.visible && pressed) { + wm_spotlight_handle_key(c); + return; + } + int next = (key_head + 1) % INPUT_QUEUE_SIZE; if (next != key_tail) { key_queue[key_head].c = c; @@ -2786,12 +3101,23 @@ void wm_process_deferred_thumbs(void) { void wm_init(void) { disk_manager_init(); disk_manager_scan(); - // Drives are now dynamically managed - only real drives are registered cmd_init(); explorer_init(); wallpaper_init(); + file_index_init(); + + // Try to load the file index from persistent cache + // If it doesn't exist, queue an async build + if (!file_index_load()) { + // No cache exists - queue async build to background + work_queue_submit(build_file_index_async, NULL); + } else { + // Cache loaded - mark as ready + spotlight_index_built = true; + } + refresh_desktop_icons(); // Initialize z-indices @@ -2857,4 +3183,7 @@ void wm_timer_tick(void) { void wm_notify_fs_change(void) { periodic_refresh_pending = true; + + file_index_invalidate_cache(); + spotlight_index_built = false; } diff --git a/src/wm/wm.h b/src/wm/wm.h index d60781e..059ca6a 100644 --- a/src/wm/wm.h +++ b/src/wm/wm.h @@ -68,6 +68,24 @@ struct Window { bool resizable; }; +#define SPOTLIGHT_MAX_RESULTS 6 +#define SPOTLIGHT_MODAL_WIDTH 520 +#define SPOTLIGHT_RESULT_HEIGHT 40 +#define SPOTLIGHT_SEARCH_HEIGHT 48 + +#include "../sys/file_index.h" + +typedef struct { + bool visible; + char search_query[256]; + int search_len; + int cursor_pos; + file_index_result_t results[SPOTLIGHT_MAX_RESULTS]; + int result_count; + int selected_index; + int last_query_hash; +} spotlight_state_t; + void wm_init(void); void wm_handle_mouse(int dx, int dy, uint8_t buttons, int dz); void wm_handle_key(char c, bool pressed);