diff --git a/kernel/src/Api/WinServer.cpp b/kernel/src/Api/WinServer.cpp index 5de78a8..0cd7275 100644 --- a/kernel/src/Api/WinServer.cpp +++ b/kernel/src/Api/WinServer.cpp @@ -284,6 +284,17 @@ namespace WinServer { return -1; } + if (slot.liveSurface != nullptr) { + Ipc::CopySurfacePreserve(newLive, newW, newH, + slot.liveSurface, slot.width, slot.height, + 0xFFFFFFFFu); + } + if (slot.snapshotSurface != nullptr) { + Ipc::CopySurfacePreserve(newSnapshot, newW, newH, + slot.snapshotSurface, slot.width, slot.height, + 0xFFFFFFFFu); + } + if (slot.liveSurface != nullptr) { Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerPml4); Ipc::ReleaseSurface(slot.liveSurface); diff --git a/kernel/src/Ipc/Ipc.cpp b/kernel/src/Ipc/Ipc.cpp index d096902..8f66828 100644 --- a/kernel/src/Ipc/Ipc.cpp +++ b/kernel/src/Ipc/Ipc.cpp @@ -118,6 +118,15 @@ namespace Ipc { uint32_t numPages; }; + static inline uint32_t* SurfacePixelPtr(Surface* surface, uint64_t pixelIndex) { + if (surface == nullptr) return nullptr; + uint64_t byteOffset = pixelIndex * sizeof(uint32_t); + uint32_t pageIndex = (uint32_t)(byteOffset / 0x1000ULL); + uint32_t pageOffset = (uint32_t)(byteOffset % 0x1000ULL); + if (pageIndex >= surface->numPages) return nullptr; + return (uint32_t*)((uint8_t*)Memory::HHDM(surface->physPages[pageIndex]) + pageOffset); + } + static HandleEntry g_handleTables[Sched::MaxProcesses][MaxHandlesPerProcess] = {}; static SurfaceMap g_surfaceMaps[Sched::MaxProcesses][MaxSurfaceMapsPerProcess] = {}; @@ -1381,6 +1390,40 @@ namespace Ipc { return 0; } + int CopySurfacePreserve(Surface* dst, int dstWidth, int dstHeight, + Surface* src, int srcWidth, int srcHeight, + uint32_t fillPixel) { + if (dst == nullptr || src == nullptr) return -1; + if (dstWidth <= 0 || dstHeight <= 0 || srcWidth <= 0 || srcHeight <= 0) return -1; + + uint64_t dstPixels = (uint64_t)dstWidth * (uint64_t)dstHeight; + uint64_t srcPixels = (uint64_t)srcWidth * (uint64_t)srcHeight; + if (dstPixels * sizeof(uint32_t) > dst->sizeBytes) return -1; + if (srcPixels * sizeof(uint32_t) > src->sizeBytes) return -1; + + for (uint64_t i = 0; i < dstPixels; i++) { + uint32_t* p = SurfacePixelPtr(dst, i); + if (p != nullptr) *p = fillPixel; + } + + int copyWidth = (dstWidth < srcWidth) ? dstWidth : srcWidth; + int copyHeight = (dstHeight < srcHeight) ? dstHeight : srcHeight; + for (int y = 0; y < copyHeight; y++) { + uint64_t dstRow = (uint64_t)y * (uint64_t)dstWidth; + uint64_t srcRow = (uint64_t)y * (uint64_t)srcWidth; + for (int x = 0; x < copyWidth; x++) { + uint32_t* srcPixel = SurfacePixelPtr(src, srcRow + (uint64_t)x); + uint32_t* dstPixel = SurfacePixelPtr(dst, dstRow + (uint64_t)x); + if (srcPixel != nullptr && dstPixel != nullptr) { + *dstPixel = *srcPixel; + } + } + } + + NotifyObjectChanged((Object*)dst); + return 0; + } + int MapSurfaceForPid(Surface* surface, int pid, uint64_t pml4Phys, uint64_t& heapNext, uint64_t& outVa) { if (surface == nullptr) return -1; diff --git a/kernel/src/Ipc/Ipc.hpp b/kernel/src/Ipc/Ipc.hpp index 92b5555..db3cfb7 100644 --- a/kernel/src/Ipc/Ipc.hpp +++ b/kernel/src/Ipc/Ipc.hpp @@ -123,6 +123,9 @@ namespace Ipc { uint64_t GetSurfaceSize(const Surface* surface); int ResizeSurface(Surface* surface, uint64_t newSize); int CopySurface(Surface* dst, Surface* src); + int CopySurfacePreserve(Surface* dst, int dstWidth, int dstHeight, + Surface* src, int srcWidth, int srcHeight, + uint32_t fillPixel); uint64_t MapSurfaceHandle(int handle); int ResizeSurfaceHandle(int handle, uint64_t newSize); int MapSurfaceForPid(Surface* surface, int pid, uint64_t pml4Phys, uint64_t& heapNext, uint64_t& outVa); diff --git a/programs/include/print/print.hpp b/programs/include/print/print.hpp index 3fd9113..e941610 100644 --- a/programs/include/print/print.hpp +++ b/programs/include/print/print.hpp @@ -31,6 +31,7 @@ static constexpr const char* SPOOL_FAILED_DIR = "0:/spool/print/failed"; static constexpr const char* SPOOL_DOCS_DIR = "0:/spool/print/docs"; static constexpr const char* SPOOL_TMP_DIR = "0:/spool/print/tmp"; static constexpr const char* SPOOL_DAEMON_PID_PATH = "0:/spool/print/daemon.pid"; +static constexpr const char* SPOOL_PROBE_STATE_PATH = "0:/spool/print/printer-probe.state"; static constexpr const char* PRINTD_BINARY = "0:/os/printd.elf"; static constexpr int MAX_PATH_LEN = 256; static constexpr int MAX_JOB_NAME_LEN = 128; @@ -88,6 +89,15 @@ struct IppCapabilities { char preferred_format[64]; }; +struct ProbeState { + bool ok; + char printer_uri[MAX_PATH_LEN]; + char probed_at[32]; + char message[128]; + char detail[MAX_DEBUG_LEN]; + IppCapabilities caps; +}; + struct IppPrintResult { bool ok; bool use_tls; @@ -120,6 +130,10 @@ inline void zero_job(JobMeta* job) { if (job) memset(job, 0, sizeof(*job)); } +inline void zero_probe_state(ProbeState* state) { + if (state) memset(state, 0, sizeof(*state)); +} + inline void safe_copy(char* dst, int dst_len, const char* src) { if (dst == nullptr || dst_len <= 0) return; if (src == nullptr) { @@ -697,6 +711,112 @@ inline bool load_job_from_path(const char* path, JobMeta* job) { return job->id[0] != '\0'; } +inline bool save_probe_state_atomic(const char* path, const ProbeState* state) { + if (path == nullptr || state == nullptr) return false; + char tmp[MAX_PATH_LEN]; + snprintf(tmp, sizeof(tmp), "%s.tmp", path); + + FILE* f = fopen(tmp, "wb"); + if (!f) return false; + + ProbeState copy = *state; + sanitize_field(copy.printer_uri); + sanitize_field(copy.probed_at); + sanitize_field(copy.message); + sanitize_field(copy.detail); + sanitize_field(copy.caps.host); + sanitize_field(copy.caps.path); + sanitize_field(copy.caps.printer_name); + sanitize_field(copy.caps.status_message); + sanitize_field(copy.caps.supported_formats); + sanitize_field(copy.caps.default_format); + sanitize_field(copy.caps.preferred_format); + + fprintf(f, "ok=%d\n", copy.ok ? 1 : 0); + fprintf(f, "printer_uri=%s\n", copy.printer_uri); + fprintf(f, "probed_at=%s\n", copy.probed_at); + fprintf(f, "message=%s\n", copy.message); + fprintf(f, "detail=%s\n", copy.detail); + fprintf(f, "caps_ok=%d\n", copy.caps.ok ? 1 : 0); + fprintf(f, "caps_supports_pdf=%d\n", copy.caps.supports_pdf ? 1 : 0); + fprintf(f, "caps_supports_text=%d\n", copy.caps.supports_text ? 1 : 0); + fprintf(f, "caps_supports_jpeg=%d\n", copy.caps.supports_jpeg ? 1 : 0); + fprintf(f, "caps_use_tls=%d\n", copy.caps.use_tls ? 1 : 0); + fprintf(f, "caps_http_status=%d\n", copy.caps.http_status); + fprintf(f, "caps_ipp_status=%u\n", (unsigned)copy.caps.ipp_status); + fprintf(f, "caps_request_id=%d\n", copy.caps.request_id); + fprintf(f, "caps_resolved_ip=%u\n", (unsigned)copy.caps.resolved_ip); + fprintf(f, "caps_port=%u\n", (unsigned)copy.caps.port); + fprintf(f, "caps_host=%s\n", copy.caps.host); + fprintf(f, "caps_path=%s\n", copy.caps.path); + fprintf(f, "caps_printer_name=%s\n", copy.caps.printer_name); + fprintf(f, "caps_status_message=%s\n", copy.caps.status_message); + fprintf(f, "caps_supported_formats=%s\n", copy.caps.supported_formats); + fprintf(f, "caps_default_format=%s\n", copy.caps.default_format); + fprintf(f, "caps_preferred_format=%s\n", copy.caps.preferred_format); + fclose(f); + + if (rename(tmp, path) != 0) { + remove(tmp); + return false; + } + return true; +} + +inline bool load_probe_state(const char* path, ProbeState* state) { + if (path == nullptr || state == nullptr) return false; + zero_probe_state(state); + + FILE* f = fopen(path, "rb"); + if (!f) return false; + + char line[512]; + while (fgets(line, sizeof(line), f) != nullptr) { + trim_line(line); + char* eq = strchr(line, '='); + if (!eq) continue; + *eq++ = '\0'; + + if (strcmp(line, "ok") == 0) state->ok = atoi(eq) != 0; + else if (strcmp(line, "printer_uri") == 0) safe_copy(state->printer_uri, sizeof(state->printer_uri), eq); + else if (strcmp(line, "probed_at") == 0) safe_copy(state->probed_at, sizeof(state->probed_at), eq); + else if (strcmp(line, "message") == 0) safe_copy(state->message, sizeof(state->message), eq); + else if (strcmp(line, "detail") == 0) safe_copy(state->detail, sizeof(state->detail), eq); + else if (strcmp(line, "caps_ok") == 0) state->caps.ok = atoi(eq) != 0; + else if (strcmp(line, "caps_supports_pdf") == 0) state->caps.supports_pdf = atoi(eq) != 0; + else if (strcmp(line, "caps_supports_text") == 0) state->caps.supports_text = atoi(eq) != 0; + else if (strcmp(line, "caps_supports_jpeg") == 0) state->caps.supports_jpeg = atoi(eq) != 0; + else if (strcmp(line, "caps_use_tls") == 0) state->caps.use_tls = atoi(eq) != 0; + else if (strcmp(line, "caps_http_status") == 0) state->caps.http_status = atoi(eq); + else if (strcmp(line, "caps_ipp_status") == 0) state->caps.ipp_status = (uint16_t)atoi(eq); + else if (strcmp(line, "caps_request_id") == 0) state->caps.request_id = atoi(eq); + else if (strcmp(line, "caps_resolved_ip") == 0) state->caps.resolved_ip = (uint32_t)strtoul(eq, nullptr, 10); + else if (strcmp(line, "caps_port") == 0) state->caps.port = (uint16_t)atoi(eq); + else if (strcmp(line, "caps_host") == 0) safe_copy(state->caps.host, sizeof(state->caps.host), eq); + else if (strcmp(line, "caps_path") == 0) safe_copy(state->caps.path, sizeof(state->caps.path), eq); + else if (strcmp(line, "caps_printer_name") == 0) safe_copy(state->caps.printer_name, sizeof(state->caps.printer_name), eq); + else if (strcmp(line, "caps_status_message") == 0) safe_copy(state->caps.status_message, sizeof(state->caps.status_message), eq); + else if (strcmp(line, "caps_supported_formats") == 0) safe_copy(state->caps.supported_formats, sizeof(state->caps.supported_formats), eq); + else if (strcmp(line, "caps_default_format") == 0) safe_copy(state->caps.default_format, sizeof(state->caps.default_format), eq); + else if (strcmp(line, "caps_preferred_format") == 0) safe_copy(state->caps.preferred_format, sizeof(state->caps.preferred_format), eq); + } + fclose(f); + return state->printer_uri[0] != '\0' || state->message[0] != '\0'; +} + +inline bool save_printer_probe_state(const ProbeState* state) { + ensure_spool_dirs(); + return save_probe_state_atomic(SPOOL_PROBE_STATE_PATH, state); +} + +inline bool load_printer_probe_state(ProbeState* state) { + return load_probe_state(SPOOL_PROBE_STATE_PATH, state); +} + +inline void clear_printer_probe_state() { + remove(SPOOL_PROBE_STATE_PATH); +} + inline bool find_job(const char* id, char* out_path, int out_path_len, char* out_state, int out_state_len) { if (id == nullptr || *id == '\0') return false; const struct { @@ -1321,6 +1441,18 @@ inline void summarize_ipp_capabilities(const IppCapabilities* caps, char* out, i caps->supports_text ? "yes" : "no"); } +inline void summarize_probe_transport(const IppUri* uri, const char* resolution, char* out, int out_len) { + if (out == nullptr || out_len <= 0) return; + out[0] = '\0'; + if (uri == nullptr) return; + snprintf(out, (size_t)out_len, "host=%s ip=%s port=%u path=%s %s", + uri->host, + (resolution && *resolution) ? resolution : "unresolved", + (unsigned)uri->port, + uri->path[0] ? uri->path : "/", + uri->use_tls ? "tls" : "plain"); +} + inline void summarize_ipp_print_result(const IppPrintResult* result, char* out, int out_len) { if (out == nullptr || out_len <= 0) return; out[0] = '\0'; @@ -1516,6 +1648,64 @@ inline bool ipp_get_printer_capabilities(const char* printer_uri, return true; } +inline bool probe_printer_uri(const char* printer_uri, ProbeState* out, char* err, int err_len) { + if (out == nullptr) return false; + zero_probe_state(out); + now_string(out->probed_at, sizeof(out->probed_at)); + if (printer_uri && *printer_uri) safe_copy(out->printer_uri, sizeof(out->printer_uri), printer_uri); + + if (printer_uri == nullptr || *printer_uri == '\0') { + safe_copy(out->message, sizeof(out->message), "No printer configured"); + safe_copy(err, err_len, out->message); + return false; + } + + IppUri normalized = {}; + char local_err[128] = {}; + if (!normalize_ipp_uri(printer_uri, &normalized, local_err, sizeof(local_err))) { + safe_copy(out->message, sizeof(out->message), local_err); + safe_copy(err, err_len, local_err); + return false; + } + + safe_copy(out->printer_uri, sizeof(out->printer_uri), normalized.normalized); + + uint32_t ip = 0; + if (!resolve_host(normalized.host, &ip)) { + summarize_probe_transport(&normalized, "unresolved", out->detail, sizeof(out->detail)); + if (host_looks_like_mdns(normalized.host)) + safe_copy(out->message, sizeof(out->message), ".local/mDNS hostnames are not supported yet"); + else + snprintf(out->message, sizeof(out->message), "Could not resolve %s", normalized.host); + safe_copy(err, err_len, out->message); + return false; + } + + char ip_text[20]; + format_ipv4(ip_text, sizeof(ip_text), ip); + summarize_probe_transport(&normalized, ip_text, out->detail, sizeof(out->detail)); + + if (!ipp_get_printer_capabilities(normalized.normalized, &out->caps, local_err, sizeof(local_err))) { + if (out->caps.host[0] == '\0') safe_copy(out->caps.host, sizeof(out->caps.host), normalized.host); + if (out->caps.path[0] == '\0') safe_copy(out->caps.path, sizeof(out->caps.path), normalized.path); + if (out->caps.port == 0) out->caps.port = normalized.port; + out->caps.resolved_ip = ip; + out->caps.use_tls = normalized.use_tls; + char summary[MAX_DEBUG_LEN] = {}; + summarize_ipp_capabilities(&out->caps, summary, sizeof(summary)); + if (summary[0]) safe_copy(out->detail, sizeof(out->detail), summary); + safe_copy(out->message, sizeof(out->message), local_err[0] ? local_err : "IPP probe failed"); + safe_copy(err, err_len, out->message); + return false; + } + + summarize_ipp_capabilities(&out->caps, out->detail, sizeof(out->detail)); + safe_copy(out->message, sizeof(out->message), "Printer probe succeeded"); + out->ok = true; + if (err && err_len > 0) err[0] = '\0'; + return true; +} + inline bool ipp_print_buffer(const char* printer_uri, const char* job_name, const char* user_name, diff --git a/programs/src/devexplorer/Makefile b/programs/src/devexplorer/Makefile index 0c29ee7..c1b2854 100644 --- a/programs/src/devexplorer/Makefile +++ b/programs/src/devexplorer/Makefile @@ -47,6 +47,7 @@ CXXFLAGS := \ -mcmodel=small \ -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 @@ -63,7 +64,7 @@ LDFLAGS := \ # ---- Source files ---- -SRCS := main.cpp render.cpp diskdetail.cpp stb_truetype_impl.cpp +SRCS := main.cpp render.cpp diskdetail.cpp printers.cpp stb_truetype_impl.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/devexplorer/devexplorer.h b/programs/src/devexplorer/devexplorer.h index 33bbf75..fb586db 100644 --- a/programs/src/devexplorer/devexplorer.h +++ b/programs/src/devexplorer/devexplorer.h @@ -29,11 +29,27 @@ static constexpr int TOOLBAR_H = 36; static constexpr int CAT_H = 28; static constexpr int ITEM_H = 24; static constexpr int MAX_DEVS = 64; +static constexpr int MAX_VIRTUAL_DEVS = 4; +static constexpr int MAX_TOTAL_DEVS = MAX_DEVS + MAX_VIRTUAL_DEVS; static constexpr int POLL_MS = 2000; static constexpr int INDENT = 28; static constexpr int FONT_SIZE = 18; -static constexpr int NUM_CATEGORIES = 9; +enum DeviceCategory { + CAT_CPU = 0, + CAT_INTERRUPT, + CAT_TIMER, + CAT_INPUT, + CAT_USB, + CAT_NETWORK, + CAT_DISPLAY, + CAT_STORAGE, + CAT_PCI, + CAT_AUDIO, + CAT_ACPI, + CAT_PRINTER, + NUM_CATEGORIES +}; static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); @@ -48,27 +64,33 @@ static constexpr Color ACCENT_COLOR = Color::from_rgb(0x33, 0x66, 0xCC); // ============================================================================ static const char* category_names[] = { - "CPU", // 0 - "Interrupt", // 1 - "Timer", // 2 - "Input", // 3 - "USB", // 4 - "Network", // 5 - "Display", // 6 - "Storage", // 7 - "PCI", // 8 + "CPU", + "Interrupt", + "Timer", + "Input", + "USB", + "Network", + "Display", + "Storage", + "PCI", + "Audio", + "ACPI", + "Printers", }; static const Color category_colors[] = { - Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue - Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple - Color::from_rgb(0x22, 0x88, 0x22), // Timer - green - Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber - Color::from_rgb(0x00, 0x88, 0x88), // USB - teal - Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange - Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo - Color::from_rgb(0x99, 0x55, 0x00), // Storage - brown - Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray + Color::from_rgb(0x33, 0x66, 0xCC), + Color::from_rgb(0x88, 0x44, 0xAA), + Color::from_rgb(0x22, 0x88, 0x22), + Color::from_rgb(0xCC, 0x88, 0x00), + Color::from_rgb(0x00, 0x88, 0x88), + Color::from_rgb(0xCC, 0x55, 0x22), + Color::from_rgb(0x44, 0x66, 0xCC), + Color::from_rgb(0x99, 0x55, 0x00), + Color::from_rgb(0x66, 0x66, 0x66), + Color::from_rgb(0x8A, 0x5A, 0xCC), + Color::from_rgb(0x55, 0x77, 0x88), + Color::from_rgb(0x2F, 0x8B, 0x57), }; // ============================================================================ @@ -83,7 +105,7 @@ struct DisplayRow { int dev_index; }; -static constexpr int MAX_DISPLAY_ROWS = MAX_DEVS + NUM_CATEGORIES; +static constexpr int MAX_DISPLAY_ROWS = MAX_TOTAL_DEVS + NUM_CATEGORIES; // ============================================================================ // Disk detail window @@ -110,7 +132,7 @@ struct DiskDetailState { // ============================================================================ struct DevExplorerState { - Montauk::DevInfo devs[MAX_DEVS]; + Montauk::DevInfo devs[MAX_TOTAL_DEVS]; int dev_count; bool collapsed[NUM_CATEGORIES]; int selected_row; @@ -142,6 +164,7 @@ void render(uint32_t* pixels); // ============================================================================ int build_display_rows(DevExplorerState* de, DisplayRow* rows); +int append_printer_devices(Montauk::DevInfo* out, int max_count); // ============================================================================ // Function declarations — diskdetail.cpp diff --git a/programs/src/devexplorer/main.cpp b/programs/src/devexplorer/main.cpp index 4ffe5ea..5ee1e54 100644 --- a/programs/src/devexplorer/main.cpp +++ b/programs/src/devexplorer/main.cpp @@ -17,6 +17,21 @@ int g_win_h = INIT_H; DevExplorerState g_state; TrueTypeFont* g_font = nullptr; +static void refresh_devices(DevExplorerState* de) { + if (de == nullptr) return; + + int kernel_count = montauk::devlist(de->devs, MAX_DEVS); + if (kernel_count < 0) kernel_count = 0; + if (kernel_count > MAX_DEVS) kernel_count = MAX_DEVS; + + int printer_count = append_printer_devices( + de->devs + kernel_count, MAX_TOTAL_DEVS - kernel_count); + if (printer_count < 0) printer_count = 0; + + de->dev_count = kernel_count + printer_count; + de->last_poll_ms = montauk::get_milliseconds(); +} + // ============================================================================ // Display row building // ============================================================================ @@ -92,7 +107,7 @@ static bool handle_list_click(int mx, int my) { bool is_double = (de.last_click_row == i) && (now - de.last_click_ms < 400); - if (is_double && de.devs[di].category == 7) { + if (is_double && de.devs[di].category == CAT_STORAGE) { int port = (int)de.devs[di]._pad[0]; open_disk_detail(port, de.devs[di].name); de.last_click_row = -1; @@ -201,8 +216,7 @@ extern "C" void _start() { } // Initial device poll - g_state.dev_count = montauk::devlist(g_state.devs, MAX_DEVS); - g_state.last_poll_ms = montauk::get_milliseconds(); + refresh_devices(&g_state); WsWindow win; if (!win.create("Devices", INIT_W, INIT_H)) @@ -221,8 +235,7 @@ extern "C" void _start() { // Poll for device refresh uint64_t now = montauk::get_milliseconds(); if (now - g_state.last_poll_ms >= POLL_MS) { - g_state.dev_count = montauk::devlist(g_state.devs, MAX_DEVS); - g_state.last_poll_ms = now; + refresh_devices(&g_state); redraw_main = true; } diff --git a/programs/src/devexplorer/printers.cpp b/programs/src/devexplorer/printers.cpp new file mode 100644 index 0000000..ec30e57 --- /dev/null +++ b/programs/src/devexplorer/printers.cpp @@ -0,0 +1,90 @@ +/* + * printers.cpp + * Virtual printer-device registration for the MontaukOS Device Explorer + * Copyright (c) 2026 Daniel Hammer + */ + +#include "devexplorer.h" +#include + +using namespace print; + +static void format_printer_detail(const ProbeState& probe, + const IppUri* normalized, + char* out, int out_len) { + if (out == nullptr || out_len <= 0) return; + out[0] = '\0'; + + const char* host = nullptr; + if (probe.caps.host[0]) host = probe.caps.host; + else if (normalized && normalized->host[0]) host = normalized->host; + + if (probe.ok) { + if (host && *host) + snprintf(out, (size_t)out_len, "Ready on %s", host); + else + snprintf(out, (size_t)out_len, "Ready"); + return; + } + + if (probe.message[0]) { + if (strstr(probe.message, "resolve")) + snprintf(out, (size_t)out_len, "Host resolution failed"); + else if (strstr(probe.message, "mDNS")) + snprintf(out, (size_t)out_len, "mDNS host not supported"); + else if (strstr(probe.message, "Unsupported format")) + snprintf(out, (size_t)out_len, "Unsupported format"); + else + safe_copy(out, out_len, probe.message); + return; + } + + if (host && *host) + snprintf(out, (size_t)out_len, "Awaiting probe on %s", host); + else + snprintf(out, (size_t)out_len, "Awaiting spooler probe"); +} + +int append_printer_devices(Montauk::DevInfo* out, int max_count) { + if (out == nullptr || max_count <= 0) return 0; + + char current_uri[MAX_PATH_LEN] = {}; + bool has_current_uri = read_default_printer_uri(current_uri, sizeof(current_uri)); + + ProbeState probe = {}; + bool has_probe = load_printer_probe_state(&probe); + if (has_probe && has_current_uri && !montauk::streq(probe.printer_uri, current_uri)) + has_probe = false; + + const char* effective_uri = nullptr; + if (has_current_uri) effective_uri = current_uri; + else if (has_probe && probe.printer_uri[0]) effective_uri = probe.printer_uri; + else return 0; + + IppUri normalized = {}; + char uri_err[128] = {}; + bool normalized_ok = normalize_ipp_uri(effective_uri, &normalized, uri_err, sizeof(uri_err)); + + Montauk::DevInfo dev = {}; + dev.category = CAT_PRINTER; + + if (has_probe && probe.caps.printer_name[0]) { + safe_copy(dev.name, sizeof(dev.name), probe.caps.printer_name); + } else if (normalized_ok && normalized.host[0]) { + safe_copy(dev.name, sizeof(dev.name), normalized.host); + } else { + safe_copy(dev.name, sizeof(dev.name), "Configured Printer"); + } + + if (has_probe) { + format_printer_detail(probe, normalized_ok ? &normalized : nullptr, + dev.detail, sizeof(dev.detail)); + } else if (normalized_ok && normalized.host[0]) { + snprintf(dev.detail, sizeof(dev.detail), "Awaiting probe on %s", normalized.host); + } else { + safe_copy(dev.detail, sizeof(dev.detail), "Awaiting spooler probe"); + } + + out[0] = dev; + return 1; +} diff --git a/programs/src/printctl/main.cpp b/programs/src/printctl/main.cpp index 629f8ed..bce6d5b 100644 --- a/programs/src/printctl/main.cpp +++ b/programs/src/printctl/main.cpp @@ -37,6 +37,31 @@ static void print_host_details(const IppUri& uri) { printf("Path: %s\n", uri.path); } +static void print_probe_state_summary(const ProbeState& probe, bool verbose) { + if (!probe.printer_uri[0] && !probe.message[0]) return; + + printf("Last probe: %s", probe.ok ? "ok" : "failed"); + if (probe.probed_at[0]) printf(" at %s", probe.probed_at); + printf("\n"); + + if (!verbose) return; + + if (probe.ok && probe.caps.printer_name[0]) printf("Printer name: %s\n", probe.caps.printer_name); + else if (probe.message[0]) printf("Probe status: %s\n", probe.message); + + if (probe.ok) { + printf("Supports PDF: %s\n", probe.caps.supports_pdf ? "yes" : "no"); + printf("Supports JPEG: %s\n", probe.caps.supports_jpeg ? "yes" : "no"); + printf("Supports text: %s\n", probe.caps.supports_text ? "yes" : "no"); + if (probe.caps.preferred_format[0]) printf("Preferred format: %s\n", probe.caps.preferred_format); + if (probe.caps.default_format[0]) printf("Default format: %s\n", probe.caps.default_format); + if (probe.caps.supported_formats[0]) printf("Supported formats: %s\n", probe.caps.supported_formats); + if (probe.caps.status_message[0]) printf("Printer status: %s\n", probe.caps.status_message); + } + + if (probe.detail[0]) printf("Probe detail: %s\n", probe.detail); +} + static void print_job_detail(const JobMeta& job, const char* state, bool verbose) { printf(" %s %s\n", job.id, job.job_name); printf(" state: %s", state ? state : job.state); @@ -94,7 +119,7 @@ static int cmd_status(const ArgList& args) { } } - char uri[MAX_PATH_LEN]; + char uri[MAX_PATH_LEN] = {}; if (read_default_printer_uri(uri, sizeof(uri))) { printf("Default printer: %s\n", uri); if (verbose) { @@ -110,6 +135,12 @@ static int cmd_status(const ArgList& args) { } else printf("Default printer: not configured\n"); + if (uri[0]) { + ProbeState probe = {}; + if (load_printer_probe_state(&probe) && strcmp(probe.printer_uri, uri) == 0) + print_probe_state_summary(probe, verbose); + } + int pid = -1; if (daemon_is_running(&pid)) printf("Daemon: running (pid %d)\n", pid); @@ -278,44 +309,39 @@ static int cmd_probe(const char* printer_uri) { return 1; } + uint32_t ip = 0; + if (resolve_host(normalized.host, &ip)) normalized.ip = ip; + printf("Printer URI: %s\n", normalized.normalized); print_host_details(normalized); - - uint32_t ip = 0; - if (!resolve_host(normalized.host, &ip)) { + if (!normalized.ip) { if (host_looks_like_mdns(normalized.host)) printf("Resolution: unresolved (.local/mDNS not supported yet)\n"); else printf("Resolution: unresolved\n"); + } else { + char ip_text[20]; + format_ipv4(ip_text, sizeof(ip_text), normalized.ip); + printf("Resolution: ok (%s)\n", ip_text); + } + + ProbeState probe = {}; + if (!probe_printer_uri(normalized.normalized, &probe, err, sizeof(err))) { + printf("Probe failed: %s\n", probe.message[0] ? probe.message : (err[0] ? err : "IPP probe failed")); + if (probe.detail[0]) printf("Detail: %s\n", probe.detail); return 1; } - normalized.ip = ip; - char ip_text[20]; - format_ipv4(ip_text, sizeof(ip_text), ip); - printf("Resolution: ok (%s)\n", ip_text); - - IppCapabilities caps = {}; - if (!ipp_get_printer_capabilities(normalized.normalized, &caps, err, sizeof(err))) { - char detail[256] = {}; - summarize_ipp_capabilities(&caps, detail, sizeof(detail)); - printf("Probe failed: %s\n", err[0] ? err : "IPP probe failed"); - if (detail[0]) printf("Detail: %s\n", detail); - return 1; - } - - char detail[256] = {}; - summarize_ipp_capabilities(&caps, detail, sizeof(detail)); printf("Probe: ok\n"); - if (caps.printer_name[0]) printf("Printer name: %s\n", caps.printer_name); - printf("Supports PDF: %s\n", caps.supports_pdf ? "yes" : "no"); - printf("Supports JPEG: %s\n", caps.supports_jpeg ? "yes" : "no"); - printf("Supports text: %s\n", caps.supports_text ? "yes" : "no"); - if (caps.preferred_format[0]) printf("Preferred format: %s\n", caps.preferred_format); - if (caps.default_format[0]) printf("Default format: %s\n", caps.default_format); - if (caps.supported_formats[0]) printf("Supported formats: %s\n", caps.supported_formats); - if (caps.status_message[0]) printf("Printer status: %s\n", caps.status_message); - if (detail[0]) printf("Detail: %s\n", detail); + if (probe.caps.printer_name[0]) printf("Printer name: %s\n", probe.caps.printer_name); + printf("Supports PDF: %s\n", probe.caps.supports_pdf ? "yes" : "no"); + printf("Supports JPEG: %s\n", probe.caps.supports_jpeg ? "yes" : "no"); + printf("Supports text: %s\n", probe.caps.supports_text ? "yes" : "no"); + if (probe.caps.preferred_format[0]) printf("Preferred format: %s\n", probe.caps.preferred_format); + if (probe.caps.default_format[0]) printf("Default format: %s\n", probe.caps.default_format); + if (probe.caps.supported_formats[0]) printf("Supported formats: %s\n", probe.caps.supported_formats); + if (probe.caps.status_message[0]) printf("Printer status: %s\n", probe.caps.status_message); + if (probe.detail[0]) printf("Detail: %s\n", probe.detail); return 0; } diff --git a/programs/src/printd/main.cpp b/programs/src/printd/main.cpp index f72b269..5ab640e 100644 --- a/programs/src/printd/main.cpp +++ b/programs/src/printd/main.cpp @@ -15,6 +15,9 @@ extern "C" { using namespace print; +static constexpr uint64_t PROBE_REFRESH_OK_MS = 60000; +static constexpr uint64_t PROBE_REFRESH_FAIL_MS = 15000; + static void log_msg(const char* fmt, ...) { va_list ap; va_start(ap, fmt); @@ -65,6 +68,56 @@ static bool write_pid_file() { return write_text_file_atomic(SPOOL_DAEMON_PID_PATH, pid); } +static void auto_probe_configured_printer(char* last_uri, int last_uri_len, + uint64_t* last_probe_ms, bool* last_probe_ok, + bool force = false) { + if (last_uri == nullptr || last_probe_ms == nullptr || last_probe_ok == nullptr) return; + + char current_uri[MAX_PATH_LEN] = {}; + bool have_uri = read_default_printer_uri(current_uri, sizeof(current_uri)); + uint64_t now = montauk::get_milliseconds(); + + if (!have_uri || current_uri[0] == '\0') { + bool had_cached_uri = last_uri[0] != '\0'; + if (had_cached_uri || file_exists(SPOOL_PROBE_STATE_PATH)) { + clear_printer_probe_state(); + if (had_cached_uri) log_msg("cleared printer probe state (no default printer configured)"); + } + last_uri[0] = '\0'; + *last_probe_ms = 0; + *last_probe_ok = false; + return; + } + + bool uri_changed = strcmp(last_uri, current_uri) != 0; + uint64_t refresh_ms = *last_probe_ok ? PROBE_REFRESH_OK_MS : PROBE_REFRESH_FAIL_MS; + if (!force && !uri_changed && *last_probe_ms != 0 && now - *last_probe_ms < refresh_ms) + return; + + ProbeState state = {}; + char err[128] = {}; + bool ok = probe_printer_uri(current_uri, &state, err, sizeof(err)); + if (!save_printer_probe_state(&state)) + log_msg("warning: failed to persist printer probe state for %s", current_uri); + + if (ok) { + if (state.caps.printer_name[0]) + log_msg("auto-probed printer %s (%s)", state.printer_uri, state.caps.printer_name); + else + log_msg("auto-probed printer %s", state.printer_uri); + if (state.detail[0]) log_msg("probe detail: %s", state.detail); + } else { + log_msg("auto-probe failed for %s: %s", + state.printer_uri[0] ? state.printer_uri : current_uri, + state.message[0] ? state.message : (err[0] ? err : "probe failed")); + if (state.detail[0]) log_msg("probe detail: %s", state.detail); + } + + safe_copy(last_uri, last_uri_len, state.printer_uri[0] ? state.printer_uri : current_uri); + *last_probe_ms = now; + *last_probe_ok = ok; +} + static void requeue_stale_active_jobs() { DIR* dir = opendir(SPOOL_ACTIVE_DIR); if (!dir) return; @@ -260,9 +313,16 @@ extern "C" void _start() { } requeue_stale_active_jobs(); + char last_probed_uri[MAX_PATH_LEN] = {}; + uint64_t last_probe_ms = 0; + bool last_probe_ok = false; + auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri), + &last_probe_ms, &last_probe_ok, true); log_msg("ready"); for (;;) { + auto_probe_configured_printer(last_probed_uri, sizeof(last_probed_uri), + &last_probe_ms, &last_probe_ok); char active_path[MAX_PATH_LEN]; if (claim_next_job(active_path, sizeof(active_path))) { process_job(active_path); diff --git a/programs/src/printers/main.cpp b/programs/src/printers/main.cpp index b451e1e..99c2219 100644 --- a/programs/src/printers/main.cpp +++ b/programs/src/printers/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -25,13 +26,29 @@ using namespace print; static constexpr int INIT_W = 560; static constexpr int INIT_H = 500; +static constexpr int CONFIG_W = 408; +static constexpr int CONFIG_H = 220; +static constexpr int TAB_BAR_H = 36; static constexpr int PAD_X = 16; static constexpr int PAD_Y = 20; static constexpr int BTN_H = 30; static constexpr int ACTION_RADIUS = 4; static constexpr int FIELD_H = 32; +static constexpr int PRINTER_ICON_SIZE = 24; + +enum PrinterTab { + TAB_OVERVIEW = 0, + TAB_DETAILS = 1, + TAB_COUNT = 2, +}; + +static const char* g_tab_labels[TAB_COUNT] = { + "Overview", + "Details", +}; struct AppState { + int active_tab; char printer_uri[MAX_PATH_LEN]; bool printers_loaded; bool daemon_running; @@ -51,12 +68,14 @@ struct AppState { bool probe_valid; bool probe_ok; char probe_message[128]; + char probe_time[32]; char probe_detail[MAX_DEBUG_LEN]; char status_msg[128]; uint64_t status_time; bool config_open; + int config_button_down; char edit_uri[MAX_PATH_LEN]; int edit_len; char edit_error[128]; @@ -71,15 +90,29 @@ struct Layout { Rect start_btn; Rect refresh_btn; Rect test_btn; + Rect printer_icon; +}; - Rect dialog; - Rect dialog_input; - Rect dialog_save; - Rect dialog_cancel; +struct ConfigLayout { + Rect input; + Rect save_btn; + Rect cancel_btn; +}; + +struct ConfigPopup { + int win_id; + uint32_t* pixels; + int width; + int height; + bool open; }; static WsWindow g_win; +static ConfigPopup g_config = {-1, nullptr, CONFIG_W, CONFIG_H, false}; static AppState g_app = {}; +static SvgIcon g_printer_icon = {}; + +static void render_config_popup(); static void str_append(char* dst, const char* src, int max) { int len = montauk::slen(dst); @@ -103,17 +136,17 @@ static void clear_probe() { g_app.probe_valid = false; g_app.probe_ok = false; g_app.probe_message[0] = '\0'; + g_app.probe_time[0] = '\0'; g_app.probe_detail[0] = '\0'; } -static void set_probe_transport_detail(const IppUri& uri, const char* resolution) { - snprintf(g_app.probe_detail, sizeof(g_app.probe_detail), - "host=%s ip=%s port=%u path=%s %s", - uri.host, - resolution && *resolution ? resolution : "unresolved", - (unsigned)uri.port, - uri.path[0] ? uri.path : "/", - uri.use_tls ? "tls" : "plain"); +static void apply_probe_state(const ProbeState& state) { + g_app.probe_caps = state.caps; + g_app.probe_valid = state.printer_uri[0] != '\0' || state.message[0] != '\0'; + g_app.probe_ok = state.ok; + safe_copy(g_app.probe_message, sizeof(g_app.probe_message), state.message); + safe_copy(g_app.probe_time, sizeof(g_app.probe_time), state.probed_at); + safe_copy(g_app.probe_detail, sizeof(g_app.probe_detail), state.detail); } static void set_accent(Color accent) { @@ -150,6 +183,13 @@ static void load_accent() { doc.destroy(); } +static void load_icons() { + if (g_printer_icon.pixels) svg_free(g_printer_icon); + g_printer_icon = svg_load("0:/icons/printer-symbolic.svg", + PRINTER_ICON_SIZE, PRINTER_ICON_SIZE, + Color::from_rgb(0x6A, 0x72, 0x7D)); +} + static int count_dir_entries(const char* path) { DIR* d = opendir(path); if (!d) return 0; @@ -239,6 +279,56 @@ static void draw_action_btn(Canvas& c, const Rect& r, const char* label, c.text(r.x + (r.w - tw) / 2, r.y + (r.h - fh) / 2, label, fg); } +static void draw_chip(Canvas& c, int x, int y, const char* label, Color bg, Color fg = colors::WHITE) { + int sfh = system_font_height(); + int chip_h = sfh + 8; + int chip_w = text_width(label) + 18; + c.fill_rounded_rect(x, y, chip_w, chip_h, chip_h / 2, bg); + c.text(x + 9, y + (chip_h - sfh) / 2, label, fg); +} + +static int chip_height() { + return system_font_height() + 8; +} + +static int control_row_text_y(int row_y) { + return row_y + 6; +} + +static int control_row_chip_y(int row_y) { + return row_y + 2; +} + +static void draw_overview_stat_row(Canvas& c, int x, int y, int w, + const char* label, int value) { + char value_buf[16]; + snprintf(value_buf, sizeof(value_buf), "%d", value); + + c.text(x, y, label, colors::TEXT_COLOR); + int value_w = text_width(value_buf); + c.text(x + w - value_w, y, value_buf, colors::TEXT_COLOR); +} + +static void draw_tabs(Canvas& c) { + int sfh = system_font_height(); + c.fill_rect(0, 0, g_win.width, TAB_BAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); + c.hline(0, TAB_BAR_H - 1, g_win.width, colors::BORDER); + + int tab_w = g_win.width / TAB_COUNT; + for (int i = 0; i < TAB_COUNT; i++) { + int tx = i * tab_w; + bool active = (i == g_app.active_tab); + if (active) { + c.fill_rect(tx, 0, tab_w, TAB_BAR_H, colors::WINDOW_BG); + c.fill_rect(tx + 4, TAB_BAR_H - 3, tab_w - 8, 3, g_app.accent); + } + + int tw = text_width(g_tab_labels[i]); + Color tc = active ? g_app.accent : Color::from_rgb(0x66, 0x66, 0x66); + c.text(tx + (tab_w - tw) / 2, (TAB_BAR_H - sfh) / 2, g_tab_labels[i], tc); + } +} + static void draw_input_field(Canvas& c, int x, int y, int w, int h, const char* label, const char* value, bool focused) { int sfh = system_font_height(); @@ -259,67 +349,127 @@ static void draw_input_field(Canvas& c, int x, int y, int w, int h, static void compute_layout(Layout* lo) { int sfh = system_font_height(); int x = PAD_X; - int y = PAD_Y; + int y = TAB_BAR_H + PAD_Y; if (status_visible()) y += sfh + 12; - lo->configure_btn = {g_win.width - x - 112 - 8 - 84, y, 112, BTN_H}; - lo->clear_btn = {lo->configure_btn.x + 120, y, 84, BTN_H}; + lo->configure_btn = {0, 0, 0, 0}; + lo->clear_btn = {0, 0, 0, 0}; + lo->probe_btn = {0, 0, 0, 0}; + lo->start_btn = {0, 0, 0, 0}; + lo->refresh_btn = {0, 0, 0, 0}; + lo->test_btn = {0, 0, 0, 0}; + lo->printer_icon = {0, 0, 0, 0}; - y += BTN_H + 8; - y += sfh + 4; - y += sfh + 14; - y += 16; + if (g_app.active_tab == TAB_OVERVIEW) { + int right_edge = g_win.width - x; + lo->clear_btn = {right_edge - 84, y, 84, BTN_H}; + lo->configure_btn = {lo->clear_btn.x - 8 - 112, y, 112, BTN_H}; + if (g_app.probe_ok && g_app.probe_caps.printer_name[0] && g_printer_icon.pixels) { + lo->printer_icon = { + x, + y + (BTN_H - PRINTER_ICON_SIZE) / 2, + PRINTER_ICON_SIZE, + PRINTER_ICON_SIZE + }; + } - lo->probe_btn = {g_win.width - x - 92, y, 92, BTN_H}; - - y += BTN_H + 8; - y += sfh + 4; - y += sfh + 4; - y += sfh + 4; - if (g_app.probe_valid && g_app.probe_ok && g_app.probe_detail[0]) y += sfh + 4; - y += sfh + 14; - y += 16; - - lo->start_btn = {g_win.width - x - 118 - 8 - 86, y, 118, BTN_H}; - lo->refresh_btn = {lo->start_btn.x + 126, y, 86, BTN_H}; - - y += BTN_H + 8; - y += sfh + 6; - y += sfh + 4; - y += sfh + 4; - if (g_app.has_active) y += sfh + 4; - if (g_app.has_active && g_app.last_active.debug_info[0]) y += sfh + 4; - if (g_app.has_failed) { + y += BTN_H + 8; y += sfh + 4; - if (g_app.last_failed.debug_info[0]) y += sfh + 4; + y += sfh + 4; + if (!g_app.probe_ok || !g_app.probe_caps.printer_name[0]) + y += sfh + 4; + y += sfh + 14; + y += 16; + + int queue_row_h = sfh + 10; + y += sfh + 8; + y += queue_row_h * 4; + y += sfh + 8; + if (g_app.has_failed) y += sfh + 4; + if (!g_app.daemon_running) y += sfh + 4; + y += 14; + y += 16; + + lo->test_btn = {g_win.width - x - 132, y, 132, BTN_H}; + } else { + lo->probe_btn = {g_win.width - x - 92, y, 92, BTN_H}; + + y += BTN_H + 8; + if (g_app.printer_uri[0]) { + char err[128] = {}; + IppUri normalized = {}; + if (normalize_ipp_uri(g_app.printer_uri, &normalized, err, sizeof(err))) { + y += sfh + 4; + y += sfh + 4; + y += sfh + 4; + y += sfh + 4; + } else { + y += sfh + 4; + y += sfh + 4; + } + } else { + y += sfh + 4; + y += sfh + 4; + } + y += sfh + 4; + if (g_app.probe_time[0]) y += sfh + 4; + if (g_app.probe_valid) { + y += sfh + 4; + if (g_app.probe_ok) y += sfh + 4; + if (g_app.probe_caps.supported_formats[0]) y += sfh + 4; + if (g_app.probe_detail[0]) y += sfh + 4; + } else { + y += sfh + 4; + } + y += 14; + y += 16; + + lo->start_btn = {g_win.width - x - 118 - 8 - 86, y, 118, BTN_H}; + lo->refresh_btn = {lo->start_btn.x + 126, y, 86, BTN_H}; + + y += BTN_H + 8; + y += sfh + 6; + y += sfh + 4; + y += sfh + 4; + if (g_app.has_active) y += sfh + 4; + if (g_app.has_active && g_app.last_active.debug_info[0]) y += sfh + 4; + if (g_app.has_failed) { + y += sfh + 4; + if (g_app.last_failed.debug_info[0]) y += sfh + 4; + } + y += 14; + y += 16; } - y += 14; - y += 16; - lo->test_btn = {g_win.width - x - 132, y, 132, BTN_H}; +} - int dw = 408; - int dh = 220; - lo->dialog = {(g_win.width - dw) / 2, (g_win.height - dh) / 2, dw, dh}; - lo->dialog_input = {lo->dialog.x + 16, lo->dialog.y + 58, dw - 32, FIELD_H}; - lo->dialog_save = {lo->dialog.x + 16, lo->dialog.y + dh - BTN_H - 16, 80, BTN_H}; - lo->dialog_cancel = {lo->dialog.x + 104, lo->dialog.y + dh - BTN_H - 16, 80, BTN_H}; +static void compute_config_layout(ConfigLayout* lo) { + int pad = 16; + int sfh = system_font_height(); + int field_w = g_config.width - pad * 2; + if (field_w < 120) field_w = 120; + + int input_box_y = 16 + sfh + 6 + sfh + 10 + sfh + 2; + lo->input = {pad, input_box_y, field_w, FIELD_H}; + lo->save_btn = {pad, g_config.height - BTN_H - 16, 80, BTN_H}; + lo->cancel_btn = {pad + 88, g_config.height - BTN_H - 16, 80, BTN_H}; } static void refresh_state(bool force = false) { uint64_t now = montauk::get_milliseconds(); if (!force && g_app.printers_loaded && now - g_app.last_refresh_ms < 1000) return; - char previous_uri[MAX_PATH_LEN]; - safe_copy(previous_uri, sizeof(previous_uri), g_app.printer_uri); - ensure_spool_dirs(); if (!read_default_printer_uri(g_app.printer_uri, sizeof(g_app.printer_uri))) g_app.printer_uri[0] = '\0'; - if (!montauk::streq(previous_uri, g_app.printer_uri)) + ProbeState probe = {}; + if (g_app.printer_uri[0] && load_printer_probe_state(&probe) && + montauk::streq(probe.printer_uri, g_app.printer_uri)) + apply_probe_state(probe); + else clear_probe(); g_app.daemon_pid = -1; @@ -335,59 +485,46 @@ static void refresh_state(bool force = false) { } static void probe_printer() { - clear_probe(); - - if (!g_app.printer_uri[0]) { - safe_copy(g_app.probe_message, sizeof(g_app.probe_message), "No printer configured"); - g_app.probe_valid = true; - g_app.probe_ok = false; - set_status("No printer configured"); - return; - } - + ProbeState probe = {}; char err[128] = {}; - IppUri normalized = {}; - if (!normalize_ipp_uri(g_app.printer_uri, &normalized, err, sizeof(err))) { - safe_copy(g_app.probe_message, sizeof(g_app.probe_message), err); - g_app.probe_valid = true; - g_app.probe_ok = false; - set_status("Printer probe failed"); - return; + bool ok = probe_printer_uri(g_app.printer_uri, &probe, err, sizeof(err)); + apply_probe_state(probe); + if (probe.printer_uri[0]) save_printer_probe_state(&probe); + set_status(ok ? "Printer probe succeeded" : (probe.message[0] ? probe.message : "Printer probe failed")); +} + +static void close_config_popup() { + if (g_config.open && g_config.win_id >= 0) + montauk::win_destroy(g_config.win_id); + g_config.win_id = -1; + g_config.pixels = nullptr; + g_config.width = CONFIG_W; + g_config.height = CONFIG_H; + g_config.open = false; + g_app.config_open = false; +} + +static void open_config_popup() { + safe_copy(g_app.edit_uri, sizeof(g_app.edit_uri), g_app.printer_uri); + g_app.edit_len = montauk::slen(g_app.edit_uri); + g_app.edit_error[0] = '\0'; + g_app.config_button_down = 0; + + if (!g_config.open) { + Montauk::WinCreateResult wres; + if (montauk::win_create("Configure Printer", CONFIG_W, CONFIG_H, &wres) < 0 || wres.id < 0) { + set_status("Failed to open printer dialog"); + return; + } + g_config.win_id = wres.id; + g_config.pixels = (uint32_t*)(uintptr_t)wres.pixelVa; + g_config.width = CONFIG_W; + g_config.height = CONFIG_H; + g_config.open = true; } - uint32_t ip = 0; - if (!resolve_host(normalized.host, &ip)) { - set_probe_transport_detail(normalized, "unresolved"); - if (host_looks_like_mdns(normalized.host)) - safe_copy(g_app.probe_message, sizeof(g_app.probe_message), - ".local/mDNS hostnames are not supported yet"); - else - snprintf(g_app.probe_message, sizeof(g_app.probe_message), - "Could not resolve %s", normalized.host); - g_app.probe_valid = true; - g_app.probe_ok = false; - set_status("Printer probe failed"); - return; - } - - char ip_text[20]; - format_ipv4(ip_text, sizeof(ip_text), ip); - set_probe_transport_detail(normalized, ip_text); - - if (!ipp_get_printer_capabilities(normalized.normalized, &g_app.probe_caps, err, sizeof(err))) { - safe_copy(g_app.probe_message, sizeof(g_app.probe_message), err[0] ? err : "IPP probe failed"); - summarize_ipp_capabilities(&g_app.probe_caps, g_app.probe_detail, sizeof(g_app.probe_detail)); - g_app.probe_valid = true; - g_app.probe_ok = false; - set_status("Printer probe failed"); - return; - } - - safe_copy(g_app.probe_message, sizeof(g_app.probe_message), "Printer probe succeeded"); - summarize_ipp_capabilities(&g_app.probe_caps, g_app.probe_detail, sizeof(g_app.probe_detail)); - g_app.probe_valid = true; - g_app.probe_ok = true; - set_status("Printer probe succeeded"); + g_app.config_open = true; + render_config_popup(); } static void save_configured_printer() { @@ -408,75 +545,278 @@ static void save_configured_printer() { return; } - g_app.config_open = false; + close_config_popup(); refresh_state(true); set_status("Printer saved"); } -static void draw_modal(Canvas& c, const Layout& lo) { - c.fill_rounded_rect(lo.dialog.x, lo.dialog.y, lo.dialog.w, lo.dialog.h, 6, colors::WINDOW_BG); - c.rect(lo.dialog.x, lo.dialog.y, lo.dialog.w, lo.dialog.h, colors::BORDER); +static void render_config_popup() { + if (!g_app.config_open || !g_config.open || g_config.win_id < 0 || g_config.pixels == nullptr) return; + Canvas c(g_config.pixels, g_config.width, g_config.height); + c.fill(colors::WINDOW_BG); + + ConfigLayout lo = {}; + compute_config_layout(&lo); int sfh = system_font_height(); - int y = lo.dialog.y + 16; - c.text(lo.dialog.x + 16, y, "Default Printer", colors::TEXT_COLOR); + int x = 16; + int y = 16; + c.text(x, y, "Default Printer", colors::TEXT_COLOR); y += sfh + 6; - c.text(lo.dialog.x + 16, y, "Enter an IPP or IPPS destination URI.", Color::from_rgb(0x88, 0x88, 0x88)); + c.text(x, y, "Enter an IPP or IPPS destination URI.", Color::from_rgb(0x88, 0x88, 0x88)); y += sfh + 10; - draw_input_field(c, lo.dialog_input.x, lo.dialog_input.y - (sfh + 2), - lo.dialog_input.w, lo.dialog_input.h, "Printer URI", + draw_input_field(c, lo.input.x, lo.input.y - (sfh + 2), + lo.input.w, lo.input.h, "Printer URI", g_app.edit_uri, true); - y = lo.dialog_input.y + lo.dialog_input.h + 12; - c.text(lo.dialog.x + 16, y, "Example: ipp://printer.local/ipp/print", Color::from_rgb(0x88, 0x88, 0x88)); + y = lo.input.y + lo.input.h + 12; + c.text(x, y, "Example: ipp://printer.local/ipp/print", Color::from_rgb(0x88, 0x88, 0x88)); y += sfh + 10; if (g_app.edit_error[0]) - c.text(lo.dialog.x + 16, y, g_app.edit_error, Color::from_rgb(0xD0, 0x3E, 0x3E)); + c.text(x, y, g_app.edit_error, Color::from_rgb(0xD0, 0x3E, 0x3E)); - draw_action_btn(c, lo.dialog_save, "Save", true, true); - draw_action_btn(c, lo.dialog_cancel, "Cancel", true, false); + draw_action_btn(c, lo.save_btn, "Save", true, true); + draw_action_btn(c, lo.cancel_btn, "Cancel", true, false); + montauk::win_present(g_config.win_id); } -static void render() { - refresh_state(); +static bool handle_config_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons) { + ConfigLayout lo = {}; + compute_config_layout(&lo); - Canvas c = g_win.canvas(); - c.fill(colors::WINDOW_BG); + bool left_pressed = (buttons & 1) && !(prev_buttons & 1); + bool left_released = !(buttons & 1) && (prev_buttons & 1); + if (left_pressed) { + if (lo.save_btn.contains(mx, my)) + g_app.config_button_down = 1; + else if (lo.cancel_btn.contains(mx, my)) + g_app.config_button_down = 2; + else + g_app.config_button_down = 0; + return false; + } + + if (!left_released) return false; + + int pressed = g_app.config_button_down; + g_app.config_button_down = 0; + + if (pressed == 1 && lo.save_btn.contains(mx, my)) { + save_configured_printer(); + return !g_app.config_open; + } + if (pressed == 2 && lo.cancel_btn.contains(mx, my)) { + close_config_popup(); + return true; + } + return false; +} + +static bool handle_config_key(const Montauk::KeyEvent& key) { + if (!key.pressed) return false; + + if (key.scancode == 0x01) { + close_config_popup(); + return true; + } + if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) { + save_configured_printer(); + return !g_app.config_open; + } + if (key.ascii == '\b' || key.scancode == 0x0E) { + if (g_app.edit_len > 0) { + g_app.edit_len--; + g_app.edit_uri[g_app.edit_len] = '\0'; + g_app.edit_error[0] = '\0'; + } + return false; + } + if (key.ascii >= 0x20 && key.ascii < 0x7F && g_app.edit_len < MAX_PATH_LEN - 1) { + g_app.edit_uri[g_app.edit_len++] = key.ascii; + g_app.edit_uri[g_app.edit_len] = '\0'; + g_app.edit_error[0] = '\0'; + } + return false; +} + +static void render_overview(Canvas& c, const Layout& lo) { Color dim = Color::from_rgb(0x88, 0x88, 0x88); Color success = Color::from_rgb(0x22, 0x88, 0x22); Color danger = Color::from_rgb(0xCC, 0x33, 0x33); + Color quiet_bg = Color::from_rgb(0xE6, 0xE6, 0xE6); int sfh = system_font_height(); int x = PAD_X; - int y = PAD_Y; + int y = TAB_BAR_H + PAD_Y; int w = g_win.width - 2 * x; - Layout lo = {}; - compute_layout(&lo); + int title_x = x; + char title[MAX_DEBUG_LEN]; if (status_visible()) { c.text(x, y, g_app.status_msg, g_app.accent); y += sfh + 12; } - // Default printer - c.text(x, y + 6, "Default Printer", colors::TEXT_COLOR); + const char* chip_label = "Not Set"; + Color chip_bg = quiet_bg; + Color chip_fg = colors::TEXT_COLOR; + if (g_app.printer_uri[0] && g_app.probe_ok) { + chip_label = "Ready"; + chip_bg = success; + chip_fg = colors::WHITE; + } else if (g_app.printer_uri[0] && g_app.probe_valid) { + chip_label = "Problem"; + chip_bg = danger; + chip_fg = colors::WHITE; + } else if (g_app.printer_uri[0]) { + chip_label = "Checking"; + chip_bg = Color::from_rgb(0xE3, 0xE8, 0xEF); + chip_fg = colors::TEXT_COLOR; + } + + int chip_w = text_width(chip_label) + 18; + int chip_x = lo.configure_btn.x - chip_w - 12; + if (chip_x < x + 74) chip_x = x + 74; + + if (lo.printer_icon.w > 0) { + title_x = lo.printer_icon.x + lo.printer_icon.w + 10; + if (chip_x < title_x + 80) chip_x = title_x + 80; + } + + if (g_app.probe_ok && g_app.probe_caps.printer_name[0]) + safe_copy(title, sizeof(title), g_app.probe_caps.printer_name); + else + safe_copy(title, sizeof(title), "Printer"); + + int title_max_w = chip_x - title_x - 12; + if (title_max_w < 80) title_max_w = 80; + text_fit(title, title, sizeof(title), title_max_w); + + if (lo.printer_icon.w > 0 && g_printer_icon.pixels) + c.icon(lo.printer_icon.x, lo.printer_icon.y, g_printer_icon); + c.text(title_x, control_row_text_y(y), title, colors::TEXT_COLOR); + draw_chip(c, chip_x, control_row_chip_y(y), chip_label, chip_bg, chip_fg); draw_action_btn(c, lo.configure_btn, "Configure", true, true); draw_action_btn(c, lo.clear_btn, "Clear", g_app.printer_uri[0] != '\0', false); y += BTN_H + 8; char line[MAX_DEBUG_LEN]; - if (g_app.printer_uri[0]) text_fit(g_app.printer_uri, line, sizeof(line), w); - else safe_copy(line, sizeof(line), "No printer configured"); - c.text(x, y, line, g_app.printer_uri[0] ? colors::TEXT_COLOR : dim); + if (!g_app.probe_ok || !g_app.probe_caps.printer_name[0]) { + if (g_app.printer_uri[0]) + safe_copy(line, sizeof(line), "Configured printer"); + else + safe_copy(line, sizeof(line), "No printer configured"); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, colors::TEXT_COLOR); + y += sfh + 4; + } + + if (g_app.printer_uri[0]) { + text_fit(g_app.printer_uri, line, sizeof(line), w); + c.text(x, y, line, dim); + } else { + c.text(x, y, "Choose an IPP or IPPS printer to start printing.", dim); + } y += sfh + 4; - c.text(x, y, "System-wide IPP / IPPS destination", dim); + + if (!g_app.printer_uri[0]) { + c.text(x, y, "The saved printer is shared system-wide through the spooler.", dim); + } else if (g_app.probe_ok) { + if (g_app.probe_caps.status_message[0]) + safe_copy(line, sizeof(line), g_app.probe_caps.status_message); + else + safe_copy(line, sizeof(line), "Printer is reachable and ready for queued jobs."); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + } else if (g_app.probe_valid) { + text_fit(g_app.probe_message, line, sizeof(line), w); + c.text(x, y, line, danger); + } else { + c.text(x, y, "Waiting for the spooler to probe the configured printer.", dim); + } y += sfh + 14; c.hline(x, y, w, colors::BORDER); y += 16; - // Connection + if (!g_app.printer_uri[0]) { + c.text(x, y, "Queue and test-page controls appear after a printer is configured.", dim); + return; + } + + c.text(x, y, "Queue", colors::TEXT_COLOR); + y += sfh + 8; + + int row_h = sfh + 10; + draw_overview_stat_row(c, x, y, w, "Waiting", g_app.queue_count); + y += row_h; + c.hline(x, y - 5, w, Color::from_rgb(0xEB, 0xEB, 0xEB)); + + draw_overview_stat_row(c, x, y, w, "Printing", g_app.active_count); + y += row_h; + c.hline(x, y - 5, w, Color::from_rgb(0xEB, 0xEB, 0xEB)); + + draw_overview_stat_row(c, x, y, w, "Completed", g_app.done_count); + y += row_h; + c.hline(x, y - 5, w, Color::from_rgb(0xEB, 0xEB, 0xEB)); + + draw_overview_stat_row(c, x, y, w, "Failed", g_app.failed_count); + y += row_h; + y += 2; + + if (g_app.has_active) { + snprintf(line, sizeof(line), "Now printing: %s", g_app.last_active.job_name); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + y += sfh + 4; + } else if (g_app.queue_count > 0) { + c.text(x, y, "Jobs are waiting to be picked up by the spooler.", dim); + y += sfh + 4; + } else { + c.text(x, y, "No jobs in queue.", dim); + y += sfh + 4; + } + + if (g_app.has_failed) { + snprintf(line, sizeof(line), "Last failure: %s", g_app.last_failed.status_message); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, danger); + y += sfh + 4; + } + + if (!g_app.daemon_running) { + c.text(x, y, "Printing is paused until the spooler starts.", danger); + y += sfh + 4; + } + + y += 10; + c.hline(x, y, w, colors::BORDER); + y += 16; + + c.text(x, y + 6, "Test Page", colors::TEXT_COLOR); + draw_action_btn(c, lo.test_btn, "Print Test Page", true, true); + y += BTN_H + 8; + c.text(x, y, "Queue a simple test page through the print spooler.", dim); + y += sfh + 4; + c.text(x, y, "Uses the configured default printer.", colors::TEXT_COLOR); +} + +static void render_details(Canvas& c, const Layout& lo) { + Color dim = Color::from_rgb(0x88, 0x88, 0x88); + Color success = Color::from_rgb(0x22, 0x88, 0x22); + Color danger = Color::from_rgb(0xCC, 0x33, 0x33); + int sfh = system_font_height(); + int x = PAD_X; + int y = TAB_BAR_H + PAD_Y; + int w = g_win.width - 2 * x; + char line[MAX_DEBUG_LEN]; + + if (status_visible()) { + c.text(x, y, g_app.status_msg, g_app.accent); + y += sfh + 12; + } + c.text(x, y + 6, "Connection", colors::TEXT_COLOR); draw_action_btn(c, lo.probe_btn, "Probe", g_app.printer_uri[0] != '\0', false); y += BTN_H + 8; @@ -485,15 +825,20 @@ static void render() { IppUri normalized = {}; char err[128] = {}; if (normalize_ipp_uri(g_app.printer_uri, &normalized, err, sizeof(err))) { - snprintf(line, sizeof(line), "Host: %s", normalized.host); + text_fit(g_app.printer_uri, line, sizeof(line), w); c.text(x, y, line, colors::TEXT_COLOR); y += sfh + 4; + snprintf(line, sizeof(line), "Host: %s", normalized.host); + c.text(x, y, line, dim); + y += sfh + 4; + uint32_t ip = 0; if (resolve_host(normalized.host, &ip)) { char ip_text[20]; format_ipv4(ip_text, sizeof(ip_text), ip); - snprintf(line, sizeof(line), "Resolved: %s", ip_text); + snprintf(line, sizeof(line), "Resolved: %s Transport: %s", ip_text, + normalized.use_tls ? "TLS" : "Plain TCP"); } else if (host_looks_like_mdns(normalized.host)) { safe_copy(line, sizeof(line), "Resolved: unavailable (.local/mDNS not supported yet)"); } else { @@ -501,20 +846,30 @@ static void render() { } text_fit(line, line, sizeof(line), w); c.text(x, y, line, dim); + y += sfh + 4; + + snprintf(line, sizeof(line), "Path: %s Port: %u", normalized.path, (unsigned)normalized.port); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); } else { - safe_copy(line, sizeof(line), "Host: invalid printer URI"); - c.text(x, y, line, danger); + c.text(x, y, "Printer URI is invalid", danger); y += sfh + 4; text_fit(err, line, sizeof(line), w); c.text(x, y, line, dim); } } else { - c.text(x, y, "Host: not configured", dim); + c.text(x, y, "No printer configured", dim); y += sfh + 4; - c.text(x, y, "Resolved: unavailable", dim); + c.text(x, y, "Use Overview to configure the system printer.", dim); } y += sfh + 4; + if (g_app.probe_time[0]) { + snprintf(line, sizeof(line), "Last probe: %s", g_app.probe_time); + c.text(x, y, line, dim); + y += sfh + 4; + } + if (g_app.probe_valid) { if (g_app.probe_ok && g_app.probe_caps.printer_name[0]) snprintf(line, sizeof(line), "Printer: %s", g_app.probe_caps.printer_name); @@ -530,30 +885,32 @@ static void render() { g_app.probe_caps.supports_pdf ? "yes" : "no", g_app.probe_caps.supports_jpeg ? "yes" : "no", g_app.probe_caps.supports_text ? "yes" : "no"); - } else if (g_app.probe_detail[0]) { - safe_copy(line, sizeof(line), g_app.probe_detail); - } else { - safe_copy(line, sizeof(line), "Run Probe to query printer capabilities."); + c.text(x, y, line, dim); + y += sfh + 4; + } + + if (g_app.probe_caps.supported_formats[0]) { + snprintf(line, sizeof(line), "Formats: %s", g_app.probe_caps.supported_formats); + text_fit(line, line, sizeof(line), w); + c.text(x, y, line, dim); + y += sfh + 4; + } + + if (g_app.probe_detail[0]) { + text_fit(g_app.probe_detail, line, sizeof(line), w); + c.text(x, y, line, dim); + y += sfh + 4; } } else { - safe_copy(line, sizeof(line), "Probe: not run"); - c.text(x, y, line, dim); + c.text(x, y, "Waiting for daemon auto-probe or run Probe.", dim); y += sfh + 4; - safe_copy(line, sizeof(line), "Run Probe to query printer capabilities."); } - text_fit(line, line, sizeof(line), w); - c.text(x, y, line, dim); - if (g_app.probe_valid && g_app.probe_ok && g_app.probe_detail[0]) { - y += sfh + 4; - text_fit(g_app.probe_detail, line, sizeof(line), w); - c.text(x, y, line, dim); - } - y += sfh + 14; + + y += 10; c.hline(x, y, w, colors::BORDER); y += 16; - // Spooler - c.text(x, y + 6, "Spooler", colors::TEXT_COLOR); + c.text(x, control_row_text_y(y), "Spooler", colors::TEXT_COLOR); draw_action_btn(c, lo.start_btn, "Start Daemon", !g_app.daemon_running, false); draw_action_btn(c, lo.refresh_btn, "Refresh", true, false); @@ -561,9 +918,7 @@ static void render() { int chip_w = text_width(daemon_label) + 18; int chip_x = lo.start_btn.x - chip_w - 12; if (chip_x < x + 70) chip_x = x + 70; - c.fill_rounded_rect(chip_x, y + 2, chip_w, sfh + 8, (sfh + 8) / 2, - g_app.daemon_running ? success : danger); - c.text(chip_x + 9, y + 6, daemon_label, colors::WHITE); + draw_chip(c, chip_x, control_row_chip_y(y), daemon_label, g_app.daemon_running ? success : danger); y += BTN_H + 8; if (g_app.daemon_running) @@ -607,23 +962,25 @@ static void render() { y += sfh + 4; } } +} - y += 10; - c.hline(x, y, w, colors::BORDER); - y += 16; +static void render() { + refresh_state(); - // Test page - c.text(x, y + 6, "Test Page", colors::TEXT_COLOR); - draw_action_btn(c, lo.test_btn, "Print Test Page", g_app.printer_uri[0] != '\0', true); - y += BTN_H + 8; - c.text(x, y, "Queue a simple test page through the print spooler.", dim); - y += sfh + 4; - c.text(x, y, - g_app.printer_uri[0] ? "Uses the configured default printer." : "Configure a printer first.", - g_app.printer_uri[0] ? colors::TEXT_COLOR : dim); + Canvas c = g_win.canvas(); + c.fill(colors::WINDOW_BG); + Layout lo = {}; + compute_layout(&lo); + draw_tabs(c); - if (g_app.config_open) - draw_modal(c, lo); + switch (g_app.active_tab) { + case TAB_OVERVIEW: + render_overview(c, lo); + break; + case TAB_DETAILS: + render_details(c, lo); + break; + } g_win.present(); } @@ -632,24 +989,22 @@ static void handle_click(int mx, int my) { Layout lo = {}; compute_layout(&lo); - if (g_app.config_open) { - if (lo.dialog_save.contains(mx, my)) { - save_configured_printer(); - } else if (lo.dialog_cancel.contains(mx, my)) { - g_app.config_open = false; - } + if (g_app.config_open) + return; + + if (my >= 0 && my < TAB_BAR_H) { + int tab_w = g_win.width / TAB_COUNT; + int tab = mx / tab_w; + if (tab >= 0 && tab < TAB_COUNT) g_app.active_tab = tab; return; } - if (lo.configure_btn.contains(mx, my)) { - safe_copy(g_app.edit_uri, sizeof(g_app.edit_uri), g_app.printer_uri); - g_app.edit_len = montauk::slen(g_app.edit_uri); - g_app.edit_error[0] = '\0'; - g_app.config_open = true; + if (g_app.active_tab == TAB_OVERVIEW && lo.configure_btn.contains(mx, my)) { + open_config_popup(); return; } - if (g_app.printer_uri[0] && lo.clear_btn.contains(mx, my)) { + if (g_app.active_tab == TAB_OVERVIEW && g_app.printer_uri[0] && lo.clear_btn.contains(mx, my)) { if (montauk::fdelete(DEFAULT_PRINTER_PATH) < 0) set_status("Failed to clear printer"); else { refresh_state(true); @@ -658,12 +1013,12 @@ static void handle_click(int mx, int my) { return; } - if (g_app.printer_uri[0] && lo.probe_btn.contains(mx, my)) { + if (g_app.active_tab == TAB_DETAILS && g_app.printer_uri[0] && lo.probe_btn.contains(mx, my)) { probe_printer(); return; } - if (!g_app.daemon_running && lo.start_btn.contains(mx, my)) { + if (g_app.active_tab == TAB_DETAILS && !g_app.daemon_running && lo.start_btn.contains(mx, my)) { char err[128] = {}; if (ensure_daemon_running(err, sizeof(err))) { refresh_state(true); @@ -674,13 +1029,13 @@ static void handle_click(int mx, int my) { return; } - if (lo.refresh_btn.contains(mx, my)) { + if (g_app.active_tab == TAB_DETAILS && lo.refresh_btn.contains(mx, my)) { refresh_state(true); set_status("Printer status refreshed"); return; } - if (g_app.printer_uri[0] && lo.test_btn.contains(mx, my)) { + if (g_app.active_tab == TAB_OVERVIEW && g_app.printer_uri[0] && lo.test_btn.contains(mx, my)) { char job_id[48] = {}; char err[128] = {}; if (submit_test_page_job(nullptr, job_id, sizeof(job_id), err, sizeof(err))) { @@ -693,29 +1048,17 @@ static void handle_click(int mx, int my) { } } +static void cycle_tab(int delta) { + int next = g_app.active_tab + delta; + while (next < 0) next += TAB_COUNT; + while (next >= TAB_COUNT) next -= TAB_COUNT; + g_app.active_tab = next; +} + static void handle_key(const Montauk::KeyEvent& key) { if (!key.pressed) return; if (g_app.config_open) { - if (key.scancode == 0x01) { - g_app.config_open = false; - return; - } - if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) { - save_configured_printer(); - return; - } - if (key.ascii == '\b' || key.scancode == 0x0E) { - if (g_app.edit_len > 0) { - g_app.edit_len--; - g_app.edit_uri[g_app.edit_len] = '\0'; - } - return; - } - if (key.ascii >= 0x20 && key.ascii < 0x7F && g_app.edit_len < MAX_PATH_LEN - 1) { - g_app.edit_uri[g_app.edit_len++] = key.ascii; - g_app.edit_uri[g_app.edit_len] = '\0'; - } return; } @@ -724,6 +1067,21 @@ static void handle_key(const Montauk::KeyEvent& key) { return; } + if (key.scancode == 0x0F) { + cycle_tab(key.shift ? -1 : 1); + return; + } + + if (key.scancode == 0x4B) { + cycle_tab(-1); + return; + } + + if (key.scancode == 0x4D) { + cycle_tab(1); + return; + } + if (key.ascii == 'r' || key.ascii == 'R') { refresh_state(true); set_status("Printer status refreshed"); @@ -747,7 +1105,9 @@ extern "C" void _start() { if (!fonts::init()) montauk::exit(1); + g_app.active_tab = TAB_OVERVIEW; load_accent(); + load_icons(); refresh_state(true); if (!g_win.create("Printers", INIT_W, INIT_H)) @@ -757,33 +1117,80 @@ extern "C" void _start() { while (g_win.id >= 0 && !g_win.closed) { Montauk::WinEvent ev; + bool redraw_config = false; + bool redraw_main = false; + + if (g_app.config_open && g_config.open) { + int cr = montauk::win_poll(g_config.win_id, &ev); + if (cr < 0) { + close_config_popup(); + redraw_main = true; + } else if (cr > 0) { + if (ev.type == 3) { + close_config_popup(); + redraw_main = true; + } else if (ev.type == 2 || ev.type == 4) { + if (ev.type == 2) { + g_config.width = ev.resize.w; + g_config.height = ev.resize.h; + g_config.pixels = (uint32_t*)(uintptr_t)montauk::win_resize( + g_config.win_id, ev.resize.w, ev.resize.h); + } + redraw_config = true; + } else if (ev.type == 0) { + if (handle_config_key(ev.key)) + redraw_main = true; + if (g_app.config_open) + redraw_config = true; + } else if (ev.type == 1) { + if (handle_config_mouse(ev.mouse.x, ev.mouse.y, + ev.mouse.buttons, ev.mouse.prev_buttons)) + redraw_main = true; + if (g_app.config_open) + redraw_config = true; + } + } + } + int r = g_win.poll(&ev); if (r < 0) break; if (r == 0) { - montauk::sleep_ms(16); + uint64_t last_refresh = g_app.last_refresh_ms; + refresh_state(false); + if (g_app.last_refresh_ms != last_refresh) + redraw_main = true; + + if (redraw_main) + render(); + if (redraw_config && g_app.config_open) + render_config_popup(); + if (!redraw_main && !(redraw_config && g_app.config_open)) + montauk::sleep_ms(16); continue; } - bool redraw = false; - if (ev.type == 3) break; - if (ev.type == 2 || ev.type == 4) redraw = true; + if (ev.type == 2 || ev.type == 4) redraw_main = true; if (ev.type == 0) { handle_key(ev.key); - redraw = true; + redraw_main = true; } else if (ev.type == 1) { bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (clicked) { handle_click(ev.mouse.x, ev.mouse.y); - redraw = true; + redraw_main = true; } } - if (redraw) + if (redraw_main) render(); + if (redraw_config && g_app.config_open) + render_config_popup(); } + close_config_popup(); + if (g_printer_icon.pixels) svg_free(g_printer_icon); g_win.destroy(); montauk::exit(0); } diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 4c99994..b49cb7b 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -55,6 +55,8 @@ ICONS=( "places/scalable/user-home.svg" "devices/scalable/computer.svg" "devices/scalable/network-wired.svg" + "devices/scalable/printer.svg" + "devices/symbolic/printer-symbolic.svg" "mimetypes/scalable/text-x-generic.svg" "mimetypes/scalable/application-x-executable.svg" "categories/scalable/help-about.svg"