feat: support for IPP printing + test page
This commit is contained in:
@@ -174,6 +174,9 @@ kernel-deps:
|
||||
kernel: kernel-deps
|
||||
$(MAKE) -C kernel
|
||||
|
||||
.PHONY: programs
|
||||
programs:
|
||||
$(MAKE) -C programs
|
||||
|
||||
.PHONY: ramdisk
|
||||
ramdisk: limine/limine kernel programs
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Fs {
|
||||
Ramdisk::Create,
|
||||
Ramdisk::Delete,
|
||||
Ramdisk::Mkdir,
|
||||
nullptr,
|
||||
Ramdisk::Rename,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -47,6 +47,36 @@ namespace Fs::Ramdisk {
|
||||
return true;
|
||||
}
|
||||
|
||||
static int NormalizePath(const char* path, char* out, int outMax) {
|
||||
if (path == nullptr || out == nullptr || outMax <= 1) return -1;
|
||||
if (path[0] == '/') path++;
|
||||
|
||||
int len = 0;
|
||||
while (path[len] != '\0' && len < outMax - 1) {
|
||||
out[len] = path[len];
|
||||
len++;
|
||||
}
|
||||
out[len] = '\0';
|
||||
|
||||
while (len > 1 && out[len - 1] == '/') {
|
||||
out[--len] = '\0';
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static int FindEntryByPath(const char* path) {
|
||||
char normalized[MaxNameLen];
|
||||
int normLen = NormalizePath(path, normalized, MaxNameLen);
|
||||
if (normLen < 0) return -1;
|
||||
|
||||
for (int i = 0; i < fileCount; i++) {
|
||||
char entryPath[MaxNameLen];
|
||||
NormalizePath(fileTable[i].name, entryPath, MaxNameLen);
|
||||
if (StrEqual(entryPath, normalized)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Initialize(void* moduleData, uint64_t moduleSize) {
|
||||
Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)";
|
||||
|
||||
@@ -405,6 +435,76 @@ namespace Fs::Ramdisk {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Rename(const char* oldPath, const char* newPath) {
|
||||
char oldNorm[MaxNameLen];
|
||||
char newNorm[MaxNameLen];
|
||||
int oldLen = NormalizePath(oldPath, oldNorm, MaxNameLen);
|
||||
int newLen = NormalizePath(newPath, newNorm, MaxNameLen);
|
||||
if (oldLen <= 0 || newLen <= 0) return -1;
|
||||
|
||||
int src = FindEntryByPath(oldNorm);
|
||||
if (src < 0) return -1;
|
||||
|
||||
int dst = FindEntryByPath(newNorm);
|
||||
if (dst >= 0 && dst != src) {
|
||||
if (Delete(newNorm) < 0) return -1;
|
||||
if (dst < src) src--;
|
||||
}
|
||||
|
||||
const bool isDirectory = fileTable[src].isDirectory;
|
||||
|
||||
if (!isDirectory) {
|
||||
if (newLen >= MaxNameLen) return -1;
|
||||
for (int i = 0; i <= newLen; i++) fileTable[src].name[i] = newNorm[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
char oldPrefix[MaxNameLen];
|
||||
if (oldLen + 1 >= MaxNameLen || newLen + 1 >= MaxNameLen) return -1;
|
||||
|
||||
for (int i = 0; i < oldLen; i++) oldPrefix[i] = oldNorm[i];
|
||||
oldPrefix[oldLen] = '/';
|
||||
oldPrefix[oldLen + 1] = '\0';
|
||||
|
||||
for (int i = 0; i < fileCount; i++) {
|
||||
char normalized[MaxNameLen];
|
||||
int normalizedLen = NormalizePath(fileTable[i].name, normalized, MaxNameLen);
|
||||
if (normalizedLen < 0) return -1;
|
||||
|
||||
bool exactMatch = StrEqual(normalized, oldNorm);
|
||||
bool childMatch = StartsWith(fileTable[i].name, oldPrefix);
|
||||
if (!exactMatch && !childMatch) continue;
|
||||
|
||||
char updated[MaxNameLen];
|
||||
if (exactMatch) {
|
||||
for (int j = 0; j <= newLen; j++) updated[j] = newNorm[j];
|
||||
} else {
|
||||
int suffixLen = StrLen(fileTable[i].name) - (oldLen + 1);
|
||||
if (newLen + 1 + suffixLen >= MaxNameLen) return -1;
|
||||
|
||||
int pos = 0;
|
||||
for (int j = 0; j < newLen; j++) updated[pos++] = newNorm[j];
|
||||
updated[pos++] = '/';
|
||||
const char* suffix = fileTable[i].name + oldLen + 1;
|
||||
while (*suffix && pos < MaxNameLen - 1) updated[pos++] = *suffix++;
|
||||
updated[pos] = '\0';
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
while (updated[pos] != '\0' && pos < MaxNameLen - 1) {
|
||||
fileTable[i].name[pos] = updated[pos];
|
||||
pos++;
|
||||
}
|
||||
if (fileTable[i].isDirectory) {
|
||||
if (pos >= MaxNameLen - 1) return -1;
|
||||
fileTable[i].name[pos++] = '/';
|
||||
}
|
||||
fileTable[i].name[pos] = '\0';
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GetFileCount() {
|
||||
return fileCount;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Fs::Ramdisk {
|
||||
int ReadDir(const char* path, const char** outNames, int maxEntries);
|
||||
int Delete(const char* path);
|
||||
int Mkdir(const char* path);
|
||||
int Rename(const char* oldPath, const char* newPath);
|
||||
int GetFileCount();
|
||||
|
||||
}
|
||||
|
||||
+65
-31
@@ -553,11 +553,6 @@ namespace Net::Tcp {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Phase 1: Send data segments with interrupts disabled (lock-safe)
|
||||
uint64_t flags;
|
||||
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
|
||||
conn->Lock.Acquire();
|
||||
|
||||
constexpr uint16_t MSS = 1460;
|
||||
uint16_t sent = 0;
|
||||
|
||||
@@ -567,6 +562,21 @@ namespace Net::Tcp {
|
||||
segLen = MSS;
|
||||
}
|
||||
|
||||
uint32_t segSeq = 0;
|
||||
uint32_t expectedAck = 0;
|
||||
uint64_t flags;
|
||||
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
|
||||
conn->Lock.Acquire();
|
||||
|
||||
if (conn->CurrentState != State::Established) {
|
||||
conn->Lock.Release();
|
||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||
return sent > 0 ? sent : -1;
|
||||
}
|
||||
|
||||
segSeq = conn->SendNext;
|
||||
expectedAck = segSeq + segLen;
|
||||
|
||||
bool ok = SendSegment(conn, FLAG_ACK | FLAG_PSH, data + sent, segLen);
|
||||
if (!ok) {
|
||||
conn->Lock.Release();
|
||||
@@ -574,9 +584,8 @@ namespace Net::Tcp {
|
||||
return sent > 0 ? sent : -1;
|
||||
}
|
||||
|
||||
conn->SendNext += segLen;
|
||||
conn->SendNext = expectedAck;
|
||||
|
||||
// Store for retransmission
|
||||
if (segLen <= sizeof(conn->RetransmitBuffer)) {
|
||||
memcpy(conn->RetransmitBuffer, data + sent, segLen);
|
||||
conn->RetransmitLen = segLen;
|
||||
@@ -584,45 +593,70 @@ namespace Net::Tcp {
|
||||
conn->RetransmitCount = 0;
|
||||
}
|
||||
|
||||
sent += segLen;
|
||||
}
|
||||
|
||||
conn->Lock.Release();
|
||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||
|
||||
// Phase 2: Wait for ACK without holding the lock (interrupts enabled)
|
||||
uint64_t startTime = Timekeeping::GetMilliseconds();
|
||||
while (conn->SendUnack != conn->SendNext) {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if ((now - startTime) > (RETRANSMIT_TIMEOUT_MS * MAX_RETRANSMITS)) {
|
||||
break;
|
||||
}
|
||||
if ((now - conn->RetransmitTime) > RETRANSMIT_TIMEOUT_MS && conn->RetransmitLen > 0) {
|
||||
conn->RetransmitCount++;
|
||||
if (conn->RetransmitCount > MAX_RETRANSMITS) {
|
||||
break;
|
||||
}
|
||||
// Retransmit with brief interrupt-disabled window
|
||||
while (true) {
|
||||
asm volatile("pushfq; pop %0; cli" : "=r"(flags) :: "memory");
|
||||
conn->Lock.Acquire();
|
||||
uint32_t savedNext = conn->SendNext;
|
||||
conn->SendNext = conn->SendUnack;
|
||||
SendSegment(conn, FLAG_ACK | FLAG_PSH,
|
||||
conn->RetransmitBuffer, conn->RetransmitLen);
|
||||
conn->SendNext = savedNext;
|
||||
conn->RetransmitTime = Timekeeping::GetMilliseconds();
|
||||
|
||||
if (conn->SendUnack >= expectedAck) {
|
||||
conn->RetransmitLen = 0;
|
||||
conn->Lock.Release();
|
||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||
continue;
|
||||
sent += segLen;
|
||||
break;
|
||||
}
|
||||
|
||||
if (conn->CurrentState != State::Established) {
|
||||
conn->SendNext = conn->SendUnack;
|
||||
conn->RetransmitLen = 0;
|
||||
conn->Lock.Release();
|
||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||
return sent > 0 ? sent : -1;
|
||||
}
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
bool shouldRetransmit =
|
||||
conn->RetransmitLen == segLen &&
|
||||
(now - conn->RetransmitTime) > RETRANSMIT_TIMEOUT_MS;
|
||||
if (shouldRetransmit) {
|
||||
conn->RetransmitCount++;
|
||||
if (conn->RetransmitCount > MAX_RETRANSMITS) {
|
||||
conn->SendNext = conn->SendUnack;
|
||||
conn->RetransmitLen = 0;
|
||||
conn->Lock.Release();
|
||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||
return sent > 0 ? sent : -1;
|
||||
}
|
||||
|
||||
uint32_t savedNext = conn->SendNext;
|
||||
conn->SendNext = segSeq;
|
||||
bool retryOk = SendSegment(conn, FLAG_ACK | FLAG_PSH,
|
||||
conn->RetransmitBuffer, conn->RetransmitLen);
|
||||
conn->SendNext = savedNext;
|
||||
conn->RetransmitTime = now;
|
||||
if (!retryOk) {
|
||||
conn->SendNext = conn->SendUnack;
|
||||
conn->RetransmitLen = 0;
|
||||
conn->Lock.Release();
|
||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||
return sent > 0 ? sent : -1;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t waitMs = 10;
|
||||
if (conn->RetransmitLen > 0 && now <= conn->RetransmitTime + RETRANSMIT_TIMEOUT_MS) {
|
||||
if (conn->RetransmitLen == segLen && now <= conn->RetransmitTime + RETRANSMIT_TIMEOUT_MS) {
|
||||
waitMs = (conn->RetransmitTime + RETRANSMIT_TIMEOUT_MS) - now;
|
||||
if (waitMs == 0) waitMs = 1;
|
||||
if (waitMs > 10) waitMs = 10;
|
||||
}
|
||||
|
||||
conn->Lock.Release();
|
||||
asm volatile("push %0; popfq" :: "r"(flags) : "memory");
|
||||
Sched::BlockOnObject(conn, waitMs);
|
||||
}
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
+19
-4
@@ -59,7 +59,7 @@ BINDIR := bin
|
||||
PROGRAMS := $(notdir $(wildcard src/*))
|
||||
|
||||
# Programs with custom Makefiles (built separately).
|
||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot
|
||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||
@@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
|
||||
# Common shared assets (wallpapers, etc.)
|
||||
COMMONKEEP := $(BINDIR)/common/.keep
|
||||
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot icons fonts bearssl libc tls libjpeg libjpegwrite install-apps
|
||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl icons fonts bearssl libc tls libjpeg libjpegwrite install-apps
|
||||
|
||||
all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP)
|
||||
all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP)
|
||||
|
||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||
@@ -192,6 +192,10 @@ procmgr: libc
|
||||
calculator: libc
|
||||
$(MAKE) -C src/calculator
|
||||
|
||||
# Build printers standalone GUI tool (depends on libc, bearssl, and tls).
|
||||
printers: bearssl libc tls
|
||||
$(MAKE) -C src/printers
|
||||
|
||||
# Build login screen via its own Makefile (depends on bearssl and libc).
|
||||
login: bearssl libc
|
||||
$(MAKE) -C src/login
|
||||
@@ -249,8 +253,16 @@ tcc: libc
|
||||
lua: libc
|
||||
$(MAKE) -C src/lua
|
||||
|
||||
# Build print spooler daemon (depends on libc, tls, and bearssl).
|
||||
printd: bearssl libc tls libjpegwrite
|
||||
$(MAKE) -C src/printd
|
||||
|
||||
# Build print control utility (depends on libc, tls, and bearssl).
|
||||
printctl: bearssl libc tls
|
||||
$(MAKE) -C src/printctl
|
||||
|
||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot
|
||||
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers
|
||||
../scripts/install_apps.sh
|
||||
|
||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||
@@ -309,8 +321,11 @@ clean:
|
||||
$(MAKE) -C src/klog clean
|
||||
$(MAKE) -C src/procmgr clean
|
||||
$(MAKE) -C src/calculator clean
|
||||
$(MAKE) -C src/printers clean
|
||||
$(MAKE) -C src/paint clean
|
||||
$(MAKE) -C src/rpgdemo clean
|
||||
$(MAKE) -C src/screenshot clean
|
||||
$(MAKE) -C src/mandelbrot clean
|
||||
$(MAKE) -C src/tcc clean
|
||||
$(MAKE) -C src/printd clean
|
||||
$(MAKE) -C src/printctl clean
|
||||
|
||||
@@ -27,7 +27,64 @@ struct Response {
|
||||
int raw_len;
|
||||
};
|
||||
|
||||
inline const char* find_header_block_end(const char* start, const char* end) {
|
||||
if (!start || !end || start >= end) return nullptr;
|
||||
|
||||
for (const char* s = start; s < end - 3; s++) {
|
||||
if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n')
|
||||
return s + 4;
|
||||
}
|
||||
for (const char* s = start; s < end - 1; s++) {
|
||||
if (s[0] == '\n' && s[1] == '\n')
|
||||
return s + 2;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline int parse_status_code(const char* start, const char* end) {
|
||||
if (!start || !end || end - start < 12) return -1;
|
||||
|
||||
const char* p = start;
|
||||
while (p < end && *p && *p != ' ') p++;
|
||||
if (p >= end || *p != ' ') return -1;
|
||||
p++;
|
||||
|
||||
int code = 0;
|
||||
int digits = 0;
|
||||
while (digits < 3 && p < end && *p >= '0' && *p <= '9') {
|
||||
code = code * 10 + (*p - '0');
|
||||
p++;
|
||||
digits++;
|
||||
}
|
||||
return digits == 3 ? code : -1;
|
||||
}
|
||||
|
||||
inline const char* find_final_response_start(const char* buf, int len) {
|
||||
if (!buf || len <= 0) return nullptr;
|
||||
|
||||
const char* start = buf;
|
||||
const char* end = buf + len;
|
||||
for (;;) {
|
||||
int code = parse_status_code(start, end);
|
||||
if (code < 0) return nullptr;
|
||||
if (code < 100 || code >= 200) return start;
|
||||
|
||||
const char* next = find_header_block_end(start, end);
|
||||
if (!next || next >= end) return nullptr;
|
||||
if (end - next < 5) return nullptr;
|
||||
if (!(next[0] == 'H' && next[1] == 'T' && next[2] == 'T' && next[3] == 'P' && next[4] == '/'))
|
||||
return nullptr;
|
||||
start = next;
|
||||
}
|
||||
}
|
||||
|
||||
inline const char* skip_informational_responses(const char* buf, int len) {
|
||||
const char* start = find_final_response_start(buf, len);
|
||||
return start ? start : buf;
|
||||
}
|
||||
|
||||
// Parse raw HTTP response in-place. Sets pointers into buf (does not copy).
|
||||
// Skips leading informational 1xx responses such as "100 Continue".
|
||||
// Returns status code, or -1 if unparseable.
|
||||
inline int parse_response(char* buf, int len, Response* out) {
|
||||
out->raw = buf;
|
||||
@@ -38,21 +95,18 @@ inline int parse_response(char* buf, int len, Response* out) {
|
||||
out->body = nullptr;
|
||||
out->body_len = 0;
|
||||
|
||||
if (len < 12) return -1; // "HTTP/1.x NNN"
|
||||
const char* start = find_final_response_start(buf, len);
|
||||
const char* end = buf + len;
|
||||
if (!start || end - start < 12) return -1; // "HTTP/1.x NNN"
|
||||
|
||||
// Parse status code from "HTTP/1.x NNN"
|
||||
const char* p = buf;
|
||||
while (*p && *p != ' ') p++;
|
||||
if (*p != ' ') return -1;
|
||||
p++;
|
||||
int code = 0;
|
||||
for (int i = 0; i < 3 && *p >= '0' && *p <= '9'; i++, p++)
|
||||
code = code * 10 + (*p - '0');
|
||||
int code = parse_status_code(start, end);
|
||||
if (code < 0) return -1;
|
||||
out->status = code;
|
||||
|
||||
// Headers start after the status line
|
||||
const char* hdr_start = buf;
|
||||
while (hdr_start < buf + len - 1) {
|
||||
const char* hdr_start = start;
|
||||
while (hdr_start < end - 1) {
|
||||
if (*hdr_start == '\r' && *(hdr_start + 1) == '\n') { hdr_start += 2; break; }
|
||||
if (*hdr_start == '\n') { hdr_start++; break; }
|
||||
hdr_start++;
|
||||
@@ -60,14 +114,21 @@ inline int parse_response(char* buf, int len, Response* out) {
|
||||
out->headers = hdr_start;
|
||||
|
||||
// Find \r\n\r\n boundary between headers and body
|
||||
for (const char* s = hdr_start; s < buf + len - 3; s++) {
|
||||
if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n') {
|
||||
out->headers_len = (int)(s - hdr_start);
|
||||
out->body = s + 4;
|
||||
const char* body = find_header_block_end(hdr_start, end);
|
||||
if (body) {
|
||||
if (body >= hdr_start + 4 &&
|
||||
body[-4] == '\r' && body[-3] == '\n' && body[-2] == '\r' && body[-1] == '\n')
|
||||
out->headers_len = (int)((body - 4) - hdr_start);
|
||||
else if (body >= hdr_start + 2 &&
|
||||
body[-2] == '\n' && body[-1] == '\n')
|
||||
out->headers_len = (int)((body - 2) - hdr_start);
|
||||
else
|
||||
out->headers_len = (int)(body - hdr_start);
|
||||
out->body = body;
|
||||
out->body_len = len - (int)(out->body - buf);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
// No body separator found — entire remainder is headers
|
||||
out->headers_len = len - (int)(hdr_start - buf);
|
||||
return code;
|
||||
|
||||
@@ -155,6 +155,9 @@ namespace montauk {
|
||||
inline int fmkdir(const char* path) {
|
||||
return (int)syscall1(Montauk::SYS_FMKDIR, (uint64_t)path);
|
||||
}
|
||||
inline int frename(const char* oldPath, const char* newPath) {
|
||||
return (int)syscall2(Montauk::SYS_FRENAME, (uint64_t)oldPath, (uint64_t)newPath);
|
||||
}
|
||||
inline int drivelist(int* outDrives, int max) {
|
||||
return (int)syscall2(Montauk::SYS_DRIVELIST, (uint64_t)outDrives, (uint64_t)max);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
.TH PRINTCTL 1
|
||||
.SH NAME
|
||||
printctl \- configure printers and submit print jobs
|
||||
.SH SYNOPSIS
|
||||
.B printctl
|
||||
.I command
|
||||
.RI [ options ]
|
||||
.SH DESCRIPTION
|
||||
.B printctl
|
||||
manages the MontaukOS userspace print spooler and submits print jobs to IPP printers.
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
.B set-printer \fIURI\fR
|
||||
Store the default printer URI.
|
||||
.TP
|
||||
.B show-printer
|
||||
Print the configured default printer URI.
|
||||
.TP
|
||||
.B print \fIFILE\fR [\-\-printer \fIURI\fR] [\-\-name \fIJOB\fR] [\-\-wait]
|
||||
Queue a file for printing.
|
||||
.TP
|
||||
.B test-page [\-\-printer \fIURI\fR] [\-\-wait] [\-\-no-wait]
|
||||
Generate and queue a simple test page.
|
||||
.TP
|
||||
.B status [\-\-verbose]
|
||||
Show daemon state and queued, active, completed, and failed jobs.
|
||||
.TP
|
||||
.B inspect \fIJOB-ID\fR
|
||||
Show full metadata and debug details for a queued, active, completed, or failed job.
|
||||
.TP
|
||||
.B probe [\fIURI\fR]
|
||||
Probe the configured printer, print host and resolution details, and show IPP capability diagnostics.
|
||||
@@ -0,0 +1,12 @@
|
||||
.TH PRINTD 1
|
||||
.SH NAME
|
||||
printd \- MontaukOS userspace print spooler daemon
|
||||
.SH SYNOPSIS
|
||||
.B printd
|
||||
.SH DESCRIPTION
|
||||
.B printd
|
||||
monitors the print spool directories, claims queued jobs, and delivers them to IPP printers.
|
||||
|
||||
It is normally launched automatically by
|
||||
.BR init (1)
|
||||
and does not require direct user interaction.
|
||||
@@ -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>
|
||||
@@ -34,6 +34,7 @@ APPS=(
|
||||
"klog|apps/scalable/utilities-terminal.svg"
|
||||
"procmgr|apps/scalable/system-monitor.svg"
|
||||
"calculator|apps/scalable/accessories-calculator.svg"
|
||||
"printers|devices/scalable/preferences-devices-printer.svg"
|
||||
"rpgdemo|apps/scalable/utilities-terminal.svg"
|
||||
"paint|apps/scalable/kolourpaint.svg"
|
||||
"screenshot|apps/scalable/gnome-screenshot.svg"
|
||||
|
||||
Reference in New Issue
Block a user