// notepad.c — Simple text editor DXE application (callback-only) // // A callback-only DXE app (hasMainLoop = false) that provides basic text // editing with file I/O. Demonstrates the standard DXE app pattern: // 1. appMain creates window + widget tree, registers callbacks, returns 0 // 2. All editing happens through the TextArea widget's built-in behavior // 3. File operations and dirty tracking are handled by menu callbacks // // The TextArea widget handles keyboard input, cursor movement, selection, // scrolling, word wrap, and undo internally. Notepad only needs to wire // up menus and file I/O around it. #include "dvxApp.h" #include "dvxDialog.h" #include "dvxWidget.h" #include "dvxWm.h" #include "shellApp.h" #include #include #include #include #include // ============================================================ // Constants // ============================================================ // 32KB text buffer limit. Keeps memory usage bounded on DOS; larger files // are silently truncated on load. The TextArea widget allocates this buffer // internally when constructed. #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] = ""; // Hash of text content at last save/open, used for cheap dirty detection 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 // ============================================================ // Callback-only: the TextArea widget handles all interactive editing // entirely within the shell's event dispatch, so no dedicated task needed. AppDescriptorT appDescriptor = { .name = "Notepad", .hasMainLoop = false, .stackSize = SHELL_STACK_DEFAULT, .priority = TS_PRIORITY_NORMAL }; // ============================================================ // Dirty tracking // ============================================================ // djb2-xor hash for dirty detection. Not cryptographic — just a fast way // to detect changes without storing a full copy of the last-saved text. // False negatives are theoretically possible but vanishingly unlikely for // text edits. This avoids the memory cost of keeping a shadow buffer. 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; } // Open in binary mode to avoid DJGPP's CR/LF translation; the TextArea // widget handles line endings internally. FILE *f = fopen(path, "rb"); if (!f) { dvxMessageBox(sCtx->shellCtx, "Error", "Could not open file.", MB_OK | MB_ICONERROR); return; } // Read entire file into a temporary buffer. Files larger than the // TextArea's buffer are silently truncated — this matches the behavior // of Windows Notepad on large files. 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 // ============================================================ // onClose is the only cleanup path for a callback-only app. We null our // pointers after destroying the window; the shell will reap the DXE on the // next frame when it detects no windows remain for this appId. 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 // ============================================================ // Entry point. Offset +20 from center so multiple notepad instances cascade // naturally rather than stacking exactly on top of each other. 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); // The widget tree is minimal: just a TextArea filling the entire content // area (weight=100). The TextArea widget provides editing, scrolling, // selection, copy/paste, and undo — all driven by keyboard events that // the shell dispatches to the focused widget. WidgetT *root = wgtInitWindow(ac, sWin); sTextArea = wgtTextArea(root, TEXT_BUF_SIZE); sTextArea->weight = 100; sFilePath[0] = '\0'; markClean(); wgtInvalidate(root); return 0; }