# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

.section Libraries
.topic shell.overview
.title DVX Shell Library
.toc 0 DVX Shell Library
.default
.index DVX Shell
.index shellApp.h
.index DXE Application Loading

.h1 DVX Shell Library

The DVX shell library manages the lifecycle of DXE applications: loading, launching, tracking, and reaping. It provides the bridge between the DVX GUI compositor and dynamically loaded DXE3 application modules.

Header: dvxshell/shellApp.h

Loaded as: bin/libs/dvxshell.lib

.h2 App Model

The shell supports two kinds of DXE apps:

.list
.item Callback-only (hasMainLoop = false) -- appMain() runs in the shell's task 0, creates windows, registers event callbacks, and returns immediately. The app lives through GUI callbacks. Lifecycle ends when the last window is closed. Simpler and cheaper (no extra stack/task).
.item Main-loop (hasMainLoop = true) -- A dedicated cooperative task is created. appMain() runs in that task and can do its own polling loop, calling tsYield() to share CPU. Lifecycle ends when appMain() returns or the task is killed. Needed for terminal emulators, games, or long computations.
.endlist

Both types use the same DXE interface: an exported appDescriptor and appMain function.

.h2 DXE Interface

Every .app DXE module must export these symbols (COFF convention uses leading underscore):

.table
  Symbol                Type                            Required
  ------                ----                            --------
  _appDescriptor        AppDescriptorT                  Yes
  _appMain              int32_t (*)(DxeAppContextT *)   Yes
  _appShutdown          void (*)(void)                  No
.endtable

The shell resolves appDescriptor at dlopen time; if missing, the load fails with an error dialog. appMain is called immediately for callback-only apps or from the new task for main-loop apps. appShutdown, if present, is called during graceful reap (but not during force kill from crashed apps).

.h2 Callback-Only vs Main-Loop Apps

The app descriptor's hasMainLoop flag selects between two very different lifecycles:

.list
.item hasMainLoop = false (callback-only). The shell calls appMain directly on task 0 at dlopen time. The function creates windows, registers event callbacks, and returns. After that the app has no executing thread of its own -- it exists purely through GUI callbacks dispatched by dvxUpdate. The shell reaps the app automatically when its last window closes. Best for modal tools, dialogs, and event-driven utilities.
.item hasMainLoop = true (main-loop). The shell creates a dedicated cooperative task (via tsCreate) with the descriptor's stackSize (or TS_DEFAULT_STACK_SIZE) and priority. appMain runs in that task and can do its own work loop, calling tsYield or any GUI function that yields. The app terminates when appMain returns (the wrapper sets AppStateTerminatingE) or when forced via shellForceKillApp. Best for terminal emulators, games, and any app with continuous background work.
.endlist

Both app types use the same export interface; only the descriptor's flags differ. Apps cannot switch modes at runtime.

.h2 Icon Conventions

Shell-level UI (Program Manager, Task Manager) displays app icons at 16x16 and 32x32. Icons are not handled by the shell itself; each app embeds its own icons via the DVX resource system (DVX_RES_ICON):

.list
.item 16x16 BMP for toolbar entries and list rows
.item 32x32 BMP for desktop shortcuts and Program Manager tiles
.endlist

The Program Manager reads the app's 32x32 icon resource when building shortcut tiles. Apps without an icon resource fall back to a default shell-provided bitmap.

.h2 State Machine

App slots progress through four states:

.code
Free -> Loaded -> Running -> Terminating -> Free
.endcode

LoadedE is transient (only during shellLoadApp before the entry point is called). TerminatingE means the app's task has exited but cleanup has not yet occurred. The shell's main loop reaps terminating apps each frame via shellReapApps().

.h2 Contents

.link shell.types Types and Constants
.link shell.lifecycle Lifecycle API
.link shell.query Query API
.link shell.config Configuration API
.link shell.desktop Desktop Callbacks
.link shell.info System Information

