feat: support for IPP printing + test page
This commit is contained in:
@@ -91,6 +91,16 @@ static bool color_eq(Color a, Color b) {
|
||||
return a.r == b.r && a.g == b.g && a.b == b.b;
|
||||
}
|
||||
|
||||
static bool settings_status_visible(SettingsState* st) {
|
||||
return st->status_msg[0] &&
|
||||
(montauk::get_milliseconds() - st->status_time < 4000);
|
||||
}
|
||||
|
||||
static void settings_set_status(SettingsState* st, const char* msg) {
|
||||
montauk::strncpy(st->status_msg, msg ? msg : "", (int)sizeof(st->status_msg));
|
||||
st->status_time = montauk::get_milliseconds();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper: find selected swatch index in a palette
|
||||
// ============================================================================
|
||||
@@ -706,7 +716,6 @@ static void adduser_on_mouse(Window* win, MouseEvent& ev) {
|
||||
int my = ev.y - cr.y;
|
||||
int pad = 16;
|
||||
int sfh = system_font_height();
|
||||
int fw = win->content_w - 2 * pad;
|
||||
int y = pad;
|
||||
|
||||
// Username field hit
|
||||
|
||||
@@ -156,6 +156,7 @@ extern "C" void _start() {
|
||||
|
||||
// ---- Stage 1: Network configuration (non-blocking) ----
|
||||
run_service("0:/os/dhcp.elf", "dhcp", false);
|
||||
run_service("0:/os/printd.elf", "print spooler", false);
|
||||
|
||||
// ---- Stage 2: Login screen -> desktop (falls back to desktop, then shell) ----
|
||||
if (!run_service("0:/os/login.elf", "login")) {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Makefile for printctl on MontaukOS
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LIBC_LIB := ../../lib/libc
|
||||
BEARSSL := ../../lib/bearssl
|
||||
TLS_LIB := ../../lib/tls
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-mno-80387 \
|
||||
-mno-mmx \
|
||||
-mno-sse \
|
||||
-mno-sse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-I $(BEARSSL)/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
TARGET := $(BINDIR)/os/printctl.elf
|
||||
DEPS := $(OBJDIR)/main.d
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJDIR)/main.o $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJDIR)/main.o $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/main.o: main.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
-include $(DEPS)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS print control utility
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <print/print.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
}
|
||||
|
||||
using namespace print;
|
||||
|
||||
static void usage() {
|
||||
printf("Usage:\n");
|
||||
printf(" printctl set-printer <ipp://host[:port]/path>\n");
|
||||
printf(" printctl show-printer\n");
|
||||
printf(" printctl print <file> [--printer <uri>] [--name <job>] [--wait]\n");
|
||||
printf(" printctl test-page [--printer <uri>] [--wait] [--no-wait]\n");
|
||||
printf(" printctl status [--verbose]\n");
|
||||
printf(" printctl inspect <job-id>\n");
|
||||
printf(" printctl probe [uri]\n");
|
||||
}
|
||||
|
||||
static void print_host_details(const IppUri& uri) {
|
||||
char ip[20];
|
||||
if (uri.ip) format_ipv4(ip, sizeof(ip), uri.ip);
|
||||
else safe_copy(ip, sizeof(ip), "unresolved");
|
||||
|
||||
printf("Host: %s\n", uri.host);
|
||||
printf("Resolved IP: %s\n", ip);
|
||||
printf("Transport: %s\n", uri.use_tls ? "TLS" : "Plain TCP");
|
||||
printf("Port: %u\n", (unsigned)uri.port);
|
||||
printf("Path: %s\n", uri.path);
|
||||
}
|
||||
|
||||
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);
|
||||
if (job.updated_at[0]) printf(" updated: %s", job.updated_at);
|
||||
printf("\n");
|
||||
|
||||
if (job.printer_uri[0]) printf(" printer: %s\n", job.printer_uri);
|
||||
if (job.doc_format[0]) printf(" format: %s\n", job.doc_format);
|
||||
if (job.source_name[0]) printf(" source: %s\n", job.source_name);
|
||||
if (job.remote_job_id > 0) printf(" printer job id: %d\n", job.remote_job_id);
|
||||
if (job.status_message[0]) printf(" status: %s\n", job.status_message);
|
||||
if ((verbose || strcmp(state, "failed") == 0 || strcmp(state, "printing") == 0) &&
|
||||
job.debug_info[0]) {
|
||||
printf(" debug: %s\n", job.debug_info);
|
||||
}
|
||||
}
|
||||
|
||||
static void print_job_line(const char* dir, const char* label, bool verbose) {
|
||||
DIR* d = opendir(dir);
|
||||
if (!d) {
|
||||
printf("%s: unavailable\n", label);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("%s:\n", label);
|
||||
int count = 0;
|
||||
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* state = strcmp(label, "Queued") == 0 ? "queued"
|
||||
: strcmp(label, "Active") == 0 ? "printing"
|
||||
: strcmp(label, "Done") == 0 ? "completed"
|
||||
: strcmp(label, "Failed") == 0 ? "failed"
|
||||
: job.state;
|
||||
print_job_detail(job, state, verbose);
|
||||
count++;
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
if (count == 0) printf(" empty\n");
|
||||
}
|
||||
|
||||
static int cmd_status(const ArgList& args) {
|
||||
ensure_spool_dirs();
|
||||
bool verbose = false;
|
||||
for (int i = 1; i < args.count; i++) {
|
||||
if (strcmp(args.argv[i], "--verbose") == 0) verbose = true;
|
||||
else {
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
char uri[MAX_PATH_LEN];
|
||||
if (read_default_printer_uri(uri, sizeof(uri))) {
|
||||
printf("Default printer: %s\n", uri);
|
||||
if (verbose) {
|
||||
char err[128] = {};
|
||||
IppUri parsed = {};
|
||||
if (normalize_ipp_uri(uri, &parsed, err, sizeof(err))) {
|
||||
resolve_host(parsed.host, &parsed.ip);
|
||||
print_host_details(parsed);
|
||||
} else {
|
||||
printf("Printer URI error: %s\n", err);
|
||||
}
|
||||
}
|
||||
} else
|
||||
printf("Default printer: not configured\n");
|
||||
|
||||
int pid = -1;
|
||||
if (daemon_is_running(&pid))
|
||||
printf("Daemon: running (pid %d)\n", pid);
|
||||
else
|
||||
printf("Daemon: stopped\n");
|
||||
|
||||
print_job_line(SPOOL_QUEUE_DIR, "Queued", verbose);
|
||||
print_job_line(SPOOL_ACTIVE_DIR, "Active", verbose);
|
||||
print_job_line(SPOOL_DONE_DIR, "Done", verbose);
|
||||
print_job_line(SPOOL_FAILED_DIR, "Failed", verbose);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_set_printer(const char* uri) {
|
||||
char err[128];
|
||||
IppUri parsed = {};
|
||||
if (!normalize_ipp_uri(uri, &parsed, err, sizeof(err))) {
|
||||
printf("printctl: %s\n", err);
|
||||
return 1;
|
||||
}
|
||||
if (!write_default_printer_uri(parsed.normalized)) {
|
||||
printf("printctl: failed to write default printer\n");
|
||||
return 1;
|
||||
}
|
||||
printf("%s\n", parsed.normalized);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int print_submission_result(const char* job_id, bool wait) {
|
||||
printf("queued job %s\n", job_id);
|
||||
if (!wait) return 0;
|
||||
|
||||
JobMeta job = {};
|
||||
char err[128] = {};
|
||||
bool ok = wait_for_job(job_id, &job, DEFAULT_WAIT_TIMEOUT_MS, err, sizeof(err));
|
||||
if (job.id[0] != '\0') {
|
||||
if (ok) {
|
||||
printf("completed: %s\n", job.status_message[0] ? job.status_message : "Printed");
|
||||
if (job.remote_job_id > 0) printf("printer job id: %d\n", job.remote_job_id);
|
||||
if (job.debug_info[0]) printf("detail: %s\n", job.debug_info);
|
||||
return 0;
|
||||
}
|
||||
printf("failed: %s\n", job.status_message[0] ? job.status_message : "Print failed");
|
||||
if (job.debug_info[0]) printf("detail: %s\n", job.debug_info);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("printctl: %s\n", err[0] ? err : "timed out waiting for job");
|
||||
char path[MAX_PATH_LEN];
|
||||
char state[16];
|
||||
if (find_job(job_id, path, sizeof(path), state, sizeof(state))) {
|
||||
JobMeta current = {};
|
||||
if (load_job_from_path(path, ¤t)) {
|
||||
printf("current state: %s\n", state);
|
||||
if (current.status_message[0]) printf("status: %s\n", current.status_message);
|
||||
if (current.debug_info[0]) printf("detail: %s\n", current.debug_info);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int cmd_print(const ArgList& args) {
|
||||
const char* file = nullptr;
|
||||
const char* printer = nullptr;
|
||||
const char* name = nullptr;
|
||||
bool wait = false;
|
||||
|
||||
for (int i = 1; i < args.count; i++) {
|
||||
if (strcmp(args.argv[i], "--printer") == 0 && i + 1 < args.count) {
|
||||
printer = args.argv[++i];
|
||||
} else if (strcmp(args.argv[i], "--name") == 0 && i + 1 < args.count) {
|
||||
name = args.argv[++i];
|
||||
} else if (strcmp(args.argv[i], "--wait") == 0) {
|
||||
wait = true;
|
||||
} else if (file == nullptr) {
|
||||
file = args.argv[i];
|
||||
} else {
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (file == nullptr) {
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char job_id[48];
|
||||
char err[128] = {};
|
||||
if (!submit_document_job(file, printer, name, job_id, sizeof(job_id), err, sizeof(err))) {
|
||||
printf("printctl: %s\n", err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return print_submission_result(job_id, wait);
|
||||
}
|
||||
|
||||
static int cmd_test_page(const ArgList& args) {
|
||||
const char* printer = nullptr;
|
||||
bool wait = true;
|
||||
|
||||
for (int i = 1; i < args.count; i++) {
|
||||
if (strcmp(args.argv[i], "--printer") == 0 && i + 1 < args.count) {
|
||||
printer = args.argv[++i];
|
||||
} else if (strcmp(args.argv[i], "--wait") == 0) {
|
||||
wait = true;
|
||||
} else if (strcmp(args.argv[i], "--no-wait") == 0) {
|
||||
wait = false;
|
||||
} else {
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
char job_id[48];
|
||||
char err[128] = {};
|
||||
if (!submit_test_page_job(printer, job_id, sizeof(job_id), err, sizeof(err))) {
|
||||
printf("printctl: %s\n", err);
|
||||
return 1;
|
||||
}
|
||||
return print_submission_result(job_id, wait);
|
||||
}
|
||||
|
||||
static int cmd_inspect(const char* job_id) {
|
||||
char path[MAX_PATH_LEN];
|
||||
char state[16];
|
||||
if (!find_job(job_id, path, sizeof(path), state, sizeof(state))) {
|
||||
printf("printctl: job %s not found\n", job_id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
JobMeta job = {};
|
||||
if (!load_job_from_path(path, &job)) {
|
||||
printf("printctl: failed to load job %s\n", job_id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Job ID: %s\n", job.id);
|
||||
printf("State: %s\n", state);
|
||||
printf("Name: %s\n", job.job_name);
|
||||
if (job.user_name[0]) printf("User: %s\n", job.user_name);
|
||||
if (job.printer_uri[0]) printf("Printer: %s\n", job.printer_uri);
|
||||
if (job.doc_format[0]) printf("Format: %s\n", job.doc_format);
|
||||
if (job.source_name[0]) printf("Source: %s\n", job.source_name);
|
||||
if (job.created_at[0]) printf("Created: %s\n", job.created_at);
|
||||
if (job.updated_at[0]) printf("Updated: %s\n", job.updated_at);
|
||||
if (job.remote_job_id > 0) printf("Printer job id: %d\n", job.remote_job_id);
|
||||
if (job.status_message[0]) printf("Status: %s\n", job.status_message);
|
||||
if (job.debug_info[0]) printf("Debug: %s\n", job.debug_info);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_probe(const char* printer_uri) {
|
||||
char uri_buf[MAX_PATH_LEN];
|
||||
if (printer_uri && *printer_uri) {
|
||||
safe_copy(uri_buf, sizeof(uri_buf), printer_uri);
|
||||
} else if (!read_default_printer_uri(uri_buf, sizeof(uri_buf))) {
|
||||
printf("printctl: no default printer is configured\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
char err[128] = {};
|
||||
IppUri normalized = {};
|
||||
if (!normalize_ipp_uri(uri_buf, &normalized, err, sizeof(err))) {
|
||||
printf("printctl: %s\n", err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Printer URI: %s\n", normalized.normalized);
|
||||
print_host_details(normalized);
|
||||
|
||||
uint32_t ip = 0;
|
||||
if (!resolve_host(normalized.host, &ip)) {
|
||||
if (host_looks_like_mdns(normalized.host))
|
||||
printf("Resolution: unresolved (.local/mDNS not supported yet)\n");
|
||||
else
|
||||
printf("Resolution: unresolved\n");
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
ArgList args = {};
|
||||
parse_args(&args);
|
||||
if (args.count <= 0) {
|
||||
usage();
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
int rc = 1;
|
||||
if (strcmp(args.argv[0], "set-printer") == 0) {
|
||||
if (args.count < 2) usage();
|
||||
else rc = cmd_set_printer(args.argv[1]);
|
||||
} else if (strcmp(args.argv[0], "show-printer") == 0) {
|
||||
char uri[MAX_PATH_LEN];
|
||||
if (read_default_printer_uri(uri, sizeof(uri))) {
|
||||
printf("%s\n", uri);
|
||||
rc = 0;
|
||||
} else {
|
||||
printf("printctl: no default printer is configured\n");
|
||||
rc = 1;
|
||||
}
|
||||
} else if (strcmp(args.argv[0], "print") == 0) {
|
||||
rc = cmd_print(args);
|
||||
} else if (strcmp(args.argv[0], "test-page") == 0) {
|
||||
rc = cmd_test_page(args);
|
||||
} else if (strcmp(args.argv[0], "status") == 0) {
|
||||
rc = cmd_status(args);
|
||||
} else if (strcmp(args.argv[0], "inspect") == 0) {
|
||||
if (args.count < 2) usage();
|
||||
else rc = cmd_inspect(args.argv[1]);
|
||||
} else if (strcmp(args.argv[0], "probe") == 0) {
|
||||
rc = cmd_probe(args.count >= 2 ? args.argv[1] : nullptr);
|
||||
} else {
|
||||
usage();
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
montauk::exit(rc);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# Makefile for printd (userspace print spooler) on MontaukOS
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LIBC_LIB := ../../lib/libc
|
||||
TLS_LIB := ../../lib/tls
|
||||
BEARSSL := ../../lib/bearssl
|
||||
JPEGWRITE := ../../lib/libjpegwrite
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-I $(BEARSSL)/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp test_page_jpeg.cpp font_data.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
LIBS := $(TLS_LIB)/libtls.a $(BEARSSL)/libbearssl.a $(JPEGWRITE)/libjpegwrite.a $(LIBC_LIB)/liblibc.a
|
||||
TARGET := $(BINDIR)/os/printd.elf
|
||||
DEPS := $(OBJS:.o=.d)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
-include $(DEPS)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1 @@
|
||||
#include "../desktop/font_data.cpp"
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS userspace print spooler daemon
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <print/print.hpp>
|
||||
#include "test_page_jpeg.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
}
|
||||
|
||||
using namespace print;
|
||||
|
||||
static void log_msg(const char* fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
printf("[printd] ");
|
||||
vprintf(fmt, ap);
|
||||
printf("\n");
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static void set_job_state(JobMeta* job, const char* state, const char* message) {
|
||||
safe_copy(job->state, sizeof(job->state), state);
|
||||
if (message && *message) safe_copy(job->status_message, sizeof(job->status_message), message);
|
||||
now_string(job->updated_at, sizeof(job->updated_at));
|
||||
}
|
||||
|
||||
static void set_job_debug(JobMeta* job, const char* detail) {
|
||||
if (job == nullptr) return;
|
||||
safe_copy(job->debug_info, sizeof(job->debug_info), detail ? detail : "");
|
||||
}
|
||||
|
||||
static void set_job_debug_from_result(JobMeta* job, const IppPrintResult* result, const char* prefix) {
|
||||
if (job == nullptr) return;
|
||||
char detail[MAX_DEBUG_LEN] = {};
|
||||
summarize_ipp_print_result(result, detail, sizeof(detail));
|
||||
if (prefix && *prefix) {
|
||||
if (detail[0]) {
|
||||
char joined[MAX_DEBUG_LEN];
|
||||
snprintf(joined, sizeof(joined), "%s; %s", prefix, detail);
|
||||
safe_copy(job->debug_info, sizeof(job->debug_info), joined);
|
||||
} else {
|
||||
safe_copy(job->debug_info, sizeof(job->debug_info), prefix);
|
||||
}
|
||||
} else {
|
||||
safe_copy(job->debug_info, sizeof(job->debug_info), detail);
|
||||
}
|
||||
}
|
||||
|
||||
static void persist_job_debug(const char* active_path, JobMeta* job, const char* detail) {
|
||||
if (active_path == nullptr || *active_path == '\0' || job == nullptr) return;
|
||||
set_job_debug(job, detail);
|
||||
now_string(job->updated_at, sizeof(job->updated_at));
|
||||
save_job_to_path_atomic(active_path, job);
|
||||
}
|
||||
|
||||
static bool write_pid_file() {
|
||||
char pid[32];
|
||||
snprintf(pid, sizeof(pid), "%d\n", montauk::getpid());
|
||||
return write_text_file_atomic(SPOOL_DAEMON_PID_PATH, pid);
|
||||
}
|
||||
|
||||
static void requeue_stale_active_jobs() {
|
||||
DIR* dir = opendir(SPOOL_ACTIVE_DIR);
|
||||
if (!dir) return;
|
||||
|
||||
struct dirent* ent = nullptr;
|
||||
while ((ent = readdir(dir)) != nullptr) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
|
||||
char src[MAX_PATH_LEN];
|
||||
char dst[MAX_PATH_LEN];
|
||||
path_join(src, sizeof(src), SPOOL_ACTIVE_DIR, ent->d_name);
|
||||
path_join(dst, sizeof(dst), SPOOL_QUEUE_DIR, ent->d_name);
|
||||
|
||||
JobMeta job = {};
|
||||
if (load_job_from_path(src, &job)) {
|
||||
set_job_state(&job, "queued", "Re-queued after daemon restart");
|
||||
save_job_to_path_atomic(src, &job);
|
||||
}
|
||||
rename(src, dst);
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
static bool claim_next_job(char* out_active_path, int out_active_path_len) {
|
||||
DIR* dir = opendir(SPOOL_QUEUE_DIR);
|
||||
if (!dir) return false;
|
||||
|
||||
bool claimed = false;
|
||||
struct dirent* ent = nullptr;
|
||||
while ((ent = readdir(dir)) != nullptr) {
|
||||
if (ent->d_name[0] == '.') continue;
|
||||
|
||||
char queued[MAX_PATH_LEN];
|
||||
char active[MAX_PATH_LEN];
|
||||
path_join(queued, sizeof(queued), SPOOL_QUEUE_DIR, ent->d_name);
|
||||
path_join(active, sizeof(active), SPOOL_ACTIVE_DIR, ent->d_name);
|
||||
|
||||
if (rename(queued, active) == 0) {
|
||||
safe_copy(out_active_path, out_active_path_len, active);
|
||||
claimed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return claimed;
|
||||
}
|
||||
|
||||
static void finalize_job(const char* active_path, const char* target_dir, JobMeta* job) {
|
||||
save_job_to_path_atomic(active_path, job);
|
||||
char target[MAX_PATH_LEN];
|
||||
make_job_file_path(target_dir, job->id, target, sizeof(target));
|
||||
if (rename(active_path, target) != 0) {
|
||||
log_msg("warning: failed to move job %s to %s", job->id, target_dir);
|
||||
}
|
||||
}
|
||||
|
||||
static bool try_print_test_page(const char* active_path, JobMeta* job,
|
||||
IppPrintResult* result, char* err, int err_len) {
|
||||
persist_job_debug(active_path, job, "querying printer capabilities");
|
||||
|
||||
IppCapabilities caps = {};
|
||||
bool have_caps = ipp_get_printer_capabilities(job->printer_uri, &caps, err, err_len);
|
||||
if (have_caps) {
|
||||
char detail[MAX_DEBUG_LEN] = {};
|
||||
summarize_ipp_capabilities(&caps, detail, sizeof(detail));
|
||||
persist_job_debug(active_path, job, detail);
|
||||
} else if (err && err[0]) {
|
||||
char detail[MAX_DEBUG_LEN];
|
||||
snprintf(detail, sizeof(detail), "capability probe failed; attempting direct submit (%s)", err);
|
||||
persist_job_debug(active_path, job, detail);
|
||||
}
|
||||
|
||||
const struct {
|
||||
const char* mime;
|
||||
bool (*generate)(uint8_t**, int*);
|
||||
bool allowed;
|
||||
} attempts[] = {
|
||||
{"application/pdf", generate_test_page_pdf, !have_caps || caps.supports_pdf},
|
||||
{"image/jpeg", generate_test_page_jpeg, !have_caps || caps.supports_jpeg},
|
||||
{"text/plain", generate_test_page_text, !have_caps || caps.supports_text},
|
||||
};
|
||||
|
||||
if (have_caps && !caps.supports_pdf && !caps.supports_jpeg && !caps.supports_text) {
|
||||
if (caps.supported_formats[0]) {
|
||||
snprintf(err, (size_t)err_len,
|
||||
"printer does not advertise a built-in test-page format (supports: %s)",
|
||||
caps.supported_formats);
|
||||
} else {
|
||||
safe_copy(err, err_len, "printer does not advertise a built-in test-page format");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(attempts) / sizeof(attempts[0]); i++) {
|
||||
if (!attempts[i].allowed) continue;
|
||||
|
||||
uint8_t* data = nullptr;
|
||||
int len = 0;
|
||||
if (!attempts[i].generate(&data, &len)) {
|
||||
safe_copy(err, err_len, "failed to generate test page");
|
||||
return false;
|
||||
}
|
||||
|
||||
char detail[MAX_DEBUG_LEN];
|
||||
snprintf(detail, sizeof(detail), "submitting test page as %s (%d bytes)",
|
||||
attempts[i].mime, len);
|
||||
persist_job_debug(active_path, job, detail);
|
||||
|
||||
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
|
||||
attempts[i].mime, data, len, result, err, err_len);
|
||||
free(data);
|
||||
if (ok) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool try_print_document(const char* active_path, JobMeta* job,
|
||||
IppPrintResult* result, char* err, int err_len) {
|
||||
persist_job_debug(active_path, job, "loading document bytes");
|
||||
|
||||
uint8_t* data = nullptr;
|
||||
int len = 0;
|
||||
if (!read_file_bytes(job->doc_path, &data, &len, err, err_len)) return false;
|
||||
|
||||
char detail[MAX_DEBUG_LEN];
|
||||
snprintf(detail, sizeof(detail), "submitting %s document (%d bytes)",
|
||||
job->doc_format[0] ? job->doc_format : "application/octet-stream", len);
|
||||
persist_job_debug(active_path, job, detail);
|
||||
|
||||
bool ok = ipp_print_buffer(job->printer_uri, job->job_name, job->user_name,
|
||||
job->doc_format, data, len, result, err, err_len);
|
||||
free(data);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void process_job(const char* active_path) {
|
||||
JobMeta job = {};
|
||||
if (!load_job_from_path(active_path, &job)) {
|
||||
log_msg("dropping unreadable job file: %s", active_path);
|
||||
remove(active_path);
|
||||
return;
|
||||
}
|
||||
|
||||
log_msg("processing job %s (%s)", job.id, job.job_name);
|
||||
set_job_state(&job, "printing", "Sending to printer");
|
||||
set_job_debug(&job, "dispatching queued job");
|
||||
save_job_to_path_atomic(active_path, &job);
|
||||
|
||||
IppPrintResult result = {};
|
||||
char err[128] = {};
|
||||
bool ok = false;
|
||||
|
||||
if (strcmp(job.source_kind, "test-page") == 0)
|
||||
ok = try_print_test_page(active_path, &job, &result, err, sizeof(err));
|
||||
else
|
||||
ok = try_print_document(active_path, &job, &result, err, sizeof(err));
|
||||
|
||||
if (ok) {
|
||||
job.remote_job_id = result.job_id;
|
||||
set_job_state(&job, "completed",
|
||||
result.status_message[0] ? result.status_message : "Printed");
|
||||
set_job_debug_from_result(&job, &result, nullptr);
|
||||
finalize_job(active_path, SPOOL_DONE_DIR, &job);
|
||||
log_msg("job %s completed (printer job id %d)", job.id, result.job_id);
|
||||
} else {
|
||||
set_job_state(&job, "failed", err[0] ? err : "Print failed");
|
||||
set_job_debug_from_result(&job, &result, err[0] ? err : nullptr);
|
||||
finalize_job(active_path, SPOOL_FAILED_DIR, &job);
|
||||
log_msg("job %s failed: %s", job.id, job.status_message);
|
||||
if (job.debug_info[0]) log_msg("job %s debug: %s", job.id, job.debug_info);
|
||||
}
|
||||
|
||||
if (job.doc_path[0] != '\0') remove(job.doc_path);
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!ensure_spool_dirs()) {
|
||||
log_msg("failed to initialize spool directories");
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
int running_pid = -1;
|
||||
if (daemon_is_running(&running_pid) && running_pid != montauk::getpid()) {
|
||||
log_msg("another print daemon is already running (pid %d)", running_pid);
|
||||
montauk::exit(0);
|
||||
}
|
||||
|
||||
if (!write_pid_file()) {
|
||||
log_msg("failed to write daemon pid file");
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
requeue_stale_active_jobs();
|
||||
log_msg("ready");
|
||||
|
||||
for (;;) {
|
||||
char active_path[MAX_PATH_LEN];
|
||||
if (claim_next_job(active_path, sizeof(active_path))) {
|
||||
process_job(active_path);
|
||||
continue;
|
||||
}
|
||||
montauk::sleep_ms(1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* test_page_jpeg.cpp
|
||||
* JPEG test-page generator for printers that do not accept PDF/text jobs.
|
||||
*/
|
||||
|
||||
#include "test_page_jpeg.hpp"
|
||||
|
||||
#include <gui/truetype.hpp>
|
||||
#include <montauk/string.h>
|
||||
#include <print/print.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define STBI_WRITE_NO_STDIO
|
||||
#include <gui/stb_image_write.h>
|
||||
}
|
||||
|
||||
namespace gui {
|
||||
extern const uint8_t font_data[256 * 16];
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
static constexpr int PAGE_W = 1240;
|
||||
static constexpr int PAGE_H = 1754;
|
||||
static constexpr const char* ROBOTO_MEDIUM_PATH = "0:/fonts/Roboto-Medium.ttf";
|
||||
static constexpr const char* ROBOTO_BOLD_PATH = "0:/fonts/Roboto-Bold.ttf";
|
||||
|
||||
struct JpegBuffer {
|
||||
uint8_t* data;
|
||||
int size;
|
||||
int capacity;
|
||||
bool failed;
|
||||
};
|
||||
|
||||
struct PageFonts {
|
||||
gui::TrueTypeFont* medium;
|
||||
gui::TrueTypeFont* bold;
|
||||
};
|
||||
|
||||
static void jpeg_write_callback(void* context, void* data, int size) {
|
||||
JpegBuffer* buf = (JpegBuffer*)context;
|
||||
if (!buf || !data || size <= 0 || buf->failed) return;
|
||||
|
||||
int needed = buf->size + size;
|
||||
if (needed > buf->capacity) {
|
||||
int new_cap = buf->capacity > 0 ? buf->capacity * 2 : 4096;
|
||||
if (new_cap < needed) new_cap = needed;
|
||||
uint8_t* new_data = (uint8_t*)realloc(buf->data, (size_t)new_cap);
|
||||
if (!new_data) {
|
||||
buf->failed = true;
|
||||
return;
|
||||
}
|
||||
buf->data = new_data;
|
||||
buf->capacity = new_cap;
|
||||
}
|
||||
|
||||
memcpy(buf->data + buf->size, data, (size_t)size);
|
||||
buf->size += size;
|
||||
}
|
||||
|
||||
static void fill_rect(uint8_t* rgb, int x, int y, int w, int h, uint8_t r, uint8_t g, uint8_t b) {
|
||||
if (!rgb || w <= 0 || h <= 0) return;
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
if (y < 0) { h += y; y = 0; }
|
||||
if (x + w > PAGE_W) w = PAGE_W - x;
|
||||
if (y + h > PAGE_H) h = PAGE_H - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
for (int yy = y; yy < y + h; yy++) {
|
||||
uint8_t* row = rgb + (yy * PAGE_W + x) * 3;
|
||||
for (int xx = 0; xx < w; xx++) {
|
||||
row[xx * 3 + 0] = r;
|
||||
row[xx * 3 + 1] = g;
|
||||
row[xx * 3 + 2] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_rect_outline(uint8_t* rgb, int x, int y, int w, int h,
|
||||
int thickness, uint8_t r, uint8_t g, uint8_t b) {
|
||||
fill_rect(rgb, x, y, w, thickness, r, g, b);
|
||||
fill_rect(rgb, x, y + h - thickness, w, thickness, r, g, b);
|
||||
fill_rect(rgb, x, y, thickness, h, r, g, b);
|
||||
fill_rect(rgb, x + w - thickness, y, thickness, h, r, g, b);
|
||||
}
|
||||
|
||||
static void blend_pixel(uint8_t* rgb, int x, int y,
|
||||
uint8_t r, uint8_t g, uint8_t b, uint8_t alpha) {
|
||||
if (!rgb || alpha == 0 || x < 0 || x >= PAGE_W || y < 0 || y >= PAGE_H) return;
|
||||
uint8_t* p = rgb + (y * PAGE_W + x) * 3;
|
||||
if (alpha == 255) {
|
||||
p[0] = r;
|
||||
p[1] = g;
|
||||
p[2] = b;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t a = alpha;
|
||||
uint32_t inv = 255 - a;
|
||||
p[0] = (uint8_t)((a * r + inv * p[0] + 127) / 255);
|
||||
p[1] = (uint8_t)((a * g + inv * p[1] + 127) / 255);
|
||||
p[2] = (uint8_t)((a * b + inv * p[2] + 127) / 255);
|
||||
}
|
||||
|
||||
static void draw_char_scaled(uint8_t* rgb, int x, int y, int scale, char ch,
|
||||
uint8_t r, uint8_t g, uint8_t b) {
|
||||
const uint8_t* glyph = &gui::font_data[(unsigned char)ch * 16];
|
||||
for (int row = 0; row < 16; row++) {
|
||||
uint8_t bits = glyph[row];
|
||||
for (int col = 0; col < 8; col++) {
|
||||
if (!(bits & (0x80 >> col))) continue;
|
||||
fill_rect(rgb, x + col * scale, y + row * scale, scale, scale, r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_text_scaled(uint8_t* rgb, int x, int y, int scale, const char* text,
|
||||
uint8_t r, uint8_t g, uint8_t b) {
|
||||
if (!rgb || !text) return;
|
||||
for (int i = 0; text[i] != '\0'; i++)
|
||||
draw_char_scaled(rgb, x + i * 8 * scale, y, scale, text[i], r, g, b);
|
||||
}
|
||||
|
||||
static bool init_page_font(gui::TrueTypeFont* font, const char* path) {
|
||||
if (!font || !path) return false;
|
||||
memset(font, 0, sizeof(*font));
|
||||
return font->init(path);
|
||||
}
|
||||
|
||||
static gui::TrueTypeFont* load_page_font(const char* path) {
|
||||
gui::TrueTypeFont* font = (gui::TrueTypeFont*)montauk::malloc(sizeof(gui::TrueTypeFont));
|
||||
if (!font) return nullptr;
|
||||
if (!init_page_font(font, path)) {
|
||||
montauk::mfree(font);
|
||||
return nullptr;
|
||||
}
|
||||
return font;
|
||||
}
|
||||
|
||||
static void free_page_font(gui::TrueTypeFont* font) {
|
||||
if (!font) return;
|
||||
for (int i = 0; i < font->cache_count; i++) {
|
||||
for (int g = 0; g < 256; g++) {
|
||||
if (font->caches[i].glyphs[g].bitmap) {
|
||||
montauk::mfree(font->caches[i].glyphs[g].bitmap);
|
||||
font->caches[i].glyphs[g].bitmap = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (font->data) {
|
||||
montauk::free(font->data);
|
||||
font->data = nullptr;
|
||||
}
|
||||
montauk::mfree(font);
|
||||
}
|
||||
|
||||
static void destroy_page_fonts(PageFonts* fonts) {
|
||||
if (!fonts) return;
|
||||
free_page_font(fonts->medium);
|
||||
free_page_font(fonts->bold);
|
||||
fonts->medium = nullptr;
|
||||
fonts->bold = nullptr;
|
||||
}
|
||||
|
||||
static PageFonts load_page_fonts() {
|
||||
PageFonts fonts = {};
|
||||
fonts.medium = load_page_font(ROBOTO_MEDIUM_PATH);
|
||||
fonts.bold = load_page_font(ROBOTO_BOLD_PATH);
|
||||
return fonts;
|
||||
}
|
||||
|
||||
static gui::TrueTypeFont* pick_font(PageFonts* fonts, bool bold) {
|
||||
if (!fonts) return nullptr;
|
||||
if (bold && fonts->bold && fonts->bold->valid) return fonts->bold;
|
||||
if (fonts->medium && fonts->medium->valid) return fonts->medium;
|
||||
if (fonts->bold && fonts->bold->valid) return fonts->bold;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static int measure_text(uint8_t* rgb, PageFonts* fonts, bool bold,
|
||||
int pixel_size, int fallback_scale, const char* text) {
|
||||
(void)rgb;
|
||||
if (!text) return 0;
|
||||
gui::TrueTypeFont* font = pick_font(fonts, bold);
|
||||
if (font && font->valid) return font->measure_text(text, pixel_size);
|
||||
return (int)strlen(text) * 8 * fallback_scale;
|
||||
}
|
||||
|
||||
static void draw_text(uint8_t* rgb, PageFonts* fonts, bool bold,
|
||||
int x, int y, int pixel_size, int fallback_scale,
|
||||
const char* text, uint8_t r, uint8_t g, uint8_t b) {
|
||||
if (!rgb || !text) return;
|
||||
|
||||
gui::TrueTypeFont* font = pick_font(fonts, bold);
|
||||
if (!font || !font->valid) {
|
||||
draw_text_scaled(rgb, x, y, fallback_scale, text, r, g, b);
|
||||
return;
|
||||
}
|
||||
|
||||
gui::GlyphCache* gc = font->get_cache(pixel_size);
|
||||
if (!gc) {
|
||||
draw_text_scaled(rgb, x, y, fallback_scale, text, r, g, b);
|
||||
return;
|
||||
}
|
||||
|
||||
int cx = x;
|
||||
int baseline = y + gc->ascent;
|
||||
for (int i = 0; text[i] != '\0'; i++) {
|
||||
gui::CachedGlyph* glyph = font->get_glyph(gc, (unsigned char)text[i]);
|
||||
if (!glyph) continue;
|
||||
|
||||
if (glyph->bitmap) {
|
||||
int gx = cx + glyph->xoff;
|
||||
int gy = baseline + glyph->yoff;
|
||||
for (int row = 0; row < glyph->height; row++) {
|
||||
for (int col = 0; col < glyph->width; col++) {
|
||||
uint8_t alpha = glyph->bitmap[row * glyph->width + col];
|
||||
if (alpha == 0) continue;
|
||||
blend_pixel(rgb, gx + col, gy + row, r, g, b, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
cx += glyph->advance;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_centered_text(uint8_t* rgb, PageFonts* fonts, bool bold,
|
||||
int y, int pixel_size, int fallback_scale,
|
||||
const char* text, uint8_t r, uint8_t g, uint8_t b) {
|
||||
if (!text) return;
|
||||
int width = measure_text(rgb, fonts, bold, pixel_size, fallback_scale, text);
|
||||
int x = (PAGE_W - width) / 2;
|
||||
draw_text(rgb, fonts, bold, x, y, pixel_size, fallback_scale, text, r, g, b);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool generate_test_page_jpeg(uint8_t** out_data, int* out_len) {
|
||||
if (out_data) *out_data = nullptr;
|
||||
if (out_len) *out_len = 0;
|
||||
|
||||
uint8_t* rgb = (uint8_t*)malloc((size_t)PAGE_W * PAGE_H * 3);
|
||||
if (!rgb) return false;
|
||||
memset(rgb, 0xFF, (size_t)PAGE_W * PAGE_H * 3);
|
||||
|
||||
PageFonts fonts = load_page_fonts();
|
||||
|
||||
draw_centered_text(rgb, &fonts, true, 50, 52, 3,
|
||||
"Montauk Operating System", 0x22, 0x22, 0x22);
|
||||
draw_centered_text(rgb, &fonts, false, 112, 26, 2,
|
||||
"Printer Test Page for IPP Printers", 0x66, 0x66, 0x66);
|
||||
fill_rect(rgb, 120, 170, PAGE_W - 240, 4, 0x29, 0x8F, 0x45);
|
||||
|
||||
fill_rect(rgb, 80, 220, PAGE_W - 160, 240, 0xFA, 0xFA, 0xFA);
|
||||
draw_rect_outline(rgb, 80, 220, PAGE_W - 160, 240, 3, 0xD9, 0xD9, 0xD9);
|
||||
|
||||
char timestamp[32];
|
||||
print::now_string(timestamp, sizeof(timestamp));
|
||||
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line), "Generated: %s", timestamp);
|
||||
draw_text(rgb, &fonts, false, 120, 268, 30, 2, line, 0x22, 0x22, 0x22);
|
||||
draw_text(rgb, &fonts, false, 120, 320, 30, 2, "Format: image/jpeg", 0x22, 0x22, 0x22);
|
||||
draw_text(rgb, &fonts, false, 120, 372, 30, 2, "If you can read this, printing works.", 0x22, 0x22, 0x22);
|
||||
|
||||
draw_text(rgb, &fonts, true, 100, 530, 32, 2, "Color Check", 0x22, 0x22, 0x22);
|
||||
draw_rect_outline(rgb, 100, 590, PAGE_W - 200, 150, 3, 0xD0, 0xD0, 0xD0);
|
||||
fill_rect(rgb, 130, 625, 150, 72, 0x30, 0x30, 0x30);
|
||||
fill_rect(rgb, 320, 625, 150, 72, 0x25, 0xA8, 0xD8);
|
||||
fill_rect(rgb, 510, 625, 150, 72, 0xD8, 0x3C, 0x82);
|
||||
fill_rect(rgb, 700, 625, 150, 72, 0xE9, 0xCF, 0x4A);
|
||||
fill_rect(rgb, 890, 625, 150, 72, 0x47, 0x98, 0x5E);
|
||||
draw_text(rgb, &fonts, false, 150, 706, 18, 1, "Black", 0x33, 0x33, 0x33);
|
||||
draw_text(rgb, &fonts, false, 352, 706, 18, 1, "Cyan", 0x33, 0x33, 0x33);
|
||||
draw_text(rgb, &fonts, false, 518, 706, 18, 1, "Magenta", 0x33, 0x33, 0x33);
|
||||
draw_text(rgb, &fonts, false, 715, 706, 18, 1, "Yellow", 0x33, 0x33, 0x33);
|
||||
draw_text(rgb, &fonts, false, 932, 706, 18, 1, "Green", 0x33, 0x33, 0x33);
|
||||
|
||||
draw_text(rgb, &fonts, true, 100, 792, 32, 2, "Grayscale", 0x22, 0x22, 0x22);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
uint8_t v = (uint8_t)(i * 255 / 9);
|
||||
fill_rect(rgb, 100 + i * ((PAGE_W - 200) / 10), 855,
|
||||
(PAGE_W - 200) / 10, 72, v, v, v);
|
||||
}
|
||||
draw_rect_outline(rgb, 100, 855, PAGE_W - 200, 72, 3, 0xC8, 0xC8, 0xC8);
|
||||
|
||||
draw_text(rgb, &fonts, true, 100, 998, 32, 2, "Alignment", 0x22, 0x22, 0x22);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
int yy = 1070 + i * 60;
|
||||
fill_rect(rgb, 100, yy, PAGE_W - 200, 3, 0x11, 0x11, 0x11);
|
||||
fill_rect(rgb, 100 + i * 40, yy - 15, 3, 33, 0x11, 0x11, 0x11);
|
||||
}
|
||||
|
||||
JpegBuffer jpeg = {};
|
||||
int ok = stbi_write_jpg_to_func(jpeg_write_callback, &jpeg, PAGE_W, PAGE_H, 3, rgb, 82);
|
||||
destroy_page_fonts(&fonts);
|
||||
free(rgb);
|
||||
if (!ok || jpeg.failed || !jpeg.data || jpeg.size <= 0) {
|
||||
free(jpeg.data);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out_data) *out_data = jpeg.data;
|
||||
else free(jpeg.data);
|
||||
if (out_len) *out_len = jpeg.size;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
bool generate_test_page_jpeg(uint8_t** out_data, int* out_len);
|
||||
@@ -0,0 +1,79 @@
|
||||
# Makefile for printers (standalone Window Server app) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-MMD -MP \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-I ../../lib/bearssl/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/printers/printers.elf
|
||||
LIBS := $(LIBDIR)/tls/libtls.a $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(BINDIR)/apps/printers
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1 @@
|
||||
#include "../desktop/font_data.cpp"
|
||||
@@ -0,0 +1,789 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Printers - standalone Window Server app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
#include <print/print.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
using namespace print;
|
||||
|
||||
static constexpr int INIT_W = 560;
|
||||
static constexpr int INIT_H = 500;
|
||||
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;
|
||||
|
||||
struct AppState {
|
||||
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_detail[MAX_DEBUG_LEN];
|
||||
|
||||
char status_msg[128];
|
||||
uint64_t status_time;
|
||||
|
||||
bool config_open;
|
||||
char edit_uri[MAX_PATH_LEN];
|
||||
int edit_len;
|
||||
char edit_error[128];
|
||||
|
||||
Color accent;
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
Rect configure_btn;
|
||||
Rect clear_btn;
|
||||
Rect probe_btn;
|
||||
Rect start_btn;
|
||||
Rect refresh_btn;
|
||||
Rect test_btn;
|
||||
|
||||
Rect dialog;
|
||||
Rect dialog_input;
|
||||
Rect dialog_save;
|
||||
Rect dialog_cancel;
|
||||
};
|
||||
|
||||
static WsWindow g_win;
|
||||
static AppState g_app = {};
|
||||
|
||||
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_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 set_accent(Color accent) {
|
||||
g_app.accent = accent;
|
||||
}
|
||||
|
||||
static void load_accent() {
|
||||
set_accent(colors::ACCENT);
|
||||
|
||||
char user[64];
|
||||
montauk::memset(user, 0, sizeof(user));
|
||||
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
|
||||
auto doc = montauk::config::load_user(user, "desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
doc.destroy();
|
||||
return;
|
||||
}
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
auto doc = montauk::config::load("desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
}
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
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 draw_action_btn(Canvas& c, const Rect& r, const char* label,
|
||||
bool enabled, bool primary) {
|
||||
Color bg;
|
||||
Color fg;
|
||||
if (!enabled) {
|
||||
bg = Color::from_rgb(0xE6, 0xE6, 0xE6);
|
||||
fg = Color::from_rgb(0x9A, 0x9A, 0x9A);
|
||||
} else if (primary) {
|
||||
bg = g_app.accent;
|
||||
fg = colors::WHITE;
|
||||
} else {
|
||||
bg = colors::WINDOW_BG;
|
||||
fg = colors::TEXT_COLOR;
|
||||
}
|
||||
|
||||
c.fill_rounded_rect(r.x, r.y, r.w, r.h, ACTION_RADIUS, bg);
|
||||
if (enabled && !primary) c.rect(r.x, r.y, r.w, r.h, colors::BORDER);
|
||||
if (!enabled) c.rect(r.x, r.y, r.w, r.h, Color::from_rgb(0xD4, 0xD4, 0xD4));
|
||||
|
||||
int tw = text_width(label);
|
||||
int fh = system_font_height();
|
||||
c.text(r.x + (r.w - tw) / 2, r.y + (r.h - fh) / 2, label, fg);
|
||||
}
|
||||
|
||||
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();
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
|
||||
c.text(x, y, label, dim);
|
||||
y += sfh + 2;
|
||||
c.fill_rounded_rect(x, y, w, h, 3, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.rect(x, y, w, h, focused ? g_app.accent : colors::BORDER);
|
||||
c.text(x + 8, y + (h - sfh) / 2, value, colors::TEXT_COLOR);
|
||||
|
||||
if (focused) {
|
||||
int tw = text_width(value);
|
||||
c.fill_rect(x + 8 + tw, y + 6, 2, h - 12, g_app.accent);
|
||||
}
|
||||
}
|
||||
|
||||
static void compute_layout(Layout* lo) {
|
||||
int sfh = system_font_height();
|
||||
int x = PAD_X;
|
||||
int y = 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};
|
||||
|
||||
y += BTN_H + 8;
|
||||
y += sfh + 4;
|
||||
y += sfh + 14;
|
||||
y += 16;
|
||||
|
||||
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 += sfh + 4;
|
||||
if (g_app.last_failed.debug_info[0]) y += sfh + 4;
|
||||
}
|
||||
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 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))
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
g_app.config_open = false;
|
||||
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);
|
||||
|
||||
int sfh = system_font_height();
|
||||
int y = lo.dialog.y + 16;
|
||||
c.text(lo.dialog.x + 16, 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));
|
||||
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",
|
||||
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 += 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));
|
||||
|
||||
draw_action_btn(c, lo.dialog_save, "Save", true, true);
|
||||
draw_action_btn(c, lo.dialog_cancel, "Cancel", true, false);
|
||||
}
|
||||
|
||||
static void render() {
|
||||
refresh_state();
|
||||
|
||||
Canvas c = g_win.canvas();
|
||||
c.fill(colors::WINDOW_BG);
|
||||
|
||||
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 = PAD_Y;
|
||||
int w = g_win.width - 2 * x;
|
||||
Layout lo = {};
|
||||
compute_layout(&lo);
|
||||
|
||||
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);
|
||||
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);
|
||||
y += sfh + 4;
|
||||
c.text(x, y, "System-wide IPP / IPPS destination", dim);
|
||||
y += sfh + 14;
|
||||
c.hline(x, y, w, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// Connection
|
||||
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;
|
||||
|
||||
if (g_app.printer_uri[0]) {
|
||||
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);
|
||||
c.text(x, y, line, colors::TEXT_COLOR);
|
||||
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);
|
||||
} else if (host_looks_like_mdns(normalized.host)) {
|
||||
safe_copy(line, sizeof(line), "Resolved: unavailable (.local/mDNS not supported yet)");
|
||||
} else {
|
||||
safe_copy(line, sizeof(line), "Resolved: unavailable");
|
||||
}
|
||||
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);
|
||||
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);
|
||||
y += sfh + 4;
|
||||
c.text(x, y, "Resolved: unavailable", 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 ? colors::TEXT_COLOR : 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");
|
||||
} 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.");
|
||||
}
|
||||
} else {
|
||||
safe_copy(line, sizeof(line), "Probe: not run");
|
||||
c.text(x, y, line, 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;
|
||||
c.hline(x, y, w, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// Spooler
|
||||
c.text(x, y + 6, "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);
|
||||
|
||||
const char* daemon_label = g_app.daemon_running ? "Running" : "Stopped";
|
||||
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);
|
||||
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, colors::TEXT_COLOR);
|
||||
y += sfh + 4;
|
||||
snprintf(line, sizeof(line), "Completed: %d Failed: %d", g_app.done_count, g_app.failed_count);
|
||||
c.text(x, y, line, colors::TEXT_COLOR);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
y += 10;
|
||||
c.hline(x, y, w, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// 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);
|
||||
|
||||
if (g_app.config_open)
|
||||
draw_modal(c, lo);
|
||||
|
||||
g_win.present();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
if (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.printer_uri[0] && lo.probe_btn.contains(mx, my)) {
|
||||
probe_printer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!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 (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)) {
|
||||
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 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;
|
||||
}
|
||||
|
||||
if (key.scancode == 0x01) {
|
||||
g_win.closed = true;
|
||||
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);
|
||||
|
||||
load_accent();
|
||||
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;
|
||||
int r = g_win.poll(&ev);
|
||||
if (r < 0) break;
|
||||
if (r == 0) {
|
||||
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 == 0) {
|
||||
handle_key(ev.key);
|
||||
redraw = 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (redraw)
|
||||
render();
|
||||
}
|
||||
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[app]
|
||||
name = "Printers"
|
||||
binary = "printers.elf"
|
||||
icon = "preferences-devices-printer.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = true
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in New Issue
Block a user