'===========================================================================
' Subject: ANSI EMULATOR                      Date: 07/24/92 (00:00)       
'  Author: Rich Geldreich                     Code: QB, PDS                
'  Origin: ANSI,EMULATOR                    Packet: ANSI.ABC
'===========================================================================
'PANSI.BAS v1.50
'ANSI emulator for QuickBASIC 4.5 and PDS
'By Richard Geldreich July 24, 1992
'Don't forget that "CALL INTERRUPT" is used- load QB with "QB/l"

'I have fixed up & improved the ANSI escape sequence state machine. It
'now works faster. I still don't know why I'm releasing this driver,
'because I'm going to release my all-assembly version very soon...
'(the assembly version of this driver is light years ahead of this program!)
'See the PrintANSI procedure for a list of bug fixes.

'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'! Don't forget to modify the "SendStatus" procedure for your !
'!                       comm package!                        !
'!    You also should modify PrintString for QB4.5 or PDS     !
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

'I welcome any suggestions or ideas about this program... It _should_
'emulate DOS's ANSI.SYS device driver... This program is in the public
'domain; do what you want with it! Have a ball!! Just try and give
'me some credit. Thanks. I have tested this driver out with many BBS's
'and door programs and it works fine. Please test this driver out before
'you release it in a program!!!

'NOTE: This program assumes that the current segment is always
'pointing twards the video buffer!! If you change the current
'segment don't forget to change it back or sparks will fly when you
'write to the screen! (see GetVSeg)

'Info:
'ClearScreen- used internally by the PrintAnsi procedure- you may
'use it to clear the current window(the current background color
'is used in the clear). The cursor is set to the upper left hand corner
'of the window after the window is cleared.

'CursorControl A- if A is non-zero then the SetCursor routine(which
'is called by PrintAnsi) will update the cursor whenever it is moved.
'If it is zero then SetCursor won't touch the cursor's position.

'GetVSeg- Returns the current video segment.

'Init- Initializes the driver. This should be called before any other
'procedure. Completly resets the entire driver, sets the window to the
'current screen page & size and moves the cursor to the upper left hand
'corner of the screen.

'Music A- if A is not zero, then ANSI music is enabled.

'PrintAnsi Char- where Char is an ASCII code from 0-255. Recognizes
'ANSI escape sequences(of course!). Processes the character and
'updates the display, if needed

'PrintString A$- prints a string to the display. Calls PrintAnsi for
'each character. Don't forget to modify this for PDS/QuickBASIC.

'ScrollUpScreen- scrolls up the current window. Uses a BIOS call.
'Normally used internally by PrintAnsi.

'SendStatus- sends a CPR sequence to the receiver.
'In other words, SendStatus will output the current X and Y coordinates
'of the cursor to the remote terminal. Used by some BBS's and doors
'to see if the user's terminal has ANSI capibilities. You must modify
'this procedure to output the status string to your comm package!
'(this is used internally by PrintAnsi)

'SetCursor- moves the cursor to its correct position(it doesn't turn
'it on however- use the LOCATE , , 1 command to do that). This procedure
'should work on all adapters, but I haven't tested it out on many
'cards yet... Use this to restore the cursor to where it should be
'after you move it. If you want, change this procedure to use QB's
'LOCATE command instead of the OUT's.

'SetWindow WorkPage, Lx,Ly,Hx,Hy- defines a window where all text
'is printed. if WorkPage is -1, then the BIOS data area is examined for
'the current screen page, otherwise WorkPage must indicate which page to
'write to. If Lx is -1, the the window will take up the entire screen
'otherwise Lx and Ly are the upper-left lines of the window(where
'1,1 is the upper corner of the screen) and Hx and Hy are the lower-right
'coordinates of the window.
'   The current cursor position is moved to the upper left corner of the
'new window. If the coordinates passed are invalid, the window is not
'modified.

'   That's all! You can add more functions if you need them; I've
'documented the PrintAnsi procedure enough for you to get
'a good idea of how it works.

DEFINT A-Z

DECLARE SUB ClearScreen ()
DECLARE SUB CursorControl (A%)
DECLARE FUNCTION GetVSeg% ()
DECLARE SUB Init ()
DECLARE SUB Music (A%)
DECLARE SUB PrintANSI (Char%)
DECLARE SUB PrintString (B$)
DECLARE SUB ScrollUpScreen ()
DECLARE SUB SendStatus (X%, Y%)
DECLARE SUB SetCursor ()
DECLARE SUB SetWindow (WorkPage%, Lx%, Ly%, Hx%, Hy%)

