diff --git a/programs/GNUmakefile b/programs/GNUmakefile index d418b90..7fd2c60 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, apps go to bin/apps//. @@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Common shared assets (wallpapers, etc.) COMMONKEEP := $(BINDIR)/common/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl icons fonts bearssl libc tls libjpeg libjpegwrite install-apps +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs icons fonts bearssl libc tls libjpeg libjpegwrite install-apps -all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) +all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -261,8 +261,12 @@ printd: bearssl libc tls libjpegwrite printctl: bearssl libc tls $(MAKE) -C src/printctl +# Build shared dialogs app (depends on libc, bearssl, and tls through print helpers). +dialogs: bearssl libc tls + $(MAKE) -C src/dialogs + # Install app bundles (manifests, icons, data files) into bin/apps//. -install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers +install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers dialogs ../scripts/install_apps.sh # Copy man pages into bin/man/ so mkramdisk.sh picks them up. @@ -329,3 +333,4 @@ clean: $(MAKE) -C src/tcc clean $(MAKE) -C src/printd clean $(MAKE) -C src/printctl clean + $(MAKE) -C src/dialogs clean diff --git a/programs/include/gui/dialogs.hpp b/programs/include/gui/dialogs.hpp new file mode 100644 index 0000000..1b74343 --- /dev/null +++ b/programs/include/gui/dialogs.hpp @@ -0,0 +1,324 @@ +/* + * dialogs.hpp + * Shared modal dialog protocol and client helpers + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace gui::dialogs { + +inline constexpr const char* APP_BINARY = "0:/apps/dialogs/dialogs.elf"; +inline constexpr const char* STORAGE_DIR = "0:/tmp/dialogs"; + +enum RequestKind : uint8_t { + REQUEST_KIND_FILE = 1, + REQUEST_KIND_PRINT = 2, +}; + +enum FileDialogMode : uint8_t { + FILE_DIALOG_OPEN = 1, + FILE_DIALOG_SAVE = 2, +}; + +struct Request { + uint8_t kind; + uint8_t mode; + char title[96]; + char initial_path[256]; + char suggested_name[128]; + char source_path[256]; + char job_name[128]; + char result_path[256]; +}; + +struct Result { + char status[16]; + char path[256]; + char job_id[64]; + char message[160]; +}; + +inline void safe_copy(char* dst, int dst_len, const char* src) { + if (!dst || dst_len <= 0) return; + if (!src) src = ""; + montauk::strncpy(dst, src, dst_len - 1); + dst[dst_len - 1] = '\0'; +} + +inline void reset_request(Request* req) { + if (!req) return; + montauk::memset(req, 0, sizeof(Request)); +} + +inline void reset_result(Result* res) { + if (!res) return; + montauk::memset(res, 0, sizeof(Result)); +} + +inline void ensure_storage_dirs() { + montauk::fmkdir("0:/tmp"); + montauk::fmkdir(STORAGE_DIR); +} + +inline bool write_text_file(const char* path, const char* text) { + if (!path || !path[0] || !text) return false; + int fd = montauk::fcreate(path); + if (fd < 0) return false; + int len = montauk::slen(text); + bool ok = montauk::fwrite(fd, (const uint8_t*)text, 0, (uint64_t)len) >= 0; + montauk::close(fd); + return ok; +} + +inline bool read_text_file(const char* path, char* out, int out_len) { + if (!out || out_len <= 0) return false; + out[0] = '\0'; + if (!path || !path[0]) return false; + int fd = montauk::open(path); + if (fd < 0) return false; + uint64_t size = montauk::getsize(fd); + if ((int)size >= out_len) size = (uint64_t)(out_len - 1); + int got = montauk::read(fd, (uint8_t*)out, 0, size); + montauk::close(fd); + if (got < 0) return false; + out[got] = '\0'; + return true; +} + +inline bool line_value(const char* text, const char* key, char* out, int out_len) { + if (!text || !key || !out || out_len <= 0) return false; + + int key_len = montauk::slen(key); + const char* p = text; + while (*p) { + const char* line = p; + while (*p && *p != '\n') p++; + int line_len = (int)(p - line); + if (*p == '\n') p++; + + if (line_len <= key_len || line[key_len] != '=') continue; + + bool match = true; + for (int i = 0; i < key_len; i++) { + if (line[i] != key[i]) { + match = false; + break; + } + } + if (!match) continue; + + const char* value = line + key_len + 1; + int value_len = line_len - key_len - 1; + while (value_len > 0 && (value[value_len - 1] == '\r' || value[value_len - 1] == '\n')) + value_len--; + if (value_len >= out_len) value_len = out_len - 1; + montauk::memcpy(out, value, (uint64_t)value_len); + out[value_len] = '\0'; + return true; + } + + return false; +} + +inline uint32_t parse_u32(const char* text) { + if (!text) return 0; + uint32_t value = 0; + for (int i = 0; text[i] >= '0' && text[i] <= '9'; i++) { + value = value * 10u + (uint32_t)(text[i] - '0'); + } + return value; +} + +inline bool write_request_file(const char* path, const Request* req) { + if (!path || !req) return false; + char text[1536]; + int n = snprintf(text, sizeof(text), + "kind=%u\n" + "mode=%u\n" + "title=%s\n" + "initial_path=%s\n" + "suggested_name=%s\n" + "source_path=%s\n" + "job_name=%s\n" + "result_path=%s\n", + (unsigned)req->kind, + (unsigned)req->mode, + req->title, + req->initial_path, + req->suggested_name, + req->source_path, + req->job_name, + req->result_path); + if (n <= 0 || n >= (int)sizeof(text)) return false; + return write_text_file(path, text); +} + +inline bool read_request_file(const char* path, Request* req) { + if (!path || !req) return false; + char text[1536]; + if (!read_text_file(path, text, sizeof(text))) return false; + + reset_request(req); + char value[256]; + if (line_value(text, "kind", value, sizeof(value))) req->kind = (uint8_t)parse_u32(value); + if (line_value(text, "mode", value, sizeof(value))) req->mode = (uint8_t)parse_u32(value); + line_value(text, "title", req->title, sizeof(req->title)); + line_value(text, "initial_path", req->initial_path, sizeof(req->initial_path)); + line_value(text, "suggested_name", req->suggested_name, sizeof(req->suggested_name)); + line_value(text, "source_path", req->source_path, sizeof(req->source_path)); + line_value(text, "job_name", req->job_name, sizeof(req->job_name)); + line_value(text, "result_path", req->result_path, sizeof(req->result_path)); + return req->kind != 0 && req->result_path[0] != '\0'; +} + +inline bool write_result_file(const char* path, const Result* res) { + if (!path || !res) return false; + char text[1024]; + int n = snprintf(text, sizeof(text), + "status=%s\n" + "path=%s\n" + "job_id=%s\n" + "message=%s\n", + res->status, + res->path, + res->job_id, + res->message); + if (n <= 0 || n >= (int)sizeof(text)) return false; + return write_text_file(path, text); +} + +inline bool read_result_file(const char* path, Result* res) { + if (!path || !res) return false; + char text[1024]; + if (!read_text_file(path, text, sizeof(text))) return false; + + reset_result(res); + line_value(text, "status", res->status, sizeof(res->status)); + line_value(text, "path", res->path, sizeof(res->path)); + line_value(text, "job_id", res->job_id, sizeof(res->job_id)); + line_value(text, "message", res->message, sizeof(res->message)); + return res->status[0] != '\0'; +} + +inline void make_unique_paths(char* request_path, int request_path_len, + char* result_path, int result_path_len) { + static uint32_t seq = 0; + ensure_storage_dirs(); + uint64_t now = montauk::get_milliseconds(); + int pid = montauk::getpid(); + uint32_t cur = ++seq; + snprintf(request_path, request_path_len, "%s/%d-%llu-%u.req", + STORAGE_DIR, pid, (unsigned long long)now, (unsigned)cur); + snprintf(result_path, result_path_len, "%s/%d-%llu-%u.res", + STORAGE_DIR, pid, (unsigned long long)now, (unsigned)cur); +} + +inline bool run_request(const Request* req, Result* out_res, char* out_message, int out_message_len) { + if (out_message && out_message_len > 0) out_message[0] = '\0'; + if (!req) { + if (out_message && out_message_len > 0) safe_copy(out_message, out_message_len, "invalid dialog request"); + return false; + } + + char request_path[256]; + char result_path[256]; + make_unique_paths(request_path, sizeof(request_path), result_path, sizeof(result_path)); + + Request req_copy = *req; + safe_copy(req_copy.result_path, sizeof(req_copy.result_path), result_path); + + if (!write_request_file(request_path, &req_copy)) { + if (out_message && out_message_len > 0) safe_copy(out_message, out_message_len, "failed to write dialog request"); + return false; + } + + int pid = montauk::spawn(APP_BINARY, request_path); + if (pid < 0) { + montauk::fdelete(request_path); + if (out_message && out_message_len > 0) safe_copy(out_message, out_message_len, "failed to start dialog"); + return false; + } + + int proc = montauk::proc_open(pid); + if (proc >= 0) { + montauk::wait_handle(proc, Montauk::IPC_SIGNAL_EXITED); + montauk::close(proc); + } else { + montauk::waitpid(pid); + } + + Result local_res = {}; + bool ok = read_result_file(result_path, &local_res); + + montauk::fdelete(request_path); + montauk::fdelete(result_path); + + if (!ok) { + if (out_message && out_message_len > 0) safe_copy(out_message, out_message_len, "dialog did not return a result"); + return false; + } + + if (out_res) *out_res = local_res; + if (out_message && out_message_len > 0) safe_copy(out_message, out_message_len, local_res.message); + return montauk::streq(local_res.status, "ok"); +} + +inline bool open_file(const char* title, + const char* initial_path, + char* out_path, int out_path_len, + char* out_message = nullptr, int out_message_len = 0) { + Request req = {}; + req.kind = REQUEST_KIND_FILE; + req.mode = FILE_DIALOG_OPEN; + safe_copy(req.title, sizeof(req.title), title ? title : "Open"); + safe_copy(req.initial_path, sizeof(req.initial_path), initial_path); + + Result res = {}; + bool ok = run_request(&req, &res, out_message, out_message_len); + if (ok && out_path && out_path_len > 0) safe_copy(out_path, out_path_len, res.path); + return ok; +} + +inline bool save_file(const char* title, + const char* initial_path, + const char* suggested_name, + char* out_path, int out_path_len, + char* out_message = nullptr, int out_message_len = 0) { + Request req = {}; + req.kind = REQUEST_KIND_FILE; + req.mode = FILE_DIALOG_SAVE; + safe_copy(req.title, sizeof(req.title), title ? title : "Save"); + safe_copy(req.initial_path, sizeof(req.initial_path), initial_path); + safe_copy(req.suggested_name, sizeof(req.suggested_name), suggested_name); + + Result res = {}; + bool ok = run_request(&req, &res, out_message, out_message_len); + if (ok && out_path && out_path_len > 0) safe_copy(out_path, out_path_len, res.path); + return ok; +} + +inline bool print_file(const char* title, + const char* source_path, + const char* job_name, + char* out_job_id, int out_job_id_len, + char* out_message = nullptr, int out_message_len = 0) { + Request req = {}; + req.kind = REQUEST_KIND_PRINT; + safe_copy(req.title, sizeof(req.title), title ? title : "Print"); + safe_copy(req.source_path, sizeof(req.source_path), source_path); + safe_copy(req.job_name, sizeof(req.job_name), job_name); + + Result res = {}; + bool ok = run_request(&req, &res, out_message, out_message_len); + if (ok && out_job_id && out_job_id_len > 0) safe_copy(out_job_id, out_job_id_len, res.job_id); + return ok; +} + +} // namespace gui::dialogs diff --git a/programs/src/dialogs/Makefile b/programs/src/dialogs/Makefile new file mode 100644 index 0000000..fb95441 --- /dev/null +++ b/programs/src/dialogs/Makefile @@ -0,0 +1,79 @@ +# Makefile for shared dialogs (standalone Window Server app) on MontaukOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -MMD -MP \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -I ../../lib/bearssl/inc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +TARGET := $(BINDIR)/apps/dialogs/dialogs.elf +LIBS := $(LIBDIR)/tls/libtls.a $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS) + mkdir -p $(BINDIR)/apps/dialogs + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +-include $(OBJS:.o=.d) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/dialogs/font_data.cpp b/programs/src/dialogs/font_data.cpp new file mode 100644 index 0000000..a591f69 --- /dev/null +++ b/programs/src/dialogs/font_data.cpp @@ -0,0 +1 @@ +#include "../desktop/font_data.cpp" diff --git a/programs/src/dialogs/main.cpp b/programs/src/dialogs/main.cpp new file mode 100644 index 0000000..72ce9be --- /dev/null +++ b/programs/src/dialogs/main.cpp @@ -0,0 +1,1471 @@ +/* + * main.cpp + * Shared modal dialogs for MontaukOS desktop apps + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +using namespace gui; +namespace dlg = gui::dialogs; + +namespace { + +constexpr int FILE_INIT_W = 720; +constexpr int FILE_INIT_H = 520; +constexpr int PRINT_INIT_W = 480; +constexpr int PRINT_INIT_H = 260; + +constexpr int TOOLBAR_H = 32; +constexpr int PATHBAR_H = 32; +constexpr int FOOTER_H_OPEN = 64; +constexpr int FOOTER_H_SAVE = 74; +constexpr int HEADER_H = 20; +constexpr int ITEM_H = 24; +constexpr int SCROLLBAR_W = 12; +constexpr int GRID_CELL_W = 80; +constexpr int GRID_CELL_H = 80; +constexpr int GRID_ICON = 48; +constexpr int GRID_PAD = 4; + +constexpr int MAX_ENTRIES = 64; +constexpr int MAX_HISTORY = 16; +constexpr int MAX_DRIVES = 16; +constexpr int BUTTON_H = 30; +constexpr int BUTTON_W = 88; + +constexpr const char* SPECIAL_FOLDER_NAMES[] = { + "Documents", "Desktop", "Music", "Videos", "Pictures", "Downloads" +}; +constexpr const char* SPECIAL_FOLDER_ICONS[] = { + "folder-blue-documents.svg", + "folder-blue-desktop.svg", + "folder-blue-music.svg", + "folder-blue-videos.svg", + "folder-blue-pictures.svg", + "folder-blue-downloads.svg" +}; +constexpr int SPECIAL_FOLDER_COUNT = 6; + +enum EntryType : uint8_t { + ENTRY_FILE = 0, + ENTRY_DIR = 1, + ENTRY_DRIVE = 3, + ENTRY_HOME = 4, + ENTRY_SPECIAL = 7, +}; + +enum FocusField : uint8_t { + FOCUS_LIST = 0, + FOCUS_PATH = 1, + FOCUS_NAME = 2, +}; + +struct FileDialogIcons { + SvgIcon icon_go_back; + SvgIcon icon_go_forward; + SvgIcon icon_go_up; + SvgIcon icon_home; + SvgIcon icon_folder; + SvgIcon icon_folder_lg; + SvgIcon icon_file; + SvgIcon icon_file_lg; + SvgIcon icon_drive; + SvgIcon icon_drive_lg; + SvgIcon icon_home_folder; + SvgIcon icon_home_folder_lg; + SvgIcon icon_special[ SPECIAL_FOLDER_COUNT ]; + SvgIcon icon_special_lg[ SPECIAL_FOLDER_COUNT ]; +}; + +struct FileDialogState { + FileDialogIcons icons; + + char home_dir[256]; + char current_path[256]; + char history[MAX_HISTORY][256]; + int history_pos; + int history_count; + + char entry_names[MAX_ENTRIES][64]; + uint8_t entry_types[MAX_ENTRIES]; + int entry_sizes[MAX_ENTRIES]; + bool is_dir[MAX_ENTRIES]; + int drive_indices[MAX_ENTRIES]; + int entry_count; + + int selected; + int last_click_item; + uint64_t last_click_time; + + Scrollbar scrollbar; + bool grid_view; + bool at_drives_root; + FocusField focus; + + char path_buf[256]; + int path_len; + int path_cursor; + + char filename_buf[128]; + int filename_len; + int filename_cursor; + + char pending_select[64]; + char error[160]; + + int mouse_x; + int mouse_y; +}; + +struct PrintDialogState { + SvgIcon printer_icon; + char source_path[256]; + char job_name[128]; + char printer_uri[256]; + char printer_name[128]; + char status[160]; + bool can_print; + int mouse_x; + int mouse_y; +}; + +struct AppState { + WsWindow win; + dlg::Request request; + dlg::Result result; + bool running; + bool is_print; + FileDialogState file; + PrintDialogState print; +}; + +AppState g_app = {}; + +struct FileDialogLayout { + Rect back_btn; + Rect forward_btn; + Rect up_btn; + Rect home_btn; + Rect view_btn; + Rect path_rect; + Rect content_rect; + Rect list_rect; + Rect footer_rect; + Rect name_rect; + Rect cancel_btn; + Rect confirm_btn; +}; + +struct PrintDialogLayout { + Rect content_rect; + Rect cancel_btn; + Rect printers_btn; + Rect refresh_btn; + Rect confirm_btn; +}; + +void safe_copy(char* dst, int dst_len, const char* src) { + dlg::safe_copy(dst, dst_len, src); +} + +int str_compare_ci(const char* a, const char* b) { + while (*a && *b) { + char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b; + if (ca != cb) return ca - cb; + a++; + b++; + } + return (unsigned char)*a - (unsigned char)*b; +} + +bool str_ends_with(const char* s, const char* suffix) { + int slen = montauk::slen(s); + int suflen = montauk::slen(suffix); + if (suflen > slen) return false; + for (int i = 0; i < suflen; i++) { + char sc = s[slen - suflen + i]; + char ec = suffix[i]; + if (sc >= 'A' && sc <= 'Z') sc += 32; + if (ec >= 'A' && ec <= 'Z') ec += 32; + if (sc != ec) return false; + } + return true; +} + +void str_append(char* dst, const char* src, int max_len) { + int len = montauk::slen(dst); + int i = 0; + while (src[i] && len < max_len - 1) { + dst[len++] = src[i++]; + } + dst[len] = '\0'; +} + +const char* path_basename(const char* path) { + const char* last = path; + for (const char* p = path; *p; p++) { + if (*p == '/' && *(p + 1)) last = p + 1; + } + return last; +} + +void build_fullpath(char* out, int out_len, const char* dir, const char* name) { + safe_copy(out, out_len, dir); + int len = montauk::slen(out); + if (len > 0 && out[len - 1] != '/') str_append(out, "/", out_len); + str_append(out, name, out_len); +} + +void split_parent_path(const char* path, char* out_dir, int out_dir_len, char* out_name, int out_name_len) { + safe_copy(out_dir, out_dir_len, ""); + safe_copy(out_name, out_name_len, ""); + if (!path || !path[0]) return; + + int last_slash = -1; + for (int i = 0; path[i]; i++) + if (path[i] == '/') last_slash = i; + + if (last_slash < 0) { + safe_copy(out_name, out_name_len, path); + return; + } + + int dir_len = last_slash + 1; + if (dir_len >= out_dir_len) dir_len = out_dir_len - 1; + montauk::memcpy(out_dir, path, (uint64_t)dir_len); + out_dir[dir_len] = '\0'; + safe_copy(out_name, out_name_len, path + last_slash + 1); +} + +bool is_dir_path(const char* path) { + if (!path || !path[0]) return false; + const char* names[1]; + return montauk::readdir(path, names, 1) >= 0; +} + +bool path_exists_via_open(const char* path) { + if (!path || !path[0]) return false; + int fd = montauk::open(path); + if (fd < 0) return false; + montauk::close(fd); + return true; +} + +void format_size(char* buf, int buf_len, int size) { + if (size < 1024) snprintf(buf, buf_len, "%d B", size); + else if (size < 1024 * 1024) { + int kb = size / 1024; + int frac = ((size % 1024) * 10) / 1024; + if (kb < 10) snprintf(buf, buf_len, "%d.%d KB", kb, frac); + else snprintf(buf, buf_len, "%d KB", kb); + } else { + int mb = size / (1024 * 1024); + int frac = ((size % (1024 * 1024)) * 10) / (1024 * 1024); + if (mb < 10) snprintf(buf, buf_len, "%d.%d MB", mb, frac); + else snprintf(buf, buf_len, "%d MB", mb); + } +} + +void fit_text_end(const char* src, char* out, int out_len, int max_px) { + if (!src || !src[0]) { + safe_copy(out, out_len, ""); + return; + } + if (text_width(src) <= max_px) { + safe_copy(out, out_len, src); + return; + } + + int slen = montauk::slen(src); + int keep = slen; + while (keep > 1) { + char candidate[256]; + candidate[0] = '.'; + candidate[1] = '.'; + int pos = 2; + const char* tail = src + (slen - keep); + while (*tail && pos < (int)sizeof(candidate) - 1) + candidate[pos++] = *tail++; + candidate[pos] = '\0'; + if (text_width(candidate) <= max_px) { + safe_copy(out, out_len, candidate); + return; + } + keep--; + } + + safe_copy(out, out_len, src); +} + +void draw_input(Canvas& c, const Rect& rect, const char* text, bool focused, int cursor_pos) { + c.fill_rect(rect.x, rect.y, rect.w, rect.h, colors::WHITE); + c.rect(rect.x, rect.y, rect.w, rect.h, focused ? colors::ACCENT : colors::BORDER); + int ty = rect.y + (rect.h - system_font_height()) / 2; + c.text(rect.x + 6, ty, text, colors::TEXT_COLOR); + if (focused) { + char prefix[256]; + int plen = cursor_pos; + if (plen > 255) plen = 255; + for (int i = 0; i < plen; i++) prefix[i] = text[i]; + prefix[plen] = '\0'; + int cx = rect.x + 6 + text_width(prefix); + c.vline(cx, rect.y + 4, rect.h - 8, colors::ACCENT); + } +} + +void draw_action_button(Canvas& c, const Rect& rect, const char* label, bool enabled, bool hovered, bool primary) { + Color bg = primary ? colors::ACCENT : Color::from_rgb(0xE8, 0xE8, 0xE8); + Color fg = primary ? colors::WHITE : colors::TEXT_COLOR; + if (!enabled) { + bg = Color::from_rgb(0xF0, 0xF0, 0xF0); + fg = Color::from_rgb(0x9A, 0x9A, 0x9A); + } else if (hovered) { + bg = primary ? Color::from_rgb(0x2B, 0x6B, 0xE0) : Color::from_rgb(0xDC, 0xDC, 0xDC); + } + c.fill_rounded_rect(rect.x, rect.y, rect.w, rect.h, 4, bg); + int tx = rect.x + (rect.w - text_width(label)) / 2; + int ty = rect.y + (rect.h - system_font_height()) / 2; + c.text(tx, ty, label, fg); +} + +void draw_scrollbar(Canvas& c, const Scrollbar& sb) { + if (sb.content_height <= sb.view_height) return; + c.fill_rect(sb.bounds.x, sb.bounds.y, sb.bounds.w, sb.bounds.h, sb.bg); + int th = sb.thumb_height(); + int ty = sb.thumb_y(); + Color thumb = (sb.hovered || sb.dragging) ? sb.hover_fg : sb.fg; + c.fill_rounded_rect(sb.bounds.x + 1, ty, sb.bounds.w - 2, th, 3, thumb); +} + +FileDialogLayout file_layout(const FileDialogState* st) { + FileDialogLayout lo = {}; + int footer_h = (g_app.request.mode == dlg::FILE_DIALOG_SAVE) ? FOOTER_H_SAVE : FOOTER_H_OPEN; + lo.back_btn = {4, 4, 24, 24}; + lo.forward_btn = {32, 4, 24, 24}; + lo.up_btn = {60, 4, 24, 24}; + lo.home_btn = {88, 4, 24, 24}; + lo.view_btn = {120, 4, 24, 24}; + lo.path_rect = {8, TOOLBAR_H + 4, g_app.win.width - 16, PATHBAR_H - 8}; + lo.content_rect = {0, TOOLBAR_H + PATHBAR_H, g_app.win.width, g_app.win.height - TOOLBAR_H - PATHBAR_H - footer_h}; + lo.footer_rect = {0, g_app.win.height - footer_h, g_app.win.width, footer_h}; + lo.list_rect = lo.content_rect; + int btn_y = lo.footer_rect.y + footer_h - BUTTON_H - 12; + lo.confirm_btn = {g_app.win.width - BUTTON_W - 16, btn_y, BUTTON_W, BUTTON_H}; + lo.cancel_btn = {lo.confirm_btn.x - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H}; + lo.name_rect = {88, lo.footer_rect.y + 12, lo.cancel_btn.x - 100, 30}; + if (lo.name_rect.w < 120) lo.name_rect.w = 120; + (void)st; + return lo; +} + +PrintDialogLayout print_layout() { + PrintDialogLayout lo = {}; + lo.content_rect = {0, 0, g_app.win.width, g_app.win.height}; + int btn_y = g_app.win.height - BUTTON_H - 16; + lo.confirm_btn = {g_app.win.width - BUTTON_W - 16, btn_y, BUTTON_W, BUTTON_H}; + lo.cancel_btn = {lo.confirm_btn.x - BUTTON_W - 8, btn_y, BUTTON_W, BUTTON_H}; + lo.refresh_btn = {16, btn_y, BUTTON_W, BUTTON_H}; + lo.printers_btn = {lo.refresh_btn.x + BUTTON_W + 8, btn_y, 112, BUTTON_H}; + return lo; +} + +void dialog_finish(const char* status, const char* path, const char* job_id, const char* message) { + safe_copy(g_app.result.status, sizeof(g_app.result.status), status); + safe_copy(g_app.result.path, sizeof(g_app.result.path), path); + safe_copy(g_app.result.job_id, sizeof(g_app.result.job_id), job_id); + safe_copy(g_app.result.message, sizeof(g_app.result.message), message); + dlg::write_result_file(g_app.request.result_path, &g_app.result); + g_app.running = false; +} + +void dialog_cancel(const char* message = "") { + dialog_finish("cancel", "", "", message); +} + +void file_sync_path_buffer(FileDialogState* st) { + if (st->at_drives_root) { + st->path_buf[0] = '\0'; + st->path_len = 0; + st->path_cursor = 0; + return; + } + safe_copy(st->path_buf, sizeof(st->path_buf), st->current_path); + st->path_len = montauk::slen(st->path_buf); + st->path_cursor = st->path_len; +} + +void file_set_filename(FileDialogState* st, const char* name) { + safe_copy(st->filename_buf, sizeof(st->filename_buf), name); + st->filename_len = montauk::slen(st->filename_buf); + st->filename_cursor = st->filename_len; +} + +void file_set_error(FileDialogState* st, const char* msg) { + safe_copy(st->error, sizeof(st->error), msg); +} + +int special_folder_index(const char* name) { + for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) { + if (montauk::streq(name, SPECIAL_FOLDER_NAMES[i])) return i; + } + return -1; +} + +void load_file_icons(FileDialogState* st) { + Color def = colors::ICON_COLOR; + st->icons.icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, def); + st->icons.icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, def); + st->icons.icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, def); + st->icons.icon_home = svg_load("0:/icons/user-home.svg", 16, 16, def); + st->icons.icon_folder = svg_load("0:/icons/folder.svg", 16, 16, def); + st->icons.icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, def); + st->icons.icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, def); + st->icons.icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, def); + st->icons.icon_drive = svg_load("0:/icons/drive-harddisk.svg", 16, 16, def); + st->icons.icon_drive_lg = svg_load("0:/icons/drive-harddisk.svg", 48, 48, def); + st->icons.icon_home_folder = svg_load("0:/icons/folder-blue-home.svg", 16, 16, def); + st->icons.icon_home_folder_lg = svg_load("0:/icons/folder-blue-home.svg", 48, 48, def); + for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) { + char path[128]; + snprintf(path, sizeof(path), "0:/icons/%s", SPECIAL_FOLDER_ICONS[i]); + st->icons.icon_special[i] = svg_load(path, 16, 16, def); + st->icons.icon_special_lg[i] = svg_load(path, 48, 48, def); + } +} + +void free_icon(SvgIcon* icon) { + if (icon && icon->pixels) { + svg_free(*icon); + icon->pixels = nullptr; + } +} + +void free_file_icons(FileDialogState* st) { + free_icon(&st->icons.icon_go_back); + free_icon(&st->icons.icon_go_forward); + free_icon(&st->icons.icon_go_up); + free_icon(&st->icons.icon_home); + free_icon(&st->icons.icon_folder); + free_icon(&st->icons.icon_folder_lg); + free_icon(&st->icons.icon_file); + free_icon(&st->icons.icon_file_lg); + free_icon(&st->icons.icon_drive); + free_icon(&st->icons.icon_drive_lg); + free_icon(&st->icons.icon_home_folder); + free_icon(&st->icons.icon_home_folder_lg); + for (int i = 0; i < SPECIAL_FOLDER_COUNT; i++) { + free_icon(&st->icons.icon_special[i]); + free_icon(&st->icons.icon_special_lg[i]); + } +} + +void file_push_history(FileDialogState* st) { + if (st->history_count > 0 && st->history_pos >= 0) { + if (montauk::streq(st->history[st->history_pos], st->current_path)) return; + } + st->history_pos++; + if (st->history_pos >= MAX_HISTORY) st->history_pos = MAX_HISTORY - 1; + safe_copy(st->history[st->history_pos], sizeof(st->history[st->history_pos]), st->current_path); + st->history_count = st->history_pos + 1; +} + +void file_apply_pending_select(FileDialogState* st) { + st->selected = -1; + if (!st->pending_select[0]) return; + for (int i = 0; i < st->entry_count; i++) { + if (montauk::streq(st->entry_names[i], st->pending_select)) { + st->selected = i; + break; + } + } + st->pending_select[0] = '\0'; +} + +void file_read_drives(FileDialogState* st) { + st->entry_count = 0; + st->at_drives_root = true; + st->current_path[0] = '\0'; + + if (st->home_dir[0] != '\0') { + int i = st->entry_count++; + safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), "Home"); + st->entry_types[i] = ENTRY_HOME; + st->entry_sizes[i] = 0; + st->is_dir[i] = true; + st->drive_indices[i] = -1; + } + + if (st->home_dir[0] != '\0') { + for (int sf = 0; sf < SPECIAL_FOLDER_COUNT && st->entry_count < MAX_ENTRIES; sf++) { + char probe[256]; + safe_copy(probe, sizeof(probe), st->home_dir); + int plen = montauk::slen(probe); + if (plen > 0 && probe[plen - 1] != '/') str_append(probe, "/", sizeof(probe)); + str_append(probe, SPECIAL_FOLDER_NAMES[sf], sizeof(probe)); + if (path_exists_via_open(probe)) { + int i = st->entry_count++; + safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), SPECIAL_FOLDER_NAMES[sf]); + st->entry_types[i] = ENTRY_SPECIAL; + st->entry_sizes[i] = 0; + st->is_dir[i] = true; + st->drive_indices[i] = sf; + } + } + } + + int drives[MAX_DRIVES]; + int drive_count = montauk::drivelist(drives, MAX_DRIVES); + for (int di = 0; di < drive_count && st->entry_count < MAX_ENTRIES; di++) { + int d = drives[di]; + int i = st->entry_count++; + char label[64]; + if (d < 10) snprintf(label, sizeof(label), "Drive %d:/", d); + else snprintf(label, sizeof(label), "Drive %d:/", d); + safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), label); + st->entry_types[i] = ENTRY_DRIVE; + st->entry_sizes[i] = 0; + st->is_dir[i] = true; + st->drive_indices[i] = d; + } + + st->scrollbar.scroll_offset = 0; + st->last_click_item = -1; + st->last_click_time = 0; + file_sync_path_buffer(st); + file_apply_pending_select(st); +} + +void file_read_dir(FileDialogState* st) { + if (!st->current_path[0] || !is_dir_path(st->current_path)) { + file_read_drives(st); + return; + } + + st->at_drives_root = false; + const char* names[MAX_ENTRIES]; + st->entry_count = montauk::readdir(st->current_path, names, MAX_ENTRIES); + if (st->entry_count < 0) st->entry_count = 0; + + const char* after_drive = st->current_path; + for (int k = 0; after_drive[k]; k++) { + if (after_drive[k] == ':' && after_drive[k + 1] == '/') { + after_drive += k + 2; + break; + } + } + + char prefix[256] = {}; + int prefix_len = 0; + if (after_drive[0] != '\0') { + safe_copy(prefix, sizeof(prefix), after_drive); + prefix_len = montauk::slen(prefix); + if (prefix_len > 0 && prefix[prefix_len - 1] != '/') { + prefix[prefix_len++] = '/'; + prefix[prefix_len] = '\0'; + } + } + + for (int i = 0; i < st->entry_count; i++) { + const char* raw = names[i]; + if (prefix_len > 0) { + bool match = true; + for (int k = 0; k < prefix_len; k++) { + if (raw[k] != prefix[k]) { match = false; break; } + } + if (match) raw += prefix_len; + } + + safe_copy(st->entry_names[i], sizeof(st->entry_names[i]), raw); + int len = montauk::slen(st->entry_names[i]); + if (len > 0 && st->entry_names[i][len - 1] == '/') { + st->is_dir[i] = true; + st->entry_names[i][len - 1] = '\0'; + } else { + st->is_dir[i] = false; + } + + st->entry_types[i] = st->is_dir[i] ? ENTRY_DIR : ENTRY_FILE; + st->entry_sizes[i] = 0; + st->drive_indices[i] = -1; + + if (!st->is_dir[i]) { + char full[512]; + build_fullpath(full, sizeof(full), st->current_path, st->entry_names[i]); + int fd = montauk::open(full); + if (fd >= 0) { + st->entry_sizes[i] = (int)montauk::getsize(fd); + montauk::close(fd); + } + } + } + + for (int i = 1; i < st->entry_count; i++) { + char tmp_name[64]; + uint8_t tmp_type = st->entry_types[i]; + int tmp_size = st->entry_sizes[i]; + bool tmp_isdir = st->is_dir[i]; + safe_copy(tmp_name, sizeof(tmp_name), st->entry_names[i]); + + int j = i - 1; + while (j >= 0) { + bool swap = false; + if (tmp_isdir && !st->is_dir[j]) swap = true; + else if (tmp_isdir == st->is_dir[j] && str_compare_ci(tmp_name, st->entry_names[j]) < 0) swap = true; + if (!swap) break; + + safe_copy(st->entry_names[j + 1], sizeof(st->entry_names[j + 1]), st->entry_names[j]); + st->entry_types[j + 1] = st->entry_types[j]; + st->entry_sizes[j + 1] = st->entry_sizes[j]; + st->is_dir[j + 1] = st->is_dir[j]; + st->drive_indices[j + 1] = st->drive_indices[j]; + j--; + } + + safe_copy(st->entry_names[j + 1], sizeof(st->entry_names[j + 1]), tmp_name); + st->entry_types[j + 1] = tmp_type; + st->entry_sizes[j + 1] = tmp_size; + st->is_dir[j + 1] = tmp_isdir; + st->drive_indices[j + 1] = -1; + } + + st->scrollbar.scroll_offset = 0; + st->last_click_item = -1; + st->last_click_time = 0; + file_sync_path_buffer(st); + file_apply_pending_select(st); +} + +void file_ensure_selected_visible(FileDialogState* st) { + if (st->selected < 0 || st->selected >= st->entry_count) return; + FileDialogLayout lo = file_layout(st); + if (st->grid_view) { + int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W; + if (cols < 1) cols = 1; + int row = st->selected / cols; + int top = row * GRID_CELL_H; + int bottom = top + GRID_CELL_H; + if (top < st->scrollbar.scroll_offset) st->scrollbar.scroll_offset = top; + else if (bottom > st->scrollbar.scroll_offset + lo.content_rect.h) + st->scrollbar.scroll_offset = bottom - lo.content_rect.h; + } else { + int view_h = lo.content_rect.h - HEADER_H; + int top = st->selected * ITEM_H; + int bottom = top + ITEM_H; + if (top < st->scrollbar.scroll_offset) st->scrollbar.scroll_offset = top; + else if (bottom > st->scrollbar.scroll_offset + view_h) + st->scrollbar.scroll_offset = bottom - view_h; + } + int ms = st->scrollbar.max_scroll(); + if (st->scrollbar.scroll_offset < 0) st->scrollbar.scroll_offset = 0; + if (st->scrollbar.scroll_offset > ms) st->scrollbar.scroll_offset = ms; +} + +void file_navigate_into(FileDialogState* st, int idx) { + if (idx < 0 || idx >= st->entry_count) return; + + if (st->at_drives_root && st->entry_types[idx] == ENTRY_HOME) { + safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); + file_push_history(st); + file_read_dir(st); + return; + } + + if (st->at_drives_root && st->entry_types[idx] == ENTRY_SPECIAL) { + safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); + int len = montauk::slen(st->current_path); + if (len > 0 && st->current_path[len - 1] != '/') str_append(st->current_path, "/", sizeof(st->current_path)); + str_append(st->current_path, st->entry_names[idx], sizeof(st->current_path)); + file_push_history(st); + file_read_dir(st); + return; + } + + if (st->at_drives_root && st->entry_types[idx] == ENTRY_DRIVE) { + snprintf(st->current_path, sizeof(st->current_path), "%d:/", st->drive_indices[idx]); + file_push_history(st); + file_read_dir(st); + return; + } + + if (st->is_dir[idx]) { + char next[256]; + build_fullpath(next, sizeof(next), st->current_path, st->entry_names[idx]); + safe_copy(st->current_path, sizeof(st->current_path), next); + file_push_history(st); + file_read_dir(st); + } +} + +bool file_accept_open(FileDialogState* st) { + if (st->selected < 0 || st->selected >= st->entry_count) { + file_set_error(st, "Select a file to open"); + return false; + } + if (st->is_dir[st->selected]) { + file_navigate_into(st, st->selected); + return false; + } + + char full[512]; + build_fullpath(full, sizeof(full), st->current_path, st->entry_names[st->selected]); + dialog_finish("ok", full, "", full); + return true; +} + +bool file_accept_save(FileDialogState* st) { + if (st->at_drives_root) { + file_set_error(st, "Choose a folder before saving"); + return false; + } + if (!st->filename_buf[0]) { + file_set_error(st, "Enter a file name"); + return false; + } + + char full[512]; + build_fullpath(full, sizeof(full), st->current_path, st->filename_buf); + dialog_finish("ok", full, "", full); + return true; +} + +void file_activate_selected(FileDialogState* st, bool from_double_click) { + if (st->selected < 0 || st->selected >= st->entry_count) return; + if (st->is_dir[st->selected]) { + file_navigate_into(st, st->selected); + return; + } + + if (g_app.request.mode == dlg::FILE_DIALOG_SAVE) { + file_set_filename(st, st->entry_names[st->selected]); + if (from_double_click) file_accept_save(st); + } else { + file_accept_open(st); + } +} + +void file_go_home(FileDialogState* st) { + file_read_drives(st); + file_push_history(st); +} + +void file_go_back(FileDialogState* st) { + if (st->history_pos <= 0) return; + st->history_pos--; + safe_copy(st->current_path, sizeof(st->current_path), st->history[st->history_pos]); + if (st->current_path[0]) file_read_dir(st); + else file_read_drives(st); +} + +void file_go_forward(FileDialogState* st) { + if (st->history_pos >= st->history_count - 1) return; + st->history_pos++; + safe_copy(st->current_path, sizeof(st->current_path), st->history[st->history_pos]); + if (st->current_path[0]) file_read_dir(st); + else file_read_drives(st); +} + +void file_go_up(FileDialogState* st) { + if (st->at_drives_root) return; + + int len = montauk::slen(st->current_path); + if (len >= 3 && st->current_path[len - 1] == '/') { + int colon = -1; + for (int i = 0; i < len; i++) { + if (st->current_path[i] == ':') { colon = i; break; } + } + if (colon >= 0 && colon + 2 == len) { + file_read_drives(st); + file_push_history(st); + return; + } + } + + if (len > 0 && st->current_path[len - 1] == '/') { + st->current_path[len - 1] = '\0'; + len--; + } + int last_slash = -1; + for (int i = len - 1; i >= 0; i--) { + if (st->current_path[i] == '/') { last_slash = i; break; } + } + if (last_slash >= 0) { + if (last_slash > 0 && st->current_path[last_slash - 1] == ':') st->current_path[last_slash + 1] = '\0'; + else st->current_path[last_slash] = '\0'; + } + file_push_history(st); + file_read_dir(st); +} + +void file_open_path(FileDialogState* st, const char* path) { + if (!path || !path[0]) { + if (st->home_dir[0]) { + safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); + file_push_history(st); + file_read_dir(st); + } else { + file_read_drives(st); + file_push_history(st); + } + return; + } + + if (is_dir_path(path)) { + safe_copy(st->current_path, sizeof(st->current_path), path); + file_push_history(st); + file_read_dir(st); + return; + } + + char dir[256]; + char name[128]; + split_parent_path(path, dir, sizeof(dir), name, sizeof(name)); + if (dir[0] && is_dir_path(dir)) { + safe_copy(st->current_path, sizeof(st->current_path), dir); + safe_copy(st->pending_select, sizeof(st->pending_select), name); + if (g_app.request.mode == dlg::FILE_DIALOG_SAVE && name[0]) file_set_filename(st, name); + file_push_history(st); + file_read_dir(st); + if (g_app.request.mode == dlg::FILE_DIALOG_OPEN && name[0]) { + for (int i = 0; i < st->entry_count; i++) { + if (montauk::streq(st->entry_names[i], name)) { + st->selected = i; + break; + } + } + } + return; + } + + file_set_error(st, "Path does not exist"); +} + +void init_file_dialog(const dlg::Request& req) { + FileDialogState* st = &g_app.file; + montauk::memset(st, 0, sizeof(*st)); + load_file_icons(st); + st->selected = -1; + st->last_click_item = -1; + st->history_pos = -1; + st->grid_view = true; + st->focus = FOCUS_LIST; + st->scrollbar.init(0, 0, SCROLLBAR_W, 100); + montauk::user::get_home_dir(st->home_dir, sizeof(st->home_dir)); + + char initial_dir[256] = {}; + char initial_name[128] = {}; + if (req.initial_path[0]) split_parent_path(req.initial_path, initial_dir, sizeof(initial_dir), initial_name, sizeof(initial_name)); + + if (req.initial_path[0] && is_dir_path(req.initial_path)) { + safe_copy(st->current_path, sizeof(st->current_path), req.initial_path); + file_read_dir(st); + file_push_history(st); + } else if (initial_dir[0] && is_dir_path(initial_dir)) { + safe_copy(st->current_path, sizeof(st->current_path), initial_dir); + safe_copy(st->pending_select, sizeof(st->pending_select), initial_name); + file_read_dir(st); + file_push_history(st); + } else if (st->home_dir[0] && is_dir_path(st->home_dir)) { + safe_copy(st->current_path, sizeof(st->current_path), st->home_dir); + file_read_dir(st); + file_push_history(st); + } else { + file_read_drives(st); + file_push_history(st); + } + + if (req.mode == dlg::FILE_DIALOG_SAVE) { + if (req.suggested_name[0]) file_set_filename(st, req.suggested_name); + else if (initial_name[0]) file_set_filename(st, initial_name); + } +} + +void draw_toolbar_icon(Canvas& c, const Rect& rect, const SvgIcon& icon) { + c.fill_rect(rect.x, rect.y, rect.w, rect.h, Color::from_rgb(0xE8, 0xE8, 0xE8)); + if (icon.pixels) { + int ix = rect.x + (rect.w - icon.width) / 2; + int iy = rect.y + (rect.h - icon.height) / 2; + c.icon(ix, iy, icon); + } +} + +void draw_grid_entries(Canvas& c, FileDialogState* st, const FileDialogLayout& lo) { + int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W; + if (cols < 1) cols = 1; + int rows = (st->entry_count + cols - 1) / cols; + int content_h = rows * GRID_CELL_H; + st->scrollbar.bounds = {c.w - SCROLLBAR_W, lo.content_rect.y, SCROLLBAR_W, lo.content_rect.h}; + st->scrollbar.content_height = content_h; + st->scrollbar.view_height = lo.content_rect.h; + + for (int i = 0; i < st->entry_count; i++) { + int col = i % cols; + int row = i / cols; + int cell_x = col * GRID_CELL_W; + int cell_y = lo.content_rect.y + row * GRID_CELL_H - st->scrollbar.scroll_offset; + if (cell_y + GRID_CELL_H <= lo.content_rect.y || cell_y >= lo.content_rect.y + lo.content_rect.h) continue; + + if (i == st->selected) { + int sy = gui_max(cell_y, lo.content_rect.y); + int sh = gui_min(cell_y + GRID_CELL_H, lo.content_rect.y + lo.content_rect.h) - sy; + int sw = gui_min(GRID_CELL_W, c.w - SCROLLBAR_W - cell_x); + if (sh > 0 && sw > 0) c.fill_rect(cell_x, sy, sw, sh, colors::MENU_HOVER); + } + + int icon_x = cell_x + (GRID_CELL_W - GRID_ICON) / 2; + int icon_y = cell_y + GRID_PAD; + SvgIcon* icon = &st->icons.icon_file_lg; + int sfi = -1; + if (st->entry_types[i] == ENTRY_SPECIAL) sfi = st->drive_indices[i]; + if (sfi >= 0 && st->icons.icon_special_lg[sfi].pixels) icon = &st->icons.icon_special_lg[sfi]; + else if (st->entry_types[i] == ENTRY_HOME && st->icons.icon_home_folder_lg.pixels) icon = &st->icons.icon_home_folder_lg; + else if (st->entry_types[i] == ENTRY_DRIVE && st->icons.icon_drive_lg.pixels) icon = &st->icons.icon_drive_lg; + else if (st->is_dir[i] && st->icons.icon_folder_lg.pixels) icon = &st->icons.icon_folder_lg; + if (icon->pixels) c.icon(icon_x, icon_y, *icon); + + char label[16]; + int nlen = montauk::slen(st->entry_names[i]); + if (nlen > 11) { + for (int k = 0; k < 9; k++) label[k] = st->entry_names[i][k]; + label[9] = '.'; + label[10] = '.'; + label[11] = '\0'; + } else { + safe_copy(label, sizeof(label), st->entry_names[i]); + } + int tx = cell_x + (GRID_CELL_W - text_width(label)) / 2; + if (tx < cell_x) tx = cell_x; + int ty = icon_y + GRID_ICON + 2; + if (ty >= lo.content_rect.y && ty + system_font_height() <= lo.content_rect.y + lo.content_rect.h) + c.text(tx, ty, label, colors::TEXT_COLOR); + } +} + +void draw_list_entries(Canvas& c, FileDialogState* st, const FileDialogLayout& lo) { + int header_y = lo.content_rect.y; + int rows_y = header_y + HEADER_H; + int rows_h = lo.content_rect.h - HEADER_H; + if (rows_h < 0) rows_h = 0; + + c.fill_rect(0, header_y, c.w, HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8)); + c.text(8, header_y + 2, "Name", Color::from_rgb(0x88, 0x88, 0x88)); + c.text(c.w - SCROLLBAR_W - 120, header_y + 2, "Size", Color::from_rgb(0x88, 0x88, 0x88)); + c.text(c.w - SCROLLBAR_W - 60, header_y + 2, "Type", Color::from_rgb(0x88, 0x88, 0x88)); + c.hline(0, header_y + HEADER_H - 1, c.w, colors::BORDER); + c.vline(c.w - SCROLLBAR_W - 124, header_y, lo.content_rect.h, colors::BORDER); + c.vline(c.w - SCROLLBAR_W - 64, header_y, lo.content_rect.h, colors::BORDER); + + st->scrollbar.bounds = {c.w - SCROLLBAR_W, rows_y, SCROLLBAR_W, rows_h}; + st->scrollbar.content_height = st->entry_count * ITEM_H; + st->scrollbar.view_height = rows_h; + + for (int i = 0; i < st->entry_count; i++) { + int y = rows_y + i * ITEM_H - st->scrollbar.scroll_offset; + if (y + ITEM_H <= rows_y || y >= rows_y + rows_h) continue; + + if (i == st->selected) c.fill_rect(0, y, c.w - SCROLLBAR_W, ITEM_H, colors::MENU_HOVER); + + int sfi = -1; + if (st->entry_types[i] == ENTRY_SPECIAL) sfi = st->drive_indices[i]; + if (sfi >= 0 && st->icons.icon_special[sfi].pixels) c.icon(8, y + 4, st->icons.icon_special[sfi]); + else if (st->entry_types[i] == ENTRY_HOME && st->icons.icon_home_folder.pixels) c.icon(8, y + 4, st->icons.icon_home_folder); + else if (st->entry_types[i] == ENTRY_DRIVE && st->icons.icon_drive.pixels) c.icon(8, y + 4, st->icons.icon_drive); + else if (st->is_dir[i] && st->icons.icon_folder.pixels) c.icon(8, y + 4, st->icons.icon_folder); + else if (st->icons.icon_file.pixels) c.icon(8, y + 4, st->icons.icon_file); + + c.text(30, y + 3, st->entry_names[i], colors::TEXT_COLOR); + + if (!st->is_dir[i]) { + char size_buf[16]; + format_size(size_buf, sizeof(size_buf), st->entry_sizes[i]); + c.text(c.w - SCROLLBAR_W - 120, y + 3, size_buf, Color::from_rgb(0x66, 0x66, 0x66)); + } + + const char* type_label = st->is_dir[i] ? "Folder" : "File"; + if (st->entry_types[i] == ENTRY_DRIVE) type_label = "Drive"; + else if (st->entry_types[i] == ENTRY_HOME) type_label = "Home"; + c.text(c.w - SCROLLBAR_W - 60, y + 3, type_label, Color::from_rgb(0x66, 0x66, 0x66)); + c.hline(0, y + ITEM_H - 1, c.w, Color::from_rgb(0xF0, 0xF0, 0xF0)); + } +} + +void draw_file_dialog() { + FileDialogState* st = &g_app.file; + Canvas c = g_app.win.canvas(); + c.fill(colors::WINDOW_BG); + + FileDialogLayout lo = file_layout(st); + + c.fill_rect(0, 0, c.w, TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); + draw_toolbar_icon(c, lo.back_btn, st->icons.icon_go_back); + draw_toolbar_icon(c, lo.forward_btn, st->icons.icon_go_forward); + draw_toolbar_icon(c, lo.up_btn, st->icons.icon_go_up); + draw_toolbar_icon(c, lo.home_btn, st->icons.icon_home); + + c.fill_rect(lo.view_btn.x, lo.view_btn.y, lo.view_btn.w, lo.view_btn.h, Color::from_rgb(0xE8, 0xE8, 0xE8)); + if (st->grid_view) { + for (int r = 0; r < 2; r++) + for (int cc = 0; cc < 2; cc++) + c.fill_rect(lo.view_btn.x + 5 + cc * 8, lo.view_btn.y + 5 + r * 8, 6, 6, colors::TEXT_COLOR); + } else { + for (int r = 0; r < 3; r++) + c.fill_rect(lo.view_btn.x + 5, lo.view_btn.y + 5 + r * 5, 14, 2, colors::TEXT_COLOR); + } + c.hline(0, TOOLBAR_H - 1, c.w, colors::BORDER); + + char path_label[256]; + if (st->at_drives_root && st->focus != FOCUS_PATH) safe_copy(path_label, sizeof(path_label), "Computer"); + else safe_copy(path_label, sizeof(path_label), st->path_buf); + draw_input(c, lo.path_rect, path_label, st->focus == FOCUS_PATH, st->path_cursor); + + if (st->grid_view) draw_grid_entries(c, st, lo); + else draw_list_entries(c, st, lo); + + draw_scrollbar(c, st->scrollbar); + + c.fill_rect(0, lo.footer_rect.y, c.w, lo.footer_rect.h, Color::from_rgb(0xF7, 0xF7, 0xF7)); + c.hline(0, lo.footer_rect.y, c.w, colors::BORDER); + + bool confirm_enabled = false; + const char* confirm_label = (g_app.request.mode == dlg::FILE_DIALOG_SAVE) ? "Save" : "Open"; + if (g_app.request.mode == dlg::FILE_DIALOG_SAVE) { + c.text(12, lo.footer_rect.y + 18, "File name", Color::from_rgb(0x66, 0x66, 0x66)); + draw_input(c, lo.name_rect, st->filename_buf, st->focus == FOCUS_NAME, st->filename_cursor); + confirm_enabled = st->filename_buf[0] != '\0' && !st->at_drives_root; + } else { + char selection[256] = {}; + if (st->selected >= 0 && st->selected < st->entry_count) { + char full[512]; + if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_HOME) safe_copy(full, sizeof(full), st->home_dir); + else if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_DRIVE) snprintf(full, sizeof(full), "%d:/", st->drive_indices[st->selected]); + else if (st->at_drives_root && st->entry_types[st->selected] == ENTRY_SPECIAL) { + safe_copy(full, sizeof(full), st->home_dir); + int len = montauk::slen(full); + if (len > 0 && full[len - 1] != '/') str_append(full, "/", sizeof(full)); + str_append(full, st->entry_names[st->selected], sizeof(full)); + } else { + build_fullpath(full, sizeof(full), st->current_path, st->entry_names[st->selected]); + } + fit_text_end(full, selection, sizeof(selection), lo.cancel_btn.x - 16); + } else { + safe_copy(selection, sizeof(selection), "Choose a file"); + } + c.text(12, lo.footer_rect.y + 14, selection, Color::from_rgb(0x66, 0x66, 0x66)); + confirm_enabled = st->selected >= 0; + } + + if (st->error[0]) { + char err[160]; + fit_text_end(st->error, err, sizeof(err), lo.cancel_btn.x - 16); + c.text(12, lo.footer_rect.y + lo.footer_rect.h - system_font_height() - 12, err, Color::from_rgb(0xC0, 0x33, 0x33)); + } + + draw_action_button(c, lo.cancel_btn, "Cancel", true, lo.cancel_btn.contains(st->mouse_x, st->mouse_y), false); + draw_action_button(c, lo.confirm_btn, confirm_label, confirm_enabled, lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true); +} + +void print_refresh(PrintDialogState* st) { + st->printer_uri[0] = '\0'; + st->printer_name[0] = '\0'; + st->status[0] = '\0'; + st->can_print = false; + + if (!print::read_default_printer_uri(st->printer_uri, sizeof(st->printer_uri))) { + safe_copy(st->status, sizeof(st->status), "No default printer configured"); + return; + } + + print::ProbeState probe = {}; + if (print::load_printer_probe_state(&probe) && montauk::streq(probe.printer_uri, st->printer_uri)) { + if (probe.caps.printer_name[0]) safe_copy(st->printer_name, sizeof(st->printer_name), probe.caps.printer_name); + if (probe.message[0]) safe_copy(st->status, sizeof(st->status), probe.message); + } + + if (!st->printer_name[0]) safe_copy(st->printer_name, sizeof(st->printer_name), "Default printer"); + if (!st->status[0]) safe_copy(st->status, sizeof(st->status), "Uses the system print spooler"); + + int fd = montauk::open(st->source_path); + if (fd >= 0) { + montauk::close(fd); + st->can_print = true; + } else { + safe_copy(st->status, sizeof(st->status), "Source document is no longer available"); + } +} + +void init_print_dialog(const dlg::Request& req) { + PrintDialogState* st = &g_app.print; + montauk::memset(st, 0, sizeof(*st)); + Color def = colors::ICON_COLOR; + st->printer_icon = svg_load("0:/icons/printer-symbolic.svg", 24, 24, def); + safe_copy(st->source_path, sizeof(st->source_path), req.source_path); + safe_copy(st->job_name, sizeof(st->job_name), req.job_name); + print_refresh(st); +} + +void draw_print_dialog() { + PrintDialogState* st = &g_app.print; + Canvas c = g_app.win.canvas(); + c.fill(colors::WINDOW_BG); + PrintDialogLayout lo = print_layout(); + + c.fill_rect(0, 0, c.w, 54, Color::from_rgb(0xF7, 0xF7, 0xF7)); + c.hline(0, 53, c.w, colors::BORDER); + if (st->printer_icon.pixels) c.icon(18, 15, st->printer_icon); + c.text(54, 16, st->printer_name[0] ? st->printer_name : "Print", colors::TEXT_COLOR); + + char source_line[256]; + fit_text_end(path_basename(st->source_path), source_line, sizeof(source_line), c.w - 36); + c.text(18, 78, "Document", Color::from_rgb(0x77, 0x77, 0x77)); + c.text(18, 100, source_line, colors::TEXT_COLOR); + + c.text(18, 136, "Printer", Color::from_rgb(0x77, 0x77, 0x77)); + char printer_line[256]; + fit_text_end(st->printer_uri[0] ? st->printer_uri : "Not configured", printer_line, sizeof(printer_line), c.w - 36); + c.text(18, 158, printer_line, colors::TEXT_COLOR); + + char status_line[160]; + fit_text_end(st->status, status_line, sizeof(status_line), c.w - 36); + c.text(18, 190, status_line, st->can_print ? Color::from_rgb(0x66, 0x66, 0x66) : Color::from_rgb(0xC0, 0x33, 0x33)); + + draw_action_button(c, lo.refresh_btn, "Refresh", true, lo.refresh_btn.contains(st->mouse_x, st->mouse_y), false); + draw_action_button(c, lo.printers_btn, "Printers", true, lo.printers_btn.contains(st->mouse_x, st->mouse_y), false); + draw_action_button(c, lo.cancel_btn, "Cancel", true, lo.cancel_btn.contains(st->mouse_x, st->mouse_y), false); + draw_action_button(c, lo.confirm_btn, "Print", st->can_print && st->printer_uri[0], lo.confirm_btn.contains(st->mouse_x, st->mouse_y), true); +} + +void print_submit(PrintDialogState* st) { + char job_id[64] = {}; + char err[160] = {}; + const char* job_name = st->job_name[0] ? st->job_name : path_basename(st->source_path); + if (!print::submit_document_job(st->source_path, nullptr, job_name, job_id, sizeof(job_id), err, sizeof(err))) { + safe_copy(st->status, sizeof(st->status), err[0] ? err : "Failed to queue print job"); + return; + } + + char msg[160]; + snprintf(msg, sizeof(msg), "Queued as %s", job_id); + dialog_finish("ok", "", job_id, msg); +} + +void text_insert(char* buf, int* len, int* cursor, int max_len, char ch) { + if (!buf || !len || !cursor || *len >= max_len - 1) return; + for (int i = *len; i > *cursor; i--) buf[i] = buf[i - 1]; + buf[*cursor] = ch; + (*len)++; + (*cursor)++; + buf[*len] = '\0'; +} + +void text_backspace(char* buf, int* len, int* cursor) { + if (!buf || !len || !cursor || *cursor <= 0 || *len <= 0) return; + for (int i = *cursor - 1; i < *len - 1; i++) buf[i] = buf[i + 1]; + (*len)--; + (*cursor)--; + buf[*len] = '\0'; +} + +void text_delete(char* buf, int* len, int* cursor) { + if (!buf || !len || !cursor || *cursor >= *len || *len <= 0) return; + for (int i = *cursor; i < *len - 1; i++) buf[i] = buf[i + 1]; + (*len)--; + buf[*len] = '\0'; +} + +int file_entry_at(FileDialogState* st, int mx, int my) { + FileDialogLayout lo = file_layout(st); + if (!lo.content_rect.contains(mx, my)) return -1; + + if (st->grid_view) { + if (mx >= lo.content_rect.w - SCROLLBAR_W) return -1; + int cols = (lo.content_rect.w - SCROLLBAR_W) / GRID_CELL_W; + if (cols < 1) cols = 1; + int col = mx / GRID_CELL_W; + int row = (my - lo.content_rect.y + st->scrollbar.scroll_offset) / GRID_CELL_H; + int idx = row * cols + col; + if (idx >= 0 && idx < st->entry_count && col < cols) return idx; + return -1; + } + + int rows_y = lo.content_rect.y + HEADER_H; + if (my < rows_y || mx >= lo.content_rect.w - SCROLLBAR_W) return -1; + int rel_y = my - rows_y + st->scrollbar.scroll_offset; + int idx = rel_y / ITEM_H; + if (idx >= 0 && idx < st->entry_count) return idx; + return -1; +} + +void handle_file_mouse(const Montauk::WinEvent& ev) { + FileDialogState* st = &g_app.file; + st->mouse_x = ev.mouse.x; + st->mouse_y = ev.mouse.y; + + FileDialogLayout lo = file_layout(st); + MouseEvent me = {ev.mouse.x, ev.mouse.y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll}; + st->scrollbar.handle_mouse(me); + + if (me.left_pressed()) { + st->error[0] = '\0'; + + if (lo.back_btn.contains(me.x, me.y)) { file_go_back(st); return; } + if (lo.forward_btn.contains(me.x, me.y)) { file_go_forward(st); return; } + if (lo.up_btn.contains(me.x, me.y)) { file_go_up(st); return; } + if (lo.home_btn.contains(me.x, me.y)) { file_go_home(st); return; } + if (lo.view_btn.contains(me.x, me.y)) { + st->grid_view = !st->grid_view; + st->scrollbar.scroll_offset = 0; + return; + } + if (lo.cancel_btn.contains(me.x, me.y)) { dialog_cancel(); return; } + if (lo.confirm_btn.contains(me.x, me.y)) { + if (g_app.request.mode == dlg::FILE_DIALOG_SAVE) file_accept_save(st); + else file_accept_open(st); + return; + } + if (lo.path_rect.contains(me.x, me.y)) { + st->focus = FOCUS_PATH; + return; + } + if (g_app.request.mode == dlg::FILE_DIALOG_SAVE && lo.name_rect.contains(me.x, me.y)) { + st->focus = FOCUS_NAME; + return; + } + + int idx = file_entry_at(st, me.x, me.y); + if (idx >= 0) { + uint64_t now = montauk::get_milliseconds(); + st->focus = FOCUS_LIST; + if (st->last_click_item == idx && (now - st->last_click_time) < 400) { + st->selected = idx; + file_activate_selected(st, true); + st->last_click_item = -1; + st->last_click_time = 0; + } else { + st->selected = idx; + st->last_click_item = idx; + st->last_click_time = now; + if (g_app.request.mode == dlg::FILE_DIALOG_SAVE && !st->is_dir[idx]) { + file_set_filename(st, st->entry_names[idx]); + } + } + file_ensure_selected_visible(st); + return; + } + + st->selected = -1; + st->focus = FOCUS_LIST; + } + + if (me.scroll != 0 && lo.content_rect.contains(me.x, me.y)) { + int step = st->grid_view ? GRID_CELL_H : ITEM_H; + st->scrollbar.scroll_offset -= me.scroll * step; + int ms = st->scrollbar.max_scroll(); + if (st->scrollbar.scroll_offset < 0) st->scrollbar.scroll_offset = 0; + if (st->scrollbar.scroll_offset > ms) st->scrollbar.scroll_offset = ms; + } +} + +void handle_file_key(const Montauk::KeyEvent& key) { + if (!key.pressed) return; + + FileDialogState* st = &g_app.file; + st->error[0] = '\0'; + + if (key.scancode == 0x01) { + dialog_cancel(); + return; + } + + if (st->focus == FOCUS_PATH) { + if (key.ascii == '\n' || key.ascii == '\r') { + file_open_path(st, st->path_buf); + st->focus = FOCUS_LIST; + return; + } + if (key.ascii == '\b' || key.scancode == 0x0E) { + text_backspace(st->path_buf, &st->path_len, &st->path_cursor); + return; + } + if (key.scancode == 0x53) { text_delete(st->path_buf, &st->path_len, &st->path_cursor); return; } + if (key.scancode == 0x4B) { if (st->path_cursor > 0) st->path_cursor--; return; } + if (key.scancode == 0x4D) { if (st->path_cursor < st->path_len) st->path_cursor++; return; } + if (key.scancode == 0x47) { st->path_cursor = 0; return; } + if (key.scancode == 0x4F) { st->path_cursor = st->path_len; return; } + if (key.ascii >= 32 && key.ascii < 127) { + text_insert(st->path_buf, &st->path_len, &st->path_cursor, (int)sizeof(st->path_buf), key.ascii); + return; + } + return; + } + + if (st->focus == FOCUS_NAME && g_app.request.mode == dlg::FILE_DIALOG_SAVE) { + if (key.ascii == '\n' || key.ascii == '\r') { file_accept_save(st); return; } + if (key.ascii == '\b' || key.scancode == 0x0E) { + text_backspace(st->filename_buf, &st->filename_len, &st->filename_cursor); + return; + } + if (key.scancode == 0x53) { text_delete(st->filename_buf, &st->filename_len, &st->filename_cursor); return; } + if (key.scancode == 0x4B) { if (st->filename_cursor > 0) st->filename_cursor--; return; } + if (key.scancode == 0x4D) { if (st->filename_cursor < st->filename_len) st->filename_cursor++; return; } + if (key.scancode == 0x47) { st->filename_cursor = 0; return; } + if (key.scancode == 0x4F) { st->filename_cursor = st->filename_len; return; } + if (key.ascii >= 32 && key.ascii < 127 && key.ascii != '/' && key.ascii != '\\') { + text_insert(st->filename_buf, &st->filename_len, &st->filename_cursor, (int)sizeof(st->filename_buf), key.ascii); + return; + } + return; + } + + if (key.ascii == '\n' || key.ascii == '\r') { + if (g_app.request.mode == dlg::FILE_DIALOG_SAVE) file_accept_save(st); + else file_accept_open(st); + return; + } + + if (key.scancode == 0x4B || key.scancode == 0x48) { + if (st->selected > 0) st->selected--; + else if (st->entry_count > 0) st->selected = 0; + file_ensure_selected_visible(st); + return; + } + + if (key.scancode == 0x4D || key.scancode == 0x50) { + if (st->selected < st->entry_count - 1) st->selected++; + else if (st->entry_count > 0) st->selected = st->entry_count - 1; + file_ensure_selected_visible(st); + return; + } + + if (key.ctrl && (key.ascii == 'l' || key.ascii == 'L')) { + st->focus = FOCUS_PATH; + return; + } + + if (key.ctrl && g_app.request.mode == dlg::FILE_DIALOG_SAVE && (key.ascii == 'n' || key.ascii == 'N')) { + st->focus = FOCUS_NAME; + return; + } +} + +void handle_print_mouse(const Montauk::WinEvent& ev) { + PrintDialogState* st = &g_app.print; + st->mouse_x = ev.mouse.x; + st->mouse_y = ev.mouse.y; + + MouseEvent me = {ev.mouse.x, ev.mouse.y, ev.mouse.buttons, ev.mouse.prev_buttons, ev.mouse.scroll}; + if (!me.left_pressed()) return; + + PrintDialogLayout lo = print_layout(); + if (lo.cancel_btn.contains(me.x, me.y)) { dialog_cancel(); return; } + if (lo.refresh_btn.contains(me.x, me.y)) { print_refresh(st); return; } + if (lo.printers_btn.contains(me.x, me.y)) { montauk::spawn("0:/apps/printers/printers.elf"); return; } + if (lo.confirm_btn.contains(me.x, me.y) && st->can_print && st->printer_uri[0]) { + print_submit(st); + } +} + +void handle_print_key(const Montauk::KeyEvent& key) { + if (!key.pressed) return; + if (key.scancode == 0x01) { + dialog_cancel(); + return; + } + if ((key.ascii == '\n' || key.ascii == '\r') && g_app.print.can_print && g_app.print.printer_uri[0]) { + print_submit(&g_app.print); + } + if (key.ctrl && (key.ascii == 'r' || key.ascii == 'R')) { + print_refresh(&g_app.print); + } +} + +void cleanup() { + if (g_app.is_print) { + free_icon(&g_app.print.printer_icon); + } else { + free_file_icons(&g_app.file); + } + g_app.win.destroy(); +} + +} // namespace + +extern "C" void _start() { + if (!fonts::init()) montauk::exit(1); + + char argbuf[256] = {}; + if (montauk::getargs(argbuf, sizeof(argbuf)) <= 0 || !argbuf[0]) montauk::exit(1); + if (!dlg::read_request_file(argbuf, &g_app.request)) montauk::exit(1); + + g_app.running = true; + dlg::reset_result(&g_app.result); + + g_app.is_print = g_app.request.kind == dlg::REQUEST_KIND_PRINT; + const int init_w = g_app.is_print ? PRINT_INIT_W : FILE_INIT_W; + const int init_h = g_app.is_print ? PRINT_INIT_H : FILE_INIT_H; + const char* title = g_app.request.title[0] ? g_app.request.title : (g_app.is_print ? "Print" : "Open"); + + if (g_app.is_print) init_print_dialog(g_app.request); + else init_file_dialog(g_app.request); + + if (!g_app.win.create(title, init_w, init_h)) montauk::exit(1); + + if (g_app.is_print) draw_print_dialog(); + else draw_file_dialog(); + g_app.win.present(); + + while (g_app.running) { + if (g_app.is_print) draw_print_dialog(); + else draw_file_dialog(); + g_app.win.present(); + + Montauk::WinEvent ev; + int rc = g_app.win.poll(&ev); + if (rc < 0) { + dialog_cancel(); + break; + } + if (rc == 0) { + montauk::sleep_ms(16); + continue; + } + + if (ev.type == 3) { + dialog_cancel(); + break; + } + + if (ev.type == 2 || ev.type == 4) { + continue; + } + + if (ev.type == 1) { + if (g_app.is_print) handle_print_mouse(ev); + else handle_file_mouse(ev); + continue; + } + + if (ev.type == 0) { + if (g_app.is_print) handle_print_key(ev.key); + else handle_file_key(ev.key); + } + } + + cleanup(); + montauk::exit(0); +} diff --git a/programs/src/dialogs/manifest.toml b/programs/src/dialogs/manifest.toml new file mode 100644 index 0000000..65498bc --- /dev/null +++ b/programs/src/dialogs/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Dialogs" +binary = "dialogs.elf" +icon = "system-file-manager.svg" + +[menu] +category = "System" +visible = false diff --git a/programs/src/dialogs/stb_truetype_impl.cpp b/programs/src/dialogs/stb_truetype_impl.cpp new file mode 100644 index 0000000..128dadd --- /dev/null +++ b/programs/src/dialogs/stb_truetype_impl.cpp @@ -0,0 +1,32 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/programs/src/pdfviewer/main.cpp b/programs/src/pdfviewer/main.cpp index cb47196..e1b59e9 100644 --- a/programs/src/pdfviewer/main.cpp +++ b/programs/src/pdfviewer/main.cpp @@ -5,6 +5,7 @@ */ #include "pdfviewer.h" +#include #include // ============================================================================ @@ -37,14 +38,15 @@ char g_status_msg[128] = {}; // Toolbar hit-testing constants // ============================================================================ -// Toolbar buttons: Open | < > | - + +// Toolbar buttons: Open | Print | < > | - + static constexpr int TB_OPEN_X0 = 4, TB_OPEN_X1 = 40; -// separator at 48 -static constexpr int TB_PREV_X0 = 56, TB_PREV_X1 = 80; -static constexpr int TB_NEXT_X0 = 84, TB_NEXT_X1 = 108; -// separator at 116 -static constexpr int TB_ZOUT_X0 = 124, TB_ZOUT_X1 = 148; -static constexpr int TB_ZIN_X0 = 152, TB_ZIN_X1 = 176; +static constexpr int TB_PRINT_X0 = 44, TB_PRINT_X1 = 80; +// separator at 88 +static constexpr int TB_PREV_X0 = 96, TB_PREV_X1 = 120; +static constexpr int TB_NEXT_X0 = 124, TB_NEXT_X1 = 148; +// separator at 156 +static constexpr int TB_ZOUT_X0 = 164, TB_ZOUT_X1 = 188; +static constexpr int TB_ZIN_X0 = 192, TB_ZIN_X1 = 216; // ============================================================================ // Helpers @@ -66,6 +68,15 @@ static void clamp_scroll() { if (g_scroll_y > ms) g_scroll_y = ms; } +static const char* file_basename(const char* path) { + if (!path || !path[0]) return ""; + const char* name = path; + for (int i = 0; path[i]; i++) { + if (path[i] == '/') name = path + i + 1; + } + return name; +} + static void go_to_page(int page) { if (page < 0) page = 0; if (page >= g_doc.page_count) page = g_doc.page_count - 1; @@ -99,6 +110,40 @@ static void open_file(const char* path) { } } +static void open_inline_pathbar(const char* initial_text) { + g_pathbar_open = true; + if (!initial_text) initial_text = ""; + str_cpy(g_pathbar_text, initial_text, sizeof(g_pathbar_text)); + g_pathbar_len = str_len(g_pathbar_text); + g_pathbar_cursor = g_pathbar_len; +} + +static void open_file_dialog() { + char path[256] = {}; + if (dialogs::open_file("Open PDF", g_filepath, path, sizeof(path), g_status_msg, sizeof(g_status_msg))) { + open_file(path); + } else if (g_status_msg[0]) { + open_inline_pathbar(g_filepath); + } +} + +static void print_file_dialog() { + if (!g_filepath[0]) { + str_cpy(g_status_msg, "Open a PDF before printing", sizeof(g_status_msg)); + return; + } + + char job_id[64] = {}; + char msg[128] = {}; + if (dialogs::print_file("Print PDF", g_filepath, file_basename(g_filepath), + job_id, sizeof(job_id), msg, sizeof(msg))) { + if (msg[0]) str_cpy(g_status_msg, msg, sizeof(g_status_msg)); + else str_cpy(g_status_msg, "Print job queued", sizeof(g_status_msg)); + } else if (msg[0]) { + str_cpy(g_status_msg, msg, sizeof(g_status_msg)); + } +} + // ============================================================================ // Entry point // ============================================================================ @@ -126,9 +171,7 @@ extern "C" void _start() { // Build window title char title[64] = "PDF Viewer"; if (g_filepath[0]) { - const char* fname = g_filepath; - for (int i = 0; g_filepath[i]; i++) - if (g_filepath[i] == '/') fname = g_filepath + i + 1; + const char* fname = file_basename(g_filepath); snprintf(title, 64, "%s - PDF Viewer", fname); } @@ -211,10 +254,12 @@ extern "C" void _start() { } // Ctrl+O: open file else if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O' || key.ascii == 15)) { - g_pathbar_open = true; - g_pathbar_text[0] = '\0'; - g_pathbar_len = 0; - g_pathbar_cursor = 0; + open_file_dialog(); + redraw = true; + } + // Ctrl+P: print file + else if (key.ctrl && (key.ascii == 'p' || key.ascii == 'P' || key.ascii == 16)) { + print_file_dialog(); redraw = true; } // Page Down / Right arrow: next page @@ -288,10 +333,10 @@ extern "C" void _start() { if (clicked && my < TOOLBAR_H && my >= TB_BTN_Y && my < TB_BTN_Y + TB_BTN_SIZE) { if (mx >= TB_OPEN_X0 && mx < TB_OPEN_X1) { - g_pathbar_open = true; - g_pathbar_text[0] = '\0'; - g_pathbar_len = 0; - g_pathbar_cursor = 0; + open_file_dialog(); + redraw = true; + } else if (mx >= TB_PRINT_X0 && mx < TB_PRINT_X1) { + print_file_dialog(); redraw = true; } else if (mx >= TB_PREV_X0 && mx < TB_PREV_X1 && g_doc.valid) { go_to_page(g_current_page - 1); diff --git a/programs/src/pdfviewer/render.cpp b/programs/src/pdfviewer/render.cpp index d382dbf..7cd464a 100644 --- a/programs/src/pdfviewer/render.cpp +++ b/programs/src/pdfviewer/render.cpp @@ -180,8 +180,9 @@ void render(uint32_t* pixels) { bx += 8; }; - // Open + // Open / Print tb_btn(36, false, "Open"); + tb_btn(36, false, "Print"); tb_sep(); // Navigation diff --git a/programs/src/spreadsheet/main.cpp b/programs/src/spreadsheet/main.cpp index fefbfb5..7330a7b 100644 --- a/programs/src/spreadsheet/main.cpp +++ b/programs/src/spreadsheet/main.cpp @@ -5,6 +5,7 @@ */ #include "spreadsheet.h" +#include #include // ============================================================================ @@ -131,6 +132,41 @@ bool hit_fill_handle(int mx, int my) { my >= fhy - 3 && my <= fhy + FILL_HANDLE_SIZE + 3; } +static void open_inline_pathbar(bool save_mode, const char* initial_text) { + g_pathbar_open = true; + g_pathbar_save = save_mode; + if (!initial_text) initial_text = ""; + str_cpy(g_pathbar_text, initial_text, sizeof(g_pathbar_text)); + g_pathbar_len = str_len(g_pathbar_text); + g_pathbar_cursor = g_pathbar_len; +} + +static void open_file_dialog() { + char path[256] = {}; + char msg[128] = {}; + if (dialogs::open_file("Open Spreadsheet", g_filepath, path, sizeof(path), msg, sizeof(msg))) { + load_file(path); + } else if (msg[0]) { + open_inline_pathbar(false, g_filepath); + } +} + +static void save_file_dialog() { + if (g_filepath[0]) { + save_file(); + return; + } + + char path[256] = {}; + char msg[128] = {}; + if (dialogs::save_file("Save Spreadsheet", "", "untitled.mss", path, sizeof(path), msg, sizeof(msg))) { + str_cpy(g_filepath, path, 256); + save_file(); + } else if (msg[0]) { + open_inline_pathbar(true, "untitled.mss"); + } +} + bool handle_toolbar_click(int mx, int my) { if (my >= TOOLBAR_H || my < TB_BTN_Y || my >= TB_BTN_Y + TB_BTN_SIZE) return false; @@ -140,22 +176,10 @@ bool handle_toolbar_click(int mx, int my) { if (mx >= TB_OPEN_X0 && mx < TB_OPEN_X1) { if (g_editing) commit_edit(); - g_pathbar_open = true; - g_pathbar_save = false; - g_pathbar_text[0] = '\0'; - g_pathbar_len = 0; - g_pathbar_cursor = 0; + open_file_dialog(); } else if (mx >= TB_SAVE_X0 && mx < TB_SAVE_X1) { if (g_editing) commit_edit(); - if (g_filepath[0]) { - save_file(); - } else { - g_pathbar_open = true; - g_pathbar_save = true; - str_cpy(g_pathbar_text, "0:/", 256); - g_pathbar_len = str_len(g_pathbar_text); - g_pathbar_cursor = g_pathbar_len; - } + save_file_dialog(); } else if (mx >= TB_CUT_X0 && mx < TB_CUT_X1) { if (!g_editing) cut_selection(); } else if (mx >= TB_COPY_X0 && mx < TB_COPY_X1) { @@ -556,24 +580,12 @@ extern "C" void _start() { // Ctrl+S: save (or Save As if no path) else if (key.ctrl && (key.ascii == 's' || key.ascii == 'S' || key.ascii == 19)) { if (g_editing) commit_edit(); - if (g_filepath[0]) { - save_file(); - } else { - g_pathbar_open = true; - g_pathbar_save = true; - str_cpy(g_pathbar_text, "0:/", 256); - g_pathbar_len = str_len(g_pathbar_text); - g_pathbar_cursor = g_pathbar_len; - } + save_file_dialog(); redraw = true; } // Ctrl+O: open else if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O' || key.ascii == 15) && !g_editing) { - g_pathbar_open = true; - g_pathbar_save = false; - g_pathbar_text[0] = '\0'; - g_pathbar_len = 0; - g_pathbar_cursor = 0; + open_file_dialog(); redraw = true; } // Printable character: start editing or insert diff --git a/programs/src/texteditor/main.cpp b/programs/src/texteditor/main.cpp index 287d31b..7aba120 100644 --- a/programs/src/texteditor/main.cpp +++ b/programs/src/texteditor/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include extern "C" { @@ -87,6 +88,7 @@ int g_scroll_y = 0; // first visible line int g_scroll_x = 0; // horizontal scroll in pixels bool g_modified = false; bool g_cursor_moved = true; +char g_status_msg[128] = {}; // File char g_filepath[256] = {}; @@ -434,24 +436,76 @@ static void ensure_cursor_visible(int visible_lines, int text_area_w) { // File I/O // ============================================================================ -static void load_file(const char* path) { +static void set_status(const char* msg) { + if (!msg) msg = ""; + montauk::strncpy(g_status_msg, msg, sizeof(g_status_msg) - 1); + g_status_msg[sizeof(g_status_msg) - 1] = '\0'; +} + +static bool looks_binary(const char* data, int len) { + if (!data || len <= 0) return false; + + int control_count = 0; + for (int i = 0; i < len; i++) { + unsigned char ch = (unsigned char)data[i]; + if (ch == 0) return true; + if (ch < 32 && ch != '\n' && ch != '\r' && ch != '\t' && ch != '\f') + control_count++; + } + + return control_count * 20 > len; +} + +static bool load_file(const char* path) { int fd = montauk::open(path); - if (fd < 0) return; + if (fd < 0) { + set_status("Failed to open file"); + return false; + } uint64_t size = montauk::getsize(fd); - if (size > (uint64_t)MAX_CAP) size = MAX_CAP; + if (size > (uint64_t)MAX_CAP) { + montauk::close(fd); + set_status("File is too large for Text Editor"); + return false; + } - if ((int)size >= g_buf_cap) { - int new_cap = (int)size + 1024; + int read_len = (int)size; + int temp_cap = read_len + 1; + if (temp_cap < 1024) temp_cap = 1024; + char* temp = (char*)montauk::malloc(temp_cap); + if (!temp) { + montauk::close(fd); + set_status("Out of memory while opening file"); + return false; + } + + int got = montauk::read(fd, (uint8_t*)temp, 0, size); + montauk::close(fd); + if (got < 0) { + montauk::mfree(temp); + set_status("Failed to read file"); + return false; + } + + temp[got] = '\0'; + if (looks_binary(temp, got)) { + montauk::mfree(temp); + set_status("Binary files cannot be opened in Text Editor"); + return false; + } + + if (got >= g_buf_cap) { + int new_cap = got + 1024; if (new_cap > MAX_CAP) new_cap = MAX_CAP; g_buffer = (char*)montauk::realloc(g_buffer, new_cap); g_buf_cap = new_cap; } - montauk::read(fd, (uint8_t*)g_buffer, 0, size); - montauk::close(fd); + montauk::memcpy(g_buffer, temp, (uint64_t)(got + 1)); + montauk::mfree(temp); - g_buf_len = (int)size; + g_buf_len = got; g_cursor_pos = 0; g_scroll_y = 0; g_scroll_x = 0; @@ -468,21 +522,28 @@ static void load_file(const char* path) { montauk::strncpy(g_filename, path, 63); g_syntax_language = syn_detect_language(g_filepath); + clear_selection(); + set_status(""); recompute_lines(); update_cursor_pos(); + return true; } static void save_file() { if (g_filepath[0] == '\0') return; int fd = montauk::fcreate(g_filepath); - if (fd < 0) return; + if (fd < 0) { + set_status("Failed to save file"); + return; + } montauk::fwrite(fd, (const uint8_t*)g_buffer, 0, g_buf_len); montauk::close(fd); g_modified = false; + set_status(""); } // ============================================================================ @@ -734,10 +795,16 @@ static void render(uint32_t* pixels) { // Left: filename char status_left[128]; - if (g_filename[0]) + if (g_status_msg[0]) { + if (g_filename[0]) + snprintf(status_left, 128, " %s%s | %s", g_filename, g_modified ? " [modified]" : "", g_status_msg); + else + snprintf(status_left, 128, " Untitled%s | %s", g_modified ? " [modified]" : "", g_status_msg); + } else if (g_filename[0]) { snprintf(status_left, 128, " %s%s", g_filename, g_modified ? " [modified]" : ""); - else + } else { snprintf(status_left, 128, " Untitled%s", g_modified ? " [modified]" : ""); + } if (g_font) g_font->draw_to_buffer(pixels, W, H, 4, sty, status_left, STATUS_TEXT, FONT_SIZE); @@ -778,20 +845,41 @@ static void pathbar_confirm(int win_id) { g_pathbar_open = false; } -static void open_pathbar_open() { +static void open_inline_pathbar(bool save_mode, const char* initial_text) { g_pathbar_open = true; - g_pathbar_save = false; - montauk::strncpy(g_pathbar_text, g_filepath, 255); + g_pathbar_save = save_mode; + if (!initial_text) initial_text = ""; + montauk::strncpy(g_pathbar_text, initial_text, 255); g_pathbar_len = montauk::slen(g_pathbar_text); g_pathbar_cursor = g_pathbar_len; } +static void open_pathbar_open() { + char path[256] = {}; + char msg[128] = {}; + if (dialogs::open_file("Open File", g_filepath, path, sizeof(path), msg, sizeof(msg))) { + load_file(path); + } else if (msg[0]) { + open_inline_pathbar(false, g_filepath); + } +} + static void open_pathbar_save() { - g_pathbar_open = true; - g_pathbar_save = true; - g_pathbar_text[0] = '\0'; - g_pathbar_len = 0; - g_pathbar_cursor = 0; + char path[256] = {}; + const char* initial_path = g_filepath[0] ? g_filepath : ""; + const char* suggested_name = g_filename[0] ? g_filename : "untitled.txt"; + char msg[128] = {}; + if (dialogs::save_file("Save File", initial_path, suggested_name, path, sizeof(path), msg, sizeof(msg))) { + montauk::strncpy(g_filepath, path, 255); + const char* name = path; + for (int i = 0; path[i]; i++) + if (path[i] == '/') name = path + i + 1; + montauk::strncpy(g_filename, name, 63); + g_syntax_language = syn_detect_language(g_filepath); + save_file(); + } else if (msg[0]) { + open_inline_pathbar(true, initial_path[0] ? initial_path : suggested_name); + } } // ============================================================================ diff --git a/programs/src/wordprocessor/document.cpp b/programs/src/wordprocessor/document.cpp index a1bd602..75f6468 100644 --- a/programs/src/wordprocessor/document.cpp +++ b/programs/src/wordprocessor/document.cpp @@ -5,6 +5,7 @@ */ #include "wordprocessor.hpp" +#include static inline int wp_min_int(int a, int b) { return a < b ? a : b; @@ -1203,14 +1204,29 @@ void wp_set_filepath(WordProcessorState* wp, const char* path) { montauk::strncpy(wp->filename, path, 63); } -void wp_open_save_pathbar(WordProcessorState* wp) { +void wp_start_pathbar(WordProcessorState* wp, bool save_mode, const char* initial_text) { + if (!initial_text) initial_text = ""; wp->show_pathbar = true; - wp->pathbar_save_mode = true; - montauk::strncpy(wp->pathbar_text, wp->filepath, 255); + wp->pathbar_save_mode = save_mode; + montauk::strncpy(wp->pathbar_text, initial_text, 255); + wp->pathbar_text[255] = '\0'; wp->pathbar_len = montauk::slen(wp->pathbar_text); wp->pathbar_cursor = wp->pathbar_len; } +void wp_open_save_pathbar(WordProcessorState* wp) { + char path[256] = {}; + const char* initial_path = wp->filepath[0] ? wp->filepath : ""; + const char* suggested_name = wp->filename[0] ? wp->filename : "document.mwp"; + char msg[160] = {}; + if (dialogs::save_file("Save Document", initial_path, suggested_name, path, sizeof(path), msg, sizeof(msg))) { + wp_set_filepath(wp, path); + wp_save_file(wp); + } else if (msg[0]) { + wp_start_pathbar(wp, true, initial_path[0] ? initial_path : suggested_name); + } +} + static int wp_serialized_size(WordProcessorState* wp) { int size = 4 + 2 + 1 + 1 + 1 + 2 + 2; for (int i = 0; i < wp->run_count; i++) diff --git a/programs/src/wordprocessor/input.cpp b/programs/src/wordprocessor/input.cpp index 3b067e2..22b2270 100644 --- a/programs/src/wordprocessor/input.cpp +++ b/programs/src/wordprocessor/input.cpp @@ -5,14 +5,15 @@ */ #include "wordprocessor.hpp" +#include static void wp_open_pathbar_for_open(WordProcessorState* wp) { - wp->show_pathbar = !wp->show_pathbar; - wp->pathbar_save_mode = false; - if (wp->show_pathbar) { - montauk::strncpy(wp->pathbar_text, wp->filepath, 255); - wp->pathbar_len = montauk::slen(wp->pathbar_text); - wp->pathbar_cursor = wp->pathbar_len; + char path[256] = {}; + char msg[160] = {}; + if (dialogs::open_file("Open Document", wp->filepath, path, sizeof(path), msg, sizeof(msg))) { + wp_load_file(wp, path); + } else if (msg[0]) { + wp_start_pathbar(wp, false, wp->filepath); } } diff --git a/programs/src/wordprocessor/wordprocessor.hpp b/programs/src/wordprocessor/wordprocessor.hpp index 7c0384b..7e9c0c3 100644 --- a/programs/src/wordprocessor/wordprocessor.hpp +++ b/programs/src/wordprocessor/wordprocessor.hpp @@ -346,6 +346,7 @@ int wp_wrap_line_start(WordProcessorState* wp, int line_idx); void wp_ensure_cursor_visible(WordProcessorState* wp, int view_h); void wp_set_filepath(WordProcessorState* wp, const char* path); +void wp_start_pathbar(WordProcessorState* wp, bool save_mode, const char* initial_text); void wp_open_save_pathbar(WordProcessorState* wp); void wp_save_file(WordProcessorState* wp); void wp_load_file(WordProcessorState* wp, const char* path); diff --git a/scripts/install_apps.sh b/scripts/install_apps.sh index 2f1486a..91b9976 100755 --- a/scripts/install_apps.sh +++ b/scripts/install_apps.sh @@ -35,6 +35,7 @@ APPS=( "procmgr|apps/scalable/system-monitor.svg" "calculator|apps/scalable/accessories-calculator.svg" "printers|devices/scalable/preferences-devices-printer.svg" + "dialogs|apps/scalable/system-file-manager.svg" "rpgdemo|apps/scalable/utilities-terminal.svg" "paint|apps/scalable/kolourpaint.svg" "screenshot|apps/scalable/gnome-screenshot.svg"