diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/index.ts b/packages/coding-agent/examples/extensions/pi-dosbox/index.ts index df8c3fe8..8e084b9b 100644 --- a/packages/coding-agent/examples/extensions/pi-dosbox/index.ts +++ b/packages/coding-agent/examples/extensions/pi-dosbox/index.ts @@ -1,44 +1,183 @@ /** * 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 - * Command: /dosbox [bundle.jsdos] */ -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; +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) { - pi.registerCommand("dosbox", { - description: "Run DOSBox emulator", + // Start DOSBox instance at session start + 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) { ctx.ui.notify("DOSBox requires interactive mode", "error"); return; } - const bundlePath = args?.trim(); - let bundleData: Uint8Array | undefined; - if (bundlePath) { - try { - const resolvedPath = resolve(ctx.cwd, bundlePath); - bundleData = await readFile(resolvedPath); - } catch (error) { - ctx.ui.notify( - `Failed to load bundle: ${error instanceof Error ? error.message : String(error)}`, - "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), 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: 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: {}, + }; + } + }, + }); } diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/package.json b/packages/coding-agent/examples/extensions/pi-dosbox/package.json index ac95e876..6f03b3a3 100644 --- a/packages/coding-agent/examples/extensions/pi-dosbox/package.json +++ b/packages/coding-agent/examples/extensions/pi-dosbox/package.json @@ -4,7 +4,6 @@ "version": "0.0.1", "type": "module", "scripts": { - "start": "npx tsx src/main.ts", "clean": "echo 'nothing to clean'", "build": "echo 'nothing to build'", "check": "echo 'nothing to check'" diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BC.EXE b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BC.EXE new file mode 100644 index 00000000..1d1ba2f2 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BC.EXE differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BCOM45.LIB b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BCOM45.LIB new file mode 100644 index 00000000..6b917e5b Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BCOM45.LIB differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BQLB45.LIB b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BQLB45.LIB new file mode 100644 index 00000000..46075967 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BQLB45.LIB differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BRUN45.LIB b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BRUN45.LIB new file mode 100644 index 00000000..3a6b27ca Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/BRUN45.LIB differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/LIB.EXE b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/LIB.EXE new file mode 100644 index 00000000..53ea8d77 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/LIB.EXE differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/LINK.EXE b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/LINK.EXE new file mode 100644 index 00000000..3aa9752f Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/LINK.EXE differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.BI b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.BI new file mode 100644 index 00000000..b1c9cfd2 --- /dev/null +++ b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.BI @@ -0,0 +1,71 @@ +'*** +' QB.BI - Assembly Support Include File +' +' Copyright 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) +' diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.EXE b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.EXE new file mode 100644 index 00000000..5dd1a1f1 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.EXE differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.INI b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.INI new file mode 100644 index 00000000..a348029a Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.INI differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.LIB b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.LIB new file mode 100644 index 00000000..1aea1094 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.LIB differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.QLB b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.QLB new file mode 100644 index 00000000..d3ed3fa9 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB.QLB differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB45ADVR.OBJ b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB45ADVR.OBJ new file mode 100644 index 00000000..607d0218 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB45ADVR.OBJ differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB45QCK.HLP b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB45QCK.HLP new file mode 100644 index 00000000..247ddf4e Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB45QCK.HLP differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB4UTIL.BI b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB4UTIL.BI new file mode 100644 index 00000000..c95a09f8 --- /dev/null +++ b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB4UTIL.BI @@ -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%()) + \ No newline at end of file diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB4UTIL.LIB b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB4UTIL.LIB new file mode 100644 index 00000000..7565fdfe Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB4UTIL.LIB differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB_EDR.LIB b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB_EDR.LIB new file mode 100644 index 00000000..6199c0d6 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/QB_EDR.LIB differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/README.DOC b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/README.DOC new file mode 100644 index 00000000..96e8ccd4 --- /dev/null +++ b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/README.DOC @@ -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 - +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. diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/Readme!!!!!!.txt b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/Readme!!!!!!.txt new file mode 100644 index 00000000..54ca62a1 --- /dev/null +++ b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/Readme!!!!!!.txt @@ -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/ --> \ No newline at end of file diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/qb45advr.hlp b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/qb45advr.hlp new file mode 100644 index 00000000..7d602e9e Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/qb45advr.hlp differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/qb45ener.hlp b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/qb45ener.hlp new file mode 100644 index 00000000..ea9ae950 Binary files /dev/null and b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/qb45ener.hlp differ diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/start.bat b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/start.bat new file mode 100644 index 00000000..c57bb5a6 --- /dev/null +++ b/packages/coding-agent/examples/extensions/pi-dosbox/qbasic/start.bat @@ -0,0 +1 @@ +qb/l \ No newline at end of file diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts index 327036e1..995c7249 100644 --- a/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts +++ b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-component.ts @@ -2,11 +2,9 @@ * DOSBox TUI Component * * 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 { allocateImageId, @@ -18,293 +16,12 @@ import { matchesKey, truncateToWidth, } from "@mariozechner/pi-tui"; -import type { CommandInterface, Emulators } from "emulators"; +import { DosboxInstance } from "./dosbox-instance.js"; const MAX_WIDTH_CELLS = 120; -let emulatorsInstance: Emulators | undefined; - -async function getEmulators(): Promise { - 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 { - 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 { - 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[ {}; - 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 = { +// js-dos key codes +const KBD: Record = { enter: 257, backspace: 259, tab: 258, @@ -340,6 +57,209 @@ const KBD = { 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 { + 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[>> 1); + } else { + c >>>= 1; + } + } + table[i] = c >>> 0; + } + return table; +} + interface ParsedKey { keyCode: number; shift: boolean; @@ -357,7 +277,6 @@ function decodeModifiers(modifierField: number): { shift: boolean; ctrl: boolean } function parseKeyWithModifiers(data: string): ParsedKey | null { - // Kitty CSI u sequences: \x1b[codepoint(:shifted(:base))?;modifier(:event)?u if (data.startsWith("\x1b[") && data.endsWith("u")) { const body = data.slice(2, -1); const [keyPart, modifierPart] = body.split(";"); @@ -374,7 +293,6 @@ function parseKeyWithModifiers(data: string): ParsedKey | null { } } - // CSI sequences: \x1b[;: const csiMatch = data.match(/^\x1b\[(\d+);(\d+)(?::\d+)?([~A-Za-z])$/); if (csiMatch) { const code = parseInt(csiMatch[1], 10); @@ -386,7 +304,6 @@ function parseKeyWithModifiers(data: string): ParsedKey | null { return { keyCode, shift, ctrl, alt }; } - // Legacy/simple input const keyCode = mapKeyToJsDos(data); if (keyCode === null) return null; 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 === 8 || codepoint === 127) return KBD.backspace; if (codepoint === 32) return KBD.space; - if (codepoint >= 97 && codepoint <= 122) return codepoint - 32; // a-z -> A-Z - if (codepoint >= 65 && codepoint <= 90) return codepoint; // A-Z - if (codepoint >= 48 && codepoint <= 57) return codepoint; // 0-9 + if (codepoint >= 97 && codepoint <= 122) return codepoint - 32; + if (codepoint >= 65 && codepoint <= 90) return codepoint; + if (codepoint >= 48 && codepoint <= 57) return codepoint; return null; } @@ -445,14 +362,6 @@ function mapCsiKeyToJsDos(code: number, suffix: string): number | null { return KBD.pageup; case 6: 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: return KBD.f5; case 17: diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-instance.ts b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-instance.ts new file mode 100644 index 00000000..7a96dfd4 --- /dev/null +++ b/packages/coding-agent/examples/extensions/pi-dosbox/src/dosbox-instance.ts @@ -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 { + 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 | null = null; + private disposed = false; + + private constructor() {} + + static async getInstance(): Promise { + 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 { + if (DosboxInstance.instance) { + await DosboxInstance.instance.dispose(); + DosboxInstance.instance = null; + } + } + + private async init(): Promise { + 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 { + 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 { + 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 = { + ".": 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 { + 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 = { + 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; +} diff --git a/packages/coding-agent/examples/extensions/pi-dosbox/src/main.ts b/packages/coding-agent/examples/extensions/pi-dosbox/src/main.ts deleted file mode 100644 index dbc0b7dd..00000000 --- a/packages/coding-agent/examples/extensions/pi-dosbox/src/main.ts +++ /dev/null @@ -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); -});