DECLARE SUB playme (A$)

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

DIM SHARED Xpos, Ypos               'cursor's position
DIM SHARED MinX, MinY, MaxX, MaxY   'current window
DIM SHARED SaveX, SaveY             'used by SCR and RCP
DIM SHARED Colors(7), Attribute
DIM SHARED CursorOn, VideoSegment, VideoOffset, CursorAddress, BytesPerLine
DIM SHARED Monochrome, CRT          'monochrome adapter flag
DIM SHARED ANSIMusic, MusicLevel
DIM SHARED Level

CONST True = -1, False = NOT True   'usefull stuff

'The color translation table is used to translate an ANSI color
'to a screen color.
ColorTable:
    DATA 0,4,2,6,1,5,3,7

'******START OF TEST PROGRAM
'The following code is not needed... It's only for testing!

SCREEN 0
WIDTH 80, 25
CLS
LOCATE , , 1            'turn cursor on

Init
ClearScreen             'clear the window
SetWindow -1, 1, 1, 80, 25 'set window at (1,2)-(80,25)

'DO
'    A$ = INKEY$: IF A$ <> "" THEN PrintString A$
'LOOP

'test ANSI music
PrintString CHR$(27) + "[MFO1CDEFGABC" + CHR$(14)

'A! = TIMER
'PrintString STRING$(5000, 65)
'B! = TIMER
'PRINT 5000 / (B! - A!)
'END

'a lame test
Esc$ = CHR$(27)
Up$ = CHR$(27) + "[A"
Down$ = CHR$(27) + "[B"
Lft$ = CHR$(27) + "[D"
Rgt$ = CHR$(27) + "[C"
Foreground = 31: Background = 40: Bold = 0
X = 1: Y = 1
DO
    A$ = CHR$(27) + "["
    IF NOT Bold THEN A$ = A$ + "0;" ELSE A$ = A$ + "1;"
    PrintString A$ + MID$(STR$(Foreground), 2) + ";" + MID$(STR$(Background), 2) + "m"
    
    Bold = NOT Bold
    Foreground = Foreground + 1
    IF Foreground > 37 THEN
        Foreground = 31
        Background = Background + 1
        IF Background > 47 THEN Background = 40
    END IF
    PrintString CHR$(27) + "[s" + CHR$(219) + CHR$(27) + "[u"
    IF Xdirect THEN
        X = X - 1
        PrintString Lft$
        IF X = 1 THEN Xdirect = 0
    ELSE
        X = X + 1
        PrintString Rgt$
        IF X = 80 THEN Xdirect = 1
    END IF
    IF Ydirect THEN
        Y = Y - 1
        PrintString Up$
        IF Y = 1 THEN Ydirect = 0
    ELSE
        Y = Y + 1
        PrintString Down$
        IF Y = 24 THEN Ydirect = 1
    END IF

LOOP UNTIL INKEY$ <> ""

END
'******END OF TEST PROGRAM

'Clears the current window. The cursor is also set to the upper-left hand
'corner of the window.
SUB ClearScreen
    DIM Regs AS RegType
    Regs.Ax = &H600
    A& = Attribute * 256&
    IF A& > 32767 THEN A = A& - 65536 ELSE A = A&
    Regs.Bx = A
    Regs.Cx = (MinY * 256&) + MinX - 257
    Regs.Dx = (MaxY * 256&) + MaxX - 257
    CALL interrupt(&H10, Regs, Regs)

    Xpos = MinX: Ypos = MinY
    SetCursor


END SUB

'Enables or disables cursor updating.
SUB CursorControl (A)
    IF A THEN
        CursorOn = True
    ELSE
        CursorOn = False
    END IF
END SUB

'Returns the current video segment.
FUNCTION GetVSeg
    GetVSeg = VideoSegment
END FUNCTION

'Initializes everything.
SUB Init
    DIM Regs AS RegType

    'default color, white on black (or black on white??)
    Attribute = 7

    Level = 0: MusicLevel = 0   'reset levels
    ANSIMusic = True            'ANSI music enabled
    CursorOn = True             'cursor movement enabled

    'read in color translation table
    RESTORE ColorTable
    FOR A = 0 TO 7: READ Colors(A): NEXT
   
    Regs.Ax = 15 * 256
    CALL interrupt(&H10, Regs, Regs)
    'if AL=7 then card is monochrome.
    IF (Regs.Ax AND 255) = 7 THEN
        VideoSegment = &HB000
        Monochrome = True
    ELSE
        VideoSegment = &HB800
        Monochrome = False
    END IF
    DEF SEG = &H40
    CRT = PEEK(&H63) + PEEK(&H64) * 256&

    'Set segment to the screen.
    DEF SEG = VideoSegment

    'window defaults to screen's page & size
    'Xpos, Ypos, SaveX, SaveY, MinX, MinY, MaxX, MaxY, VideoOffset and the
    'cursor are set up within this procedure
    SetWindow -1, -1, 0, 0, 0

END SUB

'Enables/Disables ANSI music...
SUB Music (A)
    ANSIMusic = A
END SUB

'Prints an ASCII character on the screen; filters out
'ANSI escape sequences and parses them.
'Fixups from last version(howcome nobody told me about these errors?!):
' A chr$(27) would not be processed correctly if received from within
'   another escape sequence. This has been fixed.
' SetCursor now uses a BIOS variable to get the correct OUT address... It
'   should now work on monochrome and color monitors.
' The cursor set, up & down commands are now not ignored if the cursor is
'  set to a position that is invalid.
' The entire parameter table is set to 1 so special case tests do not
'  have to be performed. Parameters will now be interpeted as 1 if they
'  are zero in the cursor set commands(these two aren't bugs, just
'  improvements!)
' ESC[m now resets the attribute to 7. The new page command, CHR$(12), now
'  resets the screen to attribute 7 before clearing(not really a bug, but...)
' OOPS!! The cursor position command, ESC[H, was processed as an absolute
'  coordinate relative to the upper-left hand of the screen... It should of
'  been processed relative to the upper-left hand corner of the window! DUMB!
'  So if the window was set to (1,2)-(80,25), and an ESC[H was received, the
'  cursor would not move anywhere.... This of course has been fixed.
'
' I discovered almost all of these little bugs while coding the assembly
' version of the driver...
SUB PrintANSI (Char) STATIC
    DIM Parameters(10)
    
    SELECT CASE Level
    CASE 0
        'normal mode
        GOSUB ProcessChar
    CASE 1
        'Level=1 after a chr$(27) is received.
        'valid escape sequence?

        IF Char <> 91 THEN
            Level = 0
            GOSUB ProcessChar
        ELSE
            'a valid escape sequence has been received
            Level = 2
            CurrentParameter = 0
            NumParameters = 0
            ValidParameter = False
            FOR A = 0 TO 10: Parameters(A) = 1: NEXT
        END IF
    CASE 2

        'inside an escape sequence
        GOSUB ProcessCode
    END SELECT
EXIT SUB

ProcessChar:
    'processes a non-ANSI code
    SELECT CASE Char
    'process new page code
    CASE 12
        Attribute = 7
        ClearScreen
    'process escape character
    CASE 27
        Level = 1
    'process enter
    CASE 13
        Xpos = MinX
        SetCursor
    'process line feed
    CASE 10
        Ypos = Ypos + 1
        IF Ypos > MaxY THEN Ypos = MaxY: ScrollUpScreen
        SetCursor
    'process backspace(non-destructive)
    CASE 8
        IF Xpos > MinX THEN
            Xpos = Xpos - 1
            SetCursor
        END IF
    'process tab key(tab stops=8)
    CASE 9
        Xpos = ((Xpos \ 8) + 1) * 8
        IF Xpos > MaxX THEN Xpos = MaxX
        SetCursor
    'process bell
    CASE 7
        'don't substitute a "BEEP" statement here!
        'BEEP resets the cursor to where QB thinks it is!
        SOUND 3140, 1.25
    'any other character is sent to the screen
    CASE ELSE

        'prints a character to the screen
        POKE CursorAddress, Char: POKE CursorAddress + 1, Attribute
        CursorAddress = CursorAddress + 2
        Xpos = Xpos + 1

        IF Xpos > MaxX THEN

            Xpos = MinX
            Ypos = Ypos + 1
            IF Ypos > MaxY THEN
                Ypos = MaxY
                ScrollUpScreen
            END IF
            SetCursor
        ELSE
            IF CursorOn THEN
                Address = CursorAddress \ 2
                OUT CRT, &HE
                OUT CRT + 1, Address \ 256
                OUT CRT, &HF
                OUT CRT + 1, Address AND 255
            END IF
        END IF
    END SELECT
RETURN
'processes a character within an ansi escape sequence
'non-valid characters are sent to the screen
ProcessCode:
'handles ANSI music...
IF MusicLevel > 0 THEN
    SELECT CASE MusicLevel
    'see if the "F" in "ESC[MF" is received...
    CASE 1
        IF Char <> 70 THEN          '"F"
            MusicLevel = 0
            Level = 0
            GOSUB ProcessChar
        ELSE
            MusicLevel = 2
            MusicString$ = ""
        END IF
        'Either add a char to the music string or play it...
    CASE 2
        IF Char <> 14 THEN
            'fall out if an escape character is received...
            IF Char = 27 THEN
                MusicString$ = ""
                MusicLevel = 0
                Level = 0
                GOSUB ProcessChar
            'assume the character received to be part of the
            'PLAY string
            ELSE
                MusicString$ = MusicString$ + CHR$(Char)
            END IF
        ELSE
            IF ANSIMusic THEN
                'play the string- the PLAY command is in a seperate
                'module to keep error checking out of this module
                playme MusicString$
            END IF
            MusicString$ = ""
            MusicLevel = 0
            Level = 0
        END IF
    END SELECT
ELSE
    SELECT CASE Char
    CASE 77                             '"M"
        MusicLevel = 1
    CASE 48 TO 57                       '0-9
        'all parameters should be lower than 199...
        IF CurrentParameter < 199 THEN
            CurrentParameter = CurrentParameter * 10 + (Char - 48)
            ValidParameter = True
        ELSE
            Level = 0
            GOSUB ProcessChar
        END IF
    CASE 59
        GOSUB MakeParameter             '";"
    'CUP-set cursor's position
    CASE 72, 102                        'H or f
        GOSUB MakeParameter
        Ypos = MinY + A - 1
        A = Parameters(1): IF A = 0 THEN A = 1
        Xpos = MinX + A - 1
        IF Xpos > MaxX THEN Xpos = MaxX
        IF Ypos > MaxY THEN Ypos = MaxY
        SetCursor
        Level = 0
    'CUU- cursor up
    CASE 65                             'A
        GOSUB MakeParameter
        Ypos = Ypos - A
        IF Ypos < MinY THEN Ypos = MinY
        SetCursor
        Level = 0
    'CUD-cursor down
    CASE 66                             'B
        GOSUB MakeParameter
        Ypos = Ypos + A
        IF Ypos > MaxY THEN Ypos = MaxY
        SetCursor
        Level = 0
    'CUF-cursor forward
    CASE 67                             'C
        GOSUB MakeParameter
        Xpos = Xpos + A
        IF Xpos > MaxX THEN Xpos = MaxX
        SetCursor
        Level = 0
    'CUB-cursor backward
    CASE 68                              'D
        GOSUB MakeParameter
        Xpos = Xpos - A

        IF Xpos < MinX THEN Xpos = MinX
        SetCursor
        Level = 0
    'SCR-save cursor position
    CASE 115                            's
        SaveX = Xpos: SaveY = Ypos
        Level = 0
    'RCP-restore cursor position
    CASE 117                            'u
        Xpos = SaveX: Ypos = SaveY
        Level = 0
        SetCursor
    'ED-erase display(ESC[2J and ESC[J work
    'both work)
    CASE 74                             'J
        ClearScreen
        Level = 0
    'EL-erase in line
    CASE 75                             'K
        A = CursorAddress
        FOR X = Xpos TO MaxX
            POKE A, 32: POKE A + 1, Attribute: A = A + 2
        NEXT
        Level = 0
    'SGR-sets new color
    CASE 109                            'm
        GOSUB MakeParameter
        'if no color codes then stuff 0 into the table
        IF NumParameters = 0 THEN Parameters(0) = 0: NumParameters = 1
        FOR A = 0 TO NumParameters - 1
            P = Parameters(A)
            SELECT CASE P
            CASE IS <= 8
                SELECT CASE P
                'all attributes off
                CASE 0
                    Attribute = 7
                'high-intensity
                CASE 1
                    Attribute = Attribute OR 8
                'blinking
                CASE 5
                    Attribute = Attribute OR 128
                'inverse
                CASE 7
                    Attribute = (Attribute AND 136) OR (Attribute AND 7) * 16 OR (Attribute AND 112) \ 16
                END SELECT
                'set foreground
            CASE 30 TO 37
                IF NOT Monochrome THEN
                    Attribute = (Attribute AND 248) OR Colors(P - 30)
                END IF
                'set background
            CASE 40 TO 47
                IF NOT Monochrome THEN
                    Attribute = (Attribute AND 143)
                    Attribute = Attribute OR Colors(P - 40) * 16
                END IF
            END SELECT
        NEXT
        Level = 0
    'DSR-outputs a CPR sequence
    'This function outputs the string "ESC[#;#R" where
    '#;# is the current Y and current X coordinate
    'to the receiver.
    'Calls SendStatus to do its dirty work...
    CASE 110
        SendStatus Xpos, Ypos
        Level = 0
    'any other code is assumed to be invalid;it's just sent to the
    'screen
    CASE ELSE
        Level = 0
        GOSUB ProcessChar
    END SELECT
END IF
RETURN
'stores a numeric parameter into the parameter table
MakeParameter:
    'check to see if a least one digit has been received
    'for this parameter and there's room left in the table
    IF ValidParameter AND NumParameters < 10 THEN
        'add parameter to table
        Parameters(NumParameters) = CurrentParameter
        NumParameters = NumParameters + 1
        CurrentParameter = 0
        ValidParameter = False
    END IF

    'Set A equal to the first parameter and make it 1 if it's 0
    A = Parameters(0)
    IF A = 0 THEN A = 1

RETURN
END SUB

'Prints a string to the display.
SUB PrintString (B$)
    A& = SADD(B$)
    IF A& < 0 THEN A& = A& + 65536

    STOP' You must change the next line if you're using QB4.5!
    'It is currently coded for PDS.

    'Segment = VARSEG(B$) + A& \ 16

    Segment = SSEG(B$) + A& \ 16    'change to VARSEG(B$) for QB4.5 & QBASIC
    
    Address = A& MOD 16
    FOR B = Address TO Address + LEN(B$) - 1
        DEF SEG = Segment
        A1 = PEEK(B)
        DEF SEG = VideoSegment
        PrintANSI A1
    NEXT
END SUB

SUB ScrollUpScreen
    DIM Regs AS RegType
    Regs.Ax = &H601
   
    A& = Attribute * 256&
    IF A& > 32767 THEN A = A& - 65536 ELSE A = A&
    Regs.Bx = A
   
    Regs.Cx = (MinY * 256&) + MinX - 257
    Regs.Dx = (MaxY * 256&) + MaxX - 257
    CALL interrupt(&H10, Regs, Regs)
END SUB

'Sends the screen's status to the receiver. You must modify the
'"PRINT #1, A$;" command to print to your comm package.
'Sends "ESC[##;##R" where ##;## is Y;X.
SUB SendStatus (X, Y)
    A$ = CHR$(27) + "[" + RIGHT$("0" + MID$(STR$(Y), 2), 2)
    A$ = A$ + ";" + RIGHT$("0" + MID$(STR$(X), 2), 2) + "R"

'*****Change the next line to print this string out to your comm package!!****
    PRINT A$;           'DON'T insert a line feed!!
    
END SUB

'Sets the cursor- uses OUT's for speed
SUB SetCursor
    'Must do this...

    CursorAddress = (Xpos - 1) * 2 + (Ypos - 1) * BytesPerLine + VideoOffset
    IF CursorOn THEN
        Address = CursorAddress \ 2
        OUT CRT, &HE
        OUT CRT + 1, Address \ 256
        OUT CRT, &HF
        OUT CRT + 1, Address AND 255
    END IF
END SUB

'Sets a new printing window.
SUB SetWindow (WorkPage, Lx, Ly, Hx, Hy)
    DEF SEG = &H40
    IF WorkPage = -1 THEN
        VideoOffset = PEEK(&H4E) + PEEK(&H4F) * 256&
    ELSE
        VideoOffset = (PEEK(&H4C) + PEEK(&H4D) * 256&) * WorkPage
    END IF

    ScreenX = PEEK(&H4A)
    ScreenY = PEEK(&H84) + 1

    IF Lx = -1 THEN
        MinX = 1: MinY = 1
        MaxX = ScreenX: MaxY = ScreenY
        BytesPerLine = MaxX * 2
    ELSE
        'change window size if coordinates are valid
        IF Lx <= Hx AND Ly <= Hy AND Hx <= ScreenX AND Hy <= ScreenY THEN
            MinX = Lx: MaxX = Hx: MinY = Ly: MaxY = Hy
        END IF
    END IF
    DEF SEG = VideoSegment
    Xpos = MinX: Ypos = MinY
    SaveX = MinX: SaveY = MinY
    SetCursor
END SUB


'-------------8<----[ Begin PLAYME.BAS ]---->8----------------
'A lone play command is present in this module so error trapping is
'seperate from the main module. This decreases the size of the driver.
DEFINT A-Z
ErrorHandler:
RESUME NEXT

SUB playme (A$)
    ON ERROR GOTO ErrorHandler
    PLAY A$
    ON ERROR GOTO 0
END SUB

'-------------8<----[ End PLAYME.BAS ]---->8----------------
