// progman.c -- Program Manager application for DVX Shell // // Displays a grid of available apps from the apps/ directory. // Double-click or Enter launches an app. Includes Task Manager (Ctrl+Esc). // // DXE App Contract: // This is a callback-only DXE app (hasMainLoop = false). It exports two // symbols: appDescriptor (metadata) and appMain (entry point). The shell // calls appMain once; we create windows, register callbacks, and return 0. // From that point on, the shell's event loop drives everything through // our window callbacks (onClose, onMenu, widget onClick, etc.). // // Because we have no main loop, we don't need a dedicated task stack. // The shell runs our callbacks in task 0 during dvxUpdate(). // // Progman is special: it's the desktop app. It calls // shellRegisterDesktopUpdate() so the shell notifies us whenever an app // is loaded, reaped, or crashes, keeping our status bar and Task Manager // list current without polling. #include "dvxApp.h" #include "dvxDialog.h" #include "dvxPrefs.h" #include "dvxWidget.h" #include "widgetBox.h" #include "widgetButton.h" #include "widgetLabel.h" #include "widgetStatusBar.h" #include "widgetTextInput.h" #include "dvxWm.h" #include "dvxPlatform.h" #include "shellApp.h" #include "shellInfo.h" #include "dvxResource.h" #include "widgetImageButton.h" #include "widgetScrollPane.h" #include "widgetWrapBox.h" #include #include #include #include #include #include #include #include "dvxMem.h" #include "stb_ds_wrap.h" // ============================================================ // Constants // ============================================================ // 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_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 #define PM_SYSINFO_WIN_W 400 #define PM_SYSINFO_WIN_H 360 // 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_MIN_ON_RUN 104 #define CMD_RESTORE_ALONE 105 #define CMD_ABOUT 300 #define CMD_TASK_MGR 301 #define CMD_SYSINFO 302 // ============================================================ // Module state // ============================================================ // Each discovered .app file in the apps/ directory tree typedef struct { 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 // each DXE is a separate shared object with its own data segment. No risk // of collision between apps even though the names look global. static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; static WindowT *sPmWindow = NULL; static WidgetT *sStatusLabel = NULL; static PrefsHandleT *sPrefs = NULL; static bool sMinOnRun = false; static bool sRestoreAlone = false; static AppEntryT *sAppFiles = NULL; // stb_ds dynamic array static int32_t sAppCount = 0; // ============================================================ // Prototypes // ============================================================ int32_t appMain(DxeAppContextT *ctx); static void buildPmWindow(void); static void desktopUpdate(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 scanAppsDirRecurse(const char *dirPath); static void showAboutDialog(void); static void showSystemInfo(void); static void updateStatusText(void); // ============================================================ // App descriptor // ============================================================ // The shell reads this exported symbol to determine how to manage the app. // hasMainLoop = false means the shell won't create a dedicated task; our // appMain runs to completion and all subsequent work happens via callbacks. // stackSize = 0 means "use the shell default" (irrelevant for callback apps). AppDescriptorT appDescriptor = { .name = "Program Manager", .hasMainLoop = false, .stackSize = SHELL_STACK_DEFAULT, .priority = TS_PRIORITY_NORMAL }; // ============================================================ // Static functions (alphabetical) // ============================================================ // Build the main Program Manager window with app buttons, menus, and status bar. // Window is centered horizontally and placed in the upper quarter vertically // so spawned app windows don't hide behind it. static void buildPmWindow(void) { int32_t screenW = sAc->display.width; int32_t screenH = sAc->display.height; int32_t winW = PM_WIN_W; int32_t winH = PM_WIN_H; int32_t winX = (screenW - winW) / 2; int32_t winY = (screenH - winH) / 4; sPmWindow = dvxCreateWindow(sAc, "Program Manager", winX, winY, winW, winH, true); if (!sPmWindow) { return; } 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 DVX", CMD_EXIT); MenuT *optMenu = wmAddMenu(menuBar, "&Options"); wmAddMenuCheckItem(optMenu, "&Minimize on Run", CMD_MIN_ON_RUN, sMinOnRun); wmAddMenuCheckItem(optMenu, "&Restore when Alone", CMD_RESTORE_ALONE, sRestoreAlone); 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 DVX...", CMD_ABOUT); wmAddMenuItem(helpMenu, "&System Information...", CMD_SYSINFO); wmAddMenuSeparator(helpMenu); wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR); // wgtInitWindow creates the root VBox widget that the window's content // area maps to. All child widgets are added to this root. WidgetT *root = wgtInitWindow(sAc, sPmWindow); // App button grid in a labeled frame. weight=100 tells the layout engine // this frame should consume all available vertical space (flex weight). WidgetT *appFrame = wgtFrame(root, "Applications"); appFrame->weight = 100; if (sAppCount == 0) { wgtLabel(appFrame, "(No applications found in apps/ directory)"); } else { // ScrollPane provides scrollbars when icons overflow. // WrapBox inside flows cells left-to-right, wrapping to // the next row when the window width is exceeded. WidgetT *scroll = wgtScrollPane(appFrame); scroll->weight = 100; wgtScrollPaneSetNoBorder(scroll, true); WidgetT *wrap = wgtWrapBox(scroll); wrap->weight = 100; wrap->spacing = wgtPixels(PM_GRID_SPACING); wrap->align = AlignCenterE; for (int32_t i = 0; i < sAppCount; i++) { WidgetT *cell = wgtVBox(wrap); 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->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); } } // Status bar at bottom; weight=100 on the label makes it fill the bar // width so text can be left-aligned naturally. WidgetT *statusBar = wgtStatusBar(root); sStatusLabel = wgtLabel(statusBar, ""); sStatusLabel->weight = 100; updateStatusText(); } // Shell calls this via shellRegisterDesktopUpdate whenever an app is loaded, // reaped, or crashes. We refresh the running count in the status bar. // (Task Manager refresh is handled by the shell's shellDesktopUpdate.) static void desktopUpdate(void) { updateStatusText(); // Auto-restore if we're the only running app and minimized if (sRestoreAlone && sPmWindow && sPmWindow->minimized) { if (shellRunningAppCount() <= 1) { wmRestoreMinimized(&sAc->stack, &sAc->dirty, &sAc->display, sPmWindow); } } } // Widget click handler for app grid buttons. userData was set to the // AppEntryT pointer during window construction, giving us the .app path. static void onAppButtonClick(WidgetT *w) { AppEntryT *entry = (AppEntryT *)w->userData; if (!entry) { return; } shellLoadApp(sAc, entry->path); updateStatusText(); if (sMinOnRun && sPmWindow) { dvxMinimizeWindow(sAc, sPmWindow); } } // Closing the Program Manager is equivalent to shutting down the entire shell. // dvxQuit() signals the main event loop to exit, which triggers // shellTerminateAllApps() to gracefully tear down all loaded DXEs. static void onPmClose(WindowT *win) { (void)win; int32_t result = dvxMessageBox(sAc, "Exit DVX", "Are you sure you want to exit DVX?", MB_YESNO | MB_ICONQUESTION); if (result == ID_YES) { dvxQuit(sAc); } } static void onPmMenu(WindowT *win, int32_t menuId) { (void)win; switch (menuId) { case CMD_RUN: { FileFilterT filters[] = { { "Applications (*.app)", "*.app" }, { "All Files (*.*)", "*.*" } }; char path[MAX_PATH_LEN]; if (dvxFileDialog(sAc, "Run Application", FD_OPEN, "apps", filters, 2, path, sizeof(path))) { shellLoadApp(sAc, path); updateStatusText(); if (sMinOnRun && sPmWindow) { dvxMinimizeWindow(sAc, sPmWindow); } } } break; case CMD_EXIT: onPmClose(sPmWindow); break; case CMD_CASCADE: dvxCascadeWindows(sAc); break; case CMD_TILE: dvxTileWindows(sAc); break; case CMD_TILE_H: dvxTileWindowsH(sAc); break; case CMD_TILE_V: dvxTileWindowsV(sAc); break; case CMD_MIN_ON_RUN: sMinOnRun = !sMinOnRun; shellEnsureConfigDir(sCtx); prefsSetBool(sPrefs, "options", "minimizeOnRun", sMinOnRun); prefsSave(sPrefs); break; case CMD_RESTORE_ALONE: sRestoreAlone = !sRestoreAlone; shellEnsureConfigDir(sCtx); prefsSetBool(sPrefs, "options", "restoreAlone", sRestoreAlone); prefsSave(sPrefs); break; case CMD_ABOUT: showAboutDialog(); break; case CMD_SYSINFO: showSystemInfo(); break; case CMD_TASK_MGR: if (shellCtrlEscFn) { shellCtrlEscFn(sAc); } break; } } // Top-level scan entry point. Recursively walks apps/ looking for .app files. // The apps/ path is relative to the working directory, which the shell sets // to the root of the DVX install before loading any apps. static void scanAppsDir(void) { // Free icons from previous scan for (int32_t i = 0; i < sAppCount; i++) { free(sAppFiles[i].iconData); } arrfree(sAppFiles); sAppFiles = NULL; sAppCount = 0; scanAppsDirRecurse("apps"); dvxLog("Progman: found %ld app(s)", (long)sAppCount); } // Recursive directory walker. Subdirectories under apps/ allow organizing // apps (e.g., apps/games/, apps/tools/). Each .app file is a DXE3 shared // object that the shell can dlopen(). We skip progman.app to avoid listing // ourselves in the launcher grid. static void scanAppsDirRecurse(const char *dirPath) { DIR *dir = opendir(dirPath); if (!dir) { if (sAppCount == 0) { dvxLog("Progman: %s directory not found", dirPath); } return; } struct dirent *ent; while ((ent = readdir(dir)) != NULL) { // Skip . and .. if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { continue; } char fullPath[MAX_PATH_LEN]; snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, ent->d_name); // Check if this is a directory -- recurse into it struct stat st; if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) { scanAppsDirRecurse(fullPath); continue; } int32_t len = strlen(ent->d_name); if (len < 5) { continue; } // Check for .app extension (case-insensitive) const char *ext = ent->d_name + len - 4; if (strcasecmp(ext, ".app") != 0) { continue; } // Skip ourselves if (strcasecmp(ent->d_name, "progman.app") == 0) { continue; } AppEntryT newEntry; memset(&newEntry, 0, sizeof(newEntry)); snprintf(newEntry.path, sizeof(newEntry.path), "%s", fullPath); // Default name from filename (without .app extension) int32_t nameLen = len - 4; if (nameLen >= SHELL_APP_NAME_MAX) { nameLen = SHELL_APP_NAME_MAX - 1; } memcpy(newEntry.name, ent->d_name, nameLen); newEntry.name[nameLen] = '\0'; if (newEntry.name[0] >= 'a' && newEntry.name[0] <= 'z') { newEntry.name[0] -= 32; } // Override from embedded resources if available newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch); if (!newEntry.iconData) { dvxLog("Progman: no icon32 resource in %s", ent->d_name); } dvxResLoadText(fullPath, "name", newEntry.name, SHELL_APP_NAME_MAX); dvxResLoadText(fullPath, "description", newEntry.tooltip, sizeof(newEntry.tooltip)); dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, ent->d_name, newEntry.iconData ? "yes" : "no"); arrput(sAppFiles, newEntry); sAppCount = (int32_t)arrlen(sAppFiles); dvxUpdate(sAc); } closedir(dir); } static void showAboutDialog(void) { dvxMessageBox(sAc, "About DVX", "DVX 1.0 - \"DOS Visual eXecutive\" GUI System.\n\n\"We have Windows at home.\"\n\nCopyright 2026 Scott Duensing\nKangaroo Punch Studios\nhttps://kangaroopunch.com", MB_OK | MB_ICONINFO); } static void showSystemInfo(void) { const char *info = shellGetSystemInfo(); if (!info || !info[0]) { dvxMessageBox(sAc, "System Information", "No system information available.", MB_OK | MB_ICONINFO); return; } // Create a window with a read-only text area int32_t screenW = sAc->display.width; int32_t screenH = sAc->display.height; int32_t winW = PM_SYSINFO_WIN_W; int32_t winH = PM_SYSINFO_WIN_H; int32_t winX = (screenW - winW) / 2; int32_t winY = (screenH - winH) / 4; WindowT *win = dvxCreateWindow(sAc, "System Information", winX, winY, winW, winH, true); if (!win) { return; } WidgetT *root = wgtInitWindow(sAc, win); WidgetT *ta = wgtTextArea(root, 4096); ta->weight = 100; wgtSetText(ta, info); // Don't disable -- wgtSetEnabled(false) blocks all input including scrollbar wgtSetReadOnly(ta, true); } 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); } // ============================================================ // Entry point // ============================================================ // The shell calls appMain exactly once after dlopen() resolves our symbols. // We scan for apps, build the UI, register our desktop update callback, then // return 0 (success). From here on the shell drives us through callbacks. // Returning non-zero would signal a load failure and the shell would unload us. int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; // Load saved preferences char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, "progman.ini", prefsPath, sizeof(prefsPath)); sPrefs = prefsLoad(prefsPath); sMinOnRun = prefsGetBool(sPrefs, "options", "minimizeOnRun", false); sRestoreAlone = prefsGetBool(sPrefs, "options", "restoreAlone", false); scanAppsDir(); buildPmWindow(); // Register for state change notifications from the shell so our status // bar and Task Manager stay current without polling shellRegisterDesktopUpdate(desktopUpdate); return 0; }