.topic shell.types
.title Shell Types and Constants
.toc 1 Types and Constants
.index AppDescriptorT
.index DxeAppContextT
.index ShellAppT
.index AppStateE
.index SHELL_APP_NAME_MAX
.index SHELL_STACK_DEFAULT
.index SHELL_DESKTOP_APP

.h1 Types and Constants

.h2 Constants

.table
  Constant              Value                           Description
  --------              -----                           -----------
  SHELL_APP_NAME_MAX    64                              Maximum length of an app name string.
  SHELL_STACK_DEFAULT   0                               Use in AppDescriptorT.stackSize to get the default task stack size.
  SHELL_DESKTOP_APP     "apps/kpunch/progman/progman.app"  Default desktop application path.
.endtable

.h2 AppDescriptorT

Exported by every DXE app as a global named appDescriptor. The shell reads it at load time to determine how to launch the app.

.table
  Field                          Type      Description
  -----                          ----      -----------
  name[SHELL_APP_NAME_MAX]       char[]    Display name of the application.
  hasMainLoop                    bool      false = callback-only, true = dedicated task.
  multiInstance                  bool      true = allow multiple simultaneous instances via temp copy.
  stackSize                      int32_t   SHELL_STACK_DEFAULT or explicit byte count for the task stack.
  priority                       int32_t   TS_PRIORITY_* value or custom priority for the task.
.endtable

.h2 DxeAppContextT

Passed as the sole argument to appMain(). Gives the app access to the shell's GUI context and its own identity. Heap-allocated by shellLoadApp so its address is stable across reallocations of the internal app slot table (apps may safely cache the pointer in their static globals).

.table
  Field                          Type                    Description
  -----                          ----                    -----------
  shellCtx                       AppContextT *           The shell's GUI context (for creating windows, drawing, etc.).
  appId                          int32_t                 This app's unique ID (slot index, 1-based).
  appPath[DVX_MAX_PATH]          char[]                  Full path to the .app DXE file.
  appDir[DVX_MAX_PATH]           char[]                  Directory containing the .app file (for loading resources).
  configDir[DVX_MAX_PATH]        char[]                  Writable config directory (e.g. CONFIG/APPS/KPUNCH/DVXBASIC/).
  args[1024]                     char[]                  Launch arguments (empty string if none).
  helpFile[DVX_MAX_PATH]         char[]                  Help file path (for F1 context help).
  helpTopic[128]                 char[]                  Current help topic ID (updated by the app at runtime).
  onHelpQuery                    void (*)(void *ctx)     Optional callback the shell fires on F1 so the app can refresh helpTopic from context.
  helpQueryCtx                   void *                  Opaque context pointer passed to onHelpQuery.
.endtable

The appDir field is derived from the .app file path at load time so apps can find their own resources via relative paths. This is necessary because the working directory is shared by all apps in DOS. The config directory is mirrored from the app path: an app at APPS/KPUNCH/DVXBASIC/dvxbasic.app gets CONFIG/APPS/KPUNCH/DVXBASIC/.

.h2 AppStateE

.table
  Value                 Description
  -----                 -----------
  AppStateFreeE         Slot is available for reuse.
  AppStateLoadedE       DXE loaded, entry point not yet called (transient).
  AppStateRunningE      Entry point called, app is active.
  AppStateTerminatingE  Shutdown in progress, awaiting reap.
.endtable

.h2 ShellAppT

Per-app slot in the shell's app table. Slot 0 is reserved for the shell itself; apps use slots 1 and above.

.table
  Field                          Type                            Description
  -----                          ----                            -----------
  appId                          int32_t                         Unique ID (slot index, 1-based; 0 = shell).
  name[SHELL_APP_NAME_MAX]       char[]                          Display name from AppDescriptorT.
  path[DVX_MAX_PATH]             char[]                          Original DXE file path.
  tempPath[DVX_MAX_PATH]         char[]                          Temp copy path for multi-instance (empty if not a copy).
  dxeHandle                      void *                          dlopen() handle for the DXE module.
  state                          AppStateE                       Current lifecycle state.
  hasMainLoop                    bool                            Whether this app has a dedicated task.
  mainTaskId                     uint32_t                        Task ID if hasMainLoop, else 0.
  entryFn                        int32_t (*)(DxeAppContextT *)   Pointer to appMain.
  shutdownFn                     void (*)(void)                  Pointer to appShutdown (may be NULL).
  dxeCtx                         DxeAppContextT *                Heap-allocated context (address stable across realloc).
.endtable

.topic shell.lifecycle
.title App Lifecycle API
.toc 1 Lifecycle API
.index shellAppInit
.index shellLoadApp
.index shellLoadAppWithArgs
.index shellReapApps
.index shellReapApp
.index shellForceKillApp
.index shellTerminateAllApps

.h1 App Lifecycle API

.h2 shellAppInit

.code
void shellAppInit(void);
.endcode

Initialize the app slot table. Seeds slot 0 (reserved for the shell). Must be called once at startup before any other shell API function.

.h2 shellLoadApp

.code
int32_t shellLoadApp(AppContextT *ctx, const char *path);
.endcode

Load and start an app from a DXE file. Returns the app ID (>= 1) on success, or -1 on error.

For callback-only apps, appMain() runs synchronously and returns before shellLoadApp returns. For main-loop apps, a cooperative task is created and the app begins running on the next tsYield().

If multiInstance is false in the app's descriptor and the same DXE is already loaded, the call fails with an error dialog. If multiInstance is true, the DXE is copied to a temp file so dlopen gets an independent code and data image.

.h2 shellLoadAppWithArgs

.code
int32_t shellLoadAppWithArgs(AppContextT *ctx, const char *path, const char *args);
.endcode

Load and run an app with arguments. The args string is copied into DxeAppContextT.args before appMain() is called. Otherwise identical to shellLoadApp().

.h2 shellReapApps

.code
bool shellReapApps(AppContextT *ctx);
.endcode

Scan for and reap finished apps. Call once per frame from the main loop.

Returns true if any apps were reaped, so the caller can trigger a desktop refresh. For main-loop apps, termination is detected by the AppStateTerminatingE state (set when appMain returns). For callback-only apps, termination is detected when no windows remain for that app.

.h2 shellReapApp

.code
void shellReapApp(AppContextT *ctx, ShellAppT *app);
.endcode

Gracefully shut down a single app. Calls the app's shutdownFn (if present), destroys all windows belonging to the app, kills its task (if any), closes the DXE handle, and frees the context. The slot returns to AppStateFreeE.

.h2 shellForceKillApp

.code
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
.endcode

Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery. Calls the app's shutdownFn (if present) so it can unregister callbacks before its DXE is unmapped, then destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.

Cleanup order: shutdownFn first (so the app can unregister callbacks while still mapped), then windows (removes from compositor), then task (frees stack), then DXE handle (unmaps code). Closing the DXE before destroying windows would cause callbacks into unmapped code.

.h2 shellTerminateAllApps

.code
void shellTerminateAllApps(AppContextT *ctx);
.endcode

Force-kill all running apps. Called during shell shutdown. Iterates all slots and calls shellForceKillApp() on each active app.

.topic shell.query
.title Query API
.toc 1 Query API
.index shellGetApp
.index shellAppSlotCount
.index shellRunningAppCount

.h1 Query API

.h2 shellGetApp

.code
ShellAppT *shellGetApp(int32_t appId);
.endcode

Look up an app slot by ID. Returns a pointer to the ShellAppT, or NULL if the ID is out of range or the slot is free. Valid app IDs are 1 through shellAppSlotCount() - 1.

.h2 shellAppSlotCount

.code
int32_t shellAppSlotCount(void);
.endcode

Return the total number of app slots (including slot 0). Use as the iteration bound when scanning all slots. Slot 0 is the shell itself; app slots start at 1.

.h2 shellRunningAppCount

.code
int32_t shellRunningAppCount(void);
.endcode

Count running apps, not counting the shell itself. Includes apps in both AppStateLoadedE and AppStateRunningE states.

.topic shell.config
.title Configuration API
.toc 1 Configuration API
.index shellEnsureConfigDir
.index shellConfigPath

.h1 Configuration API

Each app has a per-app configuration directory derived from its DXE path, mirrored under CONFIG/. For example, an app at APPS/KPUNCH/DVXBASIC/dvxbasic.app gets the config directory CONFIG/APPS/KPUNCH/DVXBASIC/.

.h2 shellEnsureConfigDir

.code
int32_t shellEnsureConfigDir(const DxeAppContextT *ctx);
.endcode

Ensure the app's config directory exists, creating all parent directories as needed. Returns 0 on success, -1 on failure.

Call this before writing any config files. The directory path comes from ctx->configDir.

.h2 shellConfigPath

.code
void shellConfigPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);
.endcode

Build a full path to a file in the app's config directory by joining ctx->configDir with the given filename.

.code
// Example:
char path[DVX_MAX_PATH];
shellConfigPath(ctx, "settings.ini", path, sizeof(path));
// -> "CONFIG/APPS/KPUNCH/PROGMAN/settings.ini"
.endcode

.topic shell.desktop
.title Desktop Callbacks
.toc 1 Desktop Callbacks
.index shellRegisterDesktopUpdate
.index shellUnregisterDesktopUpdate
.index shellDesktopUpdate
.index shellCtrlEscFn

.h1 Desktop Callbacks

The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation).

.h2 Shell Event Hooks

.table
  AppContextT field    Handler                 Purpose
  -----------------    -------                 -------
  idleCallback         idleYield               Yields to cooperative tasks when dvxUpdate has no input events and no dirty rects.
  onCtrlEsc            ctrlEscHandler          Invokes shellCtrlEscFn if the Task Manager DXE is loaded.
  onF1                 f1HelpHandler           Launches the help viewer, optionally passing the focused app's helpFile and helpTopic.
  onTitleChange        titleChangeHandler      Runs shellDesktopUpdate so Program Manager window lists refresh.
.endtable

.h2 shellRegisterDesktopUpdate

.code
void shellRegisterDesktopUpdate(void (*updateFn)(void));
.endcode

Register a callback for app state change notifications. Multiple callbacks are supported. Apps typically call this during appMain() to receive notifications.

.h2 shellUnregisterDesktopUpdate

.code
void shellUnregisterDesktopUpdate(void (*updateFn)(void));
.endcode

Remove a previously registered callback. Call this before app shutdown to avoid dangling function pointers.

.h2 shellDesktopUpdate

.code
void shellDesktopUpdate(void);
.endcode

Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh.

.h2 shellCtrlEscFn

.code
extern void (*shellCtrlEscFn)(AppContextT *ctx);
.endcode

Function pointer set by the taskmgr DXE's constructor. The shell calls this when Ctrl+Esc is pressed. NULL if the task manager is not loaded.

.topic shell.info
.title System Information
.toc 1 System Information
.index shellInfoInit
.index shellGetSystemInfo

.h1 System Information

Header: dvxshell/shellInf.h

Thin wrapper around the platform layer's hardware detection. Gathers system information at startup, logs it, and caches the result for display in dialogs.

.h2 shellInfoInit

.code
void shellInfoInit(AppContextT *ctx);
.endcode

Gather all hardware information via the platform layer, log each line to DVX.LOG, and store the result for later retrieval. Call once after dvxInit().

.h2 shellGetSystemInfo

.code
const char *shellGetSystemInfo(void);
.endcode

Return the formatted system information text. The returned pointer is valid for the lifetime of the process (static buffer in the platform layer).
