mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 10:02:23 +00:00
feat(pi-dosbox): persistent DOSBox with QBasic and agent tool
- DOSBox now starts at session_start and persists in background - /dosbox command attaches UI to running instance (Ctrl+Q detaches) - Added dosbox tool with actions: send_keys, screenshot, read_text - Bundled QuickBASIC 4.5 files, mounted at C:\QB on startup - Agent can interact with DOSBox programmatically via tool Use: pi -e ./examples/extensions/pi-dosbox Then: /dosbox to view, or let agent use the dosbox tool
This commit is contained in:
parent
fbd6b7f9ba
commit
4f343f39b9
26 changed files with 1618 additions and 373 deletions
|
|
@ -1,44 +1,183 @@
|
||||||
/**
|
/**
|
||||||
* DOSBox extension for pi
|
* 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
|
* Usage: pi --extension ./examples/extensions/pi-dosbox
|
||||||
* Command: /dosbox [bundle.jsdos]
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile } from "node:fs/promises";
|
import { StringEnum } from "@mariozechner/pi-ai";
|
||||||
import { resolve } from "node:path";
|
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
import { DosboxComponent } from "./src/dosbox-component.js";
|
import { DosboxComponent } from "./src/dosbox-component.js";
|
||||||
|
import { DosboxInstance } from "./src/dosbox-instance.js";
|
||||||
|
|
||||||
export default function (pi: ExtensionAPI) {
|
export default function (pi: ExtensionAPI) {
|
||||||
pi.registerCommand("dosbox", {
|
// Start DOSBox instance at session start
|
||||||
description: "Run DOSBox emulator",
|
pi.on("session_start", async () => {
|
||||||
|
try {
|
||||||
|
await DosboxInstance.getInstance();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start DOSBox:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
handler: async (args, ctx) => {
|
// 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) {
|
if (!ctx.hasUI) {
|
||||||
ctx.ui.notify("DOSBox requires interactive mode", "error");
|
ctx.ui.notify("DOSBox requires interactive mode", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bundlePath = args?.trim();
|
// Ensure instance is running
|
||||||
let bundleData: Uint8Array | undefined;
|
const instance = DosboxInstance.getInstanceSync();
|
||||||
if (bundlePath) {
|
if (!instance || !instance.isReady()) {
|
||||||
try {
|
ctx.ui.notify("DOSBox is not running. It should start automatically.", "error");
|
||||||
const resolvedPath = resolve(ctx.cwd, bundlePath);
|
return;
|
||||||
bundleData = await readFile(resolvedPath);
|
|
||||||
} catch (error) {
|
|
||||||
ctx.ui.notify(
|
|
||||||
`Failed to load bundle: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.ui.custom((tui, theme, _kb, done) => {
|
await ctx.ui.custom((tui, theme, _kb, done) => {
|
||||||
const fallbackColor = (s: string) => theme.fg("warning", s);
|
const fallbackColor = (s: string) => theme.fg("warning", s);
|
||||||
return new DosboxComponent(tui, fallbackColor, () => done(undefined), bundleData);
|
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: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx tsx src/main.ts",
|
|
||||||
"clean": "echo 'nothing to clean'",
|
"clean": "echo 'nothing to clean'",
|
||||||
"build": "echo 'nothing to build'",
|
"build": "echo 'nothing to build'",
|
||||||
"check": "echo 'nothing to check'"
|
"check": "echo 'nothing to check'"
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,71 @@
|
||||||
|
'***
|
||||||
|
' 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)
|
||||||
|
'
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,198 @@
|
||||||
|
'
|
||||||
|
' 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%())
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,545 @@
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
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/ -->
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
||||||
|
qb/l
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
* DOSBox TUI Component
|
* DOSBox TUI Component
|
||||||
*
|
*
|
||||||
* Renders DOSBox framebuffer as an image in the terminal.
|
* Renders DOSBox framebuffer as an image in the terminal.
|
||||||
|
* Connects to the persistent DosboxInstance.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import { dirname } from "node:path";
|
|
||||||
import { deflateSync } from "node:zlib";
|
|
||||||
import type { Component } from "@mariozechner/pi-tui";
|
import type { Component } from "@mariozechner/pi-tui";
|
||||||
import {
|
import {
|
||||||
allocateImageId,
|
allocateImageId,
|
||||||
|
|
@ -18,293 +16,12 @@ import {
|
||||||
matchesKey,
|
matchesKey,
|
||||||
truncateToWidth,
|
truncateToWidth,
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
import type { CommandInterface, Emulators } from "emulators";
|
import { DosboxInstance } from "./dosbox-instance.js";
|
||||||
|
|
||||||
const MAX_WIDTH_CELLS = 120;
|
const MAX_WIDTH_CELLS = 120;
|
||||||
|
|
||||||
let emulatorsInstance: Emulators | undefined;
|
// js-dos key codes
|
||||||
|
const KBD: Record<string, number> = {
|
||||||
async function getEmulators(): Promise<Emulators> {
|
|
||||||
if (!emulatorsInstance) {
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const distPath = dirname(require.resolve("emulators"));
|
|
||||||
// The emulators package assigns to global.emulators, not module.exports
|
|
||||||
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 class DosboxComponent implements Component {
|
|
||||||
private tui: { requestRender: () => void };
|
|
||||||
private onClose: () => void;
|
|
||||||
private ci: CommandInterface | null = null;
|
|
||||||
private image: Image | null = null;
|
|
||||||
private imageTheme: ImageTheme;
|
|
||||||
private frameWidth = 0;
|
|
||||||
private frameHeight = 0;
|
|
||||||
private loadingMessage = "Loading DOSBox...";
|
|
||||||
private errorMessage: string | null = null;
|
|
||||||
private cachedLines: string[] = [];
|
|
||||||
private cachedWidth = 0;
|
|
||||||
private cachedVersion = -1;
|
|
||||||
private version = 0;
|
|
||||||
private disposed = false;
|
|
||||||
private bundleData?: Uint8Array;
|
|
||||||
private kittyPushed = false;
|
|
||||||
private imageId: number;
|
|
||||||
|
|
||||||
wantsKeyRelease = true;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
tui: { requestRender: () => void },
|
|
||||||
fallbackColor: (s: string) => string,
|
|
||||||
onClose: () => void,
|
|
||||||
bundleData?: Uint8Array,
|
|
||||||
) {
|
|
||||||
this.tui = tui;
|
|
||||||
this.onClose = onClose;
|
|
||||||
this.bundleData = bundleData;
|
|
||||||
this.imageTheme = { fallbackColor };
|
|
||||||
this.imageId = allocateImageId();
|
|
||||||
void this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async init(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const emu = await getEmulators();
|
|
||||||
const initData = this.bundleData ?? (await this.createDefaultBundle(emu));
|
|
||||||
this.ci = await emu.dosboxDirect(initData);
|
|
||||||
|
|
||||||
const events = this.ci.events();
|
|
||||||
events.onFrameSize((width: number, height: number) => {
|
|
||||||
this.frameWidth = width;
|
|
||||||
this.frameHeight = height;
|
|
||||||
this.version++;
|
|
||||||
this.tui.requestRender();
|
|
||||||
});
|
|
||||||
events.onFrame((rgb: Uint8Array | null, rgba: Uint8Array | null) => {
|
|
||||||
this.updateFrame(rgb, rgba);
|
|
||||||
});
|
|
||||||
events.onExit(() => {
|
|
||||||
this.dispose();
|
|
||||||
this.onClose();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Push Kitty enhanced mode for proper key press/release
|
|
||||||
process.stdout.write("\x1b[>15u");
|
|
||||||
this.kittyPushed = true;
|
|
||||||
} catch (error) {
|
|
||||||
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
this.tui.requestRender();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createDefaultBundle(emu: Emulators): Promise<Uint8Array> {
|
|
||||||
const bundle = await emu.bundle();
|
|
||||||
bundle.autoexec(
|
|
||||||
"@echo off",
|
|
||||||
"cls",
|
|
||||||
"echo pi DOSBox",
|
|
||||||
"echo.",
|
|
||||||
"echo Pass a .jsdos bundle to run a game",
|
|
||||||
"echo.",
|
|
||||||
"dir",
|
|
||||||
);
|
|
||||||
return bundle.toUint8Array(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateFrame(rgb: Uint8Array | null, rgba: Uint8Array | null): void {
|
|
||||||
if (!this.frameWidth || !this.frameHeight) {
|
|
||||||
if (this.ci) {
|
|
||||||
this.frameWidth = this.ci.width();
|
|
||||||
this.frameHeight = this.ci.height();
|
|
||||||
}
|
|
||||||
if (!this.frameWidth || !this.frameHeight) return;
|
|
||||||
}
|
|
||||||
const rgbaFrame = rgba ?? (rgb ? expandRgbToRgba(rgb) : null);
|
|
||||||
if (!rgbaFrame) return;
|
|
||||||
|
|
||||||
const png = encodePng(this.frameWidth, this.frameHeight, rgbaFrame);
|
|
||||||
const base64 = png.toString("base64");
|
|
||||||
this.image = new Image(
|
|
||||||
base64,
|
|
||||||
"image/png",
|
|
||||||
this.imageTheme,
|
|
||||||
{ maxWidthCells: MAX_WIDTH_CELLS, imageId: this.imageId },
|
|
||||||
{ widthPx: this.frameWidth, heightPx: this.frameHeight },
|
|
||||||
);
|
|
||||||
this.version++;
|
|
||||||
this.tui.requestRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInput(data: string): void {
|
|
||||||
const released = isKeyRelease(data);
|
|
||||||
|
|
||||||
if (!released && matchesKey(data, Key.ctrl("q"))) {
|
|
||||||
this.dispose();
|
|
||||||
this.onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.ci) return;
|
|
||||||
|
|
||||||
const parsed = parseKeyWithModifiers(data);
|
|
||||||
if (!parsed) return;
|
|
||||||
|
|
||||||
const { keyCode, shift, ctrl, alt } = parsed;
|
|
||||||
|
|
||||||
if (shift) this.ci.sendKeyEvent(KBD.leftshift, !released);
|
|
||||||
if (ctrl) this.ci.sendKeyEvent(KBD.leftctrl, !released);
|
|
||||||
if (alt) this.ci.sendKeyEvent(KBD.leftalt, !released);
|
|
||||||
|
|
||||||
this.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.ci) {
|
|
||||||
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 quit\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;
|
|
||||||
}
|
|
||||||
if (this.ci) {
|
|
||||||
// Suppress emulators exit logging
|
|
||||||
const origLog = console.log;
|
|
||||||
const origError = console.error;
|
|
||||||
console.log = () => {};
|
|
||||||
console.error = () => {};
|
|
||||||
const ci = this.ci;
|
|
||||||
this.ci = null;
|
|
||||||
void ci
|
|
||||||
.exit()
|
|
||||||
.catch(() => undefined)
|
|
||||||
.finally(() => {
|
|
||||||
// Restore after a delay to catch async logging
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log = origLog;
|
|
||||||
console.error = origError;
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CRC_TABLE = createCrcTable();
|
|
||||||
|
|
||||||
function 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 = createChunk("IHDR", header);
|
|
||||||
const idat = createChunk("IDAT", compressed);
|
|
||||||
const iend = createChunk("IEND", Buffer.alloc(0));
|
|
||||||
|
|
||||||
return Buffer.concat([signature, ihdr, idat, iend]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function 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 = crc32(crcBuffer);
|
|
||||||
const crcOut = Buffer.alloc(4);
|
|
||||||
crcOut.writeUInt32BE(crc, 0);
|
|
||||||
return Buffer.concat([length, typeBuffer, data, crcOut]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function crc32(buffer: Buffer): number {
|
|
||||||
let crc = 0xffffffff;
|
|
||||||
for (const byte of buffer) {
|
|
||||||
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
||||||
}
|
|
||||||
return (crc ^ 0xffffffff) >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// js-dos key codes (from js-dos/src/window/dos/controls/keys.ts)
|
|
||||||
const KBD = {
|
|
||||||
enter: 257,
|
enter: 257,
|
||||||
backspace: 259,
|
backspace: 259,
|
||||||
tab: 258,
|
tab: 258,
|
||||||
|
|
@ -340,6 +57,209 @@ const KBD = {
|
||||||
f12: 301,
|
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 {
|
interface ParsedKey {
|
||||||
keyCode: number;
|
keyCode: number;
|
||||||
shift: boolean;
|
shift: boolean;
|
||||||
|
|
@ -357,7 +277,6 @@ function decodeModifiers(modifierField: number): { shift: boolean; ctrl: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseKeyWithModifiers(data: string): ParsedKey | null {
|
function parseKeyWithModifiers(data: string): ParsedKey | null {
|
||||||
// Kitty CSI u sequences: \x1b[codepoint(:shifted(:base))?;modifier(:event)?u
|
|
||||||
if (data.startsWith("\x1b[") && data.endsWith("u")) {
|
if (data.startsWith("\x1b[") && data.endsWith("u")) {
|
||||||
const body = data.slice(2, -1);
|
const body = data.slice(2, -1);
|
||||||
const [keyPart, modifierPart] = body.split(";");
|
const [keyPart, modifierPart] = body.split(";");
|
||||||
|
|
@ -374,7 +293,6 @@ function parseKeyWithModifiers(data: string): ParsedKey | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSI sequences: \x1b[<num>;<modifier>:<event><suffix>
|
|
||||||
const csiMatch = data.match(/^\x1b\[(\d+);(\d+)(?::\d+)?([~A-Za-z])$/);
|
const csiMatch = data.match(/^\x1b\[(\d+);(\d+)(?::\d+)?([~A-Za-z])$/);
|
||||||
if (csiMatch) {
|
if (csiMatch) {
|
||||||
const code = parseInt(csiMatch[1], 10);
|
const code = parseInt(csiMatch[1], 10);
|
||||||
|
|
@ -386,7 +304,6 @@ function parseKeyWithModifiers(data: string): ParsedKey | null {
|
||||||
return { keyCode, shift, ctrl, alt };
|
return { keyCode, shift, ctrl, alt };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy/simple input
|
|
||||||
const keyCode = mapKeyToJsDos(data);
|
const keyCode = mapKeyToJsDos(data);
|
||||||
if (keyCode === null) return null;
|
if (keyCode === null) return null;
|
||||||
const shift = data.length === 1 && data >= "A" && data <= "Z";
|
const shift = data.length === 1 && data >= "A" && data <= "Z";
|
||||||
|
|
@ -399,9 +316,9 @@ function codepointToJsDosKey(codepoint: number): number | null {
|
||||||
if (codepoint === 27) return KBD.esc;
|
if (codepoint === 27) return KBD.esc;
|
||||||
if (codepoint === 8 || codepoint === 127) return KBD.backspace;
|
if (codepoint === 8 || codepoint === 127) return KBD.backspace;
|
||||||
if (codepoint === 32) return KBD.space;
|
if (codepoint === 32) return KBD.space;
|
||||||
if (codepoint >= 97 && codepoint <= 122) return codepoint - 32; // a-z -> A-Z
|
if (codepoint >= 97 && codepoint <= 122) return codepoint - 32;
|
||||||
if (codepoint >= 65 && codepoint <= 90) return codepoint; // A-Z
|
if (codepoint >= 65 && codepoint <= 90) return codepoint;
|
||||||
if (codepoint >= 48 && codepoint <= 57) return codepoint; // 0-9
|
if (codepoint >= 48 && codepoint <= 57) return codepoint;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,14 +362,6 @@ function mapCsiKeyToJsDos(code: number, suffix: string): number | null {
|
||||||
return KBD.pageup;
|
return KBD.pageup;
|
||||||
case 6:
|
case 6:
|
||||||
return KBD.pagedown;
|
return KBD.pagedown;
|
||||||
case 11:
|
|
||||||
return KBD.f1;
|
|
||||||
case 12:
|
|
||||||
return KBD.f2;
|
|
||||||
case 13:
|
|
||||||
return KBD.f3;
|
|
||||||
case 14:
|
|
||||||
return KBD.f4;
|
|
||||||
case 15:
|
case 15:
|
||||||
return KBD.f5;
|
return KBD.f5;
|
||||||
case 17:
|
case 17:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,428 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 to C:
|
||||||
|
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();
|
||||||
|
bundle.autoexec(
|
||||||
|
"@echo off",
|
||||||
|
"mount c c:",
|
||||||
|
"c:",
|
||||||
|
"cls",
|
||||||
|
"echo QuickBASIC 4.5 mounted at C:\\QB",
|
||||||
|
"echo.",
|
||||||
|
"dir /w",
|
||||||
|
);
|
||||||
|
return bundle.toUint8Array(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async mountQBasic(): Promise<void> {
|
||||||
|
if (!this.ci) return;
|
||||||
|
|
||||||
|
const fs = (this.ci as unknown as { transport: { module: { FS: EmscriptenFS } } }).transport.module.FS;
|
||||||
|
const rescan = (this.ci as unknown as { transport: { module: { _rescanFilesystem: () => void } } }).transport
|
||||||
|
.module._rescanFilesystem;
|
||||||
|
|
||||||
|
// Create directory structure
|
||||||
|
try {
|
||||||
|
fs.mkdir("/c");
|
||||||
|
} catch {
|
||||||
|
/* exists */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fs.mkdir("/c/QB");
|
||||||
|
} 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(`/c/QB/${file.toUpperCase()}`, data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to mount ${file}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rescan so DOS sees the new files
|
||||||
|
rescan();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
#!/usr/bin/env npx tsx
|
|
||||||
/**
|
|
||||||
* Standalone DOSBox TUI app
|
|
||||||
*
|
|
||||||
* Usage: npx tsx src/main.ts [bundle.jsdos]
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import { resolve } from "node:path";
|
|
||||||
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
|
||||||
import { DosboxComponent } from "./dosbox-component.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const bundlePath = process.argv[2];
|
|
||||||
let bundleData: Uint8Array | undefined;
|
|
||||||
|
|
||||||
if (bundlePath) {
|
|
||||||
try {
|
|
||||||
const resolvedPath = resolve(process.cwd(), bundlePath);
|
|
||||||
bundleData = await readFile(resolvedPath);
|
|
||||||
console.log(`Loading bundle: ${resolvedPath}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load bundle: ${error instanceof Error ? error.message : String(error)}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const terminal = new ProcessTerminal();
|
|
||||||
const tui = new TUI(terminal);
|
|
||||||
|
|
||||||
const fallbackColor = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
||||||
|
|
||||||
const component = new DosboxComponent(
|
|
||||||
tui,
|
|
||||||
fallbackColor,
|
|
||||||
() => {
|
|
||||||
tui.stop();
|
|
||||||
process.exit(0);
|
|
||||||
},
|
|
||||||
bundleData,
|
|
||||||
);
|
|
||||||
|
|
||||||
tui.addChild(component);
|
|
||||||
tui.setFocus(component);
|
|
||||||
tui.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue