;       WAITASEC.ASM -- Holds and Recalls Scrolling Screens
;       ============
;                       (C) Copyright Charles Petzold, 1985
;
;                       COM file format
;

CSEG            Segment
                Assume  CS:CSEG
                Org     0080h
SWAP            Label   Byte            ; Use this area for line swapping
                Org     0100h
Entry:          Jmp     Initialize      ; Entry: Jump over resident part

;       All Data
;       --------

Cols            equ     80              ; You may want to set this to 40
ScrollLock      equ     70              ; Scan code of Scroll Lock key
NumberOfScreens dw      8               ; Can be set up to 15 for 80-column

                db      '(C) Copyright Charles Petzold, 1985'

DisplaySegment  dw      ?               ; Display address set by VideoCheck
OldInterrupt9   dd      ?               ; Saved address of real Interrupt 9
OldInterrupt10  dd      ?               ; Ditto for Interrupt 10h

TopHoldAddr     dw      Offset HoldArea ; Address of top of Hold area
BotHoldAddr     dw      ?               ; Address of bottom of Hold area
SavePointer     dw      Offset HoldArea ; Points to where text is saved
RecallPointer   dw      Offset HoldArea ; Points to where text is recalled

LockStateOn     db      0               ; Flag for lock or no lock
ScrollLockDown  db      0               ; Current state of Scroll Lock key

KeyDispatch     dw      Home,Up,PgUp    ; Dispatch addresss of routines
                dw      5 Dup (Dummy)   ; for cursor movement within
                dw      Endi,Down,PgDn  ; recalled displays

;       Intercept of Interrupt 10h -- BIOS Video Call
;       --------------------------

NewInterrupt10: Cmp     AX,0601h        ; Check if this is a 1 line scroll up
                Jne     NoNeedToBother  ; No, just continue to real interrupt
                Cmp     CX,0            ; Check if this is upper left corner
                Jne     NoNeedToBother  ; No, don't want to touch it
                Cmp     DL,Cols - 1     ; Check if DL is right hand side
                Jne     NoNeedToBother  ; Another obstacle that shuts us out
                Call    VideoCheck      ; Returns Carry if graphics
                Jc      NoNeedToBother  ; So we drop out here if Carry is set

                Sti                     ; Allow interrupts
                Cld                     ; And set direction to forward
                Push    CX              ; Save all the registers we'll use
                Push    SI
                Push    DI
                Push    DS
                Push    ES

                Push    CS                      ; Push this code segment
                Pop     ES                      ; So we can set ES to it
                Mov     DI,CS:[SavePointer]     ; This is the destination
                Mov     DS,CS:[DisplaySegment]  ; This is the source segment
                Sub     SI,SI                   ; Source is top of display
                Mov     CX,Cols                 ; The number of characters

                Call    VideoOff                ; Turn off light show if color
                Rep     Movsw                   ; Move 'em in!
                Call    VideoOn                 ; Turn display back on

                Cmp     DI,CS:[BotHoldAddr]     ; Check the new destination
                Jb      NotAtBotYet             ; Jump if it hasn't wrapped yet
                Mov     DI,CS:[TopHoldAddr]     ; Set it to top if it has
NotAtBotYet:    Mov     CS:[SavePointer],DI     ; And save it anyway

                Pop     ES                      ; Pop the registers we saved
                Pop     DS
                Pop     DI
                Pop     SI
                Pop     CX

NoNeedToBother: Jmp     CS:[OldInterrupt10]     ; And do the interrupt for real

;       Intercept of Interrupt 9h -- Hardware Keyboard Interrupt
;       --------------------------------------------------------

NewInterrupt9:  Sti                     ; Allow other interrupts
                Push    AX              ; Save the register for awhile
                In      AL,60h          ; Get the pressed key scan code
                Cmp     AL,ScrollLock   ; Check if it's the Scroll Lock
                Jz      GottaScrollLock ; If so, do special routine
                Cmp     AL,128 + ScrollLock     ; Check if scroll lock release
                Jz      ScrollLockRlse  ; Another special routine
                Jmp     AnyOtherKey     ; Otherwise, it's some other key

                                                ; Scroll Lock key depressed
                                                ; -------------------------

GottaScrollLock:Mov     CS:[ScrollLockDown],1   ; Flag Scroll Lock as depressed
                Cmp     CS:[LockStateOn],1      ; See if we're locked now
                Jz      AnyOtherKey             ; If so, will just reset

                Mov     AH,2                    ; Do keyboard interrupt
                Int     16h                     ;   to get shift states
                Test    AL,04                   ; See if Ctrl has been pressed
                Jnz     NormalProcess           ; Process normally if Break

                Call    VideoCheck              ; See if video is OK for recall
                Jc      NormalProcess           ; If not, ignore the key

                Mov     CS:[LockStateOn],1      ; Lock state now ON
                Push    CS:[SavePointer]        ; Transfer value of SavePointer
                Pop     CS:[RecallPointer]      ; ... to RecallPointer
                PushF                           ; Simulate an interrupt
                Call    CS:[OldInterrupt9]      ; Let Scroll Lock Register
                Call    VideoOn                 ; Video might be off - turn on

HoldingPattern: Cmp     CS:[LockStateOn],1      ; Wait in this loop ...
                Jz      HoldingPattern          ; ... until out of lock state

                Jmp     NormalProcess           ; Then process last key

                                                ; Scroll Lock key released
                                                ; ------------------------

ScrollLockRlse: Mov     CS:[ScrollLockDown],0   ; Register Scroll Lock release
                Cmp     CS:[LockStateOn],1      ; See if in lock state
                Jnz     NormalProcess           ; If not, just process key

                Mov     AX,CS:[SavePointer]     ; Check if the screen is in
                Cmp     AX,CS:[RecallPointer]   ;   normal position
                Jnz     NormalProcess           ; If not, just process key

                Mov     CS:[LockStateOn],0      ; If so, turn lock state OFF
                Pop     AX                      ; Restore register
                IRet                            ; Return to holding pattern

                                                ; All other keys
                                                ; --------------

AnyOtherKey:    Cmp     CS:[LockStateOn],1      ; For other keys, just do
                Jnz     NormalProcess           ; normal processing if not lock

                Call    NowInLockState          ; But call routine if locked
                Pop     AX                      ; Restore register
                IRet                            ; Return to holding pattern

NormalProcess:  Pop     AX                      ; Get back the register
                Jmp     CS:[OldInterrupt9]      ; Let the key process

;       Routine for other keys during lock state (AL = Scan Code)
;       ---------------------------------------------------------

NowInLockState: Cld                     ; String moves generally forward
                Push    CX              ; We use all these registers
                Push    DX
                Push    SI
                Push    DI
                Push    DS
                Push    ES

                Mov     DX,CS                   ; Get value of CS
                Mov     DS,DX                   ; Set DS and
                Mov     ES,DX                   ; ES to this segment
                Assume  DS:CSEG, ES:CSEG        ; And tell the assembler

                Mov     AH,AL                   ; AH is actual scan code
                And     AL,7Fh                  ; AL has release byte off

                Sub     AL,71                   ; Subtract the 'Home' key value
                Jb      WrongKey                ; No good if below
                Cmp     AL,(81 - 71)            ; Above the 'PgDn' key?
                Ja      WrongKey                ; You're not right either
                Cmp     AL,(73 - 71)            ; Check if under or = 'PgUp'
                Jbe     AllRightKey             ; Ok for this key
                Cmp     AL,(79 - 71)            ; Check if over or = 'End'
                Jae     AllRightKey             ; You pass too

WrongKey:       Cmp     [ScrollLockDown],1      ; See if scroll lock is down
                Jz      ResetKeyboard           ; If so, just ignore the key

                Call    Endi                    ; If not, restore the display
                Mov     [LockStateOn],0         ; Turn off the lock state
                Jmp     KeyReturn               ; And get out quickly

AllRightKey:    Test    AH,80h                  ; See if cursor key is release
                Jnz     ResetKeyboard           ; If so, ignore the key

                Cbw                             ; Convert scan code to word
                Add     AX,AX                   ; Double it because word access
                Mov     SI,AX                   ; Set SI to it
                Call    [KeyDispatch + SI]      ; And do the routine

ResetKeyboard:  In      AL,61h                  ; These instructions
                Mov     AH,AL                   ; reset the keyboard
                Or      AL,80h
                Out     61h,AL
                Mov     AL,AH
                Out     61h,AL

                Cli                             ; Disable interrupts
                Mov     AL,20h                  ; to reset the interrupt
                Out     20h,AL                  ; controller

KeyReturn:      Pop     ES                      ; Get back all the pushes
                Pop     DS
                Pop     DI
                Pop     SI
                Pop     DX
                Pop     CX
                Ret                             ; Return to previous routine

;       Routines to scan up through saved display
;       -----------------------------------------

Home:           Mov     CX,0FFFFh       ; 'Home' key -- "infinite" lines up
                Jmp     Short GoingUp   ; Do it

Up:             Mov     CX,1            ; 'Up' key -- 1 line up
                Jmp     Short GoingUp   ; Go to it

PgUp:           Mov     CX,25           ; 'PgUp' key -- 25 lines (1 screen) up

GoingUp:        Mov     AX,[RecallPointer]      ; Points to the text source
                Cmp     AX,[TopHoldAddr]        ; Check if it's at the top
                Ja      NotScanUpTop            ; If not there, no problem
                Mov     AX,[BotHoldAddr]        ; Otherwise must wrap around
NotScanUpTop:   Sub     AX,Cols * 2             ; Go back one line for recall
                Cmp     AX,[SavePointer]        ; Check if cycled through yet
                Je      EndGoingUp              ; If so, abort this routine

                Mov     [RecallPointer],AX      ; Save the new pointer value
                Push    CX                      ; And save our counter

                Mov     SI,2 * 24 * Cols        ; Set source to bottom line
                Call    DisplayToSwap           ; Transfer it to SWAP area

                Mov     SI,2 * 24 * Cols - 2    ; End of penultimate line
                Mov     DI,2 * 25 * Cols - 2    ; End of last line
                Std                             ; Backwards string transfer
                Call    ScrollDisplay           ; Do the screen scroll
                Cld                             ; Direction back to forward

                Mov     DI,0                    ; Destination is top line
                Call    HoldToDisplay           ; Move line to display
                Call    SwapToHold              ; And SWAP line to hold

                Pop     CX                      ; Get back the counter
                Loop    GoingUp                 ; And do CX times

Dummy:                                          ; Should never get here
EndGoingUp:     Ret                             ; But this is the end

;       Routines to scan down through saved display
;       -------------------------------------------

Endi:           Mov     CX,0FFFFh       ; 'End' key -- infinite lines down
                Jmp     Short GoingDown ; Do it

Down:           Mov     CX,1            ; 'Down' key -- 1 line down
                Jmp     Short GoingDown ; All ready

PgDn:           Mov     CX,25           ; 'PgDn' key -- 25 lines down

GoingDown:      Mov     AX,[RecallPointer]      ; Check if all through
                Cmp     AX,[SavePointer]        ; by this comparison
                Je      EndGoingDown            ; If so, exit this thing

                Push    CX                      ; Save the counter

                Mov     SI,0                    ; Set source to top line
                Call    DisplayToSwap           ; Transfer it to SWAP area

                Mov     SI,2 * 80               ; Source is second line
                Mov     DI,0                    ; Destination is top line
                Call    ScrollDisplay           ; So we can scroll up display

                Mov     DI,2 * 24 * Cols        ; Destination is bottom line
                Call    HoldToDisplay           ; For the saved text
                Call    SwapToHold              ; Move original top line in

                Mov     AX,[RecallPointer]      ; Adjust the recall pointer
                Add     AX,Cols * 2             ; By increasing by one line
                Cmp     AX,[BotHoldAddr]        ; See if wrap around
                Jb      NotScanDownBot          ; If not, skip a little
                Mov     AX,[TopHoldAddr]        ; Get the top address
NotScanDownBot: Mov     [RecallPointer],AX      ; And set to new recall

                Pop     CX                      ; Get back the counter
                Loop    GoingDown               ; Do this CX times

EndGoingDown:   Ret                             ; Go back to caller

;       Display to Swap -- Save top or bottom line of display (SI) in SWAP area
;       -----------------------------------------------------------------------

DisplayToSwap:  Push    DS                      ; Save data segment
                Mov     DS,[DisplaySegment]     ; And set it to display
                Mov     DI,Offset Swap          ; This is the destination
                Mov     CX,Cols                 ; Do one line
                Call    VideoOff                ; Turn off color video
                Rep     Movsw                   ; Do the line move
                Pop     DS                      ; Get back DS
                Ret                             ; And end

;       Scroll Video Display -- SI is source, DI destination
;       ----------------------------------------------------

ScrollDisplay:  Push    DS                      ; Save both these segments
                Push    ES
                Mov     ES,[DisplaySegment]     ; Set both to display
                Mov     DS,[DisplaySegment]
                Mov     CX,24 * Cols            ; 24 lines must be moved
                Rep     Movsw                   ; This line does it
                Pop     ES                      ; Get back the segments
                Pop     DS
                Ret                             ; And go back

;       Hold To Display -- Move one saved line to video display (DI)
;       ------------------------------------------------------------

HoldToDisplay:  Push    ES                      ; Now save just ES
                Mov     ES,[DisplaySegment]     ; And set it to display
                Mov     SI,[RecallPointer]      ; This is the source
                Mov     CX,Cols                 ; One line only
                Rep     Movsw                   ; Do the transfer
                Call    VideoOn                 ; Color display back on
                Pop     ES                      ; Get back ES
                Ret                             ; And return

;       Swap To Hold -- Completes the cycle
;       -----------------------------------

SwapToHold:     Mov     SI,Offset Swap          ; Source is SWAP
                Mov     DI,[RecallPointer]      ; Destination in HoldArea
                Mov     CX,Cols                 ; One line transfer
                Rep     Movsw                   ; Do it
                Ret                             ; And return

;       Video Check -- Returns CY flag is graphics or 40 column or not page 0
;       ---------------------------------------------------------------------

                Assume  DS:Nothing, ES:Nothing

VideoCheck:     Push    AX
                Push    BX

                Mov     CS:[DisplaySegment],0B000h      ; Set up for monochrome

                Mov     AH,15                   ; Get current video mode
                Int     10h                     ;   through display interrupt

                Cmp     AL,7                    ; See if monochrome
                Jz      VideoAOK                ; If so, skip out OK
                Cmp     AL,3                    ; Check if it's graphics
                Ja      VideoNoGood             ; If so, can't do anything
                Cmp     BH,0                    ; Check if it's Page 0
                Jnz     VideoNoGood             ; Nope?  No bad
                Cmp     AH,Cols                 ; See if columns is right
                Jne     VideoNoGood             ; No -- dishonorable discharge

                Mov     CS:[DisplaySegment],0B800h      ; Change it to color
VideoAOK:       Clc                             ; Flag for no error
                Jmp     Short VideoReturn       ; Get out
VideoNoGood:    Stc                             ; Flag for error
VideoReturn:    Pop     BX                      ; Get back register
                Pop     AX
                Ret                             ; Now go back

;       Video Off -- Turns off display for Color / Graphics
;       ---------------------------------------------------

VideoOff:       Cmp     CS:[DisplaySegment],0B800h      ; Check if color
                Jnz     NoColorTurnOff                  ; If not, punt

                Push    AX                      ; Just use two registers
                Push    DX
                Mov     DX,3DAh                 ; Color / Graphics Status Port

RetraceWait1:   In      AL,DX                   ; Get Color / Graphics Status
                Test    AL,08                   ; Check the vertical retrace
                Jnz     RetraceWait1            ; Loop till it's over

RetraceWait2:   In      AL,DX                   ; Check status again
                Test    AL,08                   ; In particular the retrace
                Jz      RetraceWait2            ; Loop till it comes

                Mov     DX,3D8h                 ; Video Mode Select
                Mov     AL,25h                  ; Byte to turn off video
                Out     DX,AL                   ; Turn it off

                Pop     DX                      ; Retrieve registers from stack
                Pop     AX
NoColorTurnOff: Ret

;       Video On -- Turns Color Display back on
;       ---------------------------------------

VideoOn:        Cmp     CS:[DisplaySegment],0B800h      ; See if color
                Jnz     NoColorTurnOn                   ; If not do nothing

                Push    AX                      ; Save registers
                Push    DX

                Mov     DX,3D8h                 ; Video mode register port
                Mov     AL,28h - (Cols EQ 80)   ; i.e., 29h for 80 columns
                Out     DX,AL                   ; Turn video back on

                Pop     DX                      ; Get back registers
                Pop     AX
NoColorTurnOn:  Ret                             ; And leave

;       Initialization Procedure
;       ------------------------

                Assume  DS:CSEG, ES:CSEG

HoldArea        Label   Word                    ; Storage of screens also

Initialize:     Mov     AX,Cols * 25 * 2        ; Characters per screen
                Mul     [NumberOfScreens]       ; AX = bytes for HoldArea
                Add     AX,Offset HoldArea      ; Calculate bottom of HoldArea
                Mov     [BotHoldAddr],AX        ; Save the bottom address
                Mov     DX,AX                   ; Last address for terminate

                Sub     AX,AX                   ; Zero out AX
                Mov     DS,AX                   ; To set DS to Vector Segment

                Cli                             ; No Interrupts now

                Les SI,DS:[9h * 4]                      ; Get Int 9 address
                Mov Word Ptr CS:[OldInterrupt9],SI              ; Save offset
                Mov Word Ptr CS:[OldInterrupt9 + 2],ES          ; Save segment
                Mov Word Ptr DS:[9h * 4],Offset NewInterrupt9   ; New offset
                Mov Word Ptr DS:[9h * 4 + 2],CS                 ; New segment

                Les SI,DS:[10h * 4]                     ; Get Int 10 address
                Mov Word Ptr CS:[OldInterrupt10],SI             ; Save offset
                Mov Word Ptr CS:[OldInterrupt10 + 2],ES         ; Save segment
                Mov Word Ptr DS:[10h * 4],Offset NewInterrupt10 ; New offset
                Mov Word Ptr DS:[10h * 4 + 2],CS                ; New segment

                Sti                             ; Interrupts back on

                Int     27h                     ; Terminate and stay resident

CSEG            EndS

                End Entry                       ; End denotes Entry point
