DVX_GUI/apps/progman/progman.c

724 lines
22 KiB
C

// 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 "dvxWidget.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "shellInfo.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
// ============================================================
// Constants
// ============================================================
// 64 entries is generous; limited by screen real estate before this cap
#define MAX_APP_FILES 64
// 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_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_MIN_ON_RUN 104
#define CMD_ABOUT 300
#define CMD_TASK_MGR 301
#define CMD_SYSINFO 302
// Task Manager column count
#define TM_COL_COUNT 4
// ============================================================
// Module state
// ============================================================
// 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
} 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 int32_t sMyAppId = 0;
static WindowT *sPmWindow = NULL;
static WidgetT *sStatusLabel = NULL;
static bool sMinOnRun = false;
static AppEntryT sAppFiles[MAX_APP_FILES];
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);
// Task Manager
static WindowT *sTmWindow = NULL;
static WidgetT *sTmListView = NULL;
static void buildTaskManager(void);
static void onTmClose(WindowT *win);
static void onTmEndTask(WidgetT *w);
static void onTmSwitchTo(WidgetT *w);
static void refreshTaskList(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 = 440;
int32_t winH = 340;
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 Shell", CMD_EXIT);
MenuT *optMenu = wmAddMenu(menuBar, "&Options");
wmAddMenuCheckItem(optMenu, "&Minimize on Run", CMD_MIN_ON_RUN, false);
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 Shell...", 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) {
WidgetT *lbl = wgtLabel(appFrame, "(No applications found in apps/ directory)");
(void)lbl;
} 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.
int32_t row = 0;
WidgetT *hbox = NULL;
for (int32_t i = 0; i < sAppCount; i++) {
if (i % PM_GRID_COLS == 0) {
hbox = wgtHBox(appFrame);
hbox->spacing = wgtPixels(8);
row++;
}
WidgetT *btn = wgtButton(hbox, 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;
}
(void)row;
}
// 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();
// dvxFitWindow sizes the window to tightly fit the widget tree,
// honoring preferred sizes. Without this, the window would use the
// initial dimensions from dvxCreateWindow even if widgets don't fit.
dvxFitWindow(sAc, sPmWindow);
}
// Build or raise the Task Manager window. Singleton pattern: if sTmWindow is
// already live, we just raise it to the top of the Z-order instead of
// creating a duplicate, mimicking Windows 3.x Task Manager behavior.
static void buildTaskManager(void) {
if (sTmWindow) {
// Already open — find it in the window stack and bring to front
for (int32_t i = 0; i < sAc->stack.count; i++) {
if (sAc->stack.windows[i] == sTmWindow) {
wmRaiseWindow(&sAc->stack, &sAc->dirty, i);
wmSetFocus(&sAc->stack, &sAc->dirty, sAc->stack.count - 1);
break;
}
}
return;
}
int32_t screenW = sAc->display.width;
int32_t screenH = sAc->display.height;
int32_t winW = 420;
int32_t winH = 280;
int32_t winX = (screenW - winW) / 2;
int32_t winY = (screenH - winH) / 3;
sTmWindow = dvxCreateWindow(sAc, "Task Manager", winX, winY, winW, winH, true);
if (!sTmWindow) {
return;
}
sTmWindow->onClose = onTmClose;
WidgetT *root = wgtInitWindow(sAc, sTmWindow);
// ListView with Name (descriptor), File (basename), Type, Status columns.
ListViewColT tmCols[TM_COL_COUNT];
tmCols[0].title = "Name";
tmCols[0].width = wgtPercent(35);
tmCols[0].align = ListViewAlignLeftE;
tmCols[1].title = "File";
tmCols[1].width = wgtPercent(30);
tmCols[1].align = ListViewAlignLeftE;
tmCols[2].title = "Type";
tmCols[2].width = wgtPercent(17);
tmCols[2].align = ListViewAlignLeftE;
tmCols[3].title = "Status";
tmCols[3].width = wgtPercent(18);
tmCols[3].align = ListViewAlignLeftE;
sTmListView = wgtListView(root);
sTmListView->weight = 100;
sTmListView->prefH = wgtPixels(160);
wgtListViewSetColumns(sTmListView, tmCols, TM_COL_COUNT);
// Button row right-aligned (AlignEndE) to follow Windows UI convention
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(sAc, sTmWindow);
}
// Shell calls this via shellRegisterDesktopUpdate whenever an app is loaded,
// reaped, or crashes. We refresh the running count and Task Manager list.
static void desktopUpdate(void) {
updateStatusText();
if (sTmWindow) {
refreshTaskList();
}
}
// 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 Shell", "Are you sure you want to exit DVX Shell?", 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;
break;
case CMD_ABOUT:
showAboutDialog();
break;
case CMD_SYSINFO:
showSystemInfo();
break;
case CMD_TASK_MGR:
buildTaskManager();
break;
}
}
// Null the static pointers before destroying so buildTaskManager() knows
// the window is gone and will create a fresh one next time.
static void onTmClose(WindowT *win) {
sTmListView = NULL;
sTmWindow = NULL;
dvxDestroyWindow(sAc, win);
}
static void onTmEndTask(WidgetT *w) {
(void)w;
if (!sTmListView) {
return;
}
int32_t sel = wgtListViewGetSelected(sTmListView);
if (sel < 0) {
return;
}
// The list view rows don't carry app IDs directly, so we re-walk the
// app slot table in the same order as refreshTaskList() to map the
// selected row index back to the correct ShellAppT. We skip our own
// appId so progman can't kill itself.
int32_t idx = 0;
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
if (i == sMyAppId) {
continue;
}
ShellAppT *app = shellGetApp(i);
if (app && app->state == AppStateRunningE) {
if (idx == sel) {
shellForceKillApp(sAc, app);
refreshTaskList();
updateStatusText();
return;
}
idx++;
}
}
}
static void onTmSwitchTo(WidgetT *w) {
(void)w;
if (!sTmListView) {
return;
}
int32_t sel = wgtListViewGetSelected(sTmListView);
if (sel < 0) {
return;
}
// Same index-to-appId mapping as onTmEndTask. We scan the window
// stack top-down (highest Z first) to find the app's topmost window,
// restore it if minimized, then raise and focus it.
int32_t idx = 0;
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
if (i == sMyAppId) {
continue;
}
ShellAppT *app = shellGetApp(i);
if (app && app->state == AppStateRunningE) {
if (idx == sel) {
// Find the topmost window for this app
for (int32_t j = sAc->stack.count - 1; j >= 0; j--) {
WindowT *win = sAc->stack.windows[j];
if (win->appId == i) {
if (win->minimized) {
wmRestoreMinimized(&sAc->stack, &sAc->dirty, win);
}
wmRaiseWindow(&sAc->stack, &sAc->dirty, j);
wmSetFocus(&sAc->stack, &sAc->dirty, sAc->stack.count - 1);
return;
}
}
return;
}
idx++;
}
}
}
// Rebuild the Task Manager list view from the shell's app slot table.
// Uses static arrays because the list view data pointers must remain valid
// until the next call to wgtListViewSetData (the widget doesn't copy strings).
static void refreshTaskList(void) {
if (!sTmListView) {
return;
}
// Flat array of cell strings: [row0_col0..col3, row1_col0..col3, ...]
static const char *cells[SHELL_MAX_APPS * TM_COL_COUNT];
static char typeStrs[SHELL_MAX_APPS][12];
static char fileStrs[SHELL_MAX_APPS][64];
int32_t rowCount = 0;
for (int32_t i = 1; i < SHELL_MAX_APPS; i++) {
if (i == sMyAppId) {
continue;
}
ShellAppT *app = shellGetApp(i);
if (app && app->state == AppStateRunningE) {
int32_t base = rowCount * TM_COL_COUNT;
// Column 0: Name (from appDescriptor)
cells[base] = app->name;
// Column 1: Filename (basename of .app path)
const char *slash = strrchr(app->path, '/');
const char *back = strrchr(app->path, '\\');
if (back > slash) {
slash = back;
}
const char *fname = slash ? slash + 1 : app->path;
snprintf(fileStrs[rowCount], sizeof(fileStrs[rowCount]), "%.63s", fname);
cells[base + 1] = fileStrs[rowCount];
// Column 2: Type (main-loop task vs callback-only)
snprintf(typeStrs[rowCount], sizeof(typeStrs[rowCount]), "%s", app->hasMainLoop ? "Task" : "Callback");
cells[base + 2] = typeStrs[rowCount];
// Column 3: Status
cells[base + 3] = "Running";
rowCount++;
}
}
wgtListViewSetData(sTmListView, cells, rowCount);
}
// 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) {
sAppCount = 0;
scanAppsDirRecurse("apps");
shellLog("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) {
shellLog("Progman: %s directory not found", dirPath);
}
return;
}
struct dirent *ent;
while ((ent = readdir(dir)) != NULL && sAppCount < MAX_APP_FILES) {
// Skip . and ..
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
char fullPath[MAX_PATH_LEN];
snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, 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 (strcmp(ext, ".app") != 0 && strcmp(ext, ".APP") != 0) {
continue;
}
// Skip ourselves
if (strcmp(ent->d_name, "progman.app") == 0 || strcmp(ent->d_name, "PROGMAN.APP") == 0) {
continue;
}
AppEntryT *entry = &sAppFiles[sAppCount];
// 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), "%s", fullPath);
sAppCount++;
}
closedir(dir);
}
static void showAboutDialog(void) {
dvxMessageBox(sAc, "About DVX Shell",
"DVX Shell 1.0\nA DOS Visual eXecutive desktop shell for DJGPP/DPMI. Using DXE3 dynamic loading for application modules.",
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 = 400;
int32_t winH = 360;
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];
// shellRunningAppCount() includes us. Subtract 1 so the user sees
// only the apps they launched, not the Program Manager itself.
int32_t count = shellRunningAppCount() - 1;
if (count < 0) {
count = 0;
}
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;
sMyAppId = ctx->appId;
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;
}