chore: move pi-dosbox to separate repo

Moved to https://github.com/badlogic/pi-dosbox
This commit is contained in:
Mario Zechner 2026-01-22 13:19:42 +01:00
parent 6289c144bf
commit 866d21c252
26 changed files with 1 additions and 1891 deletions

View file

@ -1,183 +0,0 @@
/**
* DOSBox extension for pi
*
* Features:
* - Persistent DOSBox instance running in background
* - QuickBASIC 4.5 mounted at C:\QB
* - /dosbox command to view and interact with DOSBox
* - dosbox tool for agent to send keys, read screen, take screenshots
*
* Usage: pi --extension ./examples/extensions/pi-dosbox
*/
import { StringEnum } from "@mariozechner/pi-ai";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { DosboxComponent } from "./src/dosbox-component.js";
import { DosboxInstance } from "./src/dosbox-instance.js";
export default function (pi: ExtensionAPI) {
// Start DOSBox instance at session start
pi.on("session_start", async () => {
try {
await DosboxInstance.getInstance();
} catch (error) {
console.error("Failed to start DOSBox:", error);
}
});
// Clean up on session shutdown
pi.on("session_shutdown", async () => {
await DosboxInstance.destroyInstance();
});
// Register /dosbox command to view DOSBox
pi.registerCommand("dosbox", {
description: "View and interact with DOSBox (Ctrl+Q to detach)",
handler: async (_args, ctx) => {
if (!ctx.hasUI) {
ctx.ui.notify("DOSBox requires interactive mode", "error");
return;
}
// Ensure instance is running
const instance = DosboxInstance.getInstanceSync();
if (!instance || !instance.isReady()) {
ctx.ui.notify("DOSBox is not running. It should start automatically.", "error");
return;
}
await ctx.ui.custom((tui, theme, _kb, done) => {
const fallbackColor = (s: string) => theme.fg("warning", s);
return new DosboxComponent(tui, fallbackColor, () => done(undefined));
});
},
});
// Register dosbox tool for agent interaction
pi.registerTool({
name: "dosbox",
label: "DOSBox",
description: `Interact with DOSBox emulator running QuickBASIC 4.5.
Actions:
- send_keys: Send keystrokes to DOSBox. Use \\n for Enter, \\t for Tab.
- screenshot: Get a PNG screenshot of the current DOSBox screen.
- read_text: Read text-mode screen content (returns null in graphics mode).
QuickBASIC 4.5 is mounted at C:\\QB. Run "C:\\QB\\QB.EXE" to start it.`,
parameters: Type.Object({
action: StringEnum(["send_keys", "screenshot", "read_text"] as const, {
description: "The action to perform",
}),
keys: Type.Optional(
Type.String({
description:
"For send_keys: the keys to send. Use \\n for Enter, \\t for Tab, or special:<key> for special keys (enter, backspace, tab, escape, up, down, left, right, f5)",
}),
),
}),
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
const { action, keys } = params;
const instance = DosboxInstance.getInstanceSync();
if (!instance || !instance.isReady()) {
return {
content: [{ type: "text", text: "Error: DOSBox is not running" }],
details: {},
};
}
switch (action) {
case "send_keys": {
if (!keys) {
return {
content: [{ type: "text", text: "Error: keys parameter required for send_keys action" }],
details: {},
};
}
// Handle special keys
if (keys.startsWith("special:")) {
const specialKey = keys.slice(8) as
| "enter"
| "backspace"
| "tab"
| "escape"
| "up"
| "down"
| "left"
| "right"
| "f5";
instance.sendSpecialKey(specialKey);
return {
content: [{ type: "text", text: `Sent special key: ${specialKey}` }],
details: {},
};
}
// Handle escape sequences
const processedKeys = keys.replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\r/g, "\r");
instance.sendKeys(processedKeys);
return {
content: [{ type: "text", text: `Sent ${processedKeys.length} characters` }],
details: {},
};
}
case "screenshot": {
const screenshot = instance.getScreenshot();
if (!screenshot) {
return {
content: [{ type: "text", text: "Error: No frame available yet" }],
details: {},
};
}
return {
content: [
{
type: "image",
data: screenshot.base64,
mimeType: "image/png",
},
{
type: "text",
text: `Screenshot: ${screenshot.width}x${screenshot.height} pixels`,
},
],
details: {},
};
}
case "read_text": {
const text = instance.readScreenText();
if (text === null) {
const state = instance.getState();
return {
content: [
{
type: "text",
text: `Screen is in graphics mode (${state.width}x${state.height}). Use screenshot action to see the display.`,
},
],
details: {},
};
}
return {
content: [{ type: "text", text: text || "(empty screen)" }],
details: {},
};
}
default:
return {
content: [{ type: "text", text: `Error: Unknown action: ${action}` }],
details: {},
};
}
},
});
}

View file

@ -1,22 +0,0 @@
{
"name": "pi-dosbox",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"clean": "echo 'nothing to clean'",
"build": "echo 'nothing to build'",
"check": "echo 'nothing to check'"
},
"pi": {
"extensions": [
"./index.ts"
]
},
"dependencies": {
"emulators": "^8.3.9"
},
"devDependencies": {
"@types/node": "^20.11.30"
}
}

View file

@ -1,71 +0,0 @@
'***
' QB.BI - Assembly Support Include File
'
' Copyright <C> 1987 Microsoft Corporation
'
' Purpose:
' This include file defines the types and gives the DECLARE
' statements for the assembly language routines ABSOLUTE,
' INTERRUPT, INTERRUPTX, INT86OLD, and INT86XOLD.
'
'***************************************************************************
'
' Define the type needed for INTERRUPT
'
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
'
' Define the type needed for INTERUPTX
'
TYPE RegTypeX
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
ds AS INTEGER
es AS INTEGER
END TYPE
'
' DECLARE statements for the 5 routines
' -------------------------------------
'
' Generate a software interrupt, loading all but the segment registers
'
DECLARE SUB INTERRUPT (intnum AS INTEGER,inreg AS RegType,outreg AS RegType)
'
' Generate a software interrupt, loading all registers
'
DECLARE SUB INTERRUPTX (intnum AS INTEGER,inreg AS RegTypeX, outreg AS RegTypeX)
'
' Call a routine at an absolute address.
' NOTE: If the routine called takes parameters, then they will have to
' be added to this declare statement before the parameter given.
'
DECLARE SUB ABSOLUTE (address AS INTEGER)
'
' Generate a software interrupt, loading all but the segment registers
' (old version)
'
DECLARE SUB INT86OLD (intnum AS INTEGER,_
inarray(1) AS INTEGER,_
outarray(1) AS INTEGER)
'
' Gemerate a software interrupt, loading all the registers
' (old version)
'
DECLARE SUB INT86XOLD (intnum AS INTEGER,_
inarray(1) AS INTEGER,_
outarray(1) AS INTEGER)
'

View file

@ -1,198 +0,0 @@
'
' QUICKBASIC SUPPORT ROUTINES FOR THEDRAW OBJECT FILES
'-----------------------------------------------------------------------------
' Compatible with Microsoft QuickBasic v4.0 and v4.5 text modes.
'-----------------------------------------------------------------------------
'
' There are a few routines within the QB4UTIL.LIB file. These are
' (along with brief descriptions):
'
' UNCRUNCH - Flash display routine for crunched image files.
' ASCIIDISPLAY - Display routine for ascii only image files.
' NORMALDISPLAY - Display routine for normal full binary image files.
' INITSCREENARRAY - Maps a dynamic integer array to the physical video
' memory.
'
'=============================================================================
' UNCRUNCH (imagedata,video offset)
' ASCIIDISPLAY (imagedata,video offset)
' NORMALDISPLAY (imagedata,video offset)
'=============================================================================
'
' These three subroutines operate similarly. Each takes a specific data
' format (TheDraw crunched data, ascii only, or normal binary) and displays
' the image on the screen. Monochrome and color text video displays are
' supported. The integer offset parameter is useful with block images,
' giving control over where the block appears.
'
' Example calls:
' CALL UNCRUNCH (ImageData&,vidoffset%) <- for crunched data
' CALL ASCIIDISPLAY (ImageData&,vidoffset%) <- for ascii-only data
' CALL NORMALDISPLAY (ImageData&,vidoffset%) <- for normal binary data
'
' The parameter IMAGEDATA is the identifier you assign when saving
' a QuickBasic object file with TheDraw. ImageData actually becomes a
' short function returning information Uncrunch, AsciiDisplay, and
' NormalDisplay use to find the screen contents. In addition, three
' other related integer functions are created. Assuming the identifier
' IMAGEDATA, these are:
'
' IMAGEDATAWIDTH%
' IMAGEDATADEPTH%
' IMAGEDATALENGTH%
'
' The width and depth functions return the size of the block in final
' form (ie: a full screen would yield the numbers 80 and 25 respectfully).
' The length function returns the size of the stored data. For crunched
' files and block saves this might be very small. For a 80x25 full screen
' binary image it will be 4000 bytes. The integer functions are useful for
' computing screen or window dimensions, etc...
'
' You must declare all four functions in your Basic source code before
' they can be used (naturally). The following code example illustrates.
' The identifier used is IMAGEDATA. The data is a 40 character by 10 line
' block saved as normal binary.
'
' ----------------------------------------------------------------------
' REM $INCLUDE: 'QB4UTIL.BI'
' DECLARE FUNCTION ImageData& ' Important! Do not neglect
' DECLARE FUNCTION ImageDataWidth% ' the "&" and "%" symbols
' DECLARE FUNCTION ImageDataDepth% ' after the function names.
' DECLARE FUNCTION ImageDataLength%
'
' CALL NORMALDISPLAY (ImageData&, 34 *2+( 5 *160)-162)
' ----------------------------------------------------------------------
'
' That's it! The above displays the 40x10 block at screen coordinates
' column 34, line 5 (note these two numbers in above example). If the
' data was crunched or ascii use the corresponding routine.
'
' Note: The ascii-only screen image does not have any color controls.
' Whatever the on-screen colors were before, they will be after.
' You might want to insert COLOR and CLS statements before calling
' the ASCIIDISPLAY routine.
'
' Regardless of which routine used, each remembers the original horizontal
' starting column when it goes to the next line. This permits a block to
' be displayed correctly anywhere on the screen. ie:
'
' +-------------------------------------------------+
' | |
' | | <- Pretend this
' | | is the video
' | ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ | display.
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
' | ³ÛÛ ImageData block ÛÛ³ |
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
' | ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ |
' | ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ |
' | |
' | |
' | |
' +-------------------------------------------------+
'
'
' The ImageData block could be shown in the upper-left corner of the
' screen by changing the call to:
'
' CALL NORMALDISPLAY (ImageData&,0)
'
' Notice the video offset has been removed, since we want the upper-left
' corner. To display the block in the lower-right corner you would use:
'
' CALL NORMALDISPLAY (ImageData&, 40 *2+( 15 *160)-162)
'
' The block is 40 characters wide by 10 lines deep. Therefore to display
' such a large block, we must display the block at column 40, line 15.
' (column 80 minus 40, line 25 minus 10).
'
'
' NOTES ON THE UNCRUNCH ROUTINE
' --------------------------------------------------------------------------
'
' Many people favor "crunching" screens with TheDraw because the size
' of the data generally goes down. When uncrunching an image however,
' there is no guarantee what was previously on-screen will be replaced.
'
' In particular, the uncruncher assumes the screen is previously erased to
' black thus permitting better data compression. For instance, assume the
' video completely filled with blocks, overwritten by an uncrunched image:
'
' ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³tetetetetetÛÛÛÛÛÛÛÛÛÛ³
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ eteteteteteteÛÛÛÛ³
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³tetetetetÛÛÛÛÛÛÛÛÛÛÛÛ³
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ eteÛÛÛÛÛÛÛÛÛ³
' ³ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ³ ³ etetetetetetÛÛ³
' ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ
' before uncrunch after uncrunch
'
' By omitting a CLS statement, the new text appears surrounded by bits of
' the previous screen. Proper usage would typically be:
'
' ----------------------------------------------------------------------
' REM $INCLUDE: 'QB4UTIL.BI'
' DECLARE FUNCTION ImageData& ' Important! Do not neglect
' DECLARE FUNCTION ImageDataWidth% ' the "&" and "%" symbols
' DECLARE FUNCTION ImageDataDepth% ' after the function names.
' DECLARE FUNCTION ImageDataLength%
'
' COLOR 15,0 : CLS ' Clear to black screen
' CALL UNCRUNCH (ImageData&, 34 *2+( 5 *160)-162)
' ----------------------------------------------------------------------
'
'
'=============================================================================
' INITSCREENARRAY
'=============================================================================
'
' To directly access the video screen memory requires you to use the
' PEEK/POKE statements after setting the DEF SEG value. A cumbersome
' and compiler inefficient approach. In addition, you must have some
' way of determining if a monochrome or color video is being used before
' the DEF SEG can be set properly.
'
' This subroutine offers a simpler approach, by effectively mapping or
' placing an integer array over the video screen. Instead of PEEK/POKE,
' you merely reference an array element. ie:
'
' ----------------------------------------------------------------------
' REM $INCLUDE: 'QB4UTIL.BI'
'
' REM $DYNAMIC <- very important to place this before DIM statement
' DIM S%(0)
' CALL INITSCREENARRAY (S%())
'
' S%(0) = ASC("H") + 15 *256 + 1 *4096
' S%(1) = ASC("E") + 15 *256 + 1 *4096
' S%(2) = ASC("L") + 15 *256 + 1 *4096
' S%(3) = ASC("L") + 15 *256 + 1 *4096
' S%(4) = ASC("O") + 15 *256 + 1 *4096
' ----------------------------------------------------------------------
'
' The above example directly places the message "HELLO" on the screen
' for you, in white lettering (the 15*256) on a blue background (1*4096).
' To alter the foreground color, change the 15's to some other number.
' Change the 1's for the background color.
'
' Each array element contains both the character to display plus the
' color information. This explains the bit of math following each
' ASC statement. You could minimize this using a FOR/NEXT loop.
'
' The S% array has 2000 elements (0 to 1999) representing the entire
' 80 by 25 line video. If in an EGA/VGA screen mode change the 1999 to
' 3439 or 3999 respectfully.
'
' There is no pressing reason to use the array approach, however it
' does free up the DEFSEG/PEEK/POKE combination for other uses. In
' any case, enjoy!
'
'
DECLARE SUB UNCRUNCH (X&, Z%)
DECLARE SUB ASCIIDISPLAY (X&, Z%)
DECLARE SUB NORMALDISPLAY (X&, Z%)
DECLARE SUB INITSCREENARRAY (A%())


View file

@ -1,545 +0,0 @@
README.DOC File
Release Notes for Microsoft (R) QuickBASIC
Version 4.50
(C) Copyright Microsoft Corporation, 1990
Product Serial Number: 00-007-1450-26147102
This document contains release notes for version 4.50 of the Microsoft (R)
QuickBASIC for MS-DOS (R). The information in this document is more
up-to-date than that in the manuals.
================================================================================
Contents
================================================================================
Part Description
---- -----------
1 Using QuickBASIC on a Two-Floppy System
2 Using Your Mouse with QuickBASIC
3 Supplementary Information on Mixed-Language Programming
4 Using Btrieve with QuickBASIC
5 Using the DOS 3.2 Patch for Math Accuracy
6 Miscellaneous Information About Using QuickBASIC
================================================================================
Part 1: Using QuickBASIC on a Two-Floppy System
================================================================================
Installing QuickBASIC on Floppy Disks
-------------------------------------
The SETUP program can install QuickBASIC on floppy disks for use
with a two-floppy system. You must run SETUP to install QuickBASIC
on floppy disks. You cannot run QuickBASIC from the disks provided,
because the files are stored in a compressed format.
Before you install QuickBASIC on your two-floppy system, be sure
you have enough blank, formatted disks. If you have 360K disk
drives, you will need five blank disks. For 720K disk drives, you
will need three blank disks.
To install QuickBASIC, put Disk #1 in drive A. Type A:\SETUP and
press Enter.
When your installation is complete, you should label each disk with
the names of the files that are on that disk. QuickBASIC will ask
you to swap disks when it cannot find a file that it needs, and
you will need to know which disk the file is on.
If you use 360K disks, label them as follows:
PROGRAM:
QB.EXE QB45QCK.HLP
UTILITIES:
BC.EXE LINK.EXE
BQLB45.LIB LIB.EXE
BRUN45.EXE QB.QLB
BRUN45.LIB QB.LIB
UTILITIES 2:
BCOM45.LIB QB45ENER.HLP
ADVISOR:
QB45ADVR.HLP
EXAMPLES
QB.BI BASIC examples
If you use 720K disks, label them as follows:
PROGRAM/EXAMPLES:
QB.EXE QB45QCK.HLP
QB.BI BASIC examples
UTILITIES:
BC.EXE LINK.EXE
BQLB45.LIB LIB.EXE
BRUN45.EXE QB.QLB
BRUN45.LIB QB.LIB
BCOM45.LIB
ADVISOR:
QB45ADVR.HLP QB45ENER.HLP
Running QuickBASIC from Floppy Disks
------------------------------------
During some operations, QuickBASIC asks you to swap disks one
or more times. You can minimize disk swapping by following the
procedures in this section.
Since the disks that you installed QuickBASIC on are nearly full, you
should keep your BASIC source-code (.BAS) files on a separate disk.
Label this disk SOURCE.
Copy the run-time module BRUN45.EXE from the UTILITIES disk to your
SOURCE disk. QuickBASIC needs this file to run executable programs
compiled with the run-time support option.
When you use QuickBASIC, a disk containing source-code (.BAS) files
should always be in drive B. If you want to run existing BASIC
programs (such as the example programs provided with QuickBASIC),
remove the SOURCE disk from drive B and insert the disk containing
these files.
To run QuickBASIC:
1. Insert the SOURCE disk in drive B.
2. To make drive B the current drive, type B: and press Enter.
3. Insert the PROGRAM disk (the disk containing QB.EXE) in drive A.
4. Type the following command:
A:QB.EXE
To insure that QuickBASIC always looks on both disk drives for the
files it needs, follow these steps:
1. From the Options menu, choose Set Paths.
2. Make sure each of the path settings includes both disk drives. The
following line should be in all four text boxes:
A:\;B:\
3. Choose OK.
QuickBASIC saves these path settings in the QB.INI file, so you will
not have to enter them again.
When you exit QuickBASIC or shell to DOS, you will be prompted to
insert a disk containing the file COMMAND.COM. Remove the PROGRAM
disk from drive A, insert a system disk, and press Enter.
Using Help from Floppy Disks
----------------------------
When you use the QuickBASIC Advisor online help system, you may need
to swap disks. For example, if you choose "Details" or "Example" on a
help screen, QuickBASIC will inform you that it cannot find the help
file QB45ADVR.HLP. Put the disk that contains this file in drive A and
choose Retry.
Compiling Your Programs from Floppy Disks
-----------------------------------------
To compile your program from within QuickBASIC:
1. From the Run menu, choose Make EXE File.
2. Choose Make EXE. QuickBASIC displays the message "Cannot find file
(BC.EXE)."
3. Insert the UTILITIES disk (the disk containing BC.EXE) in drive A.
Type A: and press Enter.
If the program compiles successfully, QuickBASIC invokes the LINK
utility. If LINK cannot find the library, it displays the following
message:
LINK : warning L4051 : BCOM45.LIB : cannot find library
Enter new file spec:
4. Insert the disk containing the requested library (BCOM45.LIB or
BRUN45.LIB) in drive A.
Note: The requested library may be located on the UTILITIES disk
already in drive A. If this is the case, leave this disk in drive A.
5. Type A: and press Enter. After the LINK utility finishes creating
your executable program, QuickBASIC displays the message "Cannot
find file (QB.EXE)."
6. Insert the PROGRAM disk in drive A.
7. Type A: and press Enter.
If no errors occur during compiling or linking, your compiled program
(.EXE) is created on drive B. QuickBASIC also creates an object-module
(.OBJ) file. To save space, you can delete object-module files.
================================================================================
Part 2: Using Your Mouse with QuickBASIC
================================================================================
New Mouse Driver for Use with QuickBASIC
----------------------------------------
QuickBASIC Version 4.5 can be used with any mouse that is 100%
compatible with the Microsoft Mouse. However, you must use a
Microsoft Mouse driver Version 6.00 or later. Earlier versions may
cause unpredictable behavior when used with QuickBASIC. MOUSE.COM,
Version 6.24 is supplied with QuickBASIC Version 4.5.
Especially if you are writing programs that use the mouse, you
should use the supplied version of the mouse driver when working in
QuickBASIC. Previous versions have included MOUSE.SYS, which is
installed by including the line DEVICE=MOUSE.SYS in your CONFIG.SYS
file. This version of QuickBASIC includes MOUSE.COM, which is not
installed via CONFIG.SYS. To install MOUSE.COM, just type MOUSE at
the DOS prompt. To include MOUSE.COM automatically when your machine
boots, make sure MOUSE.COM is in your search path, then put the line
MOUSE
in your AUTOEXEC.BAT file. To free up memory, you can remove the
mouse driver at any time by typing MOUSE OFF at the DOS prompt.
This will restore between 9K and 10.5K of memory with Version 6.11.
Using Mouse Function Calls from QuickBASIC Programs
---------------------------------------------------
If you are programming for the Microsoft Mouse, you should obtain
the Microsoft Mouse Programmer's Reference Guide and the library
MOUSE.LIB that comes with it. (These are not included in QuickBASIC
or Mouse package and must be ordered separately). Most of the
information in the Mouse Programmer's Reference Guide applies
directly to QuickBASIC Version 4.5. However, the following additional
restrictions must be observed:
Certain Mouse function calls (Functions 9 & 16) require you to set
up an integer array and pass the address of the array to the mouse
driver. For previous versions, the only restriction on this array
was that it had to be $STATIC (the default array type). In QuickBASIC
Version 4.5, however, the array also must be in a COMMON block if you
will be making the Mouse function call from within the QuickBASIC
environment. In addition, it is recommended that the support code
for the Mouse call be in a Quick library or linked into the
executable file when making Mouse function calls from QuickBASIC.
To produce a Quick library for using Mouse function calls from
within the QuickBASIC environment, use the following command line
(produces MOUSE.QLB):
LINK MOUSE.LIB/QU,MOUSE.QLB,,BQLB40.LIB/NOE;
An example from PIANO.BAS (included with the Microsoft Mouse
Programmer's Reference) for using Mouse function call 9:
DEFINT A-Z
DECLARE SUB MOUSE (M1, M2, M3, M4)
DIM Cursor(15, 1)
COMMON Cursor() 'Ensures array data is in DGROUP
.
. (set up Cursor() for mouse cursor shape desired)
.
M1 = 9: M2 = 6: M3 = 0
CALL MOUSE(M1, M2, M3, VARPTR(Cursor(0, 0)))
In addition to the above, note that Mouse function calls 21-23
require dynamically allocated storage out of the home data segment.
The recommended way to do this is to allocate space in a dynamic
string variable based on the return value from function call 21,
using the STRING$ or SPACE$ function. Then use VARPTR on this string
variable just prior to calling Mouse function call 22 or 23.
================================================================================
Part 3: Supplementary Information on Mixed-Language Programming
================================================================================
Linking from Within QuickC or with QCL
--------------------------------------
Microsoft QuickC and the QCL command both set the /NOI linker
by default. Therefore, you should not link from within QuickC, or
with QCL, when your program contains modules written in a case-
insensitive language such as BASIC. Use LINK to link your program
from the command line.
Pascal and FORTRAN Modules in QuickBASIC Programs
-------------------------------------------------
Modules compiled with Microsoft Pascal or FORTRAN can be linked with
BASIC programs, as described in the Microsoft Mixed-Language
Programming Guide. They can also be incorporated in Quick libraries.
However, QuickBASIC programs containing code compiled with Microsoft
Pascal must allocate at least 2K near-heap space for Pascal. This can
be done by using the DIM statement to allocate a static array of 2K or
greater in the NMALLOC named common block, for example, as follows:
DIM name%(2048)
COMMON SHARED /NMALLOC/name%()
The Pascal run-time assumes it always has at least 2K of near-heap
space available. If the Pascal code cannot allocate the required
space, QuickBASIC may crash. This applies to Pascal code in Quick
libraries as well as Pascal code linked into executable files. The
situation is similar for FORTRAN I/O, which also requires near
buffer space, and which can be provided by the same means as the
Pascal near malloc space.
STATIC Array Allocation
-----------------------
If you are writing assembly-language modules for use in QuickBASIC
programs, see Section 2.3.3, "Variable Storage Allocation," in the
BASIC Language Reference. Assembly-language code should not assume
data is in a particular segment. To avoid problems, pass data using
the SEG or CALLS keywords, or use FAR pointers. Alternatively, you
can declare all arrays dynamic (still using far pointers) since
dynamic arrays are handled identically by BC and within QuickBASIC.
Quick Libraries with Leading Zeros in the First Code Segment
------------------------------------------------------------
A Quick library containing leading zeros in the first CODE segment
is invalid, causing the message "Error in loading file <name> -
Invalid format" when you try to load it in QuickBASIC. For example,
this can occur if an assembly-language routine puts data that is
initialized to zero in the first CODE segment, and it is subsequently
listed first on the LINK command line when you make a Quick library.
If you have this problem, do either of the following:
(1) link with a BASIC module first on the LINK command line, or
(2) make sure that, in whatever module comes first on the LINK
command line, the first code segment starts with a non-zero byte.
References to DGROUP in Extended Run-Time Modules
-------------------------------------------------
For mixed-language programs that use the CHAIN command, you should
make sure that any code built into an extended run-time module does not
contain any references to DGROUP. (The CHAIN command causes DGROUP to
move, but does not update references to DGROUP.) This rule applies
only to mixed-language programs; because BASIC routines never refer
to DGROUP, you can ignore this caution for programs written entirely
in BASIC.
To avoid this problem, you can use the value of SS, since BASIC always
assumes that SS coincides with DGROUP.
================================================================================
Part 4: Using Btrieve
================================================================================
Using Btrieve in OS/2 Protected Mode
------------------------------------
In OS/2 protected mode, a BASIC program that uses Btrieve must do a
Btrieve reset call (function 28) before executing the CHAIN statement.
The program must also reopen all Btrieve files when the destination of
the CHAIN starts to run.
Using Btrieve with QuickBASIC
-----------------------------
If you use Btrieve with QuickBASIC, you must make a small change to
your programs for QuickBASIC Version 4.5. Currently your programs
contain a statement that obtains the address of the field buffer for
an open file. For example:
OPEN "NUL" AS #1
FIELD #1, 20 AS CITY$, 10 AS STATE$
FCB.ADDR% = VARPTR(#1) 'This statement obtains the address
In QuickBASIC Version 4.5, you should change the indicated statement
to return the address of the first variable in your field buffer minus
a constant, as follows:
OPEN "NUL" AS #1
FIELD #1, 20 AS CITY$, 10 AS STATE$
FCB.ADDR% = SADD(CITY$) - 188 ' CITY$ is the first field
' buffer variable
The following example shows how to obtain the same address for a
user-defined type:
TYPE ADDRESS
CITY AS STRING * 20
STATE AS STRING * 10
END TYPE
DIM ADD1 AS ADDRESS
FCB.ADDR% = VARPTR(ADD1) - 188
' or, you can use FCB.ADDR% = VARPTR(ADD1.CITY) - 188
Your programs should function correctly with Btrieve with this change.
================================================================================
Part 5: DOS 3.20 Patch
================================================================================
This information is important only if your system has all of the
following characteristics:
1. Uses MS-DOS version 3.20
2. Boots from a hard disk drive
3. Has a math coprocessor (for instance, an 8087 chip)
4. Runs programs that use floating-point math
For systems that satisfy all of the preceding conditions, you may be
able to eliminate floating-point math problems by installing a small
patch in DOS. If you are not sure whether you need the patch, perform
the following steps:
1. Copy the program PATCH87.EXE (included in this release) to the root
directory of your hard-disk drive.
2. Reboot your system from the hard disk, and do not perform any floppy-
disk operations after rebooting. It is very important that you avoid
floppy-disk I/O after rebooting, since that will affect the
reliability of the diagnostic test that you are about to perform.
3. If necessary, use the CD command to move to the root directory of
your hard-disk drive.
4. Run the PATCH87.EXE program by entering this command at the DOS
prompt:
PATCH87
5. The program performs a diagnostic test on your system to determine
whether it needs the DOS patch, and if the patch is needed,
whether the patch can be installed successfully. If the program
tells you that you need to install the DOS patch, and that it can be
done, follow the procedure described in the next section.
Note: The floating-point problem has been eliminated in versions of
MS-DOS higher than 3.20. This includes MS-DOS versions 3.21 and 3.30.
If you performed the preceding test and determined that you should
install the DOS patch on your system, perform the following steps:
1. Format a blank floppy disk. (Do NOT use the /s formatting option to
transfer system files to the disk.)
2. Use the SYS command to copy IO.SYS and MSDOS.SYS from the root
directory of your hard disk to the new floppy disk. For instance, if
you boot from drive C:, you would enter the following commands:
C:
SYS A:
3. Use the COPY command to copy COMMAND.COM and SYS.COM to the same
floppy disk.
4. Use the COPY command to copy the program PATCH87.EXE (included in
this release) to the same floppy disk.
5. Change the current drive and directory to the floppy disk, by
entering the following command:
A:
7. Install the DOS patch by entering the following command:
PATCH87 /F
WARNING: If you experience any disk errors during steps 2 through 7,
do not proceed with step 8. Reboot from your hard disk and repeat the
entire process.
8. If you have not experienced any errors, use the SYS command to
transfer the files IO.SYS and MSDOS.SYS from the floppy disk back to
your hard disk. For instance, if the boot directory of your system
is the root directory of drive C:, you would enter the following
command at the DOS prompt:
A:
SYS C:
9. The DOS patch has been installed. Reboot the system.
================================================================================
Part 6: Miscellaneous Information About Using QuickBASIC
================================================================================
Using FIXSHIFT.COM Utility
--------------------------
Some keyboards have an extra set of DIRECTION (i.e. arrow) keys, in
addition to those on the numeric keypad. A bug in the ROM BIOS of
some machines with these keyboards can interfere with the QuickBASIC
editor. The Utilities 2 disk includes a program, FIXSHIFT.COM, that
fixes this bug. If you have such a keyboard, run this program by typing
FIXSHIFT. If your machine does not have the bug, FIXSHIFT displays a
message telling you so. Otherwise FIXSHIFT prompts you for the proper
actions. FIXSHIFT takes about 450 bytes of memory. Except for the BIOS
bug, it has no effect on other programs you run.
Note on VGA Display Adapters
----------------------------
If you install an IBM (R) Personal System/2 (TM) Video Graphics
Array display adapter (VGA) in a non-PS/2 machine, the VGA adapter
should be the only adapter in the system, and you should not use
monochrome modes (SCREEN 10) if you have a color monitor. Similarly,
you should not use color modes (SCREEN 1, 2, 7, 8, 9, 11, 12, 13) if
you have a monochrome monitor.
Note on Using QuickBASIC with DOS 2.1
-------------------------------------
To use QuickBASIC with a two-floppy system under DOS 2.1, you must
put a copy of COMMAND.COM on each disk containing an executable
file ( a file with the .EXE extension).
PTR86, LOF, Naming SUB Procedures and Variables
-----------------------------------------------
PTR86 is no longer supported. Use VARSEG and VARPTR instead.
Also, when used with a communications device, LOF now returns the
amount of space remaining (in bytes) in the output buffer. In
previous versions this was returned in the input buffer. Also, note
that a variable and SUB procedure could have the same name in
previous versions. In Version 4.5, this causes a "Duplicate
definition" error message.

View file

@ -1,6 +0,0 @@
Please check "Use folder names." when you extract files (Using Winzip) or use /d option with PKZip because all the path in the INI files are set to C:\QB directory.
If you extract it in a different directory, set new paths in
Help>Set Paths from the Basic menu.
<-- Other Basic versions and many other compilers and files are available
at http://members.xoom.com/qb_best/ -->

View file

@ -1,425 +0,0 @@
/**
* DOSBox TUI Component
*
* Renders DOSBox framebuffer as an image in the terminal.
* Connects to the persistent DosboxInstance.
*/
import type { Component } from "@mariozechner/pi-tui";
import {
allocateImageId,
deleteKittyImage,
Image,
type ImageTheme,
isKeyRelease,
Key,
matchesKey,
truncateToWidth,
} from "@mariozechner/pi-tui";
import { DosboxInstance } from "./dosbox-instance.js";
const MAX_WIDTH_CELLS = 120;
// js-dos key codes
const KBD: Record<string, number> = {
enter: 257,
backspace: 259,
tab: 258,
esc: 256,
space: 32,
leftshift: 340,
rightshift: 344,
leftctrl: 341,
rightctrl: 345,
leftalt: 342,
rightalt: 346,
up: 265,
down: 264,
left: 263,
right: 262,
home: 268,
end: 269,
pageup: 266,
pagedown: 267,
insert: 260,
delete: 261,
f1: 290,
f2: 291,
f3: 292,
f4: 293,
f5: 294,
f6: 295,
f7: 296,
f8: 297,
f9: 298,
f10: 299,
f11: 300,
f12: 301,
};
export class DosboxComponent implements Component {
private tui: { requestRender: () => void };
private onClose: () => void;
private instance: DosboxInstance | null = null;
private image: Image | null = null;
private imageTheme: ImageTheme;
private loadingMessage = "Connecting to DOSBox...";
private errorMessage: string | null = null;
private cachedLines: string[] = [];
private cachedWidth = 0;
private cachedVersion = -1;
private version = 0;
private disposed = false;
private imageId: number;
private kittyPushed = false;
private frameListener: ((rgba: Uint8Array, width: number, height: number) => void) | null = null;
wantsKeyRelease = true;
constructor(tui: { requestRender: () => void }, fallbackColor: (s: string) => string, onClose: () => void) {
this.tui = tui;
this.onClose = onClose;
this.imageTheme = { fallbackColor };
this.imageId = allocateImageId();
void this.connect();
}
private async connect(): Promise<void> {
try {
this.instance = await DosboxInstance.getInstance();
// Set up frame listener
this.frameListener = (rgba: Uint8Array, width: number, height: number) => {
this.updateFrame(rgba, width, height);
};
this.instance.addFrameListener(this.frameListener);
// Get initial state
const state = this.instance.getState();
if (state.lastFrame && state.width && state.height) {
this.updateFrame(state.lastFrame, state.width, state.height);
}
// Push Kitty enhanced mode for proper key press/release
process.stdout.write("\x1b[>15u");
this.kittyPushed = true;
this.tui.requestRender();
} catch (error) {
this.errorMessage = error instanceof Error ? error.message : String(error);
this.tui.requestRender();
}
}
private updateFrame(rgba: Uint8Array, width: number, height: number): void {
const png = this.encodePng(width, height, rgba);
const base64 = png.toString("base64");
this.image = new Image(
base64,
"image/png",
this.imageTheme,
{ maxWidthCells: MAX_WIDTH_CELLS, imageId: this.imageId },
{ widthPx: width, heightPx: height },
);
this.version++;
this.tui.requestRender();
}
private encodePng(width: number, height: number, rgba: Uint8Array): Buffer {
const { deflateSync } = require("node:zlib");
const stride = width * 4;
const raw = Buffer.alloc((stride + 1) * height);
for (let y = 0; y < height; y++) {
const rowOffset = y * (stride + 1);
raw[rowOffset] = 0;
raw.set(rgba.subarray(y * stride, y * stride + stride), rowOffset + 1);
}
const compressed = deflateSync(raw);
const header = Buffer.alloc(13);
header.writeUInt32BE(width, 0);
header.writeUInt32BE(height, 4);
header[8] = 8;
header[9] = 6;
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = this.createChunk("IHDR", header);
const idat = this.createChunk("IDAT", compressed);
const iend = this.createChunk("IEND", Buffer.alloc(0));
return Buffer.concat([signature, ihdr, idat, iend]);
}
private createChunk(type: string, data: Buffer): Buffer {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length, 0);
const typeBuffer = Buffer.from(type, "ascii");
const crcBuffer = Buffer.concat([typeBuffer, data]);
const crc = this.crc32(crcBuffer);
const crcOut = Buffer.alloc(4);
crcOut.writeUInt32BE(crc, 0);
return Buffer.concat([length, typeBuffer, data, crcOut]);
}
private crc32(buffer: Buffer): number {
let crc = 0xffffffff;
for (const byte of buffer) {
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
handleInput(data: string): void {
const released = isKeyRelease(data);
if (!released && matchesKey(data, Key.ctrl("q"))) {
this.dispose();
this.onClose();
return;
}
const ci = this.instance?.getCommandInterface();
if (!ci) return;
const parsed = parseKeyWithModifiers(data);
if (!parsed) return;
const { keyCode, shift, ctrl, alt } = parsed;
if (shift) ci.sendKeyEvent(KBD.leftshift, !released);
if (ctrl) ci.sendKeyEvent(KBD.leftctrl, !released);
if (alt) ci.sendKeyEvent(KBD.leftalt, !released);
ci.sendKeyEvent(keyCode, !released);
}
invalidate(): void {
this.cachedWidth = 0;
}
render(width: number): string[] {
if (this.errorMessage) {
return [truncateToWidth(`DOSBox error: ${this.errorMessage}`, width)];
}
if (!this.instance?.isReady()) {
return [truncateToWidth(this.loadingMessage, width)];
}
if (!this.image) {
return [truncateToWidth("Waiting for DOSBox frame...", width)];
}
if (width === this.cachedWidth && this.cachedVersion === this.version) {
return this.cachedLines;
}
const imageLines = this.image.render(width);
const footer = truncateToWidth("\x1b[2mCtrl+Q to detach (DOSBox keeps running)\x1b[22m", width);
const lines = [...imageLines, footer];
this.cachedLines = lines;
this.cachedWidth = width;
this.cachedVersion = this.version;
return lines;
}
dispose(): void {
if (this.disposed) return;
this.disposed = true;
// Delete the terminal image
process.stdout.write(deleteKittyImage(this.imageId));
if (this.kittyPushed) {
process.stdout.write("\x1b[<u");
this.kittyPushed = false;
}
// Remove frame listener but DON'T dispose the instance
if (this.instance && this.frameListener) {
this.instance.removeFrameListener(this.frameListener);
}
}
}
const CRC_TABLE = createCrcTable();
function createCrcTable(): Uint32Array {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
if (c & 1) {
c = 0xedb88320 ^ (c >>> 1);
} else {
c >>>= 1;
}
}
table[i] = c >>> 0;
}
return table;
}
interface ParsedKey {
keyCode: number;
shift: boolean;
ctrl: boolean;
alt: boolean;
}
function decodeModifiers(modifierField: number): { shift: boolean; ctrl: boolean; alt: boolean } {
const modifiers = modifierField - 1;
return {
shift: (modifiers & 1) !== 0,
alt: (modifiers & 2) !== 0,
ctrl: (modifiers & 4) !== 0,
};
}
function parseKeyWithModifiers(data: string): ParsedKey | null {
if (data.startsWith("\x1b[") && data.endsWith("u")) {
const body = data.slice(2, -1);
const [keyPart, modifierPart] = body.split(";");
if (keyPart) {
const codepoint = parseInt(keyPart.split(":")[0], 10);
if (!Number.isNaN(codepoint)) {
const modifierField = modifierPart ? parseInt(modifierPart.split(":")[0], 10) : 1;
const { shift, alt, ctrl } = decodeModifiers(Number.isNaN(modifierField) ? 1 : modifierField);
const keyCode = codepointToJsDosKey(codepoint);
if (keyCode !== null) {
return { keyCode, shift, ctrl, alt };
}
}
}
}
const csiMatch = data.match(/^\x1b\[(\d+);(\d+)(?::\d+)?([~A-Za-z])$/);
if (csiMatch) {
const code = parseInt(csiMatch[1], 10);
const modifierField = parseInt(csiMatch[2], 10);
const suffix = csiMatch[3];
const { shift, alt, ctrl } = decodeModifiers(modifierField);
const keyCode = mapCsiKeyToJsDos(code, suffix);
if (keyCode === null) return null;
return { keyCode, shift, ctrl, alt };
}
const keyCode = mapKeyToJsDos(data);
if (keyCode === null) return null;
const shift = data.length === 1 && data >= "A" && data <= "Z";
return { keyCode, shift, ctrl: false, alt: false };
}
function codepointToJsDosKey(codepoint: number): number | null {
if (codepoint === 13) return KBD.enter;
if (codepoint === 9) return KBD.tab;
if (codepoint === 27) return KBD.esc;
if (codepoint === 8 || codepoint === 127) return KBD.backspace;
if (codepoint === 32) return KBD.space;
if (codepoint >= 97 && codepoint <= 122) return codepoint - 32;
if (codepoint >= 65 && codepoint <= 90) return codepoint;
if (codepoint >= 48 && codepoint <= 57) return codepoint;
return null;
}
function mapCsiKeyToJsDos(code: number, suffix: string): number | null {
switch (suffix) {
case "A":
return KBD.up;
case "B":
return KBD.down;
case "C":
return KBD.right;
case "D":
return KBD.left;
case "H":
return KBD.home;
case "F":
return KBD.end;
case "P":
return KBD.f1;
case "Q":
return KBD.f2;
case "R":
return KBD.f3;
case "S":
return KBD.f4;
case "Z":
return KBD.tab;
case "~":
switch (code) {
case 1:
case 7:
return KBD.home;
case 2:
return KBD.insert;
case 3:
return KBD.delete;
case 4:
case 8:
return KBD.end;
case 5:
return KBD.pageup;
case 6:
return KBD.pagedown;
case 15:
return KBD.f5;
case 17:
return KBD.f6;
case 18:
return KBD.f7;
case 19:
return KBD.f8;
case 20:
return KBD.f9;
case 21:
return KBD.f10;
case 23:
return KBD.f11;
case 24:
return KBD.f12;
default:
return null;
}
default:
return null;
}
}
function mapKeyToJsDos(data: string): number | null {
if (matchesKey(data, Key.enter)) return KBD.enter;
if (matchesKey(data, Key.backspace)) return KBD.backspace;
if (matchesKey(data, Key.tab)) return KBD.tab;
if (matchesKey(data, Key.escape)) return KBD.esc;
if (matchesKey(data, Key.space)) return KBD.space;
if (matchesKey(data, Key.up)) return KBD.up;
if (matchesKey(data, Key.down)) return KBD.down;
if (matchesKey(data, Key.left)) return KBD.left;
if (matchesKey(data, Key.right)) return KBD.right;
if (matchesKey(data, Key.pageUp)) return KBD.pageup;
if (matchesKey(data, Key.pageDown)) return KBD.pagedown;
if (matchesKey(data, Key.home)) return KBD.home;
if (matchesKey(data, Key.end)) return KBD.end;
if (matchesKey(data, Key.insert)) return KBD.insert;
if (matchesKey(data, Key.delete)) return KBD.delete;
if (matchesKey(data, Key.f1)) return KBD.f1;
if (matchesKey(data, Key.f2)) return KBD.f2;
if (matchesKey(data, Key.f3)) return KBD.f3;
if (matchesKey(data, Key.f4)) return KBD.f4;
if (matchesKey(data, Key.f5)) return KBD.f5;
if (matchesKey(data, Key.f6)) return KBD.f6;
if (matchesKey(data, Key.f7)) return KBD.f7;
if (matchesKey(data, Key.f8)) return KBD.f8;
if (matchesKey(data, Key.f9)) return KBD.f9;
if (matchesKey(data, Key.f10)) return KBD.f10;
if (matchesKey(data, Key.f11)) return KBD.f11;
if (matchesKey(data, Key.f12)) return KBD.f12;
if (data.length === 1) {
const code = data.charCodeAt(0);
if (data >= "a" && data <= "z") return code - 32;
if (data >= "A" && data <= "Z") return code;
if (data >= "0" && data <= "9") return code;
}
return null;
}

View file

@ -1,438 +0,0 @@
/**
* Persistent DOSBox Instance Manager
*
* Manages a singleton DOSBox instance that runs in the background.
* Provides API for sending keys, reading screen, and taking screenshots.
*/
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { deflateSync } from "node:zlib";
import type { CommandInterface, Emulators } from "emulators";
const __dirname = dirname(fileURLToPath(import.meta.url));
let emulatorsInstance: Emulators | undefined;
async function getEmulators(): Promise<Emulators> {
if (!emulatorsInstance) {
const require = createRequire(import.meta.url);
const distPath = dirname(require.resolve("emulators"));
await import("emulators");
const g = globalThis as unknown as { emulators: Emulators };
const emu = g.emulators;
emu.pathPrefix = `${distPath}/`;
emu.pathSuffix = "";
emulatorsInstance = emu;
}
return emulatorsInstance;
}
export interface DosboxState {
width: number;
height: number;
lastFrame: Uint8Array | null;
isGraphicsMode: boolean;
}
// Emscripten FS type
interface EmscriptenFS {
mkdir(path: string): void;
writeFile(path: string, data: Uint8Array | Buffer): void;
readFile(path: string): Uint8Array;
readdir(path: string): string[];
unlink(path: string): void;
}
interface EmscriptenModule {
FS: EmscriptenFS;
_rescanFilesystem?: () => void;
}
export class DosboxInstance {
private static instance: DosboxInstance | null = null;
private ci: CommandInterface | null = null;
private state: DosboxState = {
width: 0,
height: 0,
lastFrame: null,
isGraphicsMode: false,
};
private frameListeners: Set<(rgba: Uint8Array, width: number, height: number) => void> = new Set();
private initPromise: Promise<void> | null = null;
private disposed = false;
private constructor() {}
static async getInstance(): Promise<DosboxInstance> {
if (!DosboxInstance.instance) {
DosboxInstance.instance = new DosboxInstance();
await DosboxInstance.instance.init();
}
return DosboxInstance.instance;
}
static getInstanceSync(): DosboxInstance | null {
return DosboxInstance.instance;
}
static async destroyInstance(): Promise<void> {
if (DosboxInstance.instance) {
await DosboxInstance.instance.dispose();
DosboxInstance.instance = null;
}
}
private async init(): Promise<void> {
if (this.initPromise) return this.initPromise;
this.initPromise = (async () => {
const emu = await getEmulators();
const bundle = await this.createBundle(emu);
this.ci = await emu.dosboxDirect(bundle);
// Mount QBasic files after DOSBox starts
await this.mountQBasic();
const events = this.ci.events();
events.onFrameSize((width: number, height: number) => {
this.state.width = width;
this.state.height = height;
});
events.onFrame((rgb: Uint8Array | null, rgba: Uint8Array | null) => {
if (!this.state.width || !this.state.height) {
if (this.ci) {
this.state.width = this.ci.width();
this.state.height = this.ci.height();
}
}
const rgbaFrame = rgba ?? (rgb ? this.expandRgbToRgba(rgb) : null);
if (rgbaFrame) {
this.state.lastFrame = rgbaFrame;
// Detect graphics mode by checking if we're in standard text resolution
// Text mode is typically 640x400 or 720x400
this.state.isGraphicsMode = this.state.width !== 640 && this.state.width !== 720;
for (const listener of this.frameListeners) {
listener(rgbaFrame, this.state.width, this.state.height);
}
}
});
events.onExit(() => {
this.disposed = true;
DosboxInstance.instance = null;
});
})();
return this.initPromise;
}
private async createBundle(emu: Emulators): Promise<Uint8Array> {
const bundle = await emu.bundle();
// Simple autoexec - we mount files to /home/web_user which maps to C:
bundle.autoexec(
"@echo off",
"c:",
"cls",
"echo QuickBASIC 4.5 is at C:\\QB",
"echo Type: CD QB",
"echo Then: QB.EXE",
"echo.",
"dir",
);
return bundle.toUint8Array(true);
}
private async mountQBasic(): Promise<void> {
if (!this.ci) return;
// Access Emscripten module
const transport = (this.ci as unknown as { transport: { module: EmscriptenModule } }).transport;
const Module = transport.module;
const FS = Module.FS;
// jsdos mounts C: to /home/web_user by default
// Let's verify and find the correct path
const mountPath = "/home/web_user";
// Create QB directory
const qbPath = `${mountPath}/QB`;
try {
FS.mkdir(qbPath);
} catch {
/* exists */
}
// Read QBasic files from the extension directory
const qbasicDir = join(__dirname, "..", "qbasic");
const { readdirSync, readFileSync } = await import("node:fs");
const files = readdirSync(qbasicDir);
for (const file of files) {
if (file.startsWith(".")) continue;
try {
const data = readFileSync(join(qbasicDir, file));
FS.writeFile(`${qbPath}/${file.toUpperCase()}`, data);
} catch (e) {
console.error(`Failed to mount ${file}:`, e);
}
}
// Rescan so DOS sees the new files
if (Module._rescanFilesystem) {
Module._rescanFilesystem();
}
}
private expandRgbToRgba(rgb: Uint8Array): Uint8Array {
const rgba = new Uint8Array((rgb.length / 3) * 4);
for (let i = 0, j = 0; i < rgb.length; i += 3, j += 4) {
rgba[j] = rgb[i] ?? 0;
rgba[j + 1] = rgb[i + 1] ?? 0;
rgba[j + 2] = rgb[i + 2] ?? 0;
rgba[j + 3] = 255;
}
return rgba;
}
isReady(): boolean {
return this.ci !== null && !this.disposed;
}
getState(): DosboxState {
return { ...this.state };
}
getCommandInterface(): CommandInterface | null {
return this.ci;
}
addFrameListener(listener: (rgba: Uint8Array, width: number, height: number) => void): void {
this.frameListeners.add(listener);
}
removeFrameListener(listener: (rgba: Uint8Array, width: number, height: number) => void): void {
this.frameListeners.delete(listener);
}
/**
* Send key events to DOSBox
*/
sendKeys(keys: string): void {
if (!this.ci) return;
for (const key of keys) {
const keyCode = this.charToKeyCode(key);
if (keyCode !== null) {
const needsShift = this.needsShift(key);
if (needsShift) {
this.ci.sendKeyEvent(KBD.leftshift, true);
}
this.ci.sendKeyEvent(keyCode, true);
this.ci.sendKeyEvent(keyCode, false);
if (needsShift) {
this.ci.sendKeyEvent(KBD.leftshift, false);
}
}
}
}
/**
* Send a special key (enter, backspace, etc.)
*/
sendSpecialKey(key: "enter" | "backspace" | "tab" | "escape" | "up" | "down" | "left" | "right" | "f5"): void {
if (!this.ci) return;
const keyCode = KBD[key];
if (keyCode) {
this.ci.sendKeyEvent(keyCode, true);
this.ci.sendKeyEvent(keyCode, false);
}
}
/**
* Read text-mode screen content
*/
readScreenText(): string | null {
if (!this.ci) return null;
try {
// Try to get screen text from emulators API
const text = (this.ci as unknown as { screenText?: () => string }).screenText?.();
return text ?? null;
} catch {
return null;
}
}
/**
* Get screenshot as PNG base64
*/
getScreenshot(): { base64: string; width: number; height: number } | null {
if (!this.state.lastFrame || !this.state.width || !this.state.height) {
return null;
}
const png = this.encodePng(this.state.width, this.state.height, this.state.lastFrame);
return {
base64: png.toString("base64"),
width: this.state.width,
height: this.state.height,
};
}
private encodePng(width: number, height: number, rgba: Uint8Array): Buffer {
const stride = width * 4;
const raw = Buffer.alloc((stride + 1) * height);
for (let y = 0; y < height; y++) {
const rowOffset = y * (stride + 1);
raw[rowOffset] = 0;
raw.set(rgba.subarray(y * stride, y * stride + stride), rowOffset + 1);
}
const compressed = deflateSync(raw);
const header = Buffer.alloc(13);
header.writeUInt32BE(width, 0);
header.writeUInt32BE(height, 4);
header[8] = 8;
header[9] = 6;
header[10] = 0;
header[11] = 0;
header[12] = 0;
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = this.createChunk("IHDR", header);
const idat = this.createChunk("IDAT", compressed);
const iend = this.createChunk("IEND", Buffer.alloc(0));
return Buffer.concat([signature, ihdr, idat, iend]);
}
private createChunk(type: string, data: Buffer): Buffer {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length, 0);
const typeBuffer = Buffer.from(type, "ascii");
const crcBuffer = Buffer.concat([typeBuffer, data]);
const crc = this.crc32(crcBuffer);
const crcOut = Buffer.alloc(4);
crcOut.writeUInt32BE(crc, 0);
return Buffer.concat([length, typeBuffer, data, crcOut]);
}
private crc32(buffer: Buffer): number {
let crc = 0xffffffff;
for (const byte of buffer) {
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
private charToKeyCode(char: string): number | null {
const lower = char.toLowerCase();
if (lower >= "a" && lower <= "z") {
return lower.charCodeAt(0) - 32; // A-Z = 65-90
}
if (char >= "0" && char <= "9") {
return char.charCodeAt(0); // 0-9 = 48-57
}
if (char === " ") return KBD.space;
if (char === "\n" || char === "\r") return KBD.enter;
if (char === "\t") return KBD.tab;
// Common punctuation
const punct: Record<string, number> = {
".": 46,
",": 44,
";": 59,
":": 59, // shift
"'": 39,
'"': 39, // shift
"-": 45,
_: 45, // shift
"=": 61,
"+": 61, // shift
"[": 91,
"]": 93,
"\\": 92,
"/": 47,
"!": 49, // shift+1
"@": 50, // shift+2
"#": 51, // shift+3
$: 52, // shift+4
"%": 53, // shift+5
"^": 54, // shift+6
"&": 55, // shift+7
"*": 56, // shift+8
"(": 57, // shift+9
")": 48, // shift+0
};
return punct[char] ?? null;
}
private needsShift(char: string): boolean {
if (char >= "A" && char <= "Z") return true;
return '~!@#$%^&*()_+{}|:"<>?'.includes(char);
}
async dispose(): Promise<void> {
if (this.disposed) return;
this.disposed = true;
this.frameListeners.clear();
if (this.ci) {
const origLog = console.log;
const origError = console.error;
console.log = () => {};
console.error = () => {};
try {
await this.ci.exit();
} catch {
/* ignore */
}
setTimeout(() => {
console.log = origLog;
console.error = origError;
}, 100);
this.ci = null;
}
}
}
// js-dos key codes
const KBD: Record<string, number> = {
enter: 257,
backspace: 259,
tab: 258,
escape: 256,
space: 32,
leftshift: 340,
up: 265,
down: 264,
left: 263,
right: 262,
f5: 294,
};
const CRC_TABLE = createCrcTable();
function createCrcTable(): Uint32Array {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
if (c & 1) {
c = 0xedb88320 ^ (c >>> 1);
} else {
c >>>= 1;
}
}
table[i] = c >>> 0;
}
return table;
}