\r
SUB handleInput\r
\r
-' Read mouse data\r
-IF getByte(8) <> 0 THEN\r
- putByte 8, 0\r
- ' Read mouse translation along x axis\r
- mouseXDelta = getWord(2)\r
- putWord 2, 0\r
- ' Read mouse translation along y axis\r
- mouseYDelta = getWord(4)\r
- putWord 4, 0\r
- ' Read pressed mouse buttons\r
- button = getWord(6)\r
- putWord 6, 0\r
-\r
- ' Detect if left, right or both mouse buttons were pressed\r
- leftMouseButton = 0\r
- rightMouseButton = 0\r
- IF button = 1 THEN leftMouseButton = 1\r
- IF button = 2 THEN rightMouseButton = 1\r
- IF button = 3 THEN leftMouseButton = 1: rightMouseButton = 1\r
-\r
- IF rightMouseButton = 1 THEN\r
- IF leftMouseButton = 1 THEN\r
- ' If mouse left and right buttons are pressed at the same time,\r
- ' use mouse translation to move avatar around X and Z axis.\r
- cameraXSpeed = cameraXSpeed + SIN(angleY) * mouseYDelta / 4\r
- cameraZSpeed = cameraZSpeed - COS(angleY) * mouseYDelta / 4\r
- GOTO 3\r
- END IF\r
- ' If only right button is pressed, move around Y axis\r
- cameraYSpeed = cameraYSpeed + mouseYDelta / 4\r
-3\r
- mouseYDelta = 0\r
+' Read mouse data from the driver and reset its counters (every frame)\r
+' Read mouse translation along x axis\r
+mouseXDelta = getWord(2)\r
+putWord 2, 0\r
+' Read mouse translation along y axis\r
+mouseYDelta = getWord(4)\r
+putWord 4, 0\r
+' Read pressed mouse buttons\r
+button = getWord(6)\r
+putWord 6, 0\r
+\r
+' Detect if left, right or both mouse buttons were pressed\r
+leftMouseButton = 0\r
+rightMouseButton = 0\r
+IF button = 1 THEN leftMouseButton = 1\r
+IF button = 2 THEN rightMouseButton = 1\r
+IF button = 3 THEN leftMouseButton = 1: rightMouseButton = 1\r
+\r
+IF rightMouseButton = 1 THEN\r
+ IF leftMouseButton = 1 THEN\r
+ ' If mouse left and right buttons are pressed at the same time,\r
+ ' use mouse translation to move avatar around X and Z axis.\r
+ cameraXSpeed = cameraXSpeed + SIN(angleY) * mouseYDelta / 4\r
+ cameraZSpeed = cameraZSpeed - COS(angleY) * mouseYDelta / 4\r
+ GOTO 3\r
END IF\r
-\r
+ ' If only right button is pressed, move around Y axis\r
+ cameraYSpeed = cameraYSpeed + mouseYDelta / 4\r
+3\r
+ mouseYDelta = 0\r
END IF\r
\r
+\r
' Limit mouse movement to maxMove\r
IF mouseXDelta < -maxMove THEN mouseXDelta = -maxMove\r
IF mouseXDelta > maxMove THEN mouseXDelta = maxMove\r
SUB control
-' Check for mouse input
-IF getByte(8) <> 0 THEN
- putByte 8, 0
- xPosition = getWord(2)
- putWord 2, 0
- yPosition = getWord(4)
- putWord 4, 0
- button = getWord(6)
- putWord 6, 0
-
- ' Reset mouse buttons
- buttonLeft = 0
- buttonRight = 0
-
- ' Handle mouse button states
- IF button = 1 THEN buttonLeft = 1
- IF button = 2 THEN buttonRight = 1
- IF button = 3 THEN buttonLeft = 1: buttonRight = 1
-
- ' Handle mouse movements
- IF buttonRight = 1 THEN
- IF buttonLeft = 1 THEN
- speedX = speedX + SIN(angle1) * yPosition / 4
- speedZ = speedZ - COS(angle1) * yPosition / 4
- GOTO 3
- END IF
- speedY = speedY + yPosition / 4
- 3
- yPosition = 0
+' Read mouse input from the driver, resetting its counters every frame
+xPosition = getWord(2)
+putWord 2, 0
+yPosition = getWord(4)
+putWord 4, 0
+button = getWord(6)
+putWord 6, 0
+
+' Reset mouse buttons
+buttonLeft = 0
+buttonRight = 0
+
+' Handle mouse button states
+IF button = 1 THEN buttonLeft = 1
+IF button = 2 THEN buttonRight = 1
+IF button = 3 THEN buttonLeft = 1: buttonRight = 1
+
+' Handle mouse movements
+IF buttonRight = 1 THEN
+ IF buttonLeft = 1 THEN
+ speedX = speedX + SIN(angle1) * yPosition / 4
+ speedZ = speedZ - COS(angle1) * yPosition / 4
+ GOTO 3
END IF
-
+ speedY = speedY + yPosition / 4
+3
+ yPosition = 0
END IF
' Limit the position values to prevent overflow
\r
SUB control\r
\r
-' Handle mouse input\r
-IF getbyte(8) <> 0 THEN\r
- putbyte 8, 0\r
- xp = getword(2)\r
- putword 2, 0\r
- yp = getword(4)\r
- putword 4, 0\r
- butt = getword(6)\r
- putword 6, 0\r
-\r
- ' Determine which mouse buttons are pressed\r
- buttL = 0\r
- buttR = 0\r
- IF butt = 1 THEN buttL = 1\r
- IF butt = 2 THEN buttR = 1\r
- IF butt = 3 THEN buttL = 1: buttR = 1\r
-\r
- ' Handle right mouse button for up/down movement\r
- IF buttR = 1 THEN\r
- ' Handle both buttons pressed for back/front movement\r
- IF buttL = 1 THEN\r
- myxs = myxs + SIN(an1) * yp / 4\r
- myzs = myzs - COS(an1) * yp / 4\r
- GOTO 3\r
- END IF\r
-\r
- ' Handle right button for up/down movement\r
- myys = myys + yp / 4\r
-3\r
- yp = 0\r
+' Handle mouse input: read the driver and reset its counters every frame\r
+xp = getword(2)\r
+putword 2, 0\r
+yp = getword(4)\r
+putword 4, 0\r
+butt = getword(6)\r
+putword 6, 0\r
+\r
+' Determine which mouse buttons are pressed\r
+buttL = 0\r
+buttR = 0\r
+IF butt = 1 THEN buttL = 1\r
+IF butt = 2 THEN buttR = 1\r
+IF butt = 3 THEN buttL = 1: buttR = 1\r
+\r
+' Handle right mouse button for up/down movement\r
+IF buttR = 1 THEN\r
+ ' Handle both buttons pressed for back/front movement\r
+ IF buttL = 1 THEN\r
+ myxs = myxs + SIN(an1) * yp / 4\r
+ myzs = myzs - COS(an1) * yp / 4\r
+ GOTO 3\r
END IF\r
\r
+ ' Handle right button for up/down movement\r
+ myys = myys + yp / 4\r
+3\r
+ yp = 0\r
END IF\r
\r
' Clamp user input to maximum movement speed\r
SUB control\r
\r
\r
-IF getbyte(8) <> 0 THEN\r
- putbyte 8, 0\r
- xp = getword(2)\r
- putword 2, 0\r
- yp = getword(4)\r
- putword 4, 0\r
- butt = getword(6)\r
- putword 6, 0\r
- buttL = 0\r
- buttR = 0\r
- IF butt = 1 THEN buttL = 1\r
- IF butt = 2 THEN buttR = 1\r
- IF butt = 3 THEN buttL = 1: buttR = 1\r
-\r
-\r
- IF buttR = 1 THEN\r
- IF buttL = 1 THEN\r
- myxs = myxs + SIN(an1) * yp / 4\r
- myzs = myzs - COS(an1) * yp / 4\r
- GOTO 3\r
- END IF\r
- myys = myys + yp / 4\r
-3\r
- yp = 0\r
+xp = getword(2)\r
+putword 2, 0\r
+yp = getword(4)\r
+putword 4, 0\r
+butt = getword(6)\r
+putword 6, 0\r
+buttL = 0\r
+buttR = 0\r
+IF butt = 1 THEN buttL = 1\r
+IF butt = 2 THEN buttR = 1\r
+IF butt = 3 THEN buttL = 1: buttR = 1\r
+\r
+\r
+IF buttR = 1 THEN\r
+ IF buttL = 1 THEN\r
+ myxs = myxs + SIN(an1) * yp / 4\r
+ myzs = myzs - COS(an1) * yp / 4\r
+ GOTO 3\r
END IF\r
-\r
+ myys = myys + yp / 4\r
+3\r
+ yp = 0\r
END IF\r
\r
\r
\r
\r
+\r
IF xp < -maxmove THEN xp = -maxmove\r
IF xp > maxmove THEN xp = maxmove\r
an1 = an1 - xp / 150\r
rotationAngle1 = 1.5
10
+
+SOUND 0, 1
currentFrameCount = currentFrameCount + 1
Generate3DScene
DrawScene
' Get user input for their name\r
LOCATE 5, 1\r
COLOR 7\r
-INPUT "Enter your name ", userName$\r
+INPUT "Enter your name: ", userName$\r
LOCATE 5, 1\r
COLOR 8\r
-PRINT "Enter your name " + userName$\r
+PRINT "Enter your name: " + userName$\r
\r
' Greet the user\r
LOCATE 6, 1\r
3\r
CLS\r
END SUB\r
-\r
--- /dev/null
+#!/usr/bin/env bash
+# Assemble qbext.asm into qbext.com using FASM.
+# Requires: fasm (Debian package: fasm)
+set -euo pipefail
+
+cd "$(dirname "$0")"
+
+fasm qbext.asm qbext.com
:CUSTOM_ID: overview\r
:END:\r
\r
-QBasic, a popular programming language in the DOS era, lacks native\r
-mouse support. This limitation can be a hurdle for developers looking\r
-to create interactive applications. To bridge this gap, I developed a\r
-workaround that allows QBasic to use mouse input.\r
+QBasic, a popular programming language of the DOS era, lacks native\r
+mouse support. This limitation is a hurdle for developers looking to\r
+create interactive applications. To bridge this gap, I developed a\r
+workaround that allows QBasic programs to use mouse input.\r
\r
* High-level idea\r
:PROPERTIES:\r
:CUSTOM_ID: high-level-idea\r
:END:\r
\r
-Workaround to access mouse involves a Terminate and Stay Resident\r
-(TSR) program written in x86 assembly. This TSR program must be\r
-started before running QBasic program that depends on mouse. This TSR\r
-program hooks into the system's interrupt mechanism, specifically the\r
-timer interrupt (IRQ 0), allowing it to regularly check for mouse\r
-activity several times per second.\r
-\r
-When this timer interrupt triggers, the TSR reads the latest mouse's\r
-horizontal and vertical movements and button states using mouse\r
-interrupts. This data is then stored in a dedicated memory location —\r
-a data table within the TSR's memory space. The TSR uses interrupt 79h\r
-as a pointer to this data table, making it accessible to other\r
-programs, including the QBasic application.\r
-\r
-While QBasic originally is not able to read mouse, it is able to read\r
-(and write) arbitrary location in system RAM. The QBasic demonstration\r
-program begins by retrieving the address of the TSR mouse data table\r
-from the interrupt vector table using interrupt 79h. By checking a\r
-predefined magic number (1983) in the data table, the program confirms\r
-that the mouse driver is loaded. Once verified, the QBasic program\r
-continuously reads mouse data from this shared memory location, while\r
-TSR keeps updating it with latest mouse state simultaneously.\r
+The workaround is a Terminate and Stay Resident (TSR) program written\r
+in x86 assembly. It must be started before running any QBasic program\r
+that depends on the mouse. The TSR hooks into the system timer\r
+interrupt (IRQ 0), which fires about 18.2 times per second, so it can\r
+poll the mouse at that rate.\r
+\r
+On every timer tick, the TSR reads the mouse motion counters and\r
+button states through the mouse driver interrupt (INT 33h) and\r
+accumulates them into a small data table in its own memory. It then\r
+stores the address of this table in the interrupt vector table slot\r
+of interrupt 79h, making the table discoverable by other programs.\r
+Interrupt 79h is never actually invoked — its vector slot is simply\r
+reused as a well-known storage location for the pointer. This assumes\r
+no other software occupies interrupt 79h.\r
+\r
+While QBasic cannot access the mouse directly, it can read (and\r
+write) arbitrary RAM locations using DEF SEG, PEEK and POKE. The\r
+QBasic program retrieves the data table address from the interrupt\r
+79h vector slot and verifies a predefined magic number (1983) at the\r
+start of the table to confirm the driver is loaded. Once verified,\r
+the QBasic program continuously reads mouse data from this shared\r
+memory while the TSR keeps it updated in the background.\r
\r
* Terminate and Stay Resident module\r
:PROPERTIES:\r
:CUSTOM_ID: terminate-and-stay-resident-module\r
:END:\r
\r
-A DOS TSR program that hooks into the system's interrupt mechanism to\r
-regularly read mouse input and store it in a dedicated memory\r
-location.\r
+A DOS TSR program that hooks the timer interrupt to regularly read\r
+mouse input and store it in a dedicated memory table.\r
\r
Files:\r
-- [[file:qbext.asm][qbext.asm - x86 Assembly source code]]\r
+- [[file:qbext.asm][qbext.asm - x86 assembly source code (FASM)]]\r
- [[file:qbext.com][qbext.com - binary COM executable for DOS]]\r
+- [[file:build.sh][build.sh - build script that assembles qbext.asm into qbext.com]]\r
\r
+** Data table\r
\r
-Here is the detailed technical specification for an in-memory table\r
-used to exchange mouse coordinates between a TSR program and a QBasic\r
-program.\r
+In-memory table used to exchange mouse state between the TSR and a\r
+QBasic program:\r
\r
-| Offset | Size (bytes) | Description |\r
-|--------+--------------+-------------------------|\r
-| 0x00 | 2 | Magic Number (1983) |\r
-| 0x02 | 2 | Horizontal Movement (X) |\r
-| 0x04 | 2 | Vertical Movement (Y) |\r
-| 0x06 | 2 | Button Status |\r
-| 0x08 | 1 | Update counter |\r
+| Offset | Size (bytes) | Description |\r
+|--------+--------------+--------------------------------------|\r
+| 0x00 | 2 | Magic Number (1983) |\r
+| 0x02 | 2 | Horizontal Movement (X), accumulated |\r
+| 0x04 | 2 | Vertical Movement (Y), accumulated |\r
+| 0x06 | 2 | Button Status, latched |\r
\r
-- Update counter :: Signals to the QBasic program that new mouse data\r
- is available in the shared memory table. It ensures that the QBasic\r
- program only reads fresh data and avoids processing outdated or\r
- repeated data. When the TSR updates the mouse data, it increments\r
- this flag by 1 to signal the QBasic program that new data is\r
- available. QBasic can compare this number against last retrieved\r
- value. If value has been increased, then there had been update\r
- meanwhile.\r
+** Transfer protocol\r
+\r
+The TSR /accumulates/: on every timer tick it adds the latest motion\r
+deltas to the X and Y fields and ORs the current button state into the\r
+Button Status field.\r
+\r
+The consumer therefore uses a read-and-reset cycle: it reads a field\r
+and then writes 0 back to it (see the =getword= / =putword= routines\r
+in mousedrv.bas). A field that reads as 0 simply means "no new\r
+activity since the last read".\r
+\r
+Button Status bits (as reported by INT 33h function 03h):\r
+- bit 0 :: left button\r
+- bit 1 :: right button\r
+- bit 2 :: middle button\r
+\r
+Bits stay set (latched) until the consumer clears the field.\r
+\r
+* Building\r
+:PROPERTIES:\r
+:CUSTOM_ID: building\r
+:END:\r
+\r
+The TSR is written for the flat assembler (FASM). To produce\r
+=qbext.com=, run:\r
+\r
+#+begin_src sh\r
+./build.sh\r
+#+end_src\r
+\r
+* Usage\r
+:PROPERTIES:\r
+:CUSTOM_ID: usage\r
+:END:\r
+\r
+1. Boot DOS (or start DOSBox).\r
+2. Load the TSR by running =QBEXT.COM=. A startup banner is printed\r
+ and the program stays resident in memory.\r
+3. Start QBasic and run a program that uses the driver, for example\r
+ =mousedrv.bas=.\r
+\r
+If the TSR is not loaded, the demo detects the missing magic number\r
+and exits with:\r
+\r
+: FATAL ERROR: you must load\r
+: QBasic extension TSR first!\r
\r
* QBasic demonstration program\r
:PROPERTIES:\r
:CUSTOM_ID: qbasic-demonstration-program\r
:END:\r
\r
-A QBasic program that reads mouse data from the memory location\r
+A QBasic program that reads mouse data from the memory table\r
populated by the TSR and demonstrates mouse movement and button\r
-clicks.\r
-\r
+clicks. It switches to 320x200 256-color graphics mode (SCREEN 13)\r
+and draws a circle that follows the mouse. Current coordinates and a\r
+frame counter are displayed at the top of the screen; button presses\r
+are printed as they occur. Press any key to exit.\r
\r
#+attr_html: :class responsive-img\r
#+attr_latex: :width 1000px\r
; Changelog:\r
; 2004.01, Initial version\r
; 2025, Improved program readability\r
+; 2026, Inlined startup message into source, removed unused update counter\r
\r
\r
\r
mov ax, 3 ; AH = 3, function to read mouse buttons\r
int 33h ; Call mouse interrupt\r
or [CS:mouseButtons], bx ; Update mouse button states\r
- inc byte [CS:updated] ; Increment update flag. So that QBasic registers mouse update event.\r
sti ; Re-enable interrupts\r
\r
; Restore interrupted program state and return control to it\r
mouseHorisontal dw 0 ; Horizontal mouse movement counter\r
mouseVertical dw 0 ; Vertical mouse movement counter\r
mouseButtons dw 0 ; Mouse button states\r
- updated db 0 ; Flag indicating data was updated\r
\r
endPointer: ; End of resident code pointer\r
\r
msg: ; Message to display at start\r
-file 'readme.txt' ; Include content of readme.txt\r
+ db 'Mouse adapter/driver for QBasic programs.', 0Dh, 0Ah\r
+ db 'Made by Svjatoslav Agejenko: http://svjatoslav.eu', 0Dh, 0Ah\r
db '$' ; End of string marker\r
+++ /dev/null
-' Text mode windowing system. Each window can display text file.\r
-' Window content can be scrolled horizontally and vertically.\r
-' Window can have arbitrary size and location on the screen.\r
-'\r
-' This program is free software: released under Creative Commons Zero (CC0) license\r
-' by Svjatoslav Agejenko.\r
-' Email: svjatoslav@svjatoslav.eu\r
-' Homepage: http://www.svjatoslav.eu\r
-\r
-' Changelog:\r
-' 2003, Initial version\r
-' 2024-2025, Improved program readability\r
-\r
-DECLARE SUB demo ()\r
-DECLARE FUNCTION GetLineFromWindow$ (windowNum%, lineNum%)\r
-DECLARE SUB LoadFileIntoWindow (fileName$, windowNum%)\r
-DECLARE SUB SendLineToWindow (windowNum%, lineNum%, newString$)\r
-DECLARE FUNCTION GetFreeLineIndex% ()\r
-DECLARE SUB RefreshAllWindows ()\r
-DECLARE FUNCTION AddWindow% (x%, y%, widt%, haigh%, title$)\r
-DECLARE SUB DrawBox (x%, y%, widt%, haigh%, edgeStyle$)\r
-DEFINT A-Z\r
-DECLARE SUB ShowWindow (windowNum)\r
-DECLARE SUB InitializeSystem ()\r
-\r
-' Global variables for text storage\r
-DIM SHARED maxStorage%\r
-maxStorage% = 5000\r
-DIM SHARED textStorage$(1 TO maxStorage%)\r
-DIM SHARED storagePointer%\r
-\r
-' Window-related global arrays\r
-DIM SHARED windowData(1 TO 10, 1 TO 1000) ' Stores line indices for each window\r
-DIM SHARED windowX(1 TO 10), windowY(1 TO 10) ' Position of each window\r
-DIM SHARED windowWidth(1 TO 10), windowHeight(1 TO 10) ' Size of each window\r
-DIM SHARED windowActiveStatus(1 TO 10) ' Whether window is active\r
-DIM SHARED windowTitle$(1 TO 10) ' Title of each window\r
-\r
-' Window scrolling/shifting\r
-DIM SHARED windowShiftX(1 TO 10) ' Horizontal shift\r
-DIM SHARED windowShiftY(1 TO 10) ' Vertical shift\r
-\r
-DIM SHARED currentActiveWindow% ' Currently active window for display\r
-\r
-InitializeSystem\r
-\r
-demo\r
-\r
-FUNCTION AddWindow% (x%, y%, widt%, heigh%, title$)\r
- ' Adds a new window to the system\r
-\r
- ' Find an empty window slot\r
- FOR windowNum% = 1 TO 10\r
- IF windowActiveStatus(windowNum%) = 0 THEN\r
- foundWindow% = windowNum%\r
- GOTO FoundEmptySlot\r
- END IF\r
- NEXT windowNum%\r
-\r
-FoundEmptySlot:\r
- ' Initialize the window properties\r
- windowActiveStatus(foundWindow%) = 1\r
- windowX(foundWindow%) = x%\r
- windowY(foundWindow%) = y%\r
- windowWidth(foundWindow%) = widt%\r
- windowHeight(foundWindow%) = heigh%\r
- windowTitle$(foundWindow%) = title$\r
-\r
- ' Return the window number\r
- AddWindow% = foundWindow%\r
-END FUNCTION\r
-\r
-SUB ClearWindowContent (windowNum%)\r
- ' Clears all content from a window\r
-\r
- FOR lineNum% = 1 TO 1000\r
- IF windowData(windowNum%, lineNum%) > 0 THEN\r
- textStorage$(windowData(windowNum%, lineNum%)) = ""\r
- windowData(windowNum%, lineNum%) = 0\r
- END IF\r
- NEXT lineNum%\r
-END SUB\r
-\r
-SUB demo\r
- ' Create three windows with different sizes and titles\r
- window1% = AddWindow%(1, 1, 30, 10, "window 1.")\r
- window2% = AddWindow%(1, 12, 80, 30, "second window")\r
- window3% = AddWindow%(31, 2, 30, 10, "last window")\r
-\r
- ' Load the same file into all windows\r
- LoadFileIntoWindow "wsystem.bas", window2%\r
- LoadFileIntoWindow "wsystem.bas", window1%\r
- LoadFileIntoWindow "wsystem.bas", window3%\r
-\r
-AnimateWindows:\r
- ' Randomly select an active window to animate\r
- currentActiveWindow% = INT(RND * 3) + 1\r
- RefreshAllWindows\r
-\r
- ' Animate the windows by shifting their content\r
- FOR animationFrame% = 1 TO 100\r
- windowShiftX(currentActiveWindow%) = SIN(animationFrame% / 10) * 10 + 10\r
- windowShiftY(currentActiveWindow%) = animationFrame%\r
- ShowWindow currentActiveWindow%\r
- ' split-second delay\r
- SOUND 0, 1\r
- IF INKEY$ <> "" THEN SYSTEM\r
- NEXT animationFrame%\r
-\r
- GOTO AnimateWindows\r
-END SUB\r
-\r
-FUNCTION GetFreeLineIndex%\r
- ' Finds a free line in text storage\r
-\r
-FindNextLine:\r
- IF storagePointer% > 1000 THEN\r
- storagePointer% = 1\r
- END IF\r
-\r
- IF textStorage$(storagePointer%) = "" THEN\r
- GetFreeLineIndex% = storagePointer%\r
- storagePointer% = storagePointer% + 1\r
- ELSE\r
- storagePointer% = storagePointer% + 1\r
- GOTO FindNextLine\r
- END IF\r
-END FUNCTION\r
-\r
-FUNCTION GetLineFromWindow$ (windowNum%, lineNum%)\r
- ' Retrieve a line from a window's memory\r
-\r
- IF windowData(windowNum%, lineNum%) = 0 THEN\r
- GetLineFromWindow$ = ""\r
- ELSE\r
- GetLineFromWindow$ = textStorage$(windowData(windowNum%, lineNum%))\r
- END IF\r
-END FUNCTION\r
-\r
-SUB LoadFileIntoWindow (fileName$, windowNum%)\r
- ' Load a file into a window's memory\r
-\r
- OPEN fileName$ FOR INPUT AS #1\r
- FOR lineNum% = 1 TO 1000\r
- IF EOF(1) <> 0 THEN\r
- GOTO FileLoaded\r
- END IF\r
- LINE INPUT #1, lineContent$\r
- SendLineToWindow windowNum%, lineNum%, lineContent$\r
- NEXT lineNum%\r
-\r
-FileLoaded:\r
- CLOSE #1\r
-\r
- ' Fill the remaining lines with empty strings\r
- FOR blankLineNum% = lineNum% TO 1000\r
- SendLineToWindow windowNum%, blankLineNum%, ""\r
- NEXT blankLineNum%\r
-END SUB\r
-\r
-SUB RefreshAllWindows\r
- ' Redraw all active windows\r
-\r
- CLS\r
- FOR windowNum% = 1 TO 10\r
- IF windowActiveStatus(windowNum%) > 0 THEN\r
- ShowWindow windowNum%\r
- END IF\r
- NEXT windowNum%\r
-END SUB\r
-\r
-SUB SendLineToWindow (windowNum%, lineNum%, newString$)\r
- ' Stores a string in a window's memory\r
-\r
- lineContent$ = newString$\r
-\r
- ' Remove trailing spaces from the string\r
- IF lineContent$ = SPACE$(LEN(lineContent$)) THEN\r
- lineContent$ = ""\r
- END IF\r
-\r
- IF LEN(lineContent$) > 0 THEN\r
-TrimRight:\r
- IF RIGHT$(lineContent$, 1) = " " THEN\r
- lineContent$ = LEFT$(lineContent$, LEN(lineContent$) - 1)\r
- GOTO TrimRight\r
- END IF\r
- END IF\r
-\r
- ' Update the window memory with the new string\r
- IF lineContent$ = "" THEN\r
- IF windowData(windowNum%, lineNum%) > 0 THEN\r
- textStorage$(windowData(windowNum%, lineNum%)) = "": windowData(windowNum%, lineNum%) = 0\r
- END IF\r
- ELSE\r
- IF windowData(windowNum%, lineNum%) = 0 THEN\r
- windowData(windowNum%, lineNum%) = GetFreeLineIndex%\r
- END IF\r
- textStorage$(windowData(windowNum%, lineNum%)) = lineContent$\r
- END IF\r
-END SUB\r
-\r
-SUB ShowWindow (windowNum%)\r
- ' Draws a window on the screen\r
-\r
- ' Determine background color based on active window\r
- IF windowNum% = currentActiveWindow% THEN\r
- bgColor% = 1\r
- ELSE\r
- bgColor% = 0\r
- END IF\r
-\r
- x% = windowX(windowNum%)\r
- y% = windowY(windowNum%)\r
- widt% = windowWidth(windowNum%)\r
- heigh% = windowHeight(windowNum%)\r
- title$ = windowTitle$(windowNum%)\r
-\r
- COLOR 11, bgColor%\r
-\r
- ' Create border components\r
- FOR borderSegment% = 1 TO widt% - 2\r
- topBottom$ = topBottom$ + CHR$(205)\r
- NEXT borderSegment%\r
- borderTop$ = CHR$(201) + topBottom$ + CHR$(187)\r
- borderBottom$ = CHR$(200) + topBottom$ + CHR$(188)\r
-\r
- ' Draw top border\r
- LOCATE y%, x%\r
- PRINT borderTop$\r
-\r
- ' Draw bottom border\r
- LOCATE y% + heigh% - 1, x%\r
- PRINT borderBottom$\r
-\r
- ' Draw window content\r
- FOR lineNum% = 1 TO heigh% - 2\r
- LOCATE y% + lineNum%, x%\r
- lineContent$ = GetLineFromWindow$(windowNum%, lineNum% + windowShiftY(windowNum%))\r
- lineContent$ = lineContent$ + SPACE$(300)\r
- lineContent$ = RIGHT$(lineContent$, LEN(lineContent$) - windowShiftX(windowNum%))\r
- lineContent$ = LEFT$(lineContent$, widt% - 2)\r
- PRINT CHR$(186) + lineContent$ + CHR$(186)\r
- NEXT lineNum%\r
-\r
- ' Draw window title\r
- titleX% = INT(x% + (widt% / 2) - (LEN(title$) / 2) - 2)\r
- LOCATE y%, titleX%\r
- PRINT "[ "\r
- titleX% = titleX% + 2\r
-\r
- COLOR 10\r
- LOCATE y%, titleX%\r
- PRINT title$\r
-\r
- titleX% = titleX% + LEN(title$)\r
- COLOR 11\r
- LOCATE y%, titleX%\r
- PRINT " ]"\r
- COLOR 7, 0\r
-END SUB\r
-\r
-SUB InitializeSystem\r
- ' Initialize the screen and shared memory\r
-\r
- WIDTH 80, 50\r
- VIEW PRINT 1 TO 50\r
-\r
- FOR storageIndex% = 1 TO maxStorage%\r
- textStorage$(storageIndex%) = ""\r
- NEXT storageIndex%\r
-\r
- storagePointer% = 1\r
-END SUB
\ No newline at end of file
--- /dev/null
+' Text mode windowing system. Each window can display text file.\r
+' Window content can be scrolled horizontally and vertically.\r
+' Window can have arbitrary size and location on the screen.\r
+'\r
+' This program is free software: released under Creative Commons Zero (CC0) license\r
+' by Svjatoslav Agejenko.\r
+' Email: svjatoslav@svjatoslav.eu\r
+' Homepage: http://www.svjatoslav.eu\r
+\r
+' Changelog:\r
+' 2003, Initial version\r
+' 2024-2025, Improved program readability\r
+\r
+DECLARE SUB demo ()\r
+DECLARE FUNCTION GetLineFromWindow$ (windowNum%, lineNum%)\r
+DECLARE SUB LoadFileIntoWindow (fileName$, windowNum%)\r
+DECLARE SUB SendLineToWindow (windowNum%, lineNum%, newString$)\r
+DECLARE FUNCTION GetFreeLineIndex% ()\r
+DECLARE SUB RefreshAllWindows ()\r
+DECLARE FUNCTION AddWindow% (x%, y%, widt%, haigh%, title$)\r
+DECLARE SUB DrawBox (x%, y%, widt%, haigh%, edgeStyle$)\r
+DEFINT A-Z\r
+DECLARE SUB ShowWindow (windowNum)\r
+DECLARE SUB InitializeSystem ()\r
+\r
+' Global variables for text storage\r
+DIM SHARED maxStorage%\r
+maxStorage% = 5000\r
+DIM SHARED textStorage$(1 TO maxStorage%)\r
+DIM SHARED storagePointer%\r
+\r
+' Window-related global arrays\r
+DIM SHARED windowData(1 TO 10, 1 TO 1000) ' Stores line indices for each window\r
+DIM SHARED windowX(1 TO 10), windowY(1 TO 10) ' Position of each window\r
+DIM SHARED windowWidth(1 TO 10), windowHeight(1 TO 10) ' Size of each window\r
+DIM SHARED windowActiveStatus(1 TO 10) ' Whether window is active\r
+DIM SHARED windowTitle$(1 TO 10) ' Title of each window\r
+\r
+' Window scrolling/shifting\r
+DIM SHARED windowShiftX(1 TO 10) ' Horizontal shift\r
+DIM SHARED windowShiftY(1 TO 10) ' Vertical shift\r
+\r
+DIM SHARED currentActiveWindow% ' Currently active window for display\r
+\r
+InitializeSystem\r
+\r
+demo\r
+\r
+FUNCTION AddWindow% (x%, y%, widt%, heigh%, title$)\r
+ ' Adds a new window to the system\r
+\r
+ ' Find an empty window slot\r
+ FOR windowNum% = 1 TO 10\r
+ IF windowActiveStatus(windowNum%) = 0 THEN\r
+ foundWindow% = windowNum%\r
+ GOTO FoundEmptySlot\r
+ END IF\r
+ NEXT windowNum%\r
+\r
+FoundEmptySlot:\r
+ ' Initialize the window properties\r
+ windowActiveStatus(foundWindow%) = 1\r
+ windowX(foundWindow%) = x%\r
+ windowY(foundWindow%) = y%\r
+ windowWidth(foundWindow%) = widt%\r
+ windowHeight(foundWindow%) = heigh%\r
+ windowTitle$(foundWindow%) = title$\r
+\r
+ ' Return the window number\r
+ AddWindow% = foundWindow%\r
+END FUNCTION\r
+\r
+SUB ClearWindowContent (windowNum%)\r
+ ' Clears all content from a window\r
+\r
+ FOR lineNum% = 1 TO 1000\r
+ IF windowData(windowNum%, lineNum%) > 0 THEN\r
+ textStorage$(windowData(windowNum%, lineNum%)) = ""\r
+ windowData(windowNum%, lineNum%) = 0\r
+ END IF\r
+ NEXT lineNum%\r
+END SUB\r
+\r
+SUB demo\r
+ ' Create three windows with different sizes and titles\r
+ window1% = AddWindow%(1, 1, 30, 10, "window 1.")\r
+ window2% = AddWindow%(1, 12, 80, 30, "second window")\r
+ window3% = AddWindow%(31, 2, 30, 10, "last window")\r
+\r
+ ' Load the same file into all windows\r
+ LoadFileIntoWindow "wsystem.bas", window2%\r
+ LoadFileIntoWindow "wsystem.bas", window1%\r
+ LoadFileIntoWindow "wsystem.bas", window3%\r
+\r
+AnimateWindows:\r
+ ' Randomly select an active window to animate\r
+ currentActiveWindow% = INT(RND * 3) + 1\r
+ RefreshAllWindows\r
+\r
+ ' Animate the windows by shifting their content\r
+ FOR animationFrame% = 1 TO 100\r
+ windowShiftX(currentActiveWindow%) = SIN(animationFrame% / 10) * 10 + 10\r
+ windowShiftY(currentActiveWindow%) = animationFrame%\r
+ ShowWindow currentActiveWindow%\r
+ ' split-second delay\r
+ SOUND 0, 1\r
+ IF INKEY$ <> "" THEN SYSTEM\r
+ NEXT animationFrame%\r
+\r
+ GOTO AnimateWindows\r
+END SUB\r
+\r
+FUNCTION GetFreeLineIndex%\r
+ ' Finds a free line in text storage\r
+\r
+FindNextLine:\r
+ IF storagePointer% > 1000 THEN\r
+ storagePointer% = 1\r
+ END IF\r
+\r
+ IF textStorage$(storagePointer%) = "" THEN\r
+ GetFreeLineIndex% = storagePointer%\r
+ storagePointer% = storagePointer% + 1\r
+ ELSE\r
+ storagePointer% = storagePointer% + 1\r
+ GOTO FindNextLine\r
+ END IF\r
+END FUNCTION\r
+\r
+FUNCTION GetLineFromWindow$ (windowNum%, lineNum%)\r
+ ' Retrieve a line from a window's memory\r
+\r
+ IF windowData(windowNum%, lineNum%) = 0 THEN\r
+ GetLineFromWindow$ = ""\r
+ ELSE\r
+ GetLineFromWindow$ = textStorage$(windowData(windowNum%, lineNum%))\r
+ END IF\r
+END FUNCTION\r
+\r
+SUB LoadFileIntoWindow (fileName$, windowNum%)\r
+ ' Load a file into a window's memory\r
+\r
+ OPEN fileName$ FOR INPUT AS #1\r
+ FOR lineNum% = 1 TO 1000\r
+ IF EOF(1) <> 0 THEN\r
+ GOTO FileLoaded\r
+ END IF\r
+ LINE INPUT #1, lineContent$\r
+ SendLineToWindow windowNum%, lineNum%, lineContent$\r
+ NEXT lineNum%\r
+\r
+FileLoaded:\r
+ CLOSE #1\r
+\r
+ ' Fill the remaining lines with empty strings\r
+ FOR blankLineNum% = lineNum% TO 1000\r
+ SendLineToWindow windowNum%, blankLineNum%, ""\r
+ NEXT blankLineNum%\r
+END SUB\r
+\r
+SUB RefreshAllWindows\r
+ ' Redraw all active windows\r
+\r
+ CLS\r
+ FOR windowNum% = 1 TO 10\r
+ IF windowActiveStatus(windowNum%) > 0 THEN\r
+ ShowWindow windowNum%\r
+ END IF\r
+ NEXT windowNum%\r
+END SUB\r
+\r
+SUB SendLineToWindow (windowNum%, lineNum%, newString$)\r
+ ' Stores a string in a window's memory\r
+\r
+ lineContent$ = newString$\r
+\r
+ ' Remove trailing spaces from the string\r
+ IF lineContent$ = SPACE$(LEN(lineContent$)) THEN\r
+ lineContent$ = ""\r
+ END IF\r
+\r
+ IF LEN(lineContent$) > 0 THEN\r
+TrimRight:\r
+ IF RIGHT$(lineContent$, 1) = " " THEN\r
+ lineContent$ = LEFT$(lineContent$, LEN(lineContent$) - 1)\r
+ GOTO TrimRight\r
+ END IF\r
+ END IF\r
+\r
+ ' Update the window memory with the new string\r
+ IF lineContent$ = "" THEN\r
+ IF windowData(windowNum%, lineNum%) > 0 THEN\r
+ textStorage$(windowData(windowNum%, lineNum%)) = "": windowData(windowNum%, lineNum%) = 0\r
+ END IF\r
+ ELSE\r
+ IF windowData(windowNum%, lineNum%) = 0 THEN\r
+ windowData(windowNum%, lineNum%) = GetFreeLineIndex%\r
+ END IF\r
+ textStorage$(windowData(windowNum%, lineNum%)) = lineContent$\r
+ END IF\r
+END SUB\r
+\r
+SUB ShowWindow (windowNum%)\r
+ ' Draws a window on the screen\r
+\r
+ ' Determine background color based on active window\r
+ IF windowNum% = currentActiveWindow% THEN\r
+ bgColor% = 1\r
+ ELSE\r
+ bgColor% = 0\r
+ END IF\r
+\r
+ x% = windowX(windowNum%)\r
+ y% = windowY(windowNum%)\r
+ widt% = windowWidth(windowNum%)\r
+ heigh% = windowHeight(windowNum%)\r
+ title$ = windowTitle$(windowNum%)\r
+\r
+ COLOR 11, bgColor%\r
+\r
+ ' Create border components\r
+ FOR borderSegment% = 1 TO widt% - 2\r
+ topBottom$ = topBottom$ + CHR$(205)\r
+ NEXT borderSegment%\r
+ borderTop$ = CHR$(201) + topBottom$ + CHR$(187)\r
+ borderBottom$ = CHR$(200) + topBottom$ + CHR$(188)\r
+\r
+ ' Draw top border\r
+ LOCATE y%, x%\r
+ PRINT borderTop$\r
+\r
+ ' Draw bottom border\r
+ LOCATE y% + heigh% - 1, x%\r
+ PRINT borderBottom$\r
+\r
+ ' Draw window content\r
+ FOR lineNum% = 1 TO heigh% - 2\r
+ LOCATE y% + lineNum%, x%\r
+ lineContent$ = GetLineFromWindow$(windowNum%, lineNum% + windowShiftY(windowNum%))\r
+ lineContent$ = lineContent$ + SPACE$(300)\r
+ lineContent$ = RIGHT$(lineContent$, LEN(lineContent$) - windowShiftX(windowNum%))\r
+ lineContent$ = LEFT$(lineContent$, widt% - 2)\r
+ PRINT CHR$(186) + lineContent$ + CHR$(186)\r
+ NEXT lineNum%\r
+\r
+ ' Draw window title\r
+ titleX% = INT(x% + (widt% / 2) - (LEN(title$) / 2) - 2)\r
+ LOCATE y%, titleX%\r
+ PRINT "[ "\r
+ titleX% = titleX% + 2\r
+\r
+ COLOR 10\r
+ LOCATE y%, titleX%\r
+ PRINT title$\r
+\r
+ titleX% = titleX% + LEN(title$)\r
+ COLOR 11\r
+ LOCATE y%, titleX%\r
+ PRINT " ]"\r
+ COLOR 7, 0\r
+END SUB\r
+\r
+SUB InitializeSystem\r
+ ' Initialize the screen and shared memory\r
+\r
+ WIDTH 80, 50\r
+ VIEW PRINT 1 TO 50\r
+\r
+ FOR storageIndex% = 1 TO maxStorage%\r
+ textStorage$(storageIndex%) = ""\r
+ NEXT storageIndex%\r
+\r
+ storagePointer% = 1\r
+END SUB
\ No newline at end of file