diff --git a/apps/Makefile b/apps/Makefile new file mode 100644 index 0000000..023902d --- /dev/null +++ b/apps/Makefile @@ -0,0 +1,53 @@ +# DV/X Shell Applications Makefile — builds DXE3 modules + +DJGPP_PREFIX = $(HOME)/djgpp/djgpp +DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib +CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../dvx -I../tasks -I../dvxshell + +OBJDIR = ../obj/apps +BINDIR = ../bin/apps + +# App definitions: each is a subdir with a single .c file +APPS = about notepad clock + +.PHONY: all clean $(APPS) + +all: $(APPS) + +about: $(BINDIR)/about.dxe +notepad: $(BINDIR)/notepad.dxe +clock: $(BINDIR)/clock.dxe + +$(BINDIR)/about.dxe: $(OBJDIR)/about.o | $(BINDIR) + $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + +$(BINDIR)/notepad.dxe: $(OBJDIR)/notepad.o | $(BINDIR) + $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + +$(BINDIR)/clock.dxe: $(OBJDIR)/clock.o | $(BINDIR) + $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -E _appShutdown -U $< + +$(OBJDIR)/about.o: about/about.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/notepad.o: notepad/notepad.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/clock.o: clock/clock.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(BINDIR): + mkdir -p $(BINDIR) + +# Dependencies +$(OBJDIR)/about.o: about/about.c ../dvx/dvxApp.h ../dvx/dvxWidget.h ../dvxshell/shellApp.h +$(OBJDIR)/notepad.o: notepad/notepad.c ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxWm.h ../dvxshell/shellApp.h +$(OBJDIR)/clock.o: clock/clock.c ../dvx/dvxApp.h ../dvx/dvxWidget.h ../dvx/dvxDraw.h ../dvx/dvxVideo.h ../dvxshell/shellApp.h ../tasks/taskswitch.h + +clean: + rm -rf $(OBJDIR) $(BINDIR) diff --git a/apps/about/about.c b/apps/about/about.c new file mode 100644 index 0000000..7e65bf7 --- /dev/null +++ b/apps/about/about.c @@ -0,0 +1,99 @@ +// about.c — "About DV/X Shell" sample DXE application (callback-only) +// +// Demonstrates a simple callback-only app: creates a window with widgets, +// registers callbacks, and returns. The shell event loop handles the rest. + +#include "dvxApp.h" +#include "dvxWidget.h" +#include "shellApp.h" + +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx); +static void onClose(WindowT *win); +static void onOkClick(WidgetT *w); + +// ============================================================ +// App state +// ============================================================ + +static DxeAppContextT *sCtx = NULL; +static WindowT *sWin = NULL; + +// ============================================================ +// App descriptor (required DXE export) +// ============================================================ + +AppDescriptorT appDescriptor = { + .name = "About", + .hasMainLoop = false, + .stackSize = 0, + .priority = TS_PRIORITY_NORMAL +}; + +// ============================================================ +// Callbacks +// ============================================================ + +static void onClose(WindowT *win) { + dvxDestroyWindow(sCtx->shellCtx, win); + sWin = NULL; +} + + +static void onOkClick(WidgetT *w) { + (void)w; + + if (sWin) { + dvxDestroyWindow(sCtx->shellCtx, sWin); + sWin = NULL; + } +} + +// ============================================================ +// Entry point +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx) { + sCtx = ctx; + + AppContextT *ac = ctx->shellCtx; + int32_t screenW = ac->display.width; + int32_t screenH = ac->display.height; + int32_t winW = 280; + int32_t winH = 200; + int32_t winX = (screenW - winW) / 2; + int32_t winY = (screenH - winH) / 3; + + sWin = dvxCreateWindow(ac, "About DV/X Shell", winX, winY, winW, winH, false); + + if (!sWin) { + return -1; + } + + sWin->onClose = onClose; + + WidgetT *root = wgtInitWindow(ac, sWin); + + wgtLabel(root, "DV/X Shell 1.0"); + wgtHSeparator(root); + wgtLabel(root, "A DESQview/X-style desktop shell for DJGPP/DPMI."); + wgtLabel(root, "Using DXE3 dynamic loading for application modules."); + wgtSpacer(root); + + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignCenterE; + + WidgetT *okBtn = wgtButton(btnRow, "OK"); + okBtn->onClick = onOkClick; + okBtn->prefW = wgtPixels(80); + + dvxFitWindow(ac, sWin); + wgtInvalidate(root); + return 0; +} diff --git a/apps/clock/clock.c b/apps/clock/clock.c new file mode 100644 index 0000000..b8c5880 --- /dev/null +++ b/apps/clock/clock.c @@ -0,0 +1,178 @@ +// clock.c — Clock DXE application (main-loop with tsYield) +// +// Demonstrates a main-loop app: runs its own loop calling tsYield(), +// updates a clock display every second, and invalidates its window. + +#include "dvxApp.h" +#include "dvxWidget.h" +#include "dvxDraw.h" +#include "dvxVideo.h" +#include "shellApp.h" +#include "taskswitch.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// Module state +// ============================================================ + +typedef struct { + bool quit; + char timeStr[32]; + char dateStr[32]; + time_t lastUpdate; +} ClockStateT; + +static DxeAppContextT *sCtx = NULL; +static WindowT *sWin = NULL; +static ClockStateT sState; + +// ============================================================ +// Prototypes +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx); +void appShutdown(void); +static void onClose(WindowT *win); +static void onPaint(WindowT *win, RectT *dirty); +static void updateTime(void); + +// ============================================================ +// App descriptor +// ============================================================ + +AppDescriptorT appDescriptor = { + .name = "Clock", + .hasMainLoop = true, + .stackSize = 0, + .priority = TS_PRIORITY_LOW +}; + +// ============================================================ +// Callbacks (fire in task 0 during dvxUpdate) +// ============================================================ + +static void onClose(WindowT *win) { + (void)win; + sState.quit = true; +} + + +static void onPaint(WindowT *win, RectT *dirty) { + (void)dirty; + + AppContextT *ac = sCtx->shellCtx; + const BlitOpsT *ops = dvxGetBlitOps(ac); + const BitmapFontT *font = dvxGetFont(ac); + const ColorSchemeT *colors = dvxGetColors(ac); + + // Local display copy pointing at content buffer + DisplayT cd = *dvxGetDisplay(ac); + cd.backBuf = win->contentBuf; + cd.width = win->contentW; + cd.height = win->contentH; + cd.pitch = win->contentPitch; + cd.clipX = 0; + cd.clipY = 0; + cd.clipW = win->contentW; + cd.clipH = win->contentH; + + // Background + rectFill(&cd, ops, 0, 0, win->contentW, win->contentH, colors->contentBg); + + // Time string (large, centered) + int32_t timeW = textWidth(font, sState.timeStr); + int32_t timeX = (win->contentW - timeW) / 2; + int32_t timeY = win->contentH / 3; + drawText(&cd, ops, font, timeX, timeY, sState.timeStr, colors->contentFg, colors->contentBg, true); + + // Date string (centered below) + int32_t dateW = textWidth(font, sState.dateStr); + int32_t dateX = (win->contentW - dateW) / 2; + int32_t dateY = timeY + font->charHeight + 8; + drawText(&cd, ops, font, dateX, dateY, sState.dateStr, colors->contentFg, colors->contentBg, true); +} + +// ============================================================ +// Time update +// ============================================================ + +static void updateTime(void) { + time_t now = time(NULL); + struct tm *tm = localtime(&now); + + if (!tm) { + snprintf(sState.timeStr, sizeof(sState.timeStr), "--:--:--"); + snprintf(sState.dateStr, sizeof(sState.dateStr), "----:--:--"); + sState.lastUpdate = now; + return; + } + + snprintf(sState.timeStr, sizeof(sState.timeStr), "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); + snprintf(sState.dateStr, sizeof(sState.dateStr), "%04d-%02d-%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); + sState.lastUpdate = now; +} + +// ============================================================ +// Shutdown hook (optional DXE export) +// ============================================================ + +void appShutdown(void) { + sState.quit = true; +} + +// ============================================================ +// Entry point (runs in its own task) +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx) { + sCtx = ctx; + AppContextT *ac = ctx->shellCtx; + + memset(&sState, 0, sizeof(sState)); + updateTime(); + + int32_t winW = 200; + int32_t winH = 100; + int32_t winX = ac->display.width - winW - 40; + int32_t winY = 40; + + sWin = dvxCreateWindow(ac, "Clock", winX, winY, winW, winH, false); + + if (!sWin) { + return -1; + } + + sWin->onClose = onClose; + sWin->onPaint = onPaint; + + // Initial paint into content buffer + RectT full = {0, 0, sWin->contentW, sWin->contentH}; + onPaint(sWin, &full); + dvxInvalidateWindow(ac, sWin); + + // Main loop: update time, invalidate, yield + while (!sState.quit) { + time_t now = time(NULL); + + if (now != sState.lastUpdate) { + updateTime(); + onPaint(sWin, &full); + dvxInvalidateWindow(ac, sWin); + } + + tsYield(); + } + + // Cleanup + if (sWin) { + dvxDestroyWindow(ac, sWin); + sWin = NULL; + } + + return 0; +} diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c new file mode 100644 index 0000000..a4570a7 --- /dev/null +++ b/apps/notepad/notepad.c @@ -0,0 +1,340 @@ +// notepad.c — Simple text editor DXE application (callback-only) +// +// Demonstrates a callback-only app with menus, text editing, and file I/O. + +#include "dvxApp.h" +#include "dvxDialog.h" +#include "dvxWidget.h" +#include "dvxWm.h" +#include "shellApp.h" + +#include +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define TEXT_BUF_SIZE 32768 + +#define CMD_NEW 100 +#define CMD_OPEN 101 +#define CMD_SAVE 102 +#define CMD_SAVEAS 103 +#define CMD_EXIT 104 +#define CMD_CUT 200 +#define CMD_COPY 201 +#define CMD_PASTE 202 +#define CMD_SELALL 203 + +// ============================================================ +// Module state +// ============================================================ + +static DxeAppContextT *sCtx = NULL; +static WindowT *sWin = NULL; +static WidgetT *sTextArea = NULL; +static char sFilePath[260] = ""; +static uint32_t sCleanHash = 0; + +// ============================================================ +// Prototypes +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx); +static bool askSaveChanges(void); +static void doNew(void); +static void doOpen(void); +static void doSave(void); +static void doSaveAs(void); +static uint32_t hashText(const char *text); +static bool isDirty(void); +static void markClean(void); +static void onClose(WindowT *win); +static void onMenu(WindowT *win, int32_t menuId); + +// ============================================================ +// App descriptor +// ============================================================ + +AppDescriptorT appDescriptor = { + .name = "Notepad", + .hasMainLoop = false, + .stackSize = 0, + .priority = TS_PRIORITY_NORMAL +}; + +// ============================================================ +// Dirty tracking +// ============================================================ + +static uint32_t hashText(const char *text) { + if (!text) { + return 0; + } + + uint32_t h = 5381; + + while (*text) { + h = ((h << 5) + h) ^ (uint8_t)*text; + text++; + } + + return h; +} + + +static bool isDirty(void) { + const char *text = wgtGetText(sTextArea); + return hashText(text) != sCleanHash; +} + + +static void markClean(void) { + const char *text = wgtGetText(sTextArea); + sCleanHash = hashText(text); +} + + +// Returns true if it's OK to proceed (saved, discarded, or not dirty). +// Returns false if the user cancelled. +static bool askSaveChanges(void) { + if (!isDirty()) { + return true; + } + + int32_t result = dvxMessageBox(sCtx->shellCtx, "Notepad", "Save changes?", MB_YESNOCANCEL | MB_ICONQUESTION); + + if (result == ID_YES) { + doSave(); + return true; + } + + if (result == ID_NO) { + return true; + } + + return false; +} + +// ============================================================ +// File operations +// ============================================================ + +static void doNew(void) { + if (!askSaveChanges()) { + return; + } + + wgtSetText(sTextArea, ""); + sFilePath[0] = '\0'; + markClean(); + dvxSetTitle(sCtx->shellCtx, sWin, "Untitled - Notepad"); +} + + +static void doOpen(void) { + if (!askSaveChanges()) { + return; + } + + FileFilterT filters[] = { + { "Text Files (*.txt)", "*.txt" }, + { "All Files (*.*)", "*.*" } + }; + char path[260]; + + if (!dvxFileDialog(sCtx->shellCtx, "Open", FD_OPEN, NULL, filters, 2, path, sizeof(path))) { + return; + } + + FILE *f = fopen(path, "rb"); + + if (!f) { + dvxMessageBox(sCtx->shellCtx, "Error", "Could not open file.", MB_OK | MB_ICONERROR); + return; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size >= TEXT_BUF_SIZE - 1) { + size = TEXT_BUF_SIZE - 2; + } + + char *buf = (char *)malloc(size + 1); + + if (buf) { + fread(buf, 1, size, f); + buf[size] = '\0'; + wgtSetText(sTextArea, buf); + free(buf); + } + + fclose(f); + + snprintf(sFilePath, sizeof(sFilePath), "%s", path); + markClean(); + + char title[300]; + snprintf(title, sizeof(title), "%s - Notepad", sFilePath); + dvxSetTitle(sCtx->shellCtx, sWin, title); +} + + +static void doSave(void) { + if (sFilePath[0] == '\0') { + doSaveAs(); + return; + } + + const char *text = wgtGetText(sTextArea); + + if (!text) { + return; + } + + FILE *f = fopen(sFilePath, "wb"); + + if (!f) { + dvxMessageBox(sCtx->shellCtx, "Error", "Could not save file.", MB_OK | MB_ICONERROR); + return; + } + + fwrite(text, 1, strlen(text), f); + fclose(f); + markClean(); +} + + +static void doSaveAs(void) { + FileFilterT filters[] = { + { "Text Files (*.txt)", "*.txt" }, + { "All Files (*.*)", "*.*" } + }; + char path[260]; + + if (!dvxFileDialog(sCtx->shellCtx, "Save As", FD_SAVE, NULL, filters, 2, path, sizeof(path))) { + return; + } + + snprintf(sFilePath, sizeof(sFilePath), "%s", path); + doSave(); + + char title[300]; + snprintf(title, sizeof(title), "%s - Notepad", sFilePath); + dvxSetTitle(sCtx->shellCtx, sWin, title); +} + +// ============================================================ +// Callbacks +// ============================================================ + +static void onClose(WindowT *win) { + if (!askSaveChanges()) { + return; + } + + dvxDestroyWindow(sCtx->shellCtx, win); + sWin = NULL; + sTextArea = NULL; +} + + +static void onMenu(WindowT *win, int32_t menuId) { + (void)win; + + switch (menuId) { + case CMD_NEW: + doNew(); + break; + + case CMD_OPEN: + doOpen(); + break; + + case CMD_SAVE: + doSave(); + break; + + case CMD_SAVEAS: + doSaveAs(); + break; + + case CMD_EXIT: + onClose(sWin); + break; + + case CMD_CUT: + break; + + case CMD_COPY: + break; + + case CMD_PASTE: + break; + + case CMD_SELALL: + break; + } +} + +// ============================================================ +// Entry point +// ============================================================ + +int32_t appMain(DxeAppContextT *ctx) { + sCtx = ctx; + AppContextT *ac = ctx->shellCtx; + + int32_t screenW = ac->display.width; + int32_t screenH = ac->display.height; + int32_t winW = 480; + int32_t winH = 360; + int32_t winX = (screenW - winW) / 2 + 20; + int32_t winY = (screenH - winH) / 3 + 20; + + sWin = dvxCreateWindow(ac, "Untitled - Notepad", winX, winY, winW, winH, true); + + if (!sWin) { + return -1; + } + + sWin->onClose = onClose; + sWin->onMenu = onMenu; + + // Menu bar + MenuBarT *menuBar = wmAddMenuBar(sWin); + + MenuT *fileMenu = wmAddMenu(menuBar, "&File"); + wmAddMenuItem(fileMenu, "&New", CMD_NEW); + wmAddMenuItem(fileMenu, "&Open...", CMD_OPEN); + wmAddMenuSeparator(fileMenu); + wmAddMenuItem(fileMenu, "&Save", CMD_SAVE); + wmAddMenuItem(fileMenu, "Save &As...", CMD_SAVEAS); + wmAddMenuSeparator(fileMenu); + wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); + + MenuT *editMenu = wmAddMenu(menuBar, "&Edit"); + wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT); + wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY); + wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE); + wmAddMenuSeparator(editMenu); + wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELALL); + + // Widget tree + WidgetT *root = wgtInitWindow(ac, sWin); + + sTextArea = wgtTextArea(root, TEXT_BUF_SIZE); + sTextArea->weight = 100; + + sFilePath[0] = '\0'; + markClean(); + + wgtInvalidate(root); + return 0; +} diff --git a/dvx/Makefile b/dvx/Makefile index 8aa737d..3c2633d 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -86,7 +86,7 @@ $(LIBDIR): $(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h platform/dvxPlatform.h dvxTypes.h dvxPalette.h $(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h platform/dvxPlatform.h dvxTypes.h $(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h platform/dvxPlatform.h dvxTypes.h -$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h thirdparty/stb_image.h +$(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h dvxWidget.h thirdparty/stb_image.h $(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h $(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index ee6d0d1..25695e5 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -117,7 +117,26 @@ static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw bool hasCheck = false; for (int32_t k = 0; k < menu->itemCount; k++) { - int32_t itemW = textWidthAccel(&ctx->font, menu->items[k].label); + const char *label = menu->items[k].label; + const char *tab = strchr(label, '\t'); + int32_t itemW; + + if (tab) { + // Left part (with accel underline) + gap + right part (shortcut text) + char leftBuf[MAX_MENU_LABEL]; + int32_t leftLen = (int32_t)(tab - label); + + if (leftLen >= MAX_MENU_LABEL) { + leftLen = MAX_MENU_LABEL - 1; + } + + memcpy(leftBuf, label, leftLen); + leftBuf[leftLen] = '\0'; + + itemW = textWidthAccel(&ctx->font, leftBuf) + ctx->font.charWidth * 3 + textWidth(&ctx->font, tab + 1); + } else { + itemW = textWidthAccel(&ctx->font, label); + } if (itemW > maxW) { maxW = itemW; @@ -976,7 +995,35 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c } rectFill(d, ops, px + 2, itemY, pw - 4, ctx->font.charHeight, bg); - drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2 + checkMargin, itemY, item->label, fg, bg, true); + + // Split label at tab: left part is the menu text, right part is the shortcut + const char *tab = strchr(item->label, '\t'); + + if (tab) { + char leftBuf[MAX_MENU_LABEL]; + int32_t leftLen = (int32_t)(tab - item->label); + + if (leftLen >= MAX_MENU_LABEL) { + leftLen = MAX_MENU_LABEL - 1; + } + + memcpy(leftBuf, item->label, leftLen); + leftBuf[leftLen] = '\0'; + + drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2 + checkMargin, itemY, leftBuf, fg, bg, true); + + const char *right = tab + 1; + int32_t rightW = textWidth(&ctx->font, right); + int32_t rightX = px + pw - rightW - CHROME_TITLE_PAD - 4; + + if (item->subMenu) { + rightX -= SUBMENU_ARROW_WIDTH; + } + + drawText(d, ops, &ctx->font, rightX, itemY, right, fg, bg, true); + } else { + drawTextAccel(d, ops, &ctx->font, px + CHROME_TITLE_PAD + 2 + checkMargin, itemY, item->label, fg, bg, true); + } // Draw check/radio indicator if (item->checked) { @@ -1313,6 +1360,7 @@ void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int // ============================================================ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) { + win->contentDirty = true; dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); } diff --git a/dvx/dvxTypes.h b/dvx/dvxTypes.h index 9223894..7287829 100644 --- a/dvx/dvxTypes.h +++ b/dvx/dvxTypes.h @@ -276,6 +276,7 @@ typedef struct { typedef struct WindowT { int32_t id; + int32_t appId; // shell app ID (0 = shell itself) int32_t x; int32_t y; int32_t w; diff --git a/dvx/dvxWm.c b/dvx/dvxWm.c index e766a45..5c87d84 100644 --- a/dvx/dvxWm.c +++ b/dvx/dvxWm.c @@ -4,6 +4,7 @@ #include "dvxVideo.h" #include "dvxDraw.h" #include "dvxComp.h" +#include "dvxWidget.h" #include "thirdparty/stb_image.h" #include @@ -859,6 +860,12 @@ void wmDestroyWindow(WindowStackT *stack, WindowT *win) { } } + // Destroy widget tree before freeing window + if (win->widgetRoot) { + wgtDestroy(win->widgetRoot); + win->widgetRoot = NULL; + } + if (win->contentBuf) { free(win->contentBuf); } diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 68de286..8be0b89 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -576,7 +576,6 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) { if (win->onPaint) { RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); - win->contentDirty = true; // Dirty the window content area on screen so compositor redraws it if (win->widgetRoot) { diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index 1e00ed9..345aec9 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -289,7 +289,6 @@ void wgtInvalidate(WidgetT *w) { WindowT *win = w->window; RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); - win->contentDirty = true; // Dirty the window on screen dvxInvalidateWindow(ctx, win); @@ -325,8 +324,6 @@ void wgtInvalidatePaint(WidgetT *w) { WindowT *win = w->window; RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); - win->contentDirty = true; - dvxInvalidateWindow(ctx, win); } diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index 095712d..c0f4f86 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -222,7 +222,6 @@ void clearOtherSelections(WidgetT *except) { if (clearSelectionOnWidget(prev) && prevWin != except->window) { RectT fullRect = {0, 0, prevWin->contentW, prevWin->contentH}; widgetOnPaint(prevWin, &fullRect); - prevWin->contentDirty = true; dvxInvalidateWindow(ctx, prevWin); } } diff --git a/dvxshell/Makefile b/dvxshell/Makefile new file mode 100644 index 0000000..dde3cab --- /dev/null +++ b/dvxshell/Makefile @@ -0,0 +1,49 @@ +# DV/X Shell Makefile for DJGPP cross-compilation + +DJGPP_PREFIX = $(HOME)/djgpp/djgpp +DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib +CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff +CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../dvx -I../tasks +LDFLAGS = -L../lib -ldvx -ltasks -lm + +OBJDIR = ../obj/dvxshell +BINDIR = ../bin +LIBDIR = ../lib + +SRCS = shellMain.c shellApp.c shellExport.c shellDesktop.c +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGET = $(BINDIR)/dvxshell.exe + +.PHONY: all clean libs + +all: libs $(TARGET) + +libs: + $(MAKE) -C ../dvx + $(MAKE) -C ../tasks + +$(TARGET): $(OBJS) $(LIBDIR)/libdvx.a $(LIBDIR)/libtasks.a | $(BINDIR) + $(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS) -Wl,-Map=$(BINDIR)/dvxshell.map + $(EXE2COFF) $@ + cat $(CWSDSTUB) $(BINDIR)/dvxshell > $@ + rm -f $(BINDIR)/dvxshell + +$(OBJDIR)/%.o: %.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(BINDIR): + mkdir -p $(BINDIR) + +# Dependencies +$(OBJDIR)/shellMain.o: shellMain.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../tasks/taskswitch.h +$(OBJDIR)/shellApp.o: shellApp.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../tasks/taskswitch.h +$(OBJDIR)/shellExport.o: shellExport.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxDraw.h ../dvx/dvxVideo.h ../dvx/dvxWm.h ../tasks/taskswitch.h +$(OBJDIR)/shellDesktop.o: shellDesktop.c shellApp.h ../dvx/dvxApp.h ../dvx/dvxDialog.h ../dvx/dvxWidget.h ../dvx/dvxWm.h + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/dvxshell/shellApp.c b/dvxshell/shellApp.c new file mode 100644 index 0000000..69a2d4e --- /dev/null +++ b/dvxshell/shellApp.c @@ -0,0 +1,281 @@ +// shellApp.c — DV/X Shell application loading, lifecycle, and reaping +// +// Manages DXE app loading via dlopen/dlsym, resource tracking through +// sCurrentAppId, and clean teardown of both callback-only and main-loop apps. + +#include "shellApp.h" +#include "dvxDialog.h" + +#include +#include +#include +#include + +// ============================================================ +// Module state +// ============================================================ + +static ShellAppT sApps[SHELL_MAX_APPS]; +int32_t sCurrentAppId = 0; + +// ============================================================ +// Prototypes +// ============================================================ + +static int32_t allocSlot(void); +static void appTaskWrapper(void *arg); +static const char *baseName(const char *path); +void shellAppInit(void); +void shellForceKillApp(AppContextT *ctx, ShellAppT *app); +ShellAppT *shellGetApp(int32_t appId); +int32_t shellLoadApp(AppContextT *ctx, const char *path); +void shellReapApp(AppContextT *ctx, ShellAppT *app); +void shellReapApps(AppContextT *ctx); +int32_t shellRunningAppCount(void); +void shellTerminateAllApps(AppContextT *ctx); + +// ============================================================ +// Static helpers +// ============================================================ + +static int32_t allocSlot(void) { + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + if (sApps[i].state == AppStateFreeE) { + return i; + } + } + + return -1; +} + + +static void appTaskWrapper(void *arg) { + ShellAppT *app = (ShellAppT *)arg; + + sCurrentAppId = app->appId; + app->entryFn(&app->dxeCtx); + sCurrentAppId = 0; + + // App returned from its main loop — mark for reaping + app->state = AppStateTerminatingE; +} + + +static const char *baseName(const char *path) { + const char *slash = strrchr(path, '/'); + + if (!slash) { + slash = strrchr(path, '\\'); + } + + return slash ? slash + 1 : path; +} + +// ============================================================ +// Public API (alphabetical) +// ============================================================ + +void shellAppInit(void) { + memset(sApps, 0, sizeof(sApps)); +} + + +void shellForceKillApp(AppContextT *ctx, ShellAppT *app) { + if (!app || app->state == AppStateFreeE) { + return; + } + + // Destroy all windows belonging to this app + for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { + if (ctx->stack.windows[i]->appId == app->appId) { + dvxDestroyWindow(ctx, ctx->stack.windows[i]); + } + } + + // Kill the task if it has one + if (app->hasMainLoop && app->mainTaskId > 0) { + if (tsGetState(app->mainTaskId) != TaskStateTerminated) { + tsKill(app->mainTaskId); + } + } + + // Close the DXE + if (app->dxeHandle) { + dlclose(app->dxeHandle); + app->dxeHandle = NULL; + } + + app->state = AppStateFreeE; + shellLog("Shell: force-killed app '%s'", app->name); +} + + +ShellAppT *shellGetApp(int32_t appId) { + if (appId < 1 || appId >= SHELL_MAX_APPS) { + return NULL; + } + + if (sApps[appId].state == AppStateFreeE) { + return NULL; + } + + return &sApps[appId]; +} + + +int32_t shellLoadApp(AppContextT *ctx, const char *path) { + // Allocate a slot + int32_t id = allocSlot(); + + if (id < 0) { + dvxMessageBox(ctx, "Error", "Maximum number of applications reached.", MB_OK | MB_ICONERROR); + return -1; + } + + // Load the DXE + void *handle = dlopen(path, RTLD_GLOBAL); + + if (!handle) { + char msg[512]; + snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", baseName(path), dlerror()); + dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); + return -1; + } + + // Look up required symbols + AppDescriptorT *desc = (AppDescriptorT *)dlsym(handle, "_appDescriptor"); + + if (!desc) { + char msg[256]; + snprintf(msg, sizeof(msg), "%s: missing appDescriptor", baseName(path)); + dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); + dlclose(handle); + return -1; + } + + int32_t (*entry)(DxeAppContextT *) = (int32_t (*)(DxeAppContextT *))dlsym(handle, "_appMain"); + + if (!entry) { + char msg[256]; + snprintf(msg, sizeof(msg), "%s: missing appMain", baseName(path)); + dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); + dlclose(handle); + return -1; + } + + void (*shutdown)(void) = (void (*)(void))dlsym(handle, "_appShutdown"); + + // Fill in the app slot + ShellAppT *app = &sApps[id]; + memset(app, 0, sizeof(*app)); + + app->appId = id; + snprintf(app->name, SHELL_APP_NAME_MAX, "%s", desc->name); + snprintf(app->path, sizeof(app->path), "%s", path); + app->dxeHandle = handle; + app->hasMainLoop = desc->hasMainLoop; + app->entryFn = entry; + app->shutdownFn = shutdown; + app->state = AppStateLoadedE; + + // Set up the context passed to appMain + app->dxeCtx.shellCtx = ctx; + app->dxeCtx.appId = id; + + // Launch + sCurrentAppId = id; + + if (desc->hasMainLoop) { + uint32_t stackSize = desc->stackSize > 0 ? (uint32_t)desc->stackSize : TS_DEFAULT_STACK_SIZE; + int32_t priority = desc->priority; + + int32_t taskId = tsCreate(desc->name, appTaskWrapper, app, stackSize, priority); + + if (taskId < 0) { + sCurrentAppId = 0; + dvxMessageBox(ctx, "Error", "Failed to create task for application.", MB_OK | MB_ICONERROR); + dlclose(handle); + app->state = AppStateFreeE; + return -1; + } + + app->mainTaskId = (uint32_t)taskId; + } else { + // Callback-only: call entry directly in task 0 + app->entryFn(&app->dxeCtx); + } + + sCurrentAppId = 0; + app->state = AppStateRunningE; + + shellLog("Shell: loaded '%s' (id=%ld, mainLoop=%s, entry=0x%08lx, desc=0x%08lx)", app->name, (long)id, app->hasMainLoop ? "yes" : "no", (unsigned long)entry, (unsigned long)desc); + return id; +} + + +void shellReapApp(AppContextT *ctx, ShellAppT *app) { + if (!app || app->state == AppStateFreeE) { + return; + } + + // Call shutdown hook if present + if (app->shutdownFn) { + sCurrentAppId = app->appId; + app->shutdownFn(); + sCurrentAppId = 0; + } + + // Destroy all windows belonging to this app + for (int32_t i = ctx->stack.count - 1; i >= 0; i--) { + if (ctx->stack.windows[i]->appId == app->appId) { + dvxDestroyWindow(ctx, ctx->stack.windows[i]); + } + } + + // Kill the task if it has one and it's still alive + if (app->hasMainLoop && app->mainTaskId > 0) { + if (tsGetState(app->mainTaskId) != TaskStateTerminated) { + tsKill(app->mainTaskId); + } + } + + // Close the DXE + if (app->dxeHandle) { + dlclose(app->dxeHandle); + app->dxeHandle = NULL; + } + + shellLog("Shell: reaped app '%s'", app->name); + app->state = AppStateFreeE; +} + + +void shellReapApps(AppContextT *ctx) { + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + if (sApps[i].state == AppStateTerminatingE) { + shellReapApp(ctx, &sApps[i]); + } + } +} + + +int32_t shellRunningAppCount(void) { + int32_t count = 0; + + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + if (sApps[i].state == AppStateRunningE || sApps[i].state == AppStateLoadedE) { + count++; + } + } + + return count; +} + + +void shellTerminateAllApps(AppContextT *ctx) { + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + if (sApps[i].state != AppStateFreeE) { + shellForceKillApp(ctx, &sApps[i]); + } + } +} diff --git a/dvxshell/shellApp.h b/dvxshell/shellApp.h new file mode 100644 index 0000000..25eeef0 --- /dev/null +++ b/dvxshell/shellApp.h @@ -0,0 +1,125 @@ +// shellApp.h — DV/X Shell application lifecycle types and API +#ifndef SHELL_APP_H +#define SHELL_APP_H + +#include "dvxApp.h" +#include "dvxWidget.h" +#include "taskswitch.h" + +#include +#include + +// ============================================================ +// App descriptor (exported by each DXE app) +// ============================================================ + +#define SHELL_APP_NAME_MAX 64 +#define SHELL_MAX_APPS 32 + +typedef struct { + char name[SHELL_APP_NAME_MAX]; + bool hasMainLoop; + int32_t stackSize; // 0 = TS_DEFAULT_STACK_SIZE + int32_t priority; // TS_PRIORITY_* or custom +} AppDescriptorT; + +// ============================================================ +// App context (passed to appMain) +// ============================================================ + +typedef struct { + AppContextT *shellCtx; // the shell's GUI context + int32_t appId; // this app's ID +} DxeAppContextT; + +// ============================================================ +// Per-app state +// ============================================================ + +typedef enum { + AppStateFreeE, // slot available + AppStateLoadedE, // DXE loaded, not yet started + AppStateRunningE, // entry point called, active + AppStateTerminatingE // shutdown in progress +} AppStateE; + +typedef struct { + int32_t appId; // unique ID = slot index (1-based; 0 = shell) + char name[SHELL_APP_NAME_MAX]; + char path[260]; + void *dxeHandle; // dlopen() handle + AppStateE state; + bool hasMainLoop; + uint32_t mainTaskId; // task ID if hasMainLoop, else 0 + int32_t (*entryFn)(DxeAppContextT *); + void (*shutdownFn)(void); // may be NULL + DxeAppContextT dxeCtx; // context passed to appMain +} ShellAppT; + +// ============================================================ +// Shell global state +// ============================================================ + +// Current app ID for resource tracking (0 = shell) +extern int32_t sCurrentAppId; + +// ============================================================ +// App lifecycle API +// ============================================================ + +// Initialize the app slot table +void shellAppInit(void); + +// Load and start an app from a DXE file. Returns app ID (>= 1) or -1 on error. +int32_t shellLoadApp(AppContextT *ctx, const char *path); + +// Reap finished callback-only apps (call each frame from main loop) +void shellReapApps(AppContextT *ctx); + +// Gracefully shut down a single app +void shellReapApp(AppContextT *ctx, ShellAppT *app); + +// Forcibly kill an app (Task Manager "End Task") +void shellForceKillApp(AppContextT *ctx, ShellAppT *app); + +// Terminate all running apps (shell shutdown) +void shellTerminateAllApps(AppContextT *ctx); + +// Get app slot by ID (returns NULL if invalid/free) +ShellAppT *shellGetApp(int32_t appId); + +// Count running apps (not counting the shell itself) +int32_t shellRunningAppCount(void); + +// ============================================================ +// Logging +// ============================================================ + +// Write a printf-style message to SHELL.LOG +void shellLog(const char *fmt, ...); + +// ============================================================ +// DXE export table +// ============================================================ + +// Register the DXE symbol export table (call before any dlopen) +void shellExportInit(void); + +// ============================================================ +// Program Manager / Desktop +// ============================================================ + +// Initialize the Program Manager window +void shellDesktopInit(AppContextT *ctx); + +// Update status bar with running app count +void shellDesktopUpdateStatus(void); + +// ============================================================ +// Task Manager +// ============================================================ + +// Open the Task Manager dialog (Ctrl+Esc) +void shellTaskManager(AppContextT *ctx); + +#endif // SHELL_APP_H diff --git a/dvxshell/shellDesktop.c b/dvxshell/shellDesktop.c new file mode 100644 index 0000000..0d34bd2 --- /dev/null +++ b/dvxshell/shellDesktop.c @@ -0,0 +1,493 @@ +// shellDesktop.c — Program Manager window for DV/X Shell +// +// Displays a grid of available DXE apps from the apps/ directory. +// Double-click or Enter launches an app. Includes Task Manager (Ctrl+Esc). + +#include "shellApp.h" +#include "dvxDialog.h" + +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define MAX_DXE_FILES 64 +#define MAX_PATH_LEN 260 +#define PM_GRID_COLS 4 +#define PM_BTN_W 100 +#define PM_BTN_H 24 + +// Menu command IDs +#define CMD_RUN 100 +#define CMD_EXIT 101 +#define CMD_CASCADE 200 +#define CMD_TILE 201 +#define CMD_TILE_H 202 +#define CMD_TILE_V 203 +#define CMD_ABOUT 300 +#define CMD_TASK_MGR 301 + +// ============================================================ +// Module state +// ============================================================ + +typedef struct { + char name[SHELL_APP_NAME_MAX]; // display name (filename without .dxe) + char path[MAX_PATH_LEN]; // full path +} DxeEntryT; + +static AppContextT *sCtx = NULL; +static WindowT *sPmWindow = NULL; +static WidgetT *sStatusLabel = NULL; +static DxeEntryT sDxeFiles[MAX_DXE_FILES]; +static int32_t sDxeCount = 0; + +// ============================================================ +// Prototypes +// ============================================================ + +static void buildPmWindow(void); +static void onAppButtonClick(WidgetT *w); +static void onPmClose(WindowT *win); +static void onPmMenu(WindowT *win, int32_t menuId); +static void scanAppsDir(void); +static void showAboutDialog(void); +static void updateStatusText(void); + +// Task Manager +static WindowT *sTmWindow = NULL; +static WidgetT *sTmListBox = NULL; +static void buildTaskManager(void); +static void onTmClose(WindowT *win); +static void onTmEndTask(WidgetT *w); +static void onTmSwitchTo(WidgetT *w); +static void refreshTaskList(void); + +// ============================================================ +// Static functions (alphabetical) +// ============================================================ + +static void buildPmWindow(void) { + int32_t screenW = sCtx->display.width; + int32_t screenH = sCtx->display.height; + int32_t winW = 440; + int32_t winH = 340; + int32_t winX = (screenW - winW) / 2; + int32_t winY = (screenH - winH) / 4; + + sPmWindow = dvxCreateWindow(sCtx, "Program Manager", winX, winY, winW, winH, true); + + if (!sPmWindow) { + return; + } + + sPmWindow->appId = 0; + sPmWindow->onClose = onPmClose; + sPmWindow->onMenu = onPmMenu; + + // Menu bar + MenuBarT *menuBar = wmAddMenuBar(sPmWindow); + MenuT *fileMenu = wmAddMenu(menuBar, "&File"); + wmAddMenuItem(fileMenu, "&Run...", CMD_RUN); + wmAddMenuSeparator(fileMenu); + wmAddMenuItem(fileMenu, "E&xit Shell", CMD_EXIT); + + MenuT *windowMenu = wmAddMenu(menuBar, "&Window"); + wmAddMenuItem(windowMenu, "&Cascade", CMD_CASCADE); + wmAddMenuItem(windowMenu, "&Tile", CMD_TILE); + wmAddMenuItem(windowMenu, "Tile &Horizontally", CMD_TILE_H); + wmAddMenuItem(windowMenu, "Tile &Vertically", CMD_TILE_V); + + MenuT *helpMenu = wmAddMenu(menuBar, "&Help"); + wmAddMenuItem(helpMenu, "&About DV/X Shell...", CMD_ABOUT); + wmAddMenuSeparator(helpMenu); + wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR); + + // Widget tree + WidgetT *root = wgtInitWindow(sCtx, sPmWindow); + + // App button grid in a frame + WidgetT *appFrame = wgtFrame(root, "Applications"); + appFrame->weight = 100; + + if (sDxeCount == 0) { + WidgetT *lbl = wgtLabel(appFrame, "(No applications found in apps/ directory)"); + (void)lbl; + } else { + // Build rows of buttons + int32_t row = 0; + WidgetT *hbox = NULL; + + for (int32_t i = 0; i < sDxeCount; i++) { + if (i % PM_GRID_COLS == 0) { + hbox = wgtHBox(appFrame); + hbox->spacing = wgtPixels(8); + row++; + } + + WidgetT *btn = wgtButton(hbox, sDxeFiles[i].name); + btn->prefW = wgtPixels(PM_BTN_W); + btn->prefH = wgtPixels(PM_BTN_H); + btn->userData = &sDxeFiles[i]; + btn->onDblClick = onAppButtonClick; + btn->onClick = onAppButtonClick; + } + + (void)row; + } + + // Status bar + WidgetT *statusBar = wgtStatusBar(root); + sStatusLabel = wgtLabel(statusBar, ""); + updateStatusText(); + + dvxFitWindow(sCtx, sPmWindow); + wgtInvalidate(root); +} + + +static void buildTaskManager(void) { + if (sTmWindow) { + // Already open — just raise it + for (int32_t i = 0; i < sCtx->stack.count; i++) { + if (sCtx->stack.windows[i] == sTmWindow) { + wmRaiseWindow(&sCtx->stack, &sCtx->dirty, i); + wmSetFocus(&sCtx->stack, &sCtx->dirty, sCtx->stack.count - 1); + break; + } + } + return; + } + + int32_t screenW = sCtx->display.width; + int32_t screenH = sCtx->display.height; + int32_t winW = 300; + int32_t winH = 280; + int32_t winX = (screenW - winW) / 2; + int32_t winY = (screenH - winH) / 3; + + sTmWindow = dvxCreateWindow(sCtx, "Task Manager", winX, winY, winW, winH, true); + + if (!sTmWindow) { + return; + } + + sTmWindow->appId = 0; + sTmWindow->onClose = onTmClose; + + WidgetT *root = wgtInitWindow(sCtx, sTmWindow); + + // List box of running apps + sTmListBox = wgtListBox(root); + sTmListBox->weight = 100; + sTmListBox->prefH = wgtPixels(160); + + // Button row + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignEndE; + btnRow->spacing = wgtPixels(8); + + WidgetT *switchBtn = wgtButton(btnRow, "Switch To"); + switchBtn->onClick = onTmSwitchTo; + switchBtn->prefW = wgtPixels(90); + + WidgetT *endBtn = wgtButton(btnRow, "End Task"); + endBtn->onClick = onTmEndTask; + endBtn->prefW = wgtPixels(90); + + refreshTaskList(); + dvxFitWindow(sCtx, sTmWindow); + wgtInvalidate(root); +} + + +static void onAppButtonClick(WidgetT *w) { + DxeEntryT *entry = (DxeEntryT *)w->userData; + + if (!entry) { + return; + } + + shellLoadApp(sCtx, entry->path); + updateStatusText(); +} + + +static void onPmClose(WindowT *win) { + (void)win; + // Confirm exit + int32_t result = dvxMessageBox(sCtx, "Exit Shell", "Are you sure you want to exit DV/X Shell?", MB_YESNO | MB_ICONQUESTION); + + if (result == ID_YES) { + sCtx->running = false; + } +} + + +static void onPmMenu(WindowT *win, int32_t menuId) { + (void)win; + + switch (menuId) { + case CMD_RUN: + { + FileFilterT filters[] = { + { "DXE Applications (*.dxe)", "*.dxe" }, + { "All Files (*.*)", "*.*" } + }; + char path[MAX_PATH_LEN]; + + if (dvxFileDialog(sCtx, "Run Application", FD_OPEN, "apps", filters, 2, path, sizeof(path))) { + shellLoadApp(sCtx, path); + updateStatusText(); + } + } + break; + + case CMD_EXIT: + onPmClose(sPmWindow); + break; + + case CMD_CASCADE: + dvxCascadeWindows(sCtx); + break; + + case CMD_TILE: + dvxTileWindows(sCtx); + break; + + case CMD_TILE_H: + dvxTileWindowsH(sCtx); + break; + + case CMD_TILE_V: + dvxTileWindowsV(sCtx); + break; + + case CMD_ABOUT: + showAboutDialog(); + break; + + case CMD_TASK_MGR: + buildTaskManager(); + break; + } +} + + +static void onTmClose(WindowT *win) { + sTmListBox = NULL; + sTmWindow = NULL; + dvxDestroyWindow(sCtx, win); +} + + +static void onTmEndTask(WidgetT *w) { + (void)w; + + if (!sTmListBox) { + return; + } + + int32_t sel = wgtListBoxGetSelected(sTmListBox); + + if (sel < 0) { + return; + } + + // Map list index to app ID + int32_t idx = 0; + + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + ShellAppT *app = shellGetApp(i); + + if (app && app->state == AppStateRunningE) { + if (idx == sel) { + shellForceKillApp(sCtx, app); + refreshTaskList(); + updateStatusText(); + return; + } + + idx++; + } + } +} + + +static void onTmSwitchTo(WidgetT *w) { + (void)w; + + if (!sTmListBox) { + return; + } + + int32_t sel = wgtListBoxGetSelected(sTmListBox); + + if (sel < 0) { + return; + } + + // Map list index to app ID, find topmost window for that app + int32_t idx = 0; + + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + ShellAppT *app = shellGetApp(i); + + if (app && app->state == AppStateRunningE) { + if (idx == sel) { + // Find the topmost window for this app + for (int32_t j = sCtx->stack.count - 1; j >= 0; j--) { + WindowT *win = sCtx->stack.windows[j]; + + if (win->appId == i) { + if (win->minimized) { + wmRestoreMinimized(&sCtx->stack, &sCtx->dirty, win); + } + + wmRaiseWindow(&sCtx->stack, &sCtx->dirty, j); + wmSetFocus(&sCtx->stack, &sCtx->dirty, sCtx->stack.count - 1); + return; + } + } + + return; + } + + idx++; + } + } +} + + +static void refreshTaskList(void) { + if (!sTmListBox) { + return; + } + + static const char *items[SHELL_MAX_APPS]; + static char names[SHELL_MAX_APPS][SHELL_APP_NAME_MAX + 16]; + int32_t count = 0; + + for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + ShellAppT *app = shellGetApp(i); + + if (app && app->state == AppStateRunningE) { + const char *state = app->hasMainLoop ? "task" : "callback"; + snprintf(names[count], sizeof(names[count]), "%s [%s]", app->name, state); + items[count] = names[count]; + count++; + } + } + + wgtListBoxSetItems(sTmListBox, items, count); + wgtInvalidatePaint(sTmListBox); +} + + +static void scanAppsDir(void) { + DIR *dir = opendir("apps"); + + if (!dir) { + shellLog("Shell: apps/ directory not found"); + return; + } + + sDxeCount = 0; + struct dirent *ent; + + while ((ent = readdir(dir)) != NULL && sDxeCount < MAX_DXE_FILES) { + int32_t len = strlen(ent->d_name); + + if (len < 5) { + continue; + } + + // Check for .dxe extension (case-insensitive) + const char *ext = ent->d_name + len - 4; + + if (strcmp(ext, ".dxe") != 0 && strcmp(ext, ".DXE") != 0) { + continue; + } + + DxeEntryT *entry = &sDxeFiles[sDxeCount]; + + // Name = filename without extension + int32_t nameLen = len - 4; + + if (nameLen >= SHELL_APP_NAME_MAX) { + nameLen = SHELL_APP_NAME_MAX - 1; + } + + 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), "apps/%s", ent->d_name); + sDxeCount++; + } + + closedir(dir); + shellLog("Shell: found %ld DXE app(s)", (long)sDxeCount); +} + + +static void showAboutDialog(void) { + dvxMessageBox(sCtx, "About DV/X Shell", + "DV/X Shell 1.0\n\nA DESQview/X-style desktop shell for DJGPP/DPMI. Using DXE3 dynamic loading for application modules.", + MB_OK | MB_ICONINFO); +} + + +static void updateStatusText(void) { + if (!sStatusLabel) { + return; + } + + static char buf[64]; + int32_t count = shellRunningAppCount(); + + if (count == 0) { + snprintf(buf, sizeof(buf), "No applications running"); + } else if (count == 1) { + snprintf(buf, sizeof(buf), "1 application running"); + } else { + snprintf(buf, sizeof(buf), "%ld applications running", (long)count); + } + + wgtSetText(sStatusLabel, buf); + wgtInvalidatePaint(sStatusLabel); +} + +// ============================================================ +// Public API +// ============================================================ + +void shellDesktopInit(AppContextT *ctx) { + sCtx = ctx; + scanAppsDir(); + buildPmWindow(); +} + + +void shellDesktopUpdateStatus(void) { + updateStatusText(); + + // Also refresh task manager if open + if (sTmWindow) { + refreshTaskList(); + } +} + + +void shellTaskManager(AppContextT *ctx) { + sCtx = ctx; + buildTaskManager(); +} diff --git a/dvxshell/shellExport.c b/dvxshell/shellExport.c new file mode 100644 index 0000000..efb9837 --- /dev/null +++ b/dvxshell/shellExport.c @@ -0,0 +1,391 @@ +// shellExport.c — DXE export table and wrapper functions for DV/X Shell +// +// Exports all dvx*/wgt*/ts* symbols that DXE apps need. A few functions +// are wrapped for resource tracking (window ownership via appId). + +#include "shellApp.h" +#include "dvxApp.h" +#include "dvxDialog.h" +#include "dvxWidget.h" +#include "dvxDraw.h" +#include "dvxVideo.h" +#include "dvxWm.h" +#include "taskswitch.h" + +#include +#include +#include +#include +#include +#include + +// ============================================================ +// Prototypes +// ============================================================ + +static void shellRegisterExports(void); +static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable); +static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win); + +// ============================================================ +// Wrapper: dvxCreateWindow — stamps win->appId +// ============================================================ + +static WindowT *shellWrapCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t y, int32_t w, int32_t h, bool resizable) { + WindowT *win = dvxCreateWindow(ctx, title, x, y, w, h, resizable); + + if (win) { + win->appId = sCurrentAppId; + } + + return win; +} + + +// ============================================================ +// Wrapper: dvxDestroyWindow — checks for last-window reap +// ============================================================ + +static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) { + int32_t appId = win->appId; + + dvxDestroyWindow(ctx, win); + + // If this was a callback-only app's last window, mark for reaping + if (appId > 0) { + ShellAppT *app = shellGetApp(appId); + + if (app && !app->hasMainLoop && app->state == AppStateRunningE) { + // Check if app still has any windows + bool hasWindows = false; + + for (int32_t i = 0; i < ctx->stack.count; i++) { + if (ctx->stack.windows[i]->appId == appId) { + hasWindows = true; + break; + } + } + + if (!hasWindows) { + app->state = AppStateTerminatingE; + } + } + } +} + + +// ============================================================ +// Export table +// ============================================================ + +// We use extern_asm to get the actual addresses for wrapped functions +// since we export under the original names but point to our wrappers. + +DXE_EXPORT_TABLE(shellExportTable) + // Wrapped functions (exported under original names) + { "_dvxCreateWindow", (void *)shellWrapCreateWindow }, + { "_dvxDestroyWindow", (void *)shellWrapDestroyWindow }, + + // dvxApp.h — direct exports + DXE_EXPORT(dvxInit) + DXE_EXPORT(dvxShutdown) + DXE_EXPORT(dvxUpdate) + DXE_EXPORT(dvxFitWindow) + DXE_EXPORT(dvxInvalidateRect) + DXE_EXPORT(dvxInvalidateWindow) + DXE_EXPORT(dvxMinimizeWindow) + DXE_EXPORT(dvxMaximizeWindow) + DXE_EXPORT(dvxQuit) + DXE_EXPORT(dvxSetTitle) + DXE_EXPORT(dvxGetFont) + DXE_EXPORT(dvxGetColors) + DXE_EXPORT(dvxGetDisplay) + DXE_EXPORT(dvxGetBlitOps) + DXE_EXPORT(dvxSetWindowIcon) + DXE_EXPORT(dvxScreenshot) + DXE_EXPORT(dvxWindowScreenshot) + DXE_EXPORT(dvxCreateAccelTable) + DXE_EXPORT(dvxFreeAccelTable) + DXE_EXPORT(dvxAddAccel) + DXE_EXPORT(dvxCascadeWindows) + DXE_EXPORT(dvxTileWindows) + DXE_EXPORT(dvxTileWindowsH) + DXE_EXPORT(dvxTileWindowsV) + DXE_EXPORT(dvxClipboardCopy) + DXE_EXPORT(dvxClipboardGet) + + // dvxDialog.h + DXE_EXPORT(dvxMessageBox) + DXE_EXPORT(dvxFileDialog) + + // dvxDraw.h + DXE_EXPORT(rectFill) + DXE_EXPORT(rectCopy) + DXE_EXPORT(drawBevel) + DXE_EXPORT(drawChar) + DXE_EXPORT(drawText) + DXE_EXPORT(drawTextN) + DXE_EXPORT(textWidth) + DXE_EXPORT(drawTextAccel) + DXE_EXPORT(textWidthAccel) + DXE_EXPORT(drawFocusRect) + DXE_EXPORT(drawHLine) + DXE_EXPORT(drawVLine) + + // dvxVideo.h + DXE_EXPORT(packColor) + + // dvxWm.h + DXE_EXPORT(wmAddMenuBar) + DXE_EXPORT(wmAddMenu) + DXE_EXPORT(wmAddMenuItem) + DXE_EXPORT(wmAddMenuCheckItem) + DXE_EXPORT(wmAddMenuRadioItem) + DXE_EXPORT(wmAddMenuSeparator) + DXE_EXPORT(wmAddSubMenu) + DXE_EXPORT(wmAddVScrollbar) + DXE_EXPORT(wmAddHScrollbar) + DXE_EXPORT(wmSetTitle) + DXE_EXPORT(wmSetIcon) + DXE_EXPORT(wmCreateMenu) + DXE_EXPORT(wmFreeMenu) + + // dvxWidget.h — window integration + DXE_EXPORT(wgtInitWindow) + + // dvxWidget.h — containers + DXE_EXPORT(wgtVBox) + DXE_EXPORT(wgtHBox) + DXE_EXPORT(wgtFrame) + + // dvxWidget.h — basic widgets + DXE_EXPORT(wgtLabel) + DXE_EXPORT(wgtButton) + DXE_EXPORT(wgtCheckbox) + DXE_EXPORT(wgtTextInput) + DXE_EXPORT(wgtPasswordInput) + DXE_EXPORT(wgtMaskedInput) + + // dvxWidget.h — radio buttons + DXE_EXPORT(wgtRadioGroup) + DXE_EXPORT(wgtRadio) + + // dvxWidget.h — spacing + DXE_EXPORT(wgtSpacer) + DXE_EXPORT(wgtHSeparator) + DXE_EXPORT(wgtVSeparator) + + // dvxWidget.h — complex widgets + DXE_EXPORT(wgtListBox) + DXE_EXPORT(wgtTextArea) + + // dvxWidget.h — dropdown/combo + DXE_EXPORT(wgtDropdown) + DXE_EXPORT(wgtDropdownSetItems) + DXE_EXPORT(wgtDropdownGetSelected) + DXE_EXPORT(wgtDropdownSetSelected) + DXE_EXPORT(wgtComboBox) + DXE_EXPORT(wgtComboBoxSetItems) + DXE_EXPORT(wgtComboBoxGetSelected) + DXE_EXPORT(wgtComboBoxSetSelected) + + // dvxWidget.h — progress bar + DXE_EXPORT(wgtProgressBar) + DXE_EXPORT(wgtProgressBarV) + DXE_EXPORT(wgtProgressBarSetValue) + DXE_EXPORT(wgtProgressBarGetValue) + + // dvxWidget.h — slider + DXE_EXPORT(wgtSlider) + DXE_EXPORT(wgtSliderSetValue) + DXE_EXPORT(wgtSliderGetValue) + + // dvxWidget.h — spinner + DXE_EXPORT(wgtSpinner) + DXE_EXPORT(wgtSpinnerSetValue) + DXE_EXPORT(wgtSpinnerGetValue) + DXE_EXPORT(wgtSpinnerSetRange) + DXE_EXPORT(wgtSpinnerSetStep) + + // dvxWidget.h — tab control + DXE_EXPORT(wgtTabControl) + DXE_EXPORT(wgtTabPage) + DXE_EXPORT(wgtTabControlSetActive) + DXE_EXPORT(wgtTabControlGetActive) + + // dvxWidget.h — status bar / toolbar + DXE_EXPORT(wgtStatusBar) + DXE_EXPORT(wgtToolbar) + + // dvxWidget.h — tree view + DXE_EXPORT(wgtTreeView) + DXE_EXPORT(wgtTreeViewGetSelected) + DXE_EXPORT(wgtTreeViewSetSelected) + DXE_EXPORT(wgtTreeViewSetMultiSelect) + DXE_EXPORT(wgtTreeViewSetReorderable) + DXE_EXPORT(wgtTreeItem) + DXE_EXPORT(wgtTreeItemSetExpanded) + DXE_EXPORT(wgtTreeItemIsExpanded) + DXE_EXPORT(wgtTreeItemIsSelected) + DXE_EXPORT(wgtTreeItemSetSelected) + + // dvxWidget.h — list view + DXE_EXPORT(wgtListView) + DXE_EXPORT(wgtListViewSetColumns) + DXE_EXPORT(wgtListViewSetData) + DXE_EXPORT(wgtListViewGetSelected) + DXE_EXPORT(wgtListViewSetSelected) + DXE_EXPORT(wgtListViewSetSort) + DXE_EXPORT(wgtListViewSetHeaderClickCallback) + DXE_EXPORT(wgtListViewSetMultiSelect) + DXE_EXPORT(wgtListViewIsItemSelected) + DXE_EXPORT(wgtListViewSetItemSelected) + DXE_EXPORT(wgtListViewSelectAll) + DXE_EXPORT(wgtListViewClearSelection) + DXE_EXPORT(wgtListViewSetReorderable) + + // dvxWidget.h — scroll pane / splitter + DXE_EXPORT(wgtScrollPane) + DXE_EXPORT(wgtSplitter) + DXE_EXPORT(wgtSplitterSetPos) + DXE_EXPORT(wgtSplitterGetPos) + + // dvxWidget.h — image / image button + DXE_EXPORT(wgtImageButton) + DXE_EXPORT(wgtImageButtonSetData) + DXE_EXPORT(wgtImage) + DXE_EXPORT(wgtImageFromFile) + DXE_EXPORT(wgtImageSetData) + + // dvxWidget.h — canvas + DXE_EXPORT(wgtCanvas) + DXE_EXPORT(wgtCanvasClear) + DXE_EXPORT(wgtCanvasSetPenColor) + DXE_EXPORT(wgtCanvasSetPenSize) + DXE_EXPORT(wgtCanvasSave) + DXE_EXPORT(wgtCanvasLoad) + DXE_EXPORT(wgtCanvasDrawLine) + DXE_EXPORT(wgtCanvasDrawRect) + DXE_EXPORT(wgtCanvasFillRect) + DXE_EXPORT(wgtCanvasFillCircle) + DXE_EXPORT(wgtCanvasSetPixel) + DXE_EXPORT(wgtCanvasGetPixel) + + // dvxWidget.h — ANSI terminal + DXE_EXPORT(wgtAnsiTerm) + DXE_EXPORT(wgtAnsiTermWrite) + DXE_EXPORT(wgtAnsiTermClear) + DXE_EXPORT(wgtAnsiTermSetComm) + DXE_EXPORT(wgtAnsiTermSetScrollback) + DXE_EXPORT(wgtAnsiTermPoll) + DXE_EXPORT(wgtAnsiTermRepaint) + + // dvxWidget.h — operations + DXE_EXPORT(wgtInvalidate) + DXE_EXPORT(wgtInvalidatePaint) + DXE_EXPORT(wgtSetText) + DXE_EXPORT(wgtGetText) + DXE_EXPORT(wgtSetEnabled) + DXE_EXPORT(wgtSetVisible) + DXE_EXPORT(wgtSetName) + DXE_EXPORT(wgtFind) + DXE_EXPORT(wgtDestroy) + + // dvxWidget.h — list box ops + DXE_EXPORT(wgtListBoxSetItems) + DXE_EXPORT(wgtListBoxGetSelected) + DXE_EXPORT(wgtListBoxSetSelected) + DXE_EXPORT(wgtListBoxSetMultiSelect) + DXE_EXPORT(wgtListBoxIsItemSelected) + DXE_EXPORT(wgtListBoxSetItemSelected) + DXE_EXPORT(wgtListBoxSelectAll) + DXE_EXPORT(wgtListBoxClearSelection) + DXE_EXPORT(wgtListBoxSetReorderable) + + // dvxWidget.h — layout + DXE_EXPORT(wgtResolveSize) + DXE_EXPORT(wgtLayout) + DXE_EXPORT(wgtPaint) + + // taskswitch.h + DXE_EXPORT(tsYield) + DXE_EXPORT(tsCurrentId) + DXE_EXPORT(tsActiveCount) + + // Shell logging + DXE_EXPORT(shellLog) + + // libc — memory + DXE_EXPORT(malloc) + DXE_EXPORT(free) + DXE_EXPORT(calloc) + DXE_EXPORT(realloc) + + // libc — string + DXE_EXPORT(memcpy) + DXE_EXPORT(memset) + DXE_EXPORT(memmove) + DXE_EXPORT(memcmp) + DXE_EXPORT(strlen) + DXE_EXPORT(strcmp) + DXE_EXPORT(strncmp) + DXE_EXPORT(strcpy) + DXE_EXPORT(strncpy) + DXE_EXPORT(strcat) + DXE_EXPORT(strncat) + DXE_EXPORT(strchr) + DXE_EXPORT(strrchr) + DXE_EXPORT(strstr) + DXE_EXPORT(strtol) + + // libc — I/O + DXE_EXPORT(printf) + DXE_EXPORT(fprintf) + DXE_EXPORT(sprintf) + DXE_EXPORT(snprintf) + DXE_EXPORT(puts) + DXE_EXPORT(fopen) + DXE_EXPORT(fclose) + DXE_EXPORT(fread) + DXE_EXPORT(fwrite) + DXE_EXPORT(fgets) + DXE_EXPORT(fseek) + DXE_EXPORT(ftell) + DXE_EXPORT(feof) + DXE_EXPORT(ferror) + + // libc — math + DXE_EXPORT(sin) + DXE_EXPORT(cos) + DXE_EXPORT(sqrt) + + // libc — time + DXE_EXPORT(clock) + DXE_EXPORT(time) + DXE_EXPORT(localtime) + + // libc — misc + DXE_EXPORT(qsort) + DXE_EXPORT(rand) + DXE_EXPORT(srand) + DXE_EXPORT(abs) + DXE_EXPORT(atoi) +DXE_EXPORT_END + + +// ============================================================ +// shellRegisterExports +// ============================================================ + +static void shellRegisterExports(void) { + dlregsym(shellExportTable); +} + + +// ============================================================ +// Public init function +// ============================================================ + +void shellExportInit(void) { + shellRegisterExports(); +} diff --git a/dvxshell/shellMain.c b/dvxshell/shellMain.c new file mode 100644 index 0000000..1ea4122 --- /dev/null +++ b/dvxshell/shellMain.c @@ -0,0 +1,247 @@ +// shellMain.c — DV/X Shell entry point and main loop +// +// Initializes the GUI, task system, DXE export table, and Program Manager. +// Runs the cooperative main loop, yielding to app tasks and reaping +// terminated apps each frame. + +#include "shellApp.h" +#include "dvxDialog.h" + +#include +#include +#include +#include +#include +#include + +// ============================================================ +// Module state +// ============================================================ + +static AppContextT sCtx; +static jmp_buf sCrashJmp; +static volatile int sCrashSignal = 0; +static FILE *sLogFile = NULL; + +// ============================================================ +// Prototypes +// ============================================================ + +static void crashHandler(int sig); +static void idleYield(void *ctx); +static void installCrashHandler(void); +static void logCrash(int sig); + +// ============================================================ +// crashHandler — catch page faults and other fatal signals +// ============================================================ + +static void crashHandler(int sig) { + logCrash(sig); + + // Re-install handler (DJGPP resets to SIG_DFL after delivery) + signal(sig, crashHandler); + + sCrashSignal = sig; + longjmp(sCrashJmp, 1); +} + + +// ============================================================ +// idleYield — called when no dirty rects need compositing +// ============================================================ + +static void idleYield(void *ctx) { + (void)ctx; + + if (tsActiveCount() > 1) { + tsYield(); + } +} + + +// ============================================================ +// installCrashHandler +// ============================================================ + +static void installCrashHandler(void) { + signal(SIGSEGV, crashHandler); + signal(SIGFPE, crashHandler); + signal(SIGILL, crashHandler); +} + + +// ============================================================ +// logCrash — write exception details to the log +// ============================================================ + +static void logCrash(int sig) { + const char *sigName = "UNKNOWN"; + + if (sig == SIGSEGV) { + sigName = "SIGSEGV (page fault)"; + } else if (sig == SIGFPE) { + sigName = "SIGFPE (floating point exception)"; + } else if (sig == SIGILL) { + sigName = "SIGILL (illegal instruction)"; + } + + shellLog("=== CRASH ==="); + shellLog("Signal: %d (%s)", sig, sigName); + shellLog("Current app ID: %ld", (long)sCurrentAppId); + + if (sCurrentAppId > 0) { + ShellAppT *app = shellGetApp(sCurrentAppId); + + if (app) { + shellLog("App name: %s", app->name); + shellLog("App path: %s", app->path); + shellLog("Has main loop: %s", app->hasMainLoop ? "yes" : "no"); + shellLog("Task ID: %lu", (unsigned long)app->mainTaskId); + } + } else { + shellLog("Crashed in shell (task 0)"); + } + + // Dump CPU registers from exception state + jmp_buf *estate = __djgpp_exception_state_ptr; + + if (estate) { + struct __jmp_buf *regs = &(*estate)[0]; + shellLog("EIP: 0x%08lx CS: 0x%04x", regs->__eip, regs->__cs); + shellLog("EAX: 0x%08lx EBX: 0x%08lx ECX: 0x%08lx EDX: 0x%08lx", regs->__eax, regs->__ebx, regs->__ecx, regs->__edx); + shellLog("ESI: 0x%08lx EDI: 0x%08lx EBP: 0x%08lx ESP: 0x%08lx", regs->__esi, regs->__edi, regs->__ebp, regs->__esp); + shellLog("DS: 0x%04x ES: 0x%04x FS: 0x%04x GS: 0x%04x SS: 0x%04x", regs->__ds, regs->__es, regs->__fs, regs->__gs, regs->__ss); + shellLog("EFLAGS: 0x%08lx", regs->__eflags); + } +} + + +// ============================================================ +// shellLog — append a line to SHELL.LOG +// ============================================================ + +void shellLog(const char *fmt, ...) { + if (!sLogFile) { + return; + } + + va_list ap; + va_start(ap, fmt); + vfprintf(sLogFile, fmt, ap); + va_end(ap); + + fprintf(sLogFile, "\n"); + fflush(sLogFile); +} + + +// ============================================================ +// main +// ============================================================ + +int main(void) { + sLogFile = fopen("shell.log", "w"); + shellLog("DV/X Shell starting..."); + + // Initialize GUI + int32_t result = dvxInit(&sCtx, 640, 480, 32); + + if (result != 0) { + shellLog("Failed to initialize DV/X GUI (error %ld)", (long)result); + + if (sLogFile) { + fclose(sLogFile); + } + + return 1; + } + + // Initialize task system + if (tsInit() != TS_OK) { + shellLog("Failed to initialize task system"); + dvxShutdown(&sCtx); + + if (sLogFile) { + fclose(sLogFile); + } + + return 1; + } + + // Shell task (task 0) gets high priority for responsive UI + tsSetPriority(0, TS_PRIORITY_HIGH); + + // Register DXE export table + shellExportInit(); + + // Initialize app slot table + shellAppInit(); + + // Set up idle callback for cooperative yielding + sCtx.idleCallback = idleYield; + sCtx.idleCtx = &sCtx; + + // Create the Program Manager window + shellDesktopInit(&sCtx); + + // Install crash handler after everything is initialized + installCrashHandler(); + + shellLog("DV/X Shell ready."); + + // Set recovery point for crash handler + if (setjmp(sCrashJmp) != 0) { + // Returned here from crash handler via longjmp. + // If the crash was in a non-main task, the task switcher still + // thinks that task is running. Fix it before doing anything else. + tsRecoverToMain(); + + shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId); + + if (sCurrentAppId > 0) { + ShellAppT *app = shellGetApp(sCurrentAppId); + + if (app) { + shellForceKillApp(&sCtx, app); + } + } + + sCurrentAppId = 0; + sCrashSignal = 0; + shellDesktopUpdateStatus(); + } + + // Main loop + while (sCtx.running) { + dvxUpdate(&sCtx); + + // Give app tasks CPU time even during active frames + if (tsActiveCount() > 1) { + tsYield(); + } + + // Reap terminated apps + shellReapApps(&sCtx); + + // Update status display + shellDesktopUpdateStatus(); + } + + shellLog("DV/X Shell shutting down..."); + + // Clean shutdown: terminate all apps + shellTerminateAllApps(&sCtx); + + tsShutdown(); + dvxShutdown(&sCtx); + + shellLog("DV/X Shell exited."); + + if (sLogFile) { + fclose(sLogFile); + sLogFile = NULL; + } + + return 0; +} diff --git a/tasks/taskswitch.c b/tasks/taskswitch.c index 5ec5165..e1883aa 100644 --- a/tasks/taskswitch.c +++ b/tasks/taskswitch.c @@ -85,7 +85,9 @@ const char *tsGetName(uint32_t taskId); int32_t tsGetPriority(uint32_t taskId); TaskStateE tsGetState(uint32_t taskId); int32_t tsInit(void); +int32_t tsKill(uint32_t taskId); int32_t tsPause(uint32_t taskId); +void tsRecoverToMain(void); int32_t tsResume(uint32_t taskId); int32_t tsSetPriority(uint32_t taskId, int32_t priority); void tsShutdown(void); @@ -393,6 +395,32 @@ int32_t tsInit(void) { } +int32_t tsKill(uint32_t taskId) { + if (!initialized || taskId >= (uint32_t)arrlen(tasks)) { + return TS_ERR_PARAM; + } + if (!tasks[taskId].allocated) { + return TS_ERR_PARAM; + } + if (tasks[taskId].isMain) { + return TS_ERR_STATE; + } + if (taskId == currentIdx) { + return TS_ERR_STATE; + } + if (tasks[taskId].state == TaskStateTerminated) { + return TS_ERR_STATE; + } + + tasks[taskId].state = TaskStateTerminated; + free(tasks[taskId].stack); + tasks[taskId].stack = NULL; + tasks[taskId].allocated = false; + + return TS_OK; +} + + int32_t tsPause(uint32_t taskId) { if (!initialized || taskId >= (uint32_t)arrlen(tasks)) { return TS_ERR_PARAM; @@ -426,6 +454,16 @@ int32_t tsPause(uint32_t taskId) { } +void tsRecoverToMain(void) { + if (!initialized) { + return; + } + + currentIdx = 0; + tasks[0].state = TaskStateRunning; +} + + int32_t tsResume(uint32_t taskId) { if (!initialized || taskId >= (uint32_t)arrlen(tasks)) { return TS_ERR_PARAM; diff --git a/tasks/taskswitch.h b/tasks/taskswitch.h index 67607dc..902c521 100644 --- a/tasks/taskswitch.h +++ b/tasks/taskswitch.h @@ -21,7 +21,7 @@ #define TS_ERR_STATE (-5) // Defaults -#define TS_DEFAULT_STACK_SIZE 8192 +#define TS_DEFAULT_STACK_SIZE 32768 #define TS_NAME_MAX 32 // Priority levels @@ -79,6 +79,14 @@ const char *tsGetName(uint32_t taskId); // Terminate the calling task. Must not be called from the main task. void tsExit(void); +// Forcibly terminate another task. Cannot kill main task (id 0) or self. +int32_t tsKill(uint32_t taskId); + +// Crash recovery: force scheduler back to main task (id 0). +// Call after longjmp from a signal handler that fired in a non-main task. +// The crashed task is NOT cleaned up — call tsKill() afterward. +void tsRecoverToMain(void); + // Get the number of non-terminated tasks. uint32_t tsActiveCount(void);