/* * main.cpp * MontaukOS Printers - standalone Window Server app * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include #include #include } using namespace gui; 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 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; int daemon_pid; int queue_count; int active_count; int done_count; int failed_count; uint64_t last_refresh_ms; JobMeta last_active; JobMeta last_failed; bool has_active; bool has_failed; IppCapabilities probe_caps; 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; mtk::TextInputState edit_input; char edit_error[128]; int mouse_x; int mouse_y; int config_mouse_x; int config_mouse_y; Color accent; }; struct Layout { Rect configure_btn; Rect clear_btn; Rect probe_btn; Rect start_btn; Rect refresh_btn; Rect test_btn; Rect printer_icon; }; struct ConfigLayout { int input_label_y; 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); int i = 0; while (src[i] && len < max - 1) dst[len++] = src[i++]; dst[len] = '\0'; } static bool status_visible() { return g_app.status_msg[0] && (montauk::get_milliseconds() - g_app.status_time < 4000); } static void set_status(const char* msg) { montauk::strncpy(g_app.status_msg, msg ? msg : "", (int)sizeof(g_app.status_msg) - 1); g_app.status_time = montauk::get_milliseconds(); } static void clear_probe() { montauk::memset(&g_app.probe_caps, 0, sizeof(g_app.probe_caps)); 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 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) { g_app.accent = accent; } static mtk::Theme printers_theme() { mtk::Theme theme = mtk::make_theme(g_app.accent); theme.disabled_bg = Color::from_rgb(0xE6, 0xE6, 0xE6); theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A); return theme; } static void load_accent() { set_accent(mtk::load_system_accent()); } 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; int count = 0; struct dirent* ent = nullptr; while ((ent = readdir(d)) != nullptr) { if (ent->d_name[0] == '.') continue; count++; } closedir(d); return count; } static bool load_latest_job(const char* dir, JobMeta* out) { if (out) zero_job(out); DIR* d = opendir(dir); if (!d) return false; bool found = false; char best_key[32] = {}; struct dirent* ent = nullptr; while ((ent = readdir(d)) != nullptr) { if (ent->d_name[0] == '.') continue; char path[MAX_PATH_LEN]; path_join(path, sizeof(path), dir, ent->d_name); JobMeta job = {}; if (!load_job_from_path(path, &job)) continue; const char* key = job.updated_at[0] ? job.updated_at : job.created_at; if (!found || strcmp(key, best_key) > 0) { found = true; safe_copy(best_key, sizeof(best_key), key); if (out) *out = job; } } closedir(d); return found; } static void text_fit(const char* src, char* out, int out_len, int max_w) { if (!src || !out || out_len <= 0) return; montauk::strncpy(out, src, out_len - 1); if (text_width(out) <= max_w) return; const char* ell = "..."; int keep = montauk::slen(src); while (keep > 0) { char tmp[MAX_DEBUG_LEN]; int n = keep < (int)sizeof(tmp) - 4 ? keep : (int)sizeof(tmp) - 4; for (int i = 0; i < n; i++) tmp[i] = src[i]; tmp[n] = '\0'; str_append(tmp, ell, (int)sizeof(tmp)); if (text_width(tmp) <= max_w) { montauk::strncpy(out, tmp, out_len - 1); return; } keep--; } montauk::strncpy(out, ell, out_len - 1); } static void format_resolved_transport_line(const IppUri& uri, char* out, int out_len) { if (!out || out_len <= 0) return; out[0] = '\0'; const char* transport = uri.use_tls ? "TLS" : "Plain TCP"; uint32_t ip = 0; if (!parse_ipv4_literal(uri.host, &ip)) ip = g_app.probe_caps.resolved_ip; if (ip != 0) { char ip_text[20]; format_ipv4(ip_text, sizeof(ip_text), ip); snprintf(out, out_len, "Resolved: %s Transport: %s", ip_text, transport); return; } if (host_looks_like_mdns(uri.host)) { snprintf(out, out_len, "Resolved: unavailable (.local/mDNS not supported yet) Transport: %s", transport); return; } if (g_app.probe_valid) snprintf(out, out_len, "Resolved: unavailable Transport: %s", transport); else snprintf(out, out_len, "Resolved: run Probe to resolve current printer Transport: %s", transport); } static bool main_mouse_in_rect(const Rect& rect) { return rect.contains(g_app.mouse_x, g_app.mouse_y); } static bool config_mouse_in_rect(const Rect& rect) { return rect.contains(g_app.config_mouse_x, g_app.config_mouse_y); } static mtk::WidgetState main_button_state(const Rect& rect, bool enabled, bool active = false) { return mtk::widget_state(active, main_mouse_in_rect(rect), enabled); } static mtk::WidgetState config_button_state(const Rect& rect, bool enabled, bool active = false) { return mtk::widget_state(active, config_mouse_in_rect(rect), enabled); } static int control_row_text_y(int row_y) { return row_y + (BTN_H - system_font_height()) / 2; } static int control_row_badge_y(int row_y) { return row_y + (BTN_H - mtk::badge_height()) / 2; } static void draw_overview_stat_row(Canvas& c, int x, int y, int w, const char* label, int value, const mtk::Theme& theme) { char value_buf[16]; snprintf(value_buf, sizeof(value_buf), "%d", value); c.text(x, y, label, theme.text); int value_w = text_width(value_buf); c.text(x + w - value_w, y, value_buf, theme.text); } static void compute_layout(Layout* lo) { int sfh = system_font_height(); int x = PAD_X; int y = TAB_BAR_H + PAD_Y; if (status_visible()) y += sfh + 12; 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}; 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 }; } y += BTN_H + 8; 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; } } static Rect tab_bar_rect() { return {0, 0, g_win.width, TAB_BAR_H}; } static void compute_config_layout(ConfigLayout* lo) { mtk::Theme theme = printers_theme(); int pad = 16; int sfh = system_font_height(); int field_w = g_config.width - pad * 2; if (field_w < 120) field_w = 120; lo->input_label_y = 16 + sfh + 6 + sfh + 10; lo->input = mtk::labeled_text_input_rect(pad, lo->input_label_y, field_w, theme, 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 bool 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 false; bool was_loaded = g_app.printers_loaded; char old_printer_uri[MAX_PATH_LEN]; safe_copy(old_printer_uri, sizeof(old_printer_uri), g_app.printer_uri); bool old_daemon_running = g_app.daemon_running; int old_daemon_pid = g_app.daemon_pid; int old_queue_count = g_app.queue_count; int old_active_count = g_app.active_count; int old_done_count = g_app.done_count; int old_failed_count = g_app.failed_count; JobMeta old_active = g_app.last_active; JobMeta old_failed = g_app.last_failed; bool old_has_active = g_app.has_active; bool old_has_failed = g_app.has_failed; IppCapabilities old_probe_caps = g_app.probe_caps; bool old_probe_valid = g_app.probe_valid; bool old_probe_ok = g_app.probe_ok; char old_probe_message[sizeof(g_app.probe_message)]; char old_probe_time[sizeof(g_app.probe_time)]; char old_probe_detail[sizeof(g_app.probe_detail)]; safe_copy(old_probe_message, sizeof(old_probe_message), g_app.probe_message); safe_copy(old_probe_time, sizeof(old_probe_time), g_app.probe_time); safe_copy(old_probe_detail, sizeof(old_probe_detail), g_app.probe_detail); if (force || !g_app.printers_loaded) ensure_spool_dirs(); if (!read_default_printer_uri(g_app.printer_uri, sizeof(g_app.printer_uri))) g_app.printer_uri[0] = '\0'; 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; g_app.daemon_running = daemon_is_running(&g_app.daemon_pid); g_app.queue_count = count_dir_entries(SPOOL_QUEUE_DIR); g_app.active_count = count_dir_entries(SPOOL_ACTIVE_DIR); g_app.done_count = count_dir_entries(SPOOL_DONE_DIR); g_app.failed_count = count_dir_entries(SPOOL_FAILED_DIR); g_app.has_active = load_latest_job(SPOOL_ACTIVE_DIR, &g_app.last_active); g_app.has_failed = load_latest_job(SPOOL_FAILED_DIR, &g_app.last_failed); g_app.printers_loaded = true; g_app.last_refresh_ms = now; return !was_loaded || !montauk::streq(old_printer_uri, g_app.printer_uri) || old_daemon_running != g_app.daemon_running || old_daemon_pid != g_app.daemon_pid || old_queue_count != g_app.queue_count || old_active_count != g_app.active_count || old_done_count != g_app.done_count || old_failed_count != g_app.failed_count || old_has_active != g_app.has_active || (g_app.has_active && memcmp(&old_active, &g_app.last_active, sizeof(g_app.last_active)) != 0) || old_has_failed != g_app.has_failed || (g_app.has_failed && memcmp(&old_failed, &g_app.last_failed, sizeof(g_app.last_failed)) != 0) || old_probe_valid != g_app.probe_valid || old_probe_ok != g_app.probe_ok || !montauk::streq(old_probe_message, g_app.probe_message) || !montauk::streq(old_probe_time, g_app.probe_time) || !montauk::streq(old_probe_detail, g_app.probe_detail) || memcmp(&old_probe_caps, &g_app.probe_caps, sizeof(g_app.probe_caps)) != 0; } static void probe_printer() { ProbeState probe = {}; char err[128] = {}; 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); mtk::text_input_reset(g_app.edit_input, g_app.edit_len); g_app.edit_error[0] = '\0'; g_app.config_button_down = 0; g_app.config_mouse_x = -1; g_app.config_mouse_y = -1; 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; } g_app.config_open = true; render_config_popup(); } static void save_configured_printer() { if (g_app.edit_len == 0) { safe_copy(g_app.edit_error, sizeof(g_app.edit_error), "Printer URI is required"); return; } char err[128] = {}; IppUri uri = {}; if (!normalize_ipp_uri(g_app.edit_uri, &uri, err, sizeof(err))) { safe_copy(g_app.edit_error, sizeof(g_app.edit_error), err); return; } if (!write_default_printer_uri(uri.normalized)) { safe_copy(g_app.edit_error, sizeof(g_app.edit_error), "Failed to save printer"); return; } close_config_popup(); refresh_state(true); set_status("Printer saved"); } 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); mtk::Theme theme = printers_theme(); c.fill(theme.window_bg); ConfigLayout lo = {}; compute_config_layout(&lo); int sfh = system_font_height(); int x = 16; int y = 16; c.text(x, y, "Default Printer", theme.text); y += sfh + 6; c.text(x, y, "Enter an IPP or IPPS destination URI.", theme.text_muted); mtk::draw_labeled_text_field(c, lo.input.x, lo.input_label_y, lo.input.w, "Printer URI", g_app.edit_uri, g_app.edit_input.cursor, true, false, theme, lo.input.h, g_app.edit_input.selection_anchor); y = lo.input.y + lo.input.h + 12; c.text(x, y, "Example: ipp://printer.local/ipp/print", theme.text_muted); y += sfh + 10; if (g_app.edit_error[0]) c.text(x, y, g_app.edit_error, theme.danger); mtk::draw_modal_actions(c, lo.save_btn, "Save", mtk::BUTTON_PRIMARY, config_button_state(lo.save_btn, true, g_app.config_button_down == 1), lo.cancel_btn, "Cancel", config_button_state(lo.cancel_btn, true, g_app.config_button_down == 2), theme); mtk::draw_text_input_context_menu( c, g_app.edit_input, theme, mtk::text_input_has_selection(g_app.edit_input, g_app.edit_uri, MAX_PATH_LEN)); montauk::win_present(g_config.win_id); } static bool handle_config_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons) { ConfigLayout lo = {}; compute_config_layout(&lo); g_app.config_mouse_x = mx; g_app.config_mouse_y = my; bool left_pressed = (buttons & 1) && !(prev_buttons & 1); bool left_released = !(buttons & 1) && (prev_buttons & 1); bool right_pressed = (buttons & 2) && !(prev_buttons & 2); if (g_app.edit_input.context.open || g_app.edit_input.dragging) { int result = mtk::text_input_handle_mouse(g_app.edit_input, lo.input, g_app.edit_uri, MAX_PATH_LEN, mx, my, buttons, prev_buttons, g_config.width, g_config.height, true); if (result & mtk::TEXT_INPUT_CHANGED) { g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN); g_app.edit_error[0] = '\0'; } return false; } if (left_pressed || right_pressed) { if (lo.input.contains(mx, my)) { g_app.config_button_down = 0; int result = mtk::text_input_handle_mouse(g_app.edit_input, lo.input, g_app.edit_uri, MAX_PATH_LEN, mx, my, buttons, prev_buttons, g_config.width, g_config.height, true); if (result & mtk::TEXT_INPUT_CHANGED) { g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN); g_app.edit_error[0] = '\0'; } return false; } if (!left_pressed) return false; int button_down = 0; if (lo.save_btn.contains(mx, my)) button_down = 1; else if (lo.cancel_btn.contains(mx, my)) button_down = 2; g_app.config_button_down = button_down; 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; } int result = mtk::text_input_key(g_app.edit_input, g_app.edit_uri, MAX_PATH_LEN, key); if (result & mtk::TEXT_INPUT_CHANGED) { g_app.edit_len = mtk::text_input_len(g_app.edit_uri, MAX_PATH_LEN); g_app.edit_error[0] = '\0'; } return false; } static void render_overview(Canvas& c, const Layout& lo, const mtk::Theme& theme) { Color dim = theme.text_muted; Color success = Color::from_rgb(0x22, 0x88, 0x22); Color danger = theme.danger; Color quiet_bg = Color::from_rgb(0xE6, 0xE6, 0xE6); int sfh = system_font_height(); int x = PAD_X; int y = TAB_BAR_H + PAD_Y; int w = g_win.width - 2 * x; int title_x = x; char title[MAX_DEBUG_LEN]; if (status_visible()) { c.text(x, y, g_app.status_msg, theme.accent); y += sfh + 12; } const char* chip_label = "Not Set"; Color chip_bg = quiet_bg; Color chip_fg = theme.text; 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 = theme.accent_soft; chip_fg = theme.text; } int chip_w = mtk::badge_width(chip_label, theme); 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, theme.text); mtk::draw_badge(c, chip_x, control_row_badge_y(y), chip_label, chip_bg, chip_fg, theme); mtk::draw_button(c, lo.configure_btn, "Configure", mtk::BUTTON_PRIMARY, main_button_state(lo.configure_btn, true), theme); mtk::draw_button(c, lo.clear_btn, "Clear", mtk::BUTTON_SECONDARY, main_button_state(lo.clear_btn, g_app.printer_uri[0] != '\0'), theme); y += BTN_H + 8; char line[MAX_DEBUG_LEN]; 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, theme.text); 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; 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; mtk::draw_separator(c, x, y, w, theme); y += 16; 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", theme.text); y += sfh + 8; int row_h = sfh + 10; draw_overview_stat_row(c, x, y, w, "Waiting", g_app.queue_count, theme); y += row_h; mtk::draw_separator(c, x, y - 5, w, theme); draw_overview_stat_row(c, x, y, w, "Printing", g_app.active_count, theme); y += row_h; mtk::draw_separator(c, x, y - 5, w, theme); draw_overview_stat_row(c, x, y, w, "Completed", g_app.done_count, theme); y += row_h; mtk::draw_separator(c, x, y - 5, w, theme); draw_overview_stat_row(c, x, y, w, "Failed", g_app.failed_count, theme); 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; mtk::draw_separator(c, x, y, w, theme); y += 16; c.text(x, control_row_text_y(y), "Test Page", theme.text); mtk::draw_button(c, lo.test_btn, "Print Test Page", mtk::BUTTON_PRIMARY, main_button_state(lo.test_btn, true), theme); 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.", theme.text); } static void render_details(Canvas& c, const Layout& lo, const mtk::Theme& theme) { Color dim = theme.text_muted; Color success = Color::from_rgb(0x22, 0x88, 0x22); Color danger = theme.danger; 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, theme.accent); y += sfh + 12; } c.text(x, control_row_text_y(y), "Connection", theme.text); mtk::draw_button(c, lo.probe_btn, "Probe", mtk::BUTTON_SECONDARY, main_button_state(lo.probe_btn, g_app.printer_uri[0] != '\0'), theme); y += BTN_H + 8; if (g_app.printer_uri[0]) { IppUri normalized = {}; char err[128] = {}; if (normalize_ipp_uri(g_app.printer_uri, &normalized, err, sizeof(err))) { text_fit(g_app.printer_uri, line, sizeof(line), w); c.text(x, y, line, theme.text); y += sfh + 4; snprintf(line, sizeof(line), "Host: %s", normalized.host); c.text(x, y, line, dim); y += sfh + 4; format_resolved_transport_line(normalized, line, sizeof(line)); 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 { 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, "No printer configured", dim); y += sfh + 4; 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); else snprintf(line, sizeof(line), "Probe: %s", g_app.probe_message[0] ? g_app.probe_message : (g_app.probe_ok ? "ok" : "failed")); text_fit(line, line, sizeof(line), w); c.text(x, y, line, g_app.probe_ok ? theme.text : danger); y += sfh + 4; if (g_app.probe_ok) { snprintf(line, sizeof(line), "Caps: PDF %s, JPEG %s, text %s", g_app.probe_caps.supports_pdf ? "yes" : "no", g_app.probe_caps.supports_jpeg ? "yes" : "no", g_app.probe_caps.supports_text ? "yes" : "no"); 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 { c.text(x, y, "Waiting for daemon auto-probe or run Probe.", dim); y += sfh + 4; } y += 10; mtk::draw_separator(c, x, y, w, theme); y += 16; c.text(x, control_row_text_y(y), "Spooler", theme.text); mtk::draw_button(c, lo.start_btn, "Start Daemon", mtk::BUTTON_TONAL, main_button_state(lo.start_btn, !g_app.daemon_running), theme); mtk::draw_button(c, lo.refresh_btn, "Refresh", mtk::BUTTON_SECONDARY, main_button_state(lo.refresh_btn, true), theme); const char* daemon_label = g_app.daemon_running ? "Running" : "Stopped"; int chip_w = mtk::badge_width(daemon_label, theme); int chip_x = lo.start_btn.x - chip_w - 12; if (chip_x < x + 70) chip_x = x + 70; mtk::draw_badge(c, chip_x, control_row_badge_y(y), daemon_label, g_app.daemon_running ? success : danger, colors::WHITE, theme); y += BTN_H + 8; if (g_app.daemon_running) snprintf(line, sizeof(line), "Daemon PID %d", g_app.daemon_pid); else safe_copy(line, sizeof(line), "Spooler is not active"); c.text(x, y, line, dim); y += sfh + 6; snprintf(line, sizeof(line), "Queued: %d Printing: %d", g_app.queue_count, g_app.active_count); c.text(x, y, line, theme.text); y += sfh + 4; snprintf(line, sizeof(line), "Completed: %d Failed: %d", g_app.done_count, g_app.failed_count); c.text(x, y, line, theme.text); y += sfh + 4; if (g_app.has_active) { snprintf(line, sizeof(line), "Active: %s", g_app.last_active.job_name); text_fit(line, line, sizeof(line), w); c.text(x, y, line, dim); y += sfh + 4; if (g_app.last_active.debug_info[0]) { snprintf(line, sizeof(line), "Debug: %s", g_app.last_active.debug_info); text_fit(line, line, sizeof(line), w); c.text(x, y, line, 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.last_failed.debug_info[0]) { snprintf(line, sizeof(line), "Debug: %s", g_app.last_failed.debug_info); text_fit(line, line, sizeof(line), w); c.text(x, y, line, dim); y += sfh + 4; } } } static void render() { mtk::StandaloneHost host(&g_win); Canvas c = host.canvas(); mtk::Theme theme = printers_theme(); c.fill(theme.window_bg); Layout lo = {}; compute_layout(&lo); mtk::draw_tab_bar(c, tab_bar_rect(), g_tab_labels, TAB_COUNT, g_app.active_tab, theme); switch (g_app.active_tab) { case TAB_OVERVIEW: render_overview(c, lo, theme); break; case TAB_DETAILS: render_details(c, lo, theme); break; } host.present(); } static void handle_click(int mx, int my) { Layout lo = {}; compute_layout(&lo); if (g_app.config_open) return; int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my); if (tab >= 0) { g_app.active_tab = tab; return; } if (g_app.active_tab == TAB_OVERVIEW && lo.configure_btn.contains(mx, my)) { open_config_popup(); return; } 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); set_status("Printer cleared"); } return; } if (g_app.active_tab == TAB_DETAILS && g_app.printer_uri[0] && lo.probe_btn.contains(mx, my)) { probe_printer(); return; } 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); set_status("Print spooler started"); } else { set_status(err[0] ? err : "Failed to start print spooler"); } return; } 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.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))) { refresh_state(true); set_status("Test page queued"); } else { set_status(err[0] ? err : "Failed to queue test page"); } return; } } static bool hover_changed(const Rect& rect, bool enabled, int prev_x, int prev_y, int next_x, int next_y) { if (!enabled) return false; return rect.contains(prev_x, prev_y) != rect.contains(next_x, next_y); } static bool handle_mouse(const Montauk::WinEvent& ev) { if (g_app.config_open) return false; Layout lo = {}; compute_layout(&lo); int prev_x = g_app.mouse_x; int prev_y = g_app.mouse_y; g_app.mouse_x = ev.mouse.x; g_app.mouse_y = ev.mouse.y; bool redraw = false; if (g_app.active_tab == TAB_OVERVIEW) { redraw |= hover_changed(lo.configure_btn, true, prev_x, prev_y, g_app.mouse_x, g_app.mouse_y); redraw |= hover_changed(lo.clear_btn, g_app.printer_uri[0] != '\0', prev_x, prev_y, g_app.mouse_x, g_app.mouse_y); redraw |= hover_changed(lo.test_btn, g_app.printer_uri[0] != '\0', prev_x, prev_y, g_app.mouse_x, g_app.mouse_y); } else { redraw |= hover_changed(lo.probe_btn, g_app.printer_uri[0] != '\0', prev_x, prev_y, g_app.mouse_x, g_app.mouse_y); redraw |= hover_changed(lo.start_btn, !g_app.daemon_running, prev_x, prev_y, g_app.mouse_x, g_app.mouse_y); redraw |= hover_changed(lo.refresh_btn, true, prev_x, prev_y, g_app.mouse_x, g_app.mouse_y); } bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (clicked) { handle_click(ev.mouse.x, ev.mouse.y); return true; } return redraw; } 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) { return; } if (key.scancode == 0x01) { g_win.closed = true; 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"); } else if (key.ascii == 'p' || key.ascii == 'P') { if (g_app.printer_uri[0]) probe_printer(); } else if (key.ascii == 't' || key.ascii == 'T') { if (g_app.printer_uri[0]) { char job_id[48] = {}; char err[128] = {}; if (submit_test_page_job(nullptr, job_id, sizeof(job_id), err, sizeof(err))) { refresh_state(true); set_status("Test page queued"); } else { set_status(err[0] ? err : "Failed to queue test page"); } } } } extern "C" void _start() { if (!fonts::init()) montauk::exit(1); montauk::memset(&g_app, 0, sizeof(g_app)); g_app.active_tab = TAB_OVERVIEW; g_app.mouse_x = -1; g_app.mouse_y = -1; g_app.config_mouse_x = -1; g_app.config_mouse_y = -1; load_accent(); load_icons(); refresh_state(true); if (!g_win.create("Printers", INIT_W, INIT_H)) montauk::exit(1); render(); 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) { if (refresh_state(false)) redraw_main = true; if (g_app.status_msg[0] && !status_visible()) { g_app.status_msg[0] = '\0'; 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; } if (ev.type == 3) break; if (ev.type == 2 || ev.type == 4) redraw_main = true; if (ev.type == 0) { handle_key(ev.key); redraw_main = true; } else if (ev.type == 1) { redraw_main |= handle_mouse(ev); } 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); }