Many BASIC compiler and VM fixes. Other fixes. New BASIC apps.

This commit is contained in:
Scott Duensing 2026-04-16 18:18:04 -05:00
parent de60200f23
commit dcd120d769
74 changed files with 6091 additions and 505 deletions

View file

@ -11,11 +11,14 @@ BINDIR = ../bin/apps
DVXRES = ../bin/host/dvxres
# App definitions: each is a subdir with a single .c file
APPS = progman notepad clock dvxdemo cpanel imgview dvxhelp
APPS = progman clock dvxdemo cpanel dvxhelp
.PHONY: all clean $(APPS) dvxbasic
BASCOMP = ../bin/host/bascomp
BASICAPPS = iconed notepad-bas imgview-bas helpedit resedit basicdemo
all: $(APPS) dvxbasic
.PHONY: all clean $(APPS) dvxbasic $(BASICAPPS)
all: $(APPS) dvxbasic $(BASICAPPS)
dvxbasic:
$(MAKE) -C dvxbasic
@ -30,28 +33,28 @@ dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel
$(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ cpanel/cpanel.res
cd cpanel && ../$(DVXRES) build ../$@ cpanel.res
$(BINDIR)/kpunch/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/kpunch/imgview
$(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ imgview/imgview.res
cd imgview && ../$(DVXRES) build ../$@ imgview.res
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman
$(DXE3GEN) -o $@ -U $<
$(BINDIR)/kpunch/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/kpunch/notepad
$(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ notepad/notepad.res
cd notepad && ../$(DVXRES) build ../$@ notepad.res
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock
$(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ clock/clock.res
cd clock && ../$(DVXRES) build ../$@ clock.res
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp
$(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo
$(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ dvxdemo/dvxdemo.res
cd dvxdemo && ../$(DVXRES) build ../$@ dvxdemo.res
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
$(OBJDIR)/cpanel.o: cpanel/cpanel.c | $(OBJDIR)
@ -74,11 +77,52 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp
$(DXE3GEN) -o $@ -U $<
$(DVXRES) build $@ dvxhelp/dvxhelp.res
cd dvxhelp && ../$(DVXRES) build ../$@ dvxhelp.res
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
# BASIC apps (compiled from .dbp projects via bascomp)
iconed: $(BINDIR)/kpunch/iconed/iconed.app
$(BINDIR)/kpunch/iconed/iconed.app: ../sdk/samples/basic/iconed/iconed.dbp ../sdk/samples/basic/iconed/iconed.frm ../sdk/samples/basic/iconed/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/iconed dvxbasic
$(BASCOMP) ../sdk/samples/basic/iconed/iconed.dbp -o $@ -release
notepad-bas: $(BINDIR)/kpunch/notepad/notepad.app
$(BINDIR)/kpunch/notepad/notepad.app: ../sdk/samples/basic/notepad/notepad.dbp ../sdk/samples/basic/notepad/notepad.frm ../sdk/samples/basic/notepad/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/notepad dvxbasic
$(BASCOMP) ../sdk/samples/basic/notepad/notepad.dbp -o $@ -release
imgview-bas: $(BINDIR)/kpunch/imgview/imgview.app
$(BINDIR)/kpunch/imgview/imgview.app: ../sdk/samples/basic/imgview/imgview.dbp ../sdk/samples/basic/imgview/imgview.frm ../sdk/samples/basic/imgview/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/imgview dvxbasic
$(BASCOMP) ../sdk/samples/basic/imgview/imgview.dbp -o $@ -release
helpedit: $(BINDIR)/kpunch/dvxhelp/helpedit.app
$(BINDIR)/kpunch/dvxhelp/helpedit.app: ../sdk/samples/basic/helpedit/helpedit.dbp ../sdk/samples/basic/helpedit/helpedit.frm ../sdk/samples/basic/helpedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/dvxhelp dvxbasic
$(BASCOMP) ../sdk/samples/basic/helpedit/helpedit.dbp -o $@ -release
$(DVXRES) add $@ helpfile text "dvxhelp.hlp"
resedit: $(BINDIR)/kpunch/resedit/resedit.app
$(BINDIR)/kpunch/resedit/resedit.app: ../sdk/samples/basic/resedit/resedit.dbp ../sdk/samples/basic/resedit/resedit.frm ../sdk/samples/basic/resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic
$(BASCOMP) ../sdk/samples/basic/resedit/resedit.dbp -o $@ -release
$(BINDIR)/kpunch/resedit:
mkdir -p $(BINDIR)/kpunch/resedit
basicdemo: $(BINDIR)/kpunch/basicdemo/basicdemo.app
$(BINDIR)/kpunch/basicdemo/basicdemo.app: ../sdk/samples/basic/basicdemo/basicdemo.dbp ../sdk/samples/basic/basicdemo/basicdemo.frm ../sdk/samples/basic/basicdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basicdemo dvxbasic
$(BASCOMP) ../sdk/samples/basic/basicdemo/basicdemo.dbp -o $@ -release
$(BINDIR)/kpunch/basicdemo:
mkdir -p $(BINDIR)/kpunch/basicdemo
$(BINDIR)/kpunch/iconed:
mkdir -p $(BINDIR)/kpunch/iconed
$(OBJDIR):
mkdir -p $(OBJDIR)

View file

@ -1,5 +1,5 @@
# clock.res -- Resource manifest for Clock
icon32 icon clock/icon32.bmp
icon32 icon icon32.bmp
name text "Clock"
author text "DVX Project"
description text "Digital clock with date display"

View file

@ -1,7 +1,7 @@
// ctrlpanel.c -- DVX Control Panel
//
// System configuration app with four tabs:
// Mouse -- scroll direction, double-click speed, acceleration, cursor speed
// Mouse -- scroll direction, wheel speed, double-click speed, acceleration, cursor speed
// Colors -- all 18 system colors, theme load/save
// Desktop -- wallpaper image (stretch mode)
// Video -- resolution and color depth
@ -95,6 +95,7 @@ static int32_t sSavedWheelDir;
static int32_t sSavedDblClick;
static int32_t sSavedAccel;
static int32_t sSavedSpeed;
static int32_t sSavedWheelStep;
static int32_t sSavedVideoW;
static int32_t sSavedVideoH;
static int32_t sSavedVideoBpp;
@ -107,6 +108,8 @@ static WidgetT *sDblClickLbl = NULL;
static WidgetT *sAccelDrop = NULL;
static WidgetT *sSpeedSldr = NULL;
static WidgetT *sSpeedLbl = NULL;
static WidgetT *sWheelStepSldr = NULL;
static WidgetT *sWheelStepLbl = NULL;
static WidgetT *sDblTestLbl = NULL;
// Colors tab widgets
@ -152,6 +155,7 @@ static const char *mapAccelValue(int32_t val);
static void onAccelChange(WidgetT *w);
static void onSpeedSlider(WidgetT *w);
static void updateSpeedLabel(void);
static void updateWheelStepLabel(void);
static void applyMouseConfig(void);
static void onApplyTheme(WidgetT *w);
static void onBrowseTheme(WidgetT *w);
@ -170,6 +174,7 @@ static void onOk(WidgetT *w);
static void onSaveTheme(WidgetT *w);
static void onVideoApply(WidgetT *w);
static void onWheelChange(WidgetT *w);
static void onWheelStepSlider(WidgetT *w);
static void saveSnapshot(void);
static void restoreSnapshot(void);
static void scanWallpapers(void);
@ -334,6 +339,26 @@ static void buildMouseTab(WidgetT *page) {
wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
// Wheel speed (lines per notch)
wgtLabel(page, "Scroll Speed:");
WidgetT *wheelStepRow = wgtHBox(page);
wheelStepRow->spacing = wgtPixels(CP_SPACING);
wgtLabel(wheelStepRow, "Slow");
sWheelStepSldr = wgtSlider(wheelStepRow, MOUSE_WHEEL_STEP_MIN, MOUSE_WHEEL_STEP_MAX);
sWheelStepSldr->weight = 100;
sWheelStepSldr->onChange = onWheelStepSlider;
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);
wgtSliderSetValue(sWheelStepSldr, wheelStep);
wgtLabel(wheelStepRow, "Fast ");
sWheelStepLbl = wgtLabel(wheelStepRow, "");
sWheelStepLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
updateWheelStepLabel();
wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
// Double-click speed
wgtLabel(page, "Double-Click Speed:");
@ -500,6 +525,13 @@ static void onWheelChange(WidgetT *w) {
}
static void onWheelStepSlider(WidgetT *w) {
(void)w;
updateWheelStepLabel();
applyMouseConfig();
}
static void onDblClickSlider(WidgetT *w) {
(void)w;
updateDblClickLabel();
@ -588,8 +620,8 @@ static void onBrowseTheme(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Theme Files (*.thm)", "*.thm" },
{ "All Files (*.*)", "*.*" }
{ "\1" },
{ "\1" }
};
char path[DVX_MAX_PATH];
@ -604,8 +636,8 @@ static void onSaveTheme(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Theme Files (*.thm)", "*.thm" },
{ "All Files (*.*)", "*.*" }
{ "\1" },
{ "\1" }
};
char path[DVX_MAX_PATH];
@ -650,8 +682,8 @@ static void onChooseWallpaper(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Images (*.bmp;*.jpg;*.png)", "*.bmp;*.jpg;*.png" },
{ "All Files (*.*)", "*.*" }
{ "\1" },
{ "\1" }
};
char path[DVX_MAX_PATH];
@ -824,6 +856,7 @@ static void onOk(WidgetT *w) {
prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr));
prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop)));
prefsSetInt(sPrefs, "mouse", "speed", wgtSliderGetValue(sSpeedSldr));
prefsSetInt(sPrefs, "mouse", "wheelspeed", wgtSliderGetValue(sWheelStepSldr));
// Save colors to INI
for (int32_t i = 0; i < ColorCountE; i++) {
@ -898,6 +931,7 @@ static void saveSnapshot(void) {
sSavedDblClick = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC);
sSavedAccel = wgtDropdownGetSelected(sAccelDrop);
sSavedSpeed = wgtSliderGetValue(sSpeedSldr);
sSavedWheelStep = sAc->wheelStep;
sSavedVideoW = sAc->display.width;
sSavedVideoH = sAc->display.height;
sSavedVideoBpp = sAc->display.format.bitsPerPixel;
@ -918,7 +952,7 @@ static void restoreSnapshot(void) {
const char *accelName = mapAccelValue(sSavedAccel);
int32_t accelVal = mapAccelName(accelName);
int32_t speedVal = 34 - sSavedSpeed;
dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal);
dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, accelVal, speedVal, sSavedWheelStep);
// Restore video mode if changed
if (sAc->display.width != sSavedVideoW ||
@ -1097,6 +1131,18 @@ static void updateSpeedLabel(void) {
}
// ============================================================
// updateWheelStepLabel
// ============================================================
static void updateWheelStepLabel(void) {
static char buf[32];
int32_t val = wgtSliderGetValue(sWheelStepSldr);
snprintf(buf, sizeof(buf), "%ld", (long)val);
wgtSetText(sWheelStepLbl, buf);
}
// ============================================================
// applyMouseConfig -- gather current widget values and apply
// ============================================================
@ -1113,9 +1159,10 @@ static void applyMouseConfig(void) {
// slider 2 -> 32 mickeys/8px (slowest)
// slider 8 -> 26 (near default)
// slider 32 -> 2 mickeys/8px (fastest)
int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr);
int32_t speedVal = 34 - wgtSliderGetValue(sSpeedSldr);
int32_t wheelStep = wgtSliderGetValue(sWheelStepSldr);
dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal);
dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal, wheelStep);
}

View file

@ -1,5 +1,5 @@
# cpanel.res -- Resource manifest for Control Panel
icon32 icon cpanel/icon32.bmp
icon32 icon icon32.bmp
name text "Control Panel"
author text "DVX Project"
description text "System settings and preferences"

View file

@ -25,7 +25,7 @@ RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
# Compiler objects (only needed by the IDE)
COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o
COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/symtab.o $(OBJDIR)/strip.o $(OBJDIR)/obfuscate.o $(OBJDIR)/compact.o
# IDE app objects
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideMenuEditor.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
@ -38,25 +38,39 @@ STUB_TARGET = $(OBJDIR)/basstub.app
# Native test programs (host gcc, not cross-compiled)
HOSTCC = gcc
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I. -I../../core
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -D_GNU_SOURCE -I. -I../../core -I../../core/platform -I../../core/thirdparty
BINDIR = ../../bin
TEST_COMPILER = $(BINDIR)/test_compiler
TEST_VM = $(BINDIR)/test_vm
TEST_LEX = $(BINDIR)/test_lex
TEST_QUICK = $(BINDIR)/test_quick
TEST_COMPACT = $(BINDIR)/test_compact
STB_DS_IMPL = ../../core/thirdparty/stb_ds_impl.c
TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL)
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c $(STB_DS_IMPL)
TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL)
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL)
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL)
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL)
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(STB_DS_IMPL)
# Command-line compiler (host tool)
HOSTDIR = ../../bin/host
BASCOMP_SRCS = stub/bascomp.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../core/dvxPrefs.c ../../core/dvxResource.c $(STB_DS_IMPL)
BASCOMP_TARGET = $(HOSTDIR)/bascomp
# DOS command-line compiler
DOSCC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc
DOSCFLAGS = -O2 -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation -march=i486 -mtune=i586 -I../../core -I../../core/platform -I../../core/thirdparty -I.
EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff
CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE
SYSTEMDIR = ../../bin/system
.PHONY: all clean tests
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET)
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE $(SYSTEMDIR)/BASSTUB.APP
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK)
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT)
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(BINDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
@ -70,6 +84,31 @@ $(TEST_LEX): $(TEST_LEX_SRCS) | $(BINDIR)
$(TEST_QUICK): $(TEST_QUICK_SRCS) | $(BINDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_QUICK_SRCS) -lm
$(TEST_COMPACT): $(TEST_COMPACT_SRCS) | $(BINDIR)
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPACT_SRCS) -lm
# Host command-line compiler (stub deployed alongside)
$(BASCOMP_TARGET): $(BASCOMP_SRCS) $(STUB_TARGET) | $(HOSTDIR)
$(HOSTCC) $(HOSTCFLAGS) -DBASCOMP_STANDALONE -I../../tools -o $@ $(BASCOMP_SRCS) -lm
cp $(STUB_TARGET) $(HOSTDIR)/BASSTUB.APP
# DOS command-line compiler
$(SYSTEMDIR)/BASCOMP.EXE: $(BASCOMP_SRCS) | $(SYSTEMDIR)
$(DOSCC) $(DOSCFLAGS) -DBASCOMP_STANDALONE -I../../tools -o $(SYSTEMDIR)/bascomp.exe $(BASCOMP_SRCS) -lm
$(EXE2COFF) $(SYSTEMDIR)/bascomp.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/bascomp > $@
rm -f $(SYSTEMDIR)/bascomp $(SYSTEMDIR)/bascomp.exe
# Deploy stub alongside DOS compiler
$(SYSTEMDIR)/BASSTUB.APP: $(STUB_TARGET) | $(SYSTEMDIR)
cp $(STUB_TARGET) $@
$(HOSTDIR):
mkdir -p $(HOSTDIR)
$(SYSTEMDIR):
mkdir -p $(SYSTEMDIR)
# Runtime library DXE (exports symbols via dlregsym constructor)
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
$(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS)
@ -105,6 +144,12 @@ $(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(
$(OBJDIR)/strip.o: compiler/strip.c compiler/strip.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/obfuscate.o: compiler/obfuscate.c compiler/obfuscate.h runtime/vm.h runtime/values.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/compact.o: compiler/compact.c compiler/compact.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h formrt/formcfm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
@ -158,5 +203,5 @@ $(BINDIR):
mkdir -p $(BINDIR)
clean:
rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o
rm -rf $(RT_OBJS) $(COMP_OBJS) $(IDE_OBJS) $(STUB_OBJS) $(RT_TARGET) $(APP_TARGET) $(STUB_TARGET) $(BASCOMP_TARGET) $(RT_TARGETDIR)/basrt.dep $(RT_TARGETDIR) $(OBJDIR)/basrt_init.o
rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK)

View file

@ -372,47 +372,6 @@ const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name)
}
// ============================================================
// basModuleFree
// ============================================================
void basModuleFree(BasModuleT *mod) {
if (!mod) {
return;
}
free(mod->code);
if (mod->constants) {
for (int32_t i = 0; i < mod->constCount; i++) {
basStringUnref(mod->constants[i]);
}
free(mod->constants);
}
if (mod->dataPool) {
for (int32_t i = 0; i < mod->dataCount; i++) {
basValRelease(&mod->dataPool[i]);
}
free(mod->dataPool);
}
free(mod->procs);
free(mod->formVarInfo);
free(mod->debugVars);
if (mod->debugUdtDefs) {
for (int32_t i = 0; i < mod->debugUdtDefCount; i++) {
free(mod->debugUdtDefs[i].fields);
}
free(mod->debugUdtDefs);
}
free(mod);
}
// ============================================================

View file

@ -0,0 +1,564 @@
// compact.c -- Release build bytecode compaction
//
// Walks the module's bytecode, removes OP_LINE instructions (3 bytes
// each), and rewrites all code-address references so control flow
// still lands on the correct instructions.
//
// Address references:
// - BasProcEntryT::codeAddr (absolute)
// - BasFormVarInfoT::initCodeAddr (absolute, 0 = no init)
// - OP_CALL operand (absolute uint16)
// - OP_JMP / OP_JMP_TRUE / OP_JMP_FALSE operand (relative int16)
// - OP_FOR_NEXT loopTop operand (relative int16)
// - OP_ON_ERROR handler operand (relative int16; 0 = disable, not remapped)
// - GOSUB return address (emitted as OP_PUSH_INT32 followed by OP_JMP,
// where the pushed value equals the PC immediately after the JMP)
//
// Safety: if any opcode cannot be sized, any jump overflows int16, or
// the walk doesn't reach codeLen exactly, the module is left untouched.
#include "compact.h"
#include "opcodes.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// Opcode operand size table
// ============================================================
// Returns operand byte count (excluding the 1-byte opcode), or -1 if unknown.
static int32_t opOperandSize(uint8_t op) {
switch (op) {
// No operand bytes
case OP_NOP:
case OP_PUSH_TRUE: case OP_PUSH_FALSE:
case OP_POP: case OP_DUP:
case OP_LOAD_REF: case OP_STORE_REF:
case OP_ADD_INT: case OP_SUB_INT: case OP_MUL_INT:
case OP_IDIV_INT: case OP_MOD_INT: case OP_NEG_INT:
case OP_ADD_FLT: case OP_SUB_FLT: case OP_MUL_FLT:
case OP_DIV_FLT: case OP_NEG_FLT: case OP_POW:
case OP_STR_CONCAT: case OP_STR_LEFT: case OP_STR_RIGHT:
case OP_STR_MID: case OP_STR_MID2: case OP_STR_LEN:
case OP_STR_INSTR: case OP_STR_INSTR3:
case OP_STR_UCASE: case OP_STR_LCASE:
case OP_STR_TRIM: case OP_STR_LTRIM: case OP_STR_RTRIM:
case OP_STR_CHR: case OP_STR_ASC: case OP_STR_SPACE:
case OP_CMP_EQ: case OP_CMP_NE: case OP_CMP_LT:
case OP_CMP_GT: case OP_CMP_LE: case OP_CMP_GE:
case OP_AND: case OP_OR: case OP_NOT:
case OP_XOR: case OP_EQV: case OP_IMP:
case OP_GOSUB_RET: case OP_RET: case OP_RET_VAL:
case OP_FOR_POP:
case OP_CONV_INT_FLT: case OP_CONV_FLT_INT:
case OP_CONV_INT_STR: case OP_CONV_STR_INT:
case OP_CONV_FLT_STR: case OP_CONV_STR_FLT:
case OP_CONV_INT_LONG: case OP_CONV_LONG_INT:
case OP_PRINT: case OP_PRINT_NL: case OP_PRINT_TAB:
case OP_INPUT:
case OP_FILE_CLOSE: case OP_FILE_PRINT: case OP_FILE_INPUT:
case OP_FILE_EOF: case OP_FILE_LINE_INPUT:
case OP_LOAD_PROP: case OP_STORE_PROP:
case OP_LOAD_FORM: case OP_UNLOAD_FORM:
case OP_HIDE_FORM: case OP_DO_EVENTS:
case OP_MSGBOX: case OP_INPUTBOX: case OP_ME_REF:
case OP_CREATE_CTRL: case OP_FIND_CTRL: case OP_FIND_CTRL_IDX:
case OP_CREATE_CTRL_EX:
case OP_ERASE:
case OP_RESUME: case OP_RESUME_NEXT:
case OP_RAISE_ERR: case OP_ERR_NUM: case OP_ERR_CLEAR:
case OP_MATH_ABS: case OP_MATH_INT: case OP_MATH_FIX:
case OP_MATH_SGN: case OP_MATH_SQR: case OP_MATH_SIN:
case OP_MATH_COS: case OP_MATH_TAN: case OP_MATH_ATN:
case OP_MATH_LOG: case OP_MATH_EXP: case OP_MATH_RND:
case OP_MATH_RANDOMIZE:
case OP_RGB:
case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE:
case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX:
case OP_STR_STRING:
case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
case OP_SLEEP: case OP_ENVIRON:
case OP_READ_DATA: case OP_RESTORE:
case OP_FILE_WRITE: case OP_FILE_WRITE_SEP: case OP_FILE_WRITE_NL:
case OP_FILE_GET: case OP_FILE_PUT: case OP_FILE_SEEK:
case OP_FILE_LOF: case OP_FILE_LOC: case OP_FILE_FREEFILE:
case OP_FILE_INPUT_N:
case OP_STR_MID_ASGN: case OP_PRINT_USING:
case OP_PRINT_TAB_N: case OP_PRINT_SPC_N:
case OP_FORMAT: case OP_SHELL:
case OP_APP_PATH: case OP_APP_CONFIG: case OP_APP_DATA:
case OP_INI_READ: case OP_INI_WRITE:
case OP_FS_KILL: case OP_FS_NAME: case OP_FS_FILECOPY:
case OP_FS_MKDIR: case OP_FS_RMDIR: case OP_FS_CHDIR:
case OP_FS_CHDRIVE: case OP_FS_CURDIR: case OP_FS_DIR:
case OP_FS_DIR_NEXT: case OP_FS_FILELEN:
case OP_FS_GETATTR: case OP_FS_SETATTR:
case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL:
case OP_END: case OP_HALT:
return 0;
case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
case OP_PRINT_SPC: case OP_FILE_OPEN:
case OP_CALL_METHOD: case OP_SHOW_FORM:
case OP_LBOUND: case OP_UBOUND:
case OP_COMPARE_MODE:
return 1;
case OP_PUSH_INT16: case OP_PUSH_STR:
case OP_LOAD_LOCAL: case OP_STORE_LOCAL:
case OP_LOAD_GLOBAL: case OP_STORE_GLOBAL:
case OP_LOAD_FIELD: case OP_STORE_FIELD:
case OP_PUSH_LOCAL_ADDR: case OP_PUSH_GLOBAL_ADDR:
case OP_JMP: case OP_JMP_TRUE: case OP_JMP_FALSE:
case OP_CTRL_REF:
case OP_LOAD_FORM_VAR: case OP_STORE_FORM_VAR:
case OP_PUSH_FORM_ADDR:
case OP_DIM_ARRAY: case OP_REDIM:
case OP_ON_ERROR:
case OP_STR_FIXLEN:
case OP_LINE:
return 2;
case OP_STORE_ARRAY_FIELD:
case OP_FOR_INIT:
return 3;
case OP_PUSH_INT32: case OP_PUSH_FLT32:
case OP_CALL:
return 4;
case OP_FOR_NEXT:
return 5;
case OP_CALL_EXTERN:
return 6;
case OP_PUSH_FLT64:
return 8;
default:
return -1;
}
}
// ============================================================
// Little-endian helpers (bytecode is always LE regardless of host)
// ============================================================
static int16_t readI16LE(const uint8_t *p) {
return (int16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint16_t readU16LE(const uint8_t *p) {
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}
static int32_t readI32LE(const uint8_t *p) {
return (int32_t)((uint32_t)p[0] |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24));
}
static void writeI16LE(uint8_t *p, int16_t v) {
uint16_t u = (uint16_t)v;
p[0] = (uint8_t)(u & 0xFF);
p[1] = (uint8_t)((u >> 8) & 0xFF);
}
static void writeU16LE(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)((v >> 8) & 0xFF);
}
static void writeI32LE(uint8_t *p, int32_t v) {
uint32_t u = (uint32_t)v;
p[0] = (uint8_t)(u & 0xFF);
p[1] = (uint8_t)((u >> 8) & 0xFF);
p[2] = (uint8_t)((u >> 16) & 0xFF);
p[3] = (uint8_t)((u >> 24) & 0xFF);
}
// ============================================================
// Walk bytecode, build remap
// ============================================================
//
// remap[oldPos] = newPos for every byte position in [0, oldCodeLen].
// For OP_LINE bytes (removed): remap points at where the NEXT instruction
// starts in the new code.
// Final entry remap[oldCodeLen] = newCodeLen.
//
// Returns malloc'd array of size (oldCodeLen + 1), or NULL on failure.
static int32_t *buildRemap(const uint8_t *code, int32_t codeLen, int32_t *outNewLen) {
int32_t *remap = (int32_t *)malloc((codeLen + 1) * sizeof(int32_t));
if (!remap) {
return NULL;
}
int32_t oldPc = 0;
int32_t newPc = 0;
while (oldPc < codeLen) {
uint8_t op = code[oldPc];
int32_t operand = opOperandSize(op);
if (operand < 0) {
free(remap);
return NULL;
}
int32_t instSize = 1 + operand;
if (oldPc + instSize > codeLen) {
free(remap);
return NULL;
}
if (op == OP_LINE) {
// These bytes are removed; they map to where the next instruction starts.
for (int32_t i = 0; i < instSize; i++) {
remap[oldPc + i] = newPc;
}
} else {
for (int32_t i = 0; i < instSize; i++) {
remap[oldPc + i] = newPc + i;
}
newPc += instSize;
}
oldPc += instSize;
}
if (oldPc != codeLen) {
free(remap);
return NULL;
}
remap[codeLen] = newPc;
*outNewLen = newPc;
return remap;
}
// ============================================================
// GOSUB pattern detection
// ============================================================
//
// GOSUB emits:
// oldPc: OP_PUSH_INT32 (1 byte)
// oldPc+1: int32 value V (4 bytes)
// oldPc+5: OP_JMP (1 byte)
// oldPc+6: int16 offset (2 bytes)
// oldPc+8: <next instruction>
// The invariant is V == oldPc + 8 (the pushed return address).
//
// Returns true if the given position is the start of such a pattern.
static bool isGosubPush(const uint8_t *code, int32_t codeLen, int32_t pos) {
if (pos + 8 > codeLen) {
return false;
}
if (code[pos] != OP_PUSH_INT32) {
return false;
}
if (code[pos + 5] != OP_JMP) {
return false;
}
int32_t value = readI32LE(code + pos + 1);
return value == pos + 8;
}
// ============================================================
// Apply remap to a single instruction's operand
// ============================================================
//
// Returns true on success, false if an offset overflows int16 or a
// target doesn't land on a valid instruction in the old code.
static bool remapAbsU16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, uint16_t oldAddr, const int32_t *remap, int32_t newCodeLen) {
int32_t newAddr = remap[oldAddr];
if (newAddr < 0 || newAddr > newCodeLen) {
return false;
}
if (newAddr > 0xFFFF) {
return false;
}
writeU16LE(newCode + newOpPos + operandOffset, (uint16_t)newAddr);
return true;
}
// Rewrite a relative int16 offset.
// oldOpPos, oldPcAfter: position of opcode and PC after reading offset
// newOpPos, newPcAfter: same in the new code
// operandOffset: byte offset from opcode to the int16 operand
// oldOffset: the offset as stored in the old code
//
// Handles ON_ERROR's special case (offset == 0 means "disable").
// For ON_ERROR the caller passes allowZero=true; the zero is preserved as-is.
static bool remapRelI16(uint8_t *newCode, int32_t newOpPos, int32_t operandOffset, int16_t oldOffset, int32_t oldPcAfter, int32_t newPcAfter, const int32_t *remap, int32_t codeLen, int32_t newCodeLen, bool allowZero) {
if (allowZero && oldOffset == 0) {
writeI16LE(newCode + newOpPos + operandOffset, 0);
return true;
}
int32_t oldTarget = oldPcAfter + oldOffset;
if (oldTarget < 0 || oldTarget > codeLen) {
return false;
}
int32_t newTarget = remap[oldTarget];
if (newTarget < 0 || newTarget > newCodeLen) {
return false;
}
int32_t newOffset = newTarget - newPcAfter;
if (newOffset < -32768 || newOffset > 32767) {
return false;
}
writeI16LE(newCode + newOpPos + operandOffset, (int16_t)newOffset);
return true;
}
// ============================================================
// basCompactBytecode
// ============================================================
int32_t basCompactBytecode(BasModuleT *mod) {
if (!mod || !mod->code || mod->codeLen <= 0) {
return 0;
}
const uint8_t *oldCode = mod->code;
int32_t oldCodeLen = mod->codeLen;
// Count OP_LINE occurrences. If none, nothing to do.
int32_t lineCount = 0;
{
int32_t pc = 0;
while (pc < oldCodeLen) {
uint8_t op = oldCode[pc];
int32_t operand = opOperandSize(op);
if (operand < 0 || pc + 1 + operand > oldCodeLen) {
return 0; // unknown opcode -- skip compaction
}
if (op == OP_LINE) {
lineCount++;
}
pc += 1 + operand;
}
if (pc != oldCodeLen) {
return 0;
}
if (lineCount == 0) {
return 0;
}
}
int32_t newCodeLen = 0;
int32_t *remap = buildRemap(oldCode, oldCodeLen, &newCodeLen);
if (!remap) {
return 0;
}
uint8_t *newCode = (uint8_t *)malloc(newCodeLen > 0 ? newCodeLen : 1);
if (!newCode) {
free(remap);
return 0;
}
// Copy bytes (skipping OP_LINE) and rewrite address operands.
bool ok = true;
int32_t oldPc = 0;
while (oldPc < oldCodeLen && ok) {
uint8_t op = oldCode[oldPc];
int32_t operand = opOperandSize(op);
int32_t instSize = 1 + operand;
if (op == OP_LINE) {
oldPc += instSize;
continue;
}
int32_t newPc = remap[oldPc];
// Copy the instruction verbatim first; we'll overwrite operands that
// need remapping below.
memcpy(newCode + newPc, oldCode + oldPc, instSize);
switch (op) {
case OP_CALL: {
uint16_t oldAddr = readU16LE(oldCode + oldPc + 1);
if (!remapAbsU16(newCode, newPc, 1, oldAddr, remap, newCodeLen)) {
ok = false;
}
break;
}
case OP_JMP:
case OP_JMP_TRUE:
case OP_JMP_FALSE: {
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
if (!remapRelI16(newCode, newPc, 1, oldOff,
oldPc + 3, newPc + 3,
remap, oldCodeLen, newCodeLen, false)) {
ok = false;
}
break;
}
case OP_FOR_NEXT: {
int16_t oldOff = readI16LE(oldCode + oldPc + 4);
if (!remapRelI16(newCode, newPc, 4, oldOff,
oldPc + 6, newPc + 6,
remap, oldCodeLen, newCodeLen, false)) {
ok = false;
}
break;
}
case OP_ON_ERROR: {
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
if (!remapRelI16(newCode, newPc, 1, oldOff,
oldPc + 3, newPc + 3,
remap, oldCodeLen, newCodeLen, true)) {
ok = false;
}
break;
}
case OP_PUSH_INT32: {
// Detect GOSUB return-address push and remap the absolute address.
if (isGosubPush(oldCode, oldCodeLen, oldPc)) {
int32_t oldAddr = readI32LE(oldCode + oldPc + 1);
if (oldAddr < 0 || oldAddr > oldCodeLen) {
ok = false;
break;
}
int32_t newAddr = remap[oldAddr];
if (newAddr < 0 || newAddr > newCodeLen) {
ok = false;
break;
}
writeI32LE(newCode + newPc + 1, newAddr);
}
break;
}
default:
break;
}
oldPc += instSize;
}
if (!ok) {
free(newCode);
free(remap);
return 0;
}
// Rewrite proc entry points
for (int32_t i = 0; i < mod->procCount; i++) {
int32_t oldAddr = mod->procs[i].codeAddr;
if (oldAddr < 0 || oldAddr > oldCodeLen) {
free(newCode);
free(remap);
return 0;
}
mod->procs[i].codeAddr = remap[oldAddr];
}
// Rewrite form-var init code addresses. Negative means "no init code".
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
int32_t oldAddr = mod->formVarInfo[i].initCodeAddr;
if (oldAddr < 0) {
continue;
}
if (oldAddr > oldCodeLen) {
free(newCode);
free(remap);
return 0;
}
int32_t oldLen = mod->formVarInfo[i].initCodeLen;
int32_t oldEnd = oldAddr + oldLen;
if (oldEnd > oldCodeLen) {
oldEnd = oldCodeLen;
}
int32_t newAddr = remap[oldAddr];
int32_t newEnd = remap[oldEnd];
mod->formVarInfo[i].initCodeAddr = newAddr;
mod->formVarInfo[i].initCodeLen = newEnd - newAddr;
}
// Rewrite entry point
if (mod->entryPoint >= 0 && mod->entryPoint <= oldCodeLen) {
mod->entryPoint = remap[mod->entryPoint];
}
// Swap in the new code
free(mod->code);
mod->code = newCode;
mod->codeLen = newCodeLen;
int32_t removed = oldCodeLen - newCodeLen;
free(remap);
return removed;
}

View file

@ -0,0 +1,21 @@
// compact.h -- Release build bytecode compaction
//
// Removes OP_LINE instructions from the module's bytecode and
// rewrites all code-address references (proc entries, CALL operands,
// JMP/FOR_NEXT/ON_ERROR relative offsets, GOSUB return addresses,
// and formVarInfo init code addresses).
//
// Safe by construction: if any sanity check fails (unknown opcode,
// offset overflow, walk not landing on codeLen, etc.), the module
// is left unchanged and the function returns 0.
#ifndef DVXBASIC_COMPACT_H
#define DVXBASIC_COMPACT_H
#include "../runtime/vm.h"
#include <stdint.h>
// Returns the number of bytes removed, or 0 if compaction was skipped.
int32_t basCompactBytecode(BasModuleT *mod);
#endif // DVXBASIC_COMPACT_H

View file

@ -77,6 +77,7 @@ static const KeywordEntryT sKeywords[] = {
{ "IF", TOK_IF },
{ "IMP", TOK_IMP },
{ "INIREAD", TOK_INIREAD },
{ "INIREAD$", TOK_INIREAD },
{ "INIWRITE", TOK_INIWRITE },
{ "INPUT", TOK_INPUT },
{ "INTEGER", TOK_INTEGER },
@ -682,7 +683,16 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
}
}
BasTokenTypeE kwType = lookupKeyword(lex->token.text, baseLen);
// Try the full text first (including any type suffix). Suffix-bearing
// keywords like CURDIR$, DIR$, INIREAD$, INPUTBOX$ are listed in the
// keyword table with their $ and will match here. If the full text
// isn't a keyword, fall back to the base name (without suffix).
BasTokenTypeE kwType = lookupKeyword(lex->token.text, idx);
bool matchedWithSuffix = (kwType != TOK_IDENT && baseLen != idx);
if (kwType == TOK_IDENT && baseLen != idx) {
kwType = lookupKeyword(lex->token.text, baseLen);
}
// REM is a comment -- skip to end of line
if (kwType == TOK_REM) {
@ -694,9 +704,9 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
return TOK_NEWLINE;
}
// If it's a keyword and has no suffix, return the keyword token.
// String-returning builtins (SQLError$, SQLField$) also match with $.
if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_INPUTBOX)) {
// Accept the keyword if it's a plain keyword (no suffix on source) or
// if it explicitly matched a $-suffixed entry in the keyword table.
if (kwType != TOK_IDENT && (baseLen == idx || matchedWithSuffix)) {
return kwType;
}

View file

@ -0,0 +1,590 @@
// obfuscate.c -- Release build name obfuscation
//
// See obfuscate.h for the high-level description.
#include "obfuscate.h"
#include "../runtime/values.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// Event suffix list (must match strip.c)
// ============================================================
static const char *sEventSuffixes[] = {
"Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate",
"Click", "DblClick", "Change", "Timer",
"GotFocus", "LostFocus",
"KeyPress", "KeyDown", "KeyUp",
"MouseDown", "MouseUp", "MouseMove",
"Scroll", "Reposition", "Validate",
NULL
};
// ============================================================
// Name map
// ============================================================
typedef struct {
char *orig; // original name (strdup'd, case-preserved)
char *mapped; // new name (strdup'd, "C1" .. "Cn")
} NameEntryT;
typedef struct {
NameEntryT *entries;
int32_t count;
int32_t cap;
} NameMapT;
static void nameMapInit(NameMapT *m) {
m->entries = NULL;
m->count = 0;
m->cap = 0;
}
static void nameMapFree(NameMapT *m) {
for (int32_t i = 0; i < m->count; i++) {
free(m->entries[i].orig);
free(m->entries[i].mapped);
}
free(m->entries);
m->entries = NULL;
m->count = 0;
m->cap = 0;
}
// Look up an original name (case-insensitive). Returns mapped name or NULL.
static const char *nameMapLookup(const NameMapT *m, const char *name) {
for (int32_t i = 0; i < m->count; i++) {
if (strcasecmp(m->entries[i].orig, name) == 0) {
return m->entries[i].mapped;
}
}
return NULL;
}
// Add a name if not already present. Returns mapped name.
static const char *nameMapAdd(NameMapT *m, const char *name) {
const char *existing = nameMapLookup(m, name);
if (existing) {
return existing;
}
if (m->count >= m->cap) {
int32_t newCap = m->cap == 0 ? 16 : m->cap * 2;
NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT));
if (!newEntries) {
return NULL;
}
m->entries = newEntries;
m->cap = newCap;
}
char mapped[16];
snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));
m->entries[m->count].orig = strdup(name);
m->entries[m->count].mapped = strdup(mapped);
m->count++;
return m->entries[m->count - 1].mapped;
}
// ============================================================
// .frm parsing helpers
// ============================================================
// Skip ASCII whitespace. Returns pointer past whitespace.
static const char *skipWhitespace(const char *p, const char *end) {
while (p < end && (*p == ' ' || *p == '\t')) {
p++;
}
return p;
}
// Copy next whitespace-delimited token into buf. Returns pointer after token.
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize) {
int32_t len = 0;
while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && len < bufSize - 1) {
buf[len++] = *p++;
}
buf[len] = '\0';
return p;
}
// Check if name is a valid identifier (letters, digits, underscore, starts non-digit)
static bool isValidIdent(const char *name) {
if (!name || !*name) {
return false;
}
if (!isalpha((unsigned char)name[0]) && name[0] != '_') {
return false;
}
for (const char *p = name; *p; p++) {
if (!isalnum((unsigned char)*p) && *p != '_') {
return false;
}
}
return true;
}
// ============================================================
// Pass 1: collect all form/control names from .frm texts
// ============================================================
// Scan a .frm text and add all "Begin <Type> <Name>" names to the map.
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *map) {
const char *p = text;
const char *end = text + len;
while (p < end) {
// Read one line
const char *lineStart = p;
while (p < end && *p != '\n' && *p != '\r') {
p++;
}
const char *lineEnd = p;
if (p < end && *p == '\r') {
p++;
}
if (p < end && *p == '\n') {
p++;
}
// Trim leading whitespace
const char *l = skipWhitespace(lineStart, lineEnd);
// Check "Begin "
if ((lineEnd - l) < 6 || strncasecmp(l, "Begin ", 6) != 0) {
continue;
}
l += 6;
l = skipWhitespace(l, lineEnd);
// Read type name
char typeName[64];
l = readToken(l, lineEnd, typeName, sizeof(typeName));
if (typeName[0] == '\0') {
continue;
}
// Read control name
l = skipWhitespace(l, lineEnd);
char ctrlName[64];
l = readToken(l, lineEnd, ctrlName, sizeof(ctrlName));
if (ctrlName[0] && isValidIdent(ctrlName)) {
nameMapAdd(map, ctrlName);
}
}
}
// ============================================================
// Pass 2: strip BASIC code from .frm text (everything after outer End)
// ============================================================
// Find the position just after the matching End of the outermost Begin Form.
// Returns len of the stripped .frm. If no Begin Form found, returns original len.
static int32_t findFormEndPos(const char *text, int32_t len) {
int32_t nesting = 0;
bool inForm = false;
const char *p = text;
const char *end = text + len;
while (p < end) {
const char *lineStart = p;
while (p < end && *p != '\n' && *p != '\r') {
p++;
}
const char *lineEnd = p;
if (p < end && *p == '\r') {
p++;
}
if (p < end && *p == '\n') {
p++;
}
const char *l = skipWhitespace(lineStart, lineEnd);
if ((lineEnd - l) >= 6 && strncasecmp(l, "Begin ", 6) == 0) {
// Check for "Begin Form ..." to set inForm on outer open
if (!inForm) {
const char *r = l + 6;
r = skipWhitespace(r, lineEnd);
if ((lineEnd - r) >= 5 && strncasecmp(r, "Form ", 5) == 0) {
inForm = true;
}
}
nesting++;
} else if ((lineEnd - l) >= 3 && strncasecmp(l, "End", 3) == 0 &&
(lineEnd - l == 3 || l[3] == ' ' || l[3] == '\t' || l[3] == '\r')) {
nesting--;
if (inForm && nesting == 0) {
return (int32_t)(p - text);
}
}
}
return len;
}
// ============================================================
// Pass 3: rewrite .frm text with mapped names
// ============================================================
// Returns true if c is a valid identifier character.
static bool isIdentChar(int c) {
return isalnum(c) || c == '_';
}
// Scan text; for each identifier found outside of strings, if it's in
// the map, emit the mapped name instead. Output to out (returns bytes written).
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) {
int32_t outLen = 0;
int32_t i = 0;
bool inStr = false;
while (i < srcLen) {
char c = src[i];
if (c == '"') {
inStr = !inStr;
if (outLen < outCap) {
out[outLen++] = (uint8_t)c;
}
i++;
continue;
}
// Read identifier
if (!inStr && (isalpha((unsigned char)c) || c == '_')) {
int32_t identStart = i;
while (i < srcLen && isIdentChar((unsigned char)src[i])) {
i++;
}
int32_t identLen = i - identStart;
char ident[128];
if (identLen >= (int32_t)sizeof(ident)) {
identLen = (int32_t)sizeof(ident) - 1;
}
memcpy(ident, src + identStart, identLen);
ident[identLen] = '\0';
const char *mapped = nameMapLookup(map, ident);
if (mapped) {
int32_t mLen = (int32_t)strlen(mapped);
for (int32_t k = 0; k < mLen && outLen < outCap; k++) {
out[outLen++] = (uint8_t)mapped[k];
}
} else {
for (int32_t k = 0; k < identLen && outLen < outCap; k++) {
out[outLen++] = (uint8_t)ident[k];
}
}
continue;
}
if (outLen < outCap) {
out[outLen++] = (uint8_t)c;
}
i++;
}
return outLen;
}
// ============================================================
// Module rewriting
// ============================================================
// Replace the contents of a constant pool entry with a new string.
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) {
BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText));
if (!newStr) {
return;
}
basStringUnref(mod->constants[idx]);
mod->constants[idx] = newStr;
}
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
for (int32_t i = 0; i < mod->constCount; i++) {
const BasStringT *s = mod->constants[i];
if (!s) {
continue;
}
const char *mapped = nameMapLookup(map, s->data);
if (mapped) {
replaceConstant(mod, i, mapped);
}
}
}
// Check if suffix is a known event name.
static bool isEventSuffix(const char *suffix) {
for (int32_t i = 0; sEventSuffixes[i]; i++) {
if (strcasecmp(suffix, sEventSuffixes[i]) == 0) {
return true;
}
}
return false;
}
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
for (int32_t i = 0; i < mod->procCount; i++) {
BasProcEntryT *proc = &mod->procs[i];
if (proc->name[0] == '\0') {
continue;
}
// Find last underscore
char *underscore = strrchr(proc->name, '_');
if (!underscore) {
continue;
}
const char *suffix = underscore + 1;
if (!isEventSuffix(suffix)) {
continue;
}
// Split on underscore
int32_t prefixLen = (int32_t)(underscore - proc->name);
char prefix[BAS_MAX_PROC_NAME];
if (prefixLen >= (int32_t)sizeof(prefix)) {
prefixLen = (int32_t)sizeof(prefix) - 1;
}
memcpy(prefix, proc->name, prefixLen);
prefix[prefixLen] = '\0';
const char *mapped = nameMapLookup(map, prefix);
if (mapped) {
char newName[BAS_MAX_PROC_NAME];
snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix);
snprintf(proc->name, sizeof(proc->name), "%s", newName);
}
}
}
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map) {
for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
BasFormVarInfoT *fv = &mod->formVarInfo[i];
const char *mapped = nameMapLookup(map, fv->formName);
if (mapped) {
snprintf(fv->formName, sizeof(fv->formName), "%s", mapped);
}
}
}
// ============================================================
// basStripFrmComments
// ============================================================
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap) {
if (!src || srcLen <= 0 || !outBuf || outCap <= 0) {
return 0;
}
int32_t outLen = 0;
int32_t i = 0;
while (i < srcLen) {
int32_t lineStart = i;
while (i < srcLen && src[i] != '\n' && src[i] != '\r') {
i++;
}
int32_t lineEnd = i;
if (i < srcLen && src[i] == '\r') {
i++;
}
if (i < srcLen && src[i] == '\n') {
i++;
}
// Scan for first unquoted ' (comment start).
bool inStr = false;
int32_t commentStart = -1;
for (int32_t j = lineStart; j < lineEnd; j++) {
char c = src[j];
if (c == '"') {
inStr = !inStr;
} else if (c == '\'' && !inStr) {
commentStart = j;
break;
}
}
int32_t contentEnd = (commentStart >= 0) ? commentStart : lineEnd;
// Check for whole-line REM. Find first non-whitespace position.
int32_t firstNonWs = lineStart;
while (firstNonWs < contentEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) {
firstNonWs++;
}
if (contentEnd - firstNonWs >= 3 &&
strncasecmp(src + firstNonWs, "REM", 3) == 0 &&
(contentEnd - firstNonWs == 3 ||
src[firstNonWs + 3] == ' ' ||
src[firstNonWs + 3] == '\t')) {
contentEnd = firstNonWs;
}
// Trim trailing whitespace.
while (contentEnd > lineStart && (src[contentEnd - 1] == ' ' || src[contentEnd - 1] == '\t')) {
contentEnd--;
}
// Drop lines that have no non-whitespace content.
if (contentEnd <= firstNonWs) {
continue;
}
int32_t writeLen = contentEnd - lineStart;
if (outLen + writeLen + 1 >= outCap) {
break;
}
memcpy(outBuf + outLen, src + lineStart, writeLen);
outLen += writeLen;
outBuf[outLen++] = '\n';
}
return outLen;
}
// ============================================================
// Top-level entry point
// ============================================================
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) {
if (!mod || frmCount < 0) {
return;
}
NameMapT map;
nameMapInit(&map);
// Pass 1: collect all names from all .frm texts
for (int32_t i = 0; i < frmCount; i++) {
if (frmTexts[i] && frmLens[i] > 0) {
collectNamesFromFrm(frmTexts[i], frmLens[i], &map);
}
}
// Pass 2: rewrite each .frm
for (int32_t i = 0; i < frmCount; i++) {
outFrms[i].data = NULL;
outFrms[i].len = 0;
if (!frmTexts[i] || frmLens[i] <= 0) {
continue;
}
int32_t strippedLen = findFormEndPos(frmTexts[i], frmLens[i]);
// Allocate generous output buffer (mapped names are usually shorter
// than originals, but allow for growth and a trailing newline).
int32_t outCap = strippedLen + 1024;
uint8_t *outBuf = malloc(outCap);
if (!outBuf) {
continue;
}
int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap);
// Ensure trailing newline
if (outLen > 0 && outBuf[outLen - 1] != '\n' && outLen < outCap) {
outBuf[outLen++] = '\n';
}
outFrms[i].data = outBuf;
outFrms[i].len = outLen;
}
// Pass 3: rewrite module
rewriteModuleConstants(mod, &map);
rewriteModuleProcs(mod, &map);
rewriteModuleFormVars(mod, &map);
nameMapFree(&map);
}

View file

@ -0,0 +1,46 @@
// obfuscate.h -- Release build name obfuscation
//
// Replaces form and control names with generated tokens (C1, C2, ...)
// across the compiled module AND the raw .frm text resources. This
// hinders casual decompilation by removing meaningful identifiers
// from event handler names (e.g., BtnHello_Click -> C3_Click).
#ifndef DVXBASIC_OBFUSCATE_H
#define DVXBASIC_OBFUSCATE_H
#include "../runtime/vm.h"
// One .frm text after obfuscation (data is newly-allocated).
typedef struct {
uint8_t *data;
int32_t len;
} BasObfFrmT;
// Obfuscate form/control names in the module and all .frm texts.
//
// Reads original names from the Begin declarations in each .frm,
// generates C1..Cn, then rewrites:
// - The .frm text (form/control name declarations, stripping the
// trailing BASIC code section after the outer form closes)
// - Module string constants matching any original name
// - Procedure names matching <OrigName>_<Event>
// - formVarInfo entries keyed by form name
//
// frmTexts / frmLens: input .frm buffers (one per form).
// outFrms: caller-allocated array of frmCount entries; function fills
// in .data (malloc'd, caller frees) and .len for each.
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
// Strip comments from .frm text.
//
// Removes both whole-line comments (lines whose first non-whitespace
// token is `'` or `REM`) and trailing comments (everything from the
// first unquoted `'` to end of line). Pure-whitespace lines are
// dropped. Quoted string literals are preserved as-is.
//
// srcLen: input length; outBuf: caller-allocated buffer of outCap bytes.
// Returns the number of bytes written.
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
#endif // DVXBASIC_OBFUSCATE_H

View file

@ -235,6 +235,9 @@
#define OP_MATH_EXP 0xAA
#define OP_MATH_RND 0xAB
#define OP_MATH_RANDOMIZE 0xAC // seed on stack (or TIMER if -1)
#define OP_RGB 0xAD // pop b, g, r; push LONG = (r<<16)|(g<<8)|b
#define OP_GET_RED 0xAE // pop LONG color; push (color>>16) & 0xFF
#define OP_GET_GREEN 0xAF // pop LONG color; push (color>>8) & 0xFF
// ============================================================
// Conversion built-ins
@ -317,6 +320,8 @@
#define OP_CALL_EXTERN 0xCD // [uint16 libNameIdx] [uint16 funcNameIdx] [uint8 argc] [uint8 retType]
#define OP_GET_BLUE 0xD0 // pop LONG color; push color & 0xFF
// App object
#define OP_APP_PATH 0xDD // push App.Path string
#define OP_APP_CONFIG 0xDE // push App.Config string

View file

@ -77,8 +77,12 @@ static const BuiltinFuncT builtinFuncs[] = {
{"COS", OP_MATH_COS, 1, 1, BAS_TYPE_DOUBLE},
{"EXP", OP_MATH_EXP, 1, 1, BAS_TYPE_DOUBLE},
{"FIX", OP_MATH_FIX, 1, 1, BAS_TYPE_INTEGER},
{"GETBLUE", OP_GET_BLUE, 1, 1, BAS_TYPE_INTEGER},
{"GETGREEN", OP_GET_GREEN, 1, 1, BAS_TYPE_INTEGER},
{"GETRED", OP_GET_RED, 1, 1, BAS_TYPE_INTEGER},
{"INT", OP_MATH_INT, 1, 1, BAS_TYPE_INTEGER},
{"LOG", OP_MATH_LOG, 1, 1, BAS_TYPE_DOUBLE},
{"RGB", OP_RGB, 3, 3, BAS_TYPE_LONG},
{"RND", OP_MATH_RND, 0, 1, BAS_TYPE_DOUBLE},
{"SGN", OP_MATH_SGN, 1, 1, BAS_TYPE_INTEGER},
{"SIN", OP_MATH_SIN, 1, 1, BAS_TYPE_DOUBLE},
@ -1250,7 +1254,9 @@ static void parsePrimary(BasParserT *p) {
} else if (checkKeyword(p,"Config")) {
advance(p);
basEmit8(&p->cg, OP_APP_CONFIG);
} else if (checkKeyword(p,"Data")) {
} else if (checkKeyword(p,"Data") || p->lex.token.type == TOK_DATA) {
// "Data" tokenizes as TOK_DATA (the DATA/READ keyword) rather
// than TOK_IDENT, so accept it directly by token type too.
advance(p);
basEmit8(&p->cg, OP_APP_DATA);
} else {
@ -1482,23 +1488,32 @@ static void parsePrimary(BasParserT *p) {
return;
}
// MsgBox(message [, flags]) -- as function expression returning button ID
// MsgBox(message [, flags [, title]]) -- function form returning button ID
if (tt == TOK_MSGBOX) {
advance(p);
expect(p, TOK_LPAREN);
parseExpression(p); // message
if (match(p, TOK_COMMA)) {
parseExpression(p); // flags
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // default flags = vbOKOnly
}
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
}
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_MSGBOX);
return;
}
// SQL expression functions -- all require parentheses
// IniRead$(file, section, key, default)
if (tt == TOK_INIREAD) {
advance(p);
@ -2251,6 +2266,17 @@ static void parseAssignOrCall(BasParserT *p) {
error(p, buf);
return;
}
// External library SUB: emit OP_CALL_EXTERN
if (sym->isExtern) {
basEmit8(&p->cg, OP_CALL_EXTERN);
basEmitU16(&p->cg, sym->externLibIdx);
basEmitU16(&p->cg, sym->externFuncIdx);
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, sym->dataType);
return;
}
{
uint8_t baseSlot = (sym->kind == SYM_FUNCTION) ? 1 : 0;
basEmit8(&p->cg, OP_CALL);
@ -2371,7 +2397,7 @@ static void parseClose(BasParserT *p) {
static void parseConst(BasParserT *p) {
// CONST name = value
// CONST name [AS type] = value
advance(p); // consume CONST
if (!check(p, TOK_IDENT)) {
@ -2384,6 +2410,12 @@ static void parseConst(BasParserT *p) {
name[BAS_MAX_TOKEN_LEN - 1] = '\0';
advance(p);
// Optional type annotation (declarative; value's literal type still
// determines runtime representation).
if (match(p, TOK_AS)) {
(void)resolveTypeName(p);
}
expect(p, TOK_EQ);
// Parse the constant value (must be a literal)
@ -2711,9 +2743,25 @@ static void parseDeclareLibrary(BasParserT *p) {
char funcName[BAS_MAX_TOKEN_LEN];
strncpy(funcName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
funcName[BAS_MAX_TOKEN_LEN - 1] = '\0';
uint16_t funcNameIdx = basAddConstant(&p->cg, funcName, (int32_t)strlen(funcName));
advance(p);
// Strip type suffix ($%&!#) from extern name so dlsym finds the
// C function. The suffix is still used for return type via suffixToType.
char externName[BAS_MAX_TOKEN_LEN];
strncpy(externName, funcName, BAS_MAX_TOKEN_LEN - 1);
externName[BAS_MAX_TOKEN_LEN - 1] = '\0';
int32_t enLen = (int32_t)strlen(externName);
if (enLen > 0) {
char last = externName[enLen - 1];
if (last == '$' || last == '%' || last == '&' || last == '!' || last == '#') {
externName[enLen - 1] = '\0';
}
}
uint16_t funcNameIdx = basAddConstant(&p->cg, externName, (int32_t)strlen(externName));
// Parse parameter list
int32_t paramCount = 0;
uint8_t paramTypes[BAS_MAX_PARAMS];
@ -4191,20 +4239,49 @@ static void parsePrint(BasParserT *p) {
// PRINT USING "fmt"; expr [; expr] ...
advance(p); // consume PRINT
// Check for file I/O: PRINT #channel, expr
// File I/O: PRINT #channel, expr [; expr | , expr ]* [;]
//
// Channel is pushed once and DUP'd per value. `;` means no separator
// between values; `,` is treated the same (tab-zone separator not
// supported for file output). Trailing `;` suppresses the final newline.
if (check(p, TOK_HASH)) {
advance(p); // consume #
// Channel number
// Channel number -- stays on stack as "keep" for the whole statement.
parseExpression(p);
// Comma separator
expect(p, TOK_COMMA);
// Value to print
parseExpression(p);
bool trailingSep = false;
for (;;) {
// Duplicate the channel for this OP_FILE_PRINT.
basEmit8(&p->cg, OP_DUP);
parseExpression(p);
basEmit8(&p->cg, OP_FILE_PRINT);
if (check(p, TOK_SEMICOLON) || check(p, TOK_COMMA)) {
advance(p);
trailingSep = true;
if (check(p, TOK_NEWLINE) || check(p, TOK_EOF) || check(p, TOK_COLON) || check(p, TOK_ELSE)) {
break;
}
trailingSep = false;
continue;
}
break;
}
// No trailing ; or , -> write newline. Otherwise, just drop the
// kept channel value.
if (trailingSep) {
basEmit8(&p->cg, OP_POP);
} else {
basEmit8(&p->cg, OP_FILE_WRITE_NL);
}
basEmit8(&p->cg, OP_FILE_PRINT);
return;
}
@ -5187,21 +5264,31 @@ static void parseStatement(BasParserT *p) {
basEmit8(&p->cg, OP_POP); // discard result
break;
case TOK_MSGBOX:
// MsgBox message [, flags] (statement form, discard result)
case TOK_MSGBOX: {
// MsgBox message [, flags [, title]] (statement form, discards result)
advance(p);
parseExpression(p); // message
if (match(p, TOK_COMMA)) {
parseExpression(p); // flags
} else {
basEmit8(&p->cg, OP_PUSH_INT16);
basEmit16(&p->cg, 0); // default flags = MB_OK
}
if (match(p, TOK_COMMA)) {
parseExpression(p); // title
} else {
uint16_t emptyIdx = basAddConstant(&p->cg, "", 0);
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, emptyIdx);
}
basEmit8(&p->cg, OP_MSGBOX);
basEmit8(&p->cg, OP_POP); // discard result
break;
}
// SQL statement forms (no return value)
case TOK_INIWRITE:
// IniWrite file, section, key, value
advance(p);

View file

@ -3,19 +3,68 @@
// Removes debug information from a compiled module:
// - Clears debug variable info (names, scopes, types)
// - Clears debug UDT definitions
//
// Procedure names are preserved because the form runtime uses
// them for event dispatch (ControlName_EventName convention).
// - Mangles procedure names that aren't needed for runtime dispatch.
// The form runtime dispatches events by name (Control_Event pattern)
// and SetEvent looks up handlers by name at runtime, so those proc
// names must be preserved. Everything else becomes F1, F2, F3...
//
// OP_LINE removal is deferred to a future version (requires
// bytecode compaction and offset rewriting).
#include "strip.h"
#include "../runtime/values.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Events fired by name via basFormRtFireEvent* in formrt.c. Any proc
// ending in "_<EventName>" must keep its name so the dispatcher can
// find it.
static const char *sEventSuffixes[] = {
"Load", "Unload", "QueryUnload", "Resize", "Activate", "Deactivate",
"Click", "DblClick", "Change", "Timer",
"GotFocus", "LostFocus",
"KeyPress", "KeyDown", "KeyUp",
"MouseDown", "MouseUp", "MouseMove",
"Scroll", "Reposition", "Validate",
NULL
};
static bool nameEndsWithEventSuffix(const char *name) {
const char *underscore = strrchr(name, '_');
if (!underscore) {
return false;
}
const char *suffix = underscore + 1;
for (int32_t i = 0; sEventSuffixes[i]; i++) {
if (strcasecmp(suffix, sEventSuffixes[i]) == 0) {
return true;
}
}
return false;
}
static bool nameInConstantPool(const BasModuleT *mod, const char *name) {
for (int32_t i = 0; i < mod->constCount; i++) {
const BasStringT *s = mod->constants[i];
if (s && strcasecmp(s->data, name) == 0) {
return true;
}
}
return false;
}
void basStripModule(BasModuleT *mod) {
if (!mod) {
return;
@ -36,4 +85,27 @@ void basStripModule(BasModuleT *mod) {
mod->debugUdtDefs = NULL;
mod->debugUdtDefCount = 0;
}
// Mangle proc names. Keep names that are needed for runtime name
// lookup: event handlers (Control_Event pattern) and any name
// referenced as a string constant (e.g. SetEvent's target name).
int32_t nextMangled = 1;
for (int32_t i = 0; i < mod->procCount; i++) {
BasProcEntryT *proc = &mod->procs[i];
if (proc->name[0] == '\0') {
continue;
}
if (nameEndsWithEventSuffix(proc->name)) {
continue;
}
if (nameInConstantPool(mod, proc->name)) {
continue;
}
snprintf(proc->name, sizeof(proc->name), "F%ld", (long)nextMangled++);
}
}

View file

@ -1,8 +1,9 @@
// strip.h -- Release build stripping
//
// Removes debug information from a compiled module to prevent
// decompilation. Clears procedure names, debug variable info,
// and debug UDT definitions.
// Removes debug information from a compiled module to hinder
// decompilation. Clears debug variable info and debug UDT
// definitions, and mangles proc names that aren't needed for
// runtime name-based dispatch.
#ifndef DVXBASIC_STRIP_H
#define DVXBASIC_STRIP_H
@ -10,9 +11,11 @@
#include "../runtime/vm.h"
// Strip debug info from a module for release builds:
// - Clear all procedure names
// - Clear debug variable info
// - Clear debug UDT definitions
// - Mangle proc names to F1, F2, ... except for event handlers
// (matched by Control_Event suffix) and names referenced as
// string constants (SetEvent dispatch targets).
void basStripModule(BasModuleT *mod);
#endif // DVXBASIC_STRIP_H

View file

@ -8,6 +8,7 @@
#include "formcfm.h"
#include "../compiler/opcodes.h"
#include "dvxDlg.h"
#include "dvxRes.h"
#include "dvxWm.h"
#include "box/box.h"
#include "ansiTerm/ansiTerm.h"
@ -1291,20 +1292,27 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
}
}
// Check the .frm cache for reload after unload
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
// Save source and remove from cache BEFORE calling basFormRtLoadFrm,
// because basFormRtLoadFrm internally calls basFormRtLoadForm (for the
// "Begin Form" line) which would find this cache entry again and recurse.
char *src = rt->frmCache[i].frmSource;
int32_t srcLen = rt->frmCache[i].frmSourceLen;
arrdel(rt->frmCache, i);
rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
// Check the .frm cache for reload after unload.
// Use a static guard to prevent recursion: basFormRtLoadFrm calls
// basFormRtLoadForm for the "Begin Form" line, which would re-enter
// this function. The guard lets the recursive call fall through to
// bare form creation, which basFormRtLoadFrm then populates.
static bool sLoadingFrm = false;
BasFormT *form = basFormRtLoadFrm(rt, src, srcLen);
free(src);
return form;
if (!sLoadingFrm) {
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
char *src = rt->frmCache[i].frmSource;
int32_t srcLen = rt->frmCache[i].frmSourceLen;
arrdel(rt->frmCache, i);
rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
sLoadingFrm = true;
BasFormT *form = basFormRtLoadFrm(rt, src, srcLen);
sLoadingFrm = false;
free(src);
return form;
}
}
}
@ -1701,6 +1709,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
}
val = basValStringFromC(value + 1);
} else if (strcasecmp(value, "True") == 0) {
val = basValBool(true);
} else if (strcasecmp(value, "False") == 0) {
val = basValBool(false);
} else {
val = basValLong(atoi(value));
}
@ -1884,6 +1896,38 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
}
}
// Allocate per-form variable storage and run init code.
// This must happen AFTER controls are created (so init code can
// reference controls by name) but BEFORE Form_Load fires.
if (form && rt->module && rt->module->formVarInfo) {
for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) {
if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) != 0) {
continue;
}
if (!form->formVars) {
int32_t vc = rt->module->formVarInfo[j].varCount;
if (vc > 0) {
form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT));
form->formVarCount = vc;
}
}
int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr;
if (initAddr >= 0 && rt->vm) {
basVmSetCurrentForm(rt->vm, form);
basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
basVmCallSub(rt->vm, initAddr);
basVmSetCurrentForm(rt->vm, NULL);
basVmSetCurrentFormVars(rt->vm, NULL, 0);
}
break;
}
}
// Fire the Load event now that the form and controls are ready
if (form) {
basFormRtFireEvent(rt, form, form->name, "Load");
@ -1928,10 +1972,10 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
// basFormRtMsgBox
// ============================================================
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags) {
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title) {
BasFormRtT *rt = (BasFormRtT *)ctx;
return dvxMessageBox(rt->ctx, "DVX BASIC", message, flags);
return dvxMessageBox(rt->ctx, (title && title[0]) ? title : "DVX BASIC", message, flags);
}
@ -2049,14 +2093,12 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
return;
}
// "Caption" and "Text": save to the persistent textBuf and apply
// immediately. Controls are heap-allocated so textBuf addresses
// are stable across arrput calls.
// "Caption" and "Text": pass directly to the widget (all widgets
// strdup their text internally).
if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
BasStringT *s = basValFormatString(value);
snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", s->data);
wgtSetText(ctrl->widget, s->data);
basStringUnref(s);
wgtSetText(ctrl->widget, ctrl->textBuf);
return;
}
@ -2129,6 +2171,11 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
return;
}
// QueryUnload: give the form a chance to cancel
if (basFormRtFireEventWithCancel(rt, form, form->name, "QueryUnload")) {
return;
}
basFormRtFireEvent(rt, form, form->name, "Unload");
// Release per-form variables
@ -2253,8 +2300,12 @@ WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) {
CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
return fn(parent, (bool)iface->createArgs[0]);
}
case WGT_CREATE_PARENT_DATA:
return NULL;
case WGT_CREATE_PARENT_DATA: {
// create(parent, NULL, 0, 0, 0) -- empty widget, load content later via properties
typedef WidgetT *(*CreateDataFnT)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t);
CreateDataFnT fn = *(CreateDataFnT *)api;
return fn(parent, NULL, 0, 0, 0);
}
default: {
CreateParentFnT fn = *(CreateParentFnT *)api;
return fn(parent);
@ -2769,8 +2820,7 @@ static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) {
const char *val = wgtDataCtrlGetField(dataCtrl->widget, ctrl->dataField);
if (val) {
snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", val);
wgtSetText(ctrl->widget, ctrl->textBuf);
wgtSetText(ctrl->widget, val);
}
}
}
@ -3214,21 +3264,64 @@ static BasValueT zeroValue(void) {
// need C-side storage since BASIC strings can't be used as
// writable char pointers.
// Parse a filter string into FileFilterT entries.
//
// Format: "Label (pattern)|Label (pattern)|..."
// The pattern is extracted from inside the parentheses.
// Example: "Images (*.bmp;*.png;*.jpg)|Text (*.txt)|All Files (*.*)"
//
// If no parentheses, the entire entry is used as both label and pattern.
// If no pipe, treat as a single pattern for backward compatibility.
#define BAS_MAX_FILE_FILTERS 16
static int32_t parseFileFilters(const char *filter, FileFilterT *out, char *buf, int32_t bufSize) {
if (!filter || !filter[0]) {
out[0].label = "All Files (*.*)";
return 1;
}
// No pipe and no parentheses = old-style single pattern, wrap it
if (!strchr(filter, '|') && !strchr(filter, '(')) {
snprintf(buf, bufSize, "%s (%s)", filter, filter);
out[0].label = buf;
out[1].label = "All Files (*.*)";
return 2;
}
snprintf(buf, bufSize, "%s", filter);
int32_t count = 0;
char *p = buf;
while (*p && count < BAS_MAX_FILE_FILTERS) {
char *pipe = strchr(p, '|');
if (pipe) {
*pipe = '\0';
}
out[count].label = p;
count++;
p = pipe ? pipe + 1 : p + strlen(p);
}
return count > 0 ? count : 1;
}
const char *basFileOpen(const char *title, const char *filter) {
if (!sFormRt) {
return "";
}
FileFilterT filters[2];
filters[0].label = filter;
filters[0].pattern = filter;
filters[1].label = "All Files (*.*)";
filters[1].pattern = "*.*";
FileFilterT filters[BAS_MAX_FILE_FILTERS];
char filterBuf[1024];
int32_t count = parseFileFilters(filter, filters, filterBuf, sizeof(filterBuf));
static char path[260];
static char path[DVX_MAX_PATH];
path[0] = '\0';
if (dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
if (dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, count, path, sizeof(path))) {
return path;
}
@ -3241,16 +3334,14 @@ const char *basFileSave(const char *title, const char *filter) {
return "";
}
FileFilterT filters[2];
filters[0].label = filter;
filters[0].pattern = filter;
filters[1].label = "All Files (*.*)";
filters[1].pattern = "*.*";
FileFilterT filters[BAS_MAX_FILE_FILTERS];
char filterBuf[1024];
int32_t count = parseFileFilters(filter, filters, filterBuf, sizeof(filterBuf));
static char path[260];
static char path[DVX_MAX_PATH];
path[0] = '\0';
if (dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
if (dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, count, path, sizeof(path))) {
return path;
}
@ -4277,10 +4368,244 @@ void HelpView(const char *hlpFile) {
return;
}
char viewerPath[260];
// If hlpFile has no directory component, resolve it against the calling
// app's directory. This matches the convention where apps bundle their
// help file alongside the .app.
char resolved[DVX_MAX_PATH];
bool hasDir = strchr(hlpFile, '/') != NULL || strchr(hlpFile, '\\') != NULL || (hlpFile[0] && hlpFile[1] == ':');
if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) {
snprintf(resolved, sizeof(resolved), "%s%c%s", sFormRt->vm->appPath, DVX_PATH_SEP, hlpFile);
} else {
snprintf(resolved, sizeof(resolved), "%s", hlpFile);
}
char viewerPath[DVX_MAX_PATH];
snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP);
sShellLoadApp(sFormRt->ctx, viewerPath, hlpFile);
sShellLoadApp(sFormRt->ctx, viewerPath, resolved);
}
// ============================================================
// Resource file extern wrappers (DECLARE LIBRARY "basrt")
//
// Expose the DVX resource API to BASIC programs. Each function
// is a thin wrapper around the C resource API.
// ============================================================
// Maximum number of simultaneously open resource handles
#define RES_MAX_HANDLES 8
static DvxResHandleT *sResHandles[RES_MAX_HANDLES];
int32_t ResOpen(const char *path) {
if (!path) {
return 0;
}
for (int32_t i = 0; i < RES_MAX_HANDLES; i++) {
if (!sResHandles[i]) {
sResHandles[i] = dvxResOpen(path);
if (sResHandles[i]) {
return i + 1;
}
return 0;
}
}
return 0;
}
void ResClose(int32_t handle) {
int32_t idx = handle - 1;
if (idx < 0 || idx >= RES_MAX_HANDLES || !sResHandles[idx]) {
return;
}
dvxResClose(sResHandles[idx]);
sResHandles[idx] = NULL;
}
int32_t ResCount(int32_t handle) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx]) {
return (int32_t)sResHandles[idx]->entryCount;
}
return 0;
}
const char *ResName(int32_t handle, int32_t index) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
return sResHandles[idx]->entries[index].name;
}
return "";
}
int32_t ResType(int32_t handle, int32_t index) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
return (int32_t)sResHandles[idx]->entries[index].type;
}
return 0;
}
int32_t ResSize(int32_t handle, int32_t index) {
int32_t idx = handle - 1;
if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
return (int32_t)sResHandles[idx]->entries[index].size;
}
return 0;
}
const char *ResGetText(const char *path, const char *name) {
static char sBuf[1024];
sBuf[0] = '\0';
if (!path || !name) {
return sBuf;
}
DvxResHandleT *h = dvxResOpen(path);
if (!h) {
return sBuf;
}
uint32_t size = 0;
void *data = dvxResRead(h, name, &size);
dvxResClose(h);
if (data) {
uint32_t copyLen = size;
if (copyLen >= sizeof(sBuf)) {
copyLen = sizeof(sBuf) - 1;
}
memcpy(sBuf, data, copyLen);
sBuf[copyLen] = '\0';
free(data);
}
return sBuf;
}
int32_t ResAddText(const char *path, const char *name, const char *text) {
if (!path || !name) {
return 0;
}
if (!text) {
text = "";
}
return dvxResAppend(path, name, DVX_RES_TEXT, text, (uint32_t)strlen(text) + 1) == 0 ? -1 : 0;
}
int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) {
if (!path || !name || !srcFile) {
return 0;
}
FILE *f = fopen(srcFile, "rb");
if (!f) {
return 0;
}
fseek(f, 0, SEEK_END);
long fileSize = ftell(f);
fseek(f, 0, SEEK_SET);
if (fileSize <= 0) {
fclose(f);
return 0;
}
uint8_t *buf = (uint8_t *)malloc((size_t)fileSize);
if (!buf) {
fclose(f);
return 0;
}
if (fread(buf, 1, (size_t)fileSize, f) != (size_t)fileSize) {
free(buf);
fclose(f);
return 0;
}
fclose(f);
int32_t result = dvxResAppend(path, name, (uint32_t)type, buf, (uint32_t)fileSize) == 0 ? -1 : 0;
free(buf);
return result;
}
int32_t ResRemove(const char *path, const char *name) {
if (!path || !name) {
return 0;
}
return dvxResRemove(path, name) == 0 ? -1 : 0;
}
int32_t ResExtract(const char *path, const char *name, const char *outFile) {
if (!path || !name || !outFile) {
return 0;
}
DvxResHandleT *h = dvxResOpen(path);
if (!h) {
return 0;
}
uint32_t size = 0;
void *data = dvxResRead(h, name, &size);
dvxResClose(h);
if (!data) {
return 0;
}
FILE *f = fopen(outFile, "wb");
if (!f) {
free(data);
return 0;
}
int32_t result = (fwrite(data, 1, size, f) == size) ? -1 : 0;
fclose(f);
free(data);
return result;
}

View file

@ -43,7 +43,6 @@ typedef struct {
// Control instance (a widget on a form)
// ============================================================
#define BAS_MAX_TEXT_BUF 256
#define BAS_MAX_EVENT_OVERRIDES 16
// Event handler override (SetEvent)
@ -59,7 +58,6 @@ typedef struct BasControlT {
WidgetT *widget; // the DVX widget
BasFormT *form; // owning form
const WgtIfaceT *iface; // interface descriptor (from .wgt)
char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
char dataField[BAS_MAX_CTRL_NAME]; // column name for binding
char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1
@ -171,7 +169,7 @@ void *basFormRtLoadForm(void *ctx, const char *formName);
void basFormRtUnloadForm(void *ctx, void *formRef);
void basFormRtShowForm(void *ctx, void *formRef, bool modal);
void basFormRtHideForm(void *ctx, void *formRef);
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags);
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
// ---- Extern call callbacks (shared by IDE and stub) ----

View file

@ -3095,9 +3095,9 @@ static void recentOpen(int32_t index) {
static void loadFile(void) {
FileFilterT filters[] = {
{ "BASIC Files (*.bas)", "*.bas" },
{ "Form Files (*.frm)", "*.frm" },
{ "All Files (*.*)", "*.*" }
{ "BASIC Files (*.bas)" },
{ "Form Files (*.frm)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
@ -3247,7 +3247,7 @@ static void newProject(void) {
// Ask for directory via save dialog (file = name.dbp)
FileFilterT filters[] = {
{ "Project Files (*.dbp)", "*.dbp" }
{ "Project Files (*.dbp)" }
};
char dbpPath[DVX_MAX_PATH];
@ -3310,8 +3310,8 @@ static void newProject(void) {
static void openProject(void) {
FileFilterT filters[] = {
{ "Project Files (*.dbp)", "*.dbp" },
{ "All Files (*.*)", "*.*" }
{ "Project Files (*.dbp)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
@ -3964,8 +3964,8 @@ static void makeExecutable(void) {
// Ask for output path
FileFilterT filters[] = {
{ "DVX Applications (*.app)", "*.app" },
{ "All Files (*.*)", "*.*" }
{ "DVX Applications (*.app)" },
{ "All Files (*.*)" }
};
char outPath[DVX_MAX_PATH];
outPath[0] = '\0';
@ -3975,7 +3975,7 @@ static void makeExecutable(void) {
}
// Ask debug or release
const char *modeItems[] = { "Debug (include error info)", "Release (stripped)" };
const char *modeItems[] = { "Debug (include error info)" };
int32_t modeChoice = 0;
if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) {
@ -4067,9 +4067,116 @@ static void makeExecutable(void) {
fclose(outFile);
free(stubData);
// Attach app name from project properties
// Attach project property resources
const char *projName = sProject.name[0] ? sProject.name : "BASIC App";
dvxResAppend(outPath, "APPNAME", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1);
dvxResAppend(outPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1);
if (sProject.author[0]) {
dvxResAppend(outPath, "author", DVX_RES_TEXT, sProject.author, (uint32_t)strlen(sProject.author) + 1);
}
if (sProject.company[0]) {
dvxResAppend(outPath, "company", DVX_RES_TEXT, sProject.company, (uint32_t)strlen(sProject.company) + 1);
}
if (sProject.version[0]) {
dvxResAppend(outPath, "version", DVX_RES_TEXT, sProject.version, (uint32_t)strlen(sProject.version) + 1);
}
if (sProject.copyright[0]) {
dvxResAppend(outPath, "copyright", DVX_RES_TEXT, sProject.copyright, (uint32_t)strlen(sProject.copyright) + 1);
}
if (sProject.description[0]) {
dvxResAppend(outPath, "description", DVX_RES_TEXT, sProject.description, (uint32_t)strlen(sProject.description) + 1);
}
// Attach icon: project icon or fallback to IDE's noicon resource
if (sProject.iconPath[0]) {
char iconFullPath[DVX_MAX_PATH];
snprintf(iconFullPath, sizeof(iconFullPath), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.iconPath);
FILE *iconFile = fopen(iconFullPath, "rb");
if (iconFile) {
fseek(iconFile, 0, SEEK_END);
long iconSize = ftell(iconFile);
fseek(iconFile, 0, SEEK_SET);
void *iconData = malloc(iconSize);
if (iconData) {
if (fread(iconData, 1, iconSize, iconFile) == (size_t)iconSize) {
dvxResAppend(outPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconSize);
}
free(iconData);
}
fclose(iconFile);
}
} else {
// Use stock noicon from IDE resources
DvxResHandleT *ideRes = dvxResOpen(sCtx->appPath);
if (ideRes) {
uint32_t noiconSize = 0;
void *noiconData = dvxResRead(ideRes, "noicon", &noiconSize);
if (noiconData) {
dvxResAppend(outPath, "icon32", DVX_RES_ICON, noiconData, noiconSize);
free(noiconData);
}
dvxResClose(ideRes);
}
}
// Copy help file alongside the output app (if specified in project)
if (sProject.helpFile[0]) {
// Store just the filename as a text resource so the stub can find it
const char *helpBase = sProject.helpFile;
const char *sep = strrchr(helpBase, DVX_PATH_SEP);
if (sep) {
helpBase = sep + 1;
}
dvxResAppend(outPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1);
// Copy the .hlp file to sit next to the output .app
char helpSrc[DVX_MAX_PATH];
snprintf(helpSrc, sizeof(helpSrc), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.helpFile);
char outDir[DVX_MAX_PATH];
snprintf(outDir, sizeof(outDir), "%s", outPath);
char *lastSep = strrchr(outDir, DVX_PATH_SEP);
if (lastSep) {
*lastSep = '\0';
}
char helpDst[DVX_MAX_PATH];
snprintf(helpDst, sizeof(helpDst), "%s%c%s", outDir, DVX_PATH_SEP, helpBase);
FILE *hSrc = fopen(helpSrc, "rb");
if (hSrc) {
FILE *hDst = fopen(helpDst, "wb");
if (hDst) {
char cpBuf[4096];
size_t n;
while ((n = fread(cpBuf, 1, sizeof(cpBuf), hSrc)) > 0) {
fwrite(cpBuf, 1, n, hDst);
}
fclose(hDst);
}
fclose(hSrc);
}
}
// Attach MODULE resource
dvxResAppend(outPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen);
@ -8519,6 +8626,7 @@ static void updateProjectMenuState(void) {
}
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyDirty);
wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, hasProject && isIdle);
// Edit menu
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);

View file

@ -441,6 +441,7 @@ void basModuleFree(BasModuleT *mod) {
}
free(mod->procs);
free(mod->formVarInfo);
free(mod->debugVars);
if (mod->debugUdtDefs) {
@ -451,10 +452,6 @@ void basModuleFree(BasModuleT *mod) {
free(mod->debugUdtDefs);
}
if (mod->formVarInfo) {
free(mod->formVarInfo);
}
free(mod);
}

View file

@ -1144,14 +1144,26 @@ BasVmResultE basVmStep(BasVmT *vm) {
varSlot = &vm->globals[varIdx];
}
// Increment: var = var + step
double varVal = basValToNumber(*varSlot);
double stepVal = basValToNumber(fs->step);
double limVal = basValToNumber(fs->limit);
// Increment: var = var + step. Preserve the loop variable's
// original type so extern calls that pass it as an int32_t
// argument don't get an 8-byte double instead.
double varVal = basValToNumber(*varSlot);
double stepVal = basValToNumber(fs->step);
double limVal = basValToNumber(fs->limit);
uint8_t varType = varSlot->type;
varVal += stepVal;
basValRelease(varSlot);
*varSlot = basValDouble(varVal);
if (varType == BAS_TYPE_INTEGER) {
*varSlot = basValInteger((int16_t)varVal);
} else if (varType == BAS_TYPE_LONG) {
*varSlot = basValLong((int32_t)varVal);
} else if (varType == BAS_TYPE_SINGLE) {
*varSlot = basValSingle((float)varVal);
} else {
*varSlot = basValDouble(varVal);
}
// Test: if step > 0 then continue while var <= limit
// if step < 0 then continue while var >= limit
@ -1669,6 +1681,67 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_MATH_RANDOMIZE:
return execMath(vm, op);
case OP_RGB: {
// RGB(r, g, b) -> long color 0x00RRGGBB
BasValueT vb;
BasValueT vg;
BasValueT vr;
if (!pop(vm, &vb) || !pop(vm, &vg) || !pop(vm, &vr)) {
return BAS_VM_STACK_UNDERFLOW;
}
int32_t r = (int32_t)basValToNumber(vr);
int32_t g = (int32_t)basValToNumber(vg);
int32_t b = (int32_t)basValToNumber(vb);
basValRelease(&vr);
basValRelease(&vg);
basValRelease(&vb);
if (r < 0) { r = 0; }
if (r > 255) { r = 255; }
if (g < 0) { g = 0; }
if (g > 255) { g = 255; }
if (b < 0) { b = 0; }
if (b > 255) { b = 255; }
if (!push(vm, basValLong((r << 16) | (g << 8) | b))) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
case OP_GET_RED:
case OP_GET_GREEN:
case OP_GET_BLUE: {
BasValueT vc;
if (!pop(vm, &vc)) {
return BAS_VM_STACK_UNDERFLOW;
}
int32_t color = (int32_t)basValToNumber(vc);
basValRelease(&vc);
int32_t component;
if (op == OP_GET_RED) {
component = (color >> 16) & 0xFF;
} else if (op == OP_GET_GREEN) {
component = (color >> 8) & 0xFF;
} else {
component = color & 0xFF;
}
if (!push(vm, basValInteger((int16_t)component))) {
return BAS_VM_STACK_OVERFLOW;
}
break;
}
// ============================================================
// Conversion built-ins
// ============================================================
@ -2887,11 +2960,12 @@ BasVmResultE basVmStep(BasVmT *vm) {
}
case OP_MSGBOX: {
// Stack: [message, flags] — flags on top
// Stack: [message, flags, title] -- title on top
BasValueT titleVal;
BasValueT flagsVal;
BasValueT msgVal;
if (!pop(vm, &flagsVal) || !pop(vm, &msgVal)) {
if (!pop(vm, &titleVal) || !pop(vm, &flagsVal) || !pop(vm, &msgVal)) {
return BAS_VM_STACK_UNDERFLOW;
}
@ -2900,10 +2974,13 @@ BasVmResultE basVmStep(BasVmT *vm) {
if (vm->ui.msgBox) {
BasValueT sv = basValToString(msgVal);
result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags);
BasValueT tv = basValToString(titleVal);
result = vm->ui.msgBox(vm->ui.ctx, sv.strVal->data, flags, tv.strVal->data);
basValRelease(&sv);
basValRelease(&tv);
}
basValRelease(&titleVal);
basValRelease(&flagsVal);
basValRelease(&msgVal);
push(vm, basValInteger((int16_t)result));
@ -3766,13 +3843,13 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
switch (mode) {
case FILE_MODE_INPUT:
modeStr = "r";
modeStr = "rb";
break;
case FILE_MODE_OUTPUT:
modeStr = "w";
modeStr = "wb";
break;
case FILE_MODE_APPEND:
modeStr = "a";
modeStr = "ab";
break;
case FILE_MODE_RANDOM:
case FILE_MODE_BINARY:
@ -3836,6 +3913,10 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
}
case OP_FILE_PRINT: {
// Writes one value without a trailing newline. The parser emits
// OP_FILE_WRITE_NL separately when the PRINT statement finishes
// without a trailing ; so multi-value PRINT with `;` separator
// works correctly.
BasValueT val;
BasValueT channelVal;
@ -3857,7 +3938,6 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
if (s) {
fputs(s->data, (FILE *)vm->files[channel].handle);
fputc('\n', (FILE *)vm->files[channel].handle);
basStringUnref(s);
}
@ -3913,17 +3993,12 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
return BAS_VM_FILE_ERROR;
}
// Peek ahead to detect EOF before the next read
FILE *fp = (FILE *)vm->files[channel].handle;
int ch = fgetc(fp);
bool isEof;
if (ch == EOF) {
isEof = true;
} else {
ungetc(ch, fp);
isEof = false;
}
FILE *fp = (FILE *)vm->files[channel].handle;
long curPos = ftell(fp);
fseek(fp, 0, SEEK_END);
long endPos = ftell(fp);
fseek(fp, curPos, SEEK_SET);
bool isEof = (curPos >= endPos);
if (!push(vm, basValBool(isEof))) {
return BAS_VM_STACK_OVERFLOW;

View file

@ -124,7 +124,7 @@ typedef void (*BasUiShowFormFnT)(void *ctx, void *formRef, bool modal);
typedef void (*BasUiHideFormFnT)(void *ctx, void *formRef);
// Display a message box. Returns the button clicked (1=OK, 6=Yes, 7=No, 2=Cancel).
typedef int32_t (*BasUiMsgBoxFnT)(void *ctx, const char *message, int32_t flags);
typedef int32_t (*BasUiMsgBoxFnT)(void *ctx, const char *message, int32_t flags, const char *title);
// Display an input box. Returns the entered string (empty on cancel).
typedef BasStringT *(*BasUiInputBoxFnT)(void *ctx, const char *prompt, const char *title, const char *defaultText);

View file

@ -0,0 +1,667 @@
// bascomp.c -- DVX BASIC command-line compiler
//
// Compiles a .dbp project into a standalone .app file.
//
// Usage: BASCOMP project.dbp [-o output.app] [-release]
//
// The project file and all referenced source files (.bas, .frm)
// are loaded relative to the directory containing the .dbp file.
// The stub (basstub.app) is read from the same directory as the
// compiler executable.
#include "../compiler/compact.h"
#include "../compiler/lexer.h"
#include "../compiler/obfuscate.h"
#include "../compiler/parser.h"
#include "../compiler/strip.h"
#include "../compiler/opcodes.h"
#include "../runtime/vm.h"
#include "../runtime/values.h"
#include "../runtime/serialize.h"
#include "../../core/dvxRes.h"
#include "../../core/dvxPrefs.h"
#include "../../core/dvxTypes.h"
#include "../../tools/dvxResWrite.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Limits
// ============================================================
#define MAX_FILES 64
#define MAX_PATH_LEN 260
#define MAX_NAME 64
// ============================================================
// Prototypes
// ============================================================
static void concatGrow(char **buf, int32_t *cap, int32_t need);
static char *readFile(const char *path, int32_t *outLen);
static const char *extractFormCode(const char *frmText);
static void usage(void);
// ============================================================
// extractFormCode -- skip past the form layout to the code section
// ============================================================
static void concatGrow(char **buf, int32_t *cap, int32_t need) {
while (*cap < need) {
*cap *= 2;
}
*buf = (char *)realloc(*buf, *cap);
}
static const char *extractFormCode(const char *frmText) {
if (!frmText) {
return NULL;
}
const char *p = frmText;
int32_t depth = 0;
bool inForm = false;
while (*p) {
// Skip leading whitespace
while (*p == ' ' || *p == '\t') { p++; }
if (strncasecmp(p, "Begin ", 6) == 0) {
if (!inForm && strncasecmp(p + 6, "Form ", 5) == 0) {
inForm = true;
}
depth++;
} else if (strncasecmp(p, "End", 3) == 0 && (p[3] == '\0' || p[3] == '\r' || p[3] == '\n' || p[3] == ' ')) {
depth--;
if (depth <= 0 && inForm) {
// Skip past this line
while (*p && *p != '\n') { p++; }
if (*p == '\n') { p++; }
return p;
}
}
// Skip to next line
while (*p && *p != '\n') { p++; }
if (*p == '\n') { p++; }
}
return NULL;
}
// ============================================================
// readFile
// ============================================================
static char *readFile(const char *path, int32_t *outLen) {
FILE *f = fopen(path, "rb");
if (!f) {
return NULL;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *buf = (char *)malloc(size + 1);
if (!buf) {
fclose(f);
return NULL;
}
int32_t n = (int32_t)fread(buf, 1, size, f);
fclose(f);
buf[n] = '\0';
if (outLen) {
*outLen = n;
}
return buf;
}
// ============================================================
// usage
// ============================================================
static void usage(void) {
fprintf(stderr, "DVX BASIC Compiler\n\n");
fprintf(stderr, "Usage: BASCOMP project.dbp [-o output.app] [-release]\n\n");
fprintf(stderr, " project.dbp DVX BASIC project file\n");
fprintf(stderr, " -o output.app Output file (default: project name + .app)\n");
fprintf(stderr, " -release Strip debug information\n");
}
// ============================================================
// main
// ============================================================
int main(int argc, char **argv) {
if (argc < 2) {
usage();
return 1;
}
const char *dbpPath = NULL;
const char *outputPath = NULL;
bool release = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
outputPath = argv[++i];
} else if (strcmp(argv[i], "-release") == 0) {
release = true;
} else if (argv[i][0] != '-') {
dbpPath = argv[i];
} else {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
usage();
return 1;
}
}
if (!dbpPath) {
fprintf(stderr, "Error: no project file specified.\n");
usage();
return 1;
}
// Load the project file
PrefsHandleT *prefs = prefsLoad(dbpPath);
if (!prefs) {
fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
return 1;
}
// Derive project directory
char projectDir[MAX_PATH_LEN];
snprintf(projectDir, sizeof(projectDir), "%s", dbpPath);
char *sep = strrchr(projectDir, '/');
char *sep2 = strrchr(projectDir, '\\');
if (sep2 > sep) {
sep = sep2;
}
if (sep) {
*sep = '\0';
} else {
projectDir[0] = '.';
projectDir[1] = '\0';
}
// Read project metadata
const char *projName = prefsGetString(prefs, "Project", "Name", "App");
const char *author = prefsGetString(prefs, "Project", "Author", "");
const char *company = prefsGetString(prefs, "Project", "Company", "");
const char *version = prefsGetString(prefs, "Project", "Version", "");
const char *copyright = prefsGetString(prefs, "Project", "Copyright", "");
const char *description = prefsGetString(prefs, "Project", "Description", "");
const char *iconPath = prefsGetString(prefs, "Project", "Icon", "");
const char *helpFile = prefsGetString(prefs, "Project", "HelpFile", "");
const char *startupForm = prefsGetString(prefs, "Settings", "StartupForm", "");
(void)startupForm; // used implicitly by stub's basFormRtLoadAllForms
// Derive output path
char outBuf[MAX_PATH_LEN];
if (!outputPath) {
snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName);
outputPath = outBuf;
}
printf("Project: %s\n", projName);
printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug");
// Collect source files
typedef struct {
char path[MAX_PATH_LEN];
bool isForm;
} SrcFileT;
SrcFileT files[MAX_FILES];
int32_t fileCount = 0;
// Modules
for (int32_t i = 0; i < MAX_FILES; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
const char *val = prefsGetString(prefs, "Modules", key, NULL);
if (!val) {
break;
}
snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val);
files[fileCount].isForm = false;
fileCount++;
}
// Forms
for (int32_t i = 0; i < MAX_FILES; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
const char *val = prefsGetString(prefs, "Forms", key, NULL);
if (!val) {
break;
}
snprintf(files[fileCount].path, MAX_PATH_LEN, "%s/%s", projectDir, val);
files[fileCount].isForm = true;
fileCount++;
}
if (fileCount == 0) {
fprintf(stderr, "Error: project has no source files.\n");
prefsClose(prefs);
return 1;
}
// Concatenate sources (modules first, then form code)
int32_t concatCap = 8192;
char *concatBuf = (char *)malloc(concatCap);
if (!concatBuf) {
fprintf(stderr, "Error: out of memory.\n");
prefsClose(prefs);
return 1;
}
int32_t pos = 0;
// Pass 0: .bas modules, Pass 1: .frm code sections
for (int32_t pass = 0; pass < 2; pass++) {
for (int32_t i = 0; i < fileCount; i++) {
if (pass == 0 && files[i].isForm) { continue; }
if (pass == 1 && !files[i].isForm) { continue; }
int32_t srcLen = 0;
char *srcBuf = readFile(files[i].path, &srcLen);
if (!srcBuf) {
fprintf(stderr, "Error: cannot read %s\n", files[i].path);
free(concatBuf);
prefsClose(prefs);
return 1;
}
const char *code = srcBuf;
if (files[i].isForm) {
// Extract form name from "Begin Form <name>"
char formName[MAX_NAME] = "";
const char *bp = srcBuf;
while (*bp) {
while (*bp == ' ' || *bp == '\t') { bp++; }
if (strncasecmp(bp, "Begin Form ", 11) == 0) {
bp += 11;
while (*bp == ' ' || *bp == '\t') { bp++; }
int32_t ni = 0;
while (*bp && *bp != ' ' && *bp != '\t' && *bp != '\r' && *bp != '\n' && ni < MAX_NAME - 1) {
formName[ni++] = *bp++;
}
formName[ni] = '\0';
break;
}
while (*bp && *bp != '\n') { bp++; }
if (*bp == '\n') { bp++; }
}
code = extractFormCode(srcBuf);
if (!code) {
code = "";
}
int32_t codeLen = (int32_t)strlen(code);
concatGrow(&concatBuf, &concatCap, pos + codeLen + 128);
// Inject BEGINFORM directive before form code
if (formName[0]) {
pos += snprintf(concatBuf + pos, concatCap - pos, "BEGINFORM \"%s\"\n", formName);
}
memcpy(concatBuf + pos, code, codeLen);
pos += codeLen;
if (pos > 0 && concatBuf[pos - 1] != '\n') {
concatBuf[pos++] = '\n';
}
// Inject ENDFORM directive after form code
if (formName[0]) {
pos += snprintf(concatBuf + pos, concatCap - pos, "ENDFORM\n");
}
} else {
int32_t codeLen = (int32_t)strlen(code);
concatGrow(&concatBuf, &concatCap, pos + codeLen + 2);
memcpy(concatBuf + pos, code, codeLen);
pos += codeLen;
if (pos > 0 && concatBuf[pos - 1] != '\n') {
concatBuf[pos++] = '\n';
}
}
free(srcBuf);
}
}
concatBuf[pos] = '\0';
// Compile
printf("Compiling %d file(s)...\n", (int)fileCount);
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
if (!parser) {
fprintf(stderr, "Error: out of memory.\n");
free(concatBuf);
prefsClose(prefs);
return 1;
}
basParserInit(parser, concatBuf, pos);
if (!basParse(parser)) {
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
basParserFree(parser);
free(parser);
free(concatBuf);
prefsClose(prefs);
return 1;
}
free(concatBuf);
BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser);
free(parser);
if (!mod) {
fprintf(stderr, "Error: failed to build module.\n");
prefsClose(prefs);
return 1;
}
printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);
// Strip for release
if (release) {
basStripModule(mod);
printf(" stripped debug info\n");
}
// Read all .frm texts up front; they're used for obfuscation and
// then embedded as FORM0, FORM1, ... resources below.
int32_t frmCount = 0;
char *frmData[MAX_FILES];
int32_t frmLens[MAX_FILES];
int32_t frmFileIdx[MAX_FILES]; // index into files[] for this frm
for (int32_t i = 0; i < fileCount; i++) {
if (!files[i].isForm) {
continue;
}
int32_t flen = 0;
char *fdata = readFile(files[i].path, &flen);
if (!fdata) {
continue;
}
// Strip comments from the .frm text unconditionally. Comments
// are source-only; they shouldn't ship in the embedded resource
// for either debug or release builds.
int32_t stripCap = flen + 16;
uint8_t *stripped = (uint8_t *)malloc(stripCap);
if (!stripped) {
free(fdata);
continue;
}
int32_t strippedLen = basStripFrmComments(fdata, flen, stripped, stripCap);
free(fdata);
frmData[frmCount] = (char *)stripped;
frmLens[frmCount] = strippedLen;
frmFileIdx[frmCount] = i;
frmCount++;
}
// Obfuscate form/control names in release mode
BasObfFrmT obfFrms[MAX_FILES];
for (int32_t i = 0; i < MAX_FILES; i++) {
obfFrms[i].data = NULL;
obfFrms[i].len = 0;
}
if (release && frmCount > 0) {
const char *frmTexts[MAX_FILES];
for (int32_t i = 0; i < frmCount; i++) {
frmTexts[i] = frmData[i];
}
basObfuscateNames(mod, frmTexts, frmLens, frmCount, obfFrms);
printf(" obfuscated %d form(s)\n", (int)frmCount);
}
// Remove OP_LINE instructions and compact the bytecode.
if (release) {
int32_t removed = basCompactBytecode(mod);
if (removed > 0) {
printf(" compacted bytecode (-%d bytes)\n", (int)removed);
}
}
// Serialize module
int32_t modLen = 0;
uint8_t *modData = basModuleSerialize(mod, &modLen);
if (!modData) {
fprintf(stderr, "Error: failed to serialize module.\n");
basModuleFree(mod);
prefsClose(prefs);
return 1;
}
// Serialize debug info
int32_t dbgLen = 0;
uint8_t *dbgData = NULL;
if (!release) {
dbgData = basDebugSerialize(mod, &dbgLen);
}
basModuleFree(mod);
// Find the stub -- look in the compiler's own directory
char compilerDir[MAX_PATH_LEN];
snprintf(compilerDir, sizeof(compilerDir), "%s", argv[0]);
sep = strrchr(compilerDir, '/');
sep2 = strrchr(compilerDir, '\\');
if (sep2 > sep) {
sep = sep2;
}
if (sep) {
*sep = '\0';
} else {
compilerDir[0] = '.';
compilerDir[1] = '\0';
}
char stubPath[MAX_PATH_LEN];
snprintf(stubPath, sizeof(stubPath), "%s/BASSTUB.APP", compilerDir);
int32_t stubLen = 0;
char *stubData = readFile(stubPath, &stubLen);
if (!stubData) {
fprintf(stderr, "Error: cannot find stub at %s\n", stubPath);
free(modData);
free(dbgData);
prefsClose(prefs);
return 1;
}
// Write stub to output file
FILE *out = fopen(outputPath, "wb");
if (!out) {
fprintf(stderr, "Error: cannot create %s\n", outputPath);
free(stubData);
free(modData);
free(dbgData);
prefsClose(prefs);
return 1;
}
fwrite(stubData, 1, stubLen, out);
fclose(out);
free(stubData);
// Attach resources
dvxResAppendEntry(outputPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1);
if (author[0]) { dvxResAppendEntry(outputPath, "author", DVX_RES_TEXT, author, (uint32_t)strlen(author) + 1); }
if (company[0]) { dvxResAppendEntry(outputPath, "company", DVX_RES_TEXT, company, (uint32_t)strlen(company) + 1); }
if (version[0]) { dvxResAppendEntry(outputPath, "version", DVX_RES_TEXT, version, (uint32_t)strlen(version) + 1); }
if (copyright[0]) { dvxResAppendEntry(outputPath, "copyright", DVX_RES_TEXT, copyright, (uint32_t)strlen(copyright) + 1); }
if (description[0]) { dvxResAppendEntry(outputPath, "description", DVX_RES_TEXT, description, (uint32_t)strlen(description) + 1); }
// Icon
if (iconPath[0]) {
char iconFullPath[MAX_PATH_LEN];
snprintf(iconFullPath, sizeof(iconFullPath), "%s/%s", projectDir, iconPath);
int32_t iconLen = 0;
char *iconData = readFile(iconFullPath, &iconLen);
if (iconData) {
dvxResAppendEntry(outputPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconLen);
free(iconData);
}
}
// Help file name (file is expected alongside the .app)
if (helpFile[0]) {
const char *helpBase = helpFile;
const char *hs = strrchr(helpBase, '/');
const char *hs2 = strrchr(helpBase, '\\');
if (hs2 > hs) {
hs = hs2;
}
if (hs) {
helpBase = hs + 1;
}
dvxResAppendEntry(outputPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1);
// Copy help file to output directory
char helpSrc[MAX_PATH_LEN];
snprintf(helpSrc, sizeof(helpSrc), "%s/%s", projectDir, helpFile);
char outDir[MAX_PATH_LEN];
snprintf(outDir, sizeof(outDir), "%s", outputPath);
char *outSep = strrchr(outDir, '/');
char *outSep2 = strrchr(outDir, '\\');
if (outSep2 > outSep) {
outSep = outSep2;
}
if (outSep) {
*outSep = '\0';
} else {
outDir[0] = '.';
outDir[1] = '\0';
}
char helpDst[MAX_PATH_LEN];
snprintf(helpDst, sizeof(helpDst), "%s/%s", outDir, helpBase);
int32_t hLen = 0;
char *hData = readFile(helpSrc, &hLen);
if (hData) {
FILE *hf = fopen(helpDst, "wb");
if (hf) {
fwrite(hData, 1, hLen, hf);
fclose(hf);
}
free(hData);
}
}
// MODULE resource
dvxResAppendEntry(outputPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen);
free(modData);
// DEBUG resource
if (dbgData) {
dvxResAppendEntry(outputPath, "DEBUG", DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
free(dbgData);
}
// Form resources -- use obfuscated bytes in release mode, raw otherwise
for (int32_t fi = 0; fi < frmCount; fi++) {
char resName[16];
snprintf(resName, sizeof(resName), "FORM%d", (int)fi);
if (release && obfFrms[fi].data) {
dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, obfFrms[fi].data, (uint32_t)obfFrms[fi].len);
} else {
dvxResAppendEntry(outputPath, resName, DVX_RES_BINARY, frmData[fi], (uint32_t)frmLens[fi]);
}
}
// Free .frm buffers
for (int32_t i = 0; i < frmCount; i++) {
free(frmData[i]);
free(obfFrms[i].data);
}
(void)frmFileIdx; // unused (kept in case of future per-file metadata)
prefsClose(prefs);
printf("Created %s (%d bytes)\n", outputPath, (int)stubLen + (int)modLen);
return 0;
}

View file

@ -92,7 +92,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Read app name and update the shell's app record
uint32_t nameSize = 0;
char *appName = (char *)dvxResRead(res, "APPNAME", &nameSize);
char *appName = (char *)dvxResRead(res, "name", &nameSize);
if (appName) {
ShellAppT *app = shellGetApp(ctx->appId);
@ -104,6 +104,15 @@ int32_t appMain(DxeAppContextT *ctx) {
free(appName);
}
// Set help file path if present
uint32_t helpNameSize = 0;
char *helpName = (char *)dvxResRead(res, "helpfile", &helpNameSize);
if (helpName) {
snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s%c%s", ctx->appDir, DVX_PATH_SEP, helpName);
free(helpName);
}
// Load MODULE resource
uint32_t modSize = 0;
uint8_t *modData = (uint8_t *)dvxResRead(res, "MODULE", &modSize);

View file

@ -0,0 +1,326 @@
// test_compact.c -- verify that strip + compact produces identical output
//
// Compiles each test program, runs it once without compaction and once
// with strip + compact applied, and compares captured PRINT output.
// Exits nonzero if any mismatch is found.
#include "compiler/parser.h"
#include "compiler/strip.h"
#include "compiler/compact.h"
#include "runtime/vm.h"
#include "runtime/values.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_OUT 65536
typedef struct {
char *buf;
int32_t len;
int32_t cap;
} OutBufT;
static void captureCallback(void *ctx, const char *text, bool newline) {
OutBufT *ob = (OutBufT *)ctx;
int32_t tlen = (int32_t)strlen(text);
if (ob->len + tlen + 2 >= ob->cap) {
return;
}
memcpy(ob->buf + ob->len, text, tlen);
ob->len += tlen;
if (newline) {
ob->buf[ob->len++] = '\n';
}
}
static int32_t runAndCapture(const char *source, bool compact, char *outBuf, int32_t outCap) {
BasParserT parser;
basParserInit(&parser, source, (int32_t)strlen(source));
if (!basParse(&parser)) {
fprintf(stderr, "compile error: %s\n", parser.error);
basParserFree(&parser);
return -1;
}
BasModuleT *mod = basParserBuildModule(&parser);
basParserFree(&parser);
if (!mod) {
return -1;
}
if (compact) {
basStripModule(mod);
basCompactBytecode(mod);
}
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
OutBufT ob;
ob.buf = outBuf;
ob.len = 0;
ob.cap = outCap;
basVmSetPrintCallback(vm, captureCallback, &ob);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
BasVmResultE result = basVmRun(vm);
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
fprintf(stderr, "vm error %d: %s\n", result, basVmGetError(vm));
}
basVmDestroy(vm);
basModuleFree(mod);
outBuf[ob.len] = '\0';
return ob.len;
}
static int32_t sTotal = 0;
static int32_t sFailed = 0;
static void testCompact(const char *name, const char *source) {
sTotal++;
static char orig[MAX_OUT];
static char comp[MAX_OUT];
int32_t origLen = runAndCapture(source, false, orig, MAX_OUT);
int32_t compLen = runAndCapture(source, true, comp, MAX_OUT);
if (origLen < 0 || compLen < 0) {
printf("FAIL: %s (runtime error)\n", name);
sFailed++;
return;
}
if (origLen != compLen || memcmp(orig, comp, origLen) != 0) {
printf("FAIL: %s\n", name);
printf(" uncompacted (%d bytes):\n%s\n", (int)origLen, orig);
printf(" compacted (%d bytes):\n%s\n", (int)compLen, comp);
sFailed++;
return;
}
printf("PASS: %s\n", name);
}
int main(void) {
printf("DVX BASIC Bytecode Compaction Tests\n");
printf("====================================\n\n");
basStringSystemInit();
// ---- Basic control flow ----
testCompact("FOR loop",
"DIM i AS INTEGER\n"
"FOR i = 1 TO 5\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("Nested FOR",
"DIM i AS INTEGER\n"
"DIM j AS INTEGER\n"
"FOR i = 1 TO 3\n"
" FOR j = 1 TO 2\n"
" PRINT i * 10 + j;\n"
" NEXT j\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("FOR with negative STEP",
"DIM i AS INTEGER\n"
"FOR i = 5 TO 1 STEP -1\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("EXIT FOR",
"DIM i AS INTEGER\n"
"FOR i = 1 TO 100\n"
" IF i = 4 THEN EXIT FOR\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
testCompact("DO WHILE",
"DIM n AS INTEGER\n"
"n = 0\n"
"DO WHILE n < 3\n"
" PRINT n;\n"
" n = n + 1\n"
"LOOP\n"
"PRINT\n"
);
testCompact("IF THEN ELSE",
"DIM x AS INTEGER\n"
"x = 5\n"
"IF x > 3 THEN\n"
" PRINT \"big\"\n"
"ELSE\n"
" PRINT \"small\"\n"
"END IF\n"
);
testCompact("SELECT CASE",
"DIM g AS STRING\n"
"g = \"B\"\n"
"SELECT CASE g\n"
" CASE \"A\"\n"
" PRINT \"A\"\n"
" CASE \"B\", \"C\"\n"
" PRINT \"BC\"\n"
" CASE ELSE\n"
" PRINT \"?\"\n"
"END SELECT\n"
);
// ---- GOSUB: exercises the absolute address in OP_PUSH_INT32 pattern ----
testCompact("GOSUB",
"DIM n AS INTEGER\n"
"n = 10\n"
"GOSUB doubler\n"
"PRINT n\n"
"GOSUB doubler\n"
"PRINT n\n"
"END\n"
"doubler:\n"
"n = n * 2\n"
"RETURN\n"
);
testCompact("Multiple GOSUBs",
"DIM x AS INTEGER\n"
"x = 1\n"
"GOSUB a\n"
"GOSUB b\n"
"GOSUB c\n"
"PRINT x\n"
"END\n"
"a:\n"
"x = x + 10\n"
"RETURN\n"
"b:\n"
"x = x + 100\n"
"RETURN\n"
"c:\n"
"x = x + 1000\n"
"RETURN\n"
);
// ---- SUB / FUNCTION: exercises absolute CALL addresses + proc table ----
testCompact("SUB with CALL",
"CALL Greet\n"
"CALL Greet\n"
"CALL Greet\n"
"SUB Greet\n"
" PRINT \"Hello\"\n"
"END SUB\n"
);
testCompact("FUNCTION return value",
"DECLARE FUNCTION Square(x AS INTEGER) AS INTEGER\n"
"DIM r AS INTEGER\n"
"r = Square(5)\n"
"PRINT r\n"
"r = Square(7)\n"
"PRINT r\n"
"FUNCTION Square(x AS INTEGER) AS INTEGER\n"
" Square = x * x\n"
"END FUNCTION\n"
);
testCompact("Recursive FUNCTION",
"DECLARE FUNCTION Fact(n AS INTEGER) AS INTEGER\n"
"PRINT Fact(5)\n"
"FUNCTION Fact(n AS INTEGER) AS INTEGER\n"
" IF n <= 1 THEN\n"
" Fact = 1\n"
" ELSE\n"
" Fact = n * Fact(n - 1)\n"
" END IF\n"
"END FUNCTION\n"
);
// ---- ON ERROR: exercises the relative int16 offset path with non-zero ----
testCompact("ON ERROR GOTO",
"ON ERROR GOTO handler\n"
"PRINT 10 / 0\n"
"PRINT \"unreached\"\n"
"END\n"
"handler:\n"
"PRINT \"caught\"\n"
);
// ---- Mixed: many OP_LINE between jumps ----
testCompact("Long function with many lines",
"DIM total AS INTEGER\n"
"total = 0\n"
"DIM i AS INTEGER\n"
"FOR i = 1 TO 10\n"
" IF i MOD 2 = 0 THEN\n"
" total = total + i\n"
" ELSE\n"
" total = total + i * 2\n"
" END IF\n"
"NEXT i\n"
"PRINT total\n"
);
testCompact("GOSUB inside FOR",
"DIM i AS INTEGER\n"
"DIM sum AS INTEGER\n"
"sum = 0\n"
"FOR i = 1 TO 5\n"
" GOSUB addit\n"
"NEXT i\n"
"PRINT sum\n"
"END\n"
"addit:\n"
"sum = sum + i\n"
"RETURN\n"
);
testCompact("FOR inside SUB",
"CALL Loop5\n"
"SUB Loop5\n"
" DIM k AS INTEGER\n"
" FOR k = 0 TO 4\n"
" PRINT k;\n"
" NEXT k\n"
" PRINT\n"
"END SUB\n"
);
printf("\n%d/%d tests passed\n", (int)(sTotal - sFailed), (int)sTotal);
return sFailed > 0 ? 1 : 0;
}

View file

@ -900,6 +900,107 @@ int main(void) {
printf("\n");
}
// Regression test: App.Data must parse even though Data is a BASIC keyword
runProgram("App.Data parses",
"DIM p AS STRING\n"
"p = App.Data\n"
"PRINT p\n"
);
// Regression test: RGB(r,g,b) packs into 0x00RRGGBB
runProgram("RGB packing",
"PRINT RGB(255, 128, 0)\n"
"PRINT RGB(0, 255, 0)\n"
"PRINT RGB(0, 0, 255)\n"
);
// Expected: 16744448 (0xFF8000), 65280 (0x00FF00), 255 (0x0000FF)
// Regression test: GetRed/Green/Blue round-trip
runProgram("RGB round-trip",
"DIM c AS LONG\n"
"c = RGB(17, 99, 200)\n"
"PRINT GetRed(c); GetGreen(c); GetBlue(c)\n"
);
// Expected: 17 99 200
// Regression test: MsgBox function accepts optional title (3rd arg)
// The test harness's default msgBox callback returns 1; we just verify
// the program compiles and runs.
runProgram("MsgBox 3-arg compile",
"DIM r AS INTEGER\n"
"r = MsgBox(\"hi\", 0, \"My Title\")\n"
"PRINT \"ok\"\n"
);
// Regression test: CONST accepts AS type
runProgram("CONST AS type",
"CONST PI AS DOUBLE = 3.14159\n"
"CONST N AS INTEGER = 42\n"
"PRINT PI\n"
"PRINT N\n"
);
// Regression test: PRINT #ch with ; separator
runProgram("PRINT # with semicolon",
"OPEN \"/tmp/dvxbasic_psemi.txt\" FOR OUTPUT AS #1\n"
"PRINT #1, \"Line one\"\n"
"PRINT #1, \"ans = \"; 42\n"
"PRINT #1, \"x\"; \"y\"; \"z\"\n"
"CLOSE #1\n"
"DIM s AS STRING\n"
"OPEN \"/tmp/dvxbasic_psemi.txt\" FOR INPUT AS #1\n"
"DO WHILE NOT EOF(1)\n"
" LINE INPUT #1, s\n"
" PRINT s\n"
"LOOP\n"
"CLOSE #1\n"
);
// Expected: Line one / ans = 42 / xyz
// Regression test: IniRead$ with $ suffix must tokenize as keyword
runProgram("IniRead$ tokenizes",
"DIM v AS STRING\n"
"v = IniRead$(\"/tmp/nofile.ini\", \"S\", \"K\", \"default\")\n"
"PRINT v\n"
);
// Expected: default (file doesn't exist)
// Regression test: SUB extern call without parens must emit OP_CALL_EXTERN
// (previously emitted OP_CALL which called nothing, because no internal
// proc at address 0 existed for the extern.)
{
printf("=== DECLARE LIBRARY SUB no-parens ===\n");
const char *src =
"DECLARE LIBRARY \"basrt\"\n"
" DECLARE SUB DoIt(BYVAL s AS STRING)\n"
"END DECLARE\n"
"DoIt \"hello\"\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
bool ok = basParse(&parser);
if (!ok) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
BasModuleT *mod = basParserBuildModule(&parser);
bool found = false;
for (int32_t i = 0; i < mod->codeLen; i++) {
if (mod->code[i] == OP_CALL_EXTERN) {
found = true;
break;
}
}
printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n");
basModuleFree(mod);
}
basParserFree(&parser);
printf("\n");
}
// Test: Procedure table populated for SUBs and FUNCTIONs
{
printf("=== Procedure table ===\n");
@ -1213,6 +1314,56 @@ int main(void) {
);
// Expected: 1 2 / after
// Regression test: FOR loop inside a SUB with local loop variable
runProgram("FOR inside SUB with local",
"CALL MySub\n"
"SUB MySub\n"
" DIM i AS LONG\n"
" FOR i = 0 TO 3\n"
" PRINT i;\n"
" NEXT i\n"
" PRINT\n"
"END SUB\n"
);
// Expected: 0 1 2 3
// Regression test: resedit pattern -- FOR in SUB with n = FUNC(...)
runProgram("FOR with func-returned limit",
"CALL DoIt\n"
"SUB DoIt\n"
" DIM n AS LONG\n"
" n = GetCount()\n"
" DIM i AS LONG\n"
" FOR i = 0 TO n - 1\n"
" PRINT i;\n"
" NEXT i\n"
" PRINT\n"
"END SUB\n"
"FUNCTION GetCount() AS LONG\n"
" GetCount = 4\n"
"END FUNCTION\n"
);
// Expected: 0 1 2 3
// Regression test: resedit pattern -- FOR in SUB inside a FORM
runProgram("FOR in SUB inside FORM",
"BEGINFORM \"Form1\"\n"
"DIM formVar AS LONG\n"
"formVar = 42\n"
"CALL DoIt\n"
"SUB DoIt\n"
" DIM n AS LONG\n"
" n = 4\n"
" DIM ix AS LONG\n"
" FOR ix = 0 TO n - 1\n"
" PRINT ix;\n"
" NEXT ix\n"
" PRINT\n"
"END SUB\n"
"ENDFORM\n"
);
// Expected: 0 1 2 3
runProgram("EXIT DO",
"DIM n AS INTEGER\n"
"n = 0\n"

View file

@ -153,11 +153,11 @@ static void onCloseMainCb(WindowT *win) {
static const FileFilterT sFileFilters[] = {
{"All Files (*.*)", "*.*"},
{"Text Files (*.txt)", "*.txt"},
{"Batch Files (*.bat)", "*.bat"},
{"Executables (*.exe)", "*.exe"},
{"Bitmap Files (*.bmp)", "*.bmp"}
{"All Files (*.*)"},
{"Text Files (*.txt)"},
{"Batch Files (*.bat)"},
{"Executables (*.exe)"},
{"Bitmap Files (*.bmp)"}
};
static void onMenuCb(WindowT *win, int32_t menuId) {

View file

@ -1,5 +1,5 @@
# dvxdemo.res -- Resource manifest for DVX Demo
icon32 icon dvxdemo/icon32.bmp
icon32 icon icon32.bmp
name text "DVX Demo"
author text "DVX Project"
description text "Widget toolkit demonstration"

View file

@ -1,5 +1,5 @@
# dvxhelp.res -- Resource manifest for DVX Help Viewer
icon32 icon dvxhelp/icon32.bmp
icon32 icon icon32.bmp
name text "DVX Help"
author text "DVX Project"
description text "Help file viewer"

View file

@ -328,8 +328,8 @@ static void onResize(WindowT *win, int32_t contentW, int32_t contentH) {
static void openFile(void) {
FileFilterT filters[] = {
{ "Images (*.bmp;*.jpg;*.png;*.gif)", "*.bmp;*.jpg;*.png;*.gif" },
{ "All Files (*.*)", "*.*" }
{ "Images (*.bmp;*.jpg;*.png;*.gif)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];

View file

@ -1,5 +1,5 @@
# imgview.res -- Resource manifest for Image Viewer
icon32 icon imgview/icon32.bmp
icon32 icon icon32.bmp
name text "Image Viewer"
author text "DVX Project"
description text "BMP, PNG, JPEG, and GIF viewer"

View file

@ -150,8 +150,8 @@ static void doOpen(void) {
}
FileFilterT filters[] = {
{ "Text Files (*.txt)", "*.txt" },
{ "All Files (*.*)", "*.*" }
{ "Text Files (*.txt)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
@ -236,8 +236,8 @@ static void doSave(void) {
static void doSaveAs(void) {
FileFilterT filters[] = {
{ "Text Files (*.txt)", "*.txt" },
{ "All Files (*.*)", "*.*" }
{ "Text Files (*.txt)" },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];

View file

@ -1,5 +1,5 @@
# notepad.res -- Resource manifest for Notepad
icon32 icon notepad/icon32.bmp
icon32 icon icon32.bmp
name text "Notepad"
author text "DVX Project"
description text "Simple text editor"

View file

@ -73,6 +73,7 @@
#define CMD_TILE_V 203
#define CMD_MIN_ON_RUN 104
#define CMD_RESTORE_ALONE 105
#define CMD_RELOAD 106
#define CMD_ABOUT 300
#define CMD_TASK_MGR 301
#define CMD_SYSINFO 302
@ -166,6 +167,7 @@ static void buildPmWindow(void) {
MenuBarT *menuBar = wmAddMenuBar(sPmWindow);
MenuT *fileMenu = wmAddMenu(menuBar, "&File");
wmAddMenuItem(fileMenu, "&Run...", CMD_RUN);
wmAddMenuItem(fileMenu, "Re&load", CMD_RELOAD);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "E&xit DVX", CMD_EXIT);
@ -313,8 +315,8 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
case CMD_RUN:
{
FileFilterT filters[] = {
{ "Applications (*.app)", "*.app" },
{ "All Files (*.*)", "*.*" }
{ "Applications (*.app)" },
{ "All Files (*.*)" }
};
char path[MAX_PATH_LEN];
@ -329,6 +331,14 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
}
break;
case CMD_RELOAD:
dvxDestroyWindow(sAc, sPmWindow);
sPmWindow = NULL;
sStatusLabel = NULL;
scanAppsDir();
buildPmWindow();
break;
case CMD_EXIT:
onPmClose(sPmWindow);
break;

View file

@ -11,12 +11,14 @@ bpp = 16
; Mouse settings.
; wheel: normal or reversed
; wheelspeed: lines per wheel notch (1-10, default 3)
; doubleclick: double-click speed in milliseconds (200-900, default 500)
; acceleration: off, low, medium, high (default medium)
; speed: cursor speed (2-32, default 8; higher = faster)
[mouse]
wheel = normal
wheelspeed = 3
doubleclick = 500
acceleration = medium
speed = 8

View file

@ -1277,8 +1277,12 @@ static void dispatchEvents(AppContextT *ctx) {
return;
}
// Handle left button press
// Handle left button press. Consume the edge immediately so that if
// the click handler calls back into the event loop (e.g., BASIC's
// DoEvents yielding while a QueryUnload handler runs), the same
// physical click isn't re-seen as a fresh press.
if ((buttons & MOUSE_LEFT) && !(prevBtn & MOUSE_LEFT)) {
ctx->prevMouseButtons |= MOUSE_LEFT;
handleMouseButton(ctx, mx, my, buttons);
}
@ -1289,6 +1293,7 @@ static void dispatchEvents(AppContextT *ctx) {
// that apply to all their children without requiring each child to have
// its own menu, while still allowing per-widget overrides.
if ((buttons & MOUSE_RIGHT) && !(prevBtn & MOUSE_RIGHT)) {
ctx->prevMouseButtons |= MOUSE_RIGHT;
int32_t hitPart;
int32_t hitIdx = wmHitTest(&ctx->stack, mx, my, &hitPart);
@ -1396,7 +1401,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (target && wclsHas(target, WGT_METHOD_ON_KEY)) {
int32_t delta = ctx->mouseWheel * ctx->wheelDirection;
int32_t arrowKey = (delta > 0) ? (0x50 | 0x100) : (0x48 | 0x100);
int32_t steps = abs(delta) * MOUSE_WHEEL_STEP;
int32_t steps = abs(delta) * ctx->wheelStep;
for (int32_t s = 0; s < steps; s++) {
wclsOnKey(target, arrowKey, 0);
@ -1423,7 +1428,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (sb) {
int32_t oldValue = sb->value;
sb->value += ctx->mouseWheel * ctx->wheelDirection * MOUSE_WHEEL_STEP;
sb->value += ctx->mouseWheel * ctx->wheelDirection * ctx->wheelStep;
if (sb->value < sb->min) {
sb->value = sb->min;
@ -1721,11 +1726,18 @@ static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd) {
break;
case SysMenuCloseE:
// Sys menu is already closed. Composite a clean frame first
// so the menu's pixels are erased before onClose runs -- the
// close handler may open a modal dialog and we don't want the
// sys menu popup peeking out from under it.
compositeAndFlush(ctx);
if (win->onClose) {
WIN_CALLBACK(ctx, win, win->onClose(win));
} else {
dvxDestroyWindow(ctx, win);
}
break;
}
}
@ -1872,11 +1884,20 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
ctx->lastCloseClickId = -1;
closeSysMenu(ctx);
// Composite a clean frame first so the sys menu is
// fully gone before onClose runs -- the close handler
// may open a modal dialog and we don't want the menu
// peeking out from under it.
compositeAndFlush(ctx);
if (win->onClose) {
WIN_CALLBACK(ctx, win, win->onClose(win));
} else {
dvxDestroyWindow(ctx, win);
}
// Ensure sys menu is closed even if onClose re-opened it
closeSysMenu(ctx);
} else {
ctx->lastCloseClickTime = now;
ctx->lastCloseClickId = win->id;
@ -1965,8 +1986,8 @@ static void initColorScheme(AppContextT *ctx) {
static void interactiveScreenshot(AppContextT *ctx) {
FileFilterT filters[] = {
{ "PNG Images (*.png)", "*.png" },
{ "BMP Images (*.bmp)", "*.bmp" }
{ "PNG Images (*.png)" },
{ "BMP Images (*.bmp)" }
};
char path[DVX_MAX_PATH];
@ -2000,8 +2021,8 @@ static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) {
}
FileFilterT filters[] = {
{ "PNG Images (*.png)", "*.png" },
{ "BMP Images (*.bmp)", "*.bmp" }
{ "PNG Images (*.png)" },
{ "BMP Images (*.bmp)" }
};
char path[DVX_MAX_PATH];
@ -4309,6 +4330,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
ctx->lastTitleClickId = -1;
ctx->lastTitleClickTime = 0;
ctx->wheelDirection = 1;
ctx->wheelStep = MOUSE_WHEEL_STEP_DEFAULT;
ctx->dblClickTicks = DBLCLICK_THRESHOLD;
sDblClickTicks = DBLCLICK_THRESHOLD;
@ -4833,11 +4855,21 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b)
// dvxSetMouseConfig
// ============================================================
void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio) {
void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep) {
ctx->wheelDirection = (wheelDir < 0) ? -1 : 1;
ctx->dblClickTicks = (clock_t)dblClickMs * CLOCKS_PER_SEC / 1000;
sDblClickTicks = ctx->dblClickTicks;
if (wheelStep < MOUSE_WHEEL_STEP_MIN) {
wheelStep = MOUSE_WHEEL_STEP_MIN;
}
if (wheelStep > MOUSE_WHEEL_STEP_MAX) {
wheelStep = MOUSE_WHEEL_STEP_MAX;
}
ctx->wheelStep = wheelStep;
if (accelThreshold > 0) {
platformMouseSetAccel(accelThreshold);
}

View file

@ -104,6 +104,7 @@ typedef struct AppContextT {
uint32_t charHeightRecip; // fixed-point 16.16 reciprocal of font.charHeight
// Mouse configuration (loaded from preferences)
int32_t wheelDirection; // 1 = normal, -1 = reversed
int32_t wheelStep; // lines per wheel notch (1-10, default 3)
clock_t dblClickTicks; // double-click speed in clock() ticks
// Color scheme source RGB values (unpacked, for theme save/get)
uint8_t colorRgb[ColorCountE][3];
@ -134,7 +135,8 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
// dblClickMs: double-click speed in milliseconds (e.g. 500).
// accelThreshold: double-speed threshold in mickeys/sec (0 = don't change).
// mickeyRatio: mickeys per 8 pixels (0 = don't change, 8 = default speed).
void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio);
// wheelStep: lines per wheel notch (1-10, default 3).
void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, int32_t accelThreshold, int32_t mickeyRatio, int32_t wheelStep);
// ============================================================
// Color scheme

View file

@ -1071,6 +1071,32 @@ typedef struct {
static FileDialogStateT sFd;
// ============================================================
// fdExtractPattern -- extract glob pattern from inside parentheses in label
// ============================================================
static const char *fdExtractPattern(const FileFilterT *f, char *buf, int32_t bufSize) {
const char *open = strchr(f->label, '(');
const char *close = open ? strchr(open, ')') : NULL;
if (open && close && close > open + 1) {
int32_t len = (int32_t)(close - open - 1);
if (len >= bufSize) {
len = bufSize - 1;
}
memcpy(buf, open + 1, len);
buf[len] = '\0';
return buf;
}
// No parens -- use the whole label as the pattern
snprintf(buf, bufSize, "%s", f->label);
return buf;
}
// ============================================================
// fdFilterMatch -- check if filename matches a glob pattern
// ============================================================
@ -1210,9 +1236,10 @@ static void fdLoadDir(void) {
fdFreeEntries();
const char *pattern = NULL;
char patBuf[128];
if (sFd.filters && sFd.activeFilter >= 0 && sFd.activeFilter < sFd.filterCount) {
pattern = sFd.filters[sFd.activeFilter].pattern;
pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], patBuf, sizeof(patBuf));
}
DIR *dir = opendir(sFd.curDir);
@ -1561,6 +1588,59 @@ static void fdOnOk(WidgetT *w) {
return;
}
// Save dialog: if the filename has no extension, append the first
// extension from the active filter pattern. Wildcards like "*.*" or
// "*" don't add an extension.
char nameWithExt[FD_MAX_PATH];
if ((sFd.flags & FD_SAVE) && !strchr(name, '.')) {
char extPatBuf[128];
const char *pattern = fdExtractPattern(&sFd.filters[sFd.activeFilter], extPatBuf, sizeof(extPatBuf));
const char *ext = NULL;
// Find first non-wildcard extension in pattern (may be "*.txt;*.doc")
if (pattern) {
const char *p = pattern;
while (*p) {
if (p[0] == '*' && p[1] == '.') {
const char *e = p + 2;
// Skip wildcards like "*.*"
if (*e != '*' && *e != '\0') {
ext = p + 1; // points to ".ext"
break;
}
}
// Skip to next semicolon-delimited pattern
const char *semi = strchr(p, ';');
if (semi) {
p = semi + 1;
} else {
break;
}
}
}
if (ext) {
// Extract just the extension (up to ; or end)
char extBuf[16];
int32_t ei = 0;
while (ext[ei] && ext[ei] != ';' && ei < 15) {
extBuf[ei] = ext[ei];
ei++;
}
extBuf[ei] = '\0';
snprintf(nameWithExt, sizeof(nameWithExt), "%s%s", name, extBuf);
name = nameWithExt;
wgtSetText(sFd.nameInput, name);
}
}
// Accept the file (with confirmation if needed)
fdAcceptFile(name);
}

View file

@ -64,9 +64,11 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
// patterns (no semicolon-separated lists). This keeps the matching code
// trivial for a DOS filesystem where filenames are short and simple.
// File filter for the file dialog. Pattern is extracted from inside
// parentheses in the label. Example: "Text Files (*.txt)"
// Multiple extensions: "Images (*.bmp;*.png;*.jpg;*.gif)"
typedef struct {
const char *label; // e.g. "Text Files (*.txt)"
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
const char *label; // e.g. "Text Files (*.txt)" -- pattern extracted from parens
} FileFilterT;
// Display a modal file open/save dialog. The dialog shows a directory

View file

@ -77,4 +77,8 @@ void dvxResClose(DvxResHandleT *h);
// Returns 0 on success, -1 on error.
int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize);
// Remove a resource by name from a DXE file.
// Returns 0 on success, -1 on error (not found or I/O failure).
int32_t dvxResRemove(const char *path, const char *name);
#endif // DVX_RES_H

View file

@ -144,3 +144,105 @@ void dvxResClose(DvxResHandleT *h) {
int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize) {
return dvxResAppendEntry(path, name, type, data, dataSize);
}
int32_t dvxResRemove(const char *path, const char *name) {
if (!path || !name) {
return -1;
}
long dxeSize = dvxResDxeContentSize(path);
if (dxeSize < 0) {
return -1;
}
DvxResDirEntryT *entries = NULL;
uint32_t count = 0;
uint8_t **data = NULL;
dvxResReadExisting(path, dxeSize, &entries, &count, &data);
if (!entries || count == 0) {
return -1;
}
bool found = false;
for (uint32_t i = 0; i < count; i++) {
if (strcmp(entries[i].name, name) == 0) {
free(data[i]);
for (uint32_t j = i; j < count - 1; j++) {
entries[j] = entries[j + 1];
data[j] = data[j + 1];
}
count--;
found = true;
break;
}
}
if (!found) {
for (uint32_t i = 0; i < count; i++) {
free(data[i]);
}
free(data);
free(entries);
return -1;
}
int32_t result;
if (count == 0) {
// No resources left -- truncate to DXE content only
FILE *f = fopen(path, "rb");
if (!f) {
free(data);
free(entries);
return -1;
}
uint8_t *dxeBuf = (uint8_t *)malloc((size_t)dxeSize);
if (!dxeBuf) {
fclose(f);
free(data);
free(entries);
return -1;
}
if (fread(dxeBuf, 1, (size_t)dxeSize, f) != (size_t)dxeSize) {
free(dxeBuf);
fclose(f);
free(data);
free(entries);
return -1;
}
fclose(f);
f = fopen(path, "wb");
if (f) {
fwrite(dxeBuf, 1, (size_t)dxeSize, f);
fclose(f);
result = 0;
} else {
result = -1;
}
free(dxeBuf);
} else {
result = dvxResWriteBlock(path, dxeSize, entries, count, data);
}
for (uint32_t i = 0; i < count; i++) {
free(data[i]);
}
free(data);
free(entries);
return result;
}

View file

@ -611,8 +611,10 @@ typedef struct {
#define MOUSE_RIGHT 2
#define MOUSE_MIDDLE 4
// Scrollbar lines to scroll per mouse wheel notch
#define MOUSE_WHEEL_STEP 3
// Default scrollbar lines to scroll per mouse wheel notch
#define MOUSE_WHEEL_STEP_DEFAULT 3
#define MOUSE_WHEEL_STEP_MIN 1
#define MOUSE_WHEEL_STEP_MAX 10
// ============================================================
// Mouse cursor

1
run.sh
View file

@ -1,5 +1,4 @@
#!/bin/bash
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf
#SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf

View file

@ -8,10 +8,17 @@
DECLARE LIBRARY "basrt"
' Show a file Open dialog. Returns selected path, or "" if cancelled.
' filter$ is a DOS wildcard (e.g. "*.bmp", "*.txt").
' filter$ specifies file type filters, pipe-delimited:
' "Label (pattern)|Label (pattern)|..."
' The pattern is extracted from inside the parentheses.
' Examples:
' "*.txt" (single pattern, legacy)
' "Text Files (*.txt)|All Files (*.*)" (two filters)
' "Images (*.bmp;*.png;*.jpg)|All Files (*.*)" (multi-extension)
DECLARE FUNCTION basFileOpen(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING
' Show a file Save dialog. Returns selected path, or "" if cancelled.
' filter$ uses the same format as basFileOpen.
DECLARE FUNCTION basFileSave(BYVAL title AS STRING, BYVAL filter AS STRING) AS STRING
' Show a modal text input box. Returns entered text, or "" if cancelled.
@ -29,6 +36,6 @@ DECLARE LIBRARY "basrt"
END DECLARE
' Return value constants for basPromptSave
CONST DVX_SAVE_YES = 0
CONST DVX_SAVE_NO = 1
CONST DVX_SAVE_CANCEL = 2
CONST DVX_SAVE_YES = 1
CONST DVX_SAVE_NO = 2
CONST DVX_SAVE_CANCEL = 3

View file

@ -0,0 +1,50 @@
' resource.bas -- DVX Resource File Library for DVX BASIC
'
' Provides access to DVX resource blocks appended to DXE3
' files (.app, .wgt, .lib). Resources are named data entries
' of type icon, text, or binary.
'
' Usage: Add this file to your project.
CONST RES_TYPE_ICON = 1
CONST RES_TYPE_TEXT = 2
CONST RES_TYPE_BINARY = 3
DECLARE LIBRARY "basrt"
' Open a resource file for reading. Returns a handle (> 0)
' or 0 on failure. Close with ResClose when done.
DECLARE FUNCTION ResOpen(BYVAL path AS STRING) AS LONG
' Close a resource handle.
DECLARE SUB ResClose(BYVAL handle AS LONG)
' Return the number of resources in an open handle.
DECLARE FUNCTION ResCount(BYVAL handle AS LONG) AS LONG
' Return the name of a resource by zero-based index.
DECLARE FUNCTION ResName$(BYVAL handle AS LONG, BYVAL idx AS LONG)
' Return the type of a resource by zero-based index.
' Returns RES_TYPE_ICON (1), RES_TYPE_TEXT (2), or RES_TYPE_BINARY (3).
DECLARE FUNCTION ResType(BYVAL handle AS LONG, BYVAL idx AS LONG) AS LONG
' Return the size in bytes of a resource by zero-based index.
DECLARE FUNCTION ResSize(BYVAL handle AS LONG, BYVAL idx AS LONG) AS LONG
' Read a text resource by name. Opens and closes the file
' internally. Returns "" if not found.
DECLARE FUNCTION ResGetText$(BYVAL path AS STRING, BYVAL resName AS STRING)
' Add or replace a text resource. Returns True on success.
DECLARE FUNCTION ResAddText(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL text AS STRING) AS LONG
' Add or replace a resource from a file. Type should be
' RES_TYPE_ICON or RES_TYPE_BINARY. Returns True on success.
DECLARE FUNCTION ResAddFile(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL resType AS LONG, BYVAL srcFile AS STRING) AS LONG
' Remove a resource by name. Returns True on success.
DECLARE FUNCTION ResRemove(BYVAL path AS STRING, BYVAL resName AS STRING) AS LONG
' Extract a resource to a file. Returns True on success.
DECLARE FUNCTION ResExtract(BYVAL path AS STRING, BYVAL resName AS STRING, BYVAL outFile AS STRING) AS LONG
END DECLARE

BIN
sdk/samples/basic/basicdemo/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,16 @@
[Project]
Name = BASIC Demo
Author = DVX Project
Description = Comprehensive tour of DVX BASIC features
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = basicdemo.frm
Count = 1
[Settings]
StartupForm = BasicDemo

View file

@ -0,0 +1,937 @@
VERSION DVX 1.00
Begin Form BasicDemo
Caption = "DVX BASIC Feature Tour"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 600
Height = 440
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuClear
Caption = "&Clear OutArea"
End
Begin Menu mnuSaveOut
Caption = "&Save OutArea..."
End
Begin Menu mnuSepF1
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuRun
Caption = "&Demos"
Begin Menu mnuRunAll
Caption = "Run &All Text Demos"
End
Begin Menu mnuSepR1
Caption = "-"
End
Begin Menu mnuGraphics
Caption = "&Graphics..."
End
Begin Menu mnuDynamic
Caption = "&Dynamic Form..."
End
Begin Menu mnuTimer
Caption = "&Timer..."
End
End
Begin Menu mnuHelp
Caption = "&Help"
Begin Menu mnuAbout
Caption = "&About..."
End
End
Begin Frame fraButtons
Caption = "Language Demonstrations"
Layout = VBox
Weight = 0
Begin HBox rowA
Weight = 0
Begin CommandButton btnTypes
Caption = "&Types"
Weight = 1
End
Begin CommandButton btnMath
Caption = "&Math"
Weight = 1
End
Begin CommandButton btnStrings
Caption = "&Strings"
Weight = 1
End
Begin CommandButton btnArrays
Caption = "&Arrays"
Weight = 1
End
Begin CommandButton btnData
Caption = "&DATA/READ"
Weight = 1
End
End
Begin HBox rowB
Weight = 0
Begin CommandButton btnFlow
Caption = "Control &Flow"
Weight = 1
End
Begin CommandButton btnUdt
Caption = "&UDT"
Weight = 1
End
Begin CommandButton btnOpt
Caption = "&Optional"
Weight = 1
End
Begin CommandButton btnError
Caption = "&Errors"
Weight = 1
End
Begin CommandButton btnFormat
Caption = "F&ormat"
Weight = 1
End
End
Begin HBox rowC
Weight = 0
Begin CommandButton btnFileIO
Caption = "File &I/O"
Weight = 1
End
Begin CommandButton btnSystem
Caption = "S&ystem"
Weight = 1
End
Begin CommandButton btnIni
Caption = "I&NI"
Weight = 1
End
Begin CommandButton btnDialogs
Caption = "Di&alogs"
Weight = 1
End
Begin CommandButton btnClear
Caption = "Clea&r"
Weight = 1
End
End
End
Begin TextArea OutArea
Weight = 1
End
Begin Label LblStatus
Caption = "Ready. Click any button to run a demo."
Weight = 0
End
End
OPTION EXPLICIT
TYPE PointT
x AS INTEGER
y AS INTEGER
END TYPE
' ============================================================
' OutArea helpers
' ============================================================
SUB Say(s AS STRING)
OutArea.AppendText s + CHR$(10)
END SUB
SUB Header(title AS STRING)
Say ""
Say "--- " + title + " ---"
END SUB
Load BasicDemo
BasicDemo.Show
OutArea.SetShowLineNumbers False
OutArea.SetReadOnly True
Say "Welcome to the DVX BASIC Feature Tour!"
Say "Each button below runs a self-contained example."
Say "Check the Demos menu for graphics, dynamic UI, and timer demos."
Say ""
' ============================================================
' Menu handlers
' ============================================================
SUB mnuClear_Click
OutArea.Text = ""
LblStatus.Caption = "OutArea cleared."
END SUB
SUB mnuSaveOut_Click
DIM path AS STRING
path = basFileSave("Save OutArea", "Text Files (*.txt)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
OPEN path FOR OUTPUT AS #1
PRINT #1, OutArea.Text
CLOSE #1
LblStatus.Caption = "Saved: " + path
END SUB
SUB mnuExit_Click
Unload BasicDemo
END SUB
SUB mnuRunAll_Click
btnTypes_Click
btnMath_Click
btnStrings_Click
btnArrays_Click
btnData_Click
btnFlow_Click
btnUdt_Click
btnOpt_Click
btnError_Click
btnFormat_Click
btnFileIO_Click
btnSystem_Click
LblStatus.Caption = "All text demos complete."
END SUB
SUB mnuAbout_Click
DIM msg AS STRING
msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10)
msg = msg + "A visual catalog of DVX BASIC language"
msg = msg + " and runtime features." + CHR$(10) + CHR$(10)
msg = msg + "(c) 2026 DVX Project"
MsgBox msg, vbOKOnly, "About"
END SUB
' ============================================================
' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
' ============================================================
SUB btnTypes_Click
Header "Types"
DIM i AS INTEGER
DIM l AS LONG
DIM s AS SINGLE
DIM d AS DOUBLE
DIM t AS STRING
DIM b AS BOOLEAN
i = 32767
l = 2147483647
s = 3.14159
d = 2.718281828459045
t = "Hello, DVX!"
b = True
Say "INTEGER (16-bit): " + STR$(i)
Say "LONG (32-bit): " + STR$(l)
Say "SINGLE (float): " + STR$(s)
Say "DOUBLE (double): " + STR$(d)
Say "STRING: " + CHR$(34) + t + CHR$(34)
Say "BOOLEAN True: " + STR$(b)
' CONST with AS type annotation
CONST PI AS DOUBLE = 3.1415926535
Say "CONST PI = " + STR$(PI)
LblStatus.Caption = "Types demo complete."
END SUB
' ============================================================
' Math: integer + float operators, built-in functions
' ============================================================
SUB btnMath_Click
Header "Math"
DIM a AS INTEGER
DIM b AS INTEGER
a = 17
b = 5
Say "a = 17, b = 5"
Say "a + b = " + STR$(a + b)
Say "a - b = " + STR$(a - b)
Say "a * b = " + STR$(a * b)
Say "a \ b = " + STR$(a \ b) + " (integer divide)"
Say "a MOD b = " + STR$(a MOD b)
Say "a / b = " + STR$(a / b) + " (float divide)"
Say ""
Say "SQR(144) = " + STR$(SQR(144))
Say "ABS(-42) = " + STR$(ABS(-42))
Say "INT(3.7) = " + STR$(INT(3.7))
Say "FIX(-3.7) = " + STR$(FIX(-3.7))
Say "SGN(-9) = " + STR$(SGN(-9))
Say "SIN(0) = " + STR$(SIN(0))
Say "COS(0) = " + STR$(COS(0))
Say "2 ^ 10 = " + STR$(2 ^ 10)
RANDOMIZE TIMER
DIM r AS INTEGER
r = INT(RND * 100)
Say "RND (0-99) = " + STR$(r)
Say "TIMER = " + STR$(TIMER) + " (seconds since midnight)"
LblStatus.Caption = "Math demo complete."
END SUB
' ============================================================
' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
' ============================================================
SUB btnStrings_Click
Header "Strings"
DIM s AS STRING
s = "The quick brown fox"
Say "Source: " + CHR$(34) + s + CHR$(34)
Say "LEN = " + STR$(LEN(s))
Say "LEFT$(s, 3) = " + LEFT$(s, 3)
Say "RIGHT$(s, 3) = " + RIGHT$(s, 3)
Say "MID$(s, 5, 5) = " + MID$(s, 5, 5)
Say "UCASE$ = " + UCASE$(s)
Say "LCASE$('HELLO') = " + LCASE$("HELLO")
Say "INSTR(s, 'brown')= " + STR$(INSTR(s, "brown"))
Say "TRIM$(' hi ') = " + CHR$(34) + TRIM$(" hi ") + CHR$(34)
Say "STRING$(5, 42) = " + STRING$(5, 42)
Say "CHR$(65) = " + CHR$(65)
Say "ASC('A') = " + STR$(ASC("A"))
Say "HEX$(255) = " + HEX$(255)
Say "VAL('42.5xyz') = " + STR$(VAL("42.5xyz"))
LblStatus.Caption = "Strings demo complete."
END SUB
' ============================================================
' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
' ============================================================
SUB btnArrays_Click
Header "Arrays"
' 1D array
DIM squares(9) AS INTEGER
DIM i AS INTEGER
FOR i = 0 TO 9
squares(i) = i * i
NEXT i
DIM lineS AS STRING
lineS = "squares(0..9) = "
FOR i = 0 TO 9
lineS = lineS + STR$(squares(i)) + " "
NEXT i
Say lineS
' Bounds: DIM a(lo TO hi)
DIM prices(1 TO 3) AS SINGLE
prices(1) = 9.99
prices(2) = 14.99
prices(3) = 29.99
Say "LBOUND(prices) = " + STR$(LBOUND(prices)) + ", UBOUND = " + STR$(UBOUND(prices))
Say "prices(2) = " + STR$(prices(2))
' 2D array
DIM matrix(2, 2) AS INTEGER
matrix(0, 0) = 1 : matrix(0, 1) = 2 : matrix(0, 2) = 3
matrix(1, 0) = 4 : matrix(1, 1) = 5 : matrix(1, 2) = 6
matrix(2, 0) = 7 : matrix(2, 1) = 8 : matrix(2, 2) = 9
Say "3x3 matrix:"
DIM r AS INTEGER
DIM c AS INTEGER
FOR r = 0 TO 2
lineS = " "
FOR c = 0 TO 2
lineS = lineS + STR$(matrix(r, c))
NEXT c
Say lineS
NEXT r
' REDIM PRESERVE
DIM nums(2) AS INTEGER
nums(0) = 10
nums(1) = 20
nums(2) = 30
REDIM PRESERVE nums(4) AS INTEGER
nums(3) = 40
nums(4) = 50
Say "After REDIM PRESERVE: " + STR$(nums(0)) + " " + STR$(nums(1)) + " " + STR$(nums(2)) + " " + STR$(nums(3)) + " " + STR$(nums(4))
LblStatus.Caption = "Arrays demo complete."
END SUB
' ============================================================
' DATA / READ / RESTORE
' ============================================================
SUB btnData_Click
Header "DATA / READ / RESTORE"
DATA "Red", 255, 0, 0
DATA "Green", 0, 255, 0
DATA "Blue", 0, 0, 255
DIM colorName AS STRING
DIM r AS INTEGER
DIM g AS INTEGER
DIM b AS INTEGER
DIM i AS INTEGER
FOR i = 1 TO 3
READ colorName
READ r
READ g
READ b
Say colorName + ": (" + STR$(r) + "," + STR$(g) + "," + STR$(b) + ")"
NEXT i
Say ""
Say "RESTORE resets pointer. Reading first entry again:"
RESTORE
READ colorName
Say " first = " + colorName
LblStatus.Caption = "DATA/READ demo complete."
END SUB
' ============================================================
' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
' ============================================================
SUB btnFlow_Click
Header "Control Flow"
' FOR with STEP
Say "FOR i = 10 TO 0 STEP -2:"
DIM i AS INTEGER
DIM lineS AS STRING
lineS = " "
FOR i = 10 TO 0 STEP -2
lineS = lineS + STR$(i)
NEXT i
Say lineS
' DO WHILE
Say ""
Say "DO WHILE n < 32 (doubling):"
DIM n AS LONG
n = 1
lineS = " "
DO WHILE n < 32
lineS = lineS + STR$(n)
n = n * 2
LOOP
Say lineS
' IF / ELSEIF / ELSE
Say ""
DIM score AS INTEGER
score = 78
IF score >= 90 THEN
Say "score " + STR$(score) + " -> A"
ELSEIF score >= 80 THEN
Say "score " + STR$(score) + " -> B"
ELSEIF score >= 70 THEN
Say "score " + STR$(score) + " -> C"
ELSE
Say "score " + STR$(score) + " -> F"
END IF
' SELECT CASE
Say ""
DIM day AS INTEGER
day = 3
SELECT CASE day
CASE 1
Say "day 1 = Monday"
CASE 2, 3
Say "day " + STR$(day) + " = midweek"
CASE 4 TO 5
Say "day " + STR$(day) + " = late week"
CASE ELSE
Say "day " + STR$(day) + " = weekend"
END SELECT
' GOSUB / RETURN
Say ""
Say "GOSUB to a local label:"
GOSUB labelHello
Say "back from subroutine"
LblStatus.Caption = "Control flow demo complete."
EXIT SUB
labelHello:
Say " inside GOSUB"
RETURN
END SUB
' ============================================================
' User-Defined Type
' ============================================================
SUB btnUdt_Click
Header "User-Defined Type"
DIM p AS PointT
p.x = 10
p.y = 20
Say "PointT p = (" + STR$(p.x) + "," + STR$(p.y) + ")"
' Array of UDT
DIM corners(3) AS PointT
corners(0).x = 0 : corners(0).y = 0
corners(1).x = 10 : corners(1).y = 0
corners(2).x = 10 : corners(2).y = 10
corners(3).x = 0 : corners(3).y = 10
Say "Rectangle corners:"
DIM i AS INTEGER
FOR i = 0 TO 3
Say " (" + STR$(corners(i).x) + "," + STR$(corners(i).y) + ")"
NEXT i
LblStatus.Caption = "UDT demo complete."
END SUB
' ============================================================
' Optional parameters (DVX extension)
' ============================================================
FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING
IF greeting = "" THEN
greeting = "Hello"
END IF
Greet = greeting + ", " + who + "!"
END FUNCTION
SUB btnOpt_Click
Header "Optional Parameters"
Say Greet("World")
Say Greet("Scott", "Howdy")
Say Greet("DVX", "Greetings from")
LblStatus.Caption = "Optional params demo complete."
END SUB
' ============================================================
' ON ERROR GOTO
' ============================================================
SUB btnError_Click
Header "ON ERROR GOTO"
ON ERROR GOTO handler
DIM a AS INTEGER
DIM b AS INTEGER
a = 10
b = 0
Say "Attempting 10/0 ..."
Say " 10 / 0 = " + STR$(a / b)
Say "(should not reach here)"
EXIT SUB
handler:
Say " caught! ERR = " + STR$(ERR)
LblStatus.Caption = "Error handler ran successfully."
END SUB
' ============================================================
' PRINT USING / FORMAT$
' ============================================================
SUB btnFormat_Click
Header "Formatting"
Say "FORMAT$(1234.5, '#,##0.00') = " + FORMAT$(1234.5, "#,##0.00")
Say "FORMAT$(0.075, 'percent') = " + FORMAT$(0.075, "percent")
Say "FORMAT$(-42, '+#0') = " + FORMAT$(-42, "+#0")
Say "FORMAT$(3.14159, '0.00') = " + FORMAT$(3.14159, "0.00")
LblStatus.Caption = "Format demo complete."
END SUB
' ============================================================
' File I/O
' ============================================================
SUB btnFileIO_Click
Header "File I/O"
DIM path AS STRING
path = App.Data + "/demo.txt"
Say "Writing: " + path
OPEN path FOR OUTPUT AS #1
PRINT #1, "Line one"
PRINT #1, "Line two"
PRINT #1, "The answer is "; 42
CLOSE #1
Say "LOF = " + STR$(FILELEN(path))
Say "Reading back:"
OPEN path FOR INPUT AS #1
DIM ln AS STRING
DO WHILE NOT EOF(1)
LINE INPUT #1, ln
Say " " + ln
LOOP
CLOSE #1
KILL path
Say "Deleted."
LblStatus.Caption = "File I/O demo complete."
END SUB
' ============================================================
' System: App object, environment, current directory
' ============================================================
SUB btnSystem_Click
Header "System / App"
Say "App.Path = " + App.Path
Say "App.Config = " + App.Config
Say "App.Data = " + App.Data
Say "CurDir = " + CurDir()
Say "Date = " + Date$
Say "Time = " + Time$
Say "PATH env = " + LEFT$(Environ$("PATH"), 40) + "..."
LblStatus.Caption = "System demo complete."
END SUB
' ============================================================
' INI read/write
' ============================================================
SUB btnIni_Click
Header "INI Read/Write"
DIM path AS STRING
path = App.Data + "/demo.ini"
Say "Writing: " + path
IniWrite path, "General", "UserName", "Scott"
IniWrite path, "General", "Version", "1.00"
IniWrite path, "Options", "AutoSave", "True"
Say "Reading back:"
Say " UserName = " + IniRead$(path, "General", "UserName", "(missing)")
Say " Version = " + IniRead$(path, "General", "Version", "(missing)")
Say " AutoSave = " + IniRead$(path, "Options", "AutoSave", "(missing)")
Say " Missing = " + IniRead$(path, "General", "NotThere", "(default)")
KILL path
LblStatus.Caption = "INI demo complete."
END SUB
' ============================================================
' Dialog demos (spawns the real dialogs)
' ============================================================
SUB btnDialogs_Click
Header "Dialogs"
DIM response AS INTEGER
response = MsgBox("MessageBox demo." + CHR$(10) + "Are you enjoying the demo?", vbYesNo + vbQuestion, "Feedback")
IF response = vbYes THEN
Say "MsgBox: user said yes"
ELSE
Say "MsgBox: user said no"
END IF
DIM text AS STRING
text = basInputBox2("Input", "What is your name?", "Anonymous")
Say "InputBox returned: " + text
DIM choice AS INTEGER
choice = basChoiceDialog("Favorite", "Pick a color:", "Red|Green|Blue|Yellow", 1)
IF choice >= 0 THEN
Say "Choice index: " + STR$(choice)
ELSE
Say "Choice cancelled"
END IF
DIM n AS INTEGER
n = basIntInput("Number", "Pick a number (1-100):", 42, 1, 100)
Say "IntInput: " + STR$(n)
LblStatus.Caption = "Dialog demo complete."
END SUB
SUB btnClear_Click
mnuClear_Click
END SUB
' ============================================================
' Graphics demo (opens a second form with Canvas)
' ============================================================
DIM gfxWin AS LONG
gfxWin = 0
SUB mnuGraphics_Click
IF gfxWin <> 0 THEN
EXIT SUB
END IF
DIM frm AS LONG
SET frm = CreateForm("GraphicsForm", 360, 320)
GraphicsForm.Caption = "Graphics Demo"
gfxWin = frm
DIM cv AS LONG
SET cv = CreateControl(frm, "Canvas", "GfxCanvas")
GfxCanvas.Width = 340
GfxCanvas.Height = 260
GfxCanvas.Weight = 1
DIM btnRow AS LONG
SET btnRow = CreateControl(frm, "HBox", "GfxRow")
DIM bDraw AS LONG
SET bDraw = CreateControl(frm, "CommandButton", "GfxDraw", btnRow)
GfxDraw.Caption = "Draw"
SetEvent bDraw, "Click", "GfxDrawAll"
DIM bClear AS LONG
SET bClear = CreateControl(frm, "CommandButton", "GfxClear", btnRow)
GfxClear.Caption = "Clear"
SetEvent bClear, "Click", "GfxClearCanvas"
frm.Show
GfxDrawAll
END SUB
SUB GfxDrawAll
DIM w AS LONG
DIM h AS LONG
w = 340
h = 260
' Gradient background bars
DIM y AS INTEGER
DIM shade AS LONG
FOR y = 0 TO h - 1 STEP 4
shade = (y * 255) \ h
GfxCanvas.FillRect 0, y, w, 4, RGB(shade, shade \ 2, 128)
NEXT y
' Star field
RANDOMIZE TIMER
DIM s AS INTEGER
DIM sx AS INTEGER
DIM sy AS INTEGER
FOR s = 1 TO 40
sx = INT(RND * w)
sy = INT(RND * h)
GfxCanvas.SetPixel sx, sy, RGB(255, 255, 255)
NEXT s
' Rectangle + outline
GfxCanvas.FillRect 20, 20, 80, 50, RGB(240, 240, 0)
GfxCanvas.DrawRect 20, 20, 80, 50, RGB(0, 0, 0)
' Circle approximated by line segments
DIM cx AS INTEGER
DIM cy AS INTEGER
DIM r AS INTEGER
cx = 250
cy = 80
r = 40
DIM a AS DOUBLE
DIM px AS INTEGER
DIM py AS INTEGER
DIM qx AS INTEGER
DIM qy AS INTEGER
px = cx + r
py = cy
FOR a = 0 TO 6.3 STEP 0.2
qx = cx + INT(r * COS(a))
qy = cy + INT(r * SIN(a))
GfxCanvas.DrawLine px, py, qx, qy, RGB(255, 128, 0)
px = qx
py = qy
NEXT a
' Text
GfxCanvas.DrawText 60, 200, "Canvas + math + colors", RGB(255, 255, 255)
GfxCanvas.DrawText 60, 220, "DVX BASIC graphics", RGB(255, 255, 0)
GfxCanvas.Refresh
END SUB
SUB GfxClearCanvas
GfxCanvas.Clear RGB(0, 0, 0)
GfxCanvas.Refresh
END SUB
SUB GraphicsForm_Unload
gfxWin = 0
END SUB
' ============================================================
' Dynamic form demo
' ============================================================
DIM dynForm AS LONG
dynForm = 0
SUB mnuDynamic_Click
IF dynForm <> 0 THEN
EXIT SUB
END IF
DIM frm AS LONG
SET frm = CreateForm("DynForm", 320, 200)
DynForm.Caption = "Dynamic Form (built in code)"
dynForm = frm
DIM lbl AS LONG
SET lbl = CreateControl(frm, "Label", "DynLabel")
DynLabel.Caption = "This form was created 100% in code."
DIM lbl2 AS LONG
SET lbl2 = CreateControl(frm, "Label", "CountLabel")
CountLabel.Caption = "Counter: 0"
DIM btns AS LONG
SET btns = CreateControl(frm, "HBox", "DynBtns")
DIM bInc AS LONG
SET bInc = CreateControl(frm, "CommandButton", "BInc", btns)
BInc.Caption = "Count Up"
SetEvent bInc, "Click", "DynInc"
DIM bBye AS LONG
SET bBye = CreateControl(frm, "CommandButton", "BBye", btns)
BBye.Caption = "Close"
SetEvent bBye, "Click", "DynBye"
frm.Show
END SUB
DIM dynCount AS INTEGER
dynCount = 0
SUB DynInc
dynCount = dynCount + 1
CountLabel.Caption = "Counter: " + STR$(dynCount)
END SUB
SUB DynBye
Unload DynForm
END SUB
SUB DynForm_Unload
dynForm = 0
dynCount = 0
END SUB
' ============================================================
' Timer demo
' ============================================================
DIM timerWin AS LONG
timerWin = 0
SUB mnuTimer_Click
IF timerWin <> 0 THEN
EXIT SUB
END IF
DIM frm AS LONG
SET frm = CreateForm("TimerForm", 260, 140)
TimerForm.Caption = "Timer Demo"
timerWin = frm
DIM lbl AS LONG
SET lbl = CreateControl(frm, "Label", "TickLabel")
TickLabel.Caption = "Ticks: 0"
DIM t AS LONG
SET t = CreateControl(frm, "Timer", "Ticker")
Ticker.Interval = 500
SetEvent t, "Timer", "TickHandler"
frm.Show
END SUB
DIM tickCount AS LONG
tickCount = 0
SUB TickHandler
tickCount = tickCount + 1
TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$
END SUB
SUB TimerForm_Unload
timerWin = 0
tickCount = 0
END SUB

View file

@ -62,14 +62,10 @@ CommDlgDemo.Show
PRINT "Common Dialog Demo started."
DO
DoEvents
LOOP
SUB BtnFileOpen_Click
DIM path AS STRING
path = basFileOpen("Open a File", "*.bas")
path = basFileOpen("Open a File", "BASIC Files (*.bas;*.frm)|All Files (*.*)")
IF path <> "" THEN
LblResult.Caption = "Opened: " + path
PRINT "File Open: " + path
@ -82,7 +78,7 @@ END SUB
SUB BtnFileSave_Click
DIM path AS STRING
path = basFileSave("Save a File", "*.txt")
path = basFileSave("Save a File", "Text Files (*.txt)|All Files (*.*)")
IF path <> "" THEN
LblResult.Caption = "Save to: " + path
PRINT "File Save: " + path

View file

@ -47,10 +47,6 @@ frm.Show
PRINT "Dynamic form created. Try the buttons!"
DO
DoEvents
LOOP
SUB OnHelloClick
DIM name AS STRING
@ -64,7 +60,7 @@ END SUB
SUB OnFileClick
DIM path AS STRING
path = basFileOpen("Open a File", "*.*")
path = basFileOpen("Open a File", "All Files (*.*)")
IF path <> "" THEN
StatusLabel.Caption = "Selected: " + path
PRINT "File: " + path

View file

@ -1,205 +0,0 @@
VERSION DVX 1.00
' helpedit.frm -- DVX Help Editor
'
' A .dhs help source editor with syntax highlighting and
' live preview via the DVX Help Viewer.
'
' Add commdlg.bas and help.bas to your project, then click Run.
Begin Form HelpEdit
Caption = "DVX Help Editor"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 640
Height = 440
Begin HBox ToolBar
Weight = 0
Begin CommandButton BtnNew
Caption = "New"
MinWidth = 60
End
Begin CommandButton BtnOpen
Caption = "Open"
MinWidth = 60
End
Begin CommandButton BtnSave
Caption = "Save"
MinWidth = 60
End
Begin Spacer Spc1
MinWidth = 16
End
Begin CommandButton BtnCompile
Caption = "Compile"
MinWidth = 80
End
Begin CommandButton BtnPreview
Caption = "Preview"
MinWidth = 80
End
Begin Spacer Spc2
Weight = 1
End
Begin Label LblStatus
Caption = "Ready."
MinWidth = 150
End
End
Begin TextArea Editor
Weight = 1
End
End
DIM currentFile AS STRING
DIM hlpFile AS STRING
currentFile = ""
hlpFile = ""
Load HelpEdit
HelpEdit.Show
' Configure the editor
Editor.SetSyntaxMode "dhs"
Editor.SetShowLineNumbers True
Editor.SetAutoIndent True
Editor.SetCaptureTabs True
Editor.SetTabWidth 2
' Start with a template
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10)
LblStatus.Caption = "New file."
DO
DoEvents
LOOP
SUB BtnNew_Click
currentFile = ""
hlpFile = ""
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10)
LblStatus.Caption = "New file."
HelpEdit.Caption = "DVX Help Editor"
END SUB
SUB BtnOpen_Click
DIM path AS STRING
path = basFileOpen("Open Help Source", "*.dhs")
IF path = "" THEN
EXIT SUB
END IF
' Read the entire file
DIM text AS STRING
DIM line AS STRING
text = ""
OPEN path FOR INPUT AS #1
DO WHILE NOT EOF(1)
LINE INPUT #1, line
IF text <> "" THEN
text = text + CHR$(10)
END IF
text = text + line
LOOP
CLOSE #1
Editor.Text = text
currentFile = path
hlpFile = ""
LblStatus.Caption = path
HelpEdit.Caption = "DVX Help Editor - " + path
END SUB
SUB BtnSave_Click
DIM path AS STRING
IF currentFile <> "" THEN
path = currentFile
ELSE
path = basFileSave("Save Help Source", "*.dhs")
IF path = "" THEN
EXIT SUB
END IF
END IF
' Write the editor contents to file
OPEN path FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
currentFile = path
LblStatus.Caption = "Saved: " + path
HelpEdit.Caption = "DVX Help Editor - " + path
END SUB
SUB BtnCompile_Click
' Save first if we have a file
IF currentFile = "" THEN
DIM path AS STRING
path = basFileSave("Save Help Source", "*.dhs")
IF path = "" THEN
LblStatus.Caption = "Save cancelled."
EXIT SUB
END IF
OPEN path FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
currentFile = path
ELSE
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
END IF
' Derive .hlp filename from .dhs filename
DIM baseName AS STRING
baseName = currentFile
' Strip .dhs extension if present
IF LEN(baseName) > 4 THEN
IF UCASE$(RIGHT$(baseName, 4)) = ".DHS" THEN
baseName = LEFT$(baseName, LEN(baseName) - 4)
END IF
END IF
hlpFile = baseName + ".hlp"
LblStatus.Caption = "Compiling..."
DoEvents
IF HelpCompile(currentFile, hlpFile) THEN
LblStatus.Caption = "Compiled: " + hlpFile
ELSE
LblStatus.Caption = "Compile failed!"
hlpFile = ""
END IF
END SUB
SUB BtnPreview_Click
IF hlpFile = "" THEN
' Try compiling first
BtnCompile_Click
END IF
IF hlpFile = "" THEN
LblStatus.Caption = "Nothing to preview. Compile first."
EXIT SUB
END IF
LblStatus.Caption = "Opening viewer..."
HelpView hlpFile
END SUB
SUB HelpEdit_QueryUnload(Cancel AS INTEGER)
' Could prompt to save here
END SUB

BIN
sdk/samples/basic/helpedit/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,17 @@
[Project]
Name = Help Editor
Author = DVX Project
Description = DVX Help source editor with syntax highlighting and preview
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
File1 = ../../../include/basic/help.bas
Count = 2
[Forms]
File0 = helpedit.frm
Count = 1
[Settings]
StartupForm = HelpEdit

View file

@ -0,0 +1,310 @@
VERSION DVX 1.00
' helpedit.frm -- DVX Help Editor
'
' A .dhs help source editor with syntax highlighting and
' live preview via the DVX Help Viewer.
'
' Add commdlg.bas and help.bas to your project, then click Run.
Begin Form HelpEdit
Caption = "DVX Help Editor"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 640
Height = 440
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuNew
Caption = "&New"
End
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuSave
Caption = "&Save"
End
Begin Menu mnuSaveAs
Caption = "Save &As..."
End
Begin Menu mnuSep2
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuBuild
Caption = "&Build"
Begin Menu mnuCompile
Caption = "&Compile"
End
Begin Menu mnuPreview
Caption = "&Preview"
End
End
Begin Menu mnuHelp
Caption = "&Help"
Begin Menu mnuHelpTopic
Caption = "Help &Topics"
End
End
Begin TextArea Editor
Weight = 1
End
Begin Label LblStatus
Weight = 0
End
End
DIM currentFile AS STRING
DIM hlpFile AS STRING
DIM savedHash AS LONG
DIM dirty AS INTEGER
currentFile = ""
hlpFile = ""
savedHash = 0
dirty = 0
Load HelpEdit
HelpEdit.Show
' Configure the editor
Editor.SetSyntaxMode "dhs"
Editor.SetShowLineNumbers True
Editor.SetAutoIndent True
Editor.SetCaptureTabs True
Editor.SetTabWidth 2
' Start with a template
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10)
MarkClean
LblStatus.Caption = "Ready."
FUNCTION TextHash() AS LONG
DIM h AS LONG
DIM t AS STRING
t = Editor.Text
h = 0
DIM i AS INTEGER
FOR i = 1 TO LEN(t)
h = h * 31 + ASC(MID$(t, i, 1))
NEXT i
TextHash = h
END FUNCTION
FUNCTION IsDirty() AS INTEGER
IsDirty = (TextHash() <> savedHash)
END FUNCTION
FUNCTION AskSave() AS INTEGER
IF NOT IsDirty() THEN
AskSave = 0
EXIT FUNCTION
END IF
DIM result AS INTEGER
result = basPromptSave("Help Editor")
IF result = DVX_SAVE_YES THEN
FileSave
AskSave = 0
ELSEIF result = DVX_SAVE_NO THEN
AskSave = 0
ELSE
AskSave = 1
END IF
END FUNCTION
SUB UpdateTitle
DIM title AS STRING
IF currentFile <> "" THEN
title = currentFile + " - DVX Help Editor"
ELSE
title = "DVX Help Editor"
END IF
IF dirty THEN
title = "* " + title
END IF
HelpEdit.Caption = title
END SUB
SUB MarkClean
dirty = 0
savedHash = TextHash()
UpdateTitle
END SUB
SUB Editor_Change
IF NOT dirty THEN
dirty = -1
UpdateTitle
END IF
END SUB
SUB mnuNew_Click
IF AskSave() THEN
EXIT SUB
END IF
currentFile = ""
hlpFile = ""
Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10)
MarkClean
LblStatus.Caption = "New file."
UpdateTitle
END SUB
SUB mnuOpen_Click
IF AskSave() THEN
EXIT SUB
END IF
DIM path AS STRING
path = basFileOpen("Open Help Source", "Help Source (*.dhs)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
DIM text AS STRING
DIM ln AS STRING
text = ""
OPEN path FOR INPUT AS #1
DO WHILE NOT EOF(1)
LINE INPUT #1, ln
IF text <> "" THEN
text = text + CHR$(10)
END IF
text = text + ln
LOOP
CLOSE #1
Editor.Text = text
currentFile = path
hlpFile = ""
MarkClean
LblStatus.Caption = path
UpdateTitle
END SUB
SUB FileSave
IF currentFile = "" THEN
FileSaveAs
EXIT SUB
END IF
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
MarkClean
LblStatus.Caption = "Saved: " + currentFile
UpdateTitle
END SUB
SUB FileSaveAs
DIM path AS STRING
path = basFileSave("Save Help Source", "Help Source (*.dhs)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
currentFile = path
FileSave
END SUB
SUB mnuSave_Click
FileSave
END SUB
SUB mnuSaveAs_Click
FileSaveAs
END SUB
SUB mnuExit_Click
Unload HelpEdit
END SUB
SUB DoCompile
' Save first
IF currentFile = "" THEN
FileSaveAs
IF currentFile = "" THEN
LblStatus.Caption = "Save cancelled."
EXIT SUB
END IF
ELSE
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
MarkClean
END IF
' Derive .hlp filename from .dhs filename
DIM baseName AS STRING
baseName = currentFile
IF LEN(baseName) > 4 THEN
IF UCASE$(RIGHT$(baseName, 4)) = ".DHS" THEN
baseName = LEFT$(baseName, LEN(baseName) - 4)
END IF
END IF
hlpFile = baseName + ".hlp"
LblStatus.Caption = "Compiling..."
DoEvents
IF HelpCompile(currentFile, hlpFile) THEN
LblStatus.Caption = "Compiled: " + hlpFile
ELSE
LblStatus.Caption = "Compile failed!"
hlpFile = ""
END IF
END SUB
SUB mnuCompile_Click
DoCompile
END SUB
SUB mnuPreview_Click
IF hlpFile = "" THEN
DoCompile
END IF
IF hlpFile = "" THEN
EXIT SUB
END IF
LblStatus.Caption = "Opening viewer..."
HelpView hlpFile
END SUB
SUB mnuHelpTopic_Click
HelpView "dvxhelp.hlp"
END SUB
SUB HelpEdit_QueryUnload(Cancel AS INTEGER)
Cancel = AskSave()
END SUB

BIN
sdk/samples/basic/iconed/ICON32.BMP (Stored with Git LFS)

Binary file not shown.

View file

@ -5,9 +5,11 @@ Company = Kangaroo Punch Studios
Version = 1.00
Copyright = Copyright 2026 Scott Duensing
Description = Icon editor for DVX.
Icon = ICON32.BMP
[Modules]
Count = 0
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = iconed.frm

View file

@ -459,6 +459,23 @@ SUB cvEditor_MouseUp(button AS INTEGER, x AS INTEGER, y AS INTEGER)
drawing = False
END SUB
SUB IconEd_QueryUnload(Cancel AS INTEGER)
IF NOT dirty THEN
EXIT SUB
END IF
DIM result AS INTEGER
result = basPromptSave("Icon Editor")
IF result = DVX_SAVE_YES THEN
mnuSave_Click
IF dirty THEN
Cancel = 1
END IF
ELSEIF result = DVX_SAVE_CANCEL THEN
Cancel = 1
END IF
END SUB
SUB cvPalette_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
DIM col AS INTEGER
DIM row AS INTEGER

BIN
sdk/samples/basic/imgview/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,16 @@
[Project]
Name = Image Viewer
Author = DVX Project
Description = BMP, PNG, JPEG, and GIF viewer
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = imgview.frm
Count = 1
[Settings]
StartupForm = ImgViewForm

View file

@ -0,0 +1,49 @@
VERSION DVX 1.00
Begin Form ImgViewForm
Caption = "Image Viewer"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 400
Height = 320
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuClose
Caption = "&Close"
End
End
Begin Image ImgDisplay
Weight = 1
Stretch = True
End
End
'$INCLUDE: 'commdlg.bas'
SUB mnuOpen_Click
DIM path AS STRING
path = basFileOpen("Open Image", "Images (*.bmp;*.png;*.jpg;*.gif)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
ImgDisplay.Picture = path
ImgViewForm.Caption = path + " - Image Viewer"
END SUB
SUB mnuClose_Click
Unload ImgViewForm
END SUB
SUB ImgDisplay_DblClick
mnuOpen_Click
END SUB

BIN
sdk/samples/basic/notepad/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,16 @@
[Project]
Name = Notepad
Author = DVX Project
Description = Simple text editor
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
Count = 1
[Forms]
File0 = notepad.frm
Count = 1
[Settings]
StartupForm = NotepadForm

View file

@ -0,0 +1,237 @@
VERSION DVX 1.00
Begin Form NotepadForm
Caption = "Untitled - Notepad"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 480
Height = 320
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuNew
Caption = "&New"
End
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuSave
Caption = "&Save"
End
Begin Menu mnuSaveAs
Caption = "Save &As..."
End
Begin Menu mnuSep2
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuEdit
Caption = "&Edit"
Begin Menu mnuCut
Caption = "Cu&t"
End
Begin Menu mnuCopy
Caption = "&Copy"
End
Begin Menu mnuPaste
Caption = "&Paste"
End
Begin Menu mnuSep3
Caption = "-"
End
Begin Menu mnuSelAll
Caption = "Select &All"
End
End
Begin TextArea Editor
Weight = 1
End
End
'$INCLUDE: 'commdlg.bas'
DIM currentFile AS STRING
DIM savedHash AS LONG
DIM dirty AS INTEGER
currentFile = ""
savedHash = 0
dirty = 0
SUB NotepadForm_Load
Editor.SetCaptureTabs True
Editor.SetTabWidth 4
END SUB
FUNCTION TextHash() AS LONG
DIM h AS LONG
DIM t AS STRING
t = Editor.Text
h = 0
DIM i AS INTEGER
FOR i = 1 TO LEN(t)
h = h * 31 + ASC(MID$(t, i, 1))
NEXT i
TextHash = h
END FUNCTION
FUNCTION IsDirty() AS INTEGER
IsDirty = (TextHash() <> savedHash)
END FUNCTION
SUB UpdateTitle
DIM title AS STRING
IF currentFile <> "" THEN
title = currentFile + " - Notepad"
ELSE
title = "Untitled - Notepad"
END IF
IF dirty THEN
title = "* " + title
END IF
NotepadForm.Caption = title
END SUB
SUB MarkClean
dirty = 0
MarkClean
END SUB
SUB Editor_Change
IF NOT dirty THEN
dirty = -1
UpdateTitle
END IF
END SUB
FUNCTION AskSave() AS INTEGER
IF NOT IsDirty() THEN
AskSave = 0
EXIT FUNCTION
END IF
DIM result AS INTEGER
result = basPromptSave("Notepad")
IF result = DVX_SAVE_YES THEN
FileSave
AskSave = 0
ELSEIF result = DVX_SAVE_NO THEN
AskSave = 0
ELSE
AskSave = 1
END IF
END FUNCTION
SUB FileSave
IF currentFile = "" THEN
FileSaveAs
EXIT SUB
END IF
OPEN currentFile FOR OUTPUT AS #1
PRINT #1, Editor.Text
CLOSE #1
MarkClean
END SUB
SUB FileSaveAs
DIM path AS STRING
path = basFileSave("Save As", "Text Files (*.txt)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
currentFile = path
FileSave
UpdateTitle
END SUB
SUB mnuNew_Click
IF AskSave() THEN
EXIT SUB
END IF
Editor.Text = ""
currentFile = ""
MarkClean
UpdateTitle
END SUB
SUB mnuOpen_Click
IF AskSave() THEN
EXIT SUB
END IF
DIM path AS STRING
path = basFileOpen("Open", "Text Files (*.txt)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
DIM text AS STRING
DIM ln AS STRING
text = ""
OPEN path FOR INPUT AS #1
DO WHILE NOT EOF(1)
LINE INPUT #1, ln
IF text <> "" THEN
text = text + CHR$(10)
END IF
text = text + ln
LOOP
CLOSE #1
Editor.Text = text
currentFile = path
MarkClean
UpdateTitle
END SUB
SUB mnuSave_Click
FileSave
END SUB
SUB mnuSaveAs_Click
FileSaveAs
END SUB
SUB mnuExit_Click
Unload NotepadForm
END SUB
SUB mnuCut_Click
Editor.Cut
END SUB
SUB mnuCopy_Click
Editor.Copy
END SUB
SUB mnuPaste_Click
Editor.Paste
END SUB
SUB mnuSelAll_Click
Editor.SelectAll
END SUB
SUB NotepadForm_QueryUnload(Cancel AS Integer)
Cancel = AskSave()
END SUB

BIN
sdk/samples/basic/resedit/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,17 @@
[Project]
Name = Resource Editor
Author = DVX Project
Description = DVX resource file editor
Icon = ICON32.BMP
[Modules]
File0 = ../../../include/basic/commdlg.bas
File1 = ../../../include/basic/resource.bas
Count = 2
[Forms]
File0 = resedit.frm
Count = 1
[Settings]
StartupForm = ResEdit

View file

@ -0,0 +1,431 @@
VERSION DVX 1.00
' resedit.frm -- DVX Resource Editor
'
' Graphical editor for the resource blocks appended to DXE3
' files (.app, .wgt, .lib). View, add, remove, and extract
' resources of type icon, text, or binary.
'
' Add commdlg.bas and resource.bas to your project, then click Run.
Begin Form ResEdit
Caption = "DVX Resource Editor"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 500
Height = 340
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuClose
Caption = "&Close"
Enabled = False
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuResource
Caption = "&Resource"
Begin Menu mnuAddText
Caption = "Add &Text..."
Enabled = False
End
Begin Menu mnuAddFile
Caption = "Add &File..."
Enabled = False
End
Begin Menu mnuEditText
Caption = "&Edit Text..."
Enabled = False
End
Begin Menu mnuSep2
Caption = "-"
End
Begin Menu mnuExtract
Caption = "E&xtract..."
Enabled = False
End
Begin Menu mnuRemove
Caption = "&Remove"
Enabled = False
End
End
Begin ListView ResList
Weight = 1
End
Begin Label LblStatus
Caption = "No file loaded."
Weight = 0
End
End
OPTION EXPLICIT
DIM filePath AS STRING
DIM resHandle AS LONG
filePath = ""
resHandle = 0
Load ResEdit
ResEdit.Show
ResList.SetColumns "Name,20|Type,8|Size,12"
LblStatus.Caption = "Ready. Use File > Open to load a DXE file."
' ============================================================
' Type name helper
' ============================================================
FUNCTION TypeName$(t AS LONG)
IF t = RES_TYPE_ICON THEN
TypeName$ = "Icon"
ELSEIF t = RES_TYPE_TEXT THEN
TypeName$ = "Text"
ELSEIF t = RES_TYPE_BINARY THEN
TypeName$ = "Binary"
ELSE
TypeName$ = "Unknown"
END IF
END FUNCTION
' ============================================================
' Format a byte size for display
' ============================================================
FUNCTION FormatSize$(sz AS LONG)
IF sz < 1024 THEN
FormatSize$ = STR$(sz) + " B"
ELSE
FormatSize$ = STR$(sz \ 1024) + " KB"
END IF
END FUNCTION
' ============================================================
' Refresh the resource list from the open handle
' ============================================================
SUB RefreshList
ResList.Clear
IF resHandle = 0 THEN
EXIT SUB
END IF
DIM n AS LONG
n = ResCount(resHandle)
DIM ix AS LONG
FOR ix = 0 TO n - 1
ResList.AddItem ResName$(resHandle, ix)
ResList.SetCell ix, 1, TypeName$(ResType(resHandle, ix))
ResList.SetCell ix, 2, FormatSize$(ResSize(resHandle, ix))
NEXT ix
LblStatus.Caption = filePath + " - " + STR$(n) + " resource(s)"
END SUB
' ============================================================
' Close the current file
' ============================================================
SUB CloseFile
IF resHandle <> 0 THEN
ResClose resHandle
resHandle = 0
END IF
filePath = ""
ResList.Clear
ResEdit.Caption = "DVX Resource Editor"
mnuClose.Enabled = False
mnuAddText.Enabled = False
mnuAddFile.Enabled = False
mnuEditText.Enabled = False
mnuExtract.Enabled = False
mnuRemove.Enabled = False
LblStatus.Caption = "No file loaded."
END SUB
' ============================================================
' Reopen the file (after modification) and refresh
' ============================================================
SUB ReopenAndRefresh
DIM path AS STRING
path = filePath
IF resHandle <> 0 THEN
ResClose resHandle
resHandle = 0
END IF
resHandle = ResOpen(path)
IF resHandle = 0 THEN
' File may have had all resources stripped
filePath = path
ResList.Clear
LblStatus.Caption = path + " - 0 resource(s)"
ELSE
filePath = path
RefreshList
END IF
END SUB
' ============================================================
' Enable/disable selection-dependent menus
' ============================================================
SUB UpdateMenuState
DIM hasSel AS INTEGER
hasSel = (ResList.ListIndex >= 0)
mnuExtract.Enabled = hasSel
mnuRemove.Enabled = hasSel
mnuEditText.Enabled = hasSel
END SUB
' ============================================================
' Menu handlers
' ============================================================
SUB mnuOpen_Click
DIM path AS STRING
path = basFileOpen("Open DXE File", "Applications (*.app)|Widget Modules (*.wgt)|Libraries (*.lib)|All Files (*.*)")
IF path = "" THEN
EXIT SUB
END IF
CloseFile
filePath = path
ResEdit.Caption = path + " - DVX Resource Editor"
mnuClose.Enabled = True
mnuAddText.Enabled = True
mnuAddFile.Enabled = True
resHandle = ResOpen(path)
IF resHandle = 0 THEN
LblStatus.Caption = "No resources in " + path
EXIT SUB
END IF
RefreshList
END SUB
SUB mnuClose_Click
CloseFile
END SUB
SUB mnuExit_Click
Unload ResEdit
END SUB
SUB mnuAddText_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = basInputBox2("Add Text Resource", "Resource name:", "")
IF rName = "" THEN
EXIT SUB
END IF
DIM text AS STRING
text = basInputBox2("Add Text Resource", "Text value:", "")
IF ResAddText(filePath, rName, text) THEN
ReopenAndRefresh
LblStatus.Caption = "Added text resource: " + rName
ELSE
LblStatus.Caption = "Failed to add resource."
END IF
END SUB
SUB mnuAddFile_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = basInputBox2("Add File Resource", "Resource name:", "")
IF rName = "" THEN
EXIT SUB
END IF
DIM typeChoice AS LONG
typeChoice = basChoiceDialog("Resource Type", "Select resource type:", "Icon|Binary", 0)
IF typeChoice < 0 THEN
EXIT SUB
END IF
DIM typeVal AS LONG
IF typeChoice = 0 THEN
typeVal = RES_TYPE_ICON
ELSE
typeVal = RES_TYPE_BINARY
END IF
DIM srcPath AS STRING
srcPath = basFileOpen("Select Source File", "All Files (*.*)")
IF srcPath = "" THEN
EXIT SUB
END IF
IF ResAddFile(filePath, rName, typeVal, srcPath) THEN
ReopenAndRefresh
LblStatus.Caption = "Added resource: " + rName
ELSE
LblStatus.Caption = "Failed to add resource."
END IF
END SUB
SUB mnuEditText_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = ResName$(resHandle, sel)
DIM t AS LONG
t = ResType(resHandle, sel)
IF t <> RES_TYPE_TEXT THEN
LblStatus.Caption = "Only text resources can be edited inline."
EXIT SUB
END IF
DIM oldText AS STRING
oldText = ResGetText$(filePath, rName)
DIM newText AS STRING
newText = basInputBox2("Edit Text Resource", "Value for '" + rName + "':", oldText)
IF ResAddText(filePath, rName, newText) THEN
ReopenAndRefresh
LblStatus.Caption = "Updated: " + rName
ELSE
LblStatus.Caption = "Failed to update resource."
END IF
END SUB
SUB mnuExtract_Click
IF filePath = "" OR resHandle = 0 THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = ResName$(resHandle, sel)
DIM outPath AS STRING
outPath = basFileSave("Extract Resource", "All Files (*.*)")
IF outPath = "" THEN
EXIT SUB
END IF
IF ResExtract(filePath, rName, outPath) THEN
LblStatus.Caption = "Extracted '" + rName + "' to " + outPath
ELSE
LblStatus.Caption = "Failed to extract resource."
END IF
END SUB
SUB mnuRemove_Click
IF filePath = "" THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM rName AS STRING
rName = ResName$(resHandle, sel)
DIM ans AS INTEGER
ans = MsgBox("Remove resource '" + rName + "'?", vbYesNo)
IF ans = vbNo THEN
EXIT SUB
END IF
' Close handle before modifying
ResClose resHandle
resHandle = 0
IF ResRemove(filePath, rName) THEN
ReopenAndRefresh
LblStatus.Caption = "Removed: " + rName
ELSE
ReopenAndRefresh
LblStatus.Caption = "Failed to remove resource."
END IF
END SUB
' ============================================================
' ListView selection change
' ============================================================
SUB ResList_Click
UpdateMenuState
END SUB
SUB ResList_DblClick
IF filePath = "" OR resHandle = 0 THEN
EXIT SUB
END IF
DIM sel AS LONG
sel = ResList.ListIndex
IF sel < 0 THEN
EXIT SUB
END IF
DIM t AS LONG
t = ResType(resHandle, sel)
IF t = RES_TYPE_TEXT THEN
mnuEditText_Click
ELSE
mnuExtract_Click
END IF
END SUB

View file

@ -283,10 +283,11 @@ int shellMain(int argc, char *argv[]) {
else if (strcmp(accelStr, "medium") == 0) { accelVal = 64; }
else if (strcmp(accelStr, "high") == 0) { accelVal = 32; }
int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8);
int32_t speed = prefsGetInt(sPrefs, "mouse", "speed", 8);
int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);
dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed);
dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed);
dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal, speed, wheelStep);
dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s speed=%ld wheelspeed=%ld", wheelStr, (long)dblClick, accelStr, (long)speed, (long)wheelStep);
// Apply saved color scheme
bool colorsLoaded = false;

View file

@ -137,8 +137,8 @@ static void onTmRun(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Applications (*.app)", "*.app" },
{ "All Files (*.*)", "*.*" }
{ "Applications (*.app)" },
{ "All Files (*.*)" }
};
char path[TM_MAX_PATH];

View file

@ -352,10 +352,103 @@ static void writeBmp(const char *path) {
fclose(f);
}
// Icon editor: pixel grid with pencil drawing a pixel
static void iconIconEd(void) {
clear(220, 220, 220);
// Draw a 5x5 grid of colored pixels (the "canvas")
int gx = 3;
int gy = 3;
int cs = 5;
// Grid lines
for (int i = 0; i <= 5; i++) {
hline(gx, gy + i * cs, 5 * cs + 1, 160, 160, 160);
vline(gx + i * cs, gy, 5 * cs + 1, 160, 160, 160);
}
// Some colored pixels in the grid
rect(gx + 1, gy + 1, cs - 1, cs - 1, 255, 0, 0);
rect(gx + cs + 1, gy + 1, cs - 1, cs - 1, 0, 0, 255);
rect(gx + 1, gy + cs + 1, cs - 1, cs - 1, 0, 180, 0);
rect(gx + cs + 1, gy + cs + 1, cs - 1, cs - 1, 255, 255, 0);
rect(gx + 2*cs+1, gy + 2*cs+1, cs - 1, cs - 1, 255, 128, 0);
rect(gx + 3*cs+1, gy + 1, cs - 1, cs - 1, 128, 0, 255);
// Pencil (diagonal, bottom-right area)
for (int i = 0; i < 12; i++) {
pixel(18 + i, 28 - i, 230, 200, 80);
pixel(19 + i, 28 - i, 230, 200, 80);
pixel(18 + i, 29 - i, 230, 200, 80);
}
// Pencil tip
pixel(17, 29, 40, 40, 40);
pixel(17, 30, 40, 40, 40);
pixel(18, 30, 40, 40, 40);
// Eraser end
pixel(29, 17, 240, 150, 150);
pixel(30, 16, 240, 150, 150);
pixel(30, 17, 240, 150, 150);
pixel(29, 16, 240, 150, 150);
// Border
rect(0, 0, 32, 1, 128, 128, 128);
rect(0, 31, 32, 1, 128, 128, 128);
rect(0, 0, 1, 32, 128, 128, 128);
rect(31, 0, 1, 32, 128, 128, 128);
}
// Resource editor: box with stacked resource entries
static void iconResedit(void) {
clear(220, 220, 220);
// Outer box (document frame)
rect(3, 2, 26, 28, 255, 255, 255);
rect(3, 2, 26, 1, 80, 80, 80);
rect(3, 29, 26, 1, 80, 80, 80);
rect(3, 2, 1, 28, 80, 80, 80);
rect(28, 2, 1, 28, 80, 80, 80);
// Title bar
rect(4, 3, 24, 4, 0, 0, 128);
// Resource entry rows (alternating light shading)
rect(4, 8, 24, 4, 240, 240, 255);
rect(4, 12, 24, 1, 180, 180, 200);
rect(4, 13, 24, 4, 255, 255, 255);
rect(4, 17, 24, 1, 180, 180, 200);
rect(4, 18, 24, 4, 240, 240, 255);
rect(4, 22, 24, 1, 180, 180, 200);
rect(4, 23, 24, 4, 255, 255, 255);
// Type indicator colored dots in each row
circle(8, 10, 1, 0, 160, 0); // icon (green)
circle(8, 15, 1, 0, 0, 200); // text (blue)
circle(8, 20, 1, 200, 0, 0); // binary (red)
circle(8, 25, 1, 0, 0, 200); // text (blue)
// Short "name" bars in each row
rect(11, 9, 10, 2, 60, 60, 60);
rect(11, 14, 14, 2, 60, 60, 60);
rect(11, 19, 8, 2, 60, 60, 60);
rect(11, 24, 12, 2, 60, 60, 60);
// Border
rect(0, 0, 32, 1, 128, 128, 128);
rect(0, 31, 32, 1, 128, 128, 128);
rect(0, 0, 1, 32, 128, 128, 128);
rect(31, 0, 1, 32, 128, 128, 128);
}
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr, "Usage: mkicon <output.bmp> <type>\n");
fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic, help\n");
fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic, help, iconed, resedit\n");
return 1;
}
@ -378,6 +471,10 @@ int main(int argc, char **argv) {
iconImgview();
} else if (strcmp(type, "help") == 0) {
iconHelp();
} else if (strcmp(type, "iconed") == 0) {
iconIconEd();
} else if (strcmp(type, "resedit") == 0) {
iconResedit();
} else {
fprintf(stderr, "Unknown icon type: %s\n", type);
return 1;

View file

@ -1024,6 +1024,13 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) {
}
static int32_t wgtAnsiTermGetScrollback(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, 0);
AnsiTermDataT *at = (AnsiTermDataT *)w->data;
return at->scrollbackMax;
}
// ============================================================
// BASIC-facing accessors
// ============================================================
@ -1072,7 +1079,7 @@ static const struct {
static const WgtPropDescT sProps[] = {
{ "Cols", WGT_IFACE_INT, (void *)wgtAnsiTermGetCols, NULL, NULL },
{ "Rows", WGT_IFACE_INT, (void *)wgtAnsiTermGetRows, NULL, NULL },
{ "Scrollback", WGT_IFACE_INT, NULL, (void *)wgtAnsiTermSetScrollback, NULL }
{ "Scrollback", WGT_IFACE_INT, (void *)wgtAnsiTermGetScrollback, (void *)wgtAnsiTermSetScrollback, NULL }
};
static const WgtMethodDescT sMethods[] = {

View file

@ -30,6 +30,8 @@ typedef struct {
bool pressed;
bool hasTransparency;
uint32_t keyColor;
char picturePath[DVX_MAX_PATH];
bool stretch; // true = scale to fit widget bounds
} ImageDataT;
@ -55,8 +57,17 @@ void widgetImageDestroy(WidgetT *w) {
void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
ImageDataT *d = (ImageDataT *)w->data;
w->calcMinW = d->imgW;
w->calcMinH = d->imgH;
if (d->stretch) {
// Stretch mode: don't force the widget to the image size.
// Let the layout engine determine the size; the paint method
// will scale the image to fit.
w->calcMinW = 0;
w->calcMinH = 0;
} else {
w->calcMinW = d->imgW;
w->calcMinH = d->imgH;
}
}
@ -94,61 +105,108 @@ void widgetImagePaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const Bit
(void)colors;
ImageDataT *d = (ImageDataT *)w->data;
if (!d->pixelData) {
if (!d->pixelData || d->imgW <= 0 || d->imgH <= 0) {
return;
}
// Center the image within the widget bounds
int32_t imgW = d->imgW;
int32_t imgH = d->imgH;
int32_t dx = w->x + (w->w - imgW) / 2;
int32_t dy = w->y + (w->h - imgH) / 2;
int32_t wgtW = w->w;
int32_t wgtH = w->h;
// Scale to fit (if stretch enabled) or use native size
int32_t fitW;
int32_t fitH;
if (d->stretch && (imgW > wgtW || imgH > wgtH)) {
fitW = wgtW;
fitH = (imgH * wgtW) / imgW;
if (fitH > wgtH) {
fitH = wgtH;
fitW = (imgW * wgtH) / imgH;
}
if (fitW <= 0) { fitW = 1; }
if (fitH <= 0) { fitH = 1; }
} else {
fitW = imgW;
fitH = imgH;
}
// Center in widget bounds
int32_t dx = w->x + (wgtW - fitW) / 2;
int32_t dy = w->y + (wgtH - fitH) / 2;
// Offset by 1px when pressed (button-press effect)
if (d->pressed) {
dx++;
dy++;
}
if (w->enabled) {
if (d->hasTransparency) {
// If image fits at 1:1, blit directly (no scaling needed)
if (fitW == imgW && fitH == imgH) {
uint8_t *src = w->enabled ? d->pixelData : d->grayData;
if (!src) {
src = d->pixelData;
}
if (d->hasTransparency && w->enabled) {
rectCopyTransparent(disp, ops, dx, dy,
d->pixelData, d->imgPitch,
src, d->imgPitch,
0, 0, imgW, imgH, d->keyColor);
} else {
rectCopy(disp, ops, dx, dy,
d->pixelData, d->imgPitch,
src, d->imgPitch,
0, 0, imgW, imgH);
}
} else {
if (!d->grayData) {
int32_t bufSize = d->imgPitch * d->imgH;
d->grayData = (uint8_t *)malloc(bufSize);
if (d->grayData) {
DisplayT tmp = *disp;
tmp.backBuf = d->grayData;
tmp.width = d->imgW;
tmp.height = d->imgH;
tmp.pitch = d->imgPitch;
tmp.clipX = 0;
tmp.clipY = 0;
tmp.clipW = d->imgW;
tmp.clipH = d->imgH;
rectCopyGrayscale(&tmp, ops, 0, 0,
d->pixelData, d->imgPitch,
0, 0, d->imgW, d->imgH);
return;
}
// Nearest-neighbor scale: sample source pixels directly into the
// display backbuffer. This avoids allocating a scaled copy.
int32_t bpp = disp->format.bitsPerPixel;
for (int32_t y = 0; y < fitH; y++) {
int32_t screenY = dy + y;
if (screenY < disp->clipY || screenY >= disp->clipY + disp->clipH) {
continue;
}
int32_t srcY = (y * imgH) / fitH;
if (srcY >= imgH) {
srcY = imgH - 1;
}
uint8_t *srcRow = d->pixelData + srcY * d->imgPitch;
for (int32_t x = 0; x < fitW; x++) {
int32_t screenX = dx + x;
if (screenX < disp->clipX || screenX >= disp->clipX + disp->clipW) {
continue;
}
}
if (d->grayData) {
rectCopy(disp, ops, dx, dy,
d->grayData, d->imgPitch,
0, 0, imgW, imgH);
} else {
rectCopy(disp, ops, dx, dy,
d->pixelData, d->imgPitch,
0, 0, imgW, imgH);
int32_t srcX = (x * imgW) / fitW;
if (srcX >= imgW) {
srcX = imgW - 1;
}
// Copy one pixel from source to display
if (bpp == 16 || bpp == 15) {
uint16_t px = ((uint16_t *)srcRow)[srcX];
((uint16_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
} else if (bpp == 32) {
uint32_t px = ((uint32_t *)srcRow)[srcX];
((uint32_t *)(disp->backBuf + screenY * disp->pitch))[screenX] = px;
} else if (bpp == 8) {
uint8_t px = srcRow[srcX];
(disp->backBuf + screenY * disp->pitch)[screenX] = px;
}
}
}
}
@ -261,19 +319,48 @@ static void wgtImageLoadFile(WidgetT *w, const char *path) {
return;
}
dvxSetBusy(ctx, true);
int32_t imgW;
int32_t imgH;
int32_t pitch;
uint8_t *buf = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch);
dvxSetBusy(ctx, false);
if (!buf) {
return;
}
ImageDataT *d = (ImageDataT *)w->data;
snprintf(d->picturePath, sizeof(d->picturePath), "%s", path);
wgtImageSetData(w, buf, imgW, imgH, pitch);
}
static bool wgtImageGetStretch(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, false);
ImageDataT *d = (ImageDataT *)w->data;
return d->stretch;
}
static void wgtImageSetStretch(WidgetT *w, bool stretch) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ImageDataT *d = (ImageDataT *)w->data;
d->stretch = stretch;
wgtInvalidatePaint(w);
}
static const char *wgtImageGetPicture(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, "");
ImageDataT *d = (ImageDataT *)w->data;
return d->picturePath;
}
static int32_t wgtImageGetWidth(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, 0);
ImageDataT *d = (ImageDataT *)w->data;
@ -308,15 +395,16 @@ static const struct {
};
static const WgtPropDescT sProps[] = {
{ "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageLoadFile, NULL },
{ "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL },
{ "ImageWidth", WGT_IFACE_INT, (void *)wgtImageGetWidth, NULL, NULL },
{ "ImageHeight", WGT_IFACE_INT, (void *)wgtImageGetHeight, NULL, NULL }
{ "Picture", WGT_IFACE_STRING, (void *)wgtImageGetPicture, (void *)wgtImageLoadFile, NULL },
{ "Stretch", WGT_IFACE_BOOL, (void *)wgtImageGetStretch, (void *)wgtImageSetStretch, NULL }
};
static const WgtIfaceT sIface = {
.basName = "Image",
.props = sProps,
.propCount = 3,
.propCount = 4,
.methods = NULL,
.methodCount = 0,
.events = NULL,

View file

@ -28,6 +28,7 @@ typedef struct {
int32_t imgH;
int32_t imgPitch;
bool pressed;
char picturePath[DVX_MAX_PATH];
} ImageButtonDataT;
@ -324,10 +325,20 @@ static void wgtImageButtonLoadFile(WidgetT *w, const char *path) {
return;
}
ImageButtonDataT *d = (ImageButtonDataT *)w->data;
snprintf(d->picturePath, sizeof(d->picturePath), "%s", path);
wgtImageButtonSetData(w, buf, imgW, imgH, pitch);
}
static const char *wgtImageButtonGetPicture(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, "");
ImageButtonDataT *d = (ImageButtonDataT *)w->data;
return d->picturePath;
}
static int32_t wgtImageButtonGetWidth(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, 0);
ImageButtonDataT *d = (ImageButtonDataT *)w->data;
@ -360,7 +371,7 @@ static const struct {
};
static const WgtPropDescT sImgBtnProps[] = {
{ "Picture", WGT_IFACE_STRING, NULL, (void *)wgtImageButtonLoadFile, NULL },
{ "Picture", WGT_IFACE_STRING, (void *)wgtImageButtonGetPicture, (void *)wgtImageButtonLoadFile, NULL },
{ "ImageWidth", WGT_IFACE_INT, (void *)wgtImageButtonGetWidth, NULL, NULL },
{ "ImageHeight", WGT_IFACE_INT, (void *)wgtImageButtonGetHeight, NULL, NULL }
};

View file

@ -101,6 +101,10 @@ typedef struct {
char **ownedCells; // stb_ds dynamic array of strdup'd strings
bool ownsCells; // true if cellData points to ownedCells
int32_t nextCell; // next cell position for AddItem (row-major index)
// Owned column definitions (deep copies for BASIC callers)
ListViewColT *ownedColDefs; // heap array of column defs (NULL if not owned)
char **ownedColTitles; // heap array of strdup'd title strings
int32_t ownedColCount;
} ListViewDataT;
@ -369,6 +373,13 @@ void widgetListViewDestroy(WidgetT *w) {
free(lv->ownedCells[i]);
}
arrfree(lv->ownedCells);
for (int32_t i = 0; i < lv->ownedColCount; i++) {
free(lv->ownedColTitles[i]);
}
free(lv->ownedColTitles);
free(lv->ownedColDefs);
free(lv->selBits);
free(lv->sortIndex);
free(lv);
@ -1673,14 +1684,20 @@ void wgtListViewAddItem(WidgetT *w, const char *text) {
return;
}
// If at a row boundary, add colCount empty cells to start a new row
if (lv->nextCell % lv->colCount == 0) {
for (int32_t c = 0; c < lv->colCount; c++) {
arrput(lv->ownedCells, strdup(""));
}
// Always align to the next row boundary so AddItem always starts a new row,
// even if SetCell was used to fill remaining columns of the previous row.
int32_t rem = lv->nextCell % lv->colCount;
if (rem != 0) {
lv->nextCell += lv->colCount - rem;
}
// Replace the next empty cell with the provided text
// Add colCount empty cells for the new row
for (int32_t c = 0; c < lv->colCount; c++) {
arrput(lv->ownedCells, strdup(""));
}
// Set the first cell of the new row to the provided text
int32_t idx = lv->nextCell;
free(lv->ownedCells[idx]);
lv->ownedCells[idx] = strdup(text ? text : "");
@ -1945,15 +1962,61 @@ static void basSetColumns(WidgetT *w, const char *spec) {
return;
}
static char buf[1024];
VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data;
// Free previous owned columns
for (int32_t i = 0; i < lv->ownedColCount; i++) {
free(lv->ownedColTitles[i]);
}
free(lv->ownedColTitles);
free(lv->ownedColDefs);
lv->ownedColTitles = NULL;
lv->ownedColDefs = NULL;
lv->ownedColCount = 0;
// Parse spec into a temporary buffer
char buf[1024];
snprintf(buf, sizeof(buf), "%s", spec);
ListViewColT cols[BAS_MAX_LISTVIEW_COLS];
int32_t count = 0;
char *tok = buf;
// First pass: count columns
int32_t count = 0;
char *p = buf;
while (*tok && count < BAS_MAX_LISTVIEW_COLS) {
// Parse "Title,Width" or just "Title"
while (*p) {
count++;
char *sep = strchr(p, '|');
if (sep) {
p = sep + 1;
} else {
break;
}
}
if (count <= 0 || count > BAS_MAX_LISTVIEW_COLS) {
return;
}
// Allocate owned storage
lv->ownedColDefs = (ListViewColT *)calloc(count, sizeof(ListViewColT));
lv->ownedColTitles = (char **)calloc(count, sizeof(char *));
if (!lv->ownedColDefs || !lv->ownedColTitles) {
free(lv->ownedColDefs);
free(lv->ownedColTitles);
lv->ownedColDefs = NULL;
lv->ownedColTitles = NULL;
return;
}
// Second pass: parse and deep-copy
snprintf(buf, sizeof(buf), "%s", spec);
int32_t idx = 0;
char *tok = buf;
while (*tok && idx < count) {
char *sep = strchr(tok, '|');
if (sep) {
@ -1964,16 +2027,18 @@ static void basSetColumns(WidgetT *w, const char *spec) {
if (comma) {
*comma = '\0';
cols[count].title = tok;
cols[count].width = wgtChars(atoi(comma + 1));
cols[count].align = ListViewAlignLeftE;
lv->ownedColTitles[idx] = strdup(tok);
lv->ownedColDefs[idx].title = lv->ownedColTitles[idx];
lv->ownedColDefs[idx].width = wgtChars(atoi(comma + 1));
lv->ownedColDefs[idx].align = ListViewAlignLeftE;
} else {
cols[count].title = tok;
cols[count].width = 0;
cols[count].align = ListViewAlignLeftE;
lv->ownedColTitles[idx] = strdup(tok);
lv->ownedColDefs[idx].title = lv->ownedColTitles[idx];
lv->ownedColDefs[idx].width = 0;
lv->ownedColDefs[idx].align = ListViewAlignLeftE;
}
count++;
idx++;
if (sep) {
tok = sep + 1;
@ -1982,9 +2047,8 @@ static void basSetColumns(WidgetT *w, const char *spec) {
}
}
if (count > 0) {
wgtListViewSetColumns(w, cols, count);
}
lv->ownedColCount = idx;
wgtListViewSetColumns(w, lv->ownedColDefs, idx);
}

View file

@ -3760,7 +3760,7 @@ static const WgtIfaceT sIfaceTextArea = {
.events = NULL,
.eventCount = 0,
.createSig = WGT_CREATE_PARENT_INT,
.createArgs = { 4096 },
.createArgs = { 65536 },
.defaultEvent = "Change"
};

View file

@ -133,6 +133,16 @@ bool wgtTimerIsRunning(const WidgetT *w) {
}
int32_t wgtTimerGetInterval(const WidgetT *w) {
if (!w || w->type != sTypeId) {
return 0;
}
TimerDataT *d = (TimerDataT *)w->data;
return d->intervalMs;
}
void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs) {
if (!w || w->type != sTypeId) {
return;
@ -230,7 +240,7 @@ static const struct {
static const WgtPropDescT sProps[] = {
{ "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled, NULL },
{ "Interval", WGT_IFACE_INT, NULL, (void *)wgtTimerSetInterval, NULL }
{ "Interval", WGT_IFACE_INT, (void *)wgtTimerGetInterval, (void *)wgtTimerSetInterval, NULL }
};
static const WgtMethodDescT sMethods[] = {