From eaf965555bb5e349b6b257fa51fe2d99af7ec077 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 12 Oct 2026 21:23:16 -0500 Subject: [PATCH] Big disks, FAT32, and long filenames are now supported. --- docs/dvx_basic_reference.html | 79 ++- docs/dvx_system_reference.html | 106 +++- installToolchain.sh | 7 +- screenshots/86box/apps-clock.png | 4 +- screenshots/86box/apps-desktop.png | 4 +- screenshots/86box/apps-dvxbasic.png | 4 +- screenshots/86box/apps-dvxdemo.png | 4 +- screenshots/86box/apps-filemgr.png | 4 +- screenshots/86box/apps-iconed.png | 4 +- screenshots/86box/apps-imgview.png | 4 +- screenshots/86box/apps-notepad.png | 4 +- screenshots/86box/apps-resedit.png | 4 +- screenshots/86box/apps-welcome.png | 4 +- screenshots/86box/boot-boot.png | 4 +- screenshots/86box/exit-cancel.png | 4 +- screenshots/86box/exit-savechg.png | 4 +- screenshots/86box/f1-h-dvxbas.png | 4 +- screenshots/86box/f1-h-welcom.png | 4 +- screenshots/86box/firstrun-fr-open.png | 4 +- screenshots/86box/fullmode-fm-desk.png | 4 +- screenshots/86box/fullscr-fs-desk.png | 4 +- screenshots/86box/hang-ended.png | 4 +- screenshots/86box/hang-taskmgr.png | 4 +- screenshots/86box/install-in-desk.png | 4 +- screenshots/86box/install-in-welc.png | 4 +- screenshots/86box/install-su-copy.png | 4 +- screenshots/86box/install-su-done.png | 4 +- screenshots/86box/install-su-fold.png | 4 +- screenshots/86box/install-su-lic.png | 4 +- screenshots/86box/install-su-unin.png | 4 +- screenshots/86box/lfn-fm-big.png | 3 + screenshots/86box/lfn-fm-old.png | 3 + screenshots/86box/lfn-lfnsmk.png | 3 + screenshots/86box/menus-m-design.png | 4 +- screenshots/86box/menus-m-file.png | 4 +- screenshots/86box/menus-m-run.png | 4 +- screenshots/86box/net-netsmk.png | 4 +- screenshots/86box/pardsgn-pardsgn.png | 4 +- screenshots/86box/parhlp-parhlp-l.png | 4 +- screenshots/86box/parhlp-parhlp.png | 4 +- screenshots/86box/parpm-parpm.png | 4 +- screenshots/86box/print-print1.png | 4 +- screenshots/86box/safevid-sv-desk.png | 4 +- screenshots/86box/sdk-sdkapp.png | 4 +- screenshots/86box/sdk-sdkbas.png | 4 +- screenshots/86box/sdk-sdkgreet.png | 4 +- screenshots/86box/sdk-sdktbx.png | 4 +- screenshots/86box/sdkgame-sdkgame.png | 4 +- screenshots/86box/sql-sqlsmk.png | 4 +- screenshots/86box/ucdemo-ucdemo.png | 4 +- screenshots/86box/ucide-ucide-ds.png | 4 +- screenshots/86box/ucide-ucide-ru.png | 4 +- screenshots/86box/ucide-ucide-tb.png | 4 +- screenshots/86box/upgrade-su-copy.png | 4 +- screenshots/86box/upgrade-su-done.png | 4 +- screenshots/86box/upgrade-su-fold.png | 4 +- screenshots/86box/upgrade-su-lic.png | 4 +- screenshots/86box/upgrade-su-unin.png | 4 +- screenshots/86box/upgrade-su-upgr.png | 4 +- screenshots/86box/welcome-wl-help.png | 4 +- screenshots/86box/welcome-wl-open.png | 4 +- screenshots/86box/welcome-wl-tour.png | 3 + screenshots/86box/welcome-wl-tour2.png | 3 + screenshots/86box/winops-w-back.png | 4 +- screenshots/86box/winops-w-min.png | 4 +- screenshots/86box/winops-w-move.png | 4 +- screenshots/86box/winops-w-rest.png | 4 +- screenshots/86box/winops-w-size.png | 4 +- screenshots/dosbox/apps-clock.png | 4 +- screenshots/dosbox/apps-desktop.png | 4 +- screenshots/dosbox/apps-dvxbasic.png | 4 +- screenshots/dosbox/apps-dvxdemo.png | 4 +- screenshots/dosbox/apps-filemgr.png | 4 +- screenshots/dosbox/apps-iconed.png | 4 +- screenshots/dosbox/apps-imgview.png | 4 +- screenshots/dosbox/apps-notepad.png | 4 +- screenshots/dosbox/apps-resedit.png | 4 +- screenshots/dosbox/apps-welcome.png | 4 +- screenshots/dosbox/boot-boot.png | 4 +- screenshots/dosbox/exit-cancel.png | 4 +- screenshots/dosbox/exit-savechg.png | 4 +- screenshots/dosbox/f1-h-dvxbas.png | 4 +- screenshots/dosbox/f1-h-welcom.png | 4 +- screenshots/dosbox/firstrun-fr-open.png | 4 +- screenshots/dosbox/fullmode-fm-desk.png | 4 +- screenshots/dosbox/fullscr-fs-desk.png | 4 +- screenshots/dosbox/hang-ended.png | 4 +- screenshots/dosbox/hang-taskmgr.png | 4 +- screenshots/dosbox/install-in-desk.png | 4 +- screenshots/dosbox/install-in-welc.png | 4 +- screenshots/dosbox/install-su-copy.png | 4 +- screenshots/dosbox/install-su-disk.png | 4 +- screenshots/dosbox/install-su-done.png | 4 +- screenshots/dosbox/install-su-fold.png | 4 +- screenshots/dosbox/install-su-lic.png | 4 +- screenshots/dosbox/install-su-unin.png | 4 +- screenshots/dosbox/install-su-wrong.png | 4 +- screenshots/dosbox/menus-m-design.png | 4 +- screenshots/dosbox/menus-m-file.png | 4 +- screenshots/dosbox/menus-m-run.png | 4 +- screenshots/dosbox/net-netsmk.png | 4 +- screenshots/dosbox/pardsgn-pardsgn.png | 4 +- screenshots/dosbox/parhlp-parhlp-l.png | 4 +- screenshots/dosbox/parhlp-parhlp.png | 4 +- screenshots/dosbox/parpm-parpm.png | 4 +- screenshots/dosbox/print-print1.png | 4 +- screenshots/dosbox/safevid-sv-desk.png | 4 +- screenshots/dosbox/sdk-sdkapp.png | 4 +- screenshots/dosbox/sdk-sdkbas.png | 4 +- screenshots/dosbox/sdk-sdkgreet.png | 4 +- screenshots/dosbox/sdk-sdktbx.png | 4 +- screenshots/dosbox/sql-sqlsmk.png | 4 +- screenshots/dosbox/ucdemo-ucdemo.png | 4 +- screenshots/dosbox/ucide-ucide-ds.png | 4 +- screenshots/dosbox/ucide-ucide-ru.png | 4 +- screenshots/dosbox/ucide-ucide-tb.png | 4 +- screenshots/dosbox/upgrade-su-copy.png | 4 +- screenshots/dosbox/upgrade-su-done.png | 4 +- screenshots/dosbox/upgrade-su-fold.png | 4 +- screenshots/dosbox/upgrade-su-lic.png | 4 +- screenshots/dosbox/upgrade-su-unin.png | 4 +- screenshots/dosbox/upgrade-su-upgr.png | 4 +- screenshots/dosbox/welcome-wl-help.png | 4 +- screenshots/dosbox/welcome-wl-open.png | 4 +- screenshots/dosbox/welcome-wl-tour.png | 3 + screenshots/dosbox/welcome-wl-tour2.png | 3 + screenshots/dosbox/winops-w-back.png | 4 +- screenshots/dosbox/winops-w-min.png | 4 +- screenshots/dosbox/winops-w-move.png | 4 +- screenshots/dosbox/winops-w-rest.png | 4 +- screenshots/dosbox/winops-w-size.png | 4 +- screenshots/masks/apps-filemgr.mask | 4 +- screenshots/masks/apps-welcome.mask | 2 + screenshots/masks/firstrun-fr-open.mask | 2 + screenshots/masks/install-in-welc.mask | 2 + screenshots/masks/install-su-disk.mask | 6 +- screenshots/masks/install-su-fold.mask | 4 +- screenshots/masks/install-su-wrong.mask | 6 +- screenshots/masks/lfn-fm-big.mask | 3 + screenshots/masks/lfn-fm-old.mask | 3 + screenshots/masks/upgrade-su-fold.mask | 4 +- screenshots/masks/upgrade-su-upgr.mask | 2 - screenshots/masks/welcome-wl-open.mask | 2 + screenshots/masks/welcome-wl-tour.mask | 2 + screenshots/masks/welcome-wl-tour2.mask | 2 + src/apps/kpunch/dvxbasic/basdev/basProject.c | 2 +- src/apps/kpunch/dvxbasic/basdev/basProject.h | 2 +- src/apps/kpunch/dvxbasic/basrt.dhs | 1 + src/apps/kpunch/dvxbasic/compiler/basEvents.h | 7 + src/apps/kpunch/dvxbasic/compiler/compiler.c | 6 + src/apps/kpunch/dvxbasic/compiler/resolve.c | 11 + src/apps/kpunch/dvxbasic/compiler/typecheck.c | 19 +- src/apps/kpunch/dvxbasic/ctrlover.dhs | 18 +- src/apps/kpunch/dvxbasic/form.dhs | 6 +- src/apps/kpunch/dvxbasic/formrt/formCtl.c | 2 +- src/apps/kpunch/dvxbasic/formrt/formGfx.c | 116 +++-- src/apps/kpunch/dvxbasic/formrt/formrt.c | 19 +- src/apps/kpunch/dvxbasic/formrt/formrt.h | 5 +- src/apps/kpunch/dvxbasic/formrt/frmModel.c | 27 +- src/apps/kpunch/dvxbasic/formrt/frmModel.h | 3 +- src/apps/kpunch/dvxbasic/formrt/frmModelBas.c | 7 + src/apps/kpunch/dvxbasic/formrt/frmParser.c | 12 + src/apps/kpunch/dvxbasic/formrt/frmParser.h | 10 + src/apps/kpunch/dvxbasic/ide/ideDesigner.c | 34 +- src/apps/kpunch/dvxbasic/ide/ideDesigner.h | 9 + src/apps/kpunch/dvxbasic/ide/ideMain.c | 2 +- src/apps/kpunch/dvxbasic/ide/ideProperties.c | 32 +- src/apps/kpunch/dvxbasic/ide/ideToolbox.c | 2 - src/apps/kpunch/dvxbasic/ideguide.dhs | 11 +- src/apps/kpunch/dvxbasic/langref.dhs | 25 +- src/apps/kpunch/dvxbasic/runtime/serialize.c | 4 +- src/apps/kpunch/dvxbasic/runtime/vm.c | 158 ++++-- src/apps/kpunch/dvxbasic/runtime/vm.h | 1 + src/apps/kpunch/dvxbasic/test_compiler.c | 52 +- src/apps/kpunch/dvxbasic/test_suite.c | 57 +- src/apps/kpunch/dvxbasic/userctl.dhs | 4 +- src/apps/kpunch/fileMgr/fileMgr.dhs | 6 +- src/apps/kpunch/fileMgr/fileMgr.frm | 26 +- src/apps/kpunch/terminal/terminal.dhs | 4 +- src/apps/kpunch/welcome/welcome.dhs | 19 +- src/apps/kpunch/welcome/welcome.frm | 493 ++++++++++++++++-- src/include/basic/fileops.bas | 32 +- src/include/basic/shell.bas | 5 + src/include/basic/xfer.bas | 9 +- src/installer/setup.c | 296 ++++++++++- src/installer/setupCore.c | 6 +- src/installer/setupCore.h | 6 +- src/libs/kpunch/dvxshell/dvxshell.dhs | 6 +- src/libs/kpunch/dvxshell/shellAssoc.c | 13 +- src/libs/kpunch/dvxshell/shellBas.c | 15 + src/libs/kpunch/dvxshell/shellMain.c | 16 +- src/libs/kpunch/fileops/fileOps.c | 28 +- src/libs/kpunch/fileops/fileOps.h | 28 +- src/libs/kpunch/fileops/fileOpsBas.c | 49 +- src/libs/kpunch/fileops/fileOpsSys.c | 39 +- src/libs/kpunch/fileops/fileops.dhs | 10 +- src/libs/kpunch/libdvx/apiref.dhs | 89 +++- src/libs/kpunch/libdvx/dvxDialog.c | 18 +- src/libs/kpunch/libdvx/dvxDraw.c | 52 ++ src/libs/kpunch/libdvx/dvxDraw.h | 5 + src/libs/kpunch/libdvx/dvxTypes.h | 2 +- src/libs/kpunch/libdvx/dvxWgt.h | 15 +- src/libs/kpunch/libdvx/dvxWgtP.h | 8 +- src/libs/kpunch/libdvx/platform/dvxPlat.h | 55 +- .../kpunch/libdvx/platform/dvxPlatformDos.c | 210 ++++---- .../kpunch/libdvx/platform/dvxPlatformHost.c | 19 +- .../kpunch/libdvx/platform/dvxPlatformUtil.c | 134 +++++ src/libs/kpunch/libdvx/widgetCore.c | 2 + src/libs/kpunch/libdvx/widgetLayout.c | 80 ++- src/libs/kpunch/libdvx/widgetScrollbar.c | 33 +- src/libs/kpunch/sql/dvxSqlVfs.c | 18 +- src/libs/kpunch/xfer/xfer.c | 194 ++++--- src/libs/kpunch/xfer/xfer.dhs | 14 +- src/libs/kpunch/xfer/xfer.h | 13 +- src/libs/kpunch/xfer/xferBas.c | 10 +- src/libs/kpunch/xfer/xferInt.h | 18 +- src/libs/kpunch/xfer/xferZmodem.c | 21 +- src/test/abi/abiSizes.txt | 4 +- src/test/basic/Makefile | 20 +- src/test/basic/fixtures/mathFuncs.out | 8 +- src/test/dos/bigDisk.py | 194 +++++++ src/test/dos/fixtures/lfnsmk/lfnsmk.dbp | 19 + src/test/dos/fixtures/lfnsmk/lfnsmk.frm | 186 +++++++ src/test/dos/runDos.sh | 135 ++++- src/test/dos/scripts/lfn.scr | 35 ++ src/test/dos/scripts/welcome.scr | 15 +- src/test/fileops/fileOpsTest.c | 21 +- src/test/formrt/frtHarness.c | 11 +- src/test/formrt/testFormRt.c | 79 ++- src/test/formrt/testFrmModel.c | 24 +- src/test/fuzz/frmModel/seeds/negIndex.frm | 24 + src/test/lint/analyzerBaseline.txt | 2 - src/test/lint/clangTidyBaseline.txt | 4 + src/test/lint/cppcheckBaseline.txt | 2 + src/test/samples/golden/basdemo-debug.app | Bin 24376 -> 24376 bytes src/test/samples/golden/basdemo-release.app | Bin 21093 -> 21093 bytes src/test/samples/golden/clock-debug.app | Bin 9777 -> 9792 bytes src/test/samples/golden/clock-release.app | Bin 9376 -> 9391 bytes src/test/samples/golden/fileMgr-debug.app | Bin 34279 -> 34458 bytes src/test/samples/golden/fileMgr-release.app | Bin 29110 -> 29253 bytes src/test/samples/golden/helpedit-debug.app | Bin 11918 -> 11918 bytes src/test/samples/golden/helpedit-release.app | Bin 10786 -> 10786 bytes src/test/samples/golden/iconed-debug.app | Bin 17066 -> 17066 bytes src/test/samples/golden/iconed-release.app | Bin 14480 -> 14480 bytes src/test/samples/golden/imgview-debug.app | Bin 9253 -> 9253 bytes src/test/samples/golden/imgview-release.app | Bin 8834 -> 8834 bytes src/test/samples/golden/inc-comm-debug.app | Bin 6721 -> 6721 bytes src/test/samples/golden/inc-comm-release.app | Bin 6669 -> 6669 bytes src/test/samples/golden/inc-commdlg-debug.app | Bin 6314 -> 6314 bytes .../samples/golden/inc-commdlg-release.app | Bin 6262 -> 6262 bytes .../samples/golden/inc-compiler-debug.app | Bin 6908 -> 6908 bytes .../samples/golden/inc-compiler-release.app | Bin 6856 -> 6856 bytes src/test/samples/golden/inc-conn-debug.app | Bin 6536 -> 6536 bytes src/test/samples/golden/inc-conn-release.app | Bin 6484 -> 6484 bytes src/test/samples/golden/inc-debug-debug.app | Bin 6418 -> 6418 bytes src/test/samples/golden/inc-debug-release.app | Bin 6366 -> 6366 bytes .../samples/golden/inc-designer-debug.app | Bin 6861 -> 6861 bytes .../samples/golden/inc-designer-release.app | Bin 6809 -> 6809 bytes src/test/samples/golden/inc-fileops-debug.app | Bin 7298 -> 7379 bytes .../samples/golden/inc-fileops-release.app | Bin 7246 -> 7327 bytes src/test/samples/golden/inc-game-debug.app | Bin 6201 -> 6201 bytes src/test/samples/golden/inc-game-release.app | Bin 6149 -> 6149 bytes src/test/samples/golden/inc-help-debug.app | Bin 6204 -> 6204 bytes src/test/samples/golden/inc-help-release.app | Bin 6152 -> 6152 bytes src/test/samples/golden/inc-hlpfile-debug.app | Bin 6486 -> 6486 bytes .../samples/golden/inc-hlpfile-release.app | Bin 6434 -> 6434 bytes .../samples/golden/inc-joystick-debug.app | Bin 6653 -> 6653 bytes .../samples/golden/inc-joystick-release.app | Bin 6601 -> 6601 bytes src/test/samples/golden/inc-layout-debug.app | Bin 6171 -> 6171 bytes .../samples/golden/inc-layout-release.app | Bin 6119 -> 6119 bytes .../samples/golden/inc-listhelp-debug.app | Bin 6707 -> 6707 bytes .../samples/golden/inc-listhelp-release.app | Bin 6655 -> 6655 bytes src/test/samples/golden/inc-music-debug.app | Bin 6462 -> 6462 bytes src/test/samples/golden/inc-music-release.app | Bin 6410 -> 6410 bytes src/test/samples/golden/inc-net-debug.app | Bin 6560 -> 6560 bytes src/test/samples/golden/inc-net-release.app | Bin 6508 -> 6508 bytes src/test/samples/golden/inc-prefs-debug.app | Bin 6560 -> 6560 bytes src/test/samples/golden/inc-prefs-release.app | Bin 6508 -> 6508 bytes src/test/samples/golden/inc-print-debug.app | Bin 6739 -> 6739 bytes src/test/samples/golden/inc-print-release.app | Bin 6687 -> 6687 bytes .../samples/golden/inc-resource-debug.app | Bin 6288 -> 6288 bytes .../samples/golden/inc-resource-release.app | Bin 6236 -> 6236 bytes .../samples/golden/inc-security-debug.app | Bin 6437 -> 6437 bytes .../samples/golden/inc-security-release.app | Bin 6385 -> 6385 bytes src/test/samples/golden/inc-shell-debug.app | Bin 7579 -> 7594 bytes src/test/samples/golden/inc-shell-release.app | Bin 7527 -> 7542 bytes src/test/samples/golden/inc-sound-debug.app | Bin 6961 -> 6961 bytes src/test/samples/golden/inc-sound-release.app | Bin 6909 -> 6909 bytes src/test/samples/golden/inc-sql-debug.app | Bin 6316 -> 6316 bytes src/test/samples/golden/inc-sql-release.app | Bin 6264 -> 6264 bytes src/test/samples/golden/inc-system-debug.app | Bin 6852 -> 6852 bytes .../samples/golden/inc-system-release.app | Bin 6800 -> 6800 bytes src/test/samples/golden/inc-xfer-debug.app | Bin 6470 -> 6470 bytes src/test/samples/golden/inc-xfer-release.app | Bin 6418 -> 6418 bytes src/test/samples/golden/notepad-debug.app | Bin 12039 -> 12039 bytes src/test/samples/golden/notepad-release.app | Bin 10798 -> 10798 bytes src/test/samples/golden/resedit-debug.app | Bin 13357 -> 13357 bytes src/test/samples/golden/resedit-release.app | Bin 11816 -> 11816 bytes src/test/samples/golden/terminal-debug.app | Bin 46251 -> 46341 bytes src/test/samples/golden/terminal-release.app | Bin 40999 -> 41089 bytes src/test/samples/golden/welcome-debug.app | Bin 10033 -> 18516 bytes src/test/samples/golden/welcome-release.app | Bin 9669 -> 16408 bytes src/test/samples/golden/widshow-debug.app | Bin 14373 -> 14373 bytes src/test/samples/golden/widshow-release.app | Bin 13447 -> 13447 bytes src/test/setup/setupTest.c | 2 +- src/test/shell/basic/shbind/shbind.frm | 6 + src/test/terminal/Makefile | 12 +- src/test/terminal/testConn.c | 6 +- src/test/terminal/testLrzsz.c | 74 +++ src/test/terminal/testXfer.c | 36 +- src/test/unit/testDraw.c | 86 ++- src/test/unit/testLayout.c | 40 +- src/test/unit/testUtil.c | 75 ++- src/test/widgets/testCovPopups.c | 100 ++++ src/test/widgets/testMisc.c | 13 + src/widgets/kpunch/box/box.bhs | 12 +- src/widgets/kpunch/box/box.dhs | 4 +- src/widgets/kpunch/box/boxMetrics.h | 8 +- src/widgets/kpunch/canvas/canvas.bhs | 2 +- src/widgets/kpunch/canvas/canvas.dhs | 3 + src/widgets/kpunch/canvas/canvas.h | 2 + src/widgets/kpunch/canvas/widgetCanvas.c | 12 +- src/widgets/kpunch/dbGrid/widgetDbGrid.c | 32 +- src/widgets/kpunch/listBox/listBox.dhs | 2 +- src/widgets/kpunch/listBox/listbox.bhs | 2 +- src/widgets/kpunch/listBox/widgetListBox.c | 248 +++++---- src/widgets/kpunch/listView/widgetListView.c | 24 +- src/widgets/kpunch/scrollPane/scrlpane.dhs | 2 +- .../kpunch/scrollPane/widgetScrollPane.c | 58 ++- src/widgets/kpunch/treeView/widgetTreeView.c | 37 +- src/widgets/kpunch/wgtsys.dhs | 4 +- src/widgets/kpunch/wrapBox/wrapBox.dhs | 4 +- 332 files changed, 4080 insertions(+), 1130 deletions(-) create mode 100644 screenshots/86box/lfn-fm-big.png create mode 100644 screenshots/86box/lfn-fm-old.png create mode 100644 screenshots/86box/lfn-lfnsmk.png create mode 100644 screenshots/86box/welcome-wl-tour.png create mode 100644 screenshots/86box/welcome-wl-tour2.png create mode 100644 screenshots/dosbox/welcome-wl-tour.png create mode 100644 screenshots/dosbox/welcome-wl-tour2.png create mode 100644 screenshots/masks/apps-welcome.mask create mode 100644 screenshots/masks/firstrun-fr-open.mask create mode 100644 screenshots/masks/install-in-welc.mask create mode 100644 screenshots/masks/lfn-fm-big.mask create mode 100644 screenshots/masks/lfn-fm-old.mask delete mode 100644 screenshots/masks/upgrade-su-upgr.mask create mode 100644 screenshots/masks/welcome-wl-open.mask create mode 100644 screenshots/masks/welcome-wl-tour.mask create mode 100644 screenshots/masks/welcome-wl-tour2.mask create mode 100644 src/test/dos/bigDisk.py create mode 100644 src/test/dos/fixtures/lfnsmk/lfnsmk.dbp create mode 100644 src/test/dos/fixtures/lfnsmk/lfnsmk.frm create mode 100644 src/test/dos/scripts/lfn.scr create mode 100644 src/test/fuzz/frmModel/seeds/negIndex.frm diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index c400dab7..cb98113a 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -501,6 +501,7 @@ img { max-width: 100%; }
  • FileOpsIsWritable
  • FileOpsJobBytesDone
  • FileOpsJobClearAttr
  • +
  • FileOpsLongNames
  • FileOpsMakePath
  • FileOpsPathIsAbsolute
  • FileOpsPathIsInside
  • @@ -508,9 +509,12 @@ img { max-width: 100%; }
  • FileOpsReadOnlySystem
  • FileOpsReplaceFile
  • FileOpsSetModified
  • +
  • FileOpsShortPath$
  • FileOpsTempDir$
  • FileOpsTotalBytes
  • +
  • FileOpsValidLongName
  • FileOpsValidName
  • +
  • FileOpsValidShortName
  • FillColor
  • FillStyle
  • FILTER
  • @@ -528,6 +532,7 @@ img { max-width: 100%; }
  • FmRawAcquire
  • FmWrite
  • Focus rectangle
  • +
  • FontScale
  • FOR
  • FOR APPEND
  • FOR BINARY
  • @@ -694,6 +699,7 @@ img { max-width: 100%; }
  • LOF
  • LOG
  • Long
  • +
  • Long File Names
  • LOOP
  • LPRINT
  • LPRINT USING
  • @@ -969,6 +975,7 @@ img { max-width: 100%; }
  • Screen.Width
  • Scrollable Container
  • ScrollBarClick
  • +
  • ScrollBarsNeeded
  • ScrollPane
  • ScrollToControl
  • Search
  • @@ -1021,6 +1028,7 @@ img { max-width: 100%; }
  • ShellAppMemory
  • ShellArg$
  • ShellArgCount
  • +
  • ShellArgQuote$
  • ShellCommandArgs$
  • ShellCommandProgram$
  • ShellConfigFile$
  • @@ -1213,6 +1221,7 @@ img { max-width: 100%; }
  • vbBlack
  • vbBlue
  • vbBoolean
  • +
  • vbBoth
  • vbByte
  • vbCancel
  • vbCascade
  • @@ -1245,6 +1254,7 @@ img { max-width: 100%; }
  • vbHidden
  • vbHimetric
  • vbHimetric
  • +
  • vbHorizontal
  • vbHourglass
  • vbInches
  • vbInformation
  • @@ -1290,6 +1300,7 @@ img { max-width: 100%; }
  • vbRetryCancel
  • vbRightButton
  • vbSaturday
  • +
  • vbSBNone
  • vbSet
  • vbSingle
  • vbSizeNESW
  • @@ -1321,6 +1332,7 @@ img { max-width: 100%; }
  • vbUseSystem
  • vbUseSystemDayOfWeek
  • vbVariant
  • +
  • vbVertical
  • vbWednesday
  • vbWhite
  • vbYellow
  • @@ -1369,7 +1381,7 @@ img { max-width: 100%; }

    DVX BASIC IDE Guide

    -

    DVX BASIC is a Visual BASIC development environment for DVX. It provides a VB3-style integrated development environment with a code editor, form designer, project system, and a full interactive debugger -- all integrated into DVX itself.

    +

    DVX BASIC is a BASIC development environment for DVX. It provides a VB3-style integrated development environment with a code editor, form designer, project system, and a full interactive debugger -- all integrated into DVX itself.

    This guide covers every feature of the IDE: menus, toolbar, editor, form designer, project management, debugger, and auxiliary windows.

    IDE Windows

    The DVX BASIC IDE is modeled after Visual Basic 3.0. It consists of several floating windows arranged on the DVX desktop:

    @@ -2171,13 +2183,22 @@ End Sub
  • VAL keeps the magnitude of very long digit strings. Running out of memory while building a string raises error 14.
  • REDIM PRESERVE of a TYPE array keeps the existing elements.
  • Appending to a String variable with s = s + x or s = s & x no longer copies the whole string each time, so building long strings in a loop is much faster. When x is InputBox$, MsgBox, CreateForm, CreateControl or Shell, s is still read before x runs (left to right).
  • +
  • And, Or, Xor, Eqv and Imp of two Booleans give a Boolean, and Not of a Boolean gives a Boolean, as in Visual Basic 4 and later: Print (a > 1) Or (b > 1) prints True instead of -1, and TypeName gives "Boolean". With a number on either side they still work on the bits and give a number.
  • +
  • Big disks and long file names: where the disk takes long file names (DVX under Windows, or a long file name driver loaded before DVX), OPEN, KILL, NAME, MKDIR, DIR$ and the other file statements use them, and the File Manager, the file dialogs and Terminal downloads keep them. A file can be bigger than 2 GB on a FAT32 disk; LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and EOF stays right below it. FileOpsFreeBytes and FileOpsTotalBytes are right for disks past 2 GB.
  • +
  • Changed: FileOpsValidName takes the folder the name is for, FileOpsValidName(folder, itemName), since the rules depend on its disk; XferSafeName$ takes the folder a file is received into, XferSafeName$(name, folder). New: FileOpsLongNames, FileOpsShortPath$, FileOpsValidLongName and FileOpsValidShortName (fileops.bas), and ShellArgQuote$ (shell.bas), which puts a path with spaces in quotes for a command line.
  • Programs compiled with an older release must be rebuilt. A program whose procedure table records more than 64 locals for a procedure is refused when it is loaded.
  • +
  • A class may have at most 65536 member variables and a form at most 65536 form-level variables (as many as the compiled program can number); more is a compile error ("Too many member variables in X", "Too many form-level variables"), and a program file that records more is refused when it is loaded.
  • Forms and Controls

    • A reference to a removed control or to a control of an unloaded form raises an "unknown control" runtime error. CreateControl with a parent that is not a container on the same form raises "CreateControl: invalid parent".
    • SetEvent works on forms (Me) and menu items; a handler without parameters is called without the event's arguments.
    • Frame: the Caption from the .frm file or set by code now shows on the frame's border, and reading Caption gives it back (it was ignored and read back "").
    • +
    • A control's Index below 0, in a .frm file or typed in the designer, means the control is not in a control array, in the designer, the form loader and the saved file alike (a negative Index was kept, saved without an Index line and read back as a different control).
    • +
    • ListBox: a horizontal scroll bar appears when the box is narrower than its longest item (its MaxWidth, or a narrow form); Left and Right scroll it a character at a time.
    • +
    • Padding = 0 and Spacing = 0 now mean no padding and no space (before, 0 meant the default). A form, box or ScrollPane that sets none keeps its own (4 pixels for forms, VBox and HBox; a Frame's Spacing is 0, a ScrollPane's both 0), and reading Padding or Spacing gives the pixels in use. In the designer, an empty Padding or Spacing on the form means the layout's own.
    • +
    • MaxWidth and MaxHeight win over MinWidth / MinHeight and over the size a control's contents ask for, and the space above the cap goes to the other controls instead of staying empty.
    • +
    • New drawing members on forms and PictureBoxes: FontScale (Print, TextWidth and TextHeight up to 8 times as large, for titles) and ScrollBarsNeeded (which scroll bars content of a size needs in an area, as the controls decide it; vbSBNone, vbHorizontal, vbVertical, vbBoth).
    • Form.Visible = True/False behaves exactly like Show/Hide, but only on a real change: Visible = True on a form that is already shown does nothing (a modal form stays modal), and on a form that has no window it is ignored. MinWidth and MinHeight read back the value that was set. Size properties are limited to 0..32767.
    • Disabling a container (for example Frame.Enabled = False) blocks mouse and keyboard input to every control inside it (also inside a disabled ScrollPane or Splitter), and SetFocus on such a control does nothing.
    • OptionButton.Value is a read/write Boolean (True = selected); the button's index is the new read-only OptionIndex. Setting Value raises no Click.
    • @@ -2479,7 +2500,7 @@ If file$ Like "*.[!b]*" Then Print "Not a .b... extension"

      Logical / Bitwise Operators

      -

      The logical operators work on Boolean values and also perform bitwise operations on integer values. With Null they give Null unless the other side decides the answer (False And Null is False, True Or Null is True); see Variants.

      +

      The logical operators work on Boolean values and also perform bitwise operations on integer values. As in Visual Basic, two Booleans give a Boolean (True And False is False, Not True is False, and Print (a > 1) Or (b > 1) prints True); when either side is a number they work on the bits and give a number (an Integer, or a Long when a side is Long): True And 3 is 3. With Null they give Null unless the other side decides the answer (False And Null is False, True Or Null is True, a Boolean as the other side is); see Variants.

        Operator   Description
         --------   -----------
         NOT        Logical/bitwise complement
      @@ -2544,7 +2565,7 @@ Dim matrix(1 To 10, 1 To 10) As Single
       Dim Shared globalFlag As Boolean
       Dim record As PersonType
       Dim fixedStr As String * 20
      -
      Note: A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables".
      +
      Note: A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables". A program holds at most 512 module-level variables, a form at most 65536 form-level variables and a class at most 65536 member variables; more fails to compile.

      Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.

      DIM variable AS NEW ClassName (or AS NEW Collection) creates a new object at the DIM and stores it in the variable; without NEW an object variable starts as Nothing. See Objects and Class Modules. DIM variable AS NEW FormName makes a new copy of a form of the project (see Several Copies of a Form in the Form topic); DIM variable AS FormName declares a variable for that form or a copy of it.

      Variable Scope and SHARED

      @@ -3223,7 +3244,7 @@ ErrorHandler: 3 RETURN without GOSUB 4 Out of DATA 5 Illegal function call (bad argument to a built-in function; an undefined math result such as a negative number to a fractional power) - 6 Overflow (value does not fit the target type; EXP, ^ or other math leaving the numeric range) + 6 Overflow (value does not fit the target type; EXP, ^ or other math leaving the numeric range; a file size or position past 2 GB in LOF, FILELEN, LOC, SEEK, GET or PUT) 7 Out of memory 9 Subscript out of range / invalid variable or field index 10 This array is fixed or temporarily locked (REDIM, or ERASE of a dynamic array, while one of its elements, or a field of one, is passed by reference to a procedure that is still running) @@ -3653,7 +3674,7 @@ WHILE first$ <> "" first$ = DIR$ ' next match, no argument WEND

      FILELEN

      -

      Returns the length of a file in bytes. A file that does not exist raises error 53 (File not found).

      +

      Returns the length of a file in bytes. A file that does not exist raises error 53 (File not found); one past 2 GB raises error 6 (Overflow).

      bytes = FILELEN(filename$)
    @@ -3915,6 +3936,7 @@ Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)

    The dim argument of LBOUND and UBOUND must be a constant from 1 to 8.

    +

    File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). A disk can hold bigger files (up to 4 GB on FAT32); LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and GET and PUT do the same for a record that would end past it. EOF stays right while the position is below 2 GB. FileOpsListSize and FileOpsTreeBytes (fileops.bas) give any file's size as a Double.

    Miscellaneous Functions

    @@ -4284,6 +4306,7 @@ NoMoney:
  • A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item and Add are fine.
  • SHARED and DIM SHARED cannot be used in a class module; a class's code reaches the program's module variables that are declared DIM SHARED or PUBLIC.
  • A STATIC variable in a class procedure is kept per object, as in Visual Basic: each object has its own copy.
  • +
  • A class has at most 65536 member variables, its procedures' STATIC variables included; more fails to compile with "Too many member variables in ClassName".
  • Class_Initialize and Class_Terminate take no parameters.
  • A class module cannot hold statements outside its procedures; the compiler reports "Only declarations are allowed outside procedures in a class module".
  • NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH and IN are not.
  • @@ -4981,8 +5004,8 @@ End Sub

    Files and Folders

    With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type or date with folders first. FileOpsListName$, FileOpsListIsFolder and FileOpsListSize read single entries, and FileOpsListFind finds one by name.

    Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsBaseName$ and FileOpsExtension$. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.

    -

    Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName (whether a name can be used as a file name here; FileOpsError$ says why not), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.

    -

    Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).

    +

    Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.

    +

    Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).

    FileOpsCopyTree, FileOpsMoveTree and FileOpsDeleteTree do a whole folder at once. For a long copy, move or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.

    Dim job As Long
     
    @@ -5036,7 +5059,7 @@ Sub FillTaskList ()
             End If
         Next i
     End Sub
    -

    Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLine) split a command line the way ShellRunCommand does: the program (the first word, or the text in double quotes when it starts with one) and the arguments after it. ShellArgCount(commandLine) and ShellArg$(commandLine, index) split any command line into its words (index from 0) by DVX's rule: words are separated by blanks, and a word with blanks in it is written in double quotes, which ShellArg$ leaves off ("" past the last word). COMMAND$(n) gives this program's own arguments the same way.

    +

    Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLine) split a command line the way ShellRunCommand does: the program (the first word, or the text in double quotes when it starts with one) and the arguments after it. ShellArgCount(commandLine) and ShellArg$(commandLine, index) split any command line into its words (index from 0) by DVX's rule: words are separated by blanks, and a word with blanks in it is written in double quotes, which ShellArg$ leaves off ("" past the last word). ShellArgQuote$(text) writes one word that way: text in double quotes when it holds blanks, as it is otherwise, so a folder or file name with spaces in it reaches the program it is passed to whole (ShellRun "notepad.app", ShellArgQuote$(path$)). COMMAND$(n) gives this program's own arguments the same way.

    Keys for every program. HotkeyAdd(keyText) makes a key such as "Ctrl+Alt+E" work whichever program is in front and returns its id (0 when it is not a key, needs Ctrl or Alt, or another program uses it). The program asks for the keys pressed from a Timer: HotkeyNext() returns the id of the oldest one not asked for yet, 0 when there is none. HotkeyRemove id stops one; the rest stop when the program ends. HotkeyText$(keyText) writes a key the way DVX does ("ctrl+alt+e" gives "Ctrl+Alt+E"; "" when it is not a key), handy for checking what the user typed. ShellItemKeyValid(keyText) is Program Manager's rule for an item's shortcut: Ctrl together with Alt or Shift, so it never takes a key a program uses itself ("" for no shortcut is valid too).

    This program. ShellAppPath$ is this program's own path (ShellRun ShellAppPath$(), "" starts another copy), and ShellInstanceCount says how many copies are running, for example to place each new window a little lower and to the right. ShellSetPriority(priority) gives this program more or less of the computer's time while other programs run too: SHELL_PRIORITY_LOW, SHELL_PRIORITY_NORMAL (what every program starts with), SHELL_PRIORITY_HIGH or any number between; ShellPriority() reads it. A program busy with long work leaves the others room at low priority. A program allows more than one copy when Project Properties has "Allow more than one copy to run at once" ticked. ShellConfigFile$(fileName) is the full path of fileName in this program's settings folder -- where DVX's own programs keep their settings, the same for every copy of the program -- and makes the folder when it is missing; keep a desktop's groups there with prefs.bas. ShellLog text writes a line to the DVX log. ShellSetDesktop program makes a program the desktop DVX starts with the next time ("" for Program Manager again); when the desktop ends, DVX ends. ShellSetWelcome program sets the program DVX starts after the desktop the next time it starts ("" for none).

    Program groups. Program Manager keeps its groups in PROGMAN.INI; a desktop written in BASIC uses the same model and file format. ShellGroupsLoad(path) opens a groups file (a file not saved yet has the groups Main and Startup) and returns a handle, ShellGroupsSave(groups, path) writes it and ShellGroupsFree frees it. Groups and items are numbered from 0; group 0 is Installed Programs, which ShellGroupsScan(groups, skipPath) fills with every installed program and which is never saved. ShellGroupsChanged tells cheaply whether a program was installed or removed since. ShellGroupCount, ShellGroupName$, ShellGroupFind, ShellGroupAdd, ShellGroupRename, ShellGroupDelete and ShellGroupNameValid manage the groups. ShellItemCount and ShellItemAdd(groups, group, name, program) list and add items; ShellItemGet$ and ShellItemSet read and change an item's fields by name ("name", "program", "args", "folder", "icon", "key", "minimized", "col", "row", "description"); ShellItemDelete, ShellItemCopy, ShellItemMove, ShellItemReorder and ShellItemPlace rearrange them, ShellGroupArrange and ShellGroupPlaceUnplaced lay out an icon grid, and ShellItemMatches is Program Manager's search. ShellGroupsGet and ShellGroupsSet read and set the options ("view": SHELL_VIEW_LARGE, SHELL_VIEW_SMALL or SHELL_VIEW_LIST; "group", the group shown; "autoArrange", "minimizeOnRun", "restoreAlone"), and ShellGroupsHistory$ / ShellGroupsHistoryAdd keep the Run history (what ShellRunDialog$ takes). ShellItemIcon$(iconFile, program) is the file an item's icon comes from -- its icon file, else its program or picture, else the program that opens the document -- ready for LOADPICTURE.

    @@ -5332,7 +5355,7 @@ Sub Timer1_Timer () End Sub

    File Transfers

    With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say) and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.

    -

    XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name) gives the 8.3 name a receive stores a sender's file name as: no folder or device name, only characters every disk accepts.

    +

    XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts.

    Lists You Draw Yourself

    With listhelp.bas in the project, a list or icon view a program draws itself -- in a UserControl or a PictureBox -- follows the same rules as ListBox and ListView. ListSelCreate(multi, count) makes a selection for count items (several at a time when multi is True) and returns a handle; it remembers which items are selected and which one is current (the one the keyboard moves). Items are numbered from 0. Give it the clicks and keys, and ask it what to draw:

      Function                                            Description
    @@ -5567,8 +5590,8 @@ End Sub
    Height Integer R/W Current height in pixels. Setting this changes the minimum height constraint. MinWidth Integer R/W Minimum width for layout. Setting it works like setting Width; reading it returns the minimum that was set. MinHeight Integer R/W Minimum height for layout. Setting it works like setting Height; reading it returns the minimum that was set. - MaxWidth Integer R/W Maximum width cap (0 = no limit, stretch to fill). - MaxHeight Integer R/W Maximum height cap (0 = no limit, stretch to fill). + MaxWidth Integer R/W Maximum width cap (0 = no limit, stretch to fill). It wins over MinWidth and over the width the control's contents ask for. + MaxHeight Integer R/W Maximum height cap (0 = no limit, stretch to fill). It wins over MinHeight and over the height the control's contents ask for. Weight Integer R/W Layout weight. 0 = fixed size, >0 = share extra space proportionally. Visible Boolean R/W Whether the control is visible. Enabled Boolean R/W Whether the control accepts user input. Disabling a container (Frame, VBox, HBox...) also blocks mouse and keyboard input to every control inside it. @@ -5658,13 +5681,13 @@ End Sub Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]] Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles). DrawFocusRect x, y, width, height [, color] Draws the dotted rectangle DVX shows around the control that has the keyboard focus (every other pixel of its edge), for things a program draws itself, such as the focused link of a help topic. A negative width or height counts from the other edge. Point(x, y) The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly. - Print items Writes text at CurrentX, CurrentY in ForeColor, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,. + Print items Writes text at CurrentX, CurrentY in ForeColor, FontScale times as large, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,. PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]] Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations). Scale [(x1, y1)-(x2, y2)] Gives the drawing coordinates of its own: (x1, y1) is the top-left corner and (x2, y2) the bottom-right one (y2 below y1 makes y go up). Scale alone goes back to pixels. ScaleX(width [, fromScale [, toScale]]) A width in fromScale converted to toScale (see Scales). ScaleY(height [, fromScale [, toScale]]) A height in fromScale converted to toScale. - TextWidth(text) Width of the text in the scale's units. - TextHeight(text) Height of a line of text in the scale's units. + TextWidth(text) Width of the text in the scale's units, at the FontScale size. + TextHeight(text) Height of a line of text in the scale's units, at the FontScale size.

    Drawing the DVX Look

    These methods draw the parts DVX's own controls are made of, in the colours of the current theme, for controls a program draws itself (a PictureBox that acts as a list or a toolbar). Coordinates and lengths are in the scale's units; colours are RGB values.

      Method                                                          Description
    @@ -5675,6 +5698,7 @@ End Sub
    DrawScrollBar x, y, length, position, visible, total [, horizontal [, control]] A scroll bar with its arrows and its thumb, for a list of total rows of which visible are shown from row position, with its top-left corner at x, y. It stands upright (16 pixels wide, length tall, arrows at the top and bottom) unless horizontal is True: then it lies across (length wide, 16 pixels tall, arrows at the left and right ends). With control True it is the narrower bar of a control such as ListBox (14 pixels). DrawScrollArrow x, y, size, direction One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft or dvxArrowRight, in the theme's scroll bar colours. ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]]) The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing. + ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight) Which scroll bars content that big needs in an area that big, as DVX's controls decide it: vbSBNone (0), vbHorizontal (1), vbVertical (2) or vbBoth (3). A vertical bar narrows the area and may make a horizontal one needed, and the other way round. The sizes are in the object's scale. Needs no drawing. PrintAccel text [, enabled] Prints a caption at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text. TextWidthAccel(text) Width of a caption without its & signs, in the scale's units. AccelKey(text) The access key of a caption in lower case ("f" for "&File"), "" when it has none. @@ -5745,6 +5769,7 @@ End Sub DrawWidth Integer 1 Pen width in pixels. FillStyle Integer 1 How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour. FillColor Long &H000000 Colour boxes and circles are filled with. + FontScale Integer 1 How many times as large Print writes its text (1 to 8): each dot of DVX's 8 x 16 font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not. AutoRedraw Boolean False True: no Paint events (see above). ScaleMode Integer 3 Units of the coordinates (see Scales). ScaleLeft Single 0 Coordinate of the left edge. @@ -6073,8 +6098,8 @@ End Sub Visible Boolean False True shows the form and False hides it, exactly like Show and Hide (the first show fires Activate). ContextMenu String "" Name of a popup menu shown when the form is right-clicked. HelpTopic String "" Help topic opened by F1 while the form is active and the focused control has no HelpTopic of its own. Empty opens the project's help file at its default page. - Padding Integer 0 Space in pixels between the window's edges and the controls (the content box's Padding); 0 is the default of 4 pixels. - Spacing Integer 0 Space in pixels between one control and the next (the content box's Spacing); 0 is the default of 4 pixels. + Padding Integer 4 Space in pixels between the window's edges and the controls (the content box's Padding); 0 is none. + Spacing Integer 4 Space in pixels between one control and the next (the content box's Spacing); 0 is none. WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen) or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have. Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form. DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files. @@ -6154,7 +6179,7 @@ Dim g As New Form1 ' declares and makes a copy at once End If End Sub

    Drawing

    -

    Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.

    +

    Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.

    Example

    Sub Form1_Load ()
         Form1.Caption = "Hello World"
    @@ -6254,8 +6279,8 @@ End Sub
    Invalidate Asks for UserControl_Paint at the next chance, once however often it is called before then: change many things (add a hundred items) and the control is drawn once. PropertyChanged name Tells a designer that a property changed, so its property sheet shows the new value. Cls, Line, PSet, Circle, Print, Point, PaintPicture, DrawFocusRect, TextWidth, TextHeight, Scale, ScaleX, ScaleY Drawing, as on a PictureBox. - DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, TextWidthAccel, AccelKey Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox. - CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox. + DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, TextWidthAccel, AccelKey Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox. + CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, FontScale, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox.

    The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control.

    The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes.

    The Control's Events

    @@ -6369,8 +6394,8 @@ End Sub
      Property    Type      Description
       --------    -------   -------------------------------------------
       Alignment   Enum      Where the children sit along the frame from top to bottom when they do not fill it: Left (the top, the default), Center or Right (the bottom).
    -  Padding     Integer   Space in pixels between the border and the children; 0 (the default) means 4 pixels.
    -  Spacing     Integer   Space in pixels between one child and the next; 0 (the default) means no space.
    + Padding Integer Space in pixels between the border and the children: 4 unless set; 0 is none. + Spacing Integer Space in pixels between one child and the next: none unless set.

    No type-specific methods or events.

    Setting Enabled = False on a Frame disables every control inside it for mouse and keyboard input.

    Container: Yes

    @@ -6395,8 +6420,8 @@ End
      Property    Type      Description
       --------    -------   -------------------------------------------
       Alignment   Enum      Where the children sit along the row when they do not fill it: Left (the left, the default), Center or Right (the right).
    -  Padding     Integer   Space in pixels between the edge of the box and the children; 0 (the default) means 4 pixels.
    -  Spacing     Integer   Space in pixels between one child and the next; 0 (the default) means 4 pixels.
    + Padding Integer Space in pixels between the edge of the box and the children: 4 unless set; 0 is none. + Spacing Integer Space in pixels between one child and the next: 4 unless set; 0 is none.

    Alignment = Right puts a row of buttons at the right, as dialog boxes have them.

    No type-specific methods or events.

    Container: Yes

    @@ -6422,8 +6447,8 @@ End
      Property    Type      Description
       --------    -------   -------------------------------------------
       Alignment   Enum      Where the children sit along the column when they do not fill it: Left (the top, the default), Center or Right (the bottom).
    -  Padding     Integer   Space in pixels between the edge of the box and the children; 0 (the default) means 4 pixels.
    -  Spacing     Integer   Space in pixels between one child and the next; 0 (the default) means 4 pixels.
    + Padding Integer Space in pixels between the edge of the box and the children: 4 unless set; 0 is none. + Spacing Integer Space in pixels between one child and the next: 4 unless set; 0 is none.

    No type-specific methods or events.

    Container: Yes

    Default Event: Click

    @@ -6504,7 +6529,7 @@ Picture1.DrawText 60, 130, "Hello!" If Picture1.Save("output.bmp") <> 0 Then MsgBox "Could not save output.bmp" End If -

    A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight) and a Paint event: see Drawing on Forms and PictureBoxes.

    +

    A PictureBox also has Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight) and a Paint event: see Drawing on Forms and PictureBoxes.

    Common Properties, Events, and Methods

    Drawing on Forms and PictureBoxes

    @@ -6820,7 +6845,7 @@ End

    ListBox

    VB Equivalent: ListBox -- DVX Widget: listbox

    -

    A scrollable list of selectable items. Items are managed via methods (AddItem, RemoveItem, Clear). Supports single and multi-select modes, drag-to-reorder, and keyboard navigation with type-ahead search.

    +

    A scrollable list of selectable items. Items are managed via methods (AddItem, RemoveItem, Clear). Supports single and multi-select modes, drag-to-reorder, and keyboard navigation with type-ahead search. When the box is narrower than its longest item (its width limited by MaxWidth or by the form), a horizontal scroll bar appears; it and the Left and Right keys scroll the items sideways a character at a time.

    Type-Specific Properties

      Property    Type      Description
       ---------   -------   -------------------------------------------
    diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html
    index a899ce39..27e4013e 100644
    --- a/docs/dvx_system_reference.html
    +++ b/docs/dvx_system_reference.html
    @@ -528,6 +528,7 @@ img { max-width: 100%; }
     
  • drawText
  • drawTextAccel
  • drawTextN
  • +
  • drawTextScaled
  • drawVLine
  • DRBG
  • Driver Modules
  • @@ -558,6 +559,7 @@ img { max-width: 100%; }
  • dvxAppHungForget
  • dvxAppIsHung
  • dvxApplyColorScheme
  • +
  • dvxArgAppend
  • dvxBeep
  • dvxButtonBox
  • dvxCalloc
  • @@ -755,6 +757,7 @@ img { max-width: 100%; }
  • Exit DVX
  • Exiting DVX
  • F1
  • +
  • FAT32
  • File Associations
  • File Dialog
  • File Transfer
  • @@ -939,6 +942,7 @@ img { max-width: 100%; }
  • ListViewColT
  • ListViewSortE
  • Log File
  • +
  • Long File Names
  • lwIP
  • make package
  • Makefile
  • @@ -1037,6 +1041,7 @@ img { max-width: 100%; }
  • PktStreamT
  • Platform Layer
  • Platform Layer
  • +
  • PLATFORM_LONG_NAME_MAX
  • platformAppExit
  • platformBootDrive
  • platformClock
  • @@ -1060,6 +1065,8 @@ img { max-width: 100%; }
  • platformKeyStateGet
  • platformLockRegion
  • platformLogMemory
  • +
  • platformLongNameChar
  • +
  • platformLongNames
  • platformMicros
  • platformMicros
  • platformMousePoll
  • @@ -1070,9 +1077,11 @@ img { max-width: 100%; }
  • platformSetAppExitHook
  • platformSetLogMemory
  • platformSetReadOnly
  • +
  • platformShortPath
  • platformShutdownHookAdd
  • platformShutdownHookRemove
  • platformSplashNearestColor
  • +
  • platformStatSize
  • platformTempDir
  • platformTimerAdd
  • platformTimerAddFast
  • @@ -1082,6 +1091,8 @@ img { max-width: 100%; }
  • platformTimerStats
  • PlatformTimerStatsT
  • platformUnlockRegion
  • +
  • platformValidateLongName
  • +
  • platformValidateShortName
  • platformVideoInit
  • PlayTone
  • PlayWave
  • @@ -1245,6 +1256,7 @@ img { max-width: 100%; }
  • ShellAppT
  • ShellArg
  • ShellArgCount
  • +
  • ShellArgQuote
  • shellAssocCommand
  • ShellAssocCommand
  • shellAssocCount
  • @@ -1504,6 +1516,7 @@ img { max-width: 100%; }
  • wgtCanvasDrawLine
  • wgtCanvasDrawRect
  • wgtCanvasDrawText
  • +
  • wgtCanvasDrawTextScaled
  • wgtCanvasFillCircle
  • wgtCanvasFillRect
  • wgtCanvasGetBorderStyle
  • @@ -1778,6 +1791,7 @@ img { max-width: 100%; }
  • widgetPaintPopupList
  • Widgets
  • widgetScrollbarApplyHit
  • +
  • widgetScrollbarNeeds
  • WidgetT
  • WidgetT
  • WidgetT
  • @@ -3496,6 +3510,18 @@ DVX_ABI_STAMP(mylib);
    count Number of characters to render fg, bg Foreground and background packed colors opaque true = fill background, false = transparent +

    drawTextScaled

    +
    void drawTextScaled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg);
    +

    Draw a null-terminated string scale times as large: every glyph pixel becomes a scale x scale block in fg, and the background shows through. For titles and banners in the 8x16 font (DVX Setup's title, the Welcome window). The text is textWidth(font, text) * scale wide and font->charHeight * scale tall. Each run of set pixels in a glyph row is one rectFill, which clips. Scale 1 is drawText; a scale below 1 draws nothing. BASIC: FontScale on a form or PictureBox (Print, TextWidth, TextHeight).

    +
      Parameter   Description
    +  ---------   -----------
    +  d           Display context
    +  ops         Blit operations vtable
    +  font        Bitmap font
    +  x, y        Top-left corner
    +  text        Null-terminated string
    +  scale       Size multiple (1 = normal)
    +  fg          Packed text color

    textWidth

    int32_t textWidth(const BitmapFontT *font, const char *text);

    Return the pixel width of a null-terminated string (strlen(text) * charWidth).

    @@ -5068,10 +5094,10 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId); void wgtSetPaddingPx(WidgetT *w, int32_t pixels); int32_t wgtGetSpacingPx(const WidgetT *w); void wgtSetSpacingPx(WidgetT *w, int32_t pixels); -

    A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return 0 when the size is unset (the container's default) or given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 restores the default) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these.

    +

    A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return what the layout uses: the container's default while the field is WGT_GAP_DEFAULT (every widget starts so), and 0 for a size given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 is no gap) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these.

    wgtLayout

    void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font);
    -

    Execute the full two-pass layout algorithm on a tree at an explicit size. Pass 1 (bottom-up): compute minimum sizes. Pass 2 (top-down): allocate space with weighted distribution; a weighted child capped by maxW/maxH stops at its cap and the space it cannot take goes to the other weighted children. Window-managed trees are laid out by wgtInvalidate and the paint path; this is for laying out a tree on demand.

    +

    Execute the full two-pass layout algorithm on a tree at an explicit size. Pass 1 (bottom-up): compute minimum sizes; a cap (maxW/maxH) wins over a larger minimum, so a widget whose content asks for more than its cap is given its cap and the rest of the space goes to its siblings. Pass 2 (top-down): allocate space with weighted distribution; a weighted child capped by maxW/maxH stops at its cap and the space it cannot take goes to the other weighted children. Window-managed trees are laid out by wgtInvalidate and the paint path; this is for laying out a tree on demand.

      Parameter     Description
       ---------     -----------
       root          Root widget
    @@ -5300,6 +5326,7 @@ static const ButtonApiT sApi = {
       WGT_SIZE_PIXELS     0x00000000   Unit tag: pixels
       WGT_SIZE_CHARS      0x40000000   Unit tag: character widths
       WGT_SIZE_PERCENT    0x80000000   Unit tag: percentage of parent
    +  WGT_GAP_DEFAULT     0xC0000000   padding / spacing: the container's own default (wgtPixels(0) is no gap)
       WGT_WEIGHT_FILL     100          Default "flexible" weight value

    Widget Interface Descriptors

    WgtPropDescT

    @@ -6069,6 +6096,9 @@ VALIDATE_WIDGET_VOID(w, wtype); // returns void on mismatch

    widgetScrollbarThumbDragScroll

    int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t visible, int32_t relMouse, int32_t maxScroll);

    Map a dragged thumb position to a scroll position in [0, maxScroll]. relMouse is where the thumb's leading edge should be, relative to the start of a track of trackLen pixels (the mouse position minus the grab offset). Returns 0 when maxScroll <= 0.

    +

    widgetScrollbarNeeds

    +
    void widgetScrollbarNeeds(int32_t totalW, int32_t totalH, int32_t *innerW, int32_t *innerH, bool *needV, bool *needH);
    +

    Which scrollbars content totalW x totalH pixels needs in a viewport of *innerW x *innerH (the area inside the widget's border). A vertical bar narrows the viewport, which may make the content too wide; a horizontal bar shortens it, which may make the content too tall; both cases are followed through. *innerW and *innerH come back less the bars (WGT_SB_W each). For rows of a fixed height pass rows * height: the content is too tall exactly when there are more rows than whole rows fit. ListBox, ListView, TreeView, ScrollPane and DBGrid decide their bars with it; BASIC's ScrollBarsNeeded is the same rule.

    Layout Functions

    widgetCalcMinSizeBox / widgetCalcMinSizeTree

    void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
    @@ -6282,8 +6312,24 @@ void platformInjectScan(uint8_t scan);

    Query total and free physical memory.

    File System

    platformValidateFilename

    -
    const char *platformValidateFilename(const char *name);
    -

    Validate a filename against platform-specific rules (8.3 on DOS). Returns NULL if valid, or a human-readable error string.

    +
    const char *platformValidateFilename(const char *dir, const char *name);
    +

    Validate a name for a new file or folder in folder dir (NULL or "" for the current one) against the rules of its disk. On DOS: platformValidateLongName where platformLongNames(dir), else platformValidateShortName, and dir with the name must stay under DVX_MAX_PATH. The host backend refuses an empty name, . and .., separators and control characters, and ignores dir. Returns NULL if valid, or a human-readable error string. The file dialog, the File Manager (fileOpsValidateName) and the IDE's project rename all use it.

    +

    Long file names: platformLongNames / platformValidateLongName / platformValidateShortName / platformLongNameChar / platformShortPath

    +
    #define PLATFORM_LONG_NAME_MAX 255
    +
    +bool        platformLongNames(const char *path);
    +const char *platformValidateLongName(const char *name);
    +const char *platformValidateShortName(const char *name);
    +bool        platformLongNameChar(char c);
    +bool        platformShortPath(const char *path, char *out, int32_t outSize);
    +

    The DOS C library uses the long-name calls (INT 21h 71xxh) by itself whenever a driver provides them (Windows, DOSLFN), so open, fopen, rename, mkdir, stat and the directory readers all take and return long names; these functions cover what the C library does not.

    +

    platformLongNames is true when the drive holding path (NULL or "": the current drive) takes long file names. On DOS that is a loaded long-name driver covering that drive; elsewhere always.

    +

    platformValidateLongName and platformValidateShortName are the two DOS name rules by themselves, whatever the disk takes; each returns NULL or why the name is not valid. A long name has 1 to PLATFORM_LONG_NAME_MAX characters and may hold spaces, several dots and + , ; = [ ]; it may not hold a control character or \ / : * ? " < > |, and may not end with a dot or a space (DOS drops them). An 8.3 name has 1 to 8 printable ASCII characters, then at most one dot and up to 3 more, none of them a space or " * + , / : ; < = > ? [ \ ] |. Neither may be . or .., or a device name (CON, PRN, AUX, NUL, CLOCK$, COM1 to COM9, LPT1 to LPT9) before its first dot. They are shared code, so the host tests check the DOS rules too.

    +

    platformLongNameChar is true when a long name may hold character c; a file transfer uses it to turn the characters a sender's name cannot keep into _.

    +

    platformShortPath writes the short (8.3) form of an existing path into out, for what reads names before a long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (an old program's command line). On a drive with long names DOS gives every part's short name (INT 21h AX=7160h CL=1); elsewhere, and when DOS cannot, path itself is copied. False when out is too small.

    +

    platformStatSize

    +
    uint64_t platformStatSize(const struct stat *st);
    +

    The size stat reported for a file. A FAT32 file reaches 4 GB - 1 byte, but the DOS C library's st_size is a signed 32-bit off_t, so a file past 2 GB reads as negative there; platformStatSize reads it as the unsigned 32-bit size DOS keeps. Use it wherever a file size comes from stat or fstat.

    platformMkdirRecursive

    int32_t platformMkdirRecursive(const char *path);

    Create a directory and all parent directories (like mkdir -p). Existing directories are not an error. Returns 0 on success, -1 on failure (including a path of DVX_MAX_PATH characters or more).

    @@ -6341,7 +6387,7 @@ int32_t platformDriveList(PlatformDriveT *out, int32_t max); bool platformDiskSpace(const char *path, uint64_t *totalBytes, uint64_t *freeBytes); char platformBootDrive(void);

    platformDriveList fills out[] with the drives that exist, in letter order, and returns the count. On DOS it uses IOCTL calls only (INT 21h AX=4409h remote, 440Eh logical drive map, 4408h removable), never the media, so empty floppy and CD-ROM drives are listed at once; the second letter of a shared floppy drive (B: on a one-floppy system) is left out because using it makes DOS ask for a disk swap. The host backend reports one drive, "/". System Information and the file operations library both use it.

    -

    platformDiskSpace reports the size and free space of the disk holding path (DOS: INT 21h AH=36h on the path's drive letter, or the current drive; host: statvfs of the nearest existing folder of path, so a folder about to be created reports its disk). False when unknown.

    +

    platformDiskSpace reports the size and free space of the disk holding path (DOS: INT 21h AX=7303h, the FAT32 call that counts past 2 GB, on the root of the path's drive letter or the current drive, falling back to AH=36h, which tops out near 2 GB, where DOS does not have it; host: statvfs of the nearest existing folder of path, so a folder about to be created reports its disk). False when unknown.

    platformBootDrive returns the letter of the drive DOS started from (INT 21h AX=3305h, DOS 4.0+), whose root holds AUTOEXEC.BAT, or 0 where there is none (older DOS, and every platform but DOS).

    Read-only media: platformSetReadOnly / platformIsReadOnly / platformDirIsWritable / platformTempDir / platformSetLogMemory / platformLogMemory

    #define PLATFORM_LOG_MEM_MAX 16384
    @@ -6379,6 +6425,9 @@ char        arg[DVX_MAX_PATH];
     while ((p = dvxNextArg(p, arg, sizeof(arg), &quoted)) != NULL) {
         // use arg
     }
    +

    dvxArgAppend

    +
    int32_t dvxArgAppend(char *buf, int32_t bufSize, int32_t pos, const char *arg);
    +

    Append arg to the command line in buf at pos as one argument that dvxNextArg reads back whole: in double quotes when it holds a blank or a tab (a long name such as C:\My Programs\Thing.app), as it is otherwise. Returns the new length, or -1 when it does not fit (as dvxStrAppendf). The shell's file-type associations and F1 help build their command lines with it; BASIC programs have ShellArgQuote$.

    DXE Module Support

    platformRegisterDxeExports

    void platformRegisterDxeExports(void);
    @@ -6749,6 +6798,7 @@ BasValueT basValToBool(BasValueT v); BAS_VM_CALL_STACK_SIZE 64 Maximum call nesting depth. BAS_VM_MAX_GLOBALS 512 Global variable slots. BAS_VM_MAX_LOCALS 64 Local variables per stack frame. + BAS_VM_MAX_MEMBER_VARS 65536 Member variables per class object; variables per form (their opcodes' 16-bit index). BAS_VM_MAX_FOR_DEPTH 32 Maximum nested FOR loop depth. BAS_VM_MAX_FILES 16 Open file channels (1-based).

    BasVmResultE

    @@ -7721,7 +7771,7 @@ bool shellAssocGet(int32_t index, char *extension, int32_t extensionSize, cha

    Opening a Document

    bool shellAssocCommand(const char *documentPath, char *program, int32_t programSize, char *args, int32_t argsSize);
     bool shellAssocOpenDocument(AppContextT *ctx, const char *documentPath);
    -

    shellAssocCommand builds the program and argument string that open a document: the value is split at the first comma, every %1 in the arguments becomes documentPath, and without arguments documentPath alone is passed (in double quotes when it contains a blank, so dvxNextArg and COMMAND$(1) still return it whole). A program that opens documents takes the first argument of its DxeAppContextT.args (dvxNextArg) as the file to open when it starts; a BASIC program uses COMMAND$(1). shellAssocOpenDocument does that and starts the program; false when there is no association or the program does not start. It shows nothing to the user (shellRunProgram does, for a document it cannot open).

    +

    shellAssocCommand builds the program and argument string that open a document: the value is split at the first comma, every %1 in the arguments becomes documentPath, and without arguments documentPath alone is passed. The path goes in double quotes when it contains a blank (dvxArgAppend), so dvxNextArg and COMMAND$(1) still return a long name such as C:\My Documents\Letter.txt whole; a template that already quotes the token ("%1") gets the path as it is. A program that opens documents takes the first argument of its DxeAppContextT.args (dvxNextArg) as the file to open when it starts; a BASIC program uses COMMAND$(1). shellAssocOpenDocument does that and starts the program; false when there is no association or the program does not start. It shows nothing to the user (shellRunProgram does, for a document it cannot open).

    Changing

    bool shellAssocSet(const char *extension, const char *programPath);
     bool shellAssocRemove(const char *extension);
    @@ -7781,10 +7831,11 @@ int32_t     ShellAppMemory(int32_t appId);
    const char *ShellCommandArgs(const char *command); int32_t ShellArgCount(const char *command); const char *ShellArg(const char *command, int32_t index); +const char *ShellArgQuote(const char *arg); int32_t ShellAppId(void); int32_t ShellPriority(void); int32_t ShellSetPriority(int32_t priority); -

    ShellCommandProgram and ShellCommandArgs are the two parts of shellSplitCommand (what shellRunCommand runs). ShellArgCount and ShellArg walk a command line with dvxNextArg -- the tokenizer shellSplitCommand and COMMAND$(n) use -- counting the program as argument 0; ShellArg drops the quotes and gives "" past the last. ShellAppId is ctx->currentAppId. ShellPriority and ShellSetPriority are tsGetPriority / tsSetPriority on the running program's mainTaskId (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH, shell.bas SHELL_PRIORITY_*); -1 / False for a program without a task of its own (every BASIC program has one) or a priority out of range.

    +

    ShellCommandProgram and ShellCommandArgs are the two parts of shellSplitCommand (what shellRunCommand runs). ShellArgCount and ShellArg walk a command line with dvxNextArg -- the tokenizer shellSplitCommand and COMMAND$(n) use -- counting the program as argument 0; ShellArg drops the quotes and gives "" past the last. ShellArgQuote is the other direction (dvxArgAppend): arg as one word of a command line, in double quotes when it holds blanks, "" when it is too long for one (SHELL_ARGS_MAX). ShellAppId is ctx->currentAppId. ShellPriority and ShellSetPriority are tsGetPriority / tsSetPriority on the running program's mainTaskId (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH, shell.bas SHELL_PRIORITY_*); -1 / False for a program without a task of its own (every BASIC program has one) or a priority out of range.

    Hotkeys

    int32_t     HotkeyAdd(const char *keyText);
     int32_t     HotkeyNext(void);
    @@ -8133,6 +8184,8 @@ dvxSqlExec(db, sql);

    Loaded as: bin/libs/kpunch/fileops/fileops.lib

    DOS Disk Errors

    On DOS, touching a drive with no disk (or a write-protected or removed one) raises a critical error, which would normally wait at an "Abort, Retry, Fail" prompt on the hidden text screen. DVX installs a critical-error handler at startup (platformInit) that answers Fail, so every call here, and your own disk code, simply gets an error (errno) instead. platformDriveList never touches the media at all (IOCTL calls only), so listing drives costs nothing even with empty drives.

    +

    Big Disks and Long File Names

    +

    Disks past 2 GB report their real size and free space: platformDiskSpace asks DOS with the FAT32 call first and falls back to the older one, which stops near 2 GB. File sizes are 64-bit (FileOpsEntryT.size, fileOpsTreeSize, the job byte counts); a FAT32 file reaches 4 GB - 1 byte, and platformStatSize reads it correctly where stat's st_size would be negative. Where a long-name driver is running (Windows, DOSLFN) and covers the drive (platformLongNames), listings show long names, jobs copy and move them, attributes and renames work on them, fileOpsValidateName accepts them and fileOpsPathNormalize keeps their case; elsewhere the 8.3 rules apply.

    Attributes and Dates

      Function                                     Description
       --------                                     -----------
    @@ -8152,13 +8205,13 @@ dvxSqlExec(db, sql);
    fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it. fileOpsMakeDir(dir, name, err, errSize) Create a folder; a reason in err on failure. fileOpsRename(dir, oldName, newName, err, errSize) Rename within a folder; refuses a name already in use. - fileOpsValidateName(name, err, errSize) Check a new name (8.3 rules and device names on DOS). + fileOpsValidateName(dir, name, err, errSize) Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name. fileOpsPathJoin(out, size, dir, name) dir + separator + name; false when it does not fit. fileOpsPathParent(path, out, size) Parent folder; false at a root. fileOpsPathIsRoot(path) True for "C:\", "C:" or "/". fileOpsPathIsInside(path, dir) True when path is dir or below it. fileOpsPathSame(a, b) Same place (separators equal, DOS ignores case, a trailing separator ignored). - fileOpsPathNormalize(path, out, size) Canonical absolute form of an existing folder. + fileOpsPathNormalize(path, out, size) Canonical absolute form of an existing folder (DOS: upper case, except names on a disk with long names, which keep their case).

    Jobs

    A job copies, moves or deletes any number of files and folders (recursively):

    FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, "C:\\BACKUP");
    @@ -8204,7 +8257,7 @@ fileOpsJobFree(job);

    fileOpsJobClearAttr(job, attr), called before the first step, clears those attribute bits on every file a copy or move writes: Setup passes FILEOPS_ATTR_READONLY, because every file on a CD-ROM is read-only and the installed copies must not be.

    fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx) is called after every step (return false to cancel) and ask(job, kind, message, ctx) answers questions. With no callbacks, existing files are kept (No) and failing items are skipped.

    BASIC

    -

    src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.

    +

    src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.

    Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.

    @@ -10326,14 +10379,14 @@ if (xferPoll(x, nowMs()) != XferRunningE) { XferT *xferCreate(void) Allocate an idle transfer (NULL when out of memory). void xferDestroy(x) Close any open file and free the transfer. void xferOptionsDefaults(opt) Ask when a file exists, no window limit, no extra escaping, no resume request. - bool xferStartSend(x, proto, io, opt, paths, count, now) Send count files (XMODEM sends the first). False, with the reason in the message, when a file cannot be opened. opt may be NULL. + bool xferStartSend(x, proto, io, opt, paths, count, now) Send count files (XMODEM sends the first). False, with the reason in the message, when a file cannot be opened or is larger than 2 GB. opt may be NULL. bool xferStartReceive(x, proto, io, opt, target, now) XMODEM: target is the file to create. YMODEM and ZMODEM: target is the folder for the files. XferStatusE xferPoll(x, now) Move the transfer along. now is a millisecond clock (wrapping is fine). void xferCancel(x) Send the cancel sequence to the other side and end with XferCancelledE. void xferAnswer(x, action, now) Answer XferAskingE: XferExistResumeE, RenameE, OverwriteE or SkipE. const XferProgressT *xferProgress(x) Progress and outcome. bool xferDetectFeed(d, byte) Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself. - void xferSafeName(name, out) Reduce a sender's file name to a safe 8.3 name: no path, no drive, no DOS device name (CON, PRN, COM1 and so on get a leading _), only portable characters. out holds XFER_NAME_MAX bytes. + void xferSafeName(name, longNames, out) Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1 and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1). uint16_t xferCrc16(data, len) CRC-16/XMODEM (polynomial 0x1021). uint32_t xferCrc32(data, len) CRC-32 (IEEE 802.3), as ZMODEM uses it. int32_t xferBasNew(&x) For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way). @@ -10346,11 +10399,12 @@ if (xferPoll(x, nowMs()) != XferRunningE) {

    XMODEM and XMODEM-1K

    Packets are SOH (128 bytes) or STX (1024 bytes), the block number and its complement, the data, and a CRC-16 or a one-byte checksum. The receiver asks for CRC mode with 'C' every 3 seconds and falls back to checksums (NAK) after three tries, unless the sender has already started. Every packet is answered with ACK or NAK; two CANs cancel. After 10 failed tries of one packet the transfer gives up and cancels the other side. A damaged packet is refused only once the line has been quiet for a second, so the rest of it cannot be mistaken for the next packet. An EOT counts only when the line stays quiet after it (a stray 0x04 inside a damaged packet does not end the file early). A sender whose 1K block fails three times sends that block and the rest of the file in 128-byte blocks. XMODEM pads the last block with 0x1A (SUB); the receiver keeps the padding because the protocol does not carry the file size.

    YMODEM

    -

    A batch of files, always with CRC and 1K blocks (a tail of at most 128 bytes goes in a 128-byte block). Block 0 carries the file name and size before each file, and an empty block 0 ends the batch. The receiver refuses the first EOT of each file and accepts the second, and cuts the padding using the size.

    +

    A batch of files, always with CRC and 1K blocks (a tail of at most 128 bytes goes in a 128-byte block). Block 0 carries the file name and size before each file (the long name when it fits in the 128-byte block beside the other fields, else the 8.3 name), and an empty block 0 ends the batch. The receiver refuses the first EOT of each file and accepts the second, and cuts the padding using the size.

    ZMODEM

    Headers are sent in hex (to start and answer) or binary with CRC-16 or CRC-32; data travels in subpackets with a CRC over the data and the frame end. ZDLE escaping protects ZDLE, DLE, XON and XOFF (with and without the high bit) and a CR after '@'; with escapeCtl every control character is escaped (the sender announces it with ZSINIT; the receiver asks for it in ZRINIT). Five CANs in a row cancel.

    -

    The sender streams: it keeps at most 8 KB unacknowledged, asks for a progress acknowledgement every 4 KB, honours the receiver's buffer size (a receiver without flow control should set rxWindow to what its hardware buffer holds, so a slow frame never loses data), and builds a subpacket only when the transport has sent nearly everything it was given (XferIoT.pending). On an error the receiver sends ZRPOS with the last good position; the sender goes back there and halves its subpacket size (down to 64 bytes), then doubles it again after eight clean subpackets. A sender that gets no acknowledgement for the window goes back to the last acknowledged position. File names travel in the ZFILE subpacket with the size and the files and bytes left in the batch, which the receiver shows as batch progress. Commands from the other side (ZCOMMAND) are never run.

    -

    Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping and resume.

    +

    The sender streams: it keeps at most 8 KB unacknowledged, asks for a progress acknowledgement every 4 KB, honours the receiver's buffer size (a receiver without flow control should set rxWindow to what its hardware buffer holds, so a slow frame never loses data), and builds a subpacket only when the transport has sent nearly everything it was given (XferIoT.pending). On an error the receiver sends ZRPOS with the last good position; the sender goes back there and halves its subpacket size (down to 64 bytes), then doubles it again after eight clean subpackets. A sender that gets no acknowledgement for the window goes back to the last acknowledged position. File names (long names as they are) travel in the ZFILE subpacket with the size and the files and bytes left in the batch, which the receiver shows as batch progress. Commands from the other side (ZCOMMAND) are never run.

    +

    Files are at most 2 GB (sizes and positions are 32-bit): a send refuses a larger file, a size offered past 2 GB reads as unknown, and a receive that passes 2 GB stops with "A file larger than 2 GB cannot be transferred". A file already on the disk is never mistaken for a missing one, whatever its size.

    +

    Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping, resume and long file names.

    File Transfers from BASIC

    @@ -10375,7 +10429,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) { XferCancel handle Stop a running transfer (the other side is told). XferClose handle Free the handle. XferCrc16(bytes), XferCrc32(bytes) xferCrc16 / xferCrc32 of a BINARY STRING (every byte value counts); the CRC-32 as a LONG (negative when its top bit is set). - XferSafeName$(fileName) xferSafeName: the 8.3 name a receive stores a sender's file name as. + XferSafeName$(fileName, folder) xferSafeName: the name a receive into folder stores a sender's file name as (a long name where folder's disk takes them, else 8.3).

    XFER_WINDOW_MAX (xfer.bas) is the largest ZMODEM receive window conn.bas's ConnSetXferWindow takes.

    Protocols: XFER_XMODEM, XFER_XMODEM1K, XFER_YMODEM, XFER_ZMODEM. At most eight transfers run at once (XFER_MAX_FILES files in one batch).

    Dim h As Long
    @@ -10409,8 +10463,8 @@ End Sub
    prefW, prefH int32_t (tagged) Preferred size. 0 = auto. weight int32_t Extra-space distribution weight. 0 = fixed, 100 = normal. A widget with weight=200 gets twice the extra space of one with weight=100. align WidgetAlignE Main-axis alignment for children: AlignStartE, AlignCenterE, AlignEndE. - spacing int32_t (tagged) Spacing between children (containers only). 0 = default. - padding int32_t (tagged) Internal padding (containers only). 0 = default. + spacing int32_t (tagged) Spacing between children (containers only). WGT_GAP_DEFAULT = the class's default; 0 = none. + padding int32_t (tagged) Internal padding (containers only). WGT_GAP_DEFAULT = the class's default; 0 = none. fgColor uint32_t Foreground color override. 0 = use color scheme default. bgColor uint32_t Background color override. 0 = use color scheme default. visible bool Visibility state. @@ -10570,8 +10624,8 @@ WidgetT *group = wgtFrame(parent, "Options");
      Field      Description
       -----      -----------
       align      Main-axis alignment of children. HBox: Start=left, Center=center, End=right. VBox: Start=top, Center=center, End=bottom.
    -  spacing    Gap between children (tagged size). Default 4 pixels for VBox/HBox, 0 for Frame.
    -  padding    Internal padding around children (tagged size). Default 4 pixels.
    +  spacing    Gap between children (tagged size). WGT_GAP_DEFAULT (the default): 4 pixels for VBox/HBox, 0 for Frame; wgtPixels(0) is none.
    +  padding    Internal padding around children (tagged size). WGT_GAP_DEFAULT (the default): 4 pixels; wgtPixels(0) is none.
       weight     Controls how the box itself stretches within its parent.

    Frame text is managed via the standard wgtSetText() / wgtGetText() interface (the widget has WCLASS_HAS_TEXT). BASIC code can set its title via the generic "Caption" or "Text" property.

    Events

    @@ -10629,6 +10683,7 @@ btn->onClick = onOkClicked; void wgtCanvasSetPixel(w, x, y, color) Set a single pixel to the given packed color. uint32_t wgtCanvasGetPixel(w, x, y) Read a single pixel. Returns packed color. void wgtCanvasDrawText(w, x, y, text) Draw text at the given canvas coordinates using the current pen color. + void wgtCanvasDrawTextScaled(w, x, y, text, scale) Draw text scale times as large (each font pixel a scale x scale block) in the pen color: titles. Scale 1 is wgtCanvasDrawText. BASIC: FontScale and Print on a PictureBox. void wgtCanvasSetMouseCallback(w, cb) Set a mouse callback. Signature: void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag). cx/cy are canvas-relative, drag=true on mouse-move during press. int32_t wgtCanvasSave(w, path) Save the canvas buffer to an image file: BMP when path ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error. int32_t wgtCanvasLoad(w, path) Load an image file into the canvas (resizes buffer to match). Returns 0 on success, -1 on error. @@ -10657,7 +10712,8 @@ btn->onClick = onOkClicked; resize wgtCanvasResize setBorderStyle wgtCanvasSetBorderStyle getBorderStyle wgtCanvasGetBorderStyle - getDisplay wgtCanvasGetDisplay + getDisplay wgtCanvasGetDisplay + drawTextScaled wgtCanvasDrawTextScaled

    Properties

    Drawing state (pen color, pen size) is managed by the API functions above. The BASIC interface registers these properties:

      Property      Type      Access      Description
    @@ -11114,7 +11170,7 @@ btn->onClick = onSaveClicked;

    ListBox

    -

    A scrollable list of text items. Supports single and multi-select modes (Shift-click for range, Ctrl-click to toggle), drag-to-reorder, mouse wheel scroll, and keyboard navigation (Up/Down/PgUp/PgDn/Home/End with type-ahead search on printable keys). Depends on the "listhelp" helper library (declared in listbox.dep).

    +

    A scrollable list of text items. Supports single and multi-select modes (Shift-click for range, Ctrl-click to toggle), drag-to-reorder, mouse wheel scroll, and keyboard navigation (Up/Down/PgUp/PgDn/Home/End with type-ahead search on printable keys). A horizontal scrollbar appears when the box is laid out narrower than its widest item (a maxW below it); it and Left/Right scroll a character at a time. The bars follow widgetScrollbarNeeds. Depends on the "listhelp" helper library (declared in listbox.dep).

    Items can be set from a caller-owned array (wgtListBoxSetItems, which keeps a pointer to it) or managed with AddItem/RemoveItem/Clear. AddItem and RemoveItem on a list set from an array first copy that array, so they extend or shorten the caller's list without modifying it; drag-to-reorder also works on the copy.

    Header: widgets/listBox.h

    Creation

    @@ -11366,7 +11422,7 @@ WidgetT *content = wgtVBox(sp); void wgtScrollPaneSetScrollTop(w, pos) Set the vertical scroll position; the layout clamps it to the content. int32_t wgtScrollPaneGetScrollLeft(w) Horizontal scroll position in pixels. void wgtScrollPaneSetScrollLeft(w, pos) Set the horizontal scroll position; the layout clamps it to the content. -

    Padding and spacing are the common WidgetT padding / spacing fields (unset = none); wgtSetPaddingPx / wgtSetSpacingPx set them in pixels.

    +

    Padding and spacing are the common WidgetT padding / spacing fields (WGT_GAP_DEFAULT, the default, is none); wgtSetPaddingPx / wgtSetSpacingPx set them in pixels.

    When the user moves the scroll position (scrollbar arrows and trough, dragging a thumb, the arrow, page, Home and End keys) the scroll pane fires onChange (widgetFireChange) after the position is clamped; the setters, wgtScrollPaneScrollToTop and scrolling a focused child into view do not. BASIC sees it as the ScrollPane's Change event.

    API Struct (wgtRegisterApi "scrollpane")

      Slot             Function
    @@ -11888,8 +11944,8 @@ wgtButton(wrap, "Tag 3");

    WrapBox uses the common WidgetT container fields for layout control:

      Property     Description
       --------     -----------
    -  spacing      Gap between children in both the horizontal and vertical directions (tagged size). Default is 4 pixels.
    -  padding      Internal padding around children (tagged size). Default is 2 pixels.
    + spacing Gap between children in both the horizontal and vertical directions (tagged size). Default is 4 pixels; wgtPixels(0) is none. + padding Internal padding around children (tagged size). Default is 2 pixels; wgtPixels(0) is none.

    Properties (BASIC Interface)

      Property     Type                         Access       Description
       --------     ----                         ------       -----------
    diff --git a/installToolchain.sh b/installToolchain.sh
    index bed16338..9e33afd2 100755
    --- a/installToolchain.sh
    +++ b/installToolchain.sh
    @@ -230,9 +230,10 @@ else
         echo "installToolchain: 86Box ROM set installed in ${BOX86_ROMS}"
     fi
     
    -# FreeDOS boot files, the CD-ROM driver and CuteMouse.  The pin names the
    -# files taken from the image, so a change to the list installs again.
    -FREEDOS_IMG_FILES="KERNEL.SYS COMMAND.COM FREEDOS/BIN/HIMEMX.EXE FREEDOS/BIN/UDVD2.SYS FREEDOS/BIN/SHSUCDX.COM"
    +# FreeDOS boot files, the CD-ROM driver, the long file name driver
    +# (DOSLFN, for "# LFN" scripts) and CuteMouse.  The pin names the files
    +# taken from the image, so a change to the list installs again.
    +FREEDOS_IMG_FILES="KERNEL.SYS COMMAND.COM FREEDOS/BIN/HIMEMX.EXE FREEDOS/BIN/UDVD2.SYS FREEDOS/BIN/SHSUCDX.COM FREEDOS/BIN/DOSLFN.COM"
     FREEDOS_PIN="${FREEDOS_SHA256} ${CTMOUSE_SHA256} ${FREEDOS_IMG_FILES// /,}"
     
     if [ "${FORCE}" -eq 0 ] && pinned "${FREEDOS_DIR}" "${FREEDOS_PIN}"; then
    diff --git a/screenshots/86box/apps-clock.png b/screenshots/86box/apps-clock.png
    index 4fb10889..b3aa3b16 100644
    --- a/screenshots/86box/apps-clock.png
    +++ b/screenshots/86box/apps-clock.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:f6ec18b269f94472d35f27226277957107b7663276aed5d0710c223222f46918
    -size 17880
    +oid sha256:8a6e4ccd7233092f377ac0398a323d2a57f465da9a91c633e8a09402794f5ebd
    +size 17977
    diff --git a/screenshots/86box/apps-desktop.png b/screenshots/86box/apps-desktop.png
    index b4f19953..4f9610d8 100644
    --- a/screenshots/86box/apps-desktop.png
    +++ b/screenshots/86box/apps-desktop.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e7011560165479cdaa30d0e696262c97aa9c8ba906ef69a54ea07bb7cc4344ed
    -size 18200
    +oid sha256:5d14ced0f9793e53e27952b0ea5f69475f373a8c389838877ccb71ae8170818f
    +size 18508
    diff --git a/screenshots/86box/apps-dvxbasic.png b/screenshots/86box/apps-dvxbasic.png
    index 419a35df..adafd78e 100644
    --- a/screenshots/86box/apps-dvxbasic.png
    +++ b/screenshots/86box/apps-dvxbasic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:433a17e0cc1a0238b0a5b274bf63c42a438dd660fd985571ccf620b3dae58232
    -size 18285
    +oid sha256:ca33ce84d1585ab033f84a3e9063897633471477cd038439db44659931e86999
    +size 18318
    diff --git a/screenshots/86box/apps-dvxdemo.png b/screenshots/86box/apps-dvxdemo.png
    index 445aa0e6..f87d1eaa 100644
    --- a/screenshots/86box/apps-dvxdemo.png
    +++ b/screenshots/86box/apps-dvxdemo.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:18f6db09fcb2f25ce2962c73fa71a59062ac52087bc6ddd755a5b4d73c1f01b6
    -size 19646
    +oid sha256:33bd5942e94f3464108618c89896dabc72504f6f4eafea4bf7ff5a1f33212788
    +size 19660
    diff --git a/screenshots/86box/apps-filemgr.png b/screenshots/86box/apps-filemgr.png
    index 8b1fc447..7aaac1d4 100644
    --- a/screenshots/86box/apps-filemgr.png
    +++ b/screenshots/86box/apps-filemgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3354f7ea3b49b9a647d56730c7678a6ee03cb58eaf38f87ffffab2679d21c3d1
    -size 19986
    +oid sha256:5404afcf80fb3118a908e7727fbf1c23d349476fd9b25f11c2ae061e4b8d3b71
    +size 19573
    diff --git a/screenshots/86box/apps-iconed.png b/screenshots/86box/apps-iconed.png
    index 45b7c3c8..f305a142 100644
    --- a/screenshots/86box/apps-iconed.png
    +++ b/screenshots/86box/apps-iconed.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5390985238915e4e03de2d0c543d8f5ee60903fa2458f6a7bfeedd3efd4aee23
    -size 14825
    +oid sha256:a8ea0efb748f7f264cd034cdafbc33b5d35aa9ac0324be02d0daacf213aad97d
    +size 14855
    diff --git a/screenshots/86box/apps-imgview.png b/screenshots/86box/apps-imgview.png
    index 0d70c33f..a50ad693 100644
    --- a/screenshots/86box/apps-imgview.png
    +++ b/screenshots/86box/apps-imgview.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3db64140b452c26b75e386064439f8b7ad6c463ad73e0c11a21eb26afd11283e
    -size 12889
    +oid sha256:c92d11a6a8ad8e9194751f84f37f126d728ee4789e075a2b55f17053c0d0fee1
    +size 12908
    diff --git a/screenshots/86box/apps-notepad.png b/screenshots/86box/apps-notepad.png
    index c3727af8..5185b7e5 100644
    --- a/screenshots/86box/apps-notepad.png
    +++ b/screenshots/86box/apps-notepad.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:fb5755dc8331b3c887a34a70e6b039fd5b99d1635f808a001cc4e563eadbd088
    -size 13487
    +oid sha256:690342da9774fdaaef6f8d681afbe50eb8e99158362c91d38e5c43dd89dc19d1
    +size 13734
    diff --git a/screenshots/86box/apps-resedit.png b/screenshots/86box/apps-resedit.png
    index d9dc6966..6778216b 100644
    --- a/screenshots/86box/apps-resedit.png
    +++ b/screenshots/86box/apps-resedit.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:28778a6fb805a693480e079199faf15f39fd4532949dba1805d119df374cdee4
    -size 13928
    +oid sha256:e27933594c4af7e9623583d9113f7ace247b8ae92eee8bfd602d1920eb266b26
    +size 14149
    diff --git a/screenshots/86box/apps-welcome.png b/screenshots/86box/apps-welcome.png
    index a70255fa..abd2ab8f 100644
    --- a/screenshots/86box/apps-welcome.png
    +++ b/screenshots/86box/apps-welcome.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a8fe6fa1e83f0ffb11cdbfbcdbce154e1f556d8d105928e2ea0d98b5e97f02c9
    -size 16993
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/86box/boot-boot.png b/screenshots/86box/boot-boot.png
    index 2c80adad..aaa46454 100644
    --- a/screenshots/86box/boot-boot.png
    +++ b/screenshots/86box/boot-boot.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3fb9ab5f33faf4c02ed15a9d1d1280f18df993e40bc1d4bb203aa0bb7b6d3250
    -size 18114
    +oid sha256:185883d27ca0d039b7f26acfadb1908a9c7aeb4a2cdd5216c9cca13478f2d4b5
    +size 18376
    diff --git a/screenshots/86box/exit-cancel.png b/screenshots/86box/exit-cancel.png
    index 4edc2699..05fb49db 100644
    --- a/screenshots/86box/exit-cancel.png
    +++ b/screenshots/86box/exit-cancel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:25313b11442d924c54b8633ce149834b039924c777c5ab5862331e023a3df916
    -size 14061
    +oid sha256:b1857f90c8a3b3df3504042146bda7ddafec400ff43afece3f4b55e7c1223e24
    +size 14330
    diff --git a/screenshots/86box/exit-savechg.png b/screenshots/86box/exit-savechg.png
    index 05a7b6b1..089b9519 100644
    --- a/screenshots/86box/exit-savechg.png
    +++ b/screenshots/86box/exit-savechg.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7fe8add6edaf1a1ebdb513c1aae10d381716f10402b8bed19db9b8233dee779f
    -size 14093
    +oid sha256:ceb4762d64848d9756af89143d495d13acbfd682f81f85d8941eb9b8450dbcb5
    +size 14324
    diff --git a/screenshots/86box/f1-h-dvxbas.png b/screenshots/86box/f1-h-dvxbas.png
    index 9059fbb9..194b3508 100644
    --- a/screenshots/86box/f1-h-dvxbas.png
    +++ b/screenshots/86box/f1-h-dvxbas.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b0857408e79b7367384ff69d8f4bb98b0299c094a271470ae83d26ee93c9c6a3
    -size 22399
    +oid sha256:7e79f42475e0e8527dcc0660ac8a33625f7d7ae01f47a6cd15ca7e7a49177b3c
    +size 22281
    diff --git a/screenshots/86box/f1-h-welcom.png b/screenshots/86box/f1-h-welcom.png
    index 924cad82..b772f1b0 100644
    --- a/screenshots/86box/f1-h-welcom.png
    +++ b/screenshots/86box/f1-h-welcom.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:4a579e1c58609c00b176fa6976aa77154fa9d025e978605483f1e1e0793e3d96
    -size 17185
    +oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    +size 18913
    diff --git a/screenshots/86box/firstrun-fr-open.png b/screenshots/86box/firstrun-fr-open.png
    index a70255fa..abd2ab8f 100644
    --- a/screenshots/86box/firstrun-fr-open.png
    +++ b/screenshots/86box/firstrun-fr-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a8fe6fa1e83f0ffb11cdbfbcdbce154e1f556d8d105928e2ea0d98b5e97f02c9
    -size 16993
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/86box/fullmode-fm-desk.png b/screenshots/86box/fullmode-fm-desk.png
    index 2c80adad..aaa46454 100644
    --- a/screenshots/86box/fullmode-fm-desk.png
    +++ b/screenshots/86box/fullmode-fm-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3fb9ab5f33faf4c02ed15a9d1d1280f18df993e40bc1d4bb203aa0bb7b6d3250
    -size 18114
    +oid sha256:185883d27ca0d039b7f26acfadb1908a9c7aeb4a2cdd5216c9cca13478f2d4b5
    +size 18376
    diff --git a/screenshots/86box/fullscr-fs-desk.png b/screenshots/86box/fullscr-fs-desk.png
    index c1006a4f..3e95b4f3 100644
    --- a/screenshots/86box/fullscr-fs-desk.png
    +++ b/screenshots/86box/fullscr-fs-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:4fbc9cc1f7ba27a108766d613ab4618ea02b446bf8c2e9cda3fc151bb57e0645
    -size 17353
    +oid sha256:b28bb041c7adbdf8f345ab1d0df79d1f44519ac65a8df42eaf53567cb15fefd6
    +size 17396
    diff --git a/screenshots/86box/hang-ended.png b/screenshots/86box/hang-ended.png
    index 961872bf..f5c8b114 100644
    --- a/screenshots/86box/hang-ended.png
    +++ b/screenshots/86box/hang-ended.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:cf4a5faa4fd6cc3686eda52e384972571582a170db641aad18d6c7bc87dad9ed
    -size 14938
    +oid sha256:c7a6244ed6897f52b30ebc88e44f09a4e246e02ec7ce5d041fa3dd15ba7a72b6
    +size 15159
    diff --git a/screenshots/86box/hang-taskmgr.png b/screenshots/86box/hang-taskmgr.png
    index b6caf9b6..68b484ca 100644
    --- a/screenshots/86box/hang-taskmgr.png
    +++ b/screenshots/86box/hang-taskmgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:196d00f8ddc2ae0e833eb0347de4b8426faa219b2fa952e9802452c1365ea60f
    -size 15935
    +oid sha256:f3bf75064c347afb77a293b7f3105d02ce1dbe48bf6ab4e380d4ca5defc59da0
    +size 16185
    diff --git a/screenshots/86box/install-in-desk.png b/screenshots/86box/install-in-desk.png
    index 3686b74f..7786d5eb 100644
    --- a/screenshots/86box/install-in-desk.png
    +++ b/screenshots/86box/install-in-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c9a079ee9e34ce6c7217d1fed22a0e937852655caec1e275d925deddc3001e38
    -size 18154
    +oid sha256:0c83a79e9526c36849107b879f81a18f1bc9216fee6bca539d99e45542f51bda
    +size 18409
    diff --git a/screenshots/86box/install-in-welc.png b/screenshots/86box/install-in-welc.png
    index 4516c8b0..abd2ab8f 100644
    --- a/screenshots/86box/install-in-welc.png
    +++ b/screenshots/86box/install-in-welc.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:21bb78b0da44f365e9918bc34a2b1a0425ae4ca96667959e8be3f4b754f547e5
    -size 17010
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/86box/install-su-copy.png b/screenshots/86box/install-su-copy.png
    index db2a349a..e41442e3 100644
    --- a/screenshots/86box/install-su-copy.png
    +++ b/screenshots/86box/install-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2ce985d712ed7704d64256f877117d038d196ff7ecdd53facaf70dcd45ae5c1d
    -size 11206
    +oid sha256:c5a3aedf87a9d95bfe86d681049462cd11c66bb82e634088bdcb0d907e4bea38
    +size 17127
    diff --git a/screenshots/86box/install-su-done.png b/screenshots/86box/install-su-done.png
    index ae6ab8cf..436cf1ca 100644
    --- a/screenshots/86box/install-su-done.png
    +++ b/screenshots/86box/install-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:949e00a5e0b0091f862a6649b69c3a6bc884e2abd0bdcbc46cb5f84668fa8952
    -size 12102
    +oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    +size 16310
    diff --git a/screenshots/86box/install-su-fold.png b/screenshots/86box/install-su-fold.png
    index 3f10248f..80af0974 100644
    --- a/screenshots/86box/install-su-fold.png
    +++ b/screenshots/86box/install-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:0c2de1fb480d7b272907fe5da333b01c8958ccf71421619c3eea483baba586b3
    -size 14287
    +oid sha256:02cefd9f80b7b54d90d5614c08c570b25a26979c2dea6e1c932058e7f5122434
    +size 16454
    diff --git a/screenshots/86box/install-su-lic.png b/screenshots/86box/install-su-lic.png
    index 72644a79..52dd988a 100644
    --- a/screenshots/86box/install-su-lic.png
    +++ b/screenshots/86box/install-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:62f1df23ec94c8e5da49f35fb68356282fdfe4a1999285940f0610161f085f28
    -size 16569
    +oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    +size 17327
    diff --git a/screenshots/86box/install-su-unin.png b/screenshots/86box/install-su-unin.png
    index 5a958b05..af3047f5 100644
    --- a/screenshots/86box/install-su-unin.png
    +++ b/screenshots/86box/install-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:017a9069fe14f6d1f7e61141206803f4c9ec248357db714984a21c60b9e04cd8
    -size 12775
    +oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    +size 14947
    diff --git a/screenshots/86box/lfn-fm-big.png b/screenshots/86box/lfn-fm-big.png
    new file mode 100644
    index 00000000..be44ddda
    --- /dev/null
    +++ b/screenshots/86box/lfn-fm-big.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:a8ab65c364ada48ea592dff0570c6bcdd99b656f2b576057159a146cda96efb0
    +size 15964
    diff --git a/screenshots/86box/lfn-fm-old.png b/screenshots/86box/lfn-fm-old.png
    new file mode 100644
    index 00000000..a2eb6197
    --- /dev/null
    +++ b/screenshots/86box/lfn-fm-old.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:872fc284c3c76c028fa6938444fbb8fd4b4906a81c2fba68bc9977f60be37958
    +size 15316
    diff --git a/screenshots/86box/lfn-lfnsmk.png b/screenshots/86box/lfn-lfnsmk.png
    new file mode 100644
    index 00000000..27ce819e
    --- /dev/null
    +++ b/screenshots/86box/lfn-lfnsmk.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:8c9500f652006d5b48f34837dcc059eb25d878fc6d174a04aa2d17d1c137319c
    +size 17034
    diff --git a/screenshots/86box/menus-m-design.png b/screenshots/86box/menus-m-design.png
    index fa583cea..c01e4407 100644
    --- a/screenshots/86box/menus-m-design.png
    +++ b/screenshots/86box/menus-m-design.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a709d362c56c7f2bf5eeb5ff8bb94e7d4f97afa716f77b80689699dc1eea884a
    -size 22955
    +oid sha256:8cdbffd36d02a2fd16b3e86d1bcb90c487f0440e30898ebddf21bb61b26861a1
    +size 23000
    diff --git a/screenshots/86box/menus-m-file.png b/screenshots/86box/menus-m-file.png
    index 4ae5ca1b..b63007f4 100644
    --- a/screenshots/86box/menus-m-file.png
    +++ b/screenshots/86box/menus-m-file.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9cd66fbf0ac698aad9cbe4952e077a30aa6ec605aac583c9040f9c27ac1eaf6c
    -size 14495
    +oid sha256:831607e84c478326d61e42c57f52e47c3d62f428aa562c40f540e1432e5b6558
    +size 14711
    diff --git a/screenshots/86box/menus-m-run.png b/screenshots/86box/menus-m-run.png
    index 6a356062..55a5d8d7 100644
    --- a/screenshots/86box/menus-m-run.png
    +++ b/screenshots/86box/menus-m-run.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:8be5088b690bee86fc8a6f7cc55bb2495a483a2136abb0e4beb98fafb2990027
    -size 18139
    +oid sha256:c240f7fa799ab61cfbadbfcf5f8b0ada8ca157aadf02aaff2767d13151b1b05f
    +size 18173
    diff --git a/screenshots/86box/net-netsmk.png b/screenshots/86box/net-netsmk.png
    index 6e1f7fe3..59a62096 100644
    --- a/screenshots/86box/net-netsmk.png
    +++ b/screenshots/86box/net-netsmk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:11c9d203bb212057315428b8ae4ceb5efb98916e9b1a5c5b080677a3205d7079
    -size 16603
    +oid sha256:eff11e9bc8ba606e1b1c9801d53ef0aaa03d47d593ffe5d3cb2e4b92e044cce3
    +size 16692
    diff --git a/screenshots/86box/pardsgn-pardsgn.png b/screenshots/86box/pardsgn-pardsgn.png
    index 0fe9a583..3bf1cbb8 100644
    --- a/screenshots/86box/pardsgn-pardsgn.png
    +++ b/screenshots/86box/pardsgn-pardsgn.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:617df56ce40e2d0f508309ad32f96cbc9fc02cb5090dabd859a85a7e1ad31801
    -size 14442
    +oid sha256:0a766622dda77ef8d68cfb3f210d74a7585494babecb44ce98c806181ce313a6
    +size 14458
    diff --git a/screenshots/86box/parhlp-parhlp-l.png b/screenshots/86box/parhlp-parhlp-l.png
    index 6ca03e6d..c2175854 100644
    --- a/screenshots/86box/parhlp-parhlp-l.png
    +++ b/screenshots/86box/parhlp-parhlp-l.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3d4173cdb0913c648867acf611a1229eff3afa52d28dc1c80ea5fcb72e59f742
    -size 15518
    +oid sha256:8b2a9b275ec04d5ebba95daea4824cab032a694206b650c9f1500c8f87df4804
    +size 15586
    diff --git a/screenshots/86box/parhlp-parhlp.png b/screenshots/86box/parhlp-parhlp.png
    index 2f8244a4..dbdd07b2 100644
    --- a/screenshots/86box/parhlp-parhlp.png
    +++ b/screenshots/86box/parhlp-parhlp.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:acd4399dea1bb296a676325375ff32a630303af9580d0480b54802f5e63de14b
    -size 15525
    +oid sha256:da365c8447dc60454576d3330595ce1768789514112fb20586e6fe37e6343f06
    +size 15584
    diff --git a/screenshots/86box/parpm-parpm.png b/screenshots/86box/parpm-parpm.png
    index 77c5a7a4..af27c7ba 100644
    --- a/screenshots/86box/parpm-parpm.png
    +++ b/screenshots/86box/parpm-parpm.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:d072e84afb4356c744d5fcc86b2e98887fcb4653f3b9df6119f3b6c5bfc9de1f
    -size 17113
    +oid sha256:7a633e667e01222db13c40b0c2a76508f0030e1929b6ff7a16603b7f5b45d0e7
    +size 17147
    diff --git a/screenshots/86box/print-print1.png b/screenshots/86box/print-print1.png
    index e3098a78..9bb5a700 100644
    --- a/screenshots/86box/print-print1.png
    +++ b/screenshots/86box/print-print1.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:fb54263c16cc70b375ac626c9247fd27a86a64a92ade1e58ff22957aae5a74a3
    -size 14629
    +oid sha256:b33259536eda180e9a524db68ed15cc0aafd7f7b79ebbe54dab04c7db3b5fe94
    +size 14848
    diff --git a/screenshots/86box/safevid-sv-desk.png b/screenshots/86box/safevid-sv-desk.png
    index 2c80adad..aaa46454 100644
    --- a/screenshots/86box/safevid-sv-desk.png
    +++ b/screenshots/86box/safevid-sv-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3fb9ab5f33faf4c02ed15a9d1d1280f18df993e40bc1d4bb203aa0bb7b6d3250
    -size 18114
    +oid sha256:185883d27ca0d039b7f26acfadb1908a9c7aeb4a2cdd5216c9cca13478f2d4b5
    +size 18376
    diff --git a/screenshots/86box/sdk-sdkapp.png b/screenshots/86box/sdk-sdkapp.png
    index 1bf24d20..f5a79ac7 100644
    --- a/screenshots/86box/sdk-sdkapp.png
    +++ b/screenshots/86box/sdk-sdkapp.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a09cc04f2b4986d96cc50055e0f3ffd45aa6c818d27073e118bf8682f336cec1
    -size 17537
    +oid sha256:d186d8870aa1a1e4f1dde4a0136b81b04ee020ce3aa2d2ea48d09e3aaf5b204c
    +size 17600
    diff --git a/screenshots/86box/sdk-sdkbas.png b/screenshots/86box/sdk-sdkbas.png
    index b2c7607b..abad0582 100644
    --- a/screenshots/86box/sdk-sdkbas.png
    +++ b/screenshots/86box/sdk-sdkbas.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:f09cf407074d97373d1c09ed0e5e993d5af408b94cd13024dbebb2b18e19327e
    -size 16684
    +oid sha256:605b7b625968172ad9230527203a6c2bb6d915bcbbac01f131943c5be2d4093c
    +size 16799
    diff --git a/screenshots/86box/sdk-sdkgreet.png b/screenshots/86box/sdk-sdkgreet.png
    index e9334dd5..996f44e0 100644
    --- a/screenshots/86box/sdk-sdkgreet.png
    +++ b/screenshots/86box/sdk-sdkgreet.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:51348c4493faf9068188201ed7cc5a13fad38e389b72047b901bf41646112660
    -size 16664
    +oid sha256:350a86a83d5be6c60adea783b58192b539e551ec5e1dee4c33df516633ad4b00
    +size 16735
    diff --git a/screenshots/86box/sdk-sdktbx.png b/screenshots/86box/sdk-sdktbx.png
    index f0b3c508..06535181 100644
    --- a/screenshots/86box/sdk-sdktbx.png
    +++ b/screenshots/86box/sdk-sdktbx.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7ab577b67f8f4e5c1bd7aa6fb8c5d18ea03e6fdd59bb2be766840e5de3694f79
    -size 22860
    +oid sha256:b636eef5ae0dc8fe5c132c2b7d37c94ccdfa536f72d70b83b6cc2c0fc647d59c
    +size 22905
    diff --git a/screenshots/86box/sdkgame-sdkgame.png b/screenshots/86box/sdkgame-sdkgame.png
    index 6cbacdc8..1674f772 100644
    --- a/screenshots/86box/sdkgame-sdkgame.png
    +++ b/screenshots/86box/sdkgame-sdkgame.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a4e53537620ca9ac3de5c3f51b0718c335b21cffb5e6c4f1ba5d8718472edc28
    -size 14672
    +oid sha256:0cc69633147d663f0529171d4ecbf484af883910f88afff826da778f590f4ecb
    +size 14704
    diff --git a/screenshots/86box/sql-sqlsmk.png b/screenshots/86box/sql-sqlsmk.png
    index b7d2447b..0676c825 100644
    --- a/screenshots/86box/sql-sqlsmk.png
    +++ b/screenshots/86box/sql-sqlsmk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:15b5eed9b2a13188df881082a116165a3e48c9a3e15b20554df93f1367306148
    -size 16778
    +oid sha256:cea4451053e9ebf9b93fccf296e2a0b86d1e362e27fae329439064fff501c40c
    +size 16848
    diff --git a/screenshots/86box/ucdemo-ucdemo.png b/screenshots/86box/ucdemo-ucdemo.png
    index a76309de..14b8ed15 100644
    --- a/screenshots/86box/ucdemo-ucdemo.png
    +++ b/screenshots/86box/ucdemo-ucdemo.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c9b6867da3f53651d7806e4d9a145537ce7e4eb144a019146b264b4da893fad3
    -size 15749
    +oid sha256:020814aa546db1f5f088dcf17211231362910941c89b186a13c5f2d295b8457a
    +size 15818
    diff --git a/screenshots/86box/ucide-ucide-ds.png b/screenshots/86box/ucide-ucide-ds.png
    index 920b9f2b..06915901 100644
    --- a/screenshots/86box/ucide-ucide-ds.png
    +++ b/screenshots/86box/ucide-ucide-ds.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5f58d53babe125be69f0ad2a26765586184956699f7805a67f76ad5322a7b8a2
    -size 31057
    +oid sha256:524ccf39bc12ab0172c480795b7053cd8dcf3aae9fea34320c9a679b815315f1
    +size 31120
    diff --git a/screenshots/86box/ucide-ucide-ru.png b/screenshots/86box/ucide-ucide-ru.png
    index d88ca3c8..7cbd6a05 100644
    --- a/screenshots/86box/ucide-ucide-ru.png
    +++ b/screenshots/86box/ucide-ucide-ru.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1008fa88b4001f9d03f56b8cd15054a9740aaaaf5843b12796bcb8741c96d87e
    -size 25186
    +oid sha256:b4cfc025cde4eeda10c5761b232db9636466eb42650495b7c0472283d300f9be
    +size 25237
    diff --git a/screenshots/86box/ucide-ucide-tb.png b/screenshots/86box/ucide-ucide-tb.png
    index 0520a246..94c5c616 100644
    --- a/screenshots/86box/ucide-ucide-tb.png
    +++ b/screenshots/86box/ucide-ucide-tb.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9dfd1bc33ff7d35f32a9a185d4e616d45eba84a484cba89d58784d918415e382
    -size 31539
    +oid sha256:e0847ddd48455ad7a16b279e86aa7faaacaaa94acc563ea635aacaef53ee330a
    +size 31611
    diff --git a/screenshots/86box/upgrade-su-copy.png b/screenshots/86box/upgrade-su-copy.png
    index db2a349a..e41442e3 100644
    --- a/screenshots/86box/upgrade-su-copy.png
    +++ b/screenshots/86box/upgrade-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2ce985d712ed7704d64256f877117d038d196ff7ecdd53facaf70dcd45ae5c1d
    -size 11206
    +oid sha256:c5a3aedf87a9d95bfe86d681049462cd11c66bb82e634088bdcb0d907e4bea38
    +size 17127
    diff --git a/screenshots/86box/upgrade-su-done.png b/screenshots/86box/upgrade-su-done.png
    index ae6ab8cf..436cf1ca 100644
    --- a/screenshots/86box/upgrade-su-done.png
    +++ b/screenshots/86box/upgrade-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:949e00a5e0b0091f862a6649b69c3a6bc884e2abd0bdcbc46cb5f84668fa8952
    -size 12102
    +oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    +size 16310
    diff --git a/screenshots/86box/upgrade-su-fold.png b/screenshots/86box/upgrade-su-fold.png
    index 3f10248f..80af0974 100644
    --- a/screenshots/86box/upgrade-su-fold.png
    +++ b/screenshots/86box/upgrade-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:0c2de1fb480d7b272907fe5da333b01c8958ccf71421619c3eea483baba586b3
    -size 14287
    +oid sha256:02cefd9f80b7b54d90d5614c08c570b25a26979c2dea6e1c932058e7f5122434
    +size 16454
    diff --git a/screenshots/86box/upgrade-su-lic.png b/screenshots/86box/upgrade-su-lic.png
    index 72644a79..52dd988a 100644
    --- a/screenshots/86box/upgrade-su-lic.png
    +++ b/screenshots/86box/upgrade-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:62f1df23ec94c8e5da49f35fb68356282fdfe4a1999285940f0610161f085f28
    -size 16569
    +oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    +size 17327
    diff --git a/screenshots/86box/upgrade-su-unin.png b/screenshots/86box/upgrade-su-unin.png
    index 5a958b05..af3047f5 100644
    --- a/screenshots/86box/upgrade-su-unin.png
    +++ b/screenshots/86box/upgrade-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:017a9069fe14f6d1f7e61141206803f4c9ec248357db714984a21c60b9e04cd8
    -size 12775
    +oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    +size 14947
    diff --git a/screenshots/86box/upgrade-su-upgr.png b/screenshots/86box/upgrade-su-upgr.png
    index dd918110..55bc5afa 100644
    --- a/screenshots/86box/upgrade-su-upgr.png
    +++ b/screenshots/86box/upgrade-su-upgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9f69a18ad4aaef9c09fe27f220d522f35d74b9c3fc059d0e7561e85bb412edd5
    -size 15726
    +oid sha256:1030284051814c5abadf8babc30a9d92671cf0c68bdf17d4287c8cf5a0c92363
    +size 16882
    diff --git a/screenshots/86box/welcome-wl-help.png b/screenshots/86box/welcome-wl-help.png
    index 924cad82..b772f1b0 100644
    --- a/screenshots/86box/welcome-wl-help.png
    +++ b/screenshots/86box/welcome-wl-help.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:4a579e1c58609c00b176fa6976aa77154fa9d025e978605483f1e1e0793e3d96
    -size 17185
    +oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    +size 18913
    diff --git a/screenshots/86box/welcome-wl-open.png b/screenshots/86box/welcome-wl-open.png
    index a70255fa..abd2ab8f 100644
    --- a/screenshots/86box/welcome-wl-open.png
    +++ b/screenshots/86box/welcome-wl-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a8fe6fa1e83f0ffb11cdbfbcdbce154e1f556d8d105928e2ea0d98b5e97f02c9
    -size 16993
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/86box/welcome-wl-tour.png b/screenshots/86box/welcome-wl-tour.png
    new file mode 100644
    index 00000000..a772093c
    --- /dev/null
    +++ b/screenshots/86box/welcome-wl-tour.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:7a0e03692d92fbc8b95a858c72e7fb83c169bf8f19b4c5462be5f4ed6e5dff08
    +size 18342
    diff --git a/screenshots/86box/welcome-wl-tour2.png b/screenshots/86box/welcome-wl-tour2.png
    new file mode 100644
    index 00000000..d9f17e84
    --- /dev/null
    +++ b/screenshots/86box/welcome-wl-tour2.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:c63e835f1d7da0b190771c96b32c7dd532e19e3dd741ac25218361ab0055720b
    +size 18588
    diff --git a/screenshots/86box/winops-w-back.png b/screenshots/86box/winops-w-back.png
    index 820bc4fa..be07800e 100644
    --- a/screenshots/86box/winops-w-back.png
    +++ b/screenshots/86box/winops-w-back.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9a36e95540dd59ebe01bb51e25ae7cb77e9765ecdc70abbfa6950b036909a32b
    -size 13123
    +oid sha256:7f11737da501c34b2ff60c4aa9793bbfb61d8249fa1963980baddafb039d39c6
    +size 13351
    diff --git a/screenshots/86box/winops-w-min.png b/screenshots/86box/winops-w-min.png
    index 82c93406..8bf711f6 100644
    --- a/screenshots/86box/winops-w-min.png
    +++ b/screenshots/86box/winops-w-min.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7bfe936308234f34eb1f10a72e293ec8d9cf5042c2c179137548dfe482f57550
    -size 18306
    +oid sha256:e8b0f888a347c99bc1e523143d2b7fd5fb9fbae945286739a8c4bc5f7dff33a2
    +size 18596
    diff --git a/screenshots/86box/winops-w-move.png b/screenshots/86box/winops-w-move.png
    index e5bc89ff..4a1084a3 100644
    --- a/screenshots/86box/winops-w-move.png
    +++ b/screenshots/86box/winops-w-move.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5d07a00428f214141a9864d8d32f698082e401dfbba3bfed8018581da7d046be
    -size 13509
    +oid sha256:71a82787f30ceb27660c9b6e699d63d49f9ec313ed51227de108635af5ee1e05
    +size 13511
    diff --git a/screenshots/86box/winops-w-rest.png b/screenshots/86box/winops-w-rest.png
    index 72640741..e7af769f 100644
    --- a/screenshots/86box/winops-w-rest.png
    +++ b/screenshots/86box/winops-w-rest.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:cdd53d1bcea90b8fad50d59395acda3ecadfeaa9368bbeeb9832d6c661099d25
    -size 13151
    +oid sha256:65dc513b0eb279c182bebb578475fb679a276264fc0f10ea4bf2ee11566f7689
    +size 13362
    diff --git a/screenshots/86box/winops-w-size.png b/screenshots/86box/winops-w-size.png
    index 82755206..6105bc85 100644
    --- a/screenshots/86box/winops-w-size.png
    +++ b/screenshots/86box/winops-w-size.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2c68d838091a19d19201169554468d842f3c23bf37d81ebf03cac4d03b70571c
    -size 13114
    +oid sha256:ddb01ff055fb3275275546a598f70d27f1db2ea4a88dcccbc0cb9256924a49e6
    +size 13319
    diff --git a/screenshots/dosbox/apps-clock.png b/screenshots/dosbox/apps-clock.png
    index b18164a3..5cb682bd 100644
    --- a/screenshots/dosbox/apps-clock.png
    +++ b/screenshots/dosbox/apps-clock.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:d8f9fac8056245b1120801df3a8f800566cfe97acef9de6cb2fc4f757d9cb3fb
    -size 17866
    +oid sha256:2d0c37c3077ab96d4908c6277bce8732919423b80a6e1c98ad1dfb7a7f178e80
    +size 17914
    diff --git a/screenshots/dosbox/apps-desktop.png b/screenshots/dosbox/apps-desktop.png
    index b4f19953..4f9610d8 100644
    --- a/screenshots/dosbox/apps-desktop.png
    +++ b/screenshots/dosbox/apps-desktop.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e7011560165479cdaa30d0e696262c97aa9c8ba906ef69a54ea07bb7cc4344ed
    -size 18200
    +oid sha256:5d14ced0f9793e53e27952b0ea5f69475f373a8c389838877ccb71ae8170818f
    +size 18508
    diff --git a/screenshots/dosbox/apps-dvxbasic.png b/screenshots/dosbox/apps-dvxbasic.png
    index 419a35df..adafd78e 100644
    --- a/screenshots/dosbox/apps-dvxbasic.png
    +++ b/screenshots/dosbox/apps-dvxbasic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:433a17e0cc1a0238b0a5b274bf63c42a438dd660fd985571ccf620b3dae58232
    -size 18285
    +oid sha256:ca33ce84d1585ab033f84a3e9063897633471477cd038439db44659931e86999
    +size 18318
    diff --git a/screenshots/dosbox/apps-dvxdemo.png b/screenshots/dosbox/apps-dvxdemo.png
    index e47d1ae9..7ad79473 100644
    --- a/screenshots/dosbox/apps-dvxdemo.png
    +++ b/screenshots/dosbox/apps-dvxdemo.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9b41ac0b8a6129a2328068e073c3b0a46e89a3a021af64e4750bc5f45ad5e56a
    -size 19666
    +oid sha256:66d4c23a76d2f48bf92f1200fdab5fd75a18f63c0d31f08c163226e270c0fa39
    +size 19684
    diff --git a/screenshots/dosbox/apps-filemgr.png b/screenshots/dosbox/apps-filemgr.png
    index da736fdc..906a0565 100644
    --- a/screenshots/dosbox/apps-filemgr.png
    +++ b/screenshots/dosbox/apps-filemgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2e38dab2c733d66df2fbeb2464467142c72e39f5c1e73fa7dc249a773975ba6d
    -size 20147
    +oid sha256:df3900eac84b6b6841c28be5b86c7e8bddda7a12941082c185cc4973064d0ee9
    +size 19772
    diff --git a/screenshots/dosbox/apps-iconed.png b/screenshots/dosbox/apps-iconed.png
    index 45b7c3c8..f305a142 100644
    --- a/screenshots/dosbox/apps-iconed.png
    +++ b/screenshots/dosbox/apps-iconed.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5390985238915e4e03de2d0c543d8f5ee60903fa2458f6a7bfeedd3efd4aee23
    -size 14825
    +oid sha256:a8ea0efb748f7f264cd034cdafbc33b5d35aa9ac0324be02d0daacf213aad97d
    +size 14855
    diff --git a/screenshots/dosbox/apps-imgview.png b/screenshots/dosbox/apps-imgview.png
    index 0d70c33f..a50ad693 100644
    --- a/screenshots/dosbox/apps-imgview.png
    +++ b/screenshots/dosbox/apps-imgview.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3db64140b452c26b75e386064439f8b7ad6c463ad73e0c11a21eb26afd11283e
    -size 12889
    +oid sha256:c92d11a6a8ad8e9194751f84f37f126d728ee4789e075a2b55f17053c0d0fee1
    +size 12908
    diff --git a/screenshots/dosbox/apps-notepad.png b/screenshots/dosbox/apps-notepad.png
    index 10005a02..06a90b66 100644
    --- a/screenshots/dosbox/apps-notepad.png
    +++ b/screenshots/dosbox/apps-notepad.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:39dddfe72eb5e3e00e614b174e6817f7a3f0d358b024a3e7c742f98645cd7aa4
    -size 13494
    +oid sha256:6ed706f905ee45aadee7723995d1bb203a9c49de2d6e675bd96de0e470f0309a
    +size 13741
    diff --git a/screenshots/dosbox/apps-resedit.png b/screenshots/dosbox/apps-resedit.png
    index d9dc6966..6778216b 100644
    --- a/screenshots/dosbox/apps-resedit.png
    +++ b/screenshots/dosbox/apps-resedit.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:28778a6fb805a693480e079199faf15f39fd4532949dba1805d119df374cdee4
    -size 13928
    +oid sha256:e27933594c4af7e9623583d9113f7ace247b8ae92eee8bfd602d1920eb266b26
    +size 14149
    diff --git a/screenshots/dosbox/apps-welcome.png b/screenshots/dosbox/apps-welcome.png
    index a70255fa..abd2ab8f 100644
    --- a/screenshots/dosbox/apps-welcome.png
    +++ b/screenshots/dosbox/apps-welcome.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a8fe6fa1e83f0ffb11cdbfbcdbce154e1f556d8d105928e2ea0d98b5e97f02c9
    -size 16993
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/dosbox/boot-boot.png b/screenshots/dosbox/boot-boot.png
    index 2c80adad..aaa46454 100644
    --- a/screenshots/dosbox/boot-boot.png
    +++ b/screenshots/dosbox/boot-boot.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3fb9ab5f33faf4c02ed15a9d1d1280f18df993e40bc1d4bb203aa0bb7b6d3250
    -size 18114
    +oid sha256:185883d27ca0d039b7f26acfadb1908a9c7aeb4a2cdd5216c9cca13478f2d4b5
    +size 18376
    diff --git a/screenshots/dosbox/exit-cancel.png b/screenshots/dosbox/exit-cancel.png
    index 4edc2699..420c8e4d 100644
    --- a/screenshots/dosbox/exit-cancel.png
    +++ b/screenshots/dosbox/exit-cancel.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:25313b11442d924c54b8633ce149834b039924c777c5ab5862331e023a3df916
    -size 14061
    +oid sha256:c20ee5dcc0270837cf6f2bbc99974afff411f6114670ec0f1b140f8f78bd447b
    +size 14337
    diff --git a/screenshots/dosbox/exit-savechg.png b/screenshots/dosbox/exit-savechg.png
    index 05a7b6b1..089b9519 100644
    --- a/screenshots/dosbox/exit-savechg.png
    +++ b/screenshots/dosbox/exit-savechg.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7fe8add6edaf1a1ebdb513c1aae10d381716f10402b8bed19db9b8233dee779f
    -size 14093
    +oid sha256:ceb4762d64848d9756af89143d495d13acbfd682f81f85d8941eb9b8450dbcb5
    +size 14324
    diff --git a/screenshots/dosbox/f1-h-dvxbas.png b/screenshots/dosbox/f1-h-dvxbas.png
    index 9059fbb9..194b3508 100644
    --- a/screenshots/dosbox/f1-h-dvxbas.png
    +++ b/screenshots/dosbox/f1-h-dvxbas.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b0857408e79b7367384ff69d8f4bb98b0299c094a271470ae83d26ee93c9c6a3
    -size 22399
    +oid sha256:7e79f42475e0e8527dcc0660ac8a33625f7d7ae01f47a6cd15ca7e7a49177b3c
    +size 22281
    diff --git a/screenshots/dosbox/f1-h-welcom.png b/screenshots/dosbox/f1-h-welcom.png
    index 924cad82..b772f1b0 100644
    --- a/screenshots/dosbox/f1-h-welcom.png
    +++ b/screenshots/dosbox/f1-h-welcom.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:4a579e1c58609c00b176fa6976aa77154fa9d025e978605483f1e1e0793e3d96
    -size 17185
    +oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    +size 18913
    diff --git a/screenshots/dosbox/firstrun-fr-open.png b/screenshots/dosbox/firstrun-fr-open.png
    index a70255fa..abd2ab8f 100644
    --- a/screenshots/dosbox/firstrun-fr-open.png
    +++ b/screenshots/dosbox/firstrun-fr-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a8fe6fa1e83f0ffb11cdbfbcdbce154e1f556d8d105928e2ea0d98b5e97f02c9
    -size 16993
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/dosbox/fullmode-fm-desk.png b/screenshots/dosbox/fullmode-fm-desk.png
    index 2c80adad..aaa46454 100644
    --- a/screenshots/dosbox/fullmode-fm-desk.png
    +++ b/screenshots/dosbox/fullmode-fm-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3fb9ab5f33faf4c02ed15a9d1d1280f18df993e40bc1d4bb203aa0bb7b6d3250
    -size 18114
    +oid sha256:185883d27ca0d039b7f26acfadb1908a9c7aeb4a2cdd5216c9cca13478f2d4b5
    +size 18376
    diff --git a/screenshots/dosbox/fullscr-fs-desk.png b/screenshots/dosbox/fullscr-fs-desk.png
    index c1006a4f..3e95b4f3 100644
    --- a/screenshots/dosbox/fullscr-fs-desk.png
    +++ b/screenshots/dosbox/fullscr-fs-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:4fbc9cc1f7ba27a108766d613ab4618ea02b446bf8c2e9cda3fc151bb57e0645
    -size 17353
    +oid sha256:b28bb041c7adbdf8f345ab1d0df79d1f44519ac65a8df42eaf53567cb15fefd6
    +size 17396
    diff --git a/screenshots/dosbox/hang-ended.png b/screenshots/dosbox/hang-ended.png
    index e77dc8dd..3ddffdbd 100644
    --- a/screenshots/dosbox/hang-ended.png
    +++ b/screenshots/dosbox/hang-ended.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:313f4a171758d088ece11f6092a4c679dcc7f29667c46d7a0d990442f3efd02b
    -size 14936
    +oid sha256:daccd7c5c6488ab2199a948873385f016271275e85f5b0724597802fbf131a5f
    +size 15143
    diff --git a/screenshots/dosbox/hang-taskmgr.png b/screenshots/dosbox/hang-taskmgr.png
    index 2143b040..d367bbdf 100644
    --- a/screenshots/dosbox/hang-taskmgr.png
    +++ b/screenshots/dosbox/hang-taskmgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:47243d3fb7da7f07a6a9c5afeb938baccb9e16a307a4740e40db57ea2bf0d850
    -size 15931
    +oid sha256:d5a7af537870dd735566f987649201d4f8e16e3b665b8a0345bd1a9de649d372
    +size 16184
    diff --git a/screenshots/dosbox/install-in-desk.png b/screenshots/dosbox/install-in-desk.png
    index 3686b74f..7786d5eb 100644
    --- a/screenshots/dosbox/install-in-desk.png
    +++ b/screenshots/dosbox/install-in-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c9a079ee9e34ce6c7217d1fed22a0e937852655caec1e275d925deddc3001e38
    -size 18154
    +oid sha256:0c83a79e9526c36849107b879f81a18f1bc9216fee6bca539d99e45542f51bda
    +size 18409
    diff --git a/screenshots/dosbox/install-in-welc.png b/screenshots/dosbox/install-in-welc.png
    index 4516c8b0..abd2ab8f 100644
    --- a/screenshots/dosbox/install-in-welc.png
    +++ b/screenshots/dosbox/install-in-welc.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:21bb78b0da44f365e9918bc34a2b1a0425ae4ca96667959e8be3f4b754f547e5
    -size 17010
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/dosbox/install-su-copy.png b/screenshots/dosbox/install-su-copy.png
    index ec552cea..274dd2c1 100644
    --- a/screenshots/dosbox/install-su-copy.png
    +++ b/screenshots/dosbox/install-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:28ead113ab376e2ff6c197beacf06d084b2b09842612b7f974b8093d93267cfe
    -size 11358
    +oid sha256:ce1735120c474fcaad191a6d2646b164d4b84e351e958e46f8ecfa2a39eeaaa0
    +size 17281
    diff --git a/screenshots/dosbox/install-su-disk.png b/screenshots/dosbox/install-su-disk.png
    index 906dfbf1..32b9b506 100644
    --- a/screenshots/dosbox/install-su-disk.png
    +++ b/screenshots/dosbox/install-su-disk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5f65ea8b760721abb6dfab3c121216ada3236df01edd2e5814e49d8ae25a2028
    -size 12917
    +oid sha256:7f3069f7935200e9973e5902029c604c4d8f3e8e2989d2f3e608c492901ca18a
    +size 17730
    diff --git a/screenshots/dosbox/install-su-done.png b/screenshots/dosbox/install-su-done.png
    index ae6ab8cf..436cf1ca 100644
    --- a/screenshots/dosbox/install-su-done.png
    +++ b/screenshots/dosbox/install-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:949e00a5e0b0091f862a6649b69c3a6bc884e2abd0bdcbc46cb5f84668fa8952
    -size 12102
    +oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    +size 16310
    diff --git a/screenshots/dosbox/install-su-fold.png b/screenshots/dosbox/install-su-fold.png
    index 3f10248f..80af0974 100644
    --- a/screenshots/dosbox/install-su-fold.png
    +++ b/screenshots/dosbox/install-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:0c2de1fb480d7b272907fe5da333b01c8958ccf71421619c3eea483baba586b3
    -size 14287
    +oid sha256:02cefd9f80b7b54d90d5614c08c570b25a26979c2dea6e1c932058e7f5122434
    +size 16454
    diff --git a/screenshots/dosbox/install-su-lic.png b/screenshots/dosbox/install-su-lic.png
    index 72644a79..52dd988a 100644
    --- a/screenshots/dosbox/install-su-lic.png
    +++ b/screenshots/dosbox/install-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:62f1df23ec94c8e5da49f35fb68356282fdfe4a1999285940f0610161f085f28
    -size 16569
    +oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    +size 17327
    diff --git a/screenshots/dosbox/install-su-unin.png b/screenshots/dosbox/install-su-unin.png
    index 5a958b05..af3047f5 100644
    --- a/screenshots/dosbox/install-su-unin.png
    +++ b/screenshots/dosbox/install-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:017a9069fe14f6d1f7e61141206803f4c9ec248357db714984a21c60b9e04cd8
    -size 12775
    +oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    +size 14947
    diff --git a/screenshots/dosbox/install-su-wrong.png b/screenshots/dosbox/install-su-wrong.png
    index d31b6033..6136ee81 100644
    --- a/screenshots/dosbox/install-su-wrong.png
    +++ b/screenshots/dosbox/install-su-wrong.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:8ec1d513bd8ef27a667134b2ba36a37a3bf201f518899cb1c320b4a6bb82f2ed
    -size 13856
    +oid sha256:c48be5e48bd4b5890fd0ccacbd137ceac9a22efc86cfcc2cfeaeab507673b142
    +size 16944
    diff --git a/screenshots/dosbox/menus-m-design.png b/screenshots/dosbox/menus-m-design.png
    index fa583cea..c01e4407 100644
    --- a/screenshots/dosbox/menus-m-design.png
    +++ b/screenshots/dosbox/menus-m-design.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a709d362c56c7f2bf5eeb5ff8bb94e7d4f97afa716f77b80689699dc1eea884a
    -size 22955
    +oid sha256:8cdbffd36d02a2fd16b3e86d1bcb90c487f0440e30898ebddf21bb61b26861a1
    +size 23000
    diff --git a/screenshots/dosbox/menus-m-file.png b/screenshots/dosbox/menus-m-file.png
    index 4ae5ca1b..b63007f4 100644
    --- a/screenshots/dosbox/menus-m-file.png
    +++ b/screenshots/dosbox/menus-m-file.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9cd66fbf0ac698aad9cbe4952e077a30aa6ec605aac583c9040f9c27ac1eaf6c
    -size 14495
    +oid sha256:831607e84c478326d61e42c57f52e47c3d62f428aa562c40f540e1432e5b6558
    +size 14711
    diff --git a/screenshots/dosbox/menus-m-run.png b/screenshots/dosbox/menus-m-run.png
    index 6a356062..55a5d8d7 100644
    --- a/screenshots/dosbox/menus-m-run.png
    +++ b/screenshots/dosbox/menus-m-run.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:8be5088b690bee86fc8a6f7cc55bb2495a483a2136abb0e4beb98fafb2990027
    -size 18139
    +oid sha256:c240f7fa799ab61cfbadbfcf5f8b0ada8ca157aadf02aaff2767d13151b1b05f
    +size 18173
    diff --git a/screenshots/dosbox/net-netsmk.png b/screenshots/dosbox/net-netsmk.png
    index 6e1f7fe3..59a62096 100644
    --- a/screenshots/dosbox/net-netsmk.png
    +++ b/screenshots/dosbox/net-netsmk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:11c9d203bb212057315428b8ae4ceb5efb98916e9b1a5c5b080677a3205d7079
    -size 16603
    +oid sha256:eff11e9bc8ba606e1b1c9801d53ef0aaa03d47d593ffe5d3cb2e4b92e044cce3
    +size 16692
    diff --git a/screenshots/dosbox/pardsgn-pardsgn.png b/screenshots/dosbox/pardsgn-pardsgn.png
    index 0fe9a583..3bf1cbb8 100644
    --- a/screenshots/dosbox/pardsgn-pardsgn.png
    +++ b/screenshots/dosbox/pardsgn-pardsgn.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:617df56ce40e2d0f508309ad32f96cbc9fc02cb5090dabd859a85a7e1ad31801
    -size 14442
    +oid sha256:0a766622dda77ef8d68cfb3f210d74a7585494babecb44ce98c806181ce313a6
    +size 14458
    diff --git a/screenshots/dosbox/parhlp-parhlp-l.png b/screenshots/dosbox/parhlp-parhlp-l.png
    index 6ca03e6d..c2175854 100644
    --- a/screenshots/dosbox/parhlp-parhlp-l.png
    +++ b/screenshots/dosbox/parhlp-parhlp-l.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3d4173cdb0913c648867acf611a1229eff3afa52d28dc1c80ea5fcb72e59f742
    -size 15518
    +oid sha256:8b2a9b275ec04d5ebba95daea4824cab032a694206b650c9f1500c8f87df4804
    +size 15586
    diff --git a/screenshots/dosbox/parhlp-parhlp.png b/screenshots/dosbox/parhlp-parhlp.png
    index 2f8244a4..dbdd07b2 100644
    --- a/screenshots/dosbox/parhlp-parhlp.png
    +++ b/screenshots/dosbox/parhlp-parhlp.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:acd4399dea1bb296a676325375ff32a630303af9580d0480b54802f5e63de14b
    -size 15525
    +oid sha256:da365c8447dc60454576d3330595ce1768789514112fb20586e6fe37e6343f06
    +size 15584
    diff --git a/screenshots/dosbox/parpm-parpm.png b/screenshots/dosbox/parpm-parpm.png
    index b109114d..2290e609 100644
    --- a/screenshots/dosbox/parpm-parpm.png
    +++ b/screenshots/dosbox/parpm-parpm.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5facaffedcc9ddbf130a757c62c146443dcefb21a495f36bb60e3ab7774f657a
    -size 17185
    +oid sha256:45deee7eaf5ba24f1f349dc86b57d4e160abae454943cb0b65bd1fef92295de1
    +size 17255
    diff --git a/screenshots/dosbox/print-print1.png b/screenshots/dosbox/print-print1.png
    index e3098a78..3c1345a3 100644
    --- a/screenshots/dosbox/print-print1.png
    +++ b/screenshots/dosbox/print-print1.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:fb54263c16cc70b375ac626c9247fd27a86a64a92ade1e58ff22957aae5a74a3
    -size 14629
    +oid sha256:89d20bb579b7b76cd591e20fb9ac81049cdb5ae32c563341eda8b7300fce3297
    +size 14853
    diff --git a/screenshots/dosbox/safevid-sv-desk.png b/screenshots/dosbox/safevid-sv-desk.png
    index 2c80adad..aaa46454 100644
    --- a/screenshots/dosbox/safevid-sv-desk.png
    +++ b/screenshots/dosbox/safevid-sv-desk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:3fb9ab5f33faf4c02ed15a9d1d1280f18df993e40bc1d4bb203aa0bb7b6d3250
    -size 18114
    +oid sha256:185883d27ca0d039b7f26acfadb1908a9c7aeb4a2cdd5216c9cca13478f2d4b5
    +size 18376
    diff --git a/screenshots/dosbox/sdk-sdkapp.png b/screenshots/dosbox/sdk-sdkapp.png
    index 1bf24d20..f5a79ac7 100644
    --- a/screenshots/dosbox/sdk-sdkapp.png
    +++ b/screenshots/dosbox/sdk-sdkapp.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a09cc04f2b4986d96cc50055e0f3ffd45aa6c818d27073e118bf8682f336cec1
    -size 17537
    +oid sha256:d186d8870aa1a1e4f1dde4a0136b81b04ee020ce3aa2d2ea48d09e3aaf5b204c
    +size 17600
    diff --git a/screenshots/dosbox/sdk-sdkbas.png b/screenshots/dosbox/sdk-sdkbas.png
    index b2c7607b..abad0582 100644
    --- a/screenshots/dosbox/sdk-sdkbas.png
    +++ b/screenshots/dosbox/sdk-sdkbas.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:f09cf407074d97373d1c09ed0e5e993d5af408b94cd13024dbebb2b18e19327e
    -size 16684
    +oid sha256:605b7b625968172ad9230527203a6c2bb6d915bcbbac01f131943c5be2d4093c
    +size 16799
    diff --git a/screenshots/dosbox/sdk-sdkgreet.png b/screenshots/dosbox/sdk-sdkgreet.png
    index e9334dd5..996f44e0 100644
    --- a/screenshots/dosbox/sdk-sdkgreet.png
    +++ b/screenshots/dosbox/sdk-sdkgreet.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:51348c4493faf9068188201ed7cc5a13fad38e389b72047b901bf41646112660
    -size 16664
    +oid sha256:350a86a83d5be6c60adea783b58192b539e551ec5e1dee4c33df516633ad4b00
    +size 16735
    diff --git a/screenshots/dosbox/sdk-sdktbx.png b/screenshots/dosbox/sdk-sdktbx.png
    index f0b3c508..06535181 100644
    --- a/screenshots/dosbox/sdk-sdktbx.png
    +++ b/screenshots/dosbox/sdk-sdktbx.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7ab577b67f8f4e5c1bd7aa6fb8c5d18ea03e6fdd59bb2be766840e5de3694f79
    -size 22860
    +oid sha256:b636eef5ae0dc8fe5c132c2b7d37c94ccdfa536f72d70b83b6cc2c0fc647d59c
    +size 22905
    diff --git a/screenshots/dosbox/sql-sqlsmk.png b/screenshots/dosbox/sql-sqlsmk.png
    index b7d2447b..0676c825 100644
    --- a/screenshots/dosbox/sql-sqlsmk.png
    +++ b/screenshots/dosbox/sql-sqlsmk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:15b5eed9b2a13188df881082a116165a3e48c9a3e15b20554df93f1367306148
    -size 16778
    +oid sha256:cea4451053e9ebf9b93fccf296e2a0b86d1e362e27fae329439064fff501c40c
    +size 16848
    diff --git a/screenshots/dosbox/ucdemo-ucdemo.png b/screenshots/dosbox/ucdemo-ucdemo.png
    index a76309de..14b8ed15 100644
    --- a/screenshots/dosbox/ucdemo-ucdemo.png
    +++ b/screenshots/dosbox/ucdemo-ucdemo.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c9b6867da3f53651d7806e4d9a145537ce7e4eb144a019146b264b4da893fad3
    -size 15749
    +oid sha256:020814aa546db1f5f088dcf17211231362910941c89b186a13c5f2d295b8457a
    +size 15818
    diff --git a/screenshots/dosbox/ucide-ucide-ds.png b/screenshots/dosbox/ucide-ucide-ds.png
    index 920b9f2b..06915901 100644
    --- a/screenshots/dosbox/ucide-ucide-ds.png
    +++ b/screenshots/dosbox/ucide-ucide-ds.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5f58d53babe125be69f0ad2a26765586184956699f7805a67f76ad5322a7b8a2
    -size 31057
    +oid sha256:524ccf39bc12ab0172c480795b7053cd8dcf3aae9fea34320c9a679b815315f1
    +size 31120
    diff --git a/screenshots/dosbox/ucide-ucide-ru.png b/screenshots/dosbox/ucide-ucide-ru.png
    index d88ca3c8..7cbd6a05 100644
    --- a/screenshots/dosbox/ucide-ucide-ru.png
    +++ b/screenshots/dosbox/ucide-ucide-ru.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:1008fa88b4001f9d03f56b8cd15054a9740aaaaf5843b12796bcb8741c96d87e
    -size 25186
    +oid sha256:b4cfc025cde4eeda10c5761b232db9636466eb42650495b7c0472283d300f9be
    +size 25237
    diff --git a/screenshots/dosbox/ucide-ucide-tb.png b/screenshots/dosbox/ucide-ucide-tb.png
    index 0520a246..94c5c616 100644
    --- a/screenshots/dosbox/ucide-ucide-tb.png
    +++ b/screenshots/dosbox/ucide-ucide-tb.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9dfd1bc33ff7d35f32a9a185d4e616d45eba84a484cba89d58784d918415e382
    -size 31539
    +oid sha256:e0847ddd48455ad7a16b279e86aa7faaacaaa94acc563ea635aacaef53ee330a
    +size 31611
    diff --git a/screenshots/dosbox/upgrade-su-copy.png b/screenshots/dosbox/upgrade-su-copy.png
    index db2a349a..e41442e3 100644
    --- a/screenshots/dosbox/upgrade-su-copy.png
    +++ b/screenshots/dosbox/upgrade-su-copy.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2ce985d712ed7704d64256f877117d038d196ff7ecdd53facaf70dcd45ae5c1d
    -size 11206
    +oid sha256:c5a3aedf87a9d95bfe86d681049462cd11c66bb82e634088bdcb0d907e4bea38
    +size 17127
    diff --git a/screenshots/dosbox/upgrade-su-done.png b/screenshots/dosbox/upgrade-su-done.png
    index ae6ab8cf..436cf1ca 100644
    --- a/screenshots/dosbox/upgrade-su-done.png
    +++ b/screenshots/dosbox/upgrade-su-done.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:949e00a5e0b0091f862a6649b69c3a6bc884e2abd0bdcbc46cb5f84668fa8952
    -size 12102
    +oid sha256:82de2cdff532a7be265cf8ca9fb39d212677b393b64fbb191cdcd5674584c077
    +size 16310
    diff --git a/screenshots/dosbox/upgrade-su-fold.png b/screenshots/dosbox/upgrade-su-fold.png
    index 3f10248f..0db61b4a 100644
    --- a/screenshots/dosbox/upgrade-su-fold.png
    +++ b/screenshots/dosbox/upgrade-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:0c2de1fb480d7b272907fe5da333b01c8958ccf71421619c3eea483baba586b3
    -size 14287
    +oid sha256:a4a14649ab3066c4108c685dc6bb8b78b5be9f5c507eb18c3099c18311baaff3
    +size 16474
    diff --git a/screenshots/dosbox/upgrade-su-lic.png b/screenshots/dosbox/upgrade-su-lic.png
    index 72644a79..52dd988a 100644
    --- a/screenshots/dosbox/upgrade-su-lic.png
    +++ b/screenshots/dosbox/upgrade-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:62f1df23ec94c8e5da49f35fb68356282fdfe4a1999285940f0610161f085f28
    -size 16569
    +oid sha256:6082149d59c1fd4a4fa2b5333522434d2703ee0e29f653a66208d747301b5f74
    +size 17327
    diff --git a/screenshots/dosbox/upgrade-su-unin.png b/screenshots/dosbox/upgrade-su-unin.png
    index 5a958b05..af3047f5 100644
    --- a/screenshots/dosbox/upgrade-su-unin.png
    +++ b/screenshots/dosbox/upgrade-su-unin.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:017a9069fe14f6d1f7e61141206803f4c9ec248357db714984a21c60b9e04cd8
    -size 12775
    +oid sha256:351484aaaa1aa48ab8f208d235677a2ccefeaaffb9ee4a12646e49e2157ccb3d
    +size 14947
    diff --git a/screenshots/dosbox/upgrade-su-upgr.png b/screenshots/dosbox/upgrade-su-upgr.png
    index dd918110..55bc5afa 100644
    --- a/screenshots/dosbox/upgrade-su-upgr.png
    +++ b/screenshots/dosbox/upgrade-su-upgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:9f69a18ad4aaef9c09fe27f220d522f35d74b9c3fc059d0e7561e85bb412edd5
    -size 15726
    +oid sha256:1030284051814c5abadf8babc30a9d92671cf0c68bdf17d4287c8cf5a0c92363
    +size 16882
    diff --git a/screenshots/dosbox/welcome-wl-help.png b/screenshots/dosbox/welcome-wl-help.png
    index 924cad82..b772f1b0 100644
    --- a/screenshots/dosbox/welcome-wl-help.png
    +++ b/screenshots/dosbox/welcome-wl-help.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:4a579e1c58609c00b176fa6976aa77154fa9d025e978605483f1e1e0793e3d96
    -size 17185
    +oid sha256:242657f05cc4b6fc531ecef24f6e2639e169675371f282af275973cc9ebdb38e
    +size 18913
    diff --git a/screenshots/dosbox/welcome-wl-open.png b/screenshots/dosbox/welcome-wl-open.png
    index a70255fa..abd2ab8f 100644
    --- a/screenshots/dosbox/welcome-wl-open.png
    +++ b/screenshots/dosbox/welcome-wl-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:a8fe6fa1e83f0ffb11cdbfbcdbce154e1f556d8d105928e2ea0d98b5e97f02c9
    -size 16993
    +oid sha256:6ad65605b7b0d10a2fa4ce1e59a6255b1667dfef8a53fd1be0efb7e7a1f3957f
    +size 19200
    diff --git a/screenshots/dosbox/welcome-wl-tour.png b/screenshots/dosbox/welcome-wl-tour.png
    new file mode 100644
    index 00000000..a772093c
    --- /dev/null
    +++ b/screenshots/dosbox/welcome-wl-tour.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:7a0e03692d92fbc8b95a858c72e7fb83c169bf8f19b4c5462be5f4ed6e5dff08
    +size 18342
    diff --git a/screenshots/dosbox/welcome-wl-tour2.png b/screenshots/dosbox/welcome-wl-tour2.png
    new file mode 100644
    index 00000000..d9f17e84
    --- /dev/null
    +++ b/screenshots/dosbox/welcome-wl-tour2.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:c63e835f1d7da0b190771c96b32c7dd532e19e3dd741ac25218361ab0055720b
    +size 18588
    diff --git a/screenshots/dosbox/winops-w-back.png b/screenshots/dosbox/winops-w-back.png
    index e6310fb2..be07800e 100644
    --- a/screenshots/dosbox/winops-w-back.png
    +++ b/screenshots/dosbox/winops-w-back.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:ebf43b76464abdbc0191ebbe3e5e8bff3ad4c6c4e8c37b0ed81a324723428c30
    -size 13133
    +oid sha256:7f11737da501c34b2ff60c4aa9793bbfb61d8249fa1963980baddafb039d39c6
    +size 13351
    diff --git a/screenshots/dosbox/winops-w-min.png b/screenshots/dosbox/winops-w-min.png
    index 82c93406..8bf711f6 100644
    --- a/screenshots/dosbox/winops-w-min.png
    +++ b/screenshots/dosbox/winops-w-min.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:7bfe936308234f34eb1f10a72e293ec8d9cf5042c2c179137548dfe482f57550
    -size 18306
    +oid sha256:e8b0f888a347c99bc1e523143d2b7fd5fb9fbae945286739a8c4bc5f7dff33a2
    +size 18596
    diff --git a/screenshots/dosbox/winops-w-move.png b/screenshots/dosbox/winops-w-move.png
    index 2dcda5f8..4a1084a3 100644
    --- a/screenshots/dosbox/winops-w-move.png
    +++ b/screenshots/dosbox/winops-w-move.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:5a3947794d267ce12c16c6e03734243568a626b3f46f7b15d76a3ded9fed447d
    -size 13517
    +oid sha256:71a82787f30ceb27660c9b6e699d63d49f9ec313ed51227de108635af5ee1e05
    +size 13511
    diff --git a/screenshots/dosbox/winops-w-rest.png b/screenshots/dosbox/winops-w-rest.png
    index eaa0d6eb..e7af769f 100644
    --- a/screenshots/dosbox/winops-w-rest.png
    +++ b/screenshots/dosbox/winops-w-rest.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:6e738f975817d0ccae5dc082ba4de5eabcac62e1b9fea0aaf99cbe1f476cdabf
    -size 13143
    +oid sha256:65dc513b0eb279c182bebb578475fb679a276264fc0f10ea4bf2ee11566f7689
    +size 13362
    diff --git a/screenshots/dosbox/winops-w-size.png b/screenshots/dosbox/winops-w-size.png
    index 82755206..6105bc85 100644
    --- a/screenshots/dosbox/winops-w-size.png
    +++ b/screenshots/dosbox/winops-w-size.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2c68d838091a19d19201169554468d842f3c23bf37d81ebf03cac4d03b70571c
    -size 13114
    +oid sha256:ddb01ff055fb3275275546a598f70d27f1db2ea4a88dcccbc0cb9256924a49e6
    +size 13319
    diff --git a/screenshots/masks/apps-filemgr.mask b/screenshots/masks/apps-filemgr.mask
    index 3fdb62c6..1d7e7faa 100644
    --- a/screenshots/masks/apps-filemgr.mask
    +++ b/screenshots/masks/apps-filemgr.mask
    @@ -1,4 +1,4 @@
     # File sizes and dates of the build output, and the free space
    -340 168 92 230
    -514 168 114 230
    +340 168 112 230
    +530 168 98 230
     450 400 178 18
    diff --git a/screenshots/masks/apps-welcome.mask b/screenshots/masks/apps-welcome.mask
    new file mode 100644
    index 00000000..c44d1628
    --- /dev/null
    +++ b/screenshots/masks/apps-welcome.mask
    @@ -0,0 +1,2 @@
    +# The Welcome tip (a timer rotates it every few seconds)
    +44 393 572 16
    diff --git a/screenshots/masks/firstrun-fr-open.mask b/screenshots/masks/firstrun-fr-open.mask
    new file mode 100644
    index 00000000..c44d1628
    --- /dev/null
    +++ b/screenshots/masks/firstrun-fr-open.mask
    @@ -0,0 +1,2 @@
    +# The Welcome tip (a timer rotates it every few seconds)
    +44 393 572 16
    diff --git a/screenshots/masks/install-in-welc.mask b/screenshots/masks/install-in-welc.mask
    new file mode 100644
    index 00000000..c44d1628
    --- /dev/null
    +++ b/screenshots/masks/install-in-welc.mask
    @@ -0,0 +1,2 @@
    +# The Welcome tip (a timer rotates it every few seconds)
    +44 393 572 16
    diff --git a/screenshots/masks/install-su-disk.mask b/screenshots/masks/install-su-disk.mask
    index 98bb14cc..5494df0c 100644
    --- a/screenshots/masks/install-su-disk.mask
    +++ b/screenshots/masks/install-su-disk.mask
    @@ -1,2 +1,4 @@
    -# Status line and progress bar behind the prompt: the file being unpacked when the part ran out and how far it got (both change with the package)
    -72 106 496 48
    +# Status line, progress bar and billboard around the prompt: the file being unpacked when the part ran out, how far it got (both change with the package) and the board showing then (they rotate on a clock); left of, right of and under the prompt
    +72 186 82 158
    +486 186 82 158
    +154 304 332 40
    diff --git a/screenshots/masks/install-su-fold.mask b/screenshots/masks/install-su-fold.mask
    index cc2f0e61..9097904a 100644
    --- a/screenshots/masks/install-su-fold.mask
    +++ b/screenshots/masks/install-su-fold.mask
    @@ -1,4 +1,4 @@
     # Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    -202 83 176 16
    +202 163 176 16
     # Blinking text caret after C:\DVX in the folder box
    -123 131 3 18
    +123 211 3 18
    diff --git a/screenshots/masks/install-su-wrong.mask b/screenshots/masks/install-su-wrong.mask
    index 98bb14cc..52f04c1c 100644
    --- a/screenshots/masks/install-su-wrong.mask
    +++ b/screenshots/masks/install-su-wrong.mask
    @@ -1,2 +1,4 @@
    -# Status line and progress bar behind the prompt: the file being unpacked when the part ran out and how far it got (both change with the package)
    -72 106 496 48
    +# Status line, progress bar and billboard around the prompt: the file being unpacked when the part ran out, how far it got (both change with the package) and the board showing then (they rotate on a clock); left of, right of and under the prompt
    +72 186 82 158
    +486 186 82 158
    +154 320 332 24
    diff --git a/screenshots/masks/lfn-fm-big.mask b/screenshots/masks/lfn-fm-big.mask
    new file mode 100644
    index 00000000..8df8ba78
    --- /dev/null
    +++ b/screenshots/masks/lfn-fm-big.mask
    @@ -0,0 +1,3 @@
    +# File dates (the disk image is made at each run); sizes and free space
    +# stay checked
    +530 168 98 230
    diff --git a/screenshots/masks/lfn-fm-old.mask b/screenshots/masks/lfn-fm-old.mask
    new file mode 100644
    index 00000000..8df8ba78
    --- /dev/null
    +++ b/screenshots/masks/lfn-fm-old.mask
    @@ -0,0 +1,3 @@
    +# File dates (the disk image is made at each run); sizes and free space
    +# stay checked
    +530 168 98 230
    diff --git a/screenshots/masks/upgrade-su-fold.mask b/screenshots/masks/upgrade-su-fold.mask
    index cc2f0e61..9097904a 100644
    --- a/screenshots/masks/upgrade-su-fold.mask
    +++ b/screenshots/masks/upgrade-su-fold.mask
    @@ -1,4 +1,4 @@
     # Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    -202 83 176 16
    +202 163 176 16
     # Blinking text caret after C:\DVX in the folder box
    -123 131 3 18
    +123 211 3 18
    diff --git a/screenshots/masks/upgrade-su-upgr.mask b/screenshots/masks/upgrade-su-upgr.mask
    deleted file mode 100644
    index 6978924d..00000000
    --- a/screenshots/masks/upgrade-su-upgr.mask
    +++ /dev/null
    @@ -1,2 +0,0 @@
    -# Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    -202 83 176 16
    diff --git a/screenshots/masks/welcome-wl-open.mask b/screenshots/masks/welcome-wl-open.mask
    new file mode 100644
    index 00000000..c44d1628
    --- /dev/null
    +++ b/screenshots/masks/welcome-wl-open.mask
    @@ -0,0 +1,2 @@
    +# The Welcome tip (a timer rotates it every few seconds)
    +44 393 572 16
    diff --git a/screenshots/masks/welcome-wl-tour.mask b/screenshots/masks/welcome-wl-tour.mask
    new file mode 100644
    index 00000000..c44d1628
    --- /dev/null
    +++ b/screenshots/masks/welcome-wl-tour.mask
    @@ -0,0 +1,2 @@
    +# The Welcome tip (a timer rotates it every few seconds)
    +44 393 572 16
    diff --git a/screenshots/masks/welcome-wl-tour2.mask b/screenshots/masks/welcome-wl-tour2.mask
    new file mode 100644
    index 00000000..c44d1628
    --- /dev/null
    +++ b/screenshots/masks/welcome-wl-tour2.mask
    @@ -0,0 +1,2 @@
    +# The Welcome tip (a timer rotates it every few seconds)
    +44 393 572 16
    diff --git a/src/apps/kpunch/dvxbasic/basdev/basProject.c b/src/apps/kpunch/dvxbasic/basdev/basProject.c
    index 34c47172..e433ee06 100644
    --- a/src/apps/kpunch/dvxbasic/basdev/basProject.c
    +++ b/src/apps/kpunch/dvxbasic/basdev/basProject.c
    @@ -909,7 +909,7 @@ bool prjRenameFile(PrjStateT *prj, int32_t idx, const char *newName, char *err,
             return false;
         }
     
    -    const char *invalid = platformValidateFilename(name);
    +    const char *invalid = platformValidateFilename(prj->projectDir, name);
     
         if (invalid) {
             snprintf(err, errSize, "%s", invalid);
    diff --git a/src/apps/kpunch/dvxbasic/basdev/basProject.h b/src/apps/kpunch/dvxbasic/basdev/basProject.h
    index 592f1308..6a4a0fca 100644
    --- a/src/apps/kpunch/dvxbasic/basdev/basProject.h
    +++ b/src/apps/kpunch/dvxbasic/basdev/basProject.h
    @@ -90,7 +90,7 @@ typedef enum {
     typedef struct {
         // Fields are ordered by alignment to minimize struct padding.
         char    *buffer;             // in-memory edit buffer (malloc'd, NULL = not loaded)
    -    char     path[DVX_MAX_PATH]; // relative path (8.3 DOS name)
    +    char     path[DVX_MAX_PATH]; // relative path (a name the project folder's disk takes: 8.3 without long names)
         char     formName[BAS_MAX_IDENT]; // form object name (from "Begin Form ")
         uint8_t  kind;               // PrjFileKindE
         bool     modified;           // true = buffer has unsaved changes
    diff --git a/src/apps/kpunch/dvxbasic/basrt.dhs b/src/apps/kpunch/dvxbasic/basrt.dhs
    index 62b94bbb..01b12da1 100644
    --- a/src/apps/kpunch/dvxbasic/basrt.dhs
    +++ b/src/apps/kpunch/dvxbasic/basrt.dhs
    @@ -273,6 +273,7 @@ Header: apps/dvxbasic/runtime/vm.h
       BAS_VM_CALL_STACK_SIZE  64      Maximum call nesting depth.
       BAS_VM_MAX_GLOBALS      512     Global variable slots.
       BAS_VM_MAX_LOCALS       64      Local variables per stack frame.
    +  BAS_VM_MAX_MEMBER_VARS  65536   Member variables per class object; variables per form (their opcodes' 16-bit index).
       BAS_VM_MAX_FOR_DEPTH    32      Maximum nested FOR loop depth.
       BAS_VM_MAX_FILES        16      Open file channels (1-based).
     .endtable
    diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
    index eff2d6b7..3e048704 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h
    +++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
    @@ -251,6 +251,13 @@ typedef struct {
     #define BAS_ROP_TABLE_SHIFT    16          // the truth table's place in an opcode
     #define BAS_ROP_TABLE_MASK     0xFF
     
    +// ScrollBarsNeeded's answer (VB's ScrollBar constants): the bars a
    +// scrolled area needs, a bit for each.
    +#define BAS_SB_NONE            0   // vbSBNone
    +#define BAS_SB_HORIZONTAL      1   // vbHorizontal
    +#define BAS_SB_VERTICAL        2   // vbVertical
    +#define BAS_SB_BOTH            (BAS_SB_HORIZONTAL | BAS_SB_VERTICAL)  // vbBoth
    +
     // Picture.Type: a LoadPicture object is a bitmap, LoadPicture() none.
     #define BAS_PIC_TYPE_NONE      0   // vbPicTypeNone
     #define BAS_PIC_TYPE_BITMAP    1   // vbPicTypeBitmap
    diff --git a/src/apps/kpunch/dvxbasic/compiler/compiler.c b/src/apps/kpunch/dvxbasic/compiler/compiler.c
    index e8dfa18a..938d0d85 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/compiler.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/compiler.c
    @@ -223,6 +223,12 @@ static const PredefConstT sPredefConsts[] = {
         { "dvxArrowDown",         ScrollArrowDownE },
         { "dvxArrowLeft",         ScrollArrowLeftE },
         { "dvxArrowRight",        ScrollArrowRightE },
    +
    +    // ScrollBarsNeeded's answer
    +    { "vbSBNone",             BAS_SB_NONE },
    +    { "vbHorizontal",         BAS_SB_HORIZONTAL },
    +    { "vbVertical",           BAS_SB_VERTICAL },
    +    { "vbBoth",               BAS_SB_BOTH },
         { "vbLPSmall",            BAS_LP_SMALL },
         { "vbLPLarge",            BAS_LP_LARGE },
         { "vbLPSmallShell",       BAS_LP_SMALL_SHELL },
    diff --git a/src/apps/kpunch/dvxbasic/compiler/resolve.c b/src/apps/kpunch/dvxbasic/compiler/resolve.c
    index 113d0169..8afab593 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/resolve.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/resolve.c
    @@ -2658,6 +2658,12 @@ static void resolveDeclStatement(BasCompilerT *c, BasNodeT *n) {
                     return;
                 }
     
    +            // Form variables are numbered by 16-bit operands
    +            if (c->nextFormSlot > BAS_VM_MAX_MEMBER_VARS) {
    +                basErrorNode(c, n, "Too many form-level variables (%d, limit is %d)", (int)c->nextFormSlot, (int)BAS_VM_MAX_MEMBER_VARS);
    +                return;
    +            }
    +
                 s->ival = c->nextFormSlot;
                 break;
     
    @@ -2797,6 +2803,11 @@ static void resolveEndClass(BasCompilerT *c, BasStmtT *s) {
         cls->terminateSym = classEventProc(c, BAS_CLASS_TERMINATE, &s->h);
         s->sym            = init;
         s->ival           = c->nextMemberSlot;
    +
    +    // Member variables are numbered by 16-bit operands
    +    if (c->nextMemberSlot > BAS_VM_MAX_MEMBER_VARS) {
    +        basErrorNode(c, &s->h, "Too many member variables in %s (%d, limit is %d)", cls->name, (int)c->nextMemberSlot, (int)BAS_VM_MAX_MEMBER_VARS);
    +    }
     }
     
     
    diff --git a/src/apps/kpunch/dvxbasic/compiler/typecheck.c b/src/apps/kpunch/dvxbasic/compiler/typecheck.c
    index 3c759913..1076d40e 100644
    --- a/src/apps/kpunch/dvxbasic/compiler/typecheck.c
    +++ b/src/apps/kpunch/dvxbasic/compiler/typecheck.c
    @@ -133,7 +133,12 @@ uint8_t basStaticType(const BasNodeT *n) {
                 const BasOpT *op = (const BasOpT *)n;
                 uint8_t       t  = basStaticType(op->a);
     
    -            // -LONG is a LONG (or an Overflow); -INTEGER may widen
    +            // -LONG is a LONG (or an Overflow); -INTEGER may widen; NOT
    +            // of a Boolean is a Boolean
    +            if (op->op == TOK_NOT && t == BAS_TYPE_BOOLEAN) {
    +                return t;
    +            }
    +
                 return (op->op == TOK_MINUS && (t == BAS_TYPE_LONG || t == BAS_TYPE_DOUBLE)) ? t : BAS_NODE_TYPE_UNKNOWN;
             }
     
    @@ -154,8 +159,9 @@ void basTypeUnit(BasCompilerT *c, BasNodeT *unit) {
     // LONG / BYTE give a LONG (or an Overflow); + - * on a CURRENCY and a
     // CURRENCY or whole number a CURRENCY (or an Overflow), on other numbers
     // with a SINGLE / DOUBLE a DOUBLE; / and ^ on numbers a DOUBLE; & and + of
    -// two strings a string; a comparison of known values a Boolean (no Null).
    -// Two INTEGERs or BYTEs may widen, so their result type is not known.
    +// two strings a string; a comparison of known values a Boolean (no Null),
    +// as is AND / OR / XOR / EQV / IMP of two Booleans.  Two INTEGERs or BYTEs
    +// may widen, so their result type is not known.
     static uint8_t binaryType(uint8_t tok, uint8_t a, uint8_t b) {
         bool ints     = wholeType(a) && wholeType(b);
         bool numbers  = numberType(a) && numberType(b);
    @@ -195,6 +201,13 @@ static uint8_t binaryType(uint8_t tok, uint8_t a, uint8_t b) {
             case TOK_GE:
                 return (a != BAS_NODE_TYPE_UNKNOWN && b != BAS_NODE_TYPE_UNKNOWN) ? BAS_TYPE_BOOLEAN : BAS_NODE_TYPE_UNKNOWN;
     
    +        case TOK_AND:
    +        case TOK_OR:
    +        case TOK_XOR:
    +        case TOK_EQV:
    +        case TOK_IMP:
    +            return (a == BAS_TYPE_BOOLEAN && b == BAS_TYPE_BOOLEAN) ? BAS_TYPE_BOOLEAN : BAS_NODE_TYPE_UNKNOWN;
    +
             default:
                 return BAS_NODE_TYPE_UNKNOWN;
         }
    diff --git a/src/apps/kpunch/dvxbasic/ctrlover.dhs b/src/apps/kpunch/dvxbasic/ctrlover.dhs
    index 8d1b9afb..bbeb30ec 100644
    --- a/src/apps/kpunch/dvxbasic/ctrlover.dhs
    +++ b/src/apps/kpunch/dvxbasic/ctrlover.dhs
    @@ -68,8 +68,8 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
       Height       Integer   R/W   Current height in pixels. Setting this changes the minimum height constraint.
       MinWidth     Integer   R/W   Minimum width for layout. Setting it works like setting Width; reading it returns the minimum that was set.
       MinHeight    Integer   R/W   Minimum height for layout. Setting it works like setting Height; reading it returns the minimum that was set.
    -  MaxWidth     Integer   R/W   Maximum width cap (0 = no limit, stretch to fill).
    -  MaxHeight    Integer   R/W   Maximum height cap (0 = no limit, stretch to fill).
    +  MaxWidth     Integer   R/W   Maximum width cap (0 = no limit, stretch to fill). It wins over MinWidth and over the width the control's contents ask for.
    +  MaxHeight    Integer   R/W   Maximum height cap (0 = no limit, stretch to fill). It wins over MinHeight and over the height the control's contents ask for.
       Weight       Integer   R/W   Layout weight. 0 = fixed size, >0 = share extra space proportionally.
       Visible      Boolean   R/W   Whether the control is visible.
       Enabled      Boolean   R/W   Whether the control accepts user input. Disabling a container (Frame, VBox, HBox...) also blocks mouse and keyboard input to every control inside it.
    @@ -197,6 +197,7 @@ End Sub
     .index DrawWidth
     .index FillColor
     .index FillStyle
    +.index FontScale
     .index AutoRedraw
     .index Paint event
     .index Scale
    @@ -214,6 +215,11 @@ End Sub
     .index DrawScrollBar
     .index DrawScrollArrow
     .index ScrollBarClick
    +.index ScrollBarsNeeded
    +.index vbSBNone
    +.index vbHorizontal
    +.index vbVertical
    +.index vbBoth
     .index PrintAccel
     .index TextWidthAccel
     .index AccelKey
    @@ -238,13 +244,13 @@ Drawings stay until they are drawn over or cleared. In a form's own code, write
       Circle [Step](x, y), radius [, color [, start [, end [, aspect]]]]  Draws a circle, ellipse or arc, filled with FillColor when FillStyle is 0 (full circles).
       DrawFocusRect x, y, width, height [, color]                     Draws the dotted rectangle DVX shows around the control that has the keyboard focus (every other pixel of its edge), for things a program draws itself, such as the focused link of a help topic. A negative width or height counts from the other edge.
       Point(x, y)                                                     The colour of a pixel, -1 outside the picture. With fewer than 16 million colours on the screen it is the colour the screen shows: black, white and full colours such as RGB(255, 0, 0) read back exactly, others may differ slightly.
    -  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,.
    +  Print items                                                     Writes text at CurrentX, CurrentY in ForeColor, FontScale times as large, with PRINT's ; , SPC and TAB; ends by moving to the next line unless the last item is followed by ; or ,.
       PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]   Copies a picture, stretched to width x height: an image file (BMP, PNG, JPEG or GIF), a picture from LOADPICTURE, or a PictureBox given by its name as a string or by a Control variable. opcode combines each pixel with the one already there (see Raster Operations).
       Scale [(x1, y1)-(x2, y2)]                                        Gives the drawing coordinates of its own: (x1, y1) is the top-left corner and (x2, y2) the bottom-right one (y2 below y1 makes y go up). Scale alone goes back to pixels.
       ScaleX(width [, fromScale [, toScale]])                         A width in fromScale converted to toScale (see Scales).
       ScaleY(height [, fromScale [, toScale]])                        A height in fromScale converted to toScale.
    -  TextWidth(text)                                                 Width of the text in the scale's units.
    -  TextHeight(text)                                                Height of a line of text in the scale's units.
    +  TextWidth(text)                                                 Width of the text in the scale's units, at the FontScale size.
    +  TextHeight(text)                                                Height of a line of text in the scale's units, at the FontScale size.
     .endtable
     
     .h2 Drawing the DVX Look
    @@ -260,6 +266,7 @@ These methods draw the parts DVX's own controls are made of, in the colours of t
       DrawScrollBar x, y, length, position, visible, total [, horizontal [, control]]   A scroll bar with its arrows and its thumb, for a list of total rows of which visible are shown from row position, with its top-left corner at x, y. It stands upright (16 pixels wide, length tall, arrows at the top and bottom) unless horizontal is True: then it lies across (length wide, 16 pixels tall, arrows at the left and right ends). With control True it is the narrower bar of a control such as ListBox (14 pixels).
       DrawScrollArrow x, y, size, direction                           One arrow button of a scroll bar, as at the ends of DrawScrollBar's bars: a square button size wide and tall (size in the scale's horizontal units) with its top-left corner at x, y and an arrow pointing dvxArrowUp, dvxArrowDown, dvxArrowLeft or dvxArrowRight, in the theme's scroll bar colours.
       ScrollBarClick(length, offset, position, visible, total [, horizontal [, control]])   The new position after a click on a scroll bar DrawScrollBar drew with the same length, rows, direction and kind: offset is how far the click is from the top of the bar, or from its left end when horizontal is True. The arrows move one row, the trough a page, the thumb leaves it as it is. Needs no drawing.
    +  ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight)   Which scroll bars content that big needs in an area that big, as DVX's controls decide it: vbSBNone (0), vbHorizontal (1), vbVertical (2) or vbBoth (3). A vertical bar narrows the area and may make a horizontal one needed, and the other way round. The sizes are in the object's scale. Needs no drawing.
       PrintAccel text [, enabled]                                     Prints a caption at CurrentX, CurrentY with the letter after & underlined (&& prints one &); enabled False gives the greyed, embossed look of a disabled control. CurrentX moves past the text.
       TextWidthAccel(text)                                            Width of a caption without its & signs, in the scale's units.
       AccelKey(text)                                                  The access key of a caption in lower case ("f" for "&File"), "" when it has none.
    @@ -360,6 +367,7 @@ End Sub
       DrawWidth     Integer   1         Pen width in pixels.
       FillStyle     Integer   1         How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour.
       FillColor     Long      &H000000  Colour boxes and circles are filled with.
    +  FontScale     Integer   1         How many times as large Print writes its text (1 to 8): each dot of DVX's 8 x 16 font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not.
       AutoRedraw    Boolean   False     True: no Paint events (see above).
       ScaleMode     Integer   3         Units of the coordinates (see Scales).
       ScaleLeft     Single    0         Coordinate of the left edge.
    diff --git a/src/apps/kpunch/dvxbasic/form.dhs b/src/apps/kpunch/dvxbasic/form.dhs
    index f6e427dd..a8216285 100644
    --- a/src/apps/kpunch/dvxbasic/form.dhs
    +++ b/src/apps/kpunch/dvxbasic/form.dhs
    @@ -81,8 +81,8 @@ The Form is the top-level container representing a DVX window. It is declared in
       Visible     Boolean   False            True shows the form and False hides it, exactly like Show and Hide (the first show fires Activate).
       ContextMenu String    ""               Name of a popup menu shown when the form is right-clicked.
       HelpTopic   String    ""               Help topic opened by F1 while the form is active and the focused control has no HelpTopic of its own. Empty opens the project's help file at its default page.
    -  Padding     Integer   0                Space in pixels between the window's edges and the controls (the content box's Padding); 0 is the default of 4 pixels.
    -  Spacing     Integer   0                Space in pixels between one control and the next (the content box's Spacing); 0 is the default of 4 pixels.
    +  Padding     Integer   4                Space in pixels between the window's edges and the controls (the content box's Padding); 0 is none.
    +  Spacing     Integer   4                Space in pixels between one control and the next (the content box's Spacing); 0 is none.
       WindowState Integer   0                vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen) or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have.
       Controls    Collection  (none)         The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form.
       DesignMode  Boolean   False            True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files.
    @@ -198,7 +198,7 @@ End Sub
     
     .h2 Drawing
     
    -Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.
    +Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle and so on.
     
     .h2 Example
     
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formCtl.c b/src/apps/kpunch/dvxbasic/formrt/formCtl.c
    index dfafbcce..b6d1585d 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formCtl.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/formCtl.c
    @@ -905,7 +905,7 @@ static WidgetT *ucCreate(WidgetT *parent, const WgtIfaceT *iface) {
         wgtCanvasSetBorderStyle(inst->canvas, CANVAS_BORDER_NONE);
         snprintf(inst->surface.name, sizeof(inst->surface.name), "%s", BAS_UC_OBJECT);
         snprintf(inst->surface.typeName, sizeof(inst->surface.typeName), "%s", BAS_UC_OBJECT);
    -    inst->surface.index  = -1;
    +    inst->surface.index  = FRM_NO_INDEX;
         inst->surface.widget = inst->canvas;
         inst->surface.iface  = wgtGetIface(UC_CANVAS_TYPE);
     
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formGfx.c b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    index 003d6606..45c81136 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
    @@ -112,6 +112,9 @@
     #define DRAW_SB_CLICK_HORZ_ARG  5       // ... , horizontal
     #define DRAW_SB_CLICK_CTRL_ARG  6       // ... , control
     #define DRAW_SB_LINE_ROWS       1       // the rows a control bar's arrow moves
    +#define DRAW_SB_NEEDS_ARGS      4       // ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight)
    +#define DRAW_FONT_SCALE_NORMAL  1       // FontScale: the 8x16 font as it is
    +#define DRAW_FONT_SCALE_MAX     8       // FontScale: the largest multiple
     #define DRAW_FACE_NONE          (-1)    // DrawBevel face: leave the inside as it is
     #define DRAW_ACCEL_ENABLED_ARG  1       // PrintAccel text [, enabled]
     #define DRAW_ACCEL_MAX_ARGS     2
    @@ -172,6 +175,7 @@ struct BasDrawT {
         WidgetT     *canvas;         // the PictureBox, or the form's drawing area (NULL until a form draws)
         AppContextT *ctx;
         int32_t      scaleMode;      // VB ScaleMode (BAS_SCALE_*)
    +    int32_t      fontScale;      // FontScale: Print's text this many times as large
         bool         autoRedraw;     // True: no Paint events
         bool         autoRedrawSet;  // the program set AutoRedraw (False then clears to BackColor before Paint)
         bool         cacheValid;     // colour -> pixel cache
    @@ -200,6 +204,7 @@ typedef enum {
         DRAW_M_SCALEX,
         DRAW_M_SCALEY,
         DRAW_M_SCROLLBARCLICK,
    +    DRAW_M_SCROLLBARSNEEDED,
         DRAW_M_TEXTHEIGHT,
         DRAW_M_TEXTWIDTH,
         DRAW_M_TEXTWIDTHACCEL,
    @@ -214,6 +219,7 @@ typedef enum {
         DRAW_P_DRAWWIDTH,
         DRAW_P_FILLCOLOR,
         DRAW_P_FILLSTYLE,
    +    DRAW_P_FONTSCALE,
         DRAW_P_FORECOLOR,
         DRAW_P_SCALEHEIGHT,
         DRAW_P_SCALELEFT,
    @@ -224,28 +230,29 @@ typedef enum {
     } DrawPropE;
     
     static const char *const sDrawMethods[DRAW_M_COUNT] = {
    -    [DRAW_M_ACCELKEY]        = "AccelKey",
    -    [DRAW_M_CIRCLE]          = "Circle",
    -    [DRAW_M_CLS]             = "Cls",
    -    [DRAW_M_DRAWARROW]       = "DrawArrow",
    -    [DRAW_M_DRAWBEVEL]       = "DrawBevel",
    -    [DRAW_M_DRAWBUTTONFACE]  = "DrawButtonFace",
    -    [DRAW_M_DRAWFOCUSRECT]   = "DrawFocusRect",
    -    [DRAW_M_DRAWSCROLLARROW] = "DrawScrollArrow",
    -    [DRAW_M_DRAWSCROLLBAR]   = "DrawScrollBar",
    -    [DRAW_M_LINE]            = "Line",
    -    [DRAW_M_PAINTPICTURE]    = "PaintPicture",
    -    [DRAW_M_POINT]           = "Point",
    -    [DRAW_M_PRINT]           = "Print",
    -    [DRAW_M_PRINTACCEL]      = "PrintAccel",
    -    [DRAW_M_PSET]            = "PSet",
    -    [DRAW_M_SCALE]           = "Scale",
    -    [DRAW_M_SCALEX]          = "ScaleX",
    -    [DRAW_M_SCALEY]          = "ScaleY",
    -    [DRAW_M_SCROLLBARCLICK]  = "ScrollBarClick",
    -    [DRAW_M_TEXTHEIGHT]      = "TextHeight",
    -    [DRAW_M_TEXTWIDTH]       = "TextWidth",
    -    [DRAW_M_TEXTWIDTHACCEL]  = "TextWidthAccel"
    +    [DRAW_M_ACCELKEY]         = "AccelKey",
    +    [DRAW_M_CIRCLE]           = "Circle",
    +    [DRAW_M_CLS]              = "Cls",
    +    [DRAW_M_DRAWARROW]        = "DrawArrow",
    +    [DRAW_M_DRAWBEVEL]        = "DrawBevel",
    +    [DRAW_M_DRAWBUTTONFACE]   = "DrawButtonFace",
    +    [DRAW_M_DRAWFOCUSRECT]    = "DrawFocusRect",
    +    [DRAW_M_DRAWSCROLLARROW]  = "DrawScrollArrow",
    +    [DRAW_M_DRAWSCROLLBAR]    = "DrawScrollBar",
    +    [DRAW_M_LINE]             = "Line",
    +    [DRAW_M_PAINTPICTURE]     = "PaintPicture",
    +    [DRAW_M_POINT]            = "Point",
    +    [DRAW_M_PRINT]            = "Print",
    +    [DRAW_M_PRINTACCEL]       = "PrintAccel",
    +    [DRAW_M_PSET]             = "PSet",
    +    [DRAW_M_SCALE]            = "Scale",
    +    [DRAW_M_SCALEX]           = "ScaleX",
    +    [DRAW_M_SCALEY]           = "ScaleY",
    +    [DRAW_M_SCROLLBARCLICK]   = "ScrollBarClick",
    +    [DRAW_M_SCROLLBARSNEEDED] = "ScrollBarsNeeded",
    +    [DRAW_M_TEXTHEIGHT]       = "TextHeight",
    +    [DRAW_M_TEXTWIDTH]        = "TextWidth",
    +    [DRAW_M_TEXTWIDTHACCEL]   = "TextWidthAccel"
     };
     
     static const char *const sDrawProps[DRAW_P_COUNT] = {
    @@ -256,6 +263,7 @@ static const char *const sDrawProps[DRAW_P_COUNT] = {
         [DRAW_P_DRAWWIDTH]   = "DrawWidth",
         [DRAW_P_FILLCOLOR]   = "FillColor",
         [DRAW_P_FILLSTYLE]   = "FillStyle",
    +    [DRAW_P_FONTSCALE]   = "FontScale",
         [DRAW_P_FORECOLOR]   = BAS_PROP_FORECOLOR,
         [DRAW_P_SCALEHEIGHT] = "ScaleHeight",
         [DRAW_P_SCALELEFT]   = "ScaleLeft",
    @@ -369,6 +377,10 @@ bool basFormRtDrawGetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, B
                 *out = basValInteger((int16_t)d->gfx.fillStyle);
                 break;
     
    +        case DRAW_P_FONTSCALE:
    +            *out = basValInteger((int16_t)d->fontScale);
    +            break;
    +
             case DRAW_P_FORECOLOR:
                 *out = basValLong((int32_t)d->gfx.fore);
                 break;
    @@ -418,7 +430,8 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
             BasStringT *s     = argc >= 1 ? basValFormatString(args[0]) : basStringNew("", 0);
             BasDrawT   *sized = drawFor(rt, ctrl, false);
             bool        wide  = method == DRAW_M_TEXTWIDTH;
    -        double      px    = wide ? (double)s->len * rt->ctx->font.charWidth : (double)rt->ctx->font.charHeight;
    +        int32_t     scale = sized ? sized->fontScale : DRAW_FONT_SCALE_NORMAL;
    +        double      px    = wide ? (double)s->len * rt->ctx->font.charWidth * scale : (double)rt->ctx->font.charHeight * scale;
     
             if (sized) {
                 drawSync(sized);
    @@ -430,10 +443,10 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
             return true;
         }
     
    -    // AccelKey / TextWidthAccel / ScrollBarClick only measure
    -    if (method == DRAW_M_ACCELKEY || method == DRAW_M_TEXTWIDTHACCEL || method == DRAW_M_SCROLLBARCLICK) {
    +    // AccelKey / TextWidthAccel / ScrollBarClick / ScrollBarsNeeded only measure
    +    if (method == DRAW_M_ACCELKEY || method == DRAW_M_TEXTWIDTHACCEL || method == DRAW_M_SCROLLBARCLICK || method == DRAW_M_SCROLLBARSNEEDED) {
             if (!drawMeasure(rt, drawFor(rt, ctrl, false), (DrawMethodE)method, args, argc, result)) {
    -            basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAccelKey and TextWidthAccel take a caption; ScrollBarClick takes the bar's length, the click's offset from its top (or left end), the position, the rows shown, the rows in all and optionally True for a horizontal bar.", name);
    +            basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nAccelKey and TextWidthAccel take a caption; ScrollBarClick takes the bar's length, the click's offset from its top (or left end), the position, the rows shown, the rows in all and optionally True for a horizontal bar; ScrollBarsNeeded takes the content's width and height and the area's width and height.", name);
             }
     
             return true;
    @@ -600,6 +613,18 @@ bool basFormRtDrawSetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, B
                 break;
             }
     
    +        case DRAW_P_FONTSCALE: {
    +            int32_t scale = basValToInt32(value);
    +
    +            if (scale < DRAW_FONT_SCALE_NORMAL || scale > DRAW_FONT_SCALE_MAX) {
    +                basFormRtRuntimeError(rt, "Invalid property value", "FontScale must be 1 (the normal size) to 8.");
    +                break;
    +            }
    +
    +            d->fontScale = scale;
    +            break;
    +        }
    +
             case DRAW_P_FORECOLOR:
                 d->gfx.fore = (uint32_t)basValToInt32(value);
                 break;
    @@ -788,6 +813,7 @@ static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas) {
             d->gfx.lastY = 0.0;
             d->canvas    = isForm ? NULL : ctrl->widget;
             d->scaleMode = BAS_SCALE_PIXELS;
    +        d->fontScale = DRAW_FONT_SCALE_NORMAL;
             ctrl->draw   = d;
         }
     
    @@ -824,9 +850,32 @@ static bool drawIsSurface(const BasControlT *ctrl) {
     // [, horizontal [, control]]) -- the new position after a click offset
     // along a bar DrawScrollBar drew that long (down a vertical bar, across a
     // horizontal one; arrows one row, trough a page, thumb unchanged; with
    -// control the control bar's rule, widgetScrollbarApplyHit).  False for
    -// bad arguments.
    +// control the control bar's rule, widgetScrollbarApplyHit);
    +// ScrollBarsNeeded(contentWidth, contentHeight, areaWidth, areaHeight) --
    +// the bars content that big needs in an area that big, as a control's
    +// (widgetScrollbarNeeds: each bar can make the other one needed), as
    +// vbSBNone, vbHorizontal, vbVertical or vbBoth.  False for bad arguments.
     static bool drawMeasure(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result) {
    +    if (method == DRAW_M_SCROLLBARSNEEDED) {
    +        if (argc != DRAW_SB_NEEDS_ARGS) {
    +            return false;
    +        }
    +
    +        if (d) {
    +            drawSync(d);
    +        }
    +
    +        const BasGfxT *g      = d ? &d->gfx : NULL;
    +        int32_t        innerW = drawPixelSpan(g, basValToNumber(args[2]), true);
    +        int32_t        innerH = drawPixelSpan(g, basValToNumber(args[3]), false);
    +        bool           vertical;
    +        bool           horizontal;
    +
    +        widgetScrollbarNeeds(drawPixelSpan(g, basValToNumber(args[0]), true), drawPixelSpan(g, basValToNumber(args[1]), false), &innerW, &innerH, &vertical, &horizontal);
    +        *result = basValLong((vertical ? BAS_SB_VERTICAL : BAS_SB_NONE) | (horizontal ? BAS_SB_HORIZONTAL : BAS_SB_NONE));
    +        return true;
    +    }
    +
         if (method == DRAW_M_SCROLLBARCLICK) {
             if (argc < DRAW_SB_CLICK_ARGS || argc > DRAW_SB_CLICK_CTRL_ARG + 1) {
                 return false;
    @@ -1039,12 +1088,13 @@ static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across) {
     
     
     // One object Print item at CurrentX / CurrentY in ForeColor (the DVX
    -// font, transparent background).  Columns, zones and lines are counted
    -// in pixels from the left edge, CurrentX / CurrentY kept in the scale.
    +// font FontScale times as large, transparent background).  Columns,
    +// zones and lines are counted in (scaled) characters from the left edge,
    +// CurrentX / CurrentY kept in the scale.
     static void drawPrint(BasDrawT *d, BasValueT value, int32_t mode) {
         BasGfxT *g      = &d->gfx;
    -    int32_t  charW  = d->ctx->font.charWidth;
    -    int32_t  charH  = d->ctx->font.charHeight;
    +    int32_t  charW  = d->ctx->font.charWidth * d->fontScale;
    +    int32_t  charH  = d->ctx->font.charHeight * d->fontScale;
         double   left   = g->world ? g->worldLeft : 0.0;
         double   px     = basGfxToPixelX(g, g->lastX);
         int32_t  column = (int32_t)(px / charW);
    @@ -1082,7 +1132,7 @@ static void drawPrint(BasDrawT *d, BasValueT value, int32_t mode) {
         BasStringT *s = basValPrintString(value);
     
         wgtCanvasSetPenColor(d->canvas, basGfxPixel(g, g->fore));
    -    wgtCanvasDrawText(d->canvas, basGfxRound(px), basGfxRound(basGfxToPixelY(g, g->lastY)), s->data);
    +    wgtCanvasDrawTextScaled(d->canvas, basGfxRound(px), basGfxRound(basGfxToPixelY(g, g->lastY)), s->data, d->fontScale);
         g->lastX += basGfxToUnitsX(g, (double)s->len * charW);
         basStringUnref(s);
     }
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    index 3a18199a..d8280b24 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    @@ -1109,6 +1109,8 @@ static BasFormT *allocForm(BasFormRtT *rt, const char *formName, bool resizable,
         snprintf(form->name, BAS_MAX_IDENT, "%s", formName);
         snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", BAS_LAYOUT_VBOX);
         form->frmWindowState = BAS_WINSTATE_NONE;
    +    form->frmPadding     = BAS_FRM_GAP_UNSET;
    +    form->frmSpacing     = BAS_FRM_GAP_UNSET;
         form->menuIdNext     = MENU_ID_BASE;
         win->onClose     = onFormClose;
         win->onResize    = onFormResize;
    @@ -1605,7 +1607,7 @@ void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, cons
             return NULL;
         }
     
    -    ctrl->index  = -1;
    +    ctrl->index  = FRM_NO_INDEX;
         snprintf(ctrl->name,     BAS_MAX_IDENT, "%s", ctrlName);
         snprintf(ctrl->typeName, BAS_MAX_IDENT, "%s", typeName ? typeName : "");
         ctrl->widget = widget;
    @@ -2445,14 +2447,15 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
                 form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout);
             }
     
    -        // Padding / Spacing from the .frm (0, the default, changes nothing)
    -        if (form->frmPadding) {
    +        // Padding / Spacing from the .frm (0 is none); one it does not set
    +        // leaves the content box's own
    +        if (form->frmPadding != BAS_FRM_GAP_UNSET) {
                 BasValueT v = basValLong(form->frmPadding);
     
                 contentBoxProp(rt, form, BAS_PROP_PADDING, &v);
             }
     
    -        if (form->frmSpacing) {
    +        if (form->frmSpacing != BAS_FRM_GAP_UNSET) {
                 BasValueT v = basValLong(form->frmSpacing);
     
                 contentBoxProp(rt, form, BAS_PROP_SPACING, &v);
    @@ -5857,7 +5860,7 @@ static void frmLoadOnCtrlBegin(void *userData, const char *typeName, const char
             if (ctrlEntry) {
                 snprintf(ctrlEntry->name, BAS_MAX_IDENT, "%s", name);
                 snprintf(ctrlEntry->typeName, BAS_MAX_IDENT, "%s", typeName);
    -            ctrlEntry->index  = -1;
    +            ctrlEntry->index  = FRM_NO_INDEX;
                 ctrlEntry->widget = widget;
                 ctrlEntry->form   = ctx->form;
                 ctrlEntry->iface  = ctrlIface;
    @@ -5909,7 +5912,7 @@ static void frmLoadOnCtrlProp(void *userData, const char *key, const char *value
     
         // Control array index is stored on the struct, not as a widget property
         if (strcasecmp(key, BAS_PROP_INDEX) == 0) {
    -        ctrl->index = atoi(value);
    +        ctrl->index = frmParseIndex(value);
             return;
         }
     
    @@ -6063,11 +6066,11 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value
                 break;
     
             case FORM_PROP_PADDING:
    -            frm->frmPadding = num;
    +            frm->frmPadding = DVX_MAX(num, 0);
                 break;
     
             case FORM_PROP_SPACING:
    -            frm->frmSpacing = num;
    +            frm->frmSpacing = DVX_MAX(num, 0);
                 break;
     
             case FORM_PROP_WINDOWSTATE:
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
    index 7b77a60e..0bdce489 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
    +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
    @@ -69,6 +69,7 @@ typedef struct BasScreenWinT BasScreenWinT;    // the program screen's window (f
     // Layout of a form or container that names none ("VBox" / "HBox" /
     // "WrapBox" are the box widgets' BASIC names).
     #define BAS_LAYOUT_VBOX          "VBox"
    +#define BAS_FRM_GAP_UNSET        (-1)             // a form's Padding / Spacing when its .frm sets none: the content box keeps its own
     #define BAS_PICTUREBOX_TYPE      "PictureBox"     // takes the drawing members, as forms do
     #define BAS_TERMINAL_TYPE        "Terminal"       // libraries bind byte streams to it (basFormRtTermBind)
     
    @@ -369,7 +370,7 @@ typedef struct {
     typedef struct BasControlT {
         char              name[BAS_MAX_IDENT];     // VB control name (e.g. "Command1")
         char              typeName[BAS_MAX_IDENT]; // VB type name (e.g. "CommandButton")
    -    int32_t           index;                       // control array index (-1 = not in array)
    +    int32_t           index;                       // control array index (FRM_NO_INDEX = not in array)
         WidgetT          *widget;                      // the DVX widget
         BasFormT         *form;                        // owning form
         const WgtIfaceT  *iface;                       // interface descriptor (from .wgt)
    @@ -419,7 +420,7 @@ typedef struct BasFormT {
         bool              frmCentered;
         bool              frmAutoSize;
         char              frmLayout[32];            // "VBox", "HBox", or "WrapBox"
    -    int32_t           frmPadding;               // .frm Padding / Spacing (0 = the layout's default)
    +    int32_t           frmPadding;               // .frm Padding / Spacing (BAS_FRM_GAP_UNSET: none set; 0 = no gap)
         int32_t           frmSpacing;
         int32_t           frmWindowState;           // WindowState set while hidden (.frm or code): applied when shown
         int32_t           menuIdNext;               // id of the next menu item made (ids grow in menu order)
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.c b/src/apps/kpunch/dvxbasic/formrt/frmModel.c
    index 598f168a..d67bd72a 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmModel.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmModel.c
    @@ -191,6 +191,7 @@ int32_t                    frmModelSubtreeEnd(const FrmModelT *m, int32_t idx);
     char                      *frmModelText(const FrmModelT *m, int32_t *outLen);
     int32_t                    frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize);
     int32_t                    frmModelWriteSize(const FrmModelT *m);
    +static int32_t             gapPixels(const char *value);
     static bool                isEmptyValue(uint8_t type, const char *value);
     static void                loadOnCtrlBegin(void *userData, const char *typeName, const char *name);
     static void                loadOnCtrlEnd(void *userData);
    @@ -464,9 +465,11 @@ static bool formFieldText(const FrmModelT *m, const char *name, char *out, int32
         } else if (strcasecmp(name, BAS_PROP_PADDING) == 0) {
             num   = m->padding;
             isNum = true;
    +        text  = (m->padding == BAS_FRM_GAP_UNSET) ? "" : NULL;
         } else if (strcasecmp(name, BAS_PROP_SPACING) == 0) {
             num   = m->spacing;
             isNum = true;
    +        text  = (m->spacing == BAS_FRM_GAP_UNSET) ? "" : NULL;
         } else if (strcasecmp(name, BAS_PROP_WINDOWSTATE) == 0) {
             num   = m->windowState;
             isNum = true;
    @@ -517,9 +520,9 @@ static bool formPropSet(FrmModelT *m, const char *key, const char *value) {
         } else if (strcasecmp(key, BAS_PROP_HEIGHT) == 0) {
             m->height = atoi(value);
         } else if (strcasecmp(key, BAS_PROP_PADDING) == 0) {
    -        m->padding = atoi(value);
    +        m->padding = gapPixels(value);
         } else if (strcasecmp(key, BAS_PROP_SPACING) == 0) {
    -        m->spacing = atoi(value);
    +        m->spacing = gapPixels(value);
         } else if (strcasecmp(key, BAS_PROP_WINDOWSTATE) == 0) {
             m->windowState = atoi(value);
         } else if (strcasecmp(key, BAS_PROP_HELPTOPIC) == 0) {
    @@ -672,7 +675,7 @@ void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value) {
         if (ip) {
             frmIntPropSet(ctrl, ip, atoi(value));
         } else if (strcasecmp(key, BAS_PROP_INDEX) == 0) {
    -        ctrl->index = atoi(value);
    +        ctrl->index = frmParseIndex(value);
         } else if (strcasecmp(key, BAS_PROP_HELPTOPIC) == 0) {
             snprintf(ctrl->helpTopic, sizeof(ctrl->helpTopic), "%s", value);
         } else if (strcasecmp(key, BAS_PROP_VISIBLE) == 0 && !frmIfaceProp(ctrl->typeName, BAS_PROP_VISIBLE)) {
    @@ -1579,6 +1582,8 @@ FrmModelT *frmModelNew(const char *name) {
     
         m->width     = BAS_DEFAULT_FORM_W;
         m->height    = BAS_DEFAULT_FORM_H;
    +    m->padding   = BAS_FRM_GAP_UNSET;
    +    m->spacing   = BAS_FRM_GAP_UNSET;
         m->centered  = true;
         m->resizable = true;
         snprintf(m->layout, sizeof(m->layout), "%s", BAS_LAYOUT_VBOX);
    @@ -1777,7 +1782,7 @@ bool frmModelSetCtrlProp(FrmModelT *m, FrmCtrlT *ctrl, const char *name, const c
         }
     
         if (strcasecmp(name, BAS_PROP_INDEX) == 0) {
    -        frmModelMakeArrayMember(m, ctrl, value[0] ? atoi(value) : FRM_NO_INDEX);
    +        frmModelMakeArrayMember(m, ctrl, frmParseIndex(value));
             return true;
         }
     
    @@ -1940,7 +1945,7 @@ int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize) {
                 continue;
             }
     
    -        if ((strcasecmp(pd->name, BAS_PROP_PADDING) == 0 && m->padding == 0) || (strcasecmp(pd->name, BAS_PROP_SPACING) == 0 && m->spacing == 0) || (strcasecmp(pd->name, BAS_PROP_WINDOWSTATE) == 0 && m->windowState == 0)) {
    +        if ((strcasecmp(pd->name, BAS_PROP_PADDING) == 0 && m->padding == BAS_FRM_GAP_UNSET) || (strcasecmp(pd->name, BAS_PROP_SPACING) == 0 && m->spacing == BAS_FRM_GAP_UNSET) || (strcasecmp(pd->name, BAS_PROP_WINDOWSTATE) == 0 && m->windowState == 0)) {
                 continue;
             }
     
    @@ -2011,6 +2016,18 @@ int32_t frmModelWriteSize(const FrmModelT *m) {
     }
     
     
    +// A form's Padding / Spacing from .frm text: pixels in the range the
    +// runtime's content box takes (wgtSetPaddingPx), so the designer shows
    +// what the program will; "" (cleared in the designer) is none set.
    +static int32_t gapPixels(const char *value) {
    +    if (!value[0]) {
    +        return BAS_FRM_GAP_UNSET;
    +    }
    +
    +    return DVX_MIN(DVX_MAX(atoi(value), 0), WGT_GAP_PIXELS_MAX);
    +}
    +
    +
     static bool isEmptyValue(uint8_t type, const char *value) {
         if (type == WGT_IFACE_STRING) {
             return value[0] == '\0';
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.h b/src/apps/kpunch/dvxbasic/formrt/frmModel.h
    index 5fc3679a..4c65319f 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmModel.h
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmModel.h
    @@ -70,7 +70,6 @@
     #define FRM_MAX_MENU_LEVEL    (FRM_MAX_NESTING - 2)              // deepest menu level (0 = menu bar)
     #define FRM_MENU_STACK_DEPTH  (FRM_MAX_MENU_LEVEL + 1)           // menu levels 0..FRM_MAX_MENU_LEVEL
     #define FRM_MAX_PROP_INDENT   (FRM_MAX_NESTING * FRM_INDENT_SPACES) // indent of the deepest property line
    -#define FRM_NO_INDEX          (-1)   // control not in a control array; no such control / menu item
     #define FRM_NEW_MENU_ID       0      // FrmMenuItemT.id of an item added since the ids were given out
     #define FRM_DEFAULT_CTRL_W    100    // MinWidth of a new control
     #define FRM_DEFAULT_CTRL_H    30     // MinHeight of a new control
    @@ -164,7 +163,7 @@ typedef struct FrmModelT {
         int32_t       height;
         int32_t       left;          // initial position (used when not centered)
         int32_t       top;
    -    int32_t       padding;       // content box Padding / Spacing in pixels (0 = the layout's default)
    +    int32_t       padding;       // content box Padding / Spacing in pixels (BAS_FRM_GAP_UNSET: the box's own; 0 = no gap)
         int32_t       spacing;
         int32_t       windowState;   // WindowState when first shown (0 = normal, not written)
         FrmCtrlT    **controls;      // stb_ds array of heap-allocated controls, in tree order
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c b/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
    index 3b556f9a..529bb192 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
    @@ -770,6 +770,13 @@ static void liveForm(FrmLiveT *live) {
                 continue;
             }
     
    +        // A Padding / Spacing nothing set stays the content box's own
    +        const WidgetT *box = form->contentBox;
    +
    +        if (box && ((strcasecmp(pd->name, BAS_PROP_PADDING) == 0 && box->padding == WGT_GAP_DEFAULT) || (strcasecmp(pd->name, BAS_PROP_SPACING) == 0 && box->spacing == WGT_GAP_DEFAULT))) {
    +            continue;
    +        }
    +
             BasValueT v = basFormRtGetProp(live->rt, &form->formCtrl, pd->name);
     
             valueText(v, pd->type, val, sizeof(val));
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.c b/src/apps/kpunch/dvxbasic/formrt/frmParser.c
    index 49278839..3cf42631 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmParser.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.c
    @@ -39,6 +39,7 @@
     bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb);
     bool frmParseBool(const char *val);
     bool frmParseBoolDefault(const char *val, bool defaultValue);
    +int32_t frmParseIndex(const char *val);
     void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax);
     void frmStripQuotes(char *val);
     static bool readToken(const char **p, char *buf, int32_t bufMax);
    @@ -266,6 +267,17 @@ bool frmParseBoolDefault(const char *val, bool defaultValue) {
     }
     
     
    +int32_t frmParseIndex(const char *val) {
    +    int32_t index = (int32_t)atoi(val);
    +
    +    if (val[0] == '\0' || index < 0) {
    +        return FRM_NO_INDEX;
    +    }
    +
    +    return index;
    +}
    +
    +
     void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax) {
         key[0]   = '\0';
         value[0] = '\0';
    diff --git a/src/apps/kpunch/dvxbasic/formrt/frmParser.h b/src/apps/kpunch/dvxbasic/formrt/frmParser.h
    index 44a8acd8..633fbdad 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/frmParser.h
    +++ b/src/apps/kpunch/dvxbasic/formrt/frmParser.h
    @@ -55,6 +55,10 @@
     #define FRM_KW_END_LEN        ((int32_t)sizeof(FRM_KW_END) - 1)
     #define FRM_KW_VERSION_LEN    ((int32_t)sizeof(FRM_KW_VERSION) - 1)
     
    +// A control's Index when it is not in a control array (frmParseIndex);
    +// the form model also returns it for "no such control / menu item".
    +#define FRM_NO_INDEX          (-1)
    +
     // Callbacks supplied by the consumer. Any field may be NULL.
     typedef struct FrmParserCbsT {
         void *userData;
    @@ -110,6 +114,12 @@ bool frmParseBool(const char *val);
     // Use for properties whose unset state is true (menu Enabled/Visible).
     bool frmParseBoolDefault(const char *val, bool defaultValue);
     
    +// A control array Index value: its number, or FRM_NO_INDEX when it is
    +// empty or below 0.  The one Index rule for the run time, the form model
    +// and the designer, so a control the writer saves without an Index line
    +// (any index below 0) loads back the same.
    +int32_t frmParseIndex(const char *val);
    +
     // True when s[0..len) (left-trimmed, no line terminator) is an End line:
     // the End keyword alone, or followed by whitespace and/or a ' comment.
     // The one End-line rule for the parser and every .frm scanner.
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    index cdabff4e..f1133d4e 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
    @@ -55,6 +55,7 @@
     static WidgetT            *containerChildParent(FrmCtrlT *pc);
     static WidgetT            *createDesignWidget(const char *vbTypeName, WidgetT *parent);
     void                       dsgnApplyEnabled(const FrmModelT *form);
    +void                       dsgnApplyGaps(DsgnStateT *ds);
     void                       dsgnBuildPreviewMenuBar(WindowT *win, const FrmModelT *form);
     WidgetT                   *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT *parent);
     void                       dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
    @@ -66,6 +67,7 @@ void                       dsgnNewForm(DsgnStateT *ds, const char *name);
     void                       dsgnOnKey(DsgnStateT *ds, int32_t key);
     void                       dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag);
     void                       dsgnPaintOverlay(DsgnStateT *ds);
    +void                       dsgnRebuildContent(DsgnStateT *ds);
     void                       dsgnRebuildWidgets(DsgnStateT *ds);
     int32_t                    dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize);
     WidgetT                   *dsgnSelectedWidget(const DsgnStateT *ds);
    @@ -139,6 +141,15 @@ void dsgnApplyEnabled(const FrmModelT *form) {
     }
     
     
    +void dsgnApplyGaps(DsgnStateT *ds) {
    +    WidgetT *box = ds->contentBox;
    +
    +    box->padding = (ds->form->padding == BAS_FRM_GAP_UNSET) ? WGT_GAP_DEFAULT : wgtPixels(ds->form->padding);
    +    box->spacing = (ds->form->spacing == BAS_FRM_GAP_UNSET) ? WGT_GAP_DEFAULT : wgtPixels(ds->form->spacing);
    +    wgtInvalidate(box);
    +}
    +
    +
     void dsgnBuildPreviewMenuBar(WindowT *win, const FrmModelT *form) {
         if (!win || !form) {
             return;
    @@ -260,9 +271,8 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
             return;
         }
     
    -    ds->contentBox      = contentBox;
    -    contentBox->padding = wgtPixels(ds->form->padding);
    -    contentBox->spacing = wgtPixels(ds->form->spacing);
    +    ds->contentBox = contentBox;
    +    dsgnApplyGaps(ds);
     
         int32_t count = (int32_t)arrlen(ds->form->controls);
     
    @@ -495,6 +505,24 @@ void dsgnPaintOverlay(DsgnStateT *ds) {
     }
     
     
    +// The form window's root box back to its own Padding / Spacing (it is
    +// the content box of a VBox form), a new content box in the form's
    +// layout, and the controls rebuilt in it.
    +void dsgnRebuildContent(DsgnStateT *ds) {
    +    WidgetT *root = (ds->form && ds->formWin) ? ds->formWin->widgetRoot : NULL;
    +
    +    if (!root) {
    +        return;
    +    }
    +
    +    wgtDestroyChildren(root);
    +    root->padding  = WGT_GAP_DEFAULT;
    +    root->spacing  = WGT_GAP_DEFAULT;
    +    ds->contentBox = basFormRtCreateContentBox(root, ds->form->layout);
    +    dsgnRebuildWidgets(ds);
    +}
    +
    +
     void dsgnRebuildWidgets(DsgnStateT *ds) {
         WidgetT *parent = ds->form ? ds->contentBox : NULL;
     
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    index 225025ea..50d1f5a4 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    +++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
    @@ -102,6 +102,15 @@ WidgetT *dsgnCreateControlWidget(const FrmModelT *form, FrmCtrlT *ctrl, WidgetT
     // ancestors') to the live widgets.
     void dsgnApplyEnabled(const FrmModelT *form);
     
    +// The form's Padding / Spacing on the content box (one the form does not
    +// set: the layout's own), then a re-layout.
    +void dsgnApplyGaps(DsgnStateT *ds);
    +
    +// Recreate the form window's content box after the form's Layout
    +// changed: the root box back to its own Padding / Spacing, a new box in
    +// the form's layout, then dsgnRebuildWidgets.
    +void dsgnRebuildContent(DsgnStateT *ds);
    +
     // Destroy every live widget under the content box and recreate them in
     // the current controls[] order (the widget child list mirrors that order),
     // then size the form window (dsgnFitFormWin).
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c
    index c48da47a..98a563d0 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideMain.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c
    @@ -5275,7 +5275,7 @@ static void onMenu(WindowT *win, int32_t menuId) {
         if (menuId == CMD_HELP_ABOUT) {
             dvxMessageBox(sAc, "About DVX BASIC",
                 "DVX BASIC 1.0\n"
    -            "Visual BASIC Development Environment\n"
    +            "BASIC Development Environment\n"
                 "for the DVX GUI System\n"
                 "\n"
                 "Copyright 2026 Scott Duensing",
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
    index 17e818c7..5aac439a 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
    @@ -656,17 +656,11 @@ static void onPropDblClick(WidgetT *w) {
                 frmModelSetFormProp(sDs->form, BAS_PROP_LAYOUT, layoutNames[chosenIdx]);
             }
     
    -        // Rebuild the form designer to apply the new layout
    -        if (sDs->formWin && sDs->formWin->widgetRoot) {
    -            WidgetT *root = sDs->formWin->widgetRoot;
    -
    -            wgtDestroyChildren(root);
    -
    -            // The root content box uses the form's layout, not the
    -            // selected container's.  Per-container layouts are
    -            // applied separately inside dsgnCreateWidgets.
    -            sDs->contentBox = basFormRtCreateContentBox(root, sDs->form->layout);
    -            dsgnRebuildWidgets(sDs);
    +        // Rebuild the form designer to apply the new layout (the content
    +        // box takes the form's; a container's is applied inside
    +        // dsgnCreateWidgets)
    +        if (sDs->formWin) {
    +            dsgnRebuildContent(sDs);
                 dvxInvalidateWindow(sPrpCtx, sDs->formWin);
             }
     
    @@ -914,14 +908,12 @@ static void onPropDblClick(WidgetT *w) {
                 ideRenameInCode(oldName, sDs->form->name);
                 prpRebuildTree(sDs);
             } else {
    -            // Padding / Spacing below 0 mean the default; an explicit
    -            // Width / Height ends AutoSize
    -            bool isPad   = strcasecmp(propName, BAS_PROP_PADDING) == 0 || strcasecmp(propName, BAS_PROP_SPACING) == 0;
    -            bool isSize  = strcasecmp(propName, BAS_PROP_WIDTH) == 0 || strcasecmp(propName, BAS_PROP_HEIGHT) == 0;
    -            char px[PRP_INT_BUF];
    +            // An explicit Width / Height ends AutoSize (the model keeps
    +            // Padding / Spacing in 0..WGT_GAP_PIXELS_MAX, "" = none set)
    +            bool isPad  = strcasecmp(propName, BAS_PROP_PADDING) == 0 || strcasecmp(propName, BAS_PROP_SPACING) == 0;
    +            bool isSize = strcasecmp(propName, BAS_PROP_WIDTH) == 0 || strcasecmp(propName, BAS_PROP_HEIGHT) == 0;
     
    -            snprintf(px, sizeof(px), "%d", DVX_MAX(atoi(newValue), 0));
    -            frmModelSetFormProp(sDs->form, propName, isPad ? px : newValue);
    +            frmModelSetFormProp(sDs->form, propName, newValue);
     
                 if (isSize) {
                     sDs->form->autoSize = false;
    @@ -940,9 +932,7 @@ static void onPropDblClick(WidgetT *w) {
                 }
     
                 if (isPad && sDs->contentBox) {
    -                sDs->contentBox->padding = wgtPixels(sDs->form->padding);
    -                sDs->contentBox->spacing = wgtPixels(sDs->form->spacing);
    -                wgtInvalidate(sDs->contentBox);
    +                dsgnApplyGaps(sDs);
                 }
             }
     
    diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
    index cea78b82..483f442d 100644
    --- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
    +++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
    @@ -139,7 +139,6 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
         win->onClose = onTbxClose;
     
         WidgetT *root = wgtInitWindow(ctx, win);
    -    root->spacing = 0;
     
         int32_t   col     = 0;
         WidgetT  *row     = NULL;
    @@ -194,7 +193,6 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
             // Start a new row every TBX_COLS buttons
             if (col == 0) {
                 row = wgtHBox(root);
    -            row->spacing = 0;
             }
     
             // Create button in the current row
    diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs
    index 1a0c64cb..597964ce 100644
    --- a/src/apps/kpunch/dvxbasic/ideguide.dhs
    +++ b/src/apps/kpunch/dvxbasic/ideguide.dhs
    @@ -31,7 +31,7 @@
     
     .h1 DVX BASIC IDE Guide
     
    -DVX BASIC is a Visual BASIC development environment for DVX. It provides a VB3-style integrated development environment with a code editor, form designer, project system, and a full interactive debugger -- all integrated into DVX itself.
    +DVX BASIC is a BASIC development environment for DVX. It provides a VB3-style integrated development environment with a code editor, form designer, project system, and a full interactive debugger -- all integrated into DVX itself.
     
     This guide covers every feature of the IDE: menus, toolbar, editor, form designer, project management, debugger, and auxiliary windows.
     
    @@ -1322,7 +1322,11 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item VAL keeps the magnitude of very long digit strings. Running out of memory while building a string raises error 14.
     .item REDIM PRESERVE of a TYPE array keeps the existing elements.
     .item Appending to a String variable with s = s + x or s = s & x no longer copies the whole string each time, so building long strings in a loop is much faster. When x is InputBox$, MsgBox, CreateForm, CreateControl or Shell, s is still read before x runs (left to right).
    +.item And, Or, Xor, Eqv and Imp of two Booleans give a Boolean, and Not of a Boolean gives a Boolean, as in Visual Basic 4 and later: Print (a > 1) Or (b > 1) prints True instead of -1, and TypeName gives "Boolean". With a number on either side they still work on the bits and give a number.
    +.item Big disks and long file names: where the disk takes long file names (DVX under Windows, or a long file name driver loaded before DVX), OPEN, KILL, NAME, MKDIR, DIR$ and the other file statements use them, and the File Manager, the file dialogs and Terminal downloads keep them. A file can be bigger than 2 GB on a FAT32 disk; LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and EOF stays right below it. FileOpsFreeBytes and FileOpsTotalBytes are right for disks past 2 GB.
    +.item Changed: FileOpsValidName takes the folder the name is for, FileOpsValidName(folder, itemName), since the rules depend on its disk; XferSafeName$ takes the folder a file is received into, XferSafeName$(name, folder). New: FileOpsLongNames, FileOpsShortPath$, FileOpsValidLongName and FileOpsValidShortName (fileops.bas), and ShellArgQuote$ (shell.bas), which puts a path with spaces in quotes for a command line.
     .item Programs compiled with an older release must be rebuilt. A program whose procedure table records more than 64 locals for a procedure is refused when it is loaded.
    +.item A class may have at most 65536 member variables and a form at most 65536 form-level variables (as many as the compiled program can number); more is a compile error ("Too many member variables in X", "Too many form-level variables"), and a program file that records more is refused when it is loaded.
     .endlist
     
     .h2 Forms and Controls
    @@ -1331,6 +1335,11 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
     .item A reference to a removed control or to a control of an unloaded form raises an "unknown control" runtime error. CreateControl with a parent that is not a container on the same form raises "CreateControl: invalid parent".
     .item SetEvent works on forms (Me) and menu items; a handler without parameters is called without the event's arguments.
     .item Frame: the Caption from the .frm file or set by code now shows on the frame's border, and reading Caption gives it back (it was ignored and read back "").
    +.item A control's Index below 0, in a .frm file or typed in the designer, means the control is not in a control array, in the designer, the form loader and the saved file alike (a negative Index was kept, saved without an Index line and read back as a different control).
    +.item ListBox: a horizontal scroll bar appears when the box is narrower than its longest item (its MaxWidth, or a narrow form); Left and Right scroll it a character at a time.
    +.item Padding = 0 and Spacing = 0 now mean no padding and no space (before, 0 meant the default). A form, box or ScrollPane that sets none keeps its own (4 pixels for forms, VBox and HBox; a Frame's Spacing is 0, a ScrollPane's both 0), and reading Padding or Spacing gives the pixels in use. In the designer, an empty Padding or Spacing on the form means the layout's own.
    +.item MaxWidth and MaxHeight win over MinWidth / MinHeight and over the size a control's contents ask for, and the space above the cap goes to the other controls instead of staying empty.
    +.item New drawing members on forms and PictureBoxes: FontScale (Print, TextWidth and TextHeight up to 8 times as large, for titles) and ScrollBarsNeeded (which scroll bars content of a size needs in an area, as the controls decide it; vbSBNone, vbHorizontal, vbVertical, vbBoth).
     .item Form.Visible = True/False behaves exactly like Show/Hide, but only on a real change: Visible = True on a form that is already shown does nothing (a modal form stays modal), and on a form that has no window it is ignored. MinWidth and MinHeight read back the value that was set. Size properties are limited to 0..32767.
     .item Disabling a container (for example Frame.Enabled = False) blocks mouse and keyboard input to every control inside it (also inside a disabled ScrollPane or Splitter), and SetFocus on such a control does nothing.
     .item OptionButton.Value is a read/write Boolean (True = selected); the button's index is the new read-only OptionIndex. Setting Value raises no Click.
    diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
    index 99ec9863..fbe9a82c 100644
    --- a/src/apps/kpunch/dvxbasic/langref.dhs
    +++ b/src/apps/kpunch/dvxbasic/langref.dhs
    @@ -446,7 +446,7 @@ If TypeOf Source Is TextBox Then Print Source.Text
     
     .h2 Logical / Bitwise Operators
     
    -The logical operators work on Boolean values and also perform bitwise operations on integer values. With Null they give Null unless the other side decides the answer (False And Null is False, True Or Null is True); see Variants.
    +The logical operators work on Boolean values and also perform bitwise operations on integer values. As in Visual Basic, two Booleans give a Boolean (True And False is False, Not True is False, and Print (a > 1) Or (b > 1) prints True); when either side is a number they work on the bits and give a number (an Integer, or a Long when a side is Long): True And 3 is 3. With Null they give Null unless the other side decides the answer (False And Null is False, True Or Null is True, a Boolean as the other side is); see Variants.
     
     .table
       Operator   Description
    @@ -582,7 +582,7 @@ Dim fixedStr As String * 20
     .endcode
     
     .note info
    -A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables".
    +A module-level variable is visible inside a SUB or FUNCTION only when it is declared with DIM SHARED (or named in a SHARED statement in that procedure). See Variable Scope below. Inside a SUB or FUNCTION, DIM declares a local variable that is recreated on each call (use STATIC to retain its value between calls). A SUB or FUNCTION holds at most 64 parameters and local variables together, including a few hidden ones that statements such as ON ... GOTO, SWAP and READ into array elements use while they run; a procedure that needs more fails to compile with "Too many local variables". A program holds at most 512 module-level variables, a form at most 65536 form-level variables and a class at most 65536 member variables; more fails to compile.
     .endnote
     
     Fixed-length strings (STRING * n) are padded with spaces and truncated when assigned so their length is always exactly n.
    @@ -1753,7 +1753,7 @@ ErrorHandler:
       3        RETURN without GOSUB
       4        Out of DATA
       5        Illegal function call (bad argument to a built-in function; an undefined math result such as a negative number to a fractional power)
    -  6        Overflow (value does not fit the target type; EXP, ^ or other math leaving the numeric range)
    +  6        Overflow (value does not fit the target type; EXP, ^ or other math leaving the numeric range; a file size or position past 2 GB in LOF, FILELEN, LOC, SEEK, GET or PUT)
       7        Out of memory
       9        Subscript out of range / invalid variable or field index
       10       This array is fixed or temporarily locked (REDIM, or ERASE of a dynamic array, while one of its elements, or a field of one, is passed by reference to a procedure that is still running)
    @@ -2576,7 +2576,7 @@ WEND
     
     .h2 FILELEN
     
    -Returns the length of a file in bytes. A file that does not exist raises error 53 (File not found).
    +Returns the length of a file in bytes. A file that does not exist raises error 53 (File not found); one past 2 GB raises error 6 (Overflow).
     
     .code
     bytes = FILELEN(filename$)
    @@ -3057,6 +3057,8 @@ Print MonthName(Month(Date)); " "; WeekdayName(Weekday(Date), True)
     
     The dim argument of LBOUND and UBOUND must be a constant from 1 to 8.
     
    +File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes). A disk can hold bigger files (up to 4 GB on FAT32); LOF, FILELEN, LOC and SEEK raise error 6 (Overflow) for a size or position past 2 GB rather than give a wrong number, and GET and PUT do the same for a record that would end past it. EOF stays right while the position is below 2 GB. FileOpsListSize and FileOpsTreeBytes (fileops.bas) give any file's size as a Double.
    +
     .topic lang.func.misc
     .title Miscellaneous Functions
     .toc 2 Miscellaneous Functions
    @@ -3726,6 +3728,7 @@ NoMoney:
     .item A member's name cannot be a reserved word. Names such as Name, Show, Hide, Load, Print, Open, Close, Line, Input, Get, Put, Type, Error and Timer are keywords in DVX BASIC and cannot be used; Text, Caption, Value, Count, Item and Add are fine.
     .item SHARED and DIM SHARED cannot be used in a class module; a class's code reaches the program's module variables that are declared DIM SHARED or PUBLIC.
     .item A STATIC variable in a class procedure is kept per object, as in Visual Basic: each object has its own copy.
    +.item A class has at most 65536 member variables, its procedures' STATIC variables included; more fails to compile with "Too many member variables in ClassName".
     .item Class_Initialize and Class_Terminate take no parameters.
     .item A class module cannot hold statements outside its procedures; the compiler reports "Only declarations are allowed outside procedures in a class module".
     .item NEW, PROPERTY, PUBLIC, PRIVATE and TYPEOF are reserved words. VARIANT, OBJECT, COLLECTION, EMPTY, NULL, EACH and IN are not.
    @@ -4862,6 +4865,11 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
     .index FileOpsMakePath
     .index FileOpsReplaceFile
     .index FileOpsValidName
    +.index FileOpsValidLongName
    +.index FileOpsValidShortName
    +.index FileOpsShortPath$
    +.index FileOpsLongNames
    +.index Long File Names
     .index system.bas
     .index SysSetVideoMode
     .index SysSetColor
    @@ -4971,6 +4979,7 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
     .index ShellCommandArgs$
     .index ShellArgCount
     .index ShellArg$
    +.index ShellArgQuote$
     .index Running programs
     .index Program priority
     .index SysScreenshot
    @@ -5279,9 +5288,9 @@ With fileops.bas in the project, a program can show and change what is on the di
     
     Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsBaseName$ and FileOpsExtension$. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.
     
    -Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName (whether a name can be used as a file name here; FileOpsError$ says why not), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.
    +Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles and FileOpsTreeFolders (the size of a whole folder) and FileOpsFormatSize$.
     
    -Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).
    +Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when DVX itself runs from such a disk, so settings cannot be saved in its folder. FileOpsTempDir$() is the folder for scratch files ("" when there is none).
     
     FileOpsCopyTree, FileOpsMoveTree and FileOpsDeleteTree do a whole folder at once. For a long copy, move or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
     
    @@ -5349,7 +5358,7 @@ Sub FillTaskList ()
     End Sub
     .endcode
     
    -Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLine) split a command line the way ShellRunCommand does: the program (the first word, or the text in double quotes when it starts with one) and the arguments after it. ShellArgCount(commandLine) and ShellArg$(commandLine, index) split any command line into its words (index from 0) by DVX's rule: words are separated by blanks, and a word with blanks in it is written in double quotes, which ShellArg$ leaves off ("" past the last word). COMMAND$(n) gives this program's own arguments the same way.
    +Command lines. ShellCommandProgram$(commandLine) and ShellCommandArgs$(commandLine) split a command line the way ShellRunCommand does: the program (the first word, or the text in double quotes when it starts with one) and the arguments after it. ShellArgCount(commandLine) and ShellArg$(commandLine, index) split any command line into its words (index from 0) by DVX's rule: words are separated by blanks, and a word with blanks in it is written in double quotes, which ShellArg$ leaves off ("" past the last word). ShellArgQuote$(text) writes one word that way: text in double quotes when it holds blanks, as it is otherwise, so a folder or file name with spaces in it reaches the program it is passed to whole (ShellRun "notepad.app", ShellArgQuote$(path$)). COMMAND$(n) gives this program's own arguments the same way.
     
     Keys for every program. HotkeyAdd(keyText) makes a key such as "Ctrl+Alt+E" work whichever program is in front and returns its id (0 when it is not a key, needs Ctrl or Alt, or another program uses it). The program asks for the keys pressed from a Timer: HotkeyNext() returns the id of the oldest one not asked for yet, 0 when there is none. HotkeyRemove id stops one; the rest stop when the program ends. HotkeyText$(keyText) writes a key the way DVX does ("ctrl+alt+e" gives "Ctrl+Alt+E"; "" when it is not a key), handy for checking what the user typed. ShellItemKeyValid(keyText) is Program Manager's rule for an item's shortcut: Ctrl together with Alt or Shift, so it never takes a key a program uses itself ("" for no shortcut is valid too).
     
    @@ -5755,7 +5764,7 @@ End Sub
     
     With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say) and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.
     
    -XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name) gives the 8.3 name a receive stores a sender's file name as: no folder or device name, only characters every disk accepts.
    +XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts.
     
     .h2 Lists You Draw Yourself
     
    diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.c b/src/apps/kpunch/dvxbasic/runtime/serialize.c
    index b630db65..3941a764 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/serialize.c
    +++ b/src/apps/kpunch/dvxbasic/runtime/serialize.c
    @@ -1120,7 +1120,7 @@ static void bufWriteU8(BufT *b, uint8_t v) {
     // (-1: none), its procedures in the procedure table, member variables
     // among its own and member kinds known.
     static bool classRangesValid(const BasModuleT *mod, const BasClassInfoT *cls) {
    -    if (cls->varCount < 0 || cls->initCodeAddr < -1 || cls->initCodeAddr >= mod->codeLen || cls->terminateProc < -1 || cls->terminateProc >= mod->procCount) {
    +    if (cls->varCount < 0 || cls->varCount > BAS_VM_MAX_MEMBER_VARS || cls->initCodeAddr < -1 || cls->initCodeAddr >= mod->codeLen || cls->terminateProc < -1 || cls->terminateProc >= mod->procCount) {
             return false;
         }
     
    @@ -1263,7 +1263,7 @@ static bool moduleRangesValid(const BasModuleT *mod) {
         for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
             const BasFormVarInfoT *fv = &mod->formVarInfo[i];
     
    -        if (fv->varCount < 0 || fv->initCodeLen < 0) {
    +        if (fv->varCount < 0 || fv->varCount > BAS_VM_MAX_MEMBER_VARS || fv->initCodeLen < 0) {
                 return false;
             }
     
    diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c
    index df1df665..52b9326c 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/vm.c
    +++ b/src/apps/kpunch/dvxbasic/runtime/vm.c
    @@ -385,6 +385,7 @@ static BasVmResultE   callByName(BasVmT *vm, BasValueT *args, int32_t argc);
     static BasVmResultE   callMember(BasVmT *vm, BasObjectT *obj, const BasClassMemberT *m, BasValueT *args, int32_t argc, bool wantResult);
     static BasVmResultE   callNamed(BasVmT *vm, BasValueT objVal, BasValueT nameVal, BasValueT *args, int32_t argc, int32_t named, BasValueT namesVal);
     static bool           callProcAt(BasVmT *vm, int32_t codeAddr, BasObjectT *me, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount);
    +static BasVmResultE   channelTell(BasVmT *vm, FILE *fp, long *pos);
     static int32_t        clampComponent(int32_t c);
     static bool           classImplements(const BasVmT *vm, int32_t classIdx, int32_t iface);
     static bool           classListens(const BasVmT *vm, int32_t classIdx);
    @@ -507,6 +508,8 @@ static int32_t        likeElement(const char *p, int32_t pLen, int32_t pos, uint
     static int32_t        likeMatch(const char *s, int32_t sLen, const char *p, int32_t pLen, bool text);
     static void           localNow(struct tm *out, int32_t *outUsec);
     static BasVmResultE   logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b);
    +static BasValueT      logicalResult(int32_t r, bool isBool, bool isLong);
    +static void           logicalSet(BasValueT *v, int32_t r, bool isBool, bool isLong);
     static void           lprintWrite(BasVmT *vm, const char *data, int32_t len);
     static int32_t        lsetRecord(BasVmT *vm, BasValueT *args, BasValueT *out);
     static BasVmResultE   mathRangeError(BasVmT *vm, double result);
    @@ -5349,6 +5352,23 @@ static bool callProcAt(BasVmT *vm, int32_t codeAddr, BasObjectT *me, const BasVa
     }
     
     
    +// A channel's position for EOF, LOF, LOC, SEEK and GET / PUT.  These are
    +// Longs in BASIC (as in VB), so a position past 2 GB -- a FAT32 file
    +// reaches 4 GB, and DOS's C library reports such a position as negative
    +// -- raises Overflow instead of giving a wrong number.  Reading a file
    +// through to its end needs no position.
    +static BasVmResultE channelTell(BasVmT *vm, FILE *fp, long *pos) {
    +    int64_t at = ftell(fp);
    +
    +    if (at < 0 || at > INT32_MAX) {
    +        return raiseError(vm, BAS_ERR_OVERFLOW);
    +    }
    +
    +    *pos = (long)at;
    +    return BAS_VM_OK;
    +}
    +
    +
     // Clamps an RGB channel value into [0, BAS_RGB_COMPONENT_MAX].
     static int32_t clampComponent(int32_t c) {
         if (c < 0) {
    @@ -7183,15 +7203,21 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
                     }
                 } else {
                     // Read/write channels: a read-ahead would need a seek before
    -                // the next write, so compare against the end instead.
    -                long curPos = ftell(fp);
    +                // the next write, so compare against the end instead (an
    +                // end past 2 GB -- negative on DOS -- is past curPos).
    +                long         curPos;
    +                BasVmResultE posRc = channelTell(vm, fp, &curPos);
    +
    +                if (posRc != BAS_VM_OK) {
    +                    return posRc;
    +                }
     
                     fseek(fp, 0, SEEK_END);
     
                     long endPos = ftell(fp);
     
                     fseek(fp, curPos, SEEK_SET);
    -                isEof = (curPos >= endPos);
    +                isEof = endPos >= 0 && curPos >= endPos;
                 }
     
                 if (!push(vm, basValBool(isEof))) {
    @@ -7480,15 +7506,23 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
                     return chResult;
                 }
     
    -            FILE *fp       = (FILE *)vm->files[channel].handle;
    -            long  savedPos = ftell(fp);
    +            FILE        *fp = (FILE *)vm->files[channel].handle;
    +            long         savedPos;
    +            long         length;
    +            BasVmResultE posRc = channelTell(vm, fp, &savedPos);
    +
    +            if (posRc != BAS_VM_OK) {
    +                return posRc;
    +            }
     
                 fseek(fp, 0, SEEK_END);
    -
    -            long length = ftell(fp);
    -
    +            posRc = channelTell(vm, fp, &length);
                 fseek(fp, savedPos, SEEK_SET);
     
    +            if (posRc != BAS_VM_OK) {
    +                return posRc;
    +            }
    +
                 if (!push(vm, basValLong((int32_t)length))) {
                     return BAS_VM_STACK_OVERFLOW;
                 }
    @@ -7506,12 +7540,21 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
                     return chResult;
                 }
     
    -            long pos = ftell((FILE *)vm->files[channel].handle);
    +            long         pos;
    +            BasVmResultE posRc = channelTell(vm, (FILE *)vm->files[channel].handle, &pos);
    +
    +            if (posRc != BAS_VM_OK) {
    +                return posRc;
    +            }
     
                 if (vm->files[channel].mode == BAS_FILE_MODE_RANDOM) {
                     pos /= vm->files[channel].recLen;
                 }
     
    +            if (pos >= INT32_MAX) {
    +                return raiseError(vm, BAS_ERR_OVERFLOW);
    +            }
    +
                 if (!push(vm, basValLong((int32_t)(pos + 1)))) {
                     return BAS_VM_STACK_OVERFLOW;
                 }
    @@ -7530,8 +7573,13 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
     
                 // QB: BINARY reports the last byte read or written, RANDOM the
                 // last record number, sequential modes the 128-byte block count.
    -            long    pos = ftell((FILE *)vm->files[channel].handle);
    -            int32_t loc;
    +            long         pos;
    +            int32_t      loc;
    +            BasVmResultE posRc = channelTell(vm, (FILE *)vm->files[channel].handle, &pos);
    +
    +            if (posRc != BAS_VM_OK) {
    +                return posRc;
    +            }
     
                 if (vm->files[channel].mode == BAS_FILE_MODE_BINARY) {
                     loc = (int32_t)pos;
    @@ -7853,7 +7901,15 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
                     return BAS_VM_FILE_ERROR;
                 }
     
    -            if (!push(vm, basValLong((int32_t)st.st_size))) {
    +            // A Long, as in VB: a file past 2 GB (FAT32 reaches 4 GB) is
    +            // Overflow
    +            uint64_t size = platformStatSize(&st);
    +
    +            if (size > INT32_MAX) {
    +                return raiseError(vm, BAS_ERR_OVERFLOW);
    +            }
    +
    +            if (!push(vm, basValLong((int32_t)size))) {
                     return BAS_VM_STACK_OVERFLOW;
                 }
     
    @@ -7967,14 +8023,15 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
                 return rc;
             }
     
    +        bool isBool = (top->type == BAS_TYPE_BOOLEAN);
             bool isLong = (top->type == BAS_TYPE_LONG);
     
             basValRelease(top);
     
    -        // A LONG operand, or a result past the INTEGER range, gives a LONG,
    -        // as execArith does -- truncation corrupts LONG masks like
    -        // NOT &H10000.
    -        *top = intResultOf(~n, isLong);
    +        // NOT of a Boolean is a Boolean; a LONG operand, or a result past
    +        // the INTEGER range, gives a LONG, as execArith does -- truncation
    +        // corrupts LONG masks like NOT &H10000.
    +        *top = logicalResult(~n, isBool, isLong);
             return BAS_VM_OK;
         }
     
    @@ -7991,6 +8048,7 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
     
         int32_t      na;
         int32_t      nb;
    +    bool         isBool = (a.type == BAS_TYPE_BOOLEAN && b.type == BAS_TYPE_BOOLEAN);
         bool         isLong = (a.type == BAS_TYPE_LONG || b.type == BAS_TYPE_LONG);
         BasVmResultE rc     = roundedOperand(vm, a, &na);
     
    @@ -8033,9 +8091,10 @@ static BasVmResultE execLogical(BasVmT *vm, uint8_t op) {
                 break;
         }
     
    -    // LONG for a LONG operand or a result past the INTEGER range, as
    -    // execArith -- truncation corrupts LONG masks like c& AND &HFF0000.
    -    if (!push(vm, intResultOf(result, isLong))) {
    +    // Boolean for two Booleans; LONG for a LONG operand or a result past
    +    // the INTEGER range, as execArith -- truncation corrupts LONG masks
    +    // like c& AND &HFF0000.
    +    if (!push(vm, logicalResult(result, isBool, isLong))) {
             return BAS_VM_STACK_OVERFLOW;
         }
     
    @@ -10603,8 +10662,9 @@ static void localNow(struct tm *out, int32_t *outUsec) {
     
     
     // AND / OR / XOR / EQV / IMP with a Null operand, as VB: False AND Null is
    -// False, True OR Null is True, False IMP Null and Null IMP True are True;
    -// everything else is Null.  Takes over both operands.
    +// False, True OR Null is True, False IMP Null and Null IMP True are True
    +// (Booleans when the other operand is one, else integers); everything
    +// else is Null.  Takes over both operands.
     static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b) {
         bool      aNull  = (a.type == BAS_TYPE_NULL);
         bool      bNull  = (b.type == BAS_TYPE_NULL);
    @@ -10617,16 +10677,20 @@ static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b
             return raiseError(vm, BAS_ERR_OVERFLOW);
         }
     
    -    bool known = !(aNull && bNull);
    +    // The known operand's type: a Boolean result for a Boolean
    +    bool             known  = !(aNull && bNull);
    +    const BasValueT *given  = aNull ? &b : &a;
    +    bool             isBool = given->type == BAS_TYPE_BOOLEAN;
    +    bool             isLong = given->type == BAS_TYPE_LONG;
     
         // True OR Null, False IMP Null and Null IMP True are all True
         bool orTrue  = op == OP_OR && other == BAS_TRUE_BITS;
         bool impTrue = op == OP_IMP && ((bNull && other == 0) || (aNull && other == BAS_TRUE_BITS));
     
         if (known && op == OP_AND && other == 0) {
    -        result = intResult(0);
    +        result = logicalResult(0, isBool, isLong);
         } else if (known && (orTrue || impTrue)) {
    -        result = intResult(BAS_TRUE_BITS);
    +        result = logicalResult(BAS_TRUE_BITS, isBool, isLong);
         }
     
         basValRelease(&a);
    @@ -10635,6 +10699,29 @@ static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b
     }
     
     
    +// The result of a logical operator (AND OR XOR EQV IMP NOT) worth r, as
    +// logicalSet makes it.
    +static BasValueT logicalResult(int32_t r, bool isBool, bool isLong) {
    +    BasValueT v = basValBool(false);
    +
    +    logicalSet(&v, r, isBool, isLong);
    +    return v;
    +}
    +
    +
    +// *v = the result of a logical operator worth r: a Boolean when every
    +// operand was one (isBool; VB: True AND False is False, NOT True is
    +// False), else an integer as fastSetInt makes it.
    +static inline void logicalSet(BasValueT *v, int32_t r, bool isBool, bool isLong) {
    +    if (isBool) {
    +        v->type    = BAS_TYPE_BOOLEAN;
    +        v->boolVal = r ? BAS_TRUE_BITS : 0;
    +    } else {
    +        fastSetInt(v, r, isLong);
    +    }
    +}
    +
    +
     // Send LPRINT output; after a failure the rest of the statement is dropped.
     static void lprintWrite(BasVmT *vm, const char *data, int32_t len) {
         if (!vm->lprintFailed && len > 0) {
    @@ -12296,8 +12383,7 @@ static BasVmResultE recordBegin(BasVmT *vm, int32_t channel, int32_t recno, long
             }
         }
     
    -    *start = ftell((FILE *)vm->files[channel].handle);
    -    return BAS_VM_OK;
    +    return channelTell(vm, (FILE *)vm->files[channel].handle, start);
     }
     
     
    @@ -12331,8 +12417,20 @@ static BasVmResultE recordEnd(BasVmT *vm, int32_t channel, long start, bool writ
         FILE *fp = (FILE *)vm->files[channel].handle;
     
         if (vm->files[channel].mode == BAS_FILE_MODE_RANDOM) {
    -        long recLen = vm->files[channel].recLen;
    -        long used   = ftell(fp) - start;
    +        long         recLen = vm->files[channel].recLen;
    +        long         now;
    +        BasVmResultE posRc  = channelTell(vm, fp, &now);
    +
    +        if (posRc != BAS_VM_OK) {
    +            return posRc;
    +        }
    +
    +        long used = now - start;
    +
    +        // The next record would start past what a Long reaches
    +        if (start > INT32_MAX - recLen) {
    +            return raiseError(vm, BAS_ERR_OVERFLOW);
    +        }
     
             if (used > recLen) {
                 runtimeError(vm, BAS_ERR_BAD_RECORD_LEN, "Record longer than LEN");
    @@ -13302,7 +13400,7 @@ fastBitwise: {
         int32_t na = fastBitsValue(a);
         int32_t nb = fastBitsValue(b);
     
    -    fastSetInt(a, (op == OP_AND) ? (na & nb) : (op == OP_OR) ? (na | nb) : (na ^ nb), a->type == BAS_TYPE_LONG || b->type == BAS_TYPE_LONG);
    +    logicalSet(a, (op == OP_AND) ? (na & nb) : (op == OP_OR) ? (na | nb) : (na ^ nb), a->type == BAS_TYPE_BOOLEAN && b->type == BAS_TYPE_BOOLEAN, a->type == BAS_TYPE_LONG || b->type == BAS_TYPE_LONG);
         FAST_BINARY_DONE();
     }
     
    @@ -13311,7 +13409,7 @@ fastNot:
             goto fastFallback;
         }
     
    -    fastSetInt(&sp[-1], ~fastBitsValue(&sp[-1]), sp[-1].type == BAS_TYPE_LONG);
    +    logicalSet(&sp[-1], ~fastBitsValue(&sp[-1]), sp[-1].type == BAS_TYPE_BOOLEAN, sp[-1].type == BAS_TYPE_LONG);
         ip++;
         FAST_NEXT();
     
    diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h
    index 7187e97b..6308fe2e 100644
    --- a/src/apps/kpunch/dvxbasic/runtime/vm.h
    +++ b/src/apps/kpunch/dvxbasic/runtime/vm.h
    @@ -55,6 +55,7 @@
     #define BAS_VM_CALL_STACK_SIZE    64     // max call nesting
     #define BAS_VM_MAX_GLOBALS        512    // global variable slots
     #define BAS_VM_MAX_LOCALS         64     // locals per stack frame
    +#define BAS_VM_MAX_MEMBER_VARS    65536  // member variables per object, variables per form (all a 16-bit opcode index can number)
     #define BAS_VM_MAX_FOR_DEPTH      32     // nested FOR loops
     #define BAS_VM_GOSUB_STACK_SIZE   256    // pending GOSUB returns (all call frames together)
     #define BAS_VM_MAX_FILES          16     // file channel array size; index 0 unused, so 15 usable channels (1..15)
    diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c
    index 9325f838..0ea24f4d 100644
    --- a/src/apps/kpunch/dvxbasic/test_compiler.c
    +++ b/src/apps/kpunch/dvxbasic/test_compiler.c
    @@ -1568,6 +1568,22 @@ static void testCompilerLimits(void) {
         }
         expectCompileError("Too many globals rejected", src, "Too many module-level variables");
     
    +    // More member variables or form variables than their 16-bit operands
    +    // address.
    +    len = snprintf(src, TEST_LIMIT_SRC_CAP, "BEGINCLASS \"Big\"\n");
    +    for (int32_t i = 0; i <= BAS_VM_MAX_MEMBER_VARS; i++) {
    +        len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int)i);
    +    }
    +    snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "ENDCLASS\n");
    +    expectCompileError("Too many member variables rejected", src, "Too many member variables in Big");
    +
    +    len = snprintf(src, TEST_LIMIT_SRC_CAP, "BEGINFORM \"F1\"\n");
    +    for (int32_t i = 0; i <= BAS_VM_MAX_MEMBER_VARS; i++) {
    +        len += snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "DIM v%d AS INTEGER\n", (int)i);
    +    }
    +    snprintf(src + len, TEST_LIMIT_SRC_CAP - len, "ENDFORM\n");
    +    expectCompileError("Too many form variables rejected", src, "Too many form-level variables");
    +
         // More locals than a call frame holds fails at compile time, not with
         // a runtime "Invalid local variable index".
         len = snprintf(src, TEST_LIMIT_SRC_CAP, "SUB S\n");
    @@ -2012,8 +2028,8 @@ static void testConstEnumParamArray(void) {
             "CONST X = 5 XOR 3\n"
             "CONST Q = (1 = 1) AND (2 <> 3)\n"
             "PRINT T; U; M; X; Q\n",
    -        // NOT and AND give numbers here as they do at run time
    -        "True 0 49 6 -1 \n"
    +        // NOT and AND of Booleans give Booleans here as they do at run time
    +        "True False 49 6 True \n"
         );
     
         runProgram("CONST string expressions and pure built-ins",
    @@ -5178,7 +5194,7 @@ static void testObjectsAndVariants(void) {
             "True 1 NullNull\n"
             "True True True True True \n"
             "ab\n"
    -        "True 0 -1 True True True \n"
    +        "True False True True True True \n"
             "no\n"
             "True True True True \n"
             "True \n"
    @@ -9923,15 +9939,39 @@ int main(void) {
         runProgram("Operator precedence",
             "' NOT has highest unary, then AND, OR, XOR, EQV, IMP\n"
             "PRINT NOT 0 AND -1\n"              // NOT 0 = -1, -1 AND -1 = -1
    -        "PRINT (1 > 0) AND (2 > 1)\n"       // True AND True = -1
    -        "PRINT (1 > 0) OR (2 < 1)\n"        // True OR False = -1
    +        "PRINT (1 > 0) AND (2 > 1)\n"       // True AND True = True (two Booleans, VB4+)
    +        "PRINT (1 > 0) OR (2 < 1)\n"        // True OR False = True
             "PRINT 5 AND 3\n"                    // bitwise: 1
             "PRINT 5 OR 3\n"                     // bitwise: 7
             "PRINT 5 XOR 3\n"                    // bitwise: 6
             "PRINT 2 + 3 * 4 - 1\n"             // 2 + 12 - 1 = 13
             "PRINT (2 + 3) * (4 - 1)\n"         // 5 * 3 = 15
             "PRINT -2 ^ 2\n",                     // -(2^2) = -4 (VB precedence)
    -        "-1 \n-1 \n-1 \n1 \n7 \n6 \n13 \n15 \n-4 \n"
    +        "-1 \nTrue \nTrue \n1 \n7 \n6 \n13 \n15 \n-4 \n"
    +    );
    +
    +    // Logical operators on Booleans give Booleans, as in VB4+ (NOT too);
    +    // with a number they give the number's type, and with Null the known
    +    // operand's
    +    runProgram("Logical operators: Booleans give Booleans",
    +        "DIM a AS BOOLEAN\n"
    +        "DIM b AS BOOLEAN\n"
    +        "DIM v AS VARIANT\n"
    +        "a = TRUE\n"
    +        "PRINT a AND b; a OR b; a XOR b; a EQV b; a IMP b; NOT a\n"
    +        "PRINT TypeName(a AND b); TypeName(NOT b); TypeName(a EQV b)\n"
    +        "PRINT TypeName(a AND 1); TypeName(a OR 70000); TypeName(NOT 1)\n"
    +        "PRINT a AND 3; NOT FALSE\n"
    +        "v = a\n"
    +        "PRINT TypeName(v OR b); TypeName(Null AND FALSE); TypeName(0 AND Null)\n"
    +        "CONST C = TRUE AND NOT FALSE\n"
    +        "PRINT C; TypeName(C)\n",
    +        "False True True False False False \n"
    +        "BooleanBooleanBoolean\n"
    +        "IntegerLongInteger\n"
    +        "3 True \n"
    +        "BooleanBooleanInteger\n"
    +        "True Boolean\n"
         );
     
         // ============================================================
    diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c
    index 37c82d68..a29cf2de 100644
    --- a/src/apps/kpunch/dvxbasic/test_suite.c
    +++ b/src/apps/kpunch/dvxbasic/test_suite.c
    @@ -78,6 +78,8 @@
     #define TEST_ENUM_WIRE_NAME   "\x06\x00" "Circle" // the member Circle on the wire: uint16 length, then the name
     #define TEST_ENUM_WIRE_LEN    8   // its bytes
     #define TEST_ENUM_WIRE_NUL    5   // a byte of the name, made NUL
    +#define TS_BIG_FILE           "ts_big.tmp"      // a sparse file past 2 GB (testBattery)
    +#define TS_BIG_SIZE           3000000000LL      // bytes: a FAT32 file may reach 4 GB - 1
     
     static CaptureT *sUiCap = NULL;
     static char      sUiRefs[UI_STUB_REF_COUNT][UI_STUB_REF_LEN];
    @@ -1333,6 +1335,22 @@ static void testClassRoundTrip(const char *name) {
             }
         }
     
    +    // More member variables than their 16-bit operands address is refused
    +    // at load (fuzzModule: a corrupt count made every New allocate
    +    // millions of fields)
    +    if (ok) {
    +        mod->classes[0].varCount = BAS_VM_MAX_MEMBER_VARS + 1;
    +
    +        int32_t     badLen  = 0;
    +        uint8_t    *badData = basModuleSerialize(mod, &badLen);
    +        BasModuleT *bad     = badData ? basModuleDeserialize(badData, badLen) : NULL;
    +
    +        ok = badData != NULL && bad == NULL;
    +        snprintf(detail, sizeof(detail), "a class with %d member variables accepted", BAS_VM_MAX_MEMBER_VARS + 1);
    +        basModuleFree(bad);
    +        free(badData);
    +    }
    +
         basModuleFree(mod);
         expectTrue(name, ok, detail);
     }
    @@ -3443,6 +3461,16 @@ static void testSerializeCorrupt(const char *name) {
         free(bigData);
         proc.localCount = 2;
     
    +    // More form variables than their 16-bit operands address is refused
    +    // at load.
    +    formVar.varCount = BAS_VM_MAX_MEMBER_VARS + 1;
    +
    +    int32_t  formLen  = 0;
    +    uint8_t *formData = basModuleSerialize(&module, &formLen);
    +
    +    expectTrue("serialize-oversized-form", formData != NULL && basModuleDeserialize(formData, formLen) == NULL, "oversized form variable count accepted");
    +    free(formData);
    +
         uint8_t *bad = malloc(len);
     
         if (!bad) {
    @@ -7149,7 +7177,7 @@ int main(void) {
         TEST_RUNTIME_ERROR("string-negative", "PRINT STRING$(-1, 65)\n", "");
         TEST_RUNTIME_ERROR("mid-bad-start", "PRINT MID$(\"abc\", 0)\n", "Illegal");
         TEST_RUNTIME_ERROR("cint-overflow", "PRINT CINT(40000)\n", "Overflow");
    -    TEST_EQ("rnd-randomize", "RANDOMIZE 7\nDIM r AS DOUBLE\nr = RND\nPRINT r >= 0 AND r < 1\n", "-1 \n");
    +    TEST_EQ("rnd-randomize", "RANDOMIZE 7\nDIM r AS DOUBLE\nr = RND\nPRINT r >= 0 AND r < 1\n", "True \n");
     
         // WRITE # quoting and separators.
         TEST_EQ("write-hash",
    @@ -7162,6 +7190,33 @@ int main(void) {
             "CLOSE #1\nKILL \"ts_write.tmp\"\n",
             "1,\"a\",2.5\n");
     
    +    // A file past 2 GB (FAT32 reaches 4 GB): FILELEN and LOF are Longs, as
    +    // in VB, and raise Overflow; positions under 2 GB still work.
    +    FILE *big = fopen(TS_BIG_FILE, "wb");
    +
    +    if (!big || ftruncate(fileno(big), (off_t)TS_BIG_SIZE) != 0) {
    +        reportFail("big-file-positions", "could not make the sparse file");
    +    } else {
    +        TEST_EQ("big-file-positions",
    +            "ON ERROR GOTO H\n"
    +            "PRINT FILELEN(\"" TS_BIG_FILE "\")\n"
    +            "OPEN \"" TS_BIG_FILE "\" FOR BINARY AS #1\n"
    +            "PRINT LOF(1)\n"
    +            "PRINT EOF(1); LOC(1); SEEK(1)\n"
    +            "CLOSE #1\n"
    +            "END\n"
    +            "H:\n"
    +            "PRINT \"err\"; ERR\n"
    +            "RESUME NEXT\n",
    +            "err6 \nerr6 \nFalse 0 1 \n");
    +    }
    +
    +    if (big) {
    +        fclose(big);
    +    }
    +
    +    remove(TS_BIG_FILE);
    +
         // Filesystem error routing.
         TEST_EQ("name-exists",
             "OPEN \"ts_a.tmp\" FOR OUTPUT AS #1\nCLOSE #1\n"
    diff --git a/src/apps/kpunch/dvxbasic/userctl.dhs b/src/apps/kpunch/dvxbasic/userctl.dhs
    index 9ab01928..ce24446d 100644
    --- a/src/apps/kpunch/dvxbasic/userctl.dhs
    +++ b/src/apps/kpunch/dvxbasic/userctl.dhs
    @@ -131,8 +131,8 @@ Inside the control's code, UserControl is the control itself: the surface it dra
       Invalidate                   Asks for UserControl_Paint at the next chance, once however often it is called before then: change many things (add a hundred items) and the control is drawn once.
       PropertyChanged name         Tells a designer that a property changed, so its property sheet shows the new value.
       Cls, Line, PSet, Circle, Print, Point, PaintPicture, DrawFocusRect, TextWidth, TextHeight, Scale, ScaleX, ScaleY   Drawing, as on a PictureBox.
    -  DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, PrintAccel, TextWidthAccel, AccelKey   Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox.
    -  CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight   Drawing properties, as on a PictureBox.
    +  DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, TextWidthAccel, AccelKey   Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox.
    +  CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, FontScale, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight   Drawing properties, as on a PictureBox.
     .endtable
     
     The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control.
    diff --git a/src/apps/kpunch/fileMgr/fileMgr.dhs b/src/apps/kpunch/fileMgr/fileMgr.dhs
    index 583f63de..4869cc8f 100644
    --- a/src/apps/kpunch/fileMgr/fileMgr.dhs
    +++ b/src/apps/kpunch/fileMgr/fileMgr.dhs
    @@ -198,7 +198,11 @@ Select the files and folders and press Del or choose File > Delete. You are aske
     
     .h2 File Names
     
    -Names follow the DOS rules: up to eight characters, optionally followed by a dot and up to three more (for example REPORT.TXT). Letters, digits and most punctuation are allowed; spaces, the characters " * + , / : ; < = > ? [ \ ] | and device names such as CON, PRN or LPT1 are not. If a name is not allowed, the File Manager tells you why.
    +What a name may be depends on the disk. When the disk takes long file names (DVX running under Windows, or a long file name driver such as DOSLFN loaded before DVX), a name can have up to 255 characters, including spaces, several dots and + , ; = [ ] (for example Letter to Mom.final.txt); it cannot end with a dot or a space. Otherwise names follow the DOS rules: up to eight characters, optionally followed by a dot and up to three more (for example REPORT.TXT), and spaces and + , ; = [ ] are not allowed. On every disk the characters " * / : < > ? \ | and device names such as CON, PRN or LPT1 are not allowed. If a name is not allowed, the File Manager tells you why.
    +
    +A new folder is first named New Folder on a disk that takes long names, NEWFOLDR on one that does not; type over it.
    +
    +Files and disks bigger than 2 GB (a FAT32 disk) show their full size.
     
     .topic filemgr.props
     .title Properties
    diff --git a/src/apps/kpunch/fileMgr/fileMgr.frm b/src/apps/kpunch/fileMgr/fileMgr.frm
    index 7de0b838..9ce23837 100644
    --- a/src/apps/kpunch/fileMgr/fileMgr.frm
    +++ b/src/apps/kpunch/fileMgr/fileMgr.frm
    @@ -322,10 +322,14 @@ CONST FM_CASCADE = 20
     CONST FM_TREE_W = 170
     
     ' File list columns, in FILEOPS_SORT_ order (a header click sorts by its
    -' column): Name, Size (right-aligned), Type, Modified; widths in characters
    -CONST FM_COLUMNS = "Name,14|Size,12,Right|Type,10|Modified,16"
    +' column): Name, Size (right-aligned; 14 holds 4,294,967,295, the largest
    +' FAT32 file, with the cell's padding), Type, Modified; widths in
    +' characters
    +CONST FM_COLUMNS = "Name,14|Size,14,Right|Type,10|Modified,16"
     
    -CONST FM_DEFAULT_FOLDER = "NEWFOLDR"
    +' A new folder's suggested name: 8.3, or long where the disk takes them
    +CONST FM_DEFAULT_FOLDER      = "NEWFOLDR"
    +CONST FM_DEFAULT_FOLDER_LONG = "New Folder"
     CONST FM_FILTER_ALL     = "*.*"
     CONST FM_CUR_DIR        = "."
     
    @@ -394,7 +398,8 @@ SUB FileMgrForm_Load
         fmList.SetMultiSelect True
         splMain.Position = FM_TREE_W
     
    -    start = COMMAND$
    +    ' The folder to show: the first argument (a long name may be quoted)
    +    start = COMMAND$(1)
     
         IF start = "" THEN
             start = CURDIR$
    @@ -1029,8 +1034,15 @@ END SUB
     
     SUB CmdNewFolder
         DIM folderName AS STRING
    +    DIM suggested AS STRING
     
    -    folderName = DlgInputBox$("New Folder", "Name of the new folder:", FM_DEFAULT_FOLDER)
    +    suggested = FM_DEFAULT_FOLDER
    +
    +    IF FileOpsLongNames(curFolder) THEN
    +        suggested = FM_DEFAULT_FOLDER_LONG
    +    END IF
    +
    +    folderName = DlgInputBox$("New Folder", "Name of the new folder:", suggested)
     
         IF DlgInputCancelled() THEN
             EXIT SUB
    @@ -1416,9 +1428,9 @@ SUB mnuProperties_Click
     END SUB
     
     
    -' Another File Manager on this folder
    +' Another File Manager on this folder (one argument, even with blanks)
     SUB mnuNewWindow_Click
    -    ShellRun ShellAppPath$(), curFolder
    +    ShellRun ShellAppPath$(), ShellArgQuote$(curFolder)
     END SUB
     
     
    diff --git a/src/apps/kpunch/terminal/terminal.dhs b/src/apps/kpunch/terminal/terminal.dhs
    index d891db46..9f90512c 100644
    --- a/src/apps/kpunch/terminal/terminal.dhs
    +++ b/src/apps/kpunch/terminal/terminal.dhs
    @@ -264,7 +264,9 @@ Start the send on the other computer, then choose Transfer > Receive Files.... X
     
     ZMODEM downloads usually start by themselves: when the other computer starts a ZMODEM send, Terminal starts receiving without you choosing anything.
     
    -Names are changed to fit DOS if needed: long names are shortened to eight letters and a three-letter extension, spaces and unusual characters are removed or replaced, and folder names in the sender's name are ignored.
    +Folder names in the sender's name are ignored. When the download folder's disk takes long file names, files keep the names the sender gives (a character a file name cannot hold becomes _). Otherwise names are changed to fit DOS: long names are shortened to eight letters and a three-letter extension, and spaces and unusual characters are removed or replaced. When you send, the other computer gets the long name of your file where the protocol has room for it.
    +
    +A file can be at most 2 GB; a larger one cannot be sent or received.
     
     .h2 The File Transfer Window
     
    diff --git a/src/apps/kpunch/welcome/welcome.dhs b/src/apps/kpunch/welcome/welcome.dhs
    index 585b5d66..351f686c 100644
    --- a/src/apps/kpunch/welcome/welcome.dhs
    +++ b/src/apps/kpunch/welcome/welcome.dhs
    @@ -30,21 +30,28 @@
     .default
     .index Welcome
     .index First Start
    +.index Tour
     .index Don't Show This Again
     
     .h1 Welcome
     
    -The Welcome window appears when DVX starts for the first time. It tells you in a few words what DVX is and how to get help.
    +The Welcome window appears when DVX starts for the first time. It shows what DVX can do and where to start: pick a tile to start a program, or take the tour.
     
    -.h2 Buttons
    +.h2 Where to Start
     
     .table
    -  Button                 What it does
    -  ------                 ------------
    -  Open the User Guide    Open the DVX help, which explains the desktop, windows, menus and the programs that come with DVX.
    -  Close                  Close the Welcome window.
    +  Tile                   What it does
    +  ----                   ------------
    +  Take the tour          Shows a short tour of DVX in the window: Program Manager, windows, files, Control Panel, DVX BASIC and help, a page at a time. Next and Previous move through it; End the tour brings the tiles back.
    +  Write a program        Starts DVX BASIC, where you draw forms and write your own programs.
    +  Make it yours          Starts Control Panel: colors, wallpaper, the mouse, sounds and more.
    +  Browse your files      Starts File Manager.
    +  See it in action       Starts the DVX Demo, which shows the windows and controls DVX has.
    +  Read the User Guide    Opens the DVX help, which explains the desktop, windows, menus and the programs that come with DVX.
     .endtable
     
    +Click a tile's picture or its button, or press Alt with the underlined letter. Under the tiles a tip about DVX changes every few seconds. Close closes the Welcome window.
    +
     Press F1 at any time, in any program, to open the help for what you are doing.
     
     .h2 Don't Show This Again
    diff --git a/src/apps/kpunch/welcome/welcome.frm b/src/apps/kpunch/welcome/welcome.frm
    index 755d87d6..197ae84b 100644
    --- a/src/apps/kpunch/welcome/welcome.frm
    +++ b/src/apps/kpunch/welcome/welcome.frm
    @@ -4,7 +4,7 @@ Begin Form WelcomeForm
         Layout    = VBox
         AutoSize  = True
         Resizable = False
    -    Centered  = False
    +    Centered  = True
         Spacing   = 8
         Begin Menu mnuHelp
             Caption = "&Help"
    @@ -19,34 +19,154 @@ Begin Form WelcomeForm
                 Caption = "&About Welcome..."
             End
         End
    -    Begin VBox boxText
    -        Begin Label lblWelcome
    -            Caption = "Welcome to DVX!"
    -        End
    -        Begin Label lblWhat
    -            Caption = "DVX is a desktop for your DOS computer."
    -        End
    -        Begin Label lblStart
    -            Caption = "Start programs from Program Manager and work in windows."
    -        End
    -        Begin Label lblHelp
    -            Caption = "Press F1 at any time for help."
    +    Begin VBox boxBanner
    +        Begin PictureBox picBanner
    +            Width       = 536
    +            Height      = 88
    +            BorderStyle = None
             End
         End
    -    Begin CheckBox chkDontShow
    -        Caption = "&Don't show this again"
    -        Value   = True
    +    Begin HBox boxStart
    +        Spacing   = 8
    +        Begin VBox colStartLeft
    +            Weight  = 100
    +            Spacing = 6
    +            Begin HBox tileTour
    +                Spacing = 6
    +                Begin ImageButton imgTour
    +                End
    +                Begin VBox boxTourTile
    +                    Weight = 100
    +                    Begin CommandButton cmdTour
    +                        Caption = "Take the &tour"
    +                    End
    +                    Begin Label lblTourTile
    +                        Caption = "A quick look around DVX"
    +                    End
    +                End
    +            End
    +            Begin HBox tileLook
    +                Spacing = 6
    +                Begin ImageButton imgLook
    +                End
    +                Begin VBox boxLookTile
    +                    Weight = 100
    +                    Begin CommandButton cmdLook
    +                        Caption = "&Make it yours"
    +                    End
    +                    Begin Label lblLookTile
    +                        Caption = "Colors, wallpaper and more"
    +                    End
    +                End
    +            End
    +            Begin HBox tileDemo
    +                Spacing = 6
    +                Begin ImageButton imgDemo
    +                End
    +                Begin VBox boxDemoTile
    +                    Weight = 100
    +                    Begin CommandButton cmdDemo
    +                        Caption = "&See it in action"
    +                    End
    +                    Begin Label lblDemoTile
    +                        Caption = "The DVX Demo shows it off"
    +                    End
    +                End
    +            End
    +        End
    +        Begin VBox colStartRight
    +            Weight  = 100
    +            Spacing = 6
    +            Begin HBox tileCode
    +                Spacing = 6
    +                Begin ImageButton imgCode
    +                End
    +                Begin VBox boxCodeTile
    +                    Weight = 100
    +                    Begin CommandButton cmdCode
    +                        Caption = "&Write a program"
    +                    End
    +                    Begin Label lblCodeTile
    +                        Caption = "Make your own in DVX BASIC"
    +                    End
    +                End
    +            End
    +            Begin HBox tileFiles
    +                Spacing = 6
    +                Begin ImageButton imgFiles
    +                End
    +                Begin VBox boxFilesTile
    +                    Weight = 100
    +                    Begin CommandButton cmdFiles
    +                        Caption = "&Browse your files"
    +                    End
    +                    Begin Label lblFilesTile
    +                        Caption = "With the File Manager"
    +                    End
    +                End
    +            End
    +            Begin HBox tileGuide
    +                Spacing = 6
    +                Begin ImageButton imgGuide
    +                End
    +                Begin VBox boxGuideTile
    +                    Weight = 100
    +                    Begin CommandButton cmdGuide
    +                        Caption = "Read the &User Guide"
    +                    End
    +                    Begin Label lblGuideTile
    +                        Caption = "Everything, explained"
    +                    End
    +                End
    +            End
    +        End
    +    End
    +    Begin VBox boxTour
    +        Spacing   = 6
    +        Visible   = False
    +        Weight    = 100
    +        Begin PictureBox picTour
    +            Width       = 536
    +            Height      = 160
    +            BorderStyle = None
    +        End
    +        Begin HBox rowTour
    +            Alignment = Right
    +            Spacing   = 8
    +            Begin Label lblPage
    +                Caption = ""
    +            End
    +            Begin CommandButton cmdPrev
    +                Caption = "< &Previous"
    +            End
    +            Begin CommandButton cmdNext
    +                Caption = "&Next >"
    +            End
    +            Begin CommandButton cmdEndTour
    +                Caption = "&End the tour"
    +            End
    +        End
    +    End
    +    Begin Label lblTip
    +        Caption = ""
         End
         Begin HBox boxButtons
    -        Alignment = Right
    -        Spacing   = 8
    -        Begin CommandButton cmdGuide
    -            Caption = "Open the &User Guide"
    +        Spacing = 8
    +        Begin CheckBox chkDontShow
    +            Caption = "&Don't show this again"
    +            Value   = True
    +        End
    +        Begin Spacer spcButtons
    +            Weight = 100
             End
             Begin CommandButton cmdClose
                 Caption = "&Close"
             End
         End
    +    Begin Timer tmrTip
    +        Interval = 6000
    +        Enabled  = True
    +    End
     End
     
     
    @@ -77,8 +197,13 @@ End
     ' time.
     '
     ' The shell starts the program DVX.INI [shell] welcome names once the
    -' desktop is up; the DVX.INI DVX ships with names this one.  The window
    -' says what DVX is and that F1 opens help, and can open the user guide.
    +' desktop is up; the DVX.INI DVX ships with names this one.  A banner
    +' (a gradient and a big title, FontScale) greets the user; six tiles
    +' start the tour, DVX BASIC, Control Panel, File Manager, the DVX Demo
    +' and the user guide; a tip below them changes every few seconds.  The
    +' tour shows its pages in place of the tiles: each a program's icon, a
    +' title and a few lines, with Previous and Next.
    +'
     ' "Don't show this again" starts ticked: closing the window then removes
     ' the setting, so the window is seen once.  Unticking it writes the setting
     ' back with this program's path, and the window comes back at every start.
    @@ -87,16 +212,75 @@ OPTION EXPLICIT
     
     CONST HELP_FILE = "welcome.hlp"
     
    -' The window opens where a WELCOME_W x WELCOME_H window would be centred,
    -' then fits its contents (as the C Welcome did).
    -CONST WELCOME_W = 480
    -CONST WELCOME_H = 240
    +' The programs the tiles start, in DVX's programs folder
    +CONST APP_BASIC   = "\kpunch\dvxbasic\dvxbasic.app"
    +CONST APP_CPANEL  = "\kpunch\cpanel\cpanel.app"
    +CONST APP_FILES   = "\kpunch\filemgr\filemgr.app"
    +CONST APP_DEMO    = "\kpunch\dvxdemo\dvxdemo.app"
    +CONST APP_HELP    = "\kpunch\dvxhelp\dvxhelp.app"
    +CONST APP_CLOCK   = "\kpunch\clock\clock.app"
    +CONST APP_NOTEPAD = "\kpunch\notepad\notepad.app"
    +CONST APP_IMAGES  = "\kpunch\imgview\imgview.app"
    +CONST APP_ICONS   = "\kpunch\iconed\iconed.app"
    +
    +' The banner's and the tour's gradient (red, green, blue at the top and
    +' at the bottom), the title's gold, the text's pale blue, the shadow
    +CONST TOP_R        = 40
    +CONST TOP_G        = 72
    +CONST TOP_B        = 184
    +CONST BOTTOM_R     = 8
    +CONST BOTTOM_G     = 16
    +CONST BOTTOM_B     = 72
    +CONST GOLD         = &H40D0FF
    +CONST WHITE        = &HFFFFFF
    +CONST PALE         = &HFFD8C8
    +CONST SHADOW       = &H000000
    +CONST PLATE_FACE   = &HC0C0C0
    +
    +' The banner: the title's size and place, the tagline under it
    +CONST TITLE_SCALE  = 3
    +CONST BANNER_X     = 16
    +CONST BANNER_Y     = 10
    +CONST BANNER_GAP   = 6
    +
    +' The tour: pages, lines a page, a title's size; the icon's plate (a
    +' raised bevel round the icon drawn ICON_SCALE times its size) and where
    +' the text starts
    +CONST TOUR_PAGES   = 7
    +CONST TOUR_LINES   = 3
    +CONST TOUR_SCALE   = 2
    +CONST TOUR_PAD     = 14
    +CONST ICON_SIZE    = 32
    +CONST ICON_SCALE   = 2
    +CONST ICON_GRID    = 2      ' a page with several icons shows ICON_GRID x ICON_GRID
    +CONST PLATE_BEVEL  = 4
    +CONST TOUR_TEXT_X  = 108
    +CONST TOUR_GAP     = 8
    +
    +CONST TIP_COUNT    = 6
    +
    +DIM SHARED tourPage AS INTEGER
    +DIM SHARED tipIndex AS INTEGER
    +DIM SHARED tourTitle(1 TO TOUR_PAGES) AS STRING
    +DIM SHARED tourIcon(1 TO TOUR_PAGES) AS STRING
    +DIM SHARED tourLine(1 TO TOUR_PAGES, 1 TO TOUR_LINES) AS STRING
    +DIM SHARED tips(1 TO TIP_COUNT) AS STRING
     
     
     SUB WelcomeForm_Load
    -    WelcomeForm.Left = (Screen.Width - WELCOME_W) \ 2
    -    WelcomeForm.Top  = (Screen.Height - WELCOME_H) \ 2
    -    cmdClose.SetFocus
    +    FillTour
    +    FillTips
    +    imgTour.Picture  = ShellAppPath$()
    +    imgCode.Picture  = ProgramPath$(APP_BASIC)
    +    imgLook.Picture  = ProgramPath$(APP_CPANEL)
    +    imgFiles.Picture = ProgramPath$(APP_FILES)
    +    imgDemo.Picture  = ProgramPath$(APP_DEMO)
    +    imgGuide.Picture = ProgramPath$(APP_HELP)
    +    picBanner.AutoRedraw = True
    +    picTour.AutoRedraw   = True
    +    DrawBanner
    +    ShowTip 1
    +    cmdTour.SetFocus
     END SUB
     
     
    @@ -120,12 +304,263 @@ SUB SaveChoice
     END SUB
     
     
    +' A program of DVX's, by its place in the programs folder
    +FUNCTION ProgramPath$(BYVAL relPath AS STRING)
    +    ProgramPath$ = ShellProgramsFolder$() + relPath
    +END FUNCTION
    +
    +
    +' The tour's pages: each a title, the program whose icon it shows (or
    +' several, separated by "|": Program Manager has no icon of its own, its
    +' page shows a few of the programs it starts) and TOUR_LINES lines.
    +SUB FillTour
    +    SetPage 1, "A quick tour of DVX", ShellAppPath$(), "DVX turns your DOS computer into a desktop of", "windows, menus and icons. This tour shows the", "main parts. Press Next to begin."
    +    SetPage 2, "Program Manager", ProgramPath$(APP_CLOCK) + "|" + ProgramPath$(APP_NOTEPAD) + "|" + ProgramPath$(APP_IMAGES) + "|" + ProgramPath$(APP_ICONS), "Your programs live in groups. Double-click an", "icon to start it, or type its name in Search.", "File > New Group makes groups of your own."
    +    SetPage 3, "Windows", ProgramPath$(APP_DEMO), "Every program runs in a window. Drag the title", "bar to move it, or an edge to change its size.", "Alt+Tab switches windows; Alt+F4 closes one."
    +    SetPage 4, "Your files", ProgramPath$(APP_FILES), "File Manager shows your disks and folders. Copy,", "move, rename and delete files with the mouse or", "the keys; double-click a file to open it."
    +    SetPage 5, "Make it yours", ProgramPath$(APP_CPANEL), "Control Panel sets colors and themes, wallpaper,", "the mouse, sounds, joysticks, printers and the", "screen. Most changes show right away."
    +    SetPage 6, "Write your own programs", ProgramPath$(APP_BASIC), "DVX BASIC is all you need to build programs: draw", "a form, add buttons and lists, write some code", "and press F5. Make an .app to share it."
    +    SetPage 7, "Help is always there", ProgramPath$(APP_HELP), "Press F1 in any program for help about it. The", "Help Viewer holds every guide, with an index and", "a search. Have fun with DVX!"
    +END SUB
    +
    +
    +SUB SetPage(BYVAL page AS INTEGER, BYVAL title AS STRING, BYVAL icon AS STRING, BYVAL line1 AS STRING, BYVAL line2 AS STRING, BYVAL line3 AS STRING)
    +    tourTitle(page)   = title
    +    tourIcon(page)    = icon
    +    tourLine(page, 1) = line1
    +    tourLine(page, 2) = line2
    +    tourLine(page, 3) = line3
    +END SUB
    +
    +
    +SUB FillTips
    +    tips(1) = "Did you know? Press F1 in any program for help about it."
    +    tips(2) = "Did you know? Alt+Tab switches between your windows."
    +    tips(3) = "Did you know? Type in Program Manager's Search box to find a program."
    +    tips(4) = "Did you know? Ctrl+Esc opens the Task Manager."
    +    tips(5) = "Did you know? Drag a window's edge or corner to change its size."
    +    tips(6) = "Did you know? Control Panel changes colors, wallpaper and the mouse."
    +END SUB
    +
    +
    +' A blue gradient over the whole of pic, top to bottom.
    +SUB DrawGradient(BYVAL pic AS Control)
    +    DIM y AS LONG
    +    DIM h AS LONG
    +
    +    h = pic.ScaleHeight
    +
    +    FOR y = 0 TO h - 1
    +        pic.Line (0, y)-(pic.ScaleWidth - 1, y), RGB(TOP_R + (BOTTOM_R - TOP_R) * y \ h, TOP_G + (BOTTOM_G - TOP_G) * y \ h, TOP_B + (BOTTOM_B - TOP_B) * y \ h)
    +    NEXT
    +END SUB
    +
    +
    +' Text scale times the font's size in colour at x, y, over a black shadow
    +' scale dots down and to the right.
    +SUB ShadowPrint(BYVAL pic AS Control, BYVAL x AS LONG, BYVAL y AS LONG, BYVAL text AS STRING, BYVAL scale AS INTEGER, BYVAL colour AS LONG)
    +    pic.FontScale = scale
    +    pic.ForeColor = SHADOW
    +    pic.CurrentX  = x + scale
    +    pic.CurrentY  = y + scale
    +    pic.Print text;
    +    pic.ForeColor = colour
    +    pic.CurrentX  = x
    +    pic.CurrentY  = y
    +    pic.Print text;
    +    pic.FontScale = 1
    +END SUB
    +
    +
    +SUB DrawBanner
    +    DrawGradient picBanner
    +    ShadowPrint picBanner, BANNER_X, BANNER_Y, "Welcome to DVX!", TITLE_SCALE, GOLD
    +    picBanner.ForeColor = PALE
    +    picBanner.CurrentX  = BANNER_X
    +    picBanner.CurrentY  = BANNER_Y + picBanner.TextHeight("A") * TITLE_SCALE + BANNER_GAP
    +    picBanner.Print "Your desktop for DOS. Here is where to start."
    +END SUB
    +
    +
    +' Tour page page: the gradient, the program's icon on its plate, the
    +' title and its lines (the plate and the text each centred top to
    +' bottom); Previous and Next where there is somewhere to go.
    +SUB ShowTour(BYVAL page AS INTEGER)
    +    DIM i AS INTEGER
    +    DIM side AS INTEGER
    +    DIM iconTop AS INTEGER
    +    DIM textTop AS INTEGER
    +
    +    tourPage = page
    +    side     = ICON_SIZE * ICON_SCALE
    +    iconTop  = (picTour.ScaleHeight - side) \ 2
    +    textTop  = (picTour.ScaleHeight - picTour.TextHeight("A") * (TOUR_SCALE + TOUR_LINES) - TOUR_GAP) \ 2
    +    DrawGradient picTour
    +    picTour.DrawBevel TOUR_PAD - PLATE_BEVEL, iconTop - PLATE_BEVEL, side + PLATE_BEVEL * 2, side + PLATE_BEVEL * 2, False, PLATE_FACE
    +    DrawIcons tourIcon(page), TOUR_PAD, iconTop
    +    ShadowPrint picTour, TOUR_TEXT_X, textTop, tourTitle(page), TOUR_SCALE, WHITE
    +    picTour.ForeColor = PALE
    +    picTour.CurrentY  = textTop + picTour.TextHeight("A") * TOUR_SCALE + TOUR_GAP
    +
    +    FOR i = 1 TO TOUR_LINES
    +        picTour.CurrentX = TOUR_TEXT_X
    +        picTour.Print tourLine(page, i)
    +    NEXT
    +
    +    lblPage.Caption = "Page " + LTRIM$(STR$(page)) + " of " + LTRIM$(STR$(TOUR_PAGES))
    +    cmdPrev.Enabled = page > 1
    +    cmdNext.Enabled = page < TOUR_PAGES
    +END SUB
    +
    +
    +' A page's icons on the plate at x, y: one, ICON_SCALE times its size, or
    +' up to ICON_GRID x ICON_GRID (their paths separated by "|") at their own
    +' size.
    +SUB DrawIcons(BYVAL list AS STRING, BYVAL x AS LONG, BYVAL y AS LONG)
    +    DIM count AS INTEGER
    +    DIM bar AS INTEGER
    +    DIM path AS STRING
    +
    +    IF INSTR(list, "|") = 0 THEN
    +        PaintIcon list, x, y, ICON_SIZE * ICON_SCALE
    +        EXIT SUB
    +    END IF
    +
    +    DO WHILE list <> "" AND count < ICON_GRID * ICON_GRID
    +        bar = INSTR(list, "|")
    +
    +        IF bar = 0 THEN
    +            path = list
    +            list = ""
    +        ELSE
    +            path = LEFT$(list, bar - 1)
    +            list = MID$(list, bar + 1)
    +        END IF
    +
    +        PaintIcon path, x + (count MOD ICON_GRID) * ICON_SIZE, y + (count \ ICON_GRID) * ICON_SIZE, ICON_SIZE
    +        count = count + 1
    +    LOOP
    +END SUB
    +
    +
    +' One program's icon at x, y, side dots square; a file without one is left out.
    +SUB PaintIcon(BYVAL path AS STRING, BYVAL x AS LONG, BYVAL y AS LONG, BYVAL side AS LONG)
    +    ON ERROR RESUME NEXT
    +    picTour.PaintPicture LoadPicture(path), x, y, side, side
    +END SUB
    +
    +
    +' The tour in place of the tiles, or the tiles back.  The window keeps the
    +' tiles' size; boxTour (weighted) takes the tour's spare height below its
    +' buttons, so the tip and the buttons under it stay put.
    +SUB ShowStart(BYVAL touring AS BOOLEAN)
    +    boxStart.Visible = NOT touring
    +    boxTour.Visible  = touring
    +
    +    IF touring THEN
    +        ShowTour 1
    +        cmdNext.SetFocus
    +    ELSE
    +        cmdTour.SetFocus
    +    END IF
    +END SUB
    +
    +
    +SUB ShowTip(BYVAL index AS INTEGER)
    +    tipIndex       = index
    +    lblTip.Caption = tips(index)
    +END SUB
    +
    +
    +' Start one of DVX's programs (DVX says why when it cannot).
    +SUB StartProgram(BYVAL relPath AS STRING)
    +    IF ShellRun(ProgramPath$(relPath), "") = 0 THEN
    +        ShellLog "Welcome: could not start " + relPath
    +    END IF
    +END SUB
    +
    +
    +SUB tmrTip_Timer
    +    ShowTip tipIndex MOD TIP_COUNT + 1
    +END SUB
    +
    +
    +SUB cmdTour_Click
    +    ShowStart True
    +END SUB
    +
    +
    +SUB imgTour_Click
    +    ShowStart True
    +END SUB
    +
    +
    +SUB cmdCode_Click
    +    StartProgram APP_BASIC
    +END SUB
    +
    +
    +SUB imgCode_Click
    +    StartProgram APP_BASIC
    +END SUB
    +
    +
    +SUB cmdLook_Click
    +    StartProgram APP_CPANEL
    +END SUB
    +
    +
    +SUB imgLook_Click
    +    StartProgram APP_CPANEL
    +END SUB
    +
    +
    +SUB cmdFiles_Click
    +    StartProgram APP_FILES
    +END SUB
    +
    +
    +SUB imgFiles_Click
    +    StartProgram APP_FILES
    +END SUB
    +
    +
    +SUB cmdDemo_Click
    +    StartProgram APP_DEMO
    +END SUB
    +
    +
    +SUB imgDemo_Click
    +    StartProgram APP_DEMO
    +END SUB
    +
    +
     SUB cmdGuide_Click
         ' No file name: the DVX user guide
         HelpView ""
     END SUB
     
     
    +SUB imgGuide_Click
    +    HelpView ""
    +END SUB
    +
    +
    +SUB cmdPrev_Click
    +    ShowTour tourPage - 1
    +END SUB
    +
    +
    +SUB cmdNext_Click
    +    ShowTour tourPage + 1
    +END SUB
    +
    +
    +SUB cmdEndTour_Click
    +    ShowStart False
    +END SUB
    +
    +
     SUB cmdClose_Click
         Unload WelcomeForm
     END SUB
    diff --git a/src/include/basic/fileops.bas b/src/include/basic/fileops.bas
    index 4d0ac0df..cafd0da4 100644
    --- a/src/include/basic/fileops.bas
    +++ b/src/include/basic/fileops.bas
    @@ -116,6 +116,17 @@ DECLARE LIBRARY "fileops"
         ' protected floppy or a folder that does not exist).
         DECLARE FUNCTION FileOpsIsWritable(BYVAL folder AS STRING) AS LONG
     
    +    ' True when the drive holding path takes long file names (a long-name
    +    ' driver such as DOSLFN, or Windows, is running); without them names
    +    ' are 8.3.
    +    DECLARE FUNCTION FileOpsLongNames(BYVAL path AS STRING) AS LONG
    +
    +    ' The short (8.3) form of an existing file or folder, for a program
    +    ' that cannot take long names or blanks (an old DOS program's command
    +    ' line): every part's short name where the drive has long names, else
    +    ' path itself; "" when it does not fit.
    +    DECLARE FUNCTION FileOpsShortPath$(BYVAL path AS STRING) AS STRING
    +
         ' True when DVX itself runs from a disk it cannot write (a CD-ROM):
         ' settings cannot be saved in its folder then.
         DECLARE FUNCTION FileOpsReadOnlySystem() AS LONG
    @@ -159,8 +170,9 @@ DECLARE LIBRARY "fileops"
     
         ' ---- Paths and names ----
     
    -    ' The canonical full path of an existing folder (DOS: upper case, the
    -    ' drive's current folder for "C:"), or "" when it is not one.
    +    ' The canonical full path of an existing folder (DOS: upper case, but
    +    ' long names keep their case; the drive's current folder for "C:"), or
    +    ' "" when it is not one.
         DECLARE FUNCTION FileOpsPathNormalize$(BYVAL path AS STRING) AS STRING
     
         ' The folder holding path, or "" at a drive or file system root.
    @@ -214,10 +226,18 @@ DECLARE LIBRARY "fileops"
         DECLARE FUNCTION FileOpsTempPath$(BYVAL path AS STRING) AS STRING
         DECLARE FUNCTION FileOpsReplaceFile(BYVAL tmpPath AS STRING, BYVAL path AS STRING) AS LONG
     
    -    ' True when itemName can be used as a file name on this system (on
    -    ' DOS: 8.3 names, no device names such as CON, no characters a disk
    -    ' does not allow).  When not, FileOpsError$ says why.
    -    DECLARE FUNCTION FileOpsValidName(BYVAL itemName AS STRING) AS LONG
    +    ' True when itemName can be the name of a new file or folder in folder:
    +    ' on DOS a long name (up to 255 characters, spaces and several dots
    +    ' allowed) where the folder's disk takes long names (FileOpsLongNames),
    +    ' else an 8.3 name; never a device name such as CON or a character the
    +    ' disk does not allow.  When not, FileOpsError$ says why.
    +    DECLARE FUNCTION FileOpsValidName(BYVAL folder AS STRING, BYVAL itemName AS STRING) AS LONG
    +
    +    ' The two DOS name rules by themselves, whatever the disk takes: a
    +    ' long name (FileOpsValidLongName) or an 8.3 name
    +    ' (FileOpsValidShortName).  When not valid, FileOpsError$ says why.
    +    DECLARE FUNCTION FileOpsValidLongName(BYVAL itemName AS STRING) AS LONG
    +    DECLARE FUNCTION FileOpsValidShortName(BYVAL itemName AS STRING) AS LONG
     
         ' Make the folder path and every folder above it that is missing
         ' (MKDIR makes one).  True when the folder exists afterwards.
    diff --git a/src/include/basic/shell.bas b/src/include/basic/shell.bas
    index 20b032a3..008c096c 100644
    --- a/src/include/basic/shell.bas
    +++ b/src/include/basic/shell.bas
    @@ -353,6 +353,11 @@ DECLARE LIBRARY "dvxshell"
         DECLARE FUNCTION ShellArgCount(BYVAL commandLine AS STRING) AS LONG
         DECLARE FUNCTION ShellArg$(BYVAL commandLine AS STRING, BYVAL index AS LONG) AS STRING
     
    +    ' text as one word of a command line: in double quotes when it has a
    +    ' blank (a long name such as C:\My Files), so ShellArg$ and COMMAND$(n)
    +    ' read it back whole.  Use it to build the args of ShellRun.
    +    DECLARE FUNCTION ShellArgQuote$(BYVAL text AS STRING) AS STRING
    +
         ' ---- This program and DVX ----
     
         ' This program's id (see ShellRunningId).
    diff --git a/src/include/basic/xfer.bas b/src/include/basic/xfer.bas
    index d891f718..d85e74c0 100644
    --- a/src/include/basic/xfer.bas
    +++ b/src/include/basic/xfer.bas
    @@ -141,8 +141,9 @@ DECLARE LIBRARY "xfer"
         DECLARE FUNCTION XferCrc16(BYVAL bytes AS BINARY STRING) AS LONG
         DECLARE FUNCTION XferCrc32(BYVAL bytes AS BINARY STRING) AS LONG
     
    -    ' A file name made safe to store: an 8.3 name with no folder or
    -    ' device name and only characters every disk accepts (what a receive
    -    ' does with the names a sender gives).
    -    DECLARE FUNCTION XferSafeName$(BYVAL fileName AS STRING) AS STRING
    +    ' A file name made safe to store in folder, as a receive stores the
    +    ' names a sender gives: no folder part or device name; a long name with
    +    ' the characters a long name cannot hold made _ where folder's disk
    +    ' takes long names, else an 8.3 name of characters every disk accepts.
    +    DECLARE FUNCTION XferSafeName$(BYVAL fileName AS STRING, BYVAL folder AS STRING) AS STRING
     END DECLARE
    diff --git a/src/installer/setup.c b/src/installer/setup.c
    index 89eb8829..1ebf6ce8 100644
    --- a/src/installer/setup.c
    +++ b/src/installer/setup.c
    @@ -30,6 +30,13 @@
     // (the SETUP.EXE installed in the DVX folder) it offers only Uninstall of
     // its own folder.
     //
    +// The window sits on a splash backdrop drawn into the desktop's wallpaper
    +// buffer: a blue gradient with a large shadowed DVX title and tagline
    +// (drawTextScaled; no picture files on the disks).  While files are
    +// unpacked, billboards about what DVX does take turns under the progress
    +// bar, and the done page celebrates; both are a small widget class of
    +// Setup's own (board*).
    +//
     //   SETUP                      the wizard
     //   SETUP /AUTO=C:\DVX         install there without questions (licence
     //                              taken as accepted, settings kept on an
    @@ -52,8 +59,10 @@
     
     #include "dvxApp.h"
     #include "dvxDlg.h"
    +#include "dvxDraw.h"
     #include "dvxPlat.h"
    -#include "dvxWgt.h"
    +#include "dvxVideo.h"
    +#include "dvxWgtP.h"
     #include "box/box.h"
     #include "button/button.h"
     #include "checkbox/checkbox.h"
    @@ -84,7 +93,8 @@
     
     #define SETUP_TITLE        "DVX Setup"
     #define SETUP_WIN_W        520
    -#define SETUP_WIN_H        380
    +#define SETUP_WIN_H        330      // under the splash title on a 640 x 480 screen
    +#define SETUP_WIN_GAP      10       // between the splash title and the window
     #define SETUP_SPACING      6
     #define SETUP_SLICE_MS     100
     #define SETUP_DEST_FOLDER  "DVX"
    @@ -97,6 +107,27 @@
     #define SETUP_SHOT_UPGRADE "SU-UPGR.BMP"
     #define SETUP_DISK_POLL_MS 250
     
    +// Billboards (board*): each shows SETUP_BOARD_MS while files are unpacked
    +#define SETUP_BOARD_MS     4000
    +#define SETUP_BOARD_LINES  3
    +#define SETUP_BOARD_PAD    10       // inside a billboard's edges
    +#define SETUP_BOARD_GAP    6        // between its headline and its lines
    +#define SETUP_BOARD_SCALE  2        // a copy-page headline's size (drawTextScaled)
    +#define SETUP_DONE_SCALE   3        // the done page's headline size
    +#define SETUP_PERCENT      100      // sparkle positions are percentages of the board
    +#define SETUP_SPARKLE_CORE 3        // the square at a sparkle's centre, pixels across
    +
    +// The splash backdrop
    +#define SPLASH_TITLE       "DVX"
    +#define SPLASH_TAGLINE     "The DOS Visual eXecutive"
    +#define SPLASH_SUBLINE     "Windows, programs and games for your DOS computer"
    +#define SPLASH_MARGIN      24       // the title's distance from the screen's top-left corner
    +#define SPLASH_TITLE_SCALE 6
    +#define SPLASH_TAG_SCALE   2
    +#define SPLASH_SHADOW      4        // the title's shadow offset, pixels
    +#define SPLASH_TAG_DROP    20       // the tagline's top below the title's
    +#define SPLASH_SUB_GAP     12       // between the tagline and the line under it
    +
     // The widgets linked in (each module's wgtRegister, renamed by the
     // Makefile)
     void wgtRegisterBox(void);
    @@ -116,6 +147,35 @@ typedef enum {
         PageCountE
     } PageE;
     
    +// A colour as red, green, blue (packColor makes the display's pixel)
    +typedef struct {
    +    uint8_t r;
    +    uint8_t g;
    +    uint8_t b;
    +} SetupRgbT;
    +
    +// One billboard: a headline (scale times the font's size) and lines
    +typedef struct {
    +    const char *headline;
    +    const char *lines[SETUP_BOARD_LINES];
    +    int32_t     scale;
    +    bool        sparkles;
    +} SetupBoardT;
    +
    +// A billboard widget's data: the boards it shows in turn
    +typedef struct {
    +    const SetupBoardT *boards;
    +    int32_t            count;
    +    int32_t            current;
    +} BoardDataT;
    +
    +// A sparkle on the done page's board: where (percent of the board) and how big
    +typedef struct {
    +    int32_t xPct;
    +    int32_t yPct;
    +    int32_t radius;
    +} SetupSparkleT;
    +
     typedef struct {
         AppContextT     ctx;
         WindowT        *win;
    @@ -127,6 +187,7 @@ typedef struct {
         WidgetT        *autoexec;           // "start with the computer" (NULL without a boot drive)
         WidgetT        *status;
         WidgetT        *bar;
    +    WidgetT        *copyBoard;          // the progress page's billboards
         WidgetT        *doneText;           // where DVX is now
         WidgetT        *doneRun;            // how to start it
         WidgetT        *startNow;
    @@ -169,14 +230,58 @@ static const char *const sShotNames[PageCountE] = { "SU-LIC.BMP", "SU-FOLD.BMP",
     // /SHOTS: each disk prompt's picture (PkgDiskProblemE order)
     static const char *const sDiskShotNames[] = { "SU-DISK.BMP", "SU-OTHER.BMP", "SU-WRONG.BMP", "SU-BAD.BMP" };
     
    +// The splash's and the billboards' colours
    +static const SetupRgbT sSplashTop    = { 24, 48, 168 };
    +static const SetupRgbT sSplashBottom = { 0, 0, 32 };
    +static const SetupRgbT sBoardTop     = { 40, 72, 184 };
    +static const SetupRgbT sBoardBottom  = { 8, 16, 72 };
    +static const SetupRgbT sGold         = { 255, 208, 64 };
    +static const SetupRgbT sWhite        = { 255, 255, 255 };
    +static const SetupRgbT sPale         = { 200, 216, 255 };
    +static const SetupRgbT sShadow       = { 0, 0, 0 };
    +
    +// What DVX does, one billboard at a time while files are unpacked
    +static const SetupBoardT sCopyBoards[] = {
    +    { "Windows, your way",       { "Move, size, stack and tile windows with the mouse or keys.", "Program Manager starts everything; Alt+Tab switches.", "Themes and wallpaper make the desktop your own." }, SETUP_BOARD_SCALE, false },
    +    { "Build your own programs", { "DVX BASIC: draw a form, add controls, write some code.", "Press F5 to run it, then make it a program of its own.", "Forms, graphics, files, sound and databases built in." }, SETUP_BOARD_SCALE, false },
    +    { "Games, sound and music",  { "Sound effects and music while you work or play.", "Joysticks and game pads, from BASIC and from C.", "Full-screen mode gives a game the whole display." }, SETUP_BOARD_SCALE, false },
    +    { "Help on every screen",    { "Press F1 anywhere for help about what you see.", "The Help Viewer holds every guide, and searches them.", "" }, SETUP_BOARD_SCALE, false },
    +    { "Files and the world",     { "File Manager copies, moves and opens your files.", "Terminal, networking and file transfer connect you.", "" }, SETUP_BOARD_SCALE, false }
    +};
    +
    +// The done page's celebration
    +static const SetupBoardT sDoneBoard = { "DVX is ready!", { "Your new desktop is waiting.", "", "" }, SETUP_DONE_SCALE, true };
    +
    +// The sparkles take these colours in turn
    +static const SetupRgbT *const sSparkleColors[] = { &sGold, &sWhite };
    +
    +static const SetupSparkleT sSparkles[] = {
    +    { 70, 22, 5 },
    +    { 84, 60, 7 },
    +    { 93, 18, 3 },
    +    { 62, 72, 3 },
    +    { 78, 34, 3 },
    +    { 91, 82, 4 }
    +};
    +
    +static int32_t sBoardType = -1;
    +
     // ============================================================
     // Prototypes
     // ============================================================
     
     static int32_t  ask(const char *msg, int32_t flags, const char *shot, int32_t disk);
     static bool     askDisk(void *user, int32_t disk, PkgDiskProblemE problem);
    +static void     boardCalcMinSize(WidgetT *w, const BitmapFontT *font);
    +static WidgetT *boardCreate(WidgetT *parent, const SetupBoardT *boards, int32_t count);
    +static void     boardDestroy(WidgetT *w);
    +static void     boardNext(WidgetT *w);
    +static void     boardPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
     static void     buildPages(WidgetT *root);
     static WidgetT *buttonRow(WidgetT *page);
    +static void     drawShadowed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, int32_t offset, const SetupRgbT *color);
    +static void     drawSparkle(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, int32_t radius, uint32_t color);
    +static void     fillGradient(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const SetupRgbT *top, const SetupRgbT *bottom);
     static void     installAutoexec(const char *destDir);
     static bool     installCheck(const char *destDir, bool *upgrade, bool *keepSettings);
     static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInstall);
    @@ -192,6 +297,7 @@ static void     onInstall(WidgetT *w);
     static void     onNext(WidgetT *w);
     static void     onUninstall(WidgetT *w);
     static void     onUninstallAsk(WidgetT *w);
    +static void     paintBackdrop(void);
     static void     parseArgs(int argc, char *argv[]);
     static void     report(const char *msg);
     static void     shootFile(const char *name);
    @@ -203,6 +309,17 @@ static void     startDvx(void);
     static void     uninstallRun(void);
     int             main(int argc, char *argv[]);
     
    +// A billboard: paints itself, takes no input
    +static const WidgetClassT sClassBoard = {
    +    .version  = WGT_CLASS_VERSION,
    +    .flags    = 0,
    +    .handlers = {
    +        [WGT_METHOD_PAINT]         = (void *)boardPaint,
    +        [WGT_METHOD_CALC_MIN_SIZE] = (void *)boardCalcMinSize,
    +        [WGT_METHOD_DESTROY]       = (void *)boardDestroy,
    +    }
    +};
    +
     
     // A question in a message box.  /SHOTS: onAskIdle saves it as shot and
     // answers it with its first button (Yes / OK); a disk prompt (disk > 0)
    @@ -265,6 +382,95 @@ static bool askDisk(void *user, int32_t disk, PkgDiskProblemE problem) {
     }
     
     
    +// Wide enough for the widest headline or line of any of its boards, tall
    +// enough for the tallest board.
    +static void boardCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    +    const BoardDataT *b = (const BoardDataT *)w->data;
    +
    +    w->calcMinW = 0;
    +    w->calcMinH = 0;
    +
    +    for (int32_t i = 0; i < b->count; i++) {
    +        const SetupBoardT *board = &b->boards[i];
    +        int32_t            textW = textWidth(font, board->headline) * board->scale;
    +        int32_t            rows  = 0;
    +
    +        for (int32_t l = 0; l < SETUP_BOARD_LINES; l++) {
    +            textW = DVX_MAX(textW, textWidth(font, board->lines[l]));
    +            rows += board->lines[l][0] ? 1 : 0;
    +        }
    +
    +        w->calcMinW = DVX_MAX(w->calcMinW, textW + SETUP_BOARD_PAD * 2);
    +        w->calcMinH = DVX_MAX(w->calcMinH, SETUP_BOARD_PAD * 2 + font->charHeight * board->scale + SETUP_BOARD_GAP + rows * font->charHeight);
    +    }
    +}
    +
    +
    +// A billboard showing boards[0] (of count, taken in turn by its owner).
    +static WidgetT *boardCreate(WidgetT *parent, const SetupBoardT *boards, int32_t count) {
    +    WidgetT *w = widgetAlloc(parent, sBoardType);
    +
    +    if (!w) {
    +        return NULL;
    +    }
    +
    +    BoardDataT *b = (BoardDataT *)calloc(1, sizeof(BoardDataT));
    +
    +    if (!b) {
    +        widgetAllocRollback(w);
    +        return NULL;
    +    }
    +
    +    b->boards = boards;
    +    b->count  = count;
    +    w->data   = b;
    +    return w;
    +}
    +
    +
    +static void boardDestroy(WidgetT *w) {
    +    free(w->data);
    +    w->data = NULL;
    +}
    +
    +
    +// The next of a billboard's boards (after the last, the first again).
    +static void boardNext(WidgetT *w) {
    +    BoardDataT *b = (BoardDataT *)w->data;
    +
    +    b->current = (b->current + 1) % b->count;
    +    wgtInvalidatePaint(w);
    +}
    +
    +
    +// The board shown: a gradient, the shadowed headline, its lines and, on
    +// the done page, sparkles.
    +static void boardPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    +    (void)colors;
    +    const BoardDataT  *b     = (const BoardDataT *)w->data;
    +    const SetupBoardT *board = &b->boards[b->current];
    +    int32_t            x     = w->x + SETUP_BOARD_PAD;
    +    int32_t            y     = w->y + SETUP_BOARD_PAD;
    +    uint32_t           pale  = packColor(d, sPale.r, sPale.g, sPale.b);
    +
    +    fillGradient(d, ops, w->x, w->y, w->w, w->h, &sBoardTop, &sBoardBottom);
    +    drawShadowed(d, ops, font, x, y, board->headline, board->scale, board->scale, board->sparkles ? &sGold : &sWhite);
    +    y += font->charHeight * board->scale + SETUP_BOARD_GAP;
    +
    +    for (int32_t l = 0; l < SETUP_BOARD_LINES && board->lines[l][0]; l++) {
    +        drawText(d, ops, font, x, y, board->lines[l], pale, pale, false);
    +        y += font->charHeight;
    +    }
    +
    +    for (int32_t i = 0; board->sparkles && i < (int32_t)DVX_ARRAY_LEN(sSparkles); i++) {
    +        const SetupSparkleT *sp    = &sSparkles[i];
    +        const SetupRgbT     *color = sSparkleColors[i % (int32_t)DVX_ARRAY_LEN(sSparkleColors)];
    +
    +        drawSparkle(d, ops, w->x + w->w * sp->xPct / SETUP_PERCENT, w->y + w->h * sp->yPct / SETUP_PERCENT, sp->radius, packColor(d, color->r, color->g, color->b));
    +    }
    +}
    +
    +
     static void buildPages(WidgetT *root) {
         char text[SETUP_MSG_MAX];
         char boot = platformBootDrive();
    @@ -278,7 +484,7 @@ static void buildPages(WidgetT *root) {
         // Licence
         WidgetT *p = sSetup.pages[PageLicenceE];
     
    -    wgtLabel(p, "Welcome to DVX Setup. Setup installs DVX on your hard disk.");
    +    wgtLabel(p, "Welcome! Setup puts DVX on your hard disk in a few minutes.");
         wgtLabel(p, "Please read the licence:");
     
         WidgetT *licence = wgtTextArea(p, (int32_t)sizeof(sSetupLicense));
    @@ -324,13 +530,15 @@ static void buildPages(WidgetT *root) {
         // Progress
         p             = sSetup.pages[PageCopyE];
         wgtLabel(p, "Setup is installing DVX. This can take a few minutes.");
    -    sSetup.status = wgtLabel(p, "");
    -    sSetup.bar    = wgtProgressBar(p);
    -    row           = buttonRow(p);
    +    sSetup.status    = wgtLabel(p, "");
    +    sSetup.bar       = wgtProgressBar(p);
    +    sSetup.copyBoard = boardCreate(p, sCopyBoards, (int32_t)DVX_ARRAY_LEN(sCopyBoards));
    +    row              = buttonRow(p);
         wgtButton(row, "&Cancel")->onClick = onCancel;
     
         // Done
         p               = sSetup.pages[PageDoneE];
    +    boardCreate(p, &sDoneBoard, 1);
         sSetup.doneText = wgtLabel(p, "");
         sSetup.doneRun  = wgtLabel(p, "");
         sSetup.startNow = wgtCheckbox(p, "Start &DVX now");
    @@ -363,6 +571,37 @@ static WidgetT *buttonRow(WidgetT *page) {
     }
     
     
    +// Text scale times the font's size in color over a black shadow offset
    +// pixels down and to the right.
    +static void drawShadowed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, int32_t offset, const SetupRgbT *color) {
    +    drawTextScaled(d, ops, font, x + offset, y + offset, text, scale, packColor(d, sShadow.r, sShadow.g, sShadow.b));
    +    drawTextScaled(d, ops, font, x, y, text, scale, packColor(d, color->r, color->g, color->b));
    +}
    +
    +
    +// A four-pointed star: a one-pixel cross radius pixels each way from its
    +// centre, with a small square at the middle.
    +static void drawSparkle(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, int32_t radius, uint32_t color) {
    +    rectFill(d, ops, cx - radius, cy, radius * 2 + 1, 1, color);
    +    rectFill(d, ops, cx, cy - radius, 1, radius * 2 + 1, color);
    +    rectFill(d, ops, cx - SETUP_SPARKLE_CORE / 2, cy - SETUP_SPARKLE_CORE / 2, SETUP_SPARKLE_CORE, SETUP_SPARKLE_CORE, color);
    +}
    +
    +
    +// A vertical gradient from top to bottom, a row at a time.
    +static void fillGradient(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const SetupRgbT *top, const SetupRgbT *bottom) {
    +    int32_t span = DVX_MAX(h - 1, 1);
    +
    +    for (int32_t row = 0; row < h; row++) {
    +        uint8_t r = (uint8_t)(top->r + (bottom->r - top->r) * row / span);
    +        uint8_t g = (uint8_t)(top->g + (bottom->g - top->g) * row / span);
    +        uint8_t b = (uint8_t)(top->b + (bottom->b - top->b) * row / span);
    +
    +        rectFill(d, ops, x, y + row, w, 1, packColor(d, r, g, b));
    +    }
    +}
    +
    +
     // Show the exact line, ask, then back up AUTOEXEC.BAT and add it.
     static void installAutoexec(const char *destDir) {
         char path[DVX_MAX_PATH];
    @@ -448,6 +687,8 @@ static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInst
         PkgJobT       *job        = pkgJobCreate(&sSetup.src, destDir, askDisk, keepSettings ? keepSetting : NULL, NULL);
         PlatformTicksT slice      = PLATFORM_MS_TO_TICKS(SETUP_SLICE_MS);
         PlatformTicksT sliceStart = platformClock();
    +    PlatformTicksT boardTime  = PLATFORM_MS_TO_TICKS(SETUP_BOARD_MS);
    +    PlatformTicksT boardStart = sliceStart;
         PkgStepE       step       = PkgStepErrorE;
     
         if (!job) {
    @@ -468,6 +709,11 @@ static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInst
             }
     
             if (platformClock() - sliceStart >= slice) {
    +            if (sSetup.copyBoard && platformClock() - boardStart >= boardTime) {
    +                boardNext(sSetup.copyBoard);
    +                boardStart = platformClock();
    +            }
    +
                 showProgress(job);
                 dvxUpdate(&sSetup.ctx);
                 sliceStart = platformClock();
    @@ -709,6 +955,36 @@ static void onUninstallAsk(WidgetT *w) {
     }
     
     
    +// The splash: the desktop's wallpaper buffer, made the screen's size in its
    +// pixel format, gets the gradient, the shadowed title and the tagline.
    +// Called before the first frame (dvxInit has the whole screen redrawn);
    +// without the memory for it the desktop stays plain.
    +static void paintBackdrop(void) {
    +    AppContextT *ctx   = &sSetup.ctx;
    +    DisplayT     d     = ctx->display;
    +    const char  *title = SPLASH_TITLE;
    +    uint8_t     *buf   = (uint8_t *)malloc((size_t)d.pitch * (size_t)d.height);
    +
    +    if (!buf) {
    +        return;
    +    }
    +
    +    d.backBuf = buf;
    +    resetClipRect(&d);
    +    fillGradient(&d, &ctx->blitOps, 0, 0, d.width, d.height, &sSplashTop, &sSplashBottom);
    +    drawShadowed(&d, &ctx->blitOps, &ctx->font, SPLASH_MARGIN, SPLASH_MARGIN, title, SPLASH_TITLE_SCALE, SPLASH_SHADOW, &sGold);
    +
    +    int32_t tagX = SPLASH_MARGIN * 2 + textWidth(&ctx->font, title) * SPLASH_TITLE_SCALE;
    +    int32_t tagY = SPLASH_MARGIN + SPLASH_TAG_DROP;
    +
    +    drawShadowed(&d, &ctx->blitOps, &ctx->font, tagX, tagY, SPLASH_TAGLINE, SPLASH_TAG_SCALE, SPLASH_TAG_SCALE, &sWhite);
    +    drawText(&d, &ctx->blitOps, &ctx->font, tagX, tagY + ctx->font.charHeight * SPLASH_TAG_SCALE + SPLASH_SUB_GAP, SPLASH_SUBLINE, packColor(&d, sPale.r, sPale.g, sPale.b), 0, false);
    +    free(ctx->wallpaperBuf);
    +    ctx->wallpaperBuf   = buf;
    +    ctx->wallpaperPitch = d.pitch;
    +}
    +
    +
     static void parseArgs(int argc, char *argv[]) {
         for (int32_t i = 1; i < argc; i++) {
             if (strncasecmp(argv[i], SETUP_ARG_AUTO, strlen(SETUP_ARG_AUTO)) == 0) {
    @@ -796,7 +1072,8 @@ static void showUninstall(void) {
     static void startDvx(void) {
         char exe[DVX_MAX_PATH];
     
    -    if (!fileOpsPathJoin(exe, sizeof(exe), sSetup.destDir, SETUP_EXE_NAME) || platformChdir(sSetup.destDir) != 0) {
    +    // By its short path: DOS starts a program by an 8.3 name
    +    if (!setupAutoexecLine(sSetup.destDir, exe, sizeof(exe)) || platformChdir(sSetup.destDir) != 0) {
             return;
         }
     
    @@ -862,6 +1139,7 @@ int main(int argc, char *argv[]) {
         wgtRegisterProgress();
         wgtRegisterSpacer();
         wgtRegisterTextInput();
    +    sBoardType = wgtRegisterClass(&sClassBoard);
         pkgFind(sSetup.exePath, &sSetup.src);
         dvxLog("Setup: %s, package %s (%d disk(s), %d files)", sSetup.exePath, sFoundNames[sSetup.src.found], (int)sSetup.src.info.diskCount, (int)sSetup.src.info.fileCount);
     
    @@ -879,7 +1157,9 @@ int main(int argc, char *argv[]) {
             return SETUP_EXIT_FAILED;
         }
     
    -    sSetup.win = dvxCreateWindowCentered(&sSetup.ctx, SETUP_TITLE, SETUP_WIN_W, SETUP_WIN_H, false);
    +    // The window under the splash's title, centred across the screen
    +    paintBackdrop();
    +    sSetup.win = dvxCreateWindow(&sSetup.ctx, SETUP_TITLE, (sSetup.ctx.display.width - SETUP_WIN_W) / 2, SPLASH_MARGIN + sSetup.ctx.font.charHeight * SPLASH_TITLE_SCALE + SETUP_WIN_GAP, SETUP_WIN_W, SETUP_WIN_H, false);
     
         if (!sSetup.win) {
             dvxShutdown(&sSetup.ctx);
    diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c
    index 925ff2a6..4da9dd90 100644
    --- a/src/installer/setupCore.c
    +++ b/src/installer/setupCore.c
    @@ -244,8 +244,12 @@ bool setupAutoexecHasLine(const char *path, const char *line) {
     }
     
     
    +// DVX.EXE by the short path of destDir: AUTOEXEC.BAT runs before any
    +// long-name driver, and COMMAND.COM cannot take a name with blanks.
     bool setupAutoexecLine(const char *destDir, char *out, int32_t outSize) {
    -    return fileOpsPathJoin(out, outSize, destDir, SETUP_EXE_NAME);
    +    char shortDir[DVX_MAX_PATH];
    +
    +    return platformShortPath(destDir, shortDir, sizeof(shortDir)) && fileOpsPathJoin(out, outSize, shortDir, SETUP_EXE_NAME);
     }
     
     
    diff --git a/src/installer/setupCore.h b/src/installer/setupCore.h
    index 85acc40d..a3c1c565 100644
    --- a/src/installer/setupCore.h
    +++ b/src/installer/setupCore.h
    @@ -70,8 +70,10 @@ bool     setupAutoexecAdd(const char *path, const char *line, char *err, int32_t
     // case ignored).
     bool     setupAutoexecHasLine(const char *path, const char *line);
     
    -// The AUTOEXEC.BAT line that starts DVX from destDir: destDir\DVX.EXE (the
    -// loader changes to its own folder first).  False when it does not fit.
    +// The AUTOEXEC.BAT line that starts DVX from destDir: destDir\DVX.EXE by
    +// destDir's short path (AUTOEXEC.BAT runs before a long-name driver; the
    +// loader changes to its own folder first).  Setup starts DVX by it too.
    +// False when it does not fit.
     bool     setupAutoexecLine(const char *destDir, char *out, int32_t outSize);
     
     // Path of AUTOEXEC.BAT on the boot drive ("C:\AUTOEXEC.BAT").  False when
    diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs
    index 0ed3d611..4320bac3 100644
    --- a/src/libs/kpunch/dvxshell/dvxshell.dhs
    +++ b/src/libs/kpunch/dvxshell/dvxshell.dhs
    @@ -831,7 +831,7 @@ bool shellAssocCommand(const char *documentPath, char *program, int32_t programS
     bool shellAssocOpenDocument(AppContextT *ctx, const char *documentPath);
     .endcode
     
    -shellAssocCommand builds the program and argument string that open a document: the value is split at the first comma, every %1 in the arguments becomes documentPath, and without arguments documentPath alone is passed (in double quotes when it contains a blank, so dvxNextArg and COMMAND$(1) still return it whole). A program that opens documents takes the first argument of its DxeAppContextT.args (dvxNextArg) as the file to open when it starts; a BASIC program uses COMMAND$(1). shellAssocOpenDocument does that and starts the program; false when there is no association or the program does not start. It shows nothing to the user (shellRunProgram does, for a document it cannot open).
    +shellAssocCommand builds the program and argument string that open a document: the value is split at the first comma, every %1 in the arguments becomes documentPath, and without arguments documentPath alone is passed. The path goes in double quotes when it contains a blank (dvxArgAppend), so dvxNextArg and COMMAND$(1) still return a long name such as C:\My Documents\Letter.txt whole; a template that already quotes the token ("%1") gets the path as it is. A program that opens documents takes the first argument of its DxeAppContextT.args (dvxNextArg) as the file to open when it starts; a BASIC program uses COMMAND$(1). shellAssocOpenDocument does that and starts the program; false when there is no association or the program does not start. It shows nothing to the user (shellRunProgram does, for a document it cannot open).
     
     .h2 Changing
     
    @@ -885,6 +885,7 @@ shellAssocSet adds or replaces an association (programPath may carry ,arguments)
     .index ShellCommandArgs
     .index ShellArg
     .index ShellArgCount
    +.index ShellArgQuote
     .index ShellChangeCount
     .index ShellTaskManager
     .index ShellSystemInfo
    @@ -982,12 +983,13 @@ const char *ShellCommandProgram(const char *command);
     const char *ShellCommandArgs(const char *command);
     int32_t     ShellArgCount(const char *command);
     const char *ShellArg(const char *command, int32_t index);
    +const char *ShellArgQuote(const char *arg);
     int32_t     ShellAppId(void);
     int32_t     ShellPriority(void);
     int32_t     ShellSetPriority(int32_t priority);
     .endcode
     
    -ShellCommandProgram and ShellCommandArgs are the two parts of shellSplitCommand (what shellRunCommand runs). ShellArgCount and ShellArg walk a command line with dvxNextArg -- the tokenizer shellSplitCommand and COMMAND$(n) use -- counting the program as argument 0; ShellArg drops the quotes and gives "" past the last. ShellAppId is ctx->currentAppId. ShellPriority and ShellSetPriority are tsGetPriority / tsSetPriority on the running program's mainTaskId (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH, shell.bas SHELL_PRIORITY_*); -1 / False for a program without a task of its own (every BASIC program has one) or a priority out of range.
    +ShellCommandProgram and ShellCommandArgs are the two parts of shellSplitCommand (what shellRunCommand runs). ShellArgCount and ShellArg walk a command line with dvxNextArg -- the tokenizer shellSplitCommand and COMMAND$(n) use -- counting the program as argument 0; ShellArg drops the quotes and gives "" past the last. ShellArgQuote is the other direction (dvxArgAppend): arg as one word of a command line, in double quotes when it holds blanks, "" when it is too long for one (SHELL_ARGS_MAX). ShellAppId is ctx->currentAppId. ShellPriority and ShellSetPriority are tsGetPriority / tsSetPriority on the running program's mainTaskId (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH, shell.bas SHELL_PRIORITY_*); -1 / False for a program without a task of its own (every BASIC program has one) or a priority out of range.
     
     .h2 Hotkeys
     
    diff --git a/src/libs/kpunch/dvxshell/shellAssoc.c b/src/libs/kpunch/dvxshell/shellAssoc.c
    index 0f48e739..6770190d 100644
    --- a/src/libs/kpunch/dvxshell/shellAssoc.c
    +++ b/src/libs/kpunch/dvxshell/shellAssoc.c
    @@ -117,12 +117,10 @@ bool shellAssocCommand(const char *documentPath, char *program, int32_t programS
     
         args[0] = '\0';
     
    +    // The document is one argument (quoted when its path has blanks)
    +    // unless the template already quotes the token ("%1")
         if (!argsTmpl || !argsTmpl[0]) {
    -        if (strpbrk(documentPath, DVX_ARG_BLANKS)) {
    -            pos = dvxStrAppendf(args, argsSize, pos, "%c%s%c", DVX_ARG_QUOTE, documentPath, DVX_ARG_QUOTE);
    -        } else {
    -            pos = dvxStrAppendf(args, argsSize, pos, "%s", documentPath);
    -        }
    +        pos = dvxArgAppend(args, argsSize, pos, documentPath);
         } else {
             for (const char *p = argsTmpl; *p && pos >= 0;) {
                 const char *hit = strstr(p, SHELL_ASSOC_DOC_TOKEN);
    @@ -132,7 +130,10 @@ bool shellAssocCommand(const char *documentPath, char *program, int32_t programS
                     break;
                 }
     
    -            pos = dvxStrAppendf(args, argsSize, pos, "%.*s%s", (int)(hit - p), p, documentPath);
    +            bool quoted = hit > argsTmpl && hit[-1] == DVX_ARG_QUOTE;
    +
    +            pos = dvxStrAppendf(args, argsSize, pos, "%.*s", (int)(hit - p), p);
    +            pos = quoted ? dvxStrAppendf(args, argsSize, pos, "%s", documentPath) : dvxArgAppend(args, argsSize, pos, documentPath);
                 p   = hit + tokenLen;
             }
         }
    diff --git a/src/libs/kpunch/dvxshell/shellBas.c b/src/libs/kpunch/dvxshell/shellBas.c
    index b7f3a12a..81a0c42c 100644
    --- a/src/libs/kpunch/dvxshell/shellBas.c
    +++ b/src/libs/kpunch/dvxshell/shellBas.c
    @@ -110,6 +110,7 @@ int32_t                 ShellAppMemory(int32_t appId);
     const char             *ShellAppPath(void);
     const char             *ShellArg(const char *command, int32_t index);
     int32_t                 ShellArgCount(const char *command);
    +const char             *ShellArgQuote(const char *arg);
     const char             *ShellAssocCommand(const char *document);
     int32_t                 ShellAssocCount(void);
     const char             *ShellAssocExt(int32_t index);
    @@ -444,6 +445,20 @@ int32_t ShellArgCount(const char *command) {
     }
     
     
    +// arg as one word of a command line (dvxArgAppend): in double quotes when
    +// it holds blanks, so ShellArg$ and COMMAND$(n) read it back whole; ""
    +// when it is too long for one.
    +const char *ShellArgQuote(const char *arg) {
    +    static char quoted[SHELL_ARGS_MAX];
    +
    +    if (dvxArgAppend(quoted, (int32_t)sizeof(quoted), 0, arg ? arg : "") < 0) {
    +        quoted[0] = '\0';
    +    }
    +
    +    return quoted;
    +}
    +
    +
     // The program that opens document, without the association's argument
     // template ("" when none).
     const char *ShellAssocCommand(const char *document) {
    diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c
    index ffdd3f06..daee8b4c 100644
    --- a/src/libs/kpunch/dvxshell/shellMain.c
    +++ b/src/libs/kpunch/dvxshell/shellMain.c
    @@ -537,10 +537,18 @@ static void f1HelpHandler(void *ctx) {
                 SHELL_APP_CALL(app, dc->onHelpQuery(dc->helpQueryCtx));
             }
     
    -        if (dc->helpFile[0] && dc->helpTopic[0]) {
    -            snprintf(args, sizeof(args), "%s %s", dc->helpFile, dc->helpTopic);
    -        } else if (dc->helpFile[0]) {
    -            snprintf(args, sizeof(args), "%s", dc->helpFile);
    +        // The help file is one argument (quoted when its path has blanks)
    +        if (dc->helpFile[0]) {
    +            int32_t pos = dvxArgAppend(args, sizeof(args), 0, dc->helpFile);
    +
    +            if (pos >= 0 && dc->helpTopic[0]) {
    +                pos = dvxStrAppendf(args, sizeof(args), pos, " ");
    +                pos = dvxArgAppend(args, sizeof(args), pos, dc->helpTopic);
    +            }
    +
    +            if (pos < 0) {
    +                args[0] = '\0';
    +            }
             }
         }
     
    diff --git a/src/libs/kpunch/fileops/fileOps.c b/src/libs/kpunch/fileops/fileOps.c
    index 1603ae1e..7f06121a 100644
    --- a/src/libs/kpunch/fileops/fileOps.c
    +++ b/src/libs/kpunch/fileops/fileOps.c
    @@ -71,6 +71,9 @@
     #define NEW_DIR_MODE      0777
     #define NO_PARENT         (-1)
     
    +// fileOpsPathNormalize: the "C" of "C:\..." (upper case on every disk)
    +#define DOS_DRIVE_LETTER_LEN 1
    +
     DVX_ABI_STAMP(fileops);
     
     typedef enum {
    @@ -191,7 +194,7 @@ static bool addItem(FileOpsJobT *job, const char *src, const char *dst, int32_t
             return false;
         }
     
    -    return appendItem(job, src, dst, parent, S_ISDIR(st.st_mode), (uint64_t)st.st_size);
    +    return appendItem(job, src, dst, parent, S_ISDIR(st.st_mode), platformStatSize(&st));
     }
     
     
    @@ -569,7 +572,7 @@ void fileOpsListSort(FileOpsEntryT *entries, int32_t count, FileOpsSortE col, bo
     bool fileOpsMakeDir(const char *dir, const char *name, char *err, int32_t errSize) {
         char path[DVX_MAX_PATH];
     
    -    if (!fileOpsValidateName(name, err, errSize)) {
    +    if (!fileOpsValidateName(dir, name, err, errSize)) {
             return false;
         }
     
    @@ -689,6 +692,14 @@ bool fileOpsPathNormalize(const char *path, char *out, int32_t outSize) {
             return false;
         }
     
    +#ifdef __DJGPP__
    +    // DOS: upper case, except the names on a disk with long names, which
    +    // keep the case they were given (the drive letter is upper either way)
    +    int32_t upperTo = platformLongNames(canon) ? DOS_DRIVE_LETTER_LEN : (int32_t)strlen(canon);
    +#else
    +    int32_t upperTo = 0;
    +#endif
    +
         for (int32_t i = 0; canon[i]; i++) {
             char c = canon[i];
     
    @@ -696,10 +707,7 @@ bool fileOpsPathNormalize(const char *path, char *out, int32_t outSize) {
                 c = DVX_PATH_SEP[0];
             }
     
    -#ifdef __DJGPP__
    -        c = (char)toupper((unsigned char)c);
    -#endif
    -        out[i] = c;
    +        out[i]     = (i < upperTo) ? (char)toupper((unsigned char)c) : c;
             out[i + 1] = '\0';
         }
     
    @@ -751,7 +759,7 @@ bool fileOpsRename(const char *dir, const char *oldName, const char *newName, ch
         char        newPath[DVX_MAX_PATH];
         struct stat st;
     
    -    if (!fileOpsValidateName(newName, err, errSize)) {
    +    if (!fileOpsValidateName(dir, newName, err, errSize)) {
             return false;
         }
     
    @@ -802,8 +810,8 @@ void fileOpsTypeName(const FileOpsEntryT *e, char *out, int32_t outSize) {
     }
     
     
    -bool fileOpsValidateName(const char *name, char *err, int32_t errSize) {
    -    const char *problem = platformValidateFilename(name);
    +bool fileOpsValidateName(const char *dir, const char *name, char *err, int32_t errSize) {
    +    const char *problem = platformValidateFilename(dir, name);
     
         if (problem) {
             snprintf(err, errSize, "%s", problem);
    @@ -1379,7 +1387,7 @@ static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int3
         }
     
         if (!S_ISDIR(st.st_mode)) {
    -        *bytes += (uint64_t)st.st_size;
    +        *bytes += platformStatSize(&st);
             (*files)++;
             return true;
         }
    diff --git a/src/libs/kpunch/fileops/fileOps.h b/src/libs/kpunch/fileops/fileOps.h
    index 7e1ca9e0..2ada02b4 100644
    --- a/src/libs/kpunch/fileops/fileOps.h
    +++ b/src/libs/kpunch/fileops/fileOps.h
    @@ -207,7 +207,8 @@ bool         fileOpsPathIsRoot(const char *path);
     bool         fileOpsPathJoin(char *out, int32_t outSize, const char *dir, const char *name);
     
     // Canonical absolute form of an existing folder (native separators; on
    -// DOS upper case).  False when path is not an existing folder.
    +// DOS upper case, except names on a disk with long names, which keep
    +// their case).  False when path is not an existing folder.
     bool         fileOpsPathNormalize(const char *path, char *out, int32_t outSize);
     
     // Parent folder of path.  False at a root.
    @@ -227,8 +228,10 @@ bool         fileOpsTreeSize(const char *path, uint64_t *bytes, int32_t *files,
     // Display type of an entry: "Folder", "Program", "TXT File", "File".
     void         fileOpsTypeName(const FileOpsEntryT *e, char *out, int32_t outSize);
     
    -// Check a new file or folder name.  False with a reason in err.
    -bool         fileOpsValidateName(const char *name, char *err, int32_t errSize);
    +// Check a new file or folder name for folder dir, by the rules of its
    +// disk (platformValidateFilename: long names where the drive takes them,
    +// else 8.3 on DOS).  False with a reason in err.
    +bool         fileOpsValidateName(const char *dir, const char *name, char *err, int32_t errSize);
     
     // ------------------------------------------------------------
     // Jobs
    @@ -299,7 +302,7 @@ const char  *FileOpsDriveList(void);
     int32_t      FileOpsDriveType(const char *drive);
     
     // Why the last FileOpsMakeDir / FileOpsRename / FileOpsReplaceFile /
    -// FileOpsValidName failed.
    +// FileOpsValidName / FileOpsValidLongName / FileOpsValidShortName failed.
     const char  *FileOpsError(void);
     
     const char  *FileOpsExtension(const char *name);
    @@ -350,6 +353,10 @@ const char  *FileOpsListRows(int32_t list);
     double       FileOpsListSize(int32_t list, int32_t index);
     void         FileOpsListSort(int32_t list, int32_t column, int32_t descending);
     
    +// True when the drive holding path takes long file names
    +// (platformLongNames).
    +int32_t      FileOpsLongNames(const char *path);
    +
     int32_t      FileOpsMakeDir(const char *dir, const char *name);
     
     // Make path and every missing folder above it (True when it exists then).
    @@ -386,6 +393,9 @@ int32_t      FileOpsSetAttr(const char *path, int32_t attr);
     // Modification time from FileOpsModified's text (fileOpsParseDate).
     int32_t      FileOpsSetModified(const char *path, const char *stamp);
     
    +// platformShortPath; "" when it does not fit.
    +const char  *FileOpsShortPath(const char *path);
    +
     // platformTempDir.
     const char  *FileOpsTempDir(void);
     const char  *FileOpsTempPath(const char *path);
    @@ -400,8 +410,12 @@ int32_t      FileOpsTreeFiles(void);
     int32_t      FileOpsTreeFolders(void);
     int32_t      FileOpsTreeSize(const char *path);
     
    -// True when name is a valid file name here (fileOpsValidateName);
    -// FileOpsError$ gives the reason when it is not.
    -int32_t      FileOpsValidName(const char *name);
    +// FileOpsValidName: a valid name for a new file or folder in folder
    +// (fileOpsValidateName); ValidLongName / ValidShortName: the long-name or
    +// 8.3 rules alone (platformValidateLongName / ShortName), whatever the
    +// disk takes.  FileOpsError$ gives the reason when a name is not valid.
    +int32_t      FileOpsValidLongName(const char *name);
    +int32_t      FileOpsValidName(const char *folder, const char *name);
    +int32_t      FileOpsValidShortName(const char *name);
     
     #endif // FILE_OPS_H
    diff --git a/src/libs/kpunch/fileops/fileOpsBas.c b/src/libs/kpunch/fileops/fileOpsBas.c
    index c9444e60..a35bef53 100644
    --- a/src/libs/kpunch/fileops/fileOpsBas.c
    +++ b/src/libs/kpunch/fileops/fileOpsBas.c
    @@ -86,7 +86,7 @@ typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn);
     
     static BasHandleT *sHandles       = NULL;   // stb_ds; handle = index + 1
     static bool        sCleanupHooked = false;
    -static char        sError[FILEOPS_MSG_MAX]; // why the last MakeDir / Rename / ReplaceFile / ValidName failed
    +static char        sError[FILEOPS_MSG_MAX]; // why the last MakeDir / Rename / ReplaceFile / Valid* failed
     static char       *sText          = NULL;   // stb_ds: the last string result
     static uint64_t    sTreeBytes     = 0;      // the last FileOpsTreeSize
     static int32_t     sTreeFiles     = 0;
    @@ -106,6 +106,7 @@ static FileOpsJobT         *jobOf(int32_t job);
     static bool                 jobProgress(int32_t job, FileOpsProgressT *p);
     static const FileOpsEntryT *listEntry(int32_t list, int32_t index);
     static BasHandleT          *listOf(int32_t list);
    +static int32_t              nameResult(const char *problem);
     static int32_t              runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll);
     static void                 textAppend(const char *s);
     static const char          *textOf(const char *s);
    @@ -543,6 +544,11 @@ void FileOpsListSort(int32_t list, int32_t column, int32_t descending) {
     }
     
     
    +int32_t FileOpsLongNames(const char *path) {
    +    return platformLongNames(path) ? BAS_TRUE : BAS_FALSE;
    +}
    +
    +
     int32_t FileOpsMakeDir(const char *dir, const char *name) {
         sError[0] = '\0';
     
    @@ -668,6 +674,15 @@ int32_t FileOpsSetModified(const char *path, const char *stamp) {
     }
     
     
    +// The short (8.3) form of an existing path (platformShortPath): path
    +// itself where its drive has no long names, "" when it does not fit.
    +const char *FileOpsShortPath(const char *path) {
    +    char shortPath[DVX_MAX_PATH];
    +
    +    return (path && platformShortPath(path, shortPath, sizeof(shortPath))) ? textOf(shortPath) : "";
    +}
    +
    +
     // The folder for scratch files (the TEMP or TMP setting), "" when
     // neither is set.
     const char *FileOpsTempDir(void) {
    @@ -715,13 +730,27 @@ int32_t FileOpsTreeSize(const char *path) {
     }
     
     
    -// True when name can be used as a file name on this system; when not,
    -// FileOpsError$ says why (on DOS: 8.3 names, no device names such as CON,
    -// no characters a disk does not allow).
    -int32_t FileOpsValidName(const char *name) {
    +// The DOS long-name rules by themselves, whatever the disk takes
    +// (platformValidateLongName); FileOpsError$ says why not.
    +int32_t FileOpsValidLongName(const char *name) {
    +    return nameResult(platformValidateLongName(name));
    +}
    +
    +
    +// True when name can be a new file or folder's name in folder; when not,
    +// FileOpsError$ says why (on DOS: long names where folder's drive takes
    +// them, else 8.3; never a device name such as CON or a character the
    +// disk does not allow).
    +int32_t FileOpsValidName(const char *folder, const char *name) {
         sError[0] = '\0';
     
    -    return fileOpsValidateName(name, sError, sizeof(sError)) ? BAS_TRUE : BAS_FALSE;
    +    return fileOpsValidateName(folder, name, sError, sizeof(sError)) ? BAS_TRUE : BAS_FALSE;
    +}
    +
    +
    +// The 8.3 rules by themselves (platformValidateShortName).
    +int32_t FileOpsValidShortName(const char *name) {
    +    return nameResult(platformValidateShortName(name));
     }
     
     
    @@ -819,6 +848,14 @@ static BasHandleT *listOf(int32_t list) {
     }
     
     
    +// True for a name rule that found no problem; else False, with the
    +// problem kept for FileOpsError$.
    +static int32_t nameResult(const char *problem) {
    +    snprintf(sError, sizeof(sError), "%s", problem ? problem : "");
    +    return problem ? BAS_FALSE : BAS_TRUE;
    +}
    +
    +
     // Run one job over one source; True only when nothing failed.
     static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll) {
         FileOpsJobT *job = fileOpsJobCreate(op, destDir);
    diff --git a/src/libs/kpunch/fileops/fileOpsSys.c b/src/libs/kpunch/fileops/fileOpsSys.c
    index aa11dbac..d4ad71d7 100644
    --- a/src/libs/kpunch/fileops/fileOpsSys.c
    +++ b/src/libs/kpunch/fileops/fileOpsSys.c
    @@ -26,9 +26,12 @@
     // DOS: DOS services are reached through DPMI function 0300h (simulate a
     // real-mode interrupt) issued directly with INT 31h, and path arguments
     // travel in the DJGPP transfer buffer.  Both are available to a module
    -// without extra loader exports.  A disk error (drive not ready, write
    -// protected) fails the call through the platform's permanent
    -// critical-error handler (platformInit).  Drives and free space come from
    +// without extra loader exports.  On a drive with long names
    +// (platformLongNames) the attribute calls take their long-name form, and
    +// findfirst / open use the C library's (the old calls take 8.3 names
    +// only).  Sizes are DOS's unsigned 32 bits.  A disk error (drive not
    +// ready, write protected) fails the call through the platform's
    +// permanent critical-error handler (platformInit).  Drives and free space come from
     // the platform layer (platformDriveList, platformDiskSpace).
     //
     // Host: POSIX.
    @@ -55,6 +58,9 @@
     
     #define DOS_FN_GET_ATTR        0x4300
     #define DOS_FN_SET_ATTR        0x4301
    +#define DOS_FN_LFN_ATTR        0x7143  // the long-name form of 43xxh: BL 0 get, 1 set
    +#define DOS_LFN_ATTR_GET       0x00
    +#define DOS_LFN_ATTR_SET       0x01
     #define DOS_FN_SET_FILE_TIME   0x5701
     #define CPU_FLAG_CF            0x0001
     
    @@ -97,12 +103,29 @@
     // ============================================================
     
     #ifdef __DJGPP__
    +static void   attrCall(const char *path, bool set, __dpmi_regs *r);
     static bool   dosInt(uint32_t intNo, __dpmi_regs *r);
     static time_t dosTime(uint16_t date, uint16_t time);
     static bool   putPath(const char *path, __dpmi_regs *r);
     #endif
     
     
    +#ifdef __DJGPP__
    +// The INT 21h attribute call for path: 7143h where the drive takes long
    +// names (43xxh takes only short ones there), else 4300h / 4301h.
    +static void attrCall(const char *path, bool set, __dpmi_regs *r) {
    +    memset(r, 0, sizeof(*r));
    +
    +    if (platformLongNames(path)) {
    +        r->x.ax = DOS_FN_LFN_ATTR;
    +        r->h.bl = set ? DOS_LFN_ATTR_SET : DOS_LFN_ATTR_GET;
    +    } else {
    +        r->x.ax = set ? DOS_FN_SET_ATTR : DOS_FN_GET_ATTR;
    +    }
    +}
    +#endif
    +
    +
     #ifdef __DJGPP__
     // Simulate a real-mode interrupt (DPMI 0300h) with r.  False when the
     // DPMI call itself failed; DOS errors come back in r->x.flags.
    @@ -148,8 +171,7 @@ bool fileOpsGetAttr(const char *path, uint8_t *attr) {
     #ifdef __DJGPP__
         __dpmi_regs r;
     
    -    memset(&r, 0, sizeof(r));
    -    r.x.ax = DOS_FN_GET_ATTR;
    +    attrCall(path, false, &r);
     
         if (!putPath(path, &r)) {
             return false;
    @@ -190,8 +212,7 @@ bool fileOpsSetAttr(const char *path, uint8_t attr) {
     #ifdef __DJGPP__
         __dpmi_regs r;
     
    -    memset(&r, 0, sizeof(r));
    -    r.x.ax = DOS_FN_SET_ATTR;
    +    attrCall(path, true, &r);
         r.x.cx = attr & FILEOPS_ATTR_EDITABLE;
     
         if (!putPath(path, &r)) {
    @@ -304,7 +325,7 @@ bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *
     
             e.name  = name;
             e.isDir = (ff.ff_attrib & FA_DIREC) != 0;
    -        e.size  = e.isDir ? 0 : (uint64_t)ff.ff_fsize;
    +        e.size  = e.isDir ? 0 : (uint32_t)ff.ff_fsize;  // DOS sizes are unsigned (up to 4 GB - 1)
             e.mtime = dosTime(ff.ff_fdate, ff.ff_ftime);
             more    = fn(&e, ctx);
         }
    @@ -335,7 +356,7 @@ bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *
     
             e.name  = ent->d_name;
             e.isDir = S_ISDIR(st.st_mode);
    -        e.size  = e.isDir ? 0 : (uint64_t)st.st_size;
    +        e.size  = e.isDir ? 0 : platformStatSize(&st);
             e.mtime = st.st_mtime;
             more    = fn(&e, ctx);
         }
    diff --git a/src/libs/kpunch/fileops/fileops.dhs b/src/libs/kpunch/fileops/fileops.dhs
    index 0f65f23b..31e95255 100644
    --- a/src/libs/kpunch/fileops/fileops.dhs
    +++ b/src/libs/kpunch/fileops/fileops.dhs
    @@ -46,6 +46,10 @@ Loaded as: bin/libs/kpunch/fileops/fileops.lib
     
     On DOS, touching a drive with no disk (or a write-protected or removed one) raises a critical error, which would normally wait at an "Abort, Retry, Fail" prompt on the hidden text screen. DVX installs a critical-error handler at startup (platformInit) that answers Fail, so every call here, and your own disk code, simply gets an error (errno) instead. platformDriveList never touches the media at all (IOCTL calls only), so listing drives costs nothing even with empty drives.
     
    +.h2 Big Disks and Long File Names
    +
    +Disks past 2 GB report their real size and free space: platformDiskSpace asks DOS with the FAT32 call first and falls back to the older one, which stops near 2 GB. File sizes are 64-bit (FileOpsEntryT.size, fileOpsTreeSize, the job byte counts); a FAT32 file reaches 4 GB - 1 byte, and platformStatSize reads it correctly where stat's st_size would be negative. Where a long-name driver is running (Windows, DOSLFN) and covers the drive (platformLongNames), listings show long names, jobs copy and move them, attributes and renames work on them, fileOpsValidateName accepts them and fileOpsPathNormalize keeps their case; elsewhere the 8.3 rules apply.
    +
     .h2 Attributes and Dates
     
     .table
    @@ -71,13 +75,13 @@ On DOS, touching a drive with no disk (or a write-protected or removed one) rais
       fileOpsTreeSize(path, &bytes, &files, &folders)       Total size of a file or of a folder and everything in it.
       fileOpsMakeDir(dir, name, err, errSize)               Create a folder; a reason in err on failure.
       fileOpsRename(dir, oldName, newName, err, errSize)    Rename within a folder; refuses a name already in use.
    -  fileOpsValidateName(name, err, errSize)               Check a new name (8.3 rules and device names on DOS).
    +  fileOpsValidateName(dir, name, err, errSize)          Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name.
       fileOpsPathJoin(out, size, dir, name)                 dir + separator + name; false when it does not fit.
       fileOpsPathParent(path, out, size)                    Parent folder; false at a root.
       fileOpsPathIsRoot(path)                               True for "C:\", "C:" or "/".
       fileOpsPathIsInside(path, dir)                        True when path is dir or below it.
       fileOpsPathSame(a, b)                                 Same place (separators equal, DOS ignores case, a trailing separator ignored).
    -  fileOpsPathNormalize(path, out, size)                 Canonical absolute form of an existing folder.
    +  fileOpsPathNormalize(path, out, size)                 Canonical absolute form of an existing folder (DOS: upper case, except names on a disk with long names, which keep their case).
     .endtable
     
     .h2 Jobs
    @@ -139,6 +143,6 @@ fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx)
     
     .h2 BASIC
     
    -src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
    +src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$) and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
     
     Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.
    diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
    index 9ae0ea1e..301afd1d 100644
    --- a/src/libs/kpunch/libdvx/apiref.dhs
    +++ b/src/libs/kpunch/libdvx/apiref.dhs
    @@ -856,6 +856,7 @@ Reset the clip rectangle to the full display dimensions.
     .index drawChar
     .index drawText
     .index drawTextN
    +.index drawTextScaled
     .index textWidth
     .index accelParse
     .index drawTextAccel
    @@ -1046,6 +1047,26 @@ Optimized batch text rendering for a known character count. Computes clip bounds
       opaque      true = fill background, false = transparent
     .endtable
     
    +.h2 drawTextScaled
    +
    +.code
    +void drawTextScaled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg);
    +.endcode
    +
    +Draw a null-terminated string scale times as large: every glyph pixel becomes a scale x scale block in fg, and the background shows through. For titles and banners in the 8x16 font (DVX Setup's title, the Welcome window). The text is textWidth(font, text) * scale wide and font->charHeight * scale tall. Each run of set pixels in a glyph row is one rectFill, which clips. Scale 1 is drawText; a scale below 1 draws nothing. BASIC: FontScale on a form or PictureBox (Print, TextWidth, TextHeight).
    +
    +.table
    +  Parameter   Description
    +  ---------   -----------
    +  d           Display context
    +  ops         Blit operations vtable
    +  font        Bitmap font
    +  x, y        Top-left corner
    +  text        Null-terminated string
    +  scale       Size multiple (1 = normal)
    +  fg          Packed text color
    +.endtable
    +
     .h2 textWidth
     
     .code
    @@ -4413,7 +4434,7 @@ int32_t wgtGetSpacingPx(const WidgetT *w);
     void    wgtSetSpacingPx(WidgetT *w, int32_t pixels);
     .endcode
     
    -A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return 0 when the size is unset (the container's default) or given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 restores the default) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these.
    +A container's padding (space inside its edge) and spacing (gap between children) in pixels. The getters return what the layout uses: the container's default while the field is WGT_GAP_DEFAULT (every widget starts so), and 0 for a size given in another unit. The setters clamp to 0..WGT_GAP_PIXELS_MAX (0 is no gap) and re-lay out the window. The BASIC Padding and Spacing properties of VBox, HBox, Frame and ScrollPane use these.
     
     .h3 wgtLayout
     
    @@ -4421,7 +4442,7 @@ A container's padding (space inside its edge) and spacing (gap between children)
     void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font);
     .endcode
     
    -Execute the full two-pass layout algorithm on a tree at an explicit size. Pass 1 (bottom-up): compute minimum sizes. Pass 2 (top-down): allocate space with weighted distribution; a weighted child capped by maxW/maxH stops at its cap and the space it cannot take goes to the other weighted children. Window-managed trees are laid out by wgtInvalidate and the paint path; this is for laying out a tree on demand.
    +Execute the full two-pass layout algorithm on a tree at an explicit size. Pass 1 (bottom-up): compute minimum sizes; a cap (maxW/maxH) wins over a larger minimum, so a widget whose content asks for more than its cap is given its cap and the rest of the space goes to its siblings. Pass 2 (top-down): allocate space with weighted distribution; a weighted child capped by maxW/maxH stops at its cap and the space it cannot take goes to the other weighted children. Window-managed trees are laid out by wgtInvalidate and the paint path; this is for laying out a tree on demand.
     
     .table
       Parameter     Description
    @@ -4834,6 +4855,7 @@ typedef struct WidgetClassT {
       WGT_SIZE_PIXELS     0x00000000   Unit tag: pixels
       WGT_SIZE_CHARS      0x40000000   Unit tag: character widths
       WGT_SIZE_PERCENT    0x80000000   Unit tag: percentage of parent
    +  WGT_GAP_DEFAULT     0xC0000000   padding / spacing: the container's own default (wgtPixels(0) is no gap)
       WGT_WEIGHT_FILL     100          Default "flexible" weight value
     .endtable
     
    @@ -6104,6 +6126,7 @@ Free every tracked allocation charged to the given app. Called by the shell afte
     .index widgetFindByAccel
     .index widgetDrawScrollbarH
     .index widgetScrollbarApplyHit
    +.index widgetScrollbarNeeds
     .index widgetDrawScrollbarV
     
     .h1 dvxWgtP.h -- Widget Plugin API
    @@ -6398,6 +6421,14 @@ int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t
     
     Map a dragged thumb position to a scroll position in [0, maxScroll]. relMouse is where the thumb's leading edge should be, relative to the start of a track of trackLen pixels (the mouse position minus the grab offset). Returns 0 when maxScroll <= 0.
     
    +.h3 widgetScrollbarNeeds
    +
    +.code
    +void widgetScrollbarNeeds(int32_t totalW, int32_t totalH, int32_t *innerW, int32_t *innerH, bool *needV, bool *needH);
    +.endcode
    +
    +Which scrollbars content totalW x totalH pixels needs in a viewport of *innerW x *innerH (the area inside the widget's border). A vertical bar narrows the viewport, which may make the content too wide; a horizontal bar shortens it, which may make the content too tall; both cases are followed through. *innerW and *innerH come back less the bars (WGT_SB_W each). For rows of a fixed height pass rows * height: the content is too tall exactly when there are more rows than whole rows fit. ListBox, ListView, TreeView, ScrollPane and DBGrid decide their bars with it; BASIC's ScrollBarsNeeded is the same rule.
    +
     .h2 Layout Functions
     
     .h3 widgetCalcMinSizeBox / widgetCalcMinSizeTree
    @@ -6528,6 +6559,16 @@ stb_ds dynamic array. Each widget DXE appends its class pointer via wgtRegisterC
     .index platformIsReadOnly
     .index platformDirIsWritable
     .index platformTempDir
    +.index platformLongNames
    +.index platformValidateLongName
    +.index platformValidateShortName
    +.index platformLongNameChar
    +.index platformStatSize
    +.index platformShortPath
    +.index PLATFORM_LONG_NAME_MAX
    +.index Long File Names
    +.index FAT32
    +.index dvxArgAppend
     .index Read-only Media
     .index PlatformDriveT
     .index Critical Error Handler
    @@ -6894,10 +6935,40 @@ Query total and free physical memory.
     .h3 platformValidateFilename
     
     .code
    -const char *platformValidateFilename(const char *name);
    +const char *platformValidateFilename(const char *dir, const char *name);
     .endcode
     
    -Validate a filename against platform-specific rules (8.3 on DOS). Returns NULL if valid, or a human-readable error string.
    +Validate a name for a new file or folder in folder dir (NULL or "" for the current one) against the rules of its disk. On DOS: platformValidateLongName where platformLongNames(dir), else platformValidateShortName, and dir with the name must stay under DVX_MAX_PATH. The host backend refuses an empty name, . and .., separators and control characters, and ignores dir. Returns NULL if valid, or a human-readable error string. The file dialog, the File Manager (fileOpsValidateName) and the IDE's project rename all use it.
    +
    +.h3 Long file names: platformLongNames / platformValidateLongName / platformValidateShortName / platformLongNameChar / platformShortPath
    +
    +.code
    +#define PLATFORM_LONG_NAME_MAX 255
    +
    +bool        platformLongNames(const char *path);
    +const char *platformValidateLongName(const char *name);
    +const char *platformValidateShortName(const char *name);
    +bool        platformLongNameChar(char c);
    +bool        platformShortPath(const char *path, char *out, int32_t outSize);
    +.endcode
    +
    +The DOS C library uses the long-name calls (INT 21h 71xxh) by itself whenever a driver provides them (Windows, DOSLFN), so open, fopen, rename, mkdir, stat and the directory readers all take and return long names; these functions cover what the C library does not.
    +
    +platformLongNames is true when the drive holding path (NULL or "": the current drive) takes long file names. On DOS that is a loaded long-name driver covering that drive; elsewhere always.
    +
    +platformValidateLongName and platformValidateShortName are the two DOS name rules by themselves, whatever the disk takes; each returns NULL or why the name is not valid. A long name has 1 to PLATFORM_LONG_NAME_MAX characters and may hold spaces, several dots and + , ; = [ ]; it may not hold a control character or \ / : * ? " < > |, and may not end with a dot or a space (DOS drops them). An 8.3 name has 1 to 8 printable ASCII characters, then at most one dot and up to 3 more, none of them a space or " * + , / : ; < = > ? [ \ ] |. Neither may be . or .., or a device name (CON, PRN, AUX, NUL, CLOCK$, COM1 to COM9, LPT1 to LPT9) before its first dot. They are shared code, so the host tests check the DOS rules too.
    +
    +platformLongNameChar is true when a long name may hold character c; a file transfer uses it to turn the characters a sender's name cannot keep into _.
    +
    +platformShortPath writes the short (8.3) form of an existing path into out, for what reads names before a long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (an old program's command line). On a drive with long names DOS gives every part's short name (INT 21h AX=7160h CL=1); elsewhere, and when DOS cannot, path itself is copied. False when out is too small.
    +
    +.h3 platformStatSize
    +
    +.code
    +uint64_t platformStatSize(const struct stat *st);
    +.endcode
    +
    +The size stat reported for a file. A FAT32 file reaches 4 GB - 1 byte, but the DOS C library's st_size is a signed 32-bit off_t, so a file past 2 GB reads as negative there; platformStatSize reads it as the unsigned 32-bit size DOS keeps. Use it wherever a file size comes from stat or fstat.
     
     .h3 platformMkdirRecursive
     
    @@ -7016,7 +7087,7 @@ char    platformBootDrive(void);
     
     platformDriveList fills out[] with the drives that exist, in letter order, and returns the count. On DOS it uses IOCTL calls only (INT 21h AX=4409h remote, 440Eh logical drive map, 4408h removable), never the media, so empty floppy and CD-ROM drives are listed at once; the second letter of a shared floppy drive (B: on a one-floppy system) is left out because using it makes DOS ask for a disk swap. The host backend reports one drive, "/". System Information and the file operations library both use it.
     
    -platformDiskSpace reports the size and free space of the disk holding path (DOS: INT 21h AH=36h on the path's drive letter, or the current drive; host: statvfs of the nearest existing folder of path, so a folder about to be created reports its disk). False when unknown.
    +platformDiskSpace reports the size and free space of the disk holding path (DOS: INT 21h AX=7303h, the FAT32 call that counts past 2 GB, on the root of the path's drive letter or the current drive, falling back to AH=36h, which tops out near 2 GB, where DOS does not have it; host: statvfs of the nearest existing folder of path, so a folder about to be created reports its disk). False when unknown.
     
     platformBootDrive returns the letter of the drive DOS started from (INT 21h AX=3305h, DOS 4.0+), whose root holds AUTOEXEC.BAT, or 0 where there is none (older DOS, and every platform but DOS).
     
    @@ -7090,6 +7161,14 @@ while ((p = dvxNextArg(p, arg, sizeof(arg), "ed)) != NULL) {
     }
     .endcode
     
    +.h3 dvxArgAppend
    +
    +.code
    +int32_t dvxArgAppend(char *buf, int32_t bufSize, int32_t pos, const char *arg);
    +.endcode
    +
    +Append arg to the command line in buf at pos as one argument that dvxNextArg reads back whole: in double quotes when it holds a blank or a tab (a long name such as C:\My Programs\Thing.app), as it is otherwise. Returns the new length, or -1 when it does not fit (as dvxStrAppendf). The shell's file-type associations and F1 help build their command lines with it; BASIC programs have ShellArgQuote$.
    +
     .h2 DXE Module Support
     
     .h3 platformRegisterDxeExports
    diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c
    index 732d7741..d16ac1e3 100644
    --- a/src/libs/kpunch/libdvx/dvxDialog.c
    +++ b/src/libs/kpunch/libdvx/dvxDialog.c
    @@ -1383,16 +1383,13 @@ static int32_t fdDirPartLen(const char *name) {
     
     
     // Convert a list display name to a real directory name by stripping the
    -// surrounding brackets used for visual distinction ("[name]" -> "name").
    +// brackets around it used for visual distinction ("[name]" -> "name"; a
    +// long name may hold ']' itself).
     static void fdDisplayToDirName(const char *display, char *out, int32_t outSize) {
    -    if (display[0] == '[') {
    -        snprintf(out, outSize, "%s", display + 1);
    +    size_t len = strlen(display);
     
    -        char *bracket = strchr(out, ']');
    -
    -        if (bracket) {
    -            *bracket = '\0';
    -        }
    +    if (len >= 2 && display[0] == '[' && display[len - 1] == ']') {
    +        snprintf(out, outSize, "%.*s", (int)(len - 2), display + 1);
         } else {
             snprintf(out, outSize, "%s", display);
         }
    @@ -1766,10 +1763,11 @@ static void fdOnPathKey(WidgetT *w, int32_t keyCode, int32_t shift) {
     }
     
     
    -// Check filename is valid for target OS.
    +// Check the name by the rules of the folder's disk (long names where it
    +// takes them).
     
     static bool fdValidateFilename(FileDialogStateT *fd, const char *name) {
    -    const char *error = platformValidateFilename(name);
    +    const char *error = platformValidateFilename(fd->curDir, name);
     
         if (error) {
             dvxErrorBox(fd->ctx, "Invalid Filename", error);
    diff --git a/src/libs/kpunch/libdvx/dvxDraw.c b/src/libs/kpunch/libdvx/dvxDraw.c
    index bbf2ccc0..b416036b 100644
    --- a/src/libs/kpunch/libdvx/dvxDraw.c
    +++ b/src/libs/kpunch/libdvx/dvxDraw.c
    @@ -1328,6 +1328,58 @@ void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_
     }
     
     
    +// Text with every glyph pixel drawn as a scale x scale block in fg (the
    +// background shows through): titles and banners in the 8x16 font.  Each
    +// run of set pixels in a glyph row is one rectFill, which clips; a glyph
    +// cell outside the clip rect is skipped whole.  Scale 1 is drawText.
    +void drawTextScaled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg) {
    +    if (!text || scale < 1) {
    +        return;
    +    }
    +
    +    if (scale == 1) {
    +        drawText(d, ops, font, x, y, text, fg, fg, false);
    +        return;
    +    }
    +
    +    int32_t cellW = font->charWidth * scale;
    +    int32_t cellH = font->charHeight * scale;
    +
    +    if (y + cellH <= d->clipY || y >= d->clipY + d->clipH) {
    +        return;
    +    }
    +
    +    for (; *text; text++, x += cellW) {
    +        int32_t idx = (uint8_t)*text - font->firstChar;
    +
    +        if (idx < 0 || idx >= font->numChars || x + cellW <= d->clipX || x >= d->clipX + d->clipW) {
    +            continue;
    +        }
    +
    +        const uint8_t *glyph = font->glyphData + idx * font->charHeight;
    +
    +        for (int32_t row = 0; row < font->charHeight; row++) {
    +            int32_t col = 0;
    +
    +            while (col < font->charWidth) {
    +                int32_t end = col;
    +
    +                while (end < font->charWidth && (glyph[row] & sGlyphBit[end])) {
    +                    end++;
    +                }
    +
    +                if (end > col) {
    +                    rectFill(d, ops, x + col * scale, y + row * scale, (end - col) * scale, scale, fg);
    +                    col = end;
    +                } else {
    +                    col++;
    +                }
    +            }
    +        }
    +    }
    +}
    +
    +
     // ============================================================
     // drawVLine
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/dvxDraw.h b/src/libs/kpunch/libdvx/dvxDraw.h
    index 4563bec7..3efeb118 100644
    --- a/src/libs/kpunch/libdvx/dvxDraw.h
    +++ b/src/libs/kpunch/libdvx/dvxDraw.h
    @@ -83,6 +83,11 @@ void drawText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t
     // than drawChar-per-character for long runs (e.g. terminal rows, list items).
     void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque);
     
    +// Draw a null-terminated string scale times as large (each glyph pixel a
    +// scale x scale block, textWidth * scale wide) in fg over what is there:
    +// titles and banners.  Scale 1 is drawText; below 1 draws nothing.
    +void drawTextScaled(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, uint32_t fg);
    +
     // Returns the pixel width of a null-terminated string (count * charWidth).
     int32_t textWidth(const BitmapFontT *font, const char *text);
     
    diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h
    index 78444c63..63ef0361 100644
    --- a/src/libs/kpunch/libdvx/dvxTypes.h
    +++ b/src/libs/kpunch/libdvx/dvxTypes.h
    @@ -65,7 +65,7 @@
     // src/test/abi/abiSizes.txt (make -C src/test/abi update) in the same
     // change; the abi test fails when a public struct changes size without a
     // bump, and when the table was not regenerated after one.
    -#define DVX_ABI_VERSION 7
    +#define DVX_ABI_VERSION 8
     
     // A module's stamp is named , e.g. the
     // stamp of LIBS/KPUNCH/LIBDVX/LIBDVX.LIB is libdvxAbiVersion.
    diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h
    index eb152f96..933b3359 100644
    --- a/src/libs/kpunch/libdvx/dvxWgt.h
    +++ b/src/libs/kpunch/libdvx/dvxWgt.h
    @@ -80,6 +80,11 @@ struct WidgetClassT;
     #define WGT_SIZE_CHARS      0x40000000
     #define WGT_SIZE_PERCENT    0x80000000
     
    +// A container's padding / spacing left to the container's own default
    +// (the one type tag no size uses), so that wgtPixels(0) is no gap.
    +// Every widget starts with it.
    +#define WGT_GAP_DEFAULT     ((int32_t)WGT_SIZE_TYPE_MASK)
    +
     // Standard "fill remaining space" weight for VBox/HBox children.
     // A widget with WGT_WEIGHT_FILL gets one share of extra space; use
     // 2*, 3* etc. to give siblings proportionally more space.
    @@ -331,8 +336,8 @@ typedef struct WidgetT {
     
         // Container properties
         WidgetAlignE     align;      // main-axis alignment for children
    -    int32_t          spacing;    // tagged size for spacing between children (0 = default)
    -    int32_t          padding;    // tagged size for internal padding (0 = default)
    +    int32_t          spacing;    // tagged size for spacing between children (WGT_GAP_DEFAULT = the class's default)
    +    int32_t          padding;    // tagged size for internal padding (WGT_GAP_DEFAULT = the class's default)
     
         // Colors (0 = use color scheme defaults)
         uint32_t         fgColor;
    @@ -849,9 +854,9 @@ int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth
     
     // Padding / Spacing of a container in pixels, as the BASIC Padding and
     // Spacing properties of the boxes and the ScrollPane read and write them.
    -// The getters return 0 for an unset size (the container's default) or one
    -// in another unit; the setters clamp to 0..WGT_GAP_PIXELS_MAX (0 = the
    -// default) and re-lay out the window.
    +// The getters return what the layout uses (the container's default when
    +// it is WGT_GAP_DEFAULT); the setters clamp to 0..WGT_GAP_PIXELS_MAX (0 =
    +// no gap) and re-lay out the window.
     #define WGT_GAP_PIXELS_MAX 4096
     
     int32_t wgtGetPaddingPx(const WidgetT *w);
    diff --git a/src/libs/kpunch/libdvx/dvxWgtP.h b/src/libs/kpunch/libdvx/dvxWgtP.h
    index 1d6cf17e..c4aa443a 100644
    --- a/src/libs/kpunch/libdvx/dvxWgtP.h
    +++ b/src/libs/kpunch/libdvx/dvxWgtP.h
    @@ -237,13 +237,17 @@ void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
     // visible]; returns true (with *outDragOff) when the thumb was grabbed.
     bool widgetScrollbarApplyHit(int32_t sbLen, int32_t relPos, int32_t total, int32_t visible, int32_t lineStep, int32_t pageStep, int32_t *pPos, int32_t *outDragOff);
     ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos);
    +// Which bars content totalW x totalH needs in a viewport *innerW x *innerH
    +// (each bar can make the other one needed); the viewport comes back less
    +// the bars.
    +void widgetScrollbarNeeds(int32_t totalW, int32_t totalH, int32_t *innerW, int32_t *innerH, bool *needV, bool *needH);
     
     // ============================================================
     // Layout functions (widgetLayout.c)
     // ============================================================
     
    -// Resolves a box container's padding/gap (tagged sizes with defaults) and
    -// its class-supplied extraTop/borderW overrides.  font may be NULL when
    +// Resolves a box container's padding/gap (tagged sizes; the class's
    +// defaults for WGT_GAP_DEFAULT) and its class-supplied extraTop/borderW.  font may be NULL when
     // only borderW is wanted (character-unit sizes then resolve to 0).
     void widgetBoxMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW);
     void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font);
    diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h
    index 11d3df19..dbc3ffca 100644
    --- a/src/libs/kpunch/libdvx/platform/dvxPlat.h
    +++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h
    @@ -537,12 +537,49 @@ const char *platformGetSystemInfo(const DisplayT *display);
     // File system
     // ============================================================
     
    -// Validate a filename against platform-specific rules. On DOS this
    -// enforces 8.3 naming (no long filenames), checks for reserved device
    -// names (CON, PRN, etc.), and rejects characters illegal in FAT filenames.
    -// Returns NULL if the filename is valid, or a human-readable error string
    -// describing why it's invalid. Used by the file dialog's save-as validation.
    -const char *platformValidateFilename(const char *name);
    +// Longest long file name (platformValidateLongName)
    +#define PLATFORM_LONG_NAME_MAX 255
    +
    +// The short (8.3) form of an existing path, for what reads names before a
    +// long-name driver loads (AUTOEXEC.BAT) or cannot take blanks (a DOS
    +// command line): on a drive with long names DOS gives every part's short
    +// name; elsewhere, and when DOS cannot, path itself.  False when out is
    +// too small.
    +bool platformShortPath(const char *path, char *out, int32_t outSize);
    +
    +struct stat;
    +
    +// The size stat reported for a file.  DOS sizes are 32-bit unsigned (a
    +// FAT32 file reaches 4 GB - 1) where the DOS C library's st_size is a
    +// signed 32-bit off_t, so a file past 2 GB reads as negative there.
    +uint64_t platformStatSize(const struct stat *st);
    +
    +// True when the drive holding path (NULL or "": the current drive) takes
    +// long file names: on DOS when a long-name driver (Windows, DOSLFN) is
    +// loaded and covers that drive; elsewhere always.  Without them DOS names
    +// are 8.3.
    +bool platformLongNames(const char *path);
    +
    +// Validate a name for a new file or folder in folder dir (NULL or "": the
    +// current one) against the rules of its disk.  On DOS: long names where
    +// platformLongNames(dir) (platformValidateLongName), else 8.3
    +// (platformValidateShortName), and dir with the name must fit
    +// DVX_MAX_PATH; elsewhere: not empty, . or .., no separators or control
    +// characters.  Returns NULL if the name is valid, or a human-readable
    +// error string describing why it's invalid.
    +const char *platformValidateFilename(const char *dir, const char *name);
    +
    +// The DOS name rules by themselves (NULL or why not): a long name (1 to
    +// PLATFORM_LONG_NAME_MAX characters, spaces, several dots and + , ; = [ ]
    +// allowed, none of control characters and \ / : * ? " < > |, no dot or
    +// space at the end) or an 8.3 name; neither may be a device name (CON,
    +// PRN, AUX, NUL, CLOCK$, COMn, LPTn) before its first dot.
    +const char *platformValidateLongName(const char *name);
    +const char *platformValidateShortName(const char *name);
    +
    +// True when a long name may hold character c (not a control character or
    +// one of \ / : * ? " < > |).
    +bool platformLongNameChar(char c);
     
     // Query current system memory.  Sets totalKb and freeKb to the total
     // and free physical memory in kilobytes.  Returns false if unavailable.
    @@ -645,6 +682,12 @@ int32_t dvxCopyTrimmed(char *dst, int32_t dstSize, const char *src);
     // NULL when only blanks are left.  Loop until NULL to walk every argument.
     const char *dvxNextArg(const char *p, char *out, int32_t outSize, bool *quoted);
     
    +// Append arg to the command line in buf at pos as one argument that
    +// dvxNextArg reads back whole: in quotes when it holds blanks (a long
    +// name such as C:\My Programs\Thing.app).  Returns the new length, or -1
    +// when it does not fit (as dvxStrAppendf).
    +int32_t dvxArgAppend(char *buf, int32_t bufSize, int32_t pos, const char *arg);
    +
     // Case-insensitive check that `name` ends with `ext` (which should
     // include the leading dot, e.g. ".app").  Returns false if either
     // argument is NULL or name is shorter than ext.
    diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
    index eb09ea02..bf5c6fea 100644
    --- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
    +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
    @@ -270,6 +270,9 @@
     #define DOS_BREAK_OFF          0x0000
     #define DOS_FN_GET_BOOT_DRIVE  0x3305  // DL = boot drive (1 = A:)
     #define DOS_FN_DISK_FREE       0x3600
    +#define DOS_FN_EXT_DISK_FREE   0x7303  // FAT32 DOS (7.1, FreeDOS): free space of the root at DS:DX into ES:DI, CX bytes
    +#define DOS_FN_LFN_TRUENAME    0x7160  // long-name truename of DS:SI into ES:DI, CL = form
    +#define LFN_TRUENAME_SHORT     1       // 7160h CL: the short (8.3) form
     #define DOS_FN_IOCTL_REMOVABLE 0x4408
     #define DOS_FN_IOCTL_REMOTE    0x4409
     #define DOS_FN_IOCTL_DRIVE_MAP 0x440E  // AL = letter the drive answers to now (0 = only one)
    @@ -277,6 +280,15 @@
     #define DOS_DRIVE_REMOTE_BIT   0x1000  // 4409h DX bit 12: remote (network / CD-ROM)
     #define DOS_REMOVABLE_MEDIA    0       // 4408h AX: removable
     #define DOS_DEFAULT_DRIVE      0       // 36h DL: the current drive
    +
    +// The 7303h extended free-space record (its version word at +2 goes in as
    +// 0) and where it sits in the transfer buffer, after the "X:\" root
    +#define EXT_FREE_SIZE          0x2C
    +#define EXT_FREE_SEC_PER_CLUS  0x04
    +#define EXT_FREE_BYTES_PER_SEC 0x08
    +#define EXT_FREE_AVAIL_CLUS    0x0C
    +#define EXT_FREE_TOTAL_CLUS    0x10
    +#define EXT_FREE_ROOT_LEN      4
     #define DOS_VERSION_BYTE_SHIFT 8
     
     // DOS critical-error handler: the real-mode vector and a stub in DOS
    @@ -410,6 +422,7 @@ typedef int32_t (*ModeScoreFnT)(const VbeModeT *m, int32_t requestedW, int32_t r
     static uint8_t                 biosVideoMode(void);
     static bool                    cpuHasTsc(void);
     static uint32_t                estimateClockMhz(void);
    +static bool                    extDiskSpace(int32_t drive, uint64_t *totalBytes, uint64_t *freeBytes);
     static uint32_t                farPtrToLinear(uint32_t addr);
     static int32_t                 findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, ModeScoreFnT score, uint16_t *outMode, DisplayT *d);
     static bool                    hasCpuid(void);
    @@ -669,6 +682,43 @@ static uint32_t estimateClockMhz(void) {
     }
     
     
    +// INT 21h AX=7303h, the FAT32 free-space call: 32-bit cluster counts, so
    +// a disk past 2 GB (which AH=36h caps) reports its true size.  drive is
    +// 1-based (0 = the current one).  False where DOS lacks the call (it
    +// leaves the record zero) or the drive does not answer it.
    +static bool extDiskSpace(int32_t drive, uint64_t *totalBytes, uint64_t *freeBytes) {
    +    static const uint8_t zero[EXT_FREE_SIZE];
    +    char                 root[EXT_FREE_ROOT_LEN] = "A:" DVX_PATH_SEP;
    +    uint32_t             rec                     = __tb + EXT_FREE_ROOT_LEN;
    +    __dpmi_regs          r;
    +
    +    root[0] = (char)('A' + ((drive == DOS_DEFAULT_DRIVE) ? getdisk() : drive - 1));
    +    dosmemput(root, sizeof(root), __tb);
    +    dosmemput(zero, sizeof(zero), rec);
    +    memset(&r, 0, sizeof(r));
    +    r.x.ax = DOS_FN_EXT_DISK_FREE;
    +    r.x.ds = (uint16_t)(__tb >> REAL_MODE_SEG_SHIFT);
    +    r.x.dx = (uint16_t)(__tb & REAL_MODE_OFF_MASK);
    +    r.x.es = (uint16_t)(rec >> REAL_MODE_SEG_SHIFT);
    +    r.x.di = (uint16_t)(rec & REAL_MODE_OFF_MASK);
    +    r.x.cx = EXT_FREE_SIZE;
    +    __dpmi_int(INT_DOS, &r);
    +
    +    uint32_t secPerClus  = _farpeekl(_dos_ds, rec + EXT_FREE_SEC_PER_CLUS);
    +    uint32_t bytesPerSec = _farpeekl(_dos_ds, rec + EXT_FREE_BYTES_PER_SEC);
    +
    +    if ((r.x.flags & CPU_FLAG_CF) || secPerClus == 0 || bytesPerSec == 0) {
    +        return false;
    +    }
    +
    +    uint64_t clusterSize = (uint64_t)secPerClus * bytesPerSec;
    +
    +    *totalBytes = clusterSize * _farpeekl(_dos_ds, rec + EXT_FREE_TOTAL_CLUS);
    +    *freeBytes  = clusterSize * _farpeekl(_dos_ds, rec + EXT_FREE_AVAIL_CLUS);
    +    return true;
    +}
    +
    +
     // Convert a real-mode far pointer stored at linear address addr (offset
     // word, then segment word) into a linear address.
     static uint32_t farPtrToLinear(uint32_t addr) {
    @@ -1088,20 +1138,22 @@ static void platformCrashHandler(int sig) {
     }
     
     
    -// INT 21h AH=36h: AX sectors per cluster, BX free clusters, CX bytes per
    -// sector, DX total clusters.  An empty drive fails through the
    -// critical-error handler (AX = FFFFh).
    +// The FAT32 call first (extDiskSpace), then INT 21h AH=36h: AX sectors
    +// per cluster, BX free clusters, CX bytes per sector, DX total clusters
    +// (at most 2 GB).  An empty drive fails through the critical-error
    +// handler (AX = FFFFh).
     bool platformDiskSpace(const char *path, uint64_t *totalBytes, uint64_t *freeBytes) {
    +    int32_t drive = (path[0] && path[1] == ':') ? toupper((unsigned char)path[0]) - 'A' + 1 : DOS_DEFAULT_DRIVE;
    +
    +    if (extDiskSpace(drive, totalBytes, freeBytes)) {
    +        return true;
    +    }
    +
         __dpmi_regs r;
     
         memset(&r, 0, sizeof(r));
         r.x.ax = DOS_FN_DISK_FREE;
    -    r.x.dx = DOS_DEFAULT_DRIVE;
    -
    -    if (path[0] && path[1] == ':') {
    -        r.x.dx = (uint16_t)(toupper((unsigned char)path[0]) - 'A' + 1);
    -    }
    -
    +    r.x.dx = (uint16_t)drive;
         __dpmi_int(INT_DOS, &r);
     
         if (r.x.ax == DOS_DISK_INVALID) {
    @@ -1771,6 +1823,14 @@ void platformLogCrashDetail(int sig, PlatformLogFnT logFn) {
     }
     
     
    +// The C library's answer (INT 21h AX=71A0h on the drive's root, unless
    +// the LFN environment variable says n): the same test it uses to pick the
    +// long-name calls for open, rename, mkdir and findfirst.
    +bool platformLongNames(const char *path) {
    +    return _use_lfn((path && path[0]) ? path : NULL) != 0;
    +}
    +
    +
     // PIT clocks up to the last interrupt plus the part of the running
     // period: from the TSC once a period has been measured, else from the
     // latched channel 0 count (and the PIC, for an interrupt not yet
    @@ -2096,6 +2156,40 @@ void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) {
     }
     
     
    +// INT 21h AX=7160h CL=1, the long-name call for the short form of an
    +// existing path (CH=0: a SUBSTed drive as its true path).  Not the C
    +// library's _truename_sfn: that asks with CL=0 (the fully qualified
    +// name), which Windows answers in short names but DOSLFN in long ones.
    +bool platformShortPath(const char *path, char *out, int32_t outSize) {
    +    char     sfn[DVX_MAX_PATH];
    +    size_t   len = strlen(path) + 1;
    +    uint32_t dst = __tb + (uint32_t)len;
    +    bool     ok  = false;
    +
    +    if (len <= sizeof(sfn) && platformLongNames(path)) {
    +        __dpmi_regs r;
    +
    +        dosmemput(path, len, __tb);
    +        memset(&r, 0, sizeof(r));
    +        r.x.ax = DOS_FN_LFN_TRUENAME;
    +        r.h.cl = LFN_TRUENAME_SHORT;
    +        r.x.ds = (uint16_t)(__tb >> REAL_MODE_SEG_SHIFT);
    +        r.x.si = (uint16_t)(__tb & REAL_MODE_OFF_MASK);
    +        r.x.es = (uint16_t)(dst >> REAL_MODE_SEG_SHIFT);
    +        r.x.di = (uint16_t)(dst & REAL_MODE_OFF_MASK);
    +        __dpmi_int(INT_DOS, &r);
    +
    +        if (!(r.x.flags & CPU_FLAG_CF)) {
    +            dosmemget(dst, sizeof(sfn), sfn);
    +            sfn[sizeof(sfn) - 1] = '\0';
    +            ok = true;
    +        }
    +    }
    +
    +    return snprintf(out, (size_t)outSize, "%s", ok ? sfn : path) < outSize;
    +}
    +
    +
     // Copies 'count' 16-bit pixels. Since each pixel is 2 bytes, we
     // only need to check bit 1 of the address for dword alignment
     // (bit 0 is always clear for 16-bit aligned data). A single
    @@ -2455,91 +2549,17 @@ int32_t platformUnlockRegion(const void *addr, uint32_t size, bool isCode) {
     }
     
     
    -// DOS 8.3 filename validation.
    -//
    -// Validates that a filename conforms to DOS 8.3 conventions:
    -//   - Base name: 1-8 chars, extension: 0-3 chars, one dot max
    -//   - No spaces or special characters that DOS can't handle
    -//   - Not a reserved device name (CON, PRN, AUX, NUL, COMn, LPTn)
    -//
    -// The reserved name check compares the base name only (before the
    -// dot), case-insensitive, because DOS treats "CON.TXT" the same
    -// as the CON device  -- the extension is ignored for device names.
    -//
    -// Returns NULL on success, or a human-readable error string on failure.
    -// On non-DOS platforms, this function would be replaced with one that
    -// validates for that platform's filesystem rules.
    +// The name rules of dir's drive: long names where a long-name driver
    +// covers it, else 8.3 (both in dvxPlatformUtil.c); the folder and the
    +// name together must fit DVX_MAX_PATH.
    +const char *platformValidateFilename(const char *dir, const char *name) {
    +    const char *problem = platformLongNames(dir) ? platformValidateLongName(name) : platformValidateShortName(name);
     
    -const char *platformValidateFilename(const char *name) {
    -    static const char *reserved[] = {
    -        "CON", "PRN", "AUX", "NUL",
    -        "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
    -        "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
    -        NULL
    -    };
    -
    -    if (!name || name[0] == '\0') {
    -        return "Filename must not be empty.";
    +    if (!problem && dir && dir[0] && strlen(dir) + strlen(DVX_PATH_SEP) + strlen(name) >= DVX_MAX_PATH) {
    +        problem = "The name is too long for this folder.";
         }
     
    -    // Split into base and extension
    -    const char *dot = strrchr(name, '.');
    -    int32_t baseLen;
    -    int32_t extLen;
    -
    -    if (dot) {
    -        baseLen = (int32_t)(dot - name);
    -        extLen  = (int32_t)strlen(dot + 1);
    -    } else {
    -        baseLen = (int32_t)strlen(name);
    -        extLen  = 0;
    -    }
    -
    -    if (baseLen < 1 || baseLen > 8) {
    -        return "Filename must be 1-8 characters before the extension.";
    -    }
    -
    -    if (extLen > 3) {
    -        return "Extension must be 3 characters or fewer.";
    -    }
    -
    -    // Check for invalid characters
    -    for (const char *p = name; *p; p++) {
    -        if (*p == '.') {
    -            continue;
    -        }
    -
    -        if (*p < '!' || *p > '~') {
    -            return "Filename contains invalid characters.";
    -        }
    -
    -        if (strchr(" \"*+,/:;<=>?[\\]|", *p)) {
    -            return "Filename contains invalid characters.";
    -        }
    -    }
    -
    -    // Check for multiple dots
    -    if (dot && strchr(name, '.') != dot) {
    -        return "Filename may contain only one dot.";
    -    }
    -
    -    // Check reserved device names (compare base only, case-insensitive)
    -    char base[9];
    -    int32_t copyLen = baseLen < 8 ? baseLen : 8;
    -
    -    for (int32_t i = 0; i < copyLen; i++) {
    -        base[i] = toupper((unsigned char)name[i]);
    -    }
    -
    -    base[copyLen] = '\0';
    -
    -    for (const char **r = reserved; *r; r++) {
    -        if (strcmp(base, *r) == 0) {
    -            return "That name is a reserved device name.";
    -        }
    -    }
    -
    -    return NULL;
    +    return problem;
     }
     
     
    @@ -3305,6 +3325,7 @@ extern float __floatdisf(long long);
     DXE_EXPORT_TABLE(sDxeExportTable)
     
         // --- dvx helpers (lives in dvx.exe, used by all modules) ---
    +    DXE_EXPORT(dvxArgAppend)
         DXE_EXPORT(dvxCalloc)
         DXE_EXPORT(dvxCopyTrimmed)
         DXE_EXPORT(dvxFree)
    @@ -3389,6 +3410,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
         DXE_EXPORT(platformLockRegion)
         DXE_EXPORT(platformLogCrashDetail)
         DXE_EXPORT(platformLogMemory)
    +    DXE_EXPORT(platformLongNameChar)
    +    DXE_EXPORT(platformLongNames)
         DXE_EXPORT(platformMicros)
         DXE_EXPORT(platformMkdirRecursive)
         DXE_EXPORT(platformMouseInit)
    @@ -3411,6 +3434,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
         DXE_EXPORT(platformSetLogMemory)
         DXE_EXPORT(platformSetLogPath)
         DXE_EXPORT(platformSetReadOnly)
    +    DXE_EXPORT(platformShortPath)
         DXE_EXPORT(platformShutdownHookAdd)
         DXE_EXPORT(platformShutdownHookRemove)
         DXE_EXPORT(platformSpanCopy16)
    @@ -3419,6 +3443,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
         DXE_EXPORT(platformSpanFill16)
         DXE_EXPORT(platformSpanFill32)
         DXE_EXPORT(platformSpanFill8)
    +    DXE_EXPORT(platformStatSize)
         DXE_EXPORT(platformStripLineEndings)
         DXE_EXPORT(platformTempDir)
         DXE_EXPORT(platformTimerAdd)
    @@ -3430,6 +3455,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
         DXE_EXPORT(platformTimerStats)
         DXE_EXPORT(platformUnlockRegion)
         DXE_EXPORT(platformValidateFilename)
    +    DXE_EXPORT(platformValidateLongName)
    +    DXE_EXPORT(platformValidateShortName)
         DXE_EXPORT(platformVideoEnumModes)
         DXE_EXPORT(platformVideoExclusiveBegin)
         DXE_EXPORT(platformVideoExclusiveCapture)
    @@ -3704,6 +3731,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
         DXE_EXPORT(ftruncate)
         DXE_EXPORT(getpid)
         DXE_EXPORT(gettimeofday)
    +    DXE_EXPORT(llseek)
         DXE_EXPORT(lseek)
         DXE_EXPORT(open)
         DXE_EXPORT(read)
    diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
    index 955e9fa3..374de3eb 100644
    --- a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
    +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
    @@ -711,6 +711,13 @@ void platformLogCrashDetail(int sig, PlatformLogFnT logFn) {
     }
     
     
    +// Host file systems take long names everywhere.
    +bool platformLongNames(const char *path) {
    +    (void)path;
    +    return true;
    +}
    +
    +
     uint64_t platformMicros(void) {
         if (sRealTime) {
             return sRealTimeMicros + (wallClockUs() - sRealTimeBase);
    @@ -785,6 +792,12 @@ void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) {
     }
     
     
    +// Host file systems have no short names: path itself.
    +bool platformShortPath(const char *path, char *out, int32_t outSize) {
    +    return snprintf(out, (size_t)outSize, "%s", path) < outSize;
    +}
    +
    +
     void platformSpanCopy16(uint8_t *dst, const uint8_t *src, int32_t count) {
         memcpy(dst, src, (size_t)count * 2);
     }
    @@ -889,8 +902,10 @@ int32_t platformUnlockRegion(const void *addr, uint32_t size, bool isCode) {
     
     // Host filesystems accept nearly anything; keep the checks that matter
     // for a name typed into a save dialog: non-empty, no directory separators,
    -// no control characters, and not "." or "..".
    -const char *platformValidateFilename(const char *name) {
    +// no control characters, and not "." or ".." (dir does not change them).
    +const char *platformValidateFilename(const char *dir, const char *name) {
    +    (void)dir;
    +
         if (!name || name[0] == '\0') {
             return "Filename must not be empty.";
         }
    diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
    index f9cf7b9b..3108f0df 100644
    --- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
    +++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
    @@ -93,6 +93,16 @@
     // Scratch file platformDirIsWritable makes and removes
     #define WRITE_PROBE_NAME     "DVXWTEST.$$$"
     
    +// File name rules (platformValidateShortName / platformValidateLongName):
    +// an 8.3 name's parts, and the characters each kind of name cannot hold
    +// (besides control characters)
    +#define SHORT_NAME_BASE_MAX  8
    +#define SHORT_NAME_EXT_MAX   3
    +#define SHORT_NAME_BAD_CHARS " \"*+,/:;<=>?[\\]|"
    +#define LONG_NAME_BAD_CHARS  "\"*/:<>?\\|"
    +#define NAME_CHAR_FIRST      '!'   // an 8.3 name holds '!'..'~' only
    +#define NAME_CHAR_LAST       '~'
    +
     
     
     // ============================================================
    @@ -118,6 +128,8 @@ typedef struct {
     // Prototypes
     // ============================================================
     
    +static bool             deviceName(const char *name, int32_t len);
    +int32_t                 dvxArgAppend(char *buf, int32_t bufSize, int32_t pos, const char *arg);
     int32_t                 dvxCopyTrimmed(char *dst, int32_t dstSize, const char *src);
     bool                    dvxHasExt(const char *name, const char *ext);
     void                    dvxLog(const char *fmt, ...);
    @@ -141,6 +153,7 @@ bool                    platformFilterMatch(const char *name, const char *filter
     bool                    platformGlobMatch(const char *pattern, const char *name);
     bool                    platformIsReadOnly(void);
     const char             *platformLogMemory(void);
    +bool                    platformLongNameChar(char c);
     int32_t                 platformMkdirRecursive(const char *path);
     int32_t                 platformNormalizeFKeyScan(int32_t scancode);
     void                    platformNormalizeKeyEvent(PlatformKeyEventT *evt, int32_t liveMods);
    @@ -155,8 +168,11 @@ void                    platformSetAppExitHook(PlatformAppExitFnT fn);
     void                    platformSetLogMemory(void);
     void                    platformSetLogPath(const char *path);
     void                    platformSetReadOnly(bool readOnly);
    +uint64_t                platformStatSize(const struct stat *st);
     int32_t                 platformStripLineEndings(char *buf, int32_t len);
     const char             *platformTempDir(void);
    +const char             *platformValidateLongName(const char *name);
    +const char             *platformValidateShortName(const char *name);
     
     
     // ============================================================
    @@ -174,6 +190,14 @@ static int32_t sLogMemLen = 0;
     // DVX's folder cannot be written to (platformSetReadOnly)
     static bool sReadOnly = false;
     
    +// The device names DOS reserves: no file or folder may take one, whatever
    +// its extension (CON.TXT is the console)
    +static const char *const sDeviceNames[] = {
    +    "CON", "PRN", "AUX", "NUL", "CLOCK$",
    +    "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
    +    "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
    +};
    +
     // Where exit() and abort() of loaded modules go first (platformAppExit).
     static PlatformAppExitFnT sAppExitHook = NULL;
     
    @@ -212,6 +236,28 @@ static const char sAltScanToAscii[ALT_SCAN_TABLE_SIZE] = {
     };
     
     
    +// True when the first len characters of name are a reserved device name
    +// (any case).
    +static bool deviceName(const char *name, int32_t len) {
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sDeviceNames); i++) {
    +        if ((int32_t)strlen(sDeviceNames[i]) == len && strncasecmp(name, sDeviceNames[i], (size_t)len) == 0) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +int32_t dvxArgAppend(char *buf, int32_t bufSize, int32_t pos, const char *arg) {
    +    if (strpbrk(arg, DVX_ARG_BLANKS)) {
    +        return dvxStrAppendf(buf, bufSize, pos, "%c%s%c", DVX_ARG_QUOTE, arg, DVX_ARG_QUOTE);
    +    }
    +
    +    return dvxStrAppendf(buf, bufSize, pos, "%s", arg);
    +}
    +
    +
     int32_t dvxCopyTrimmed(char *dst, int32_t dstSize, const char *src) {
         if (!dst || dstSize <= 0) {
             return 0;
    @@ -679,6 +725,11 @@ const char *platformLogMemory(void) {
     }
     
     
    +bool platformLongNameChar(char c) {
    +    return !iscntrl((unsigned char)c) && !strchr(LONG_NAME_BAD_CHARS, c);
    +}
    +
    +
     int32_t platformMkdirRecursive(const char *path) {
         char buf[DVX_MAX_PATH];
     
    @@ -947,6 +998,15 @@ void platformSetReadOnly(bool readOnly) {
     }
     
     
    +uint64_t platformStatSize(const struct stat *st) {
    +#ifdef __DJGPP__
    +    return (uint32_t)st->st_size;
    +#else
    +    return (uint64_t)st->st_size;
    +#endif
    +}
    +
    +
     // Remove every carriage return in place and NUL-terminate; buf must hold
     // len + 1 bytes.  Used when reading DOS-format text files on hosts that
     // don't auto-strip them.
    @@ -974,3 +1034,77 @@ const char *platformTempDir(void) {
         return (dir && dir[0]) ? dir : NULL;
     }
     
    +
    +// A long name (a long-name driver covers the disk): 1..PLATFORM_LONG_NAME_MAX
    +// characters, spaces, several dots and + , ; = [ ] allowed; no control
    +// characters or \ / : * ? " < > |, no dot or space at the end (DOS drops
    +// them), not . or .., and not a device name before the first dot.
    +const char *platformValidateLongName(const char *name) {
    +    if (!name || name[0] == '\0') {
    +        return "Filename must not be empty.";
    +    }
    +
    +    size_t len = strlen(name);
    +
    +    if (len > PLATFORM_LONG_NAME_MAX) {
    +        return "Filename must be 255 characters or fewer.";
    +    }
    +
    +    if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
    +        return "That name is reserved.";
    +    }
    +
    +    for (const char *p = name; *p; p++) {
    +        if (!platformLongNameChar(*p)) {
    +            return "Filename contains invalid characters.";
    +        }
    +    }
    +
    +    if (name[len - 1] == '.' || name[len - 1] == ' ') {
    +        return "Filename must not end with a dot or a space.";
    +    }
    +
    +    if (deviceName(name, (int32_t)strcspn(name, "."))) {
    +        return "That name is a reserved device name.";
    +    }
    +
    +    return NULL;
    +}
    +
    +
    +// A DOS 8.3 name: 1-8 characters, then at most one dot and 0-3 more; only
    +// '!'..'~' and none of the characters FAT reserves; not a device name
    +// (the part before the dot: DOS ignores the extension for devices).
    +const char *platformValidateShortName(const char *name) {
    +    if (!name || name[0] == '\0') {
    +        return "Filename must not be empty.";
    +    }
    +
    +    const char *dot     = strchr(name, '.');
    +    int32_t     baseLen = dot ? (int32_t)(dot - name) : (int32_t)strlen(name);
    +    int32_t     extLen  = dot ? (int32_t)strlen(dot + 1) : 0;
    +
    +    if (baseLen < 1 || baseLen > SHORT_NAME_BASE_MAX) {
    +        return "Filename must be 1-8 characters before the extension.";
    +    }
    +
    +    if (dot && strchr(dot + 1, '.')) {
    +        return "Filename may contain only one dot.";
    +    }
    +
    +    if (extLen > SHORT_NAME_EXT_MAX) {
    +        return "Extension must be 3 characters or fewer.";
    +    }
    +
    +    for (const char *p = name; *p; p++) {
    +        if (*p != '.' && (*p < NAME_CHAR_FIRST || *p > NAME_CHAR_LAST || strchr(SHORT_NAME_BAD_CHARS, *p))) {
    +            return "Filename contains invalid characters.";
    +        }
    +    }
    +
    +    if (deviceName(name, baseLen)) {
    +        return "That name is a reserved device name.";
    +    }
    +
    +    return NULL;
    +}
    diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c
    index f641cabc..c384d19b 100644
    --- a/src/libs/kpunch/libdvx/widgetCore.c
    +++ b/src/libs/kpunch/libdvx/widgetCore.c
    @@ -313,6 +313,8 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) {
         w->wclass  = widgetClassTable[type];
         w->visible = true;
         w->enabled = true;
    +    w->padding = WGT_GAP_DEFAULT;
    +    w->spacing = WGT_GAP_DEFAULT;
         w->serial  = ++sWidgetSerial;
     
         if (w->wclass->flags & WCLASS_NEEDS_POLL) {
    diff --git a/src/libs/kpunch/libdvx/widgetLayout.c b/src/libs/kpunch/libdvx/widgetLayout.c
    index 5ce38350..29c743ad 100644
    --- a/src/libs/kpunch/libdvx/widgetLayout.c
    +++ b/src/libs/kpunch/libdvx/widgetLayout.c
    @@ -57,7 +57,8 @@
     
     static int32_t boxCapRoom(const WidgetT *c, bool horiz, int32_t innerW, int32_t innerH, const BitmapFontT *font);
     static bool    boxChildFrozen(const WidgetT *c, int32_t capRoom, int32_t freeExtra, int32_t freeWeight);
    -static int32_t gapPixelsOf(int32_t size);
    +static int32_t boxChildMin(const WidgetT *c, bool horiz, int32_t innerW, int32_t innerH, const BitmapFontT *font);
    +static int32_t capMin(int32_t minPx, int32_t maxTagged, int32_t parentPx, const BitmapFontT *font);
     static int32_t gapSizeOf(int32_t pixels);
     
     
    @@ -72,9 +73,8 @@ static int32_t boxCapRoom(const WidgetT *c, bool horiz, int32_t innerW, int32_t
         }
     
         int32_t maxPx = wgtResolveSize(maxTagged, horiz ? innerW : innerH, font->charWidth);
    -    int32_t cmin  = horiz ? c->calcMinW : c->calcMinH;
     
    -    return DVX_MAX(maxPx - cmin, 0);
    +    return DVX_MAX(maxPx - boxChildMin(c, horiz, innerW, innerH, font), 0);
     }
     
     
    @@ -86,27 +86,54 @@ static bool boxChildFrozen(const WidgetT *c, int32_t capRoom, int32_t freeExtra,
     }
     
     
    -// Pixels of a Padding / Spacing size given in pixels (0 for one that is
    -// unset or in another unit).
    -static int32_t gapPixelsOf(int32_t size) {
    -    return ((uint32_t)size & WGT_SIZE_TYPE_MASK) == WGT_SIZE_PIXELS ? size : 0;
    +// A box child's main-axis minimum, no larger than its main-axis cap (a
    +// percentage cap resolved against the box's inner size).
    +static int32_t boxChildMin(const WidgetT *c, bool horiz, int32_t innerW, int32_t innerH, const BitmapFontT *font) {
    +    return horiz ? capMin(c->calcMinW, c->maxW, innerW, font) : capMin(c->calcMinH, c->maxH, innerH, font);
     }
     
     
    -// A Padding / Spacing size from pixels (negative counts as 0, the
    -// container's default; at most WGT_GAP_PIXELS_MAX).
    +// minPx no larger than the size cap maxTagged (0: none): a widget is never
    +// laid out beyond its cap, so a minimum above it would only reserve space
    +// that no widget gets.  A percentage cap needs the parent's size, parentPx,
    +// which the measure pass does not know yet (0): it caps in the layout.
    +static int32_t capMin(int32_t minPx, int32_t maxTagged, int32_t parentPx, const BitmapFontT *font) {
    +    bool percent = ((uint32_t)maxTagged & WGT_SIZE_TYPE_MASK) == WGT_SIZE_PERCENT;
    +
    +    if (!maxTagged || (percent && parentPx <= 0)) {
    +        return minPx;
    +    }
    +
    +    return DVX_MIN(minPx, wgtResolveSize(maxTagged, parentPx, font->charWidth));
    +}
    +
    +
    +// A Padding / Spacing size from pixels (negative counts as 0; at most
    +// WGT_GAP_PIXELS_MAX).
     static int32_t gapSizeOf(int32_t pixels) {
         return wgtPixels(DVX_MIN(DVX_MAX(pixels, 0), WGT_GAP_PIXELS_MAX));
     }
     
     
     int32_t wgtGetPaddingPx(const WidgetT *w) {
    -    return gapPixelsOf(w->padding);
    +    int32_t pad;
    +    int32_t gap;
    +    int32_t extraTop;
    +    int32_t borderW;
    +
    +    widgetBoxMetrics(w, NULL, &pad, &gap, &extraTop, &borderW);
    +    return pad;
     }
     
     
     int32_t wgtGetSpacingPx(const WidgetT *w) {
    -    return gapPixelsOf(w->spacing);
    +    int32_t pad;
    +    int32_t gap;
    +    int32_t extraTop;
    +    int32_t borderW;
    +
    +    widgetBoxMetrics(w, NULL, &pad, &gap, &extraTop, &borderW);
    +    return gap;
     }
     
     
    @@ -189,28 +216,21 @@ void wgtSetSpacingPx(WidgetT *w, int32_t pixels) {
     
     
     // Resolves the layout metrics of a box container: padding and gap come
    -// from the widget's tagged sizes (falling back to DEFAULT_PADDING /
    -// DEFAULT_SPACING when unset), then a class-supplied getLayoutMetrics
    -// may override them and add extraTop / borderW (Frame, TabPage).
    +// from the widget's tagged sizes (DEFAULT_PADDING / DEFAULT_SPACING for
    +// WGT_GAP_DEFAULT; 0 is no gap), then a class-supplied getLayoutMetrics
    +// may put its own defaults in place of those and add extraTop / borderW
    +// (Frame, TabPage).
     // Shared by the measure pass, the arrange pass, and
     // widgetFrameBorderWidth so the three never disagree.
     
     void widgetBoxMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) {
         int32_t charW = font ? font->charWidth : 0;
     
    -    *pad      = wgtResolveSize(w->padding, 0, charW);
    -    *gap      = wgtResolveSize(w->spacing, 0, charW);
    +    *pad      = (w->padding == WGT_GAP_DEFAULT) ? DEFAULT_PADDING : wgtResolveSize(w->padding, 0, charW);
    +    *gap      = (w->spacing == WGT_GAP_DEFAULT) ? DEFAULT_SPACING : wgtResolveSize(w->spacing, 0, charW);
         *extraTop = 0;
         *borderW  = 0;
     
    -    if (*pad == 0) {
    -        *pad = DEFAULT_PADDING;
    -    }
    -
    -    if (*gap == 0) {
    -        *gap = DEFAULT_SPACING;
    -    }
    -
         if (wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) {
             wclsGetLayoutMetrics(w, font, pad, gap, extraTop, borderW);
         }
    @@ -289,7 +309,8 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
     // After the type-specific measure, explicit minW/minH size hints
     // are applied as a floor. This allows "this widget should be at
     // least N pixels/chars wide" without changing the widget's natural
    -// size calculation. Hints only increase the minimum, never shrink it.
    +// size calculation.  Then a maxW/maxH cap in pixels or characters is
    +// applied as a ceiling (a percentage cap waits for the layout).
     
     void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
         if (wclsHas(w, WGT_METHOD_CALC_MIN_SIZE)) {
    @@ -318,6 +339,10 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
                 w->calcMinH = hintH;
             }
         }
    +
    +    // The cap wins over the minimum, as in the layout
    +    w->calcMinW = capMin(w->calcMinW, w->maxW, 0, font);
    +    w->calcMinH = capMin(w->calcMinH, w->maxH, 0, font);
     }
     
     
    @@ -380,7 +405,7 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
             }
     
             count++;
    -        totalMin    += horiz ? c->calcMinW : c->calcMinH;
    +        totalMin    += boxChildMin(c, horiz, innerW, innerH, font);
             totalWeight += c->weight;
     
             if (c->weight > 0 && (horiz ? c->maxW : c->maxH)) {
    @@ -461,8 +486,7 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
                 continue;
             }
     
    -        int32_t cmin     = horiz ? c->calcMinW : c->calcMinH;
    -        int32_t mainSize = cmin;
    +        int32_t mainSize = boxChildMin(c, horiz, innerW, innerH, font);
     
             // Distribute extra space by weight
             if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
    diff --git a/src/libs/kpunch/libdvx/widgetScrollbar.c b/src/libs/kpunch/libdvx/widgetScrollbar.c
    index dcde6ad2..48e80b3b 100644
    --- a/src/libs/kpunch/libdvx/widgetScrollbar.c
    +++ b/src/libs/kpunch/libdvx/widgetScrollbar.c
    @@ -24,7 +24,8 @@
     //
     // These are not widgets themselves -- they are stateless rendering and
     // hit-testing utilities for every widget that draws its own scrollbars
    -// (ScrollPane, TreeView, TextArea, ListBox, ListView, DBGrid, Terminal).
    +// (ScrollPane, TreeView, TextArea, ListBox, ListView, DBGrid, Terminal),
    +// and widgetScrollbarNeeds, which bars a scrolled area needs.
     // Each owning widget stores its own scroll position; these functions are
     // purely geometric.
     //
    @@ -159,3 +160,33 @@ ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSi
     
         return ScrollHitThumbE;
     }
    +
    +
    +// Which scrollbars a scrolled area needs: content totalW x totalH in a
    +// viewport *innerW x *innerH (the area inside the border).  A vertical
    +// bar narrows the viewport, which may make the content too wide; a
    +// horizontal bar shortens it, which may make the content too tall.
    +// *innerW and *innerH come back less the bars.  Rows of a fixed height
    +// fit the same way: rows * height > height of the viewport exactly when
    +// there are more rows than whole rows fit.
    +void widgetScrollbarNeeds(int32_t totalW, int32_t totalH, int32_t *innerW, int32_t *innerH, bool *needV, bool *needH) {
    +    bool vertical = totalH > *innerH;
    +
    +    if (vertical) {
    +        *innerW -= WGT_SB_W;
    +    }
    +
    +    bool horizontal = totalW > *innerW;
    +
    +    if (horizontal) {
    +        *innerH -= WGT_SB_W;
    +
    +        if (!vertical && totalH > *innerH) {
    +            vertical  = true;
    +            *innerW  -= WGT_SB_W;
    +        }
    +    }
    +
    +    *needV = vertical;
    +    *needH = horizontal;
    +}
    diff --git a/src/libs/kpunch/sql/dvxSqlVfs.c b/src/libs/kpunch/sql/dvxSqlVfs.c
    index 173472f6..fb59262f 100644
    --- a/src/libs/kpunch/sql/dvxSqlVfs.c
    +++ b/src/libs/kpunch/sql/dvxSqlVfs.c
    @@ -224,7 +224,7 @@ static int vfsAccess(sqlite3_vfs *vfs, const char *name, int flags, int *result)
         }
     
         if (flags == SQLITE_ACCESS_EXISTS) {
    -        *result = stat(path, &st) == 0 && (!S_ISREG(st.st_mode) || st.st_size > 0);
    +        *result = stat(path, &st) == 0 && (!S_ISREG(st.st_mode) || platformStatSize(&st) > 0);
         } else {
             *result = access(path, flags == SQLITE_ACCESS_READWRITE ? R_OK | W_OK : R_OK) == 0;
         }
    @@ -395,7 +395,7 @@ static int vfsFileSize(sqlite3_file *file, sqlite3_int64 *size) {
             return SQLITE_IOERR_FSTAT;
         }
     
    -    *size = st.st_size;
    +    *size = (sqlite3_int64)platformStatSize(&st);
         return SQLITE_OK;
     }
     
    @@ -687,16 +687,22 @@ static int vfsSectorSize(sqlite3_file *file) {
     }
     
     
    -// False when offset does not fit the platform's off_t (DJGPP: 2 GB).
    +// False when offset is past what the disk's files reach.  DOS: llseek's
    +// 64-bit offset reaches a FAT32 file's whole 4 GB (lseek's off_t stops at
    +// 2 GB); elsewhere the platform's off_t.
     static bool vfsSeek(VfsFileT *vf, sqlite3_int64 offset) {
    +#ifdef __DJGPP__
    +    bool ok = offset >= 0 && offset <= UINT32_MAX && llseek(vf->fd, (offset_t)offset, SEEK_SET) == (offset_t)offset;
    +#else
         off_t pos = (off_t)offset;
    +    bool  ok  = (sqlite3_int64)pos == offset && lseek(vf->fd, pos, SEEK_SET) == pos;
    +#endif
     
    -    if ((sqlite3_int64)pos != offset || lseek(vf->fd, pos, SEEK_SET) != pos) {
    +    if (!ok) {
             sLastErrno = errno;
    -        return false;
         }
     
    -    return true;
    +    return ok;
     }
     
     
    diff --git a/src/libs/kpunch/xfer/xfer.c b/src/libs/kpunch/xfer/xfer.c
    index 6adf8b0c..746de49d 100644
    --- a/src/libs/kpunch/xfer/xfer.c
    +++ b/src/libs/kpunch/xfer/xfer.c
    @@ -40,6 +40,7 @@
     #include 
     #include 
     #include 
    +#include 
     
     // ABI stamp checked by the loader (see DVX_ABI_VERSION).
     DVX_ABI_STAMP(xfer);
    @@ -108,13 +109,8 @@ DVX_ABI_STAMP(xfer);
     static const uint8_t sDetectSeq[] = { '*', '*', 0x18, 'B', '0', '0' };
     #define DETECT_LEN              ((int32_t)sizeof(sDetectSeq))
     
    -// DOS device names: a file named like one (with any extension) opens the
    -// device instead of creating a file.
    -static const char *const sReservedNames[] = {
    -    "AUX", "CLOCK$", "COM1", "COM2", "COM3", "COM4", "CON", "LPT1", "LPT2", "LPT3", "NUL", "PRN"
    -};
    -
    -#define RESERVED_COUNT          ((int32_t)(sizeof(sReservedNames) / sizeof(sReservedNames[0])))
    +// A file the transfer counters (Longs) cannot carry
    +#define TOO_LARGE_MSG           "A file larger than 2 GB cannot be transferred"
     
     static uint32_t sCrc32Table[CRC_TABLE_SIZE];
     static bool     sCrc32Ready = false;
    @@ -131,12 +127,13 @@ static bool    checkOk(const XferT *x, int32_t dataLen);
     static uint8_t checksum8(const uint8_t *data, int32_t len);
     static bool    drainWhileAsking(XferT *x);
     static void    failOpen(XferT *x, const char *path, bool tellPeer);
    -static long    fileSizeOf(const char *path);
    +static int64_t fileSizeOf(const char *path);
     static bool    initCommon(XferT *x, XferProtoE proto, const XferIoT *io, const XferOptionsT *opt, bool sending);
     static void    joinPath(const char *dir, const char *name, char *out);
     static bool    makeUniquePath(const char *dir, const char *name, char *out);
     static void    mapNamePart(const char *src, int32_t srcLen, char *dst, int32_t dstMax);
     static bool    openReceiveFile(XferT *x, const char *path, bool append, int32_t localSize);
    +static void    prefixReserved(char *name, bool longNames);
     static void    queueByte(XferT *x, uint8_t b);
     static void    queueData(XferT *x);
     static void    queueHeader(XferT *x, bool endOfBatch);
    @@ -252,18 +249,12 @@ static void failOpen(XferT *x, const char *path, bool tellPeer) {
     }
     
     
    -// Size of an existing file, or -1 when it cannot be opened.
    -static long fileSizeOf(const char *path) {
    -    FILE *fp = fopen(path, "rb");
    +// Size of an existing file or folder (DOS sizes reach 4 GB - 1: never
    +// read as "missing"), or -1 when there is none.
    +static int64_t fileSizeOf(const char *path) {
    +    struct stat st;
     
    -    if (!fp) {
    -        return -1;
    -    }
    -
    -    fseek(fp, 0, SEEK_END);
    -    long size = ftell(fp);
    -    fclose(fp);
    -    return size;
    +    return (stat(path, &st) == 0) ? (int64_t)platformStatSize(&st) : -1;
     }
     
     
    @@ -305,16 +296,17 @@ static void joinPath(const char *dir, const char *name, char *out) {
     }
     
     
    -// Build dir + name, adding "~N" to the base name until no file of that
    -// name exists.
    +// Build dir + name, adding "~N" to the base name until no file or folder
    +// of that name exists.  The base is what comes before the extension (the
    +// last dot of a long name, the only one of an 8.3 name) and stays within
    +// the disk's limit with the suffix.
     static bool makeUniquePath(const char *dir, const char *name, char *out) {
    -    char        base[BASE_MAX + 1];
    +    bool        longNames = platformLongNames(dir);
         char        cand[XFER_NAME_MAX + SUFFIX_MAX];
    -    const char *dot     = strchr(name, '.');
    -    const char *ext     = dot ? dot : "";
    -    int32_t     baseLen = dot ? (int32_t)(dot - name) : (int32_t)strlen(name);
    -
    -    snprintf(base, sizeof(base), "%.*s", (int)baseLen, name);
    +    const char *dot       = longNames ? strrchr(name, '.') : strchr(name, '.');
    +    const char *ext       = dot ? dot : "";
    +    int32_t     baseLen   = dot ? (int32_t)(dot - name) : (int32_t)strlen(name);
    +    int32_t     baseMax   = longNames ? PLATFORM_LONG_NAME_MAX - (int32_t)strlen(ext) : BASE_MAX;
     
         for (int32_t n = 0; n <= UNIQUE_MAX; n++) {
             char suffix[SUFFIX_MAX];
    @@ -325,9 +317,9 @@ static bool makeUniquePath(const char *dir, const char *name, char *out) {
                 snprintf(suffix, sizeof(suffix), "~%d", (int)n);
             }
     
    -        int32_t keep = BASE_MAX - (int32_t)strlen(suffix);
    +        int32_t keep = DVX_MIN(baseLen, baseMax - (int32_t)strlen(suffix));
     
    -        snprintf(cand, sizeof(cand), "%.*s%s%s", (int)keep, base, suffix, ext);
    +        snprintf(cand, sizeof(cand), "%.*s%s%s", (int)keep, name, suffix, ext);
             joinPath(dir, cand, out);
     
             if (fileSizeOf(out) < 0) {
    @@ -374,8 +366,23 @@ static bool openReceiveFile(XferT *x, const char *path, bool append, int32_t loc
         snprintf(x->prog.fileName, sizeof(x->prog.fileName), "%s", path);
         x->prog.fileSize  = x->prog.askRemoteSize;
         x->prog.resumedAt = append ? localSize : 0;
    -    xferSetFileBytes(x, x->prog.resumedAt);
    -    return true;
    +    return xferSetFileBytes(x, x->prog.resumedAt);
    +}
    +
    +
    +// A '_' in front of a name xferSafeName built when it is a device name
    +// (CON.TXT would open the console): the one rule such a name can still
    +// break.  The name stays within XFER_NAME_MAX - 1 characters.
    +static void prefixReserved(char *name, bool longNames) {
    +    if (!(longNames ? platformValidateLongName(name) : platformValidateShortName(name))) {
    +        return;
    +    }
    +
    +    size_t len = DVX_MIN(strlen(name), (size_t)XFER_NAME_MAX - 2);
    +
    +    memmove(name + 1, name, len);
    +    name[0]       = RESERVED_PREFIX;
    +    name[len + 1] = '\0';
     }
     
     
    @@ -622,7 +629,10 @@ static void recvPacket(XferT *x, uint32_t nowMs) {
             return;
         }
     
    -    xferSetFileBytes(x, x->prog.fileBytes + keep);
    +    if (!xferSetFileBytes(x, (int64_t)x->prog.fileBytes + keep)) {
    +        return;
    +    }
    +
         x->blockNum = blk;
         x->eotSeen  = false;
         x->tries    = 0;
    @@ -775,9 +785,10 @@ static void sendByte(XferT *x, uint8_t b, uint32_t nowMs) {
                 // requests to resend: resending on them would duplicate block 1
                 // and shift every later ACK by one block.
                 if (b == XFER_ACK) {
    -                xferSetFileBytes(x, x->prog.fileBytes + x->outData);
    -                queueData(x);
    -                x->deadline = nowMs + XFER_REPLY_MS;
    +                if (xferSetFileBytes(x, (int64_t)x->prog.fileBytes + x->outData)) {
    +                    queueData(x);
    +                    x->deadline = nowMs + XFER_REPLY_MS;
    +                }
                 } else if (b == XFER_NAK) {
                     retrySend(x, nowMs);
                 }
    @@ -1049,15 +1060,25 @@ void xferFail(XferT *x, const char *msg) {
     
     
     int32_t xferFileInfo(const XferT *x, const char *mode, char *out, int32_t outSize) {
    -    char    name[XFER_NAME_MAX];
    -    int32_t left  = x->prog.fileCount - x->prog.fileIndex;
    -    int32_t bytes = x->prog.batchSize - x->batchDone;
    +    const char *path      = x->paths[x->prog.fileIndex];
    +    char        name[XFER_NAME_MAX];
    +    char        fields[XFER_INFO_FIELDS_MAX];
    +    int32_t     left      = x->prog.fileCount - x->prog.fileIndex;
    +    int32_t     fieldsLen = snprintf(fields, sizeof(fields), "%ld 0 %s 0 %ld %lld", (long)x->prog.fileSize, mode, (long)left, (long long)(x->batchTotal - x->batchDone));
     
    -    xferSafeName(x->paths[x->prog.fileIndex], name);
    +    // The long name where it fits beside the fields, else the 8.3 one
    +    // (which always does: a YMODEM block 0 holds 128 bytes)
    +    xferSafeName(path, platformLongNames(path), name);
     
    -    int32_t nameLen = snprintf(out, (size_t)outSize, "%s", name);
    +    if ((int32_t)strlen(name) + 1 + fieldsLen + 1 > outSize) {
    +        xferSafeName(path, false, name);
    +    }
     
    -    return nameLen + 1 + snprintf(out + nameLen + 1, (size_t)(outSize - nameLen - 1), "%ld 0 %s 0 %ld %ld", (long)x->prog.fileSize, mode, (long)left, (long)bytes);
    +    int32_t nameLen = (int32_t)strlen(name);
    +
    +    memcpy(out, name, (size_t)nameLen + 1);
    +    memcpy(out + nameLen + 1, fields, (size_t)fieldsLen + 1);
    +    return nameLen + 1 + fieldsLen;
     }
     
     
    @@ -1082,19 +1103,19 @@ void xferFlushOut(XferT *x) {
     
     
     XferOpenE xferOpenReceived(XferT *x, const char *remoteName, int32_t remoteSize, bool canResume, bool resumeRequested) {
    -    xferSafeName(remoteName, x->recvName);
    +    xferSafeName(remoteName, platformLongNames(x->paths[0]), x->recvName);
         joinPath(x->paths[0], x->recvName, x->askPath);
         x->prog.askRemoteSize = remoteSize;
         x->prog.askLocalSize  = 0;
         x->prog.askCanResume  = false;
     
    -    long local = fileSizeOf(x->askPath);
    +    int64_t local = fileSizeOf(x->askPath);
     
         if (local < 0) {
             return openReceiveFile(x, x->askPath, false, 0) ? XferOpenOkE : XferOpenFailE;
         }
     
    -    x->prog.askLocalSize = ((int64_t)local > INT32_MAX) ? INT32_MAX : (int32_t)local;
    +    x->prog.askLocalSize = (local > INT32_MAX) ? INT32_MAX : (int32_t)local;
         x->prog.askCanResume = canResume && remoteSize > 0 && x->prog.askLocalSize < remoteSize;
     
         if (resumeRequested && x->prog.askCanResume) {
    @@ -1120,9 +1141,8 @@ bool xferOpenSendFile(XferT *x) {
             return false;
         }
     
    -    fseek(x->fp, 0, SEEK_END);
    -    long size = ftell(x->fp);
    -    fseek(x->fp, 0, SEEK_SET);
    +    // xferStartSend refused a file past 2 GB
    +    int64_t size = fileSizeOf(path);
     
         snprintf(x->prog.fileName, sizeof(x->prog.fileName), "%s", path);
         x->prog.fileSize  = (size < 0) ? 0 : (int32_t)size;
    @@ -1227,10 +1247,8 @@ void xferQueueBytes(XferT *x, const uint8_t *data, int32_t len) {
     }
     
     
    -void xferSafeName(const char *name, char *out) {
    +void xferSafeName(const char *name, bool longNames, char *out) {
         const char *leaf = name;
    -    char        base[BASE_MAX + 1];
    -    char        ext[EXT_MAX + 1];
     
         for (const char *p = name; *p; p++) {
             if (*p == '/' || *p == '\\' || *p == ':') {
    @@ -1238,32 +1256,52 @@ void xferSafeName(const char *name, char *out) {
             }
         }
     
    -    const char *dot     = strrchr(leaf, '.');
    -    int32_t     baseLen = dot ? (int32_t)(dot - leaf) : (int32_t)strlen(leaf);
    +    if (longNames) {
    +        int32_t n = 0;
     
    -    mapNamePart(leaf, baseLen, base, BASE_MAX);
    -    mapNamePart(dot ? dot + 1 : "", dot ? (int32_t)strlen(dot + 1) : 0, ext, EXT_MAX);
    -
    -    if (base[0] == '\0') {
    -        snprintf(base, sizeof(base), "%s", FALLBACK_NAME);
    -    }
    -
    -    for (int32_t i = 0; i < RESERVED_COUNT; i++) {
    -        if (strcmp(base, sReservedNames[i]) == 0) {
    -            memmove(base + 1, base, BASE_MAX - 1);
    -            base[0]        = RESERVED_PREFIX;
    -            base[BASE_MAX] = '\0';
    -            break;
    +        // Leading blanks, trailing dots and blanks go (DOS drops them)
    +        for (const char *p = leaf + strspn(leaf, " "); *p && n < PLATFORM_LONG_NAME_MAX; p++) {
    +            out[n++] = platformLongNameChar(*p) ? *p : '_';
             }
    +
    +        while (n > 0 && (out[n - 1] == '.' || out[n - 1] == ' ')) {
    +            n--;
    +        }
    +
    +        out[n] = '\0';
    +
    +        if (n == 0) {
    +            snprintf(out, XFER_NAME_MAX, "%s", FALLBACK_NAME);
    +        }
    +    } else {
    +        char        base[BASE_MAX + 1];
    +        char        ext[EXT_MAX + 1];
    +        const char *dot     = strrchr(leaf, '.');
    +        int32_t     baseLen = dot ? (int32_t)(dot - leaf) : (int32_t)strlen(leaf);
    +
    +        mapNamePart(leaf, baseLen, base, BASE_MAX);
    +        mapNamePart(dot ? dot + 1 : "", dot ? (int32_t)strlen(dot + 1) : 0, ext, EXT_MAX);
    +
    +        if (base[0] == '\0') {
    +            snprintf(base, sizeof(base), "%s", FALLBACK_NAME);
    +        }
    +
    +        snprintf(out, XFER_NAME_MAX, "%s%s%s", base, ext[0] ? "." : "", ext);
         }
     
    -    snprintf(out, XFER_NAME_MAX, "%s%s%s", base, ext[0] ? "." : "", ext);
    +    prefixReserved(out, longNames);
     }
     
     
    -void xferSetFileBytes(XferT *x, int32_t bytes) {
    -    x->prog.fileBytes  = bytes;
    -    x->prog.batchBytes = x->batchDone + bytes;
    +bool xferSetFileBytes(XferT *x, int64_t bytes) {
    +    if (bytes > INT32_MAX) {
    +        xferFail(x, TOO_LARGE_MSG);
    +        return false;
    +    }
    +
    +    x->prog.fileBytes  = (int32_t)bytes;
    +    x->prog.batchBytes = (int32_t)DVX_MIN(x->batchDone + bytes, (int64_t)INT32_MAX);
    +    return true;
     }
     
     
    @@ -1315,23 +1353,29 @@ bool xferStartSend(XferT *x, XferProtoE proto, const XferIoT *io, const XferOpti
             count = 1;
         }
     
    -    // Every file must exist before anything is sent; the batch size
    -    // drives the batch progress.
    -    x->prog.batchSize = 0;
    -
    +    // Every file must exist before anything is sent, and none may pass
    +    // what a file's count carries (2 GB); the batch size drives the batch
    +    // progress (unknown past 2 GB).
         for (int32_t i = 0; i < count; i++) {
             snprintf(x->paths[i], sizeof(x->paths[i]), "%s", paths[i]);
     
    -        long size = fileSizeOf(x->paths[i]);
    +        int64_t size = fileSizeOf(x->paths[i]);
     
             if (size < 0) {
                 failOpen(x, x->paths[i], false);
                 return false;
             }
     
    -        x->prog.batchSize += (int32_t)size;
    +        if (size > INT32_MAX) {
    +            xferFinish(x, XferFailedE, TOO_LARGE_MSG);
    +            return false;
    +        }
    +
    +        x->batchTotal += size;
         }
     
    +    x->prog.batchSize = (x->batchTotal > INT32_MAX) ? -1 : (int32_t)x->batchTotal;
    +
         x->prog.fileCount = count;
         x->deadline       = nowMs + SEND_START_MS;
     
    diff --git a/src/libs/kpunch/xfer/xfer.dhs b/src/libs/kpunch/xfer/xfer.dhs
    index 610fed2d..73b34859 100644
    --- a/src/libs/kpunch/xfer/xfer.dhs
    +++ b/src/libs/kpunch/xfer/xfer.dhs
    @@ -109,14 +109,14 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
       XferT *xferCreate(void)                                   Allocate an idle transfer (NULL when out of memory).
       void xferDestroy(x)                                       Close any open file and free the transfer.
       void xferOptionsDefaults(opt)                             Ask when a file exists, no window limit, no extra escaping, no resume request.
    -  bool xferStartSend(x, proto, io, opt, paths, count, now)  Send count files (XMODEM sends the first). False, with the reason in the message, when a file cannot be opened. opt may be NULL.
    +  bool xferStartSend(x, proto, io, opt, paths, count, now)  Send count files (XMODEM sends the first). False, with the reason in the message, when a file cannot be opened or is larger than 2 GB. opt may be NULL.
       bool xferStartReceive(x, proto, io, opt, target, now)     XMODEM: target is the file to create. YMODEM and ZMODEM: target is the folder for the files.
       XferStatusE xferPoll(x, now)                              Move the transfer along. now is a millisecond clock (wrapping is fine).
       void xferCancel(x)                                        Send the cancel sequence to the other side and end with XferCancelledE.
       void xferAnswer(x, action, now)                           Answer XferAskingE: XferExistResumeE, RenameE, OverwriteE or SkipE.
       const XferProgressT *xferProgress(x)                      Progress and outcome.
       bool xferDetectFeed(d, byte)                              Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself.
    -  void xferSafeName(name, out)                              Reduce a sender's file name to a safe 8.3 name: no path, no drive, no DOS device name (CON, PRN, COM1 and so on get a leading _), only portable characters. out holds XFER_NAME_MAX bytes.
    +  void xferSafeName(name, longNames, out)                   Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1 and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1).
       uint16_t xferCrc16(data, len)                             CRC-16/XMODEM (polynomial 0x1021).
       uint32_t xferCrc32(data, len)                             CRC-32 (IEEE 802.3), as ZMODEM uses it.
       int32_t xferBasNew(&x)                                    For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way).
    @@ -142,15 +142,17 @@ Packets are SOH (128 bytes) or STX (1024 bytes), the block number and its comple
     
     .h2 YMODEM
     
    -A batch of files, always with CRC and 1K blocks (a tail of at most 128 bytes goes in a 128-byte block). Block 0 carries the file name and size before each file, and an empty block 0 ends the batch. The receiver refuses the first EOT of each file and accepts the second, and cuts the padding using the size.
    +A batch of files, always with CRC and 1K blocks (a tail of at most 128 bytes goes in a 128-byte block). Block 0 carries the file name and size before each file (the long name when it fits in the 128-byte block beside the other fields, else the 8.3 name), and an empty block 0 ends the batch. The receiver refuses the first EOT of each file and accepts the second, and cuts the padding using the size.
     
     .h2 ZMODEM
     
     Headers are sent in hex (to start and answer) or binary with CRC-16 or CRC-32; data travels in subpackets with a CRC over the data and the frame end. ZDLE escaping protects ZDLE, DLE, XON and XOFF (with and without the high bit) and a CR after '@'; with escapeCtl every control character is escaped (the sender announces it with ZSINIT; the receiver asks for it in ZRINIT). Five CANs in a row cancel.
     
    -The sender streams: it keeps at most 8 KB unacknowledged, asks for a progress acknowledgement every 4 KB, honours the receiver's buffer size (a receiver without flow control should set rxWindow to what its hardware buffer holds, so a slow frame never loses data), and builds a subpacket only when the transport has sent nearly everything it was given (XferIoT.pending). On an error the receiver sends ZRPOS with the last good position; the sender goes back there and halves its subpacket size (down to 64 bytes), then doubles it again after eight clean subpackets. A sender that gets no acknowledgement for the window goes back to the last acknowledged position. File names travel in the ZFILE subpacket with the size and the files and bytes left in the batch, which the receiver shows as batch progress. Commands from the other side (ZCOMMAND) are never run.
    +The sender streams: it keeps at most 8 KB unacknowledged, asks for a progress acknowledgement every 4 KB, honours the receiver's buffer size (a receiver without flow control should set rxWindow to what its hardware buffer holds, so a slow frame never loses data), and builds a subpacket only when the transport has sent nearly everything it was given (XferIoT.pending). On an error the receiver sends ZRPOS with the last good position; the sender goes back there and halves its subpacket size (down to 64 bytes), then doubles it again after eight clean subpackets. A sender that gets no acknowledgement for the window goes back to the last acknowledged position. File names (long names as they are) travel in the ZFILE subpacket with the size and the files and bytes left in the batch, which the receiver shows as batch progress. Commands from the other side (ZCOMMAND) are never run.
     
    -Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping and resume.
    +Files are at most 2 GB (sizes and positions are 32-bit): a send refuses a larger file, a size offered past 2 GB reads as unknown, and a receive that passes 2 GB stops with "A file larger than 2 GB cannot be transferred". A file already on the disk is never mistaken for a missing one, whatever its size.
    +
    +Interoperability: the engines are tested against lrz and lsz (lrzsz 0.12.20) in both directions for every protocol, including escaping, resume and long file names.
     
     .topic lib.xfer.basic
     .title File Transfers from BASIC
    @@ -186,7 +188,7 @@ Transfers over a connection (conn.bas: ConnSendFiles, ConnReceiveFiles) are the
       XferCancel handle                    Stop a running transfer (the other side is told).
       XferClose handle                     Free the handle.
       XferCrc16(bytes), XferCrc32(bytes)   xferCrc16 / xferCrc32 of a BINARY STRING (every byte value counts); the CRC-32 as a LONG (negative when its top bit is set).
    -  XferSafeName$(fileName)              xferSafeName: the 8.3 name a receive stores a sender's file name as.
    +  XferSafeName$(fileName, folder)      xferSafeName: the name a receive into folder stores a sender's file name as (a long name where folder's disk takes them, else 8.3).
     .endtable
     
     XFER_WINDOW_MAX (xfer.bas) is the largest ZMODEM receive window conn.bas's ConnSetXferWindow takes.
    diff --git a/src/libs/kpunch/xfer/xfer.h b/src/libs/kpunch/xfer/xfer.h
    index 9826ee64..510058a0 100644
    --- a/src/libs/kpunch/xfer/xfer.h
    +++ b/src/libs/kpunch/xfer/xfer.h
    @@ -48,11 +48,13 @@
     #ifndef XFER_H
     #define XFER_H
     
    +#include "dvxPlat.h"
    +
     #include 
     #include 
     
     #define XFER_PATH_MAX           260
    -#define XFER_NAME_MAX           13      // 8.3 name plus NUL
    +#define XFER_NAME_MAX           (PLATFORM_LONG_NAME_MAX + 1) // a long name plus NUL (xferSafeName)
     #define XFER_MSG_MAX            96
     #define XFER_MAX_FILES          16
     
    @@ -172,9 +174,12 @@ XferStatusE xferPoll(XferT *x, uint32_t nowMs);
     // Progress of the transfer (never NULL for a valid x)
     const XferProgressT *xferProgress(const XferT *x);
     
    -// Reduce a sender-supplied file name to a safe 8.3 name (no path, no
    -// device name, only portable characters).  out holds XFER_NAME_MAX bytes.
    -void xferSafeName(const char *name, char *out);
    +// Reduce a file name a sender supplied to one safe to store: no path, no
    +// device name.  With longNames (the disk takes them, platformLongNames)
    +// a long name -- characters a long name cannot hold become '_' -- else an
    +// 8.3 name of portable characters in upper case.  out holds XFER_NAME_MAX
    +// bytes.
    +void xferSafeName(const char *name, bool longNames, char *out);
     
     // BASIC handles (xferBas.c, src/include/basic/xfer.bas) for the BASIC
     // functions of other libraries, which start transfers over their own
    diff --git a/src/libs/kpunch/xfer/xferBas.c b/src/libs/kpunch/xfer/xferBas.c
    index b4d4b724..50cf81b3 100644
    --- a/src/libs/kpunch/xfer/xferBas.c
    +++ b/src/libs/kpunch/xfer/xferBas.c
    @@ -100,7 +100,7 @@ int32_t                     XferPoll(int32_t handle);
     int32_t                     XferProtocol(int32_t handle);
     int32_t                     XferReceive(int32_t com, int32_t proto, const char *target);
     int32_t                     XferResumedAt(int32_t handle);
    -const char                 *XferSafeName(const char *fileName);
    +const char                 *XferSafeName(const char *fileName, const char *folder);
     int32_t                     XferSendFile(int32_t com, int32_t proto, const char *path);
     int32_t                     XferSending(int32_t handle);
     int32_t                     XferSize(int32_t handle);
    @@ -402,12 +402,12 @@ int32_t XferResumedAt(int32_t handle) {
     }
     
     
    -// A file name a sender offered, made safe to store here: an 8.3 name
    -// without a folder or a device name, only characters every disk takes.
    -const char *XferSafeName(const char *fileName) {
    +// A file name a sender offered, made safe to store in folder: a long name
    +// where folder's disk takes them, else an 8.3 name (xferSafeName).
    +const char *XferSafeName(const char *fileName, const char *folder) {
         static char name[XFER_NAME_MAX];
     
    -    xferSafeName(fileName ? fileName : "", name);
    +    xferSafeName(fileName ? fileName : "", platformLongNames(folder), name);
         return name;
     }
     
    diff --git a/src/libs/kpunch/xfer/xferInt.h b/src/libs/kpunch/xfer/xferInt.h
    index c1bc516c..c4d8aa06 100644
    --- a/src/libs/kpunch/xfer/xferInt.h
    +++ b/src/libs/kpunch/xfer/xferInt.h
    @@ -43,6 +43,11 @@
     // XMODEM packet: start byte, block, complement, data, CRC or checksum
     #define XFER_BLOCK_SMALL        128
     #define XFER_BLOCK_LARGE        1024
    +
    +// The fields xferFileInfo puts after a name ("size mtime mode serial
    +// files-left bytes-left" and the NUL): a long name goes out when it fits
    +// beside them, the 8.3 name otherwise
    +#define XFER_INFO_FIELDS_MAX    64
     #define XFER_PKT_HDR            3
     #define XFER_PKT_CRC            2
     #define XFER_PKT_SUM            1
    @@ -151,7 +156,8 @@ struct XferS {
         uint8_t       pkt[XFER_IN_MAX];             // received packet being assembled
         int32_t       pktLen;
         int32_t       pktNeed;
    -    int32_t       batchDone;                    // bytes of the files already finished
    +    int64_t       batchDone;                    // bytes of the files already finished
    +    int64_t       batchTotal;                   // sender: bytes of every file of the batch
         uint32_t      deadline;                     // next timeout
         int32_t       tries;                        // retries of the current packet / start pokes
         int32_t       canCount;                     // consecutive CANs seen (XMODEM family)
    @@ -186,7 +192,9 @@ void xferFail(XferT *x, const char *msg);
     // bytesleft" for paths[fileIndex] (mtime 0 = unknown, serial 0).  Every
     // field is sent: receivers such as lrz read absent fields from
     // uninitialized memory, so the name case and file date they chose varied
    -// from run to run.  Returns the length, excluding the final NUL.
    +// from run to run.  The name is the file's long name where its disk has
    +// them and it fits beside the fields, else its 8.3 name.  Returns the
    +// length, excluding the final NUL.
     int32_t xferFileInfo(const XferT *x, const char *mode, char *out, int32_t outSize);
     
     // End the transfer with a status and message (closes the file).
    @@ -206,8 +214,10 @@ bool xferOpenSendFile(XferT *x);
     // Append bytes to the output queue behind anything unsent.
     void xferQueueBytes(XferT *x, const uint8_t *data, int32_t len);
     
    -// Set the bytes done in the current file (and the batch progress).
    -void xferSetFileBytes(XferT *x, int32_t bytes);
    +// Set the bytes done in the current file (and the batch progress, which
    +// stops at INT32_MAX).  A file's count is a Long (XferBytes): a file past
    +// 2 GB fails the transfer instead of wrapping, and this returns false.
    +bool xferSetFileBytes(XferT *x, int64_t bytes);
     
     // Wrap-safe "now is at or past deadline"
     bool xferTimeReached(uint32_t nowMs, uint32_t deadline);
    diff --git a/src/libs/kpunch/xfer/xferZmodem.c b/src/libs/kpunch/xfer/xferZmodem.c
    index b8a3b8b9..a7398e49 100644
    --- a/src/libs/kpunch/xfer/xferZmodem.c
    +++ b/src/libs/kpunch/xfer/xferZmodem.c
    @@ -166,7 +166,7 @@
     
     #define RZ_COMMAND              "rz\r"
     #define OVER_AND_OUT            "OO"
    -#define FILE_INFO_MAX           96
    +#define FILE_INFO_MAX           (XFER_NAME_MAX + XFER_INFO_FIELDS_MAX)
     #define FILE_MODE_TEXT          "100644"
     #define HEX_DIGITS              "0123456789abcdef"
     #define DECIMAL                 10
    @@ -315,7 +315,11 @@ static void dataDone(XferT *x, bool good, uint32_t nowMs) {
                 }
     
                 zm->filePos += (uint32_t)x->pktLen;
    -            xferSetFileBytes(x, (int32_t)zm->filePos);
    +
    +            if (!xferSetFileBytes(x, zm->filePos)) {
    +                return;
    +            }
    +
                 x->tries = 0;
     
                 if (zm->dataEnd == ZCRCQ || zm->dataEnd == ZCRCW) {
    @@ -421,7 +425,7 @@ static void fileInfo(XferT *x, uint32_t nowMs) {
         }
     
         if (parsed > INFO_BYTES_LEFT && vals[INFO_BYTES_LEFT] > 0 && (int64_t)vals[INFO_BYTES_LEFT] <= (int64_t)INT32_MAX - x->batchDone) {
    -        x->prog.batchSize = x->batchDone + (int32_t)vals[INFO_BYTES_LEFT];
    +        x->prog.batchSize = (int32_t)(x->batchDone + vals[INFO_BYTES_LEFT]);
         }
     
         xferZmAnswered(x, xferOpenReceived(x, name, remote, true, x->zm.fileOpts == ZCRESUM), nowMs);
    @@ -688,7 +692,10 @@ static void queueNextSubpacket(XferT *x, uint32_t nowMs) {
         zm->filePos     = end;
         zm->sinceSync  += (uint32_t)n;
         zm->sinceQuery  = (frameEnd == ZCRCQ) ? 0 : zm->sinceQuery + (uint32_t)n;
    -    xferSetFileBytes(x, (int32_t)end);
    +
    +    if (!xferSetFileBytes(x, end)) {
    +        return;
    +    }
     
         if (eof) {
             queuePosHeader(x, ZEOF, end, false);
    @@ -884,7 +891,11 @@ static void reposition(XferT *x, uint32_t pos, bool afterError, uint32_t nowMs)
         zm->frameEnded = false;
         x->phase       = XferPhZsDataE;
         x->deadline    = nowMs + XFER_REPLY_MS;
    -    xferSetFileBytes(x, (int32_t)pos);
    +
    +    if (!xferSetFileBytes(x, pos)) {
    +        return;
    +    }
    +
         resetOut(x);
         queuePosHeader(x, ZDATA, pos, false);
     }
    diff --git a/src/test/abi/abiSizes.txt b/src/test/abi/abiSizes.txt
    index 115ec7e8..9b594d4b 100644
    --- a/src/test/abi/abiSizes.txt
    +++ b/src/test/abi/abiSizes.txt
    @@ -2,7 +2,7 @@
     # (i386 DJGPP), recorded at the DVX_ABI_VERSION below.  Generated by
     # `make -C src/test/abi update`; read abiCheck.py and DVX_ABI_VERSION in
     # src/libs/kpunch/libdvx/dvxTypes.h before changing it.
    -version 7
    +version 8
     AccelEntryT           12
     AccelTableT           12
     AnsiTermApiT          36
    @@ -25,7 +25,7 @@ BitmapFontT           20
     BlitOpsT              12
     BoxApiT               20
     ButtonApiT            12
    -CanvasApiT            80
    +CanvasApiT            84
     CheckboxApiT          20
     ColorSchemeT          80
     ComboBoxApiT          52
    diff --git a/src/test/basic/Makefile b/src/test/basic/Makefile
    index 9bbe8f79..2d57d986 100644
    --- a/src/test/basic/Makefile
    +++ b/src/test/basic/Makefile
    @@ -37,7 +37,7 @@
     #                        bench/baseline.txt, the Phase 0 baseline) to
     #                        BENCH_NEW (default the last make bench)
     #
    -# Tunables: FUZZ_N (mutations per seed), FUZZ_SEED, FUZZ_STEPS;
    +# Tunables: FUZZ_N (mutations per seed), FUZZ_SEED, FUZZ_STEPS, FUZZ_MEM_MB;
     # BENCH_ARGS (extra basBench options, e.g. -runs 9, -profile, -filter NAME).
     
     include ../../common.mk
    @@ -73,11 +73,21 @@ GOLDEN      = $(abspath $(REPO)/src/test/samples/golden)
     FUZZ_N      = 10
     FUZZ_SEED   = 12345
     FUZZ_STEPS  = 200000
    +FUZZ_MEM_MB = 512
     
     # Corrupt length fields legitimately ask for huge allocations; the
     # deserializer must handle a NULL from malloc, so let ASan return one
    -# instead of aborting the run.
    -ASAN_ENV    = ASAN_OPTIONS=allocator_may_return_null=1:detect_leaks=1
    +# instead of aborting the run.  A fuzzer gets FUZZ_MEM_MB of memory, as
    +# a DOS machine has a limit: past it an allocation fails (a program gets
    +# Out of memory) rather than the host running out.  ASan builds use its
    +# soft RSS limit (an address-space limit breaks its shadow memory),
    +# plain builds an address-space limit.
    +ASAN_ENV    = ASAN_OPTIONS=allocator_may_return_null=1:detect_leaks=1:soft_rss_limit_mb=$(FUZZ_MEM_MB)
    +ifeq ($(TESTSAN),1)
    +FUZZ_MEM_CAP = true
    +else
    +FUZZ_MEM_CAP = ulimit -v $$(($(FUZZ_MEM_MB) * 1024))
    +endif
     
     COMPILER_SRCS = $(DVXBASIC)/compiler/lexer.c $(DVXBASIC)/compiler/ast.c $(DVXBASIC)/compiler/scope.c $(DVXBASIC)/compiler/parse.c $(DVXBASIC)/compiler/parseExpr.c $(DVXBASIC)/compiler/parseStmt.c $(DVXBASIC)/compiler/parseDecl.c $(DVXBASIC)/compiler/resolve.c $(DVXBASIC)/compiler/resolveExpr.c $(DVXBASIC)/compiler/typecheck.c $(DVXBASIC)/compiler/fold.c $(DVXBASIC)/compiler/gen.c $(DVXBASIC)/compiler/genExpr.c $(DVXBASIC)/compiler/genStmt.c $(DVXBASIC)/compiler/compiler.c $(DVXBASIC)/compiler/codegen.c
     RUNTIME_SRCS  = $(DVXBASIC)/runtime/vm.c $(DVXBASIC)/runtime/values.c $(DVXBASIC)/runtime/dates.c $(DVXBASIC)/runtime/serialize.c $(DVXBASIC)/runtime/gfx.c $(DVXBASIC)/runtime/screen.c $(LIBDVX)/dvxPrefs.c
    @@ -121,8 +131,8 @@ corpus: | $(OUTDIR)
     fuzz: build corpus
     	rm -rf $(FUZZWORK)
     	mkdir -p $(FUZZWORK)
    -	cd $(FUZZWORK) && $(ASAN_ENV) $(FUZZ_COMPILE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) $(CORPUS)
    -	cd $(FUZZWORK) && $(ASAN_ENV) $(FUZZ_MODULE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) -corpus $(CORPUS) $(wildcard $(GOLDEN)/*.app)
    +	cd $(FUZZWORK) && $(FUZZ_MEM_CAP) && $(ASAN_ENV) $(FUZZ_COMPILE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) $(CORPUS)
    +	cd $(FUZZWORK) && $(FUZZ_MEM_CAP) && $(ASAN_ENV) $(FUZZ_MODULE) -n $(FUZZ_N) -seed $(FUZZ_SEED) -steps $(FUZZ_STEPS) -corpus $(CORPUS) $(wildcard $(GOLDEN)/*.app)
     
     $(FUZZ_COMPILE): $(FUZZ_COMPILE_OBJS) $(TESTSAN_STAMP) | $(HOSTDIR)
     	$(HOSTCC) $(HOSTLDFLAGS) -o $@ $(FUZZ_COMPILE_OBJS) -lm
    diff --git a/src/test/basic/fixtures/mathFuncs.out b/src/test/basic/fixtures/mathFuncs.out
    index f9b71753..ab75cd73 100644
    --- a/src/test/basic/fixtures/mathFuncs.out
    +++ b/src/test/basic/fixtures/mathFuncs.out
    @@ -2,11 +2,11 @@
     0 1 0 0 0 
     1 -1 3 -3 1024 1.4142135623731 
     0.25 2.5 1000 0.0015 
    -1 7 6 -6 0 -1 
    -True False True False -1 
    --1 0 
    +1 7 6 -6 False True 
    +True False True False True 
    +True False 
     33023 1 2 3 
    --1 
    +True 
     1000000000000 
     0.3 
     0.333333333333333 
    diff --git a/src/test/dos/bigDisk.py b/src/test/dos/bigDisk.py
    new file mode 100644
    index 00000000..85e7384a
    --- /dev/null
    +++ b/src/test/dos/bigDisk.py
    @@ -0,0 +1,194 @@
    +#!/usr/bin/env python3
    +
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +# bigDisk.py -- disk image helpers for runDos.sh (86Box hard disks).
    +#
    +#   bigDisk.py partition IMAGE START COUNT TYPE ACTIVE HEADS SPT
    +#       Write the first partition entry of IMAGE's master boot record (and
    +#       the 55 AA signature): COUNT sectors from sector START, partition
    +#       type TYPE (6 FAT16, 0x0B FAT32), bootable when ACTIVE is 1,
    +#       with the CHS fields for a disk of HEADS heads and SPT sectors per
    +#       track (cylinders past 1023 are written as 1023, as DOS expects).
    +#
    +#   bigDisk.py grow IMAGE START NAME BYTES
    +#       Make the file NAME (an 8.3 name in the root folder) of the FAT32
    +#       partition at sector START BYTES long without writing its data:
    +#       free clusters after the last used one are chained to it in every
    +#       FAT copy, its directory entry gets the size and the FSInfo sector
    +#       the new free count.  A sparse image stays small; the file reads
    +#       as zeros.  The file must exist (mcopy a one-byte file first).
    +
    +import struct
    +import sys
    +
    +SECTOR_BYTES     = 512
    +MBR_TABLE        = 446
    +MBR_SIGNATURE    = 510
    +SIGNATURE        = b"\x55\xaa"
    +ENTRY_BYTES      = 16
    +TABLE_ENTRIES    = 4
    +ACTIVE_FLAG      = 0x80
    +CYL_MAX          = 1023
    +DIR_ENTRY_BYTES  = 32
    +DIR_END          = 0x00
    +DIR_FREE         = 0xE5
    +ATTR_LFN         = 0x0F
    +FAT_ENTRY_BYTES  = 4
    +FAT_MASK         = 0x0FFFFFFF
    +FAT_EOC          = 0x0FFFFFFF
    +FAT_EOC_MIN      = 0x0FFFFFF8
    +FIRST_CLUSTER    = 2
    +FSINFO_FREE      = 488
    +FSINFO_NEXT      = 492
    +FSINFO_UNKNOWN   = 0xFFFFFFFF
    +
    +
    +def chs(lba, heads, spt):
    +    cyl  = min(lba // (heads * spt), CYL_MAX)
    +    head = (lba // spt) % heads
    +    sect = lba % spt + 1
    +
    +    return bytes([head, ((cyl >> 2) & 0xC0) | sect, cyl & 0xFF])
    +
    +
    +def partition(path, start, count, ptype, active, heads, spt):
    +    entry = bytes([ACTIVE_FLAG if active else 0]) + chs(start, heads, spt) + bytes([ptype]) + chs(start + count - 1, heads, spt) + struct.pack("= FIRST_CLUSTER and entry(last) == 0:
    +            last -= 1
    +
    +        if last + need >= FIRST_CLUSTER + cluster_count:
    +            sys.exit("bigDisk: no room for %d bytes" % size)
    +
    +        prev = first_cluster
    +
    +        for n in range(last + 1, last + 1 + need):
    +            set_entry(prev, n)
    +            prev = n
    +
    +        set_entry(prev, FAT_EOC)
    +
    +        for copy in range(fats):
    +            f.seek(fat_offset + copy * fat_sects * sector_bytes)
    +            f.write(fat)
    +
    +        f.seek(dir_pos + 28)
    +        f.write(struct.pack(""; the label
    +' shows the D: sizes.  "D:\Long Folder Name" is left for the File Manager.
    +
    +OPTION EXPLICIT
    +
    +CONST BIG_DRIVE    = "D:\"
    +CONST BIG_FILE     = "D:\BIG3GB.DAT"
    +CONST BIG_BYTES    = 3000000000#
    +CONST FOUR_GB      = 4294967296#
    +CONST OLD_FOLDER   = "D:\Old Projects"
    +CONST OLD_NOTE     = "D:\Old Projects\Read Me First.txt"
    +CONST LONG_DIR     = "Long Folder Name"
    +CONST LONG_FILE    = "My Notes.final.txt"
    +CONST RENAMED      = "Renamed Notes v2.txt"
    +CONST BOOT_DRIVE   = "C:\"
    +CONST ERR_OVERFLOW = 6
    +
    +DIM SHARED failText AS STRING
    +
    +
    +SUB Check(BYVAL ok AS BOOLEAN, BYVAL what AS STRING)
    +    IF NOT ok AND failText = "" THEN
    +        failText = what
    +    END IF
    +END SUB
    +
    +
    +' The error FILELEN raises for path, 0 for none.
    +FUNCTION FileLenError(BYVAL path AS STRING) AS LONG
    +    DIM n AS LONG
    +
    +    ON ERROR GOTO Failed
    +    n = FILELEN(path)
    +    FileLenError = 0
    +    EXIT FUNCTION
    +Failed:
    +    FileLenError = ERR
    +    RESUME Done
    +Done:
    +END FUNCTION
    +
    +
    +' The error LOF raises for path opened for BINARY, 0 for none.
    +FUNCTION LofError(BYVAL path AS STRING) AS LONG
    +    DIM n AS LONG
    +    DIM ch AS INTEGER
    +
    +    ch = FREEFILE
    +    OPEN path FOR BINARY AS #ch
    +    ON ERROR GOTO Failed
    +    n = LOF(ch)
    +    LofError = 0
    +    CLOSE #ch
    +    EXIT FUNCTION
    +Failed:
    +    LofError = ERR
    +    RESUME Done
    +Done:
    +    CLOSE #ch
    +END FUNCTION
    +
    +
    +' Make, write, rename, list and mark read-only a long-named file in a
    +' long-named folder on drive; the folder stays.
    +SUB CheckLongNames(BYVAL drive AS STRING)
    +    DIM folder AS STRING
    +    DIM path AS STRING
    +    DIM shortPath AS STRING
    +    DIM ch AS INTEGER
    +
    +    folder = drive + LONG_DIR
    +    Check FileOpsLongNames(drive), drive + " long names"
    +    Check FileOpsValidName(drive, LONG_FILE), drive + " ValidName"
    +    Check NOT FileOpsValidName(drive, "bad|name.txt"), drive + " ValidName bad"
    +    Check FileOpsMakeDir(drive, LONG_DIR) AND FileOpsIsFolder(folder), drive + " MakeDir: " + FileOpsError$()
    +
    +    path = folder + "\" + LONG_FILE
    +    ch = FREEFILE
    +    OPEN path FOR OUTPUT AS #ch
    +    PRINT #ch, "long name"
    +    CLOSE #ch
    +
    +    Check FileOpsRename(folder, LONG_FILE, RENAMED), drive + " Rename: " + FileOpsError$()
    +    path = folder + "\" + RENAMED
    +    Check DIR$(folder + "\*.txt") = RENAMED, drive + " DIR$ " + DIR$(folder + "\*.txt")
    +    Check FileOpsSetAttr(path, FILEOPS_ATTR_READONLY) AND (FileOpsGetAttr(path) AND FILEOPS_ATTR_READONLY) <> 0, drive + " SetAttr"
    +    Check FileOpsPathNormalize$(folder) = folder, drive + " Normalize " + FileOpsPathNormalize$(folder)
    +
    +    shortPath = FileOpsShortPath$(path)
    +    Check shortPath <> path AND INSTR(shortPath, "~") > 0 AND FileOpsValidShortName(FileOpsBaseName$(shortPath)), drive + " ShortPath " + shortPath
    +END SUB
    +
    +
    +SUB LfnSmoke_Load
    +    DIM listing AS LONG
    +    DIM idx AS LONG
    +    DIM total AS DOUBLE
    +    DIM freeBytes AS DOUBLE
    +
    +    ' The big disk: real sizes past 4 GB, the 3 GB file
    +    Check INSTR(FileOpsDriveList$(), "D") > 0, "no D:"
    +    total = FileOpsTotalBytes(BIG_DRIVE)
    +    freeBytes = FileOpsFreeBytes(BIG_DRIVE)
    +    lblResult.Caption = "D: " + FileOpsFormatSize$(total) + " total, " + FileOpsFormatSize$(freeBytes) + " free"
    +    Check total > FOUR_GB AND freeBytes > FOUR_GB AND freeBytes < total, "D: sizes"
    +
    +    listing = FileOpsList(BIG_DRIVE, "")
    +    idx     = FileOpsListFind(listing, "BIG3GB.DAT")
    +    Check idx >= 0, "BIG3GB.DAT not listed"
    +
    +    IF idx >= 0 THEN
    +        Check FileOpsListSize(listing, idx) = BIG_BYTES, "list size " + FileOpsFormatSize$(FileOpsListSize(listing, idx))
    +    END IF
    +
    +    idx = FileOpsListFind(listing, "Old Projects")
    +    Check idx >= 0, "Old Projects not listed"
    +
    +    IF idx >= 0 THEN
    +        Check FileOpsListIsFolder(listing, idx), "Old Projects not a folder"
    +    END IF
    +
    +    FileOpsListFree listing
    +    Check FileOpsTreeSize(BIG_FILE) AND FileOpsTreeBytes() = BIG_BYTES, "tree size " + FileOpsFormatSize$(FileOpsTreeBytes())
    +    Check FileLenError(BIG_FILE) = ERR_OVERFLOW, "FILELEN error" + STR$(FileLenError(BIG_FILE))
    +    Check LofError(BIG_FILE) = ERR_OVERFLOW, "LOF error" + STR$(LofError(BIG_FILE))
    +    Check FileLenError(OLD_NOTE) = 0 AND FILELEN(OLD_NOTE) > 0, "Read Me First.txt"
    +
    +    ' Long names on FAT32 and FAT16, a copy between them
    +    CheckLongNames BIG_DRIVE
    +    CheckLongNames BOOT_DRIVE
    +    Check FileOpsDeleteTree(BOOT_DRIVE + LONG_DIR) AND NOT FileOpsIsFolder(BOOT_DRIVE + LONG_DIR), "C: DeleteTree"
    +    Check FileOpsCopyTree(OLD_FOLDER, BOOT_DRIVE, 0) AND FILELEN(BOOT_DRIVE + "Old Projects\Read Me First.txt") = FILELEN(OLD_NOTE), "CopyTree"
    +    Check FileOpsDeleteTree(BOOT_DRIVE + "Old Projects"), "C: DeleteTree copy"
    +    Check XferSafeName$("Long File.txt", BIG_DRIVE) = "Long File.txt", "XferSafeName " + XferSafeName$("Long File.txt", BIG_DRIVE)
    +
    +    IF failText = "" THEN
    +        LfnSmoke.Caption = "LFN PASS"
    +    ELSE
    +        LfnSmoke.Caption = "LFN FAIL " + failText
    +    END IF
    +END SUB
    diff --git a/src/test/dos/runDos.sh b/src/test/dos/runDos.sh
    index f678469a..62e4b244 100755
    --- a/src/test/dos/runDos.sh
    +++ b/src/test/dos/runDos.sh
    @@ -128,6 +128,37 @@ HDD_HEADS=16
     HDD_SPT=63
     SECTOR=512
     PART_START=${HDD_SPT}
    +FAT16_PART_TYPE=0x06
    +BOOTABLE=1
    +
    +# Big disks and long file names (86Box only).  "# LFN" loads FreeDOS's
    +# long file name driver (DOSLFN.COM, ./installToolchain.sh) before DVX.
    +# "# BIGDISK" adds a second IDE disk (primary slave, D:) of 16383
    +# cylinders x 16 heads x 63 sectors (about 8 GB) as a sparse image.  The
    +# Award BIOS auto-detects the slave and gives DOS 1023 cylinders x 255
    +# heads x 63 sectors of it through CHS calls; it has no INT 13h
    +# extensions, so the FreeDOS kernel skips an LBA partition (type 0Ch) and
    +# one that ends past that range.  The disk has one CHS FAT32 partition
    +# (type 0Bh, 32 KB clusters) filling that range (about 7.8 GB) holding a
    +# long-named folder with a long-named file, and BIGDISK_FILE, a
    +# BIGDISK_FILE_BYTES file whose clusters are only chained in the FAT
    +# (bigDisk.py grow), so the image stays a few MB.
    +LFN_DRIVER="${FREEDOS_DIR}/DOSLFN.COM"
    +BIGDISK_CYLS=16383
    +BIGDISK_BIOS_CYLS=1023
    +BIGDISK_HEADS=255
    +BIGDISK_CLUSTER_SECTORS=64
    +BIGDISK_PART_TYPE=0x0B
    +BIGDISK_LABEL="BIGDISK"
    +BIGDISK_FILE="BIG3GB.DAT"
    +BIGDISK_FILE_BYTES=3000000000
    +BIGDISK_FOLDER="Old Projects"
    +BIGDISK_NOTE="Read Me First.txt"
    +BIGDISK_PY="${HERE}/bigDisk.py"
    +NEED_LFN=0
    +NEED_BIGDISK=0
    +LFN_TEXT=""
    +BIGDISK_IMG=""
     
     DOSBOX_MEM_MB=32
     
    @@ -180,7 +211,8 @@ NEED_JOYDRIVE=0
     # writer (ALSOFT_CONF in the run folder), DOSBox through SDL's disk audio
     # driver (32-bit float stereo, 48 kHz).  After the run audioCmp.py
     # compares /audio.wav|raw with golden/