From fdc72f98a1791c9012847bfa3c38ef42ec5b8d45 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 26 Mar 2026 20:13:26 -0500 Subject: [PATCH] Resource support! Apps now show as icons in the Program Manager. --- Makefile | 10 +- apps/Makefile | 16 +- apps/clock/clock.res | 5 + apps/clock/icon32.bmp | 3 + apps/cpanel/cpanel.res | 5 + apps/cpanel/icon32.bmp | 3 + apps/dvxdemo/dvxdemo.res | 5 + apps/dvxdemo/icon32.bmp | 3 + apps/imgview/icon32.bmp | 3 + apps/imgview/imgview.res | 5 + apps/notepad/icon32.bmp | 3 + apps/notepad/notepad.res | 5 + apps/progman/progman.c | 99 ++++- core/Makefile | 2 +- core/dvxApp.c | 66 ++++ core/dvxApp.h | 4 + core/dvxResource.c | 140 +++++++ core/dvxResource.h | 73 ++++ core/widgetLayout.c | 28 +- shell/shellMain.c | 10 +- tools/Makefile | 22 ++ tools/dvxres.c | 814 +++++++++++++++++++++++++++++++++++++++ tools/mkicon.c | 271 +++++++++++++ 23 files changed, 1564 insertions(+), 31 deletions(-) create mode 100644 apps/clock/clock.res create mode 100644 apps/clock/icon32.bmp create mode 100644 apps/cpanel/cpanel.res create mode 100644 apps/cpanel/icon32.bmp create mode 100644 apps/dvxdemo/dvxdemo.res create mode 100644 apps/dvxdemo/icon32.bmp create mode 100644 apps/imgview/icon32.bmp create mode 100644 apps/imgview/imgview.res create mode 100644 apps/notepad/icon32.bmp create mode 100644 apps/notepad/notepad.res create mode 100644 core/dvxResource.c create mode 100644 core/dvxResource.h create mode 100644 tools/Makefile create mode 100644 tools/dvxres.c create mode 100644 tools/mkicon.c diff --git a/Makefile b/Makefile index 3533398..6e1ee64 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,9 @@ # Builds the full DVX stack: core library, task switcher, # bootstrap loader, text help library, widgets, shell, and apps. -.PHONY: all clean core tasks loader texthelp listhelp widgets shell taskmgr apps +.PHONY: all clean core tasks loader texthelp listhelp widgets shell taskmgr apps tools -all: core tasks loader texthelp listhelp widgets shell taskmgr apps +all: core tasks loader texthelp listhelp widgets shell taskmgr apps tools core: $(MAKE) -C core @@ -31,7 +31,10 @@ shell: core tasks taskmgr: shell $(MAKE) -C taskmgr -apps: core tasks shell +tools: + $(MAKE) -C tools + +apps: core tasks shell tools $(MAKE) -C apps clean: @@ -44,6 +47,7 @@ clean: $(MAKE) -C shell clean $(MAKE) -C taskmgr clean $(MAKE) -C apps clean + $(MAKE) -C tools clean -rmdir obj 2>/dev/null -rm -rf bin/config bin/widgets bin/libs -rmdir bin/apps/cpanel bin/apps/imgview bin/apps/progman bin/apps/notepad bin/apps/clock bin/apps/dvxdemo bin/apps bin 2>/dev/null diff --git a/apps/Makefile b/apps/Makefile index e5a121f..28185e2 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -8,6 +8,7 @@ CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/pl OBJDIR = ../obj/apps BINDIR = ../bin/apps +DVXRES = ../bin/dvxres # App definitions: each is a subdir with a single .c file APPS = progman notepad clock dvxdemo cpanel imgview @@ -23,25 +24,30 @@ notepad: $(BINDIR)/notepad/notepad.app clock: $(BINDIR)/clock/clock.app dvxdemo: $(BINDIR)/dvxdemo/dvxdemo.app -$(BINDIR)/cpanel/cpanel.app: $(OBJDIR)/cpanel.o | $(BINDIR)/cpanel +$(BINDIR)/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/cpanel $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DVXRES) build $@ cpanel/cpanel.res -$(BINDIR)/imgview/imgview.app: $(OBJDIR)/imgview.o | $(BINDIR)/imgview +$(BINDIR)/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/imgview $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DVXRES) build $@ imgview/imgview.res $(BINDIR)/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/progman $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< -$(BINDIR)/notepad/notepad.app: $(OBJDIR)/notepad.o | $(BINDIR)/notepad +$(BINDIR)/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/notepad $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DVXRES) build $@ notepad/notepad.res -$(BINDIR)/clock/clock.app: $(OBJDIR)/clock.o | $(BINDIR)/clock +$(BINDIR)/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/clock $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -E _appShutdown -U $< + $(DVXRES) build $@ clock/clock.res DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp -$(BINDIR)/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) | $(BINDIR)/dvxdemo +$(BINDIR)/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/dvxdemo $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DVXRES) build $@ dvxdemo/dvxdemo.res cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/dvxdemo/ $(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR) diff --git a/apps/clock/clock.res b/apps/clock/clock.res new file mode 100644 index 0000000..58bc945 --- /dev/null +++ b/apps/clock/clock.res @@ -0,0 +1,5 @@ +# clock.res -- Resource manifest for Clock +icon32 icon clock/icon32.bmp +name text "Clock" +author text "DVX Project" +description text "Digital clock with date display" diff --git a/apps/clock/icon32.bmp b/apps/clock/icon32.bmp new file mode 100644 index 0000000..7fe939d --- /dev/null +++ b/apps/clock/icon32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3358a4f7b545b0eb14a8c3371a5d2777fc4ad59b0b7a0956e0376364c1b20d23 +size 3126 diff --git a/apps/cpanel/cpanel.res b/apps/cpanel/cpanel.res new file mode 100644 index 0000000..7eac7ee --- /dev/null +++ b/apps/cpanel/cpanel.res @@ -0,0 +1,5 @@ +# cpanel.res -- Resource manifest for Control Panel +icon32 icon cpanel/icon32.bmp +name text "Control Panel" +author text "DVX Project" +description text "System settings and preferences" diff --git a/apps/cpanel/icon32.bmp b/apps/cpanel/icon32.bmp new file mode 100644 index 0000000..f0682ac --- /dev/null +++ b/apps/cpanel/icon32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c3d28e533dac0994999fae3e053a28b89fbfd0152ecc152a76be4863316fad7 +size 3126 diff --git a/apps/dvxdemo/dvxdemo.res b/apps/dvxdemo/dvxdemo.res new file mode 100644 index 0000000..950dcdf --- /dev/null +++ b/apps/dvxdemo/dvxdemo.res @@ -0,0 +1,5 @@ +# dvxdemo.res -- Resource manifest for DVX Demo +icon32 icon dvxdemo/icon32.bmp +name text "DVX Demo" +author text "DVX Project" +description text "Widget toolkit demonstration" diff --git a/apps/dvxdemo/icon32.bmp b/apps/dvxdemo/icon32.bmp new file mode 100644 index 0000000..9d09bda --- /dev/null +++ b/apps/dvxdemo/icon32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2e69cba0284673922b99674345bf9534ad2b2d31655be7631a6db8b596d43f1 +size 3126 diff --git a/apps/imgview/icon32.bmp b/apps/imgview/icon32.bmp new file mode 100644 index 0000000..fec9d13 --- /dev/null +++ b/apps/imgview/icon32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acab450e62fe1d807f507f777dcaaaf0b13f82cda61235e6bbb5744c51f0fbe8 +size 3126 diff --git a/apps/imgview/imgview.res b/apps/imgview/imgview.res new file mode 100644 index 0000000..8df2a48 --- /dev/null +++ b/apps/imgview/imgview.res @@ -0,0 +1,5 @@ +# imgview.res -- Resource manifest for Image Viewer +icon32 icon imgview/icon32.bmp +name text "Image Viewer" +author text "DVX Project" +description text "BMP, PNG, JPEG, and GIF viewer" diff --git a/apps/notepad/icon32.bmp b/apps/notepad/icon32.bmp new file mode 100644 index 0000000..184ce88 --- /dev/null +++ b/apps/notepad/icon32.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1090f31bd32600092c514d1c0449e3ce28ce98dff3a0222071046fcf007b62 +size 3126 diff --git a/apps/notepad/notepad.res b/apps/notepad/notepad.res new file mode 100644 index 0000000..83eab69 --- /dev/null +++ b/apps/notepad/notepad.res @@ -0,0 +1,5 @@ +# notepad.res -- Resource manifest for Notepad +icon32 icon notepad/icon32.bmp +name text "Notepad" +author text "DVX Project" +description text "Simple text editor" diff --git a/apps/progman/progman.c b/apps/progman/progman.c index b725897..da71646 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -31,6 +31,8 @@ #include "dvxPlatform.h" #include "shellApp.h" #include "shellInfo.h" +#include "dvxResource.h" +#include "widgetImageButton.h" #include #include @@ -50,9 +52,12 @@ // DOS 8.3 paths are short, but long names under DJGPP can reach ~260 #define MAX_PATH_LEN 260 // Grid layout for app buttons: 4 columns, rows created dynamically +#define PM_TOOLTIP_LEN 128 #define PM_GRID_COLS 4 #define PM_BTN_W 100 #define PM_BTN_H 24 +#define PM_CELL_W 80 +#define PM_CELL_H 64 #define PM_WIN_W 440 #define PM_WIN_H 340 #define PM_GRID_SPACING 8 @@ -77,8 +82,13 @@ // Each discovered .app file in the apps/ directory tree typedef struct { - char name[SHELL_APP_NAME_MAX]; // display name (filename without .app) - char path[MAX_PATH_LEN]; // full path + char name[SHELL_APP_NAME_MAX]; // short display name (from "name" resource or filename) + char tooltip[PM_TOOLTIP_LEN]; // description for tooltip (from "description" resource) + char path[MAX_PATH_LEN]; // full path + uint8_t *iconData; // 32x32 icon in display format (NULL if none) + int32_t iconW; + int32_t iconH; + int32_t iconPitch; } AppEntryT; // Module-level statics (s prefix). DXE apps use file-scoped statics because @@ -182,23 +192,53 @@ static void buildPmWindow(void) { if (sAppCount == 0) { wgtLabel(appFrame, "(No applications found in apps/ directory)"); } else { - // Build rows of buttons. Each row is an HBox holding PM_GRID_COLS - // buttons. userData points back to the AppEntryT so the click - // callback knows which app to launch. + // Build grid of app icons. Each cell is a VBox with an image + // button (or text button if no icon) and a label underneath. WidgetT *hbox = NULL; for (int32_t i = 0; i < sAppCount; i++) { if (i % PM_GRID_COLS == 0) { hbox = wgtHBox(appFrame); hbox->spacing = wgtPixels(PM_GRID_SPACING); + hbox->align = AlignCenterE; } - WidgetT *btn = wgtButton(hbox, sAppFiles[i].name); - btn->prefW = wgtPixels(PM_BTN_W); - btn->prefH = wgtPixels(PM_BTN_H); - btn->userData = &sAppFiles[i]; + WidgetT *cell = wgtVBox(hbox); + cell->align = AlignCenterE; + cell->minW = wgtPixels(PM_CELL_W); + cell->minH = wgtPixels(PM_CELL_H); + + WidgetT *btn = NULL; + + if (sAppFiles[i].iconData) { + int32_t dataSize = sAppFiles[i].iconPitch * sAppFiles[i].iconH; + uint8_t *iconCopy = (uint8_t *)malloc(dataSize); + + if (iconCopy) { + memcpy(iconCopy, sAppFiles[i].iconData, dataSize); + btn = wgtImageButton(cell, iconCopy, sAppFiles[i].iconW, sAppFiles[i].iconH, sAppFiles[i].iconPitch); + btn->maxW = wgtPixels(sAppFiles[i].iconW + 4); + btn->maxH = wgtPixels(sAppFiles[i].iconH + 4); + } + } + + if (!btn) { + btn = wgtButton(cell, sAppFiles[i].name); + btn->prefW = wgtPixels(PM_BTN_W); + btn->prefH = wgtPixels(PM_BTN_H); + } + + btn->userData = &sAppFiles[i]; btn->onDblClick = onAppButtonClick; btn->onClick = onAppButtonClick; + + if (sAppFiles[i].tooltip[0]) { + wgtSetTooltip(btn, sAppFiles[i].tooltip); + } + + WidgetT *label = wgtLabel(cell, sAppFiles[i].name); + label->maxW = wgtPixels(PM_CELL_W); + wgtLabelSetAlign(label, AlignCenterE); } } @@ -386,7 +426,11 @@ static void scanAppsDirRecurse(const char *dirPath) { AppEntryT *entry = &sAppFiles[sAppCount]; - // Name = filename without extension + snprintf(entry->path, sizeof(entry->path), "%s", fullPath); + entry->iconData = NULL; + entry->tooltip[0] = '\0'; + + // Default name from filename (without .app extension) int32_t nameLen = len - 4; if (nameLen >= SHELL_APP_NAME_MAX) { @@ -396,12 +440,43 @@ static void scanAppsDirRecurse(const char *dirPath) { memcpy(entry->name, ent->d_name, nameLen); entry->name[nameLen] = '\0'; - // Capitalize first letter for display if (entry->name[0] >= 'a' && entry->name[0] <= 'z') { entry->name[0] -= 32; } - snprintf(entry->path, sizeof(entry->path), "%s", fullPath); + // Override from embedded resources if available + DvxResHandleT *res = dvxResOpen(fullPath); + + if (res) { + uint32_t iconSize = 0; + void *iconBuf = dvxResRead(res, "icon32", &iconSize); + + if (iconBuf) { + entry->iconData = dvxLoadImageFromMemory(sAc, (const uint8_t *)iconBuf, (int32_t)iconSize, &entry->iconW, &entry->iconH, &entry->iconPitch); + free(iconBuf); + } + + uint32_t nameSize = 0; + void *nameBuf = dvxResRead(res, "name", &nameSize); + + if (nameBuf) { + strncpy(entry->name, (const char *)nameBuf, SHELL_APP_NAME_MAX - 1); + entry->name[SHELL_APP_NAME_MAX - 1] = '\0'; + free(nameBuf); + } + + uint32_t descSize = 0; + void *descBuf = dvxResRead(res, "description", &descSize); + + if (descBuf) { + strncpy(entry->tooltip, (const char *)descBuf, sizeof(entry->tooltip) - 1); + entry->tooltip[sizeof(entry->tooltip) - 1] = '\0'; + free(descBuf); + } + + dvxResClose(res); + } + sAppCount++; } diff --git a/core/Makefile b/core/Makefile index f6cd6f4..521b1a7 100644 --- a/core/Makefile +++ b/core/Makefile @@ -14,7 +14,7 @@ LIBSDIR = ../bin/libs # Core sources SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c \ - dvxApp.c dvxDialog.c dvxPrefs.c \ + dvxApp.c dvxDialog.c dvxPrefs.c dvxResource.c \ widgetClass.c widgetCore.c widgetScrollbar.c \ widgetLayout.c widgetEvent.c widgetOps.c diff --git a/core/dvxApp.c b/core/dvxApp.c index 1853cc6..960a241 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -2529,6 +2529,72 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i } +// ============================================================ +// dvxLoadImageFromMemory +// ============================================================ +// +// Same as dvxLoadImage but loads from a memory buffer (e.g. a +// resource read via dvxResRead). Supports BMP, PNG, JPEG, GIF. + +uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch) { + if (!ctx || !data || dataLen <= 0) { + return NULL; + } + + const DisplayT *d = &ctx->display; + + int imgW; + int imgH; + int channels; + uint8_t *rgb = stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, 3); + + if (!rgb) { + return NULL; + } + + int32_t bpp = d->format.bytesPerPixel; + int32_t pitch = imgW * bpp; + uint8_t *buf = (uint8_t *)malloc(pitch * imgH); + + if (!buf) { + stbi_image_free(rgb); + return NULL; + } + + for (int32_t y = 0; y < imgH; y++) { + for (int32_t x = 0; x < imgW; x++) { + const uint8_t *src = rgb + (y * imgW + x) * 3; + uint32_t color = packColor(d, src[0], src[1], src[2]); + uint8_t *dst = buf + y * pitch + x * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } + + stbi_image_free(rgb); + + if (outW) { + *outW = imgW; + } + + if (outH) { + *outH = imgH; + } + + if (outPitch) { + *outPitch = pitch; + } + + return buf; +} + + // ============================================================ // dvxInvalidateRect // ============================================================ diff --git a/core/dvxApp.h b/core/dvxApp.h index de770de..5f7b768 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -289,6 +289,10 @@ void dvxTileWindowsV(AppContextT *ctx); // image dimensions and row pitch in bytes. uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch); +// Load an image from a memory buffer (e.g. resource data). Same output +// format as dvxLoadImage. Caller must free the result with dvxFreeImage. +uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH, int32_t *outPitch); + // Free a pixel buffer returned by dvxLoadImage. void dvxFreeImage(uint8_t *data); diff --git a/core/dvxResource.c b/core/dvxResource.c new file mode 100644 index 0000000..486d496 --- /dev/null +++ b/core/dvxResource.c @@ -0,0 +1,140 @@ +// dvxResource.c -- DVX resource runtime API +// +// Reads the resource block appended to DXE3 files. The resource +// block is located by reading the footer at the end of the file. + +#include "dvxResource.h" + +#include +#include +#include + + +DvxResHandleT *dvxResOpen(const char *path) { + if (!path) { + return NULL; + } + + FILE *f = fopen(path, "rb"); + + if (!f) { + return NULL; + } + + // Read footer from end of file + if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0) { + fclose(f); + return NULL; + } + + DvxResFooterT footer; + + if (fread(&footer, sizeof(footer), 1, f) != 1) { + fclose(f); + return NULL; + } + + if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) { + fclose(f); + return NULL; + } + + // Read directory + if (fseek(f, footer.dirOffset, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + + DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT)); + + if (!entries) { + fclose(f); + return NULL; + } + + if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) { + free(entries); + fclose(f); + return NULL; + } + + fclose(f); + + DvxResHandleT *h = (DvxResHandleT *)malloc(sizeof(DvxResHandleT)); + + if (!h) { + free(entries); + return NULL; + } + + strncpy(h->path, path, sizeof(h->path) - 1); + h->path[sizeof(h->path) - 1] = '\0'; + h->entries = entries; + h->entryCount = footer.entryCount; + + return h; +} + + +const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) { + if (!h || !name) { + return NULL; + } + + for (uint32_t i = 0; i < h->entryCount; i++) { + if (strcmp(h->entries[i].name, name) == 0) { + return &h->entries[i]; + } + } + + return NULL; +} + + +void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize) { + const DvxResDirEntryT *entry = dvxResFind(h, name); + + if (!entry) { + return NULL; + } + + FILE *f = fopen(h->path, "rb"); + + if (!f) { + return NULL; + } + + if (fseek(f, entry->offset, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + + void *buf = malloc(entry->size); + + if (!buf) { + fclose(f); + return NULL; + } + + if (fread(buf, 1, entry->size, f) != entry->size) { + free(buf); + fclose(f); + return NULL; + } + + fclose(f); + + if (outSize) { + *outSize = entry->size; + } + + return buf; +} + + +void dvxResClose(DvxResHandleT *h) { + if (h) { + free(h->entries); + free(h); + } +} diff --git a/core/dvxResource.h b/core/dvxResource.h new file mode 100644 index 0000000..3ab7735 --- /dev/null +++ b/core/dvxResource.h @@ -0,0 +1,73 @@ +// dvxResource.h -- DVX resource format and runtime API +// +// Resources are appended to DXE3 files (.app, .wgt, .lib) after the +// normal DXE content. dlopen never reads past the DXE header-specified +// sections, so the appended data is invisible to the loader. +// +// File layout: +// [DXE3 content] -- untouched, loaded by dlopen +// [resource data entries] -- sequential, variable length +// [resource directory] -- fixed-size entries, one per resource +// [footer] -- magic + directory offset + count +// +// Reading starts from the end: seek to EOF - sizeof(footer), verify +// the magic, then seek to the directory and read entries. + +#ifndef DVX_RESOURCE_H +#define DVX_RESOURCE_H + +#include + +// Resource type IDs +#define DVX_RES_ICON 1 // image data (BMP icon: 16x16, 32x32, etc.) +#define DVX_RES_TEXT 2 // null-terminated string (author, copyright, etc.) +#define DVX_RES_BINARY 3 // arbitrary binary data (app-specific) + +// Footer magic: "DVXR" as a 32-bit little-endian value +#define DVX_RES_MAGIC 0x52585644UL + +// Maximum resource name length (including null terminator) +#define DVX_RES_NAME_MAX 32 + +// Directory entry (48 bytes each) +typedef struct { + char name[DVX_RES_NAME_MAX]; // resource name (e.g. "icon16", "author") + uint32_t type; // DVX_RES_ICON, DVX_RES_TEXT, DVX_RES_BINARY + uint32_t offset; // absolute file offset of data + uint32_t size; // data size in bytes + uint32_t reserved; // padding for future use +} DvxResDirEntryT; + +// Footer (16 bytes, at the very end of the file) +typedef struct { + uint32_t magic; // DVX_RES_MAGIC + uint32_t dirOffset; // absolute file offset of directory + uint32_t entryCount; // number of resources + uint32_t reserved; +} DvxResFooterT; + +// Runtime resource handle (opaque to callers) +typedef struct { + char path[260]; + DvxResDirEntryT *entries; + uint32_t entryCount; +} DvxResHandleT; + +// Open a resource handle by reading the footer and directory. +// Returns NULL if the file has no resource block. +DvxResHandleT *dvxResOpen(const char *path); + +// Find a resource by name and read its data into a malloc'd buffer. +// Sets *outSize to the data size. Returns NULL if not found. +// Caller must free the returned buffer. +void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize); + +// Find a resource by name and return its directory entry. +// Returns NULL if not found. The returned pointer is valid until +// dvxResClose is called. +const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name); + +// Close the handle and free all associated memory. +void dvxResClose(DvxResHandleT *h); + +#endif // DVX_RESOURCE_H diff --git a/core/widgetLayout.c b/core/widgetLayout.c index 5ab9a24..883427a 100644 --- a/core/widgetLayout.c +++ b/core/widgetLayout.c @@ -290,15 +290,35 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) { } // Assign geometry + int32_t crossSize = availCross; + + // Apply max size on cross axis + if (horiz && c->maxH) { + int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth); + + if (crossSize > maxPx) { + crossSize = maxPx; + } + } else if (!horiz && c->maxW) { + int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth); + + if (crossSize > maxPx) { + crossSize = maxPx; + } + } + + // Center on cross axis when child is constrained smaller + int32_t crossOff = (crossSize < availCross) ? (availCross - crossSize) / 2 : 0; + if (horiz) { c->x = pos; - c->y = innerY; + c->y = innerY + crossOff; c->w = mainSize; - c->h = availCross; + c->h = crossSize; } else { - c->x = innerX; + c->x = innerX + crossOff; c->y = pos; - c->w = availCross; + c->w = crossSize; c->h = mainSize; } diff --git a/shell/shellMain.c b/shell/shellMain.c index f9f240c..65e9a7b 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -276,6 +276,10 @@ int shellMain(int argc, char *argv[]) { sCtx.onTitleChange = titleChangeHandler; sCtx.titleChangeCtx = &sCtx; + // Install crash handler before loading apps so faults during app + // initialization are caught and recovered from gracefully. + platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog); + // Load the desktop app (configurable via [shell] desktop= in dvx.ini) const char *desktopApp = prefsGetString("shell", "desktop", SHELL_DESKTOP_APP); int32_t desktopId = shellLoadApp(&sCtx, desktopApp); @@ -287,12 +291,6 @@ int shellMain(int argc, char *argv[]) { return 1; } - // Install crash handler after everything is initialized -- if - // initialization itself crashes, we want the default behavior - // (abort with register dump) rather than our recovery path, because - // the system isn't in a recoverable state yet. - platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog); - dvxLog("DVX Shell ready."); // Set recovery point for crash handler. setjmp returns 0 on initial diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000..bbc2e96 --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,22 @@ +# DVX Tools Makefile +# +# Builds native (host) utilities. These run on the development machine +# (Linux or DOS), not on the target. No cross-compilation needed. + +CC = gcc +CFLAGS = -O2 -Wall -Wextra -I../core + +BINDIR = ../bin + +.PHONY: all clean + +all: $(BINDIR)/dvxres + +$(BINDIR)/dvxres: dvxres.c ../core/dvxResource.c ../core/dvxResource.h | $(BINDIR) + $(CC) $(CFLAGS) -o $@ dvxres.c ../core/dvxResource.c + +$(BINDIR): + mkdir -p $(BINDIR) + +clean: + rm -f $(BINDIR)/dvxres diff --git a/tools/dvxres.c b/tools/dvxres.c new file mode 100644 index 0000000..feaf74a --- /dev/null +++ b/tools/dvxres.c @@ -0,0 +1,814 @@ +// dvxres.c -- DVX resource tool +// +// Manages resource blocks appended to DXE3 files (.app, .wgt, .lib). +// Builds on both DOS (DJGPP) and Linux (GCC) with no dependencies +// beyond standard C. +// +// Commands: +// dvxres add +// dvxres build +// dvxres list +// dvxres get [outfile] +// dvxres strip +// +// The resource block is appended after the DXE3 content: +// [resource data entries] +// [resource directory] +// [footer with magic + directory offset + count] + +#include "../core/dvxResource.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, const char *dataArg); +static int cmdBuild(const char *dxePath, const char *manifestPath); +static int cmdGet(const char *dxePath, const char *name, const char *outPath); +static int cmdList(const char *dxePath); +static int cmdStrip(const char *dxePath); +static long dxeContentSize(const char *path); +static int parseType(const char *typeStr); +static int readExistingResources(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData); +static void usage(void); + + +// ============================================================ +// dxeContentSize +// ============================================================ +// +// Returns the size of the DXE3 content (before any appended resources). +// If the file has a resource footer, returns the start of the resource +// data. Otherwise returns the full file size. + +static long dxeContentSize(const char *path) { + FILE *f = fopen(path, "rb"); + + if (!f) { + return -1; + } + + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + + if (fileSize < (long)sizeof(DvxResFooterT)) { + fclose(f); + return fileSize; + } + + // Check for resource footer + fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); + DvxResFooterT footer; + + if (fread(&footer, sizeof(footer), 1, f) == 1 && footer.magic == DVX_RES_MAGIC) { + // Find the earliest resource data offset + fseek(f, footer.dirOffset, SEEK_SET); + DvxResDirEntryT entry; + long earliest = footer.dirOffset; + + for (uint32_t i = 0; i < footer.entryCount; i++) { + if (fread(&entry, sizeof(entry), 1, f) == 1) { + if ((long)entry.offset < earliest) { + earliest = (long)entry.offset; + } + } + } + + fclose(f); + return earliest; + } + + fclose(f); + return fileSize; +} + + +// ============================================================ +// parseType +// ============================================================ + +static int parseType(const char *typeStr) { + if (strcmp(typeStr, "icon") == 0 || strcmp(typeStr, "image") == 0) { + return DVX_RES_ICON; + } + + if (strcmp(typeStr, "text") == 0) { + return DVX_RES_TEXT; + } + + if (strcmp(typeStr, "binary") == 0) { + return DVX_RES_BINARY; + } + + return -1; +} + + +// ============================================================ +// readExistingResources +// ============================================================ +// +// Reads any existing resource block from the file. Returns arrays +// of directory entries and data blobs. Caller frees everything. + +static int readExistingResources(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData) { + *outEntries = NULL; + *outCount = 0; + *outData = NULL; + + FILE *f = fopen(path, "rb"); + + if (!f) { + return -1; + } + + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + + if (fileSize <= dxeSize) { + fclose(f); + return 0; + } + + // Check for footer + fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); + DvxResFooterT footer; + + if (fread(&footer, sizeof(footer), 1, f) != 1 || footer.magic != DVX_RES_MAGIC) { + fclose(f); + return 0; + } + + // Read directory + fseek(f, footer.dirOffset, SEEK_SET); + DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT)); + + if (!entries) { + fclose(f); + return -1; + } + + if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) { + free(entries); + fclose(f); + return -1; + } + + // Read each resource's data + uint8_t **data = (uint8_t **)malloc(footer.entryCount * sizeof(uint8_t *)); + + if (!data) { + free(entries); + fclose(f); + return -1; + } + + for (uint32_t i = 0; i < footer.entryCount; i++) { + data[i] = (uint8_t *)malloc(entries[i].size); + + if (!data[i]) { + for (uint32_t j = 0; j < i; j++) { + free(data[j]); + } + + free(data); + free(entries); + fclose(f); + return -1; + } + + fseek(f, entries[i].offset, SEEK_SET); + fread(data[i], 1, entries[i].size, f); + } + + fclose(f); + *outEntries = entries; + *outCount = footer.entryCount; + *outData = data; + return 0; +} + + +// ============================================================ +// writeResources +// ============================================================ +// +// Truncates the file to dxeSize and writes the resource block. + +static int writeResources(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data) { + // Read the DXE content first + FILE *f = fopen(path, "rb"); + + if (!f) { + return -1; + } + + uint8_t *dxeBuf = (uint8_t *)malloc(dxeSize); + + if (!dxeBuf) { + fclose(f); + return -1; + } + + if (fread(dxeBuf, 1, dxeSize, f) != (size_t)dxeSize) { + free(dxeBuf); + fclose(f); + return -1; + } + + fclose(f); + + // Rewrite file: DXE content + resources + f = fopen(path, "wb"); + + if (!f) { + free(dxeBuf); + return -1; + } + + fwrite(dxeBuf, 1, dxeSize, f); + free(dxeBuf); + + // Write resource data entries and update offsets + for (uint32_t i = 0; i < count; i++) { + entries[i].offset = (uint32_t)ftell(f); + fwrite(data[i], 1, entries[i].size, f); + } + + // Write directory + uint32_t dirOffset = (uint32_t)ftell(f); + fwrite(entries, sizeof(DvxResDirEntryT), count, f); + + // Write footer + DvxResFooterT footer; + footer.magic = DVX_RES_MAGIC; + footer.dirOffset = dirOffset; + footer.entryCount = count; + footer.reserved = 0; + fwrite(&footer, sizeof(footer), 1, f); + + fclose(f); + return 0; +} + + +// ============================================================ +// cmdAdd +// ============================================================ + +static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, const char *dataArg) { + int type = parseType(typeStr); + + if (type < 0) { + fprintf(stderr, "Unknown resource type: %s (use icon, text, or binary)\n", typeStr); + return 1; + } + + if (strlen(name) >= DVX_RES_NAME_MAX) { + fprintf(stderr, "Resource name too long (max %d chars)\n", DVX_RES_NAME_MAX - 1); + return 1; + } + + long dxeSize = dxeContentSize(dxePath); + + if (dxeSize < 0) { + fprintf(stderr, "Cannot open: %s\n", dxePath); + return 1; + } + + // Read existing resources + DvxResDirEntryT *entries = NULL; + uint32_t count = 0; + uint8_t **data = NULL; + readExistingResources(dxePath, dxeSize, &entries, &count, &data); + + // Remove existing entry with same name (replace) + for (uint32_t i = 0; i < count; i++) { + if (strcmp(entries[i].name, name) == 0) { + free(data[i]); + + for (uint32_t j = i; j < count - 1; j++) { + entries[j] = entries[j + 1]; + data[j] = data[j + 1]; + } + + count--; + break; + } + } + + // Prepare new resource data + uint8_t *newData = NULL; + uint32_t newSize = 0; + + if (type == DVX_RES_TEXT) { + // Text data from command line argument + newSize = (uint32_t)strlen(dataArg) + 1; + newData = (uint8_t *)malloc(newSize); + + if (!newData) { + fprintf(stderr, "Out of memory\n"); + return 1; + } + + memcpy(newData, dataArg, newSize); + } else { + // Binary/icon data from file + FILE *df = fopen(dataArg, "rb"); + + if (!df) { + fprintf(stderr, "Cannot open data file: %s\n", dataArg); + return 1; + } + + fseek(df, 0, SEEK_END); + newSize = (uint32_t)ftell(df); + fseek(df, 0, SEEK_SET); + + newData = (uint8_t *)malloc(newSize); + + if (!newData) { + fclose(df); + fprintf(stderr, "Out of memory\n"); + return 1; + } + + if (fread(newData, 1, newSize, df) != newSize) { + fclose(df); + free(newData); + fprintf(stderr, "Failed to read: %s\n", dataArg); + return 1; + } + + fclose(df); + } + + // Append to arrays + entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); + data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); + + memset(&entries[count], 0, sizeof(DvxResDirEntryT)); + strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1); + entries[count].type = (uint32_t)type; + entries[count].size = newSize; + data[count] = newData; + count++; + + // Write + int result = writeResources(dxePath, dxeSize, entries, count, data); + + // Cleanup + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + + if (result != 0) { + fprintf(stderr, "Failed to write resources to: %s\n", dxePath); + return 1; + } + + printf("Added '%s' (%s, %u bytes) to %s\n", name, typeStr, newSize, dxePath); + return 0; +} + + +// ============================================================ +// cmdBuild +// ============================================================ + +static int cmdBuild(const char *dxePath, const char *manifestPath) { + FILE *mf = fopen(manifestPath, "r"); + + if (!mf) { + fprintf(stderr, "Cannot open manifest: %s\n", manifestPath); + return 1; + } + + long dxeSize = dxeContentSize(dxePath); + + if (dxeSize < 0) { + fclose(mf); + fprintf(stderr, "Cannot open: %s\n", dxePath); + return 1; + } + + // Strip any existing resources first -- start fresh + DvxResDirEntryT *entries = NULL; + uint32_t count = 0; + uint8_t **data = NULL; + + char line[1024]; + int lineNum = 0; + + while (fgets(line, sizeof(line), mf)) { + lineNum++; + + // Strip trailing newline/CR + size_t len = strlen(line); + + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[--len] = '\0'; + } + + // Skip empty lines and comments + char *p = line; + + while (*p && isspace((unsigned char)*p)) { + p++; + } + + if (*p == '\0' || *p == '#') { + continue; + } + + // Parse: name type data + // name and type are whitespace-delimited + // data is either a quoted string or a filename (rest of line) + char name[DVX_RES_NAME_MAX]; + char typeStr[16]; + char dataArg[512]; + + if (sscanf(p, "%31s %15s", name, typeStr) != 2) { + fprintf(stderr, "%s:%d: expected: name type data\n", manifestPath, lineNum); + goto fail; + } + + // Advance past name and type to get data + char *dp = p; + + // Skip name + while (*dp && !isspace((unsigned char)*dp)) { + dp++; + } + + while (*dp && isspace((unsigned char)*dp)) { + dp++; + } + + // Skip type + while (*dp && !isspace((unsigned char)*dp)) { + dp++; + } + + while (*dp && isspace((unsigned char)*dp)) { + dp++; + } + + if (*dp == '\0') { + fprintf(stderr, "%s:%d: missing data argument\n", manifestPath, lineNum); + goto fail; + } + + // Handle quoted strings: strip quotes + if (*dp == '"') { + dp++; + char *end = strrchr(dp, '"'); + + if (end) { + *end = '\0'; + } + } + + strncpy(dataArg, dp, sizeof(dataArg) - 1); + dataArg[sizeof(dataArg) - 1] = '\0'; + + int type = parseType(typeStr); + + if (type < 0) { + fprintf(stderr, "%s:%d: unknown type: %s\n", manifestPath, lineNum, typeStr); + goto fail; + } + + // Prepare data + uint8_t *newData = NULL; + uint32_t newSize = 0; + + if (type == DVX_RES_TEXT) { + newSize = (uint32_t)strlen(dataArg) + 1; + newData = (uint8_t *)malloc(newSize); + + if (!newData) { + fprintf(stderr, "Out of memory\n"); + goto fail; + } + + memcpy(newData, dataArg, newSize); + } else { + FILE *df = fopen(dataArg, "rb"); + + if (!df) { + fprintf(stderr, "%s:%d: cannot open: %s\n", manifestPath, lineNum, dataArg); + goto fail; + } + + fseek(df, 0, SEEK_END); + newSize = (uint32_t)ftell(df); + fseek(df, 0, SEEK_SET); + + newData = (uint8_t *)malloc(newSize); + + if (!newData) { + fclose(df); + fprintf(stderr, "Out of memory\n"); + goto fail; + } + + if (fread(newData, 1, newSize, df) != newSize) { + fclose(df); + free(newData); + fprintf(stderr, "%s:%d: failed to read: %s\n", manifestPath, lineNum, dataArg); + goto fail; + } + + fclose(df); + } + + // Append + entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT)); + data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *)); + + memset(&entries[count], 0, sizeof(DvxResDirEntryT)); + strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1); + entries[count].type = (uint32_t)type; + entries[count].size = newSize; + data[count] = newData; + count++; + + printf(" %s (%s, %u bytes)\n", name, typeStr, newSize); + } + + fclose(mf); + + if (count == 0) { + printf("No resources in manifest.\n"); + free(entries); + free(data); + return 0; + } + + int result = writeResources(dxePath, dxeSize, entries, count, data); + + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + + if (result != 0) { + fprintf(stderr, "Failed to write resources to: %s\n", dxePath); + return 1; + } + + printf("Built %u resource(s) into %s\n", count, dxePath); + return 0; + +fail: + fclose(mf); + + for (uint32_t i = 0; i < count; i++) { + free(data[i]); + } + + free(data); + free(entries); + return 1; +} + + +// ============================================================ +// cmdList +// ============================================================ + +static int cmdList(const char *dxePath) { + FILE *f = fopen(dxePath, "rb"); + + if (!f) { + fprintf(stderr, "Cannot open: %s\n", dxePath); + return 1; + } + + fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END); + DvxResFooterT footer; + + if (fread(&footer, sizeof(footer), 1, f) != 1 || footer.magic != DVX_RES_MAGIC) { + fclose(f); + printf("No resources in %s\n", dxePath); + return 0; + } + + fseek(f, footer.dirOffset, SEEK_SET); + + printf("Resources in %s (%u entries):\n", dxePath, footer.entryCount); + printf(" %-31s %-8s %s\n", "Name", "Type", "Size"); + printf(" %-31s %-8s %s\n", "-------------------------------", "--------", "--------"); + + for (uint32_t i = 0; i < footer.entryCount; i++) { + DvxResDirEntryT entry; + + if (fread(&entry, sizeof(entry), 1, f) != 1) { + break; + } + + const char *typeStr = "unknown"; + + if (entry.type == DVX_RES_ICON) { + typeStr = "icon"; + } else if (entry.type == DVX_RES_TEXT) { + typeStr = "text"; + } else if (entry.type == DVX_RES_BINARY) { + typeStr = "binary"; + } + + printf(" %-31s %-8s %u\n", entry.name, typeStr, entry.size); + } + + fclose(f); + return 0; +} + + +// ============================================================ +// cmdGet +// ============================================================ + +static int cmdGet(const char *dxePath, const char *name, const char *outPath) { + DvxResHandleT *h = dvxResOpen(dxePath); + + if (!h) { + fprintf(stderr, "No resources in %s\n", dxePath); + return 1; + } + + uint32_t size = 0; + void *buf = dvxResRead(h, name, &size); + + dvxResClose(h); + + if (!buf) { + fprintf(stderr, "Resource not found: %s\n", name); + return 1; + } + + if (outPath) { + FILE *f = fopen(outPath, "wb"); + + if (!f) { + free(buf); + fprintf(stderr, "Cannot write: %s\n", outPath); + return 1; + } + + fwrite(buf, 1, size, f); + fclose(f); + printf("Extracted '%s' (%u bytes) to %s\n", name, size, outPath); + } else { + // Write to stdout + fwrite(buf, 1, size, stdout); + } + + free(buf); + return 0; +} + + +// ============================================================ +// cmdStrip +// ============================================================ + +static int cmdStrip(const char *dxePath) { + long dxeSize = dxeContentSize(dxePath); + + if (dxeSize < 0) { + fprintf(stderr, "Cannot open: %s\n", dxePath); + return 1; + } + + // Read file size + FILE *f = fopen(dxePath, "rb"); + + if (!f) { + return 1; + } + + fseek(f, 0, SEEK_END); + long fileSize = ftell(f); + fclose(f); + + if (fileSize == dxeSize) { + printf("No resources to strip.\n"); + return 0; + } + + // Read DXE content + f = fopen(dxePath, "rb"); + uint8_t *buf = (uint8_t *)malloc(dxeSize); + + if (!buf) { + fclose(f); + fprintf(stderr, "Out of memory\n"); + return 1; + } + + fread(buf, 1, dxeSize, f); + fclose(f); + + // Rewrite truncated + f = fopen(dxePath, "wb"); + + if (!f) { + free(buf); + return 1; + } + + fwrite(buf, 1, dxeSize, f); + fclose(f); + free(buf); + + printf("Stripped %ld bytes of resources from %s\n", fileSize - dxeSize, dxePath); + return 0; +} + + +// ============================================================ +// usage +// ============================================================ + +static void usage(void) { + fprintf(stderr, "DVX Resource Tool\n\n"); + fprintf(stderr, "Usage:\n"); + fprintf(stderr, " dvxres add \n"); + fprintf(stderr, " dvxres build \n"); + fprintf(stderr, " dvxres list \n"); + fprintf(stderr, " dvxres get [outfile]\n"); + fprintf(stderr, " dvxres strip \n\n"); + fprintf(stderr, "Types: icon (or image), text, binary\n\n"); + fprintf(stderr, "For 'add' with type 'text', is the string value.\n"); + fprintf(stderr, "For 'add' with type 'icon' or 'binary', is a file path.\n\n"); + fprintf(stderr, "Manifest format (one per line):\n"); + fprintf(stderr, " name type data\n"); + fprintf(stderr, " # lines starting with # are comments\n"); +} + + +// ============================================================ +// main +// ============================================================ + +int main(int argc, char **argv) { + if (argc < 3) { + usage(); + return 1; + } + + const char *cmd = argv[1]; + const char *file = argv[2]; + + if (strcmp(cmd, "add") == 0) { + if (argc < 6) { + fprintf(stderr, "Usage: dvxres add \n"); + return 1; + } + + return cmdAdd(file, argv[3], argv[4], argv[5]); + } + + if (strcmp(cmd, "build") == 0) { + if (argc < 4) { + fprintf(stderr, "Usage: dvxres build \n"); + return 1; + } + + return cmdBuild(file, argv[3]); + } + + if (strcmp(cmd, "list") == 0) { + return cmdList(file); + } + + if (strcmp(cmd, "get") == 0) { + if (argc < 4) { + fprintf(stderr, "Usage: dvxres get [outfile]\n"); + return 1; + } + + return cmdGet(file, argv[3], argc >= 5 ? argv[4] : NULL); + } + + if (strcmp(cmd, "strip") == 0) { + return cmdStrip(file); + } + + fprintf(stderr, "Unknown command: %s\n", cmd); + usage(); + return 1; +} diff --git a/tools/mkicon.c b/tools/mkicon.c new file mode 100644 index 0000000..311993a --- /dev/null +++ b/tools/mkicon.c @@ -0,0 +1,271 @@ +// mkicon.c -- Generate simple 32x32 BMP icons for DVX apps +// +// Usage: mkicon +// Types: clock, notepad, cpanel, dvxdemo, imgview +// +// Generates recognizable pixel-art icons at 32x32 24-bit BMP. + +#include +#include +#include + +#define W 32 +#define H 32 + +static uint8_t pixels[H][W][3]; // BGR + +static void clear(uint8_t r, uint8_t g, uint8_t b) { + for (int y = 0; y < H; y++) { + for (int x = 0; x < W; x++) { + pixels[y][x][0] = b; + pixels[y][x][1] = g; + pixels[y][x][2] = r; + } + } +} + +static void pixel(int x, int y, uint8_t r, uint8_t g, uint8_t b) { + if (x >= 0 && x < W && y >= 0 && y < H) { + pixels[y][x][0] = b; + pixels[y][x][1] = g; + pixels[y][x][2] = r; + } +} + +static void rect(int x0, int y0, int w, int h, uint8_t r, uint8_t g, uint8_t b) { + for (int y = y0; y < y0 + h && y < H; y++) { + for (int x = x0; x < x0 + w && x < W; x++) { + pixel(x, y, r, g, b); + } + } +} + +static void circle(int cx, int cy, int radius, uint8_t r, uint8_t g, uint8_t b) { + for (int y = -radius; y <= radius; y++) { + for (int x = -radius; x <= radius; x++) { + if (x * x + y * y <= radius * radius) { + pixel(cx + x, cy + y, r, g, b); + } + } + } +} + +static void hline(int x0, int y, int w, uint8_t r, uint8_t g, uint8_t b) { + for (int x = x0; x < x0 + w; x++) { + pixel(x, y, r, g, b); + } +} + +static void vline(int x, int y0, int h, uint8_t r, uint8_t g, uint8_t b) { + for (int y = y0; y < y0 + h; y++) { + pixel(x, y, r, g, b); + } +} + +// Clock icon: circle with hands +static void iconClock(void) { + clear(192, 192, 192); + circle(15, 15, 13, 255, 255, 255); + circle(15, 15, 12, 240, 240, 255); + + // Rim + for (int a = 0; a < 360; a++) { + double rad = a * 3.14159265 / 180.0; + int x = 15 + (int)(13.0 * __builtin_cos(rad)); + int y = 15 + (int)(13.0 * __builtin_sin(rad)); + pixel(x, y, 0, 0, 128); + } + + // Hour marks + for (int i = 0; i < 12; i++) { + double rad = i * 30.0 * 3.14159265 / 180.0; + int x = 15 + (int)(11.0 * __builtin_cos(rad)); + int y = 15 + (int)(11.0 * __builtin_sin(rad)); + pixel(x, y, 0, 0, 128); + } + + // Hour hand (pointing ~10 o'clock) + vline(15, 7, 8, 0, 0, 128); + + // Minute hand (pointing ~2 o'clock) + for (int i = 0; i < 10; i++) { + pixel(15 + i * 2 / 3, 15 - i * 2 / 3, 0, 0, 200); + } + + // Center dot + pixel(15, 15, 200, 0, 0); +} + +// Notepad icon: page with lines +static void iconNotepad(void) { + clear(192, 192, 192); + rect(6, 3, 20, 26, 255, 255, 240); + + // Border + hline(6, 3, 20, 0, 0, 0); + hline(6, 28, 20, 0, 0, 0); + vline(6, 3, 26, 0, 0, 0); + vline(25, 3, 26, 0, 0, 0); + + // Folded corner + for (int i = 0; i < 5; i++) { + hline(21 + i, 3 + i, 5 - i, 200, 200, 180); + } + + // Text lines + for (int i = 0; i < 7; i++) { + int lineW = (i == 3) ? 12 : (i == 6) ? 8 : 16; + hline(9, 8 + i * 3, lineW, 0, 0, 128); + } +} + +// Control Panel icon: gear/wrench +static void iconCpanel(void) { + clear(192, 192, 192); + + // Gear body + circle(15, 15, 8, 128, 128, 128); + circle(15, 15, 5, 192, 192, 192); + + // Gear teeth (N/S/E/W and diagonals) + rect(13, 2, 5, 4, 128, 128, 128); + rect(13, 26, 5, 4, 128, 128, 128); + rect(2, 13, 4, 5, 128, 128, 128); + rect(26, 13, 4, 5, 128, 128, 128); + rect(4, 4, 4, 4, 128, 128, 128); + rect(24, 4, 4, 4, 128, 128, 128); + rect(4, 24, 4, 4, 128, 128, 128); + rect(24, 24, 4, 4, 128, 128, 128); + + // Inner circle (hole) + circle(15, 15, 3, 64, 64, 64); +} + +// DVX Demo icon: colorful diamond +static void iconDvxdemo(void) { + clear(192, 192, 192); + + // Diamond shape with color gradient + for (int y = 0; y < 16; y++) { + int hw = y + 1; + + for (int x = -hw; x <= hw; x++) { + int px = 15 + x; + int py = 2 + y; + uint8_t r = (uint8_t)(128 + y * 8); + uint8_t g = (uint8_t)(64 + (hw - (x < 0 ? -x : x)) * 8); + uint8_t b = (uint8_t)(200 - y * 4); + pixel(px, py, r, g, b); + } + } + + for (int y = 0; y < 14; y++) { + int hw = 14 - y; + + for (int x = -hw; x <= hw; x++) { + int px = 15 + x; + int py = 18 + y; + uint8_t r = (uint8_t)(255 - y * 8); + uint8_t g = (uint8_t)(128 - y * 4); + uint8_t b = (uint8_t)(100 + y * 8); + pixel(px, py, r, g, b); + } + } +} + +// Image Viewer icon: landscape in a frame +static void iconImgview(void) { + clear(192, 192, 192); + + // Frame + rect(3, 5, 26, 22, 100, 80, 60); + rect(5, 7, 22, 18, 135, 200, 235); + + // Sun + circle(21, 11, 3, 255, 220, 50); + + // Mountain + for (int y = 0; y < 10; y++) { + int w = y * 2 + 1; + hline(15 - y, 15 + y, w, 80, 140, 80); + } + + // Ground + rect(5, 22, 22, 3, 60, 120, 60); +} + +static void writeBmp(const char *path) { + int32_t rowPad = (4 - (W * 3) % 4) % 4; + int32_t rowSize = W * 3 + rowPad; + int32_t dataSize = rowSize * H; + int32_t fileSize = 54 + dataSize; + + uint8_t header[54]; + memset(header, 0, sizeof(header)); + + // BMP header + header[0] = 'B'; + header[1] = 'M'; + *(int32_t *)&header[2] = fileSize; + *(int32_t *)&header[10] = 54; + + // DIB header + *(int32_t *)&header[14] = 40; + *(int32_t *)&header[18] = W; + *(int32_t *)&header[22] = H; + *(int16_t *)&header[26] = 1; + *(int16_t *)&header[28] = 24; + *(int32_t *)&header[34] = dataSize; + + FILE *f = fopen(path, "wb"); + + if (!f) { + fprintf(stderr, "Cannot write: %s\n", path); + return; + } + + fwrite(header, 1, 54, f); + + // BMP rows are bottom-to-top + uint8_t pad[3] = {0}; + + for (int y = H - 1; y >= 0; y--) { + fwrite(pixels[y], 1, W * 3, f); + + if (rowPad > 0) { + fwrite(pad, 1, rowPad, f); + } + } + + fclose(f); +} + +int main(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, "Usage: mkicon \n"); + fprintf(stderr, "Types: clock, notepad, cpanel, dvxdemo, imgview\n"); + return 1; + } + + const char *path = argv[1]; + const char *type = argv[2]; + + if (strcmp(type, "clock") == 0) { + iconClock(); + } else if (strcmp(type, "notepad") == 0) { + iconNotepad(); + } else if (strcmp(type, "cpanel") == 0) { + iconCpanel(); + } else if (strcmp(type, "dvxdemo") == 0) { + iconDvxdemo(); + } else if (strcmp(type, "imgview") == 0) { + iconImgview(); + } else { + fprintf(stderr, "Unknown icon type: %s\n", type); + return 1; + } + + writeBmp(path); + printf("Generated %s (%s)\n", path, type); + return 0; +}