592 lines
15 KiB
C
592 lines
15 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).
|
|
// This is a callback-only DXE app: creates windows, registers callbacks,
|
|
// and returns.
|
|
|
|
#include "dvxApp.h"
|
|
#include "dvxDialog.h"
|
|
#include "dvxWidget.h"
|
|
#include "dvxWm.h"
|
|
#include "shellApp.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define MAX_APP_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_MIN_ON_RUN 104
|
|
#define CMD_ABOUT 300
|
|
#define CMD_TASK_MGR 301
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
char name[SHELL_APP_NAME_MAX]; // display name (filename without .app)
|
|
char path[MAX_PATH_LEN]; // full path
|
|
} AppEntryT;
|
|
|
|
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 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
|
|
// ============================================================
|
|
|
|
AppDescriptorT appDescriptor = {
|
|
.name = "Program Manager",
|
|
.hasMainLoop = false,
|
|
.stackSize = 0,
|
|
.priority = TS_PRIORITY_NORMAL
|
|
};
|
|
|
|
// ============================================================
|
|
// Static functions (alphabetical)
|
|
// ============================================================
|
|
|
|
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);
|
|
wmAddMenuSeparator(helpMenu);
|
|
wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR);
|
|
|
|
// Widget tree
|
|
WidgetT *root = wgtInitWindow(sAc, sPmWindow);
|
|
|
|
// App button grid in a frame
|
|
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
|
|
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
|
|
WidgetT *statusBar = wgtStatusBar(root);
|
|
sStatusLabel = wgtLabel(statusBar, "");
|
|
sStatusLabel->weight = 100;
|
|
updateStatusText();
|
|
|
|
dvxFitWindow(sAc, sPmWindow);
|
|
}
|
|
|
|
|
|
static void buildTaskManager(void) {
|
|
if (sTmWindow) {
|
|
// Already open — just raise it
|
|
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 = 300;
|
|
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);
|
|
|
|
// List view of running apps
|
|
ListViewColT tmCols[3];
|
|
tmCols[0].title = "Name";
|
|
tmCols[0].width = wgtPercent(50);
|
|
tmCols[0].align = ListViewAlignLeftE;
|
|
tmCols[1].title = "Type";
|
|
tmCols[1].width = wgtPercent(25);
|
|
tmCols[1].align = ListViewAlignLeftE;
|
|
tmCols[2].title = "Status";
|
|
tmCols[2].width = wgtPercent(25);
|
|
tmCols[2].align = ListViewAlignLeftE;
|
|
|
|
sTmListView = wgtListView(root);
|
|
sTmListView->weight = 100;
|
|
sTmListView->prefH = wgtPixels(160);
|
|
wgtListViewSetColumns(sTmListView, tmCols, 3);
|
|
|
|
// 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(sAc, sTmWindow);
|
|
}
|
|
|
|
|
|
static void desktopUpdate(void) {
|
|
updateStatusText();
|
|
|
|
if (sTmWindow) {
|
|
refreshTaskList();
|
|
}
|
|
}
|
|
|
|
|
|
static void onAppButtonClick(WidgetT *w) {
|
|
AppEntryT *entry = (AppEntryT *)w->userData;
|
|
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
|
|
shellLoadApp(sAc, entry->path);
|
|
updateStatusText();
|
|
|
|
if (sMinOnRun && sPmWindow) {
|
|
dvxMinimizeWindow(sAc, sPmWindow);
|
|
}
|
|
}
|
|
|
|
|
|
static void onPmClose(WindowT *win) {
|
|
(void)win;
|
|
// Confirm exit
|
|
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_TASK_MGR:
|
|
buildTaskManager();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
// Map list index to app ID (skip our own appId)
|
|
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;
|
|
}
|
|
|
|
// Map list index to app ID (skip our own appId), find topmost window
|
|
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++;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void refreshTaskList(void) {
|
|
if (!sTmListView) {
|
|
return;
|
|
}
|
|
|
|
// 3 columns per row: Name, Type, Status
|
|
static const char *cells[SHELL_MAX_APPS * 3];
|
|
static char typeStrs[SHELL_MAX_APPS][12];
|
|
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 * 3;
|
|
cells[base] = app->name;
|
|
snprintf(typeStrs[rowCount], sizeof(typeStrs[rowCount]), "%s", app->hasMainLoop ? "Task" : "Callback");
|
|
cells[base + 1] = typeStrs[rowCount];
|
|
cells[base + 2] = "Running";
|
|
rowCount++;
|
|
}
|
|
}
|
|
|
|
wgtListViewSetData(sTmListView, cells, rowCount);
|
|
}
|
|
|
|
|
|
static void scanAppsDir(void) {
|
|
sAppCount = 0;
|
|
scanAppsDirRecurse("apps");
|
|
shellLog("Progman: found %ld app(s)", (long)sAppCount);
|
|
}
|
|
|
|
|
|
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 updateStatusText(void) {
|
|
if (!sStatusLabel) {
|
|
return;
|
|
}
|
|
|
|
static char buf[64];
|
|
|
|
// Subtract 1 to exclude ourselves from the count
|
|
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
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx) {
|
|
sCtx = ctx;
|
|
sAc = ctx->shellCtx;
|
|
sMyAppId = ctx->appId;
|
|
|
|
scanAppsDir();
|
|
buildPmWindow();
|
|
|
|
// Register for state change notifications from the shell
|
|
shellRegisterDesktopUpdate(desktopUpdate);
|
|
|
|
return 0;
|
|
}
|