;
; Low level IBM/PC console keyboard/window interface routines
; for use with the DDS MICRO-C compiler.
;
; Copyright 1989-2000 Dave Dunfield
; All rights reserved.
;
; Permission granted for personal (non-commercial) use only.
;
?CBASE  EQU     $B800           ; Color video screen segment
?MBASE  EQU     $B000           ; Monochrome video screen segment
;
; Initialized variables & tables
;
W_BASE  DW      ?CBASE          ; Mono=B000, Cga=B800
W_PAGE  DB      0               ; Current video page
W_OPEN  DW      0               ; Last opened window
; Special key table - General keys
?W_KEYS DW      $4800           ; KUA   (Up arrow)
        DW      $5000           ; KDA   (Down arrow)
        DW      $4B00           ; KLA   (Left arrow)
        DW      $4D00           ; KRA   (Right arrow)
        DW      $4900           ; KPU   (PgUp)
        DW      $5100           ; KPD   (PgDn)
        DW      $4700           ; KHO   (Home)
        DW      $4F00           ; KEN   (End)
        DW      $4E2B           ; KKP   (Keypad '+')
        DW      $4A2D           ; KKM   (Keypad '-')
        DW      $5200           ; KIN   (Ins)
        DW      $5300           ; KDL   (Del)
        DW      $0E08           ; KBS   (Backspace)
; Function keys
        DW      $3B00           ; K1    (F1)
        DW      $3C00           ; K2    (F2)
        DW      $3D00           ; K3    (F3)
        DW      $3E00           ; K4    (F4)
        DW      $3F00           ; K5    (F5)
        DW      $4000           ; K6    (F6)
        DW      $4100           ; K7    (F7)
        DW      $4200           ; K8    (F8)
        DW      $4300           ; K9    (F9)
        DW      $4400           ; K10   (F10)
; Special control keys
        DW      $8400           ; CPU   (CTRL-PgUp)
        DW      $7600           ; CPD   (CTRL-PgDn)
        DW      $7700           ; CHO   (CTRL-Home)
        DW      $7500           ; CEN   (CTRL-End)
        DW      0
; BOX character table
?BOXTAB DB      $C4,$B3,$DA,$BF,$C0,$D9
        DB      $CD,$BA,$C9,$BB,$C8,$BC
        DB      $CD,$B3,$D5,$B8,$D4,$BE
;
; Open a window: w_open(px, py, sx, sy, flags)
;
wopen   PUSH    BP              ; Save callers stack frame
        MOV     BP,SP           ; Address parameters
        PUSH    ES              ; Save ES
; Set up video mode
        MOV     AH,#$0F         ; Get video mode
        INT     $10             ; Call BIOS
        MOV     W_PAGE,BH       ; Save video page
        MOV     BX,#?CBASE      ; Assume COLOR address
        CMP     AL,#$07         ; Monochrome?
        JNZ     ?oinit1         ; No, assumption correct
        MOV     BX,#?MBASE      ; Get MONOCHROME address
?oinit1 MOV     W_BASE,BX       ; Set video base address
        MOV     ES,BX           ; Set up segment
; Allocate a window buffer
        XOR     AH,AH           ; zero high
        MOV     AL,5[BP]        ; Get video flags
        AND     AL,#$80         ; Do we save screen
        JZ      ?oinit2         ; No, do not
        MOV     AL,6[BP]        ; Get 'Y' size
        MOV     BL,8[BP]        ; Get 'X' size
        MUL     BL              ; Calculate total size
        SHL     AX,1            ; X2 for attributes
?oinit2 ADD     AX,#14          ; Include overhead
        PUSH    AX              ; Pass as parameter
        CALL    malloc          ; Allocate the buffer
        MOV     SI,AX           ; Set up pointer
        POP     DX              ; Clean up stack
        AND     SI,SI           ; Did we get it?
        JNZ     ?oinit3         ; Yes, proceed
; Couldn't allocate, return with bad news
        POP     ES              ; Restore extra set
        POP     BP              ; Restore caller
        RET
; Fill in window parameter block
?oinit3 MOV     DL,12[BP]       ; Get 'X' position
        MOV     DH,10[BP]       ; Get 'Y' position
        MOV     2[SI],DX        ; Save it
        MOV     DL,8[BP]        ; Get 'X' size
        MOV     DH,6[BP]        ; Get 'Y' size
        MOV     4[SI],DX        ; Save size
        MOV     BX,4[BP]        ; Get window flags
        MOV     [SI],BX         ; Save attributes & flags
        MOV     DX,W_OPEN       ; Get last opened window
        MOV     8[SI],DX        ; Save in buffer
        XOR     DX,DX           ; Reset cursor position
        MOV     6[SI],DX        ; Save initial cursor position
; Save window contents if required
        TEST    BH,#$80         ; Do we save it?
        JZ      ?oinit5         ; No we don't
; ... Set up pointers to screen & save area
        CALL    ?xyaddr         ; Get base address
        MOV     DX,4[SI]        ; Get 'X' and 'Y' sizes
        LEA     DI,14[SI]       ; Offset to data area
        PUSH    SI              ; Save SI
        MOV     SI,BX           ; Get base window address
; ... Save one line at a time
        PUSH    ES              ; Stack ES
        PUSH    DS              ; And DS
        POP     ES              ; For swap
        POP     DS              ; ES <> DS
?oinit4 PUSH    SI              ; Save source
        MOV     CL,DL           ; Get 'X' size
        XOR     CH,CH           ; Zero high size
        REP
        MOVSW                   ; Move one line
        POP     SI              ; Restore it
        ADD     SI,#160         ; Offset to next line
        DEC     DH              ; Reduce count
        JNZ     ?oinit4         ; And proceed
; ... Restore registers and continue
        PUSH    ES              ; Stack ES
        PUSH    DS              ; And DS
        POP     ES              ; For swap
        POP     DS              ; ES <> DS
        POP     SI              ; Restore SI
        XOR     DX,DX           ; Reset cursor position
        MOV     BH,5[BP]        ; Get flags back
; Draw box if required
?oinit5 MOV     BP,#?BOXTAB     ; Point to BOX table
        MOV     CX,4[SI]        ; Get size of box
        SUB     CH,#2           ; Adjust for bar
        DEC     CL              ; Adjust for bar
        AND     BH,#$60         ; Draw BOX?
        JZ      ?oinit8         ; No we don't
; ... Select box type
        CMP     BH,#$40         ; Box1?
        JZ      ?oinit6         ; Use this one
        ADD     BP,#6           ; Offset to next
        CMP     BH,#$20         ; Box2?
        JZ      ?oinit6         ; Use this one
        ADD     BP,#6           ; Offset to next
?oinit6 PUSH    CX              ; Save size
        CALL    ?xyaddr         ; Get address
; ... Draw the top (With corners)
        MOV     AH,[SI]         ; Get attribute
        MOV     AL,2[BP]        ; Top left corner
        MOV     ES:[BX],AX      ; Write top corner
        LEA     DI,2[BX]        ; Get value
        XOR     CH,CH           ; Zero high
        DEC     CL              ; Adjust for bar
        MOV     AL,0[BP]        ; Horizontal line
        REP
        STOSW                   ; Draw the line
        MOV     AL,3[BP]        ; Top right hand corner
        MOV     ES:[DI],AX      ; Write it
        POP     CX              ; Restore position
; ... Draw the box sides
?oinit7 INC     DH              ; Advance
        MOV     AL,1[BP]        ; Box side
        CALL    ?xyaddr         ; Get position
        MOV     ES:[BX],AX      ; Write it
        ADD     DL,CL           ; Offset
        CALL    ?xyaddr         ; Get position
        MOV     ES:[BX],AX      ; Write it
        SUB     DL,CL           ; Backup
        DEC     CH              ; Reduce count
        JNZ     ?oinit7         ; Do them all
; ... Draw the bottom (With corners)
        INC     DH              ; Advance to bottom
        CALL    ?xyaddr         ; get address
        MOV     AL,4[BP]        ; Lower left corner
        MOV     ES:[BX],AX      ; Write it
        LEA     DI,2[BX]        ; Position to line
        DEC     CL              ; Adjust for bar
        MOV     AL,0[BP]        ; Horizontal line
        REP
        STOSW                   ; Write it out
        MOV     AL,5[BP]        ; Lower right corner
        MOV     ES:[DI],AX      ; Write it out
; ... Reduce size of active region
        MOV     AX,2[SI]        ; Get position
        INC     AL              ; Advance 'X'
        INC     AH              ; Advance 'Y'
        MOV     2[SI],AX        ; Resave
        MOV     AX,4[SI]        ; Get size
        SUB     AH,#2           ; Chop out bars
        SUB     AL,#2           ; Chop out bars
        MOV     4[SI],AX        ; And resave
        XOR     DX,DX           ; Reset cursor position
; Clear screen if required
?oinit8 MOV     BH,1[SI]        ; Get flags back
        TEST    BH,#$10         ; Clear screen?
        JZ      ?oinit9         ; No, its ok
        CALL    ?cleos          ; Clear one line
; Save cursor information
?oinit9 MOV     BH,W_PAGE       ; Get video page
        MOV     AH,#$03         ; Get cursor info
        INT     $10             ; Get cursor
        MOV     10[SI],CX       ; Save shape
        MOV     12[SI],DX       ; Save position
        MOV     AX,SI           ; Get buffer address
        MOV     W_OPEN,AX       ; Save this window pointer
        POP     ES              ; Restore ES
        POP     BP              ; Restore callers stack frame
        RET
;
; Close current window: wclose()
;
wclose  CALL    ?xsetup         ; Get last window
        JMP     <?wclos0        ; And proceed
;
; Close a window: w_close(window)
;
w_close CALL    ?wsetup         ; Get parameters
?wclos0 XOR     DX,DX           ; Zero position
        MOV     BH,1[SI]        ; Get open flags
; If window was BOXed, adjust sizes first
        TEST    BH,#$60         ; Was it BOXed
        JZ      ?wclos1         ; No, don't adjust
        MOV     AX,2[SI]        ; Get position
        DEC     AL              ; Expand 'X'
        DEC     AH              ; Expand 'Y'
        MOV     2[SI],AX        ; Resave
        MOV     AX,4[SI]        ; Get size
        ADD     AH,#2           ; Adjust for bars
        ADD     AL,#2           ; Adjust for bars
        MOV     4[SI],AX        ; Resave
; Clear window if requested
?wclos1 TEST    BH,#$08         ; Clear window?
        JZ      ?wclos2         ; No, try next
        CALL    ?cleos          ; Clear the window
        MOV     BH,1[SI]        ; Get flags back
; Restore previous contents if saved
?wclos2 TEST    BH,#$80         ; Do we restore it
        JZ      ?wclos4         ; No, just clear
; ... Set up pointers to screen & save area
        CALL    ?xyaddr         ; Calculate base address
        MOV     DX,4[SI]        ; Get 'X' and 'Y' sizes
        PUSH    SI              ; Save SI
        LEA     SI,14[SI]       ; Offset to data area
        MOV     DI,BX           ; Get base window address
; ... Restore one line at a time
?wclos3 PUSH    DI              ; Save source
        MOV     CL,DL           ; Get 'X' size
        XOR     CH,CH           ; Zero high size
        REP
        MOVSW                   ; Move one line
        POP     DI              ; Restore it
        ADD     DI,#160         ; Offset to next line
        DEC     DH              ; Reduce count
        JNZ     ?wclos3         ; And proceed
        POP     SI              ; Restore SI
        MOV     BH,1[SI]        ; Get flags back
; Restore the cursor
?wclos4 MOV     CX,10[SI]       ; Get cursor shape
        MOV     AH,#$01         ; Set shape
        INT     $10             ; Call BIOS
        MOV     DX,12[SI]       ; Get position
        MOV     BH,W_PAGE       ; Get display page
        MOV     AH,#$02         ; Set position
        INT     $10             ; Call BIOS
; If this is the active window, switch to previously open
        CMP     SI,W_OPEN       ; Is this it?
        JNZ     ?wclos5         ; No, don't reset
        MOV     AX,8[SI]        ; Get last free
        MOV     W_OPEN,AX       ; Save it
; Release the buffer contents
?wclos5 PUSH    SI              ; Save address of buffer
        CALL    free            ; Release it
        POP     SI              ; Fixup stack
        POP     ES              ; Restore ES
        POP     BP              ; Restore BP
        RET
;
; Write character into current window: wputc(int c)
;
wputc   CALL    ?xsetup         ; Get last open window
        MOV     AX,4[BP]        ; Get character
        JMP     <?tstbel        ; And proceed
;
; Write a character to the video display: w_putc(int c, window)
;
w_putc  CALL    ?wsetup         ; Set up video addresses
        MOV     AX,6[BP]        ; Get character to display
; Handle BELL
?tstbel CMP     AX,#'G'-$40     ; BELL code?
        JNZ     ?tstcr          ; No, try next
        MOV     AX,#$0E07       ; Write BELL code
        XOR     BX,BX           ; Write to page 0
        INT     $10             ; Call BIOS
        JMP     <?vedit         ; and exit
; Handle CARRIAGE RETURN
?tstcr  CMP     AX,#'M'-$40     ; Is it carriage return
        JNZ     ?tstbs          ; No, try next
        XOR     DL,DL           ; Reset 'X' position
        JMP     <?vedit         ; and proceed
; Handle BACKSPACE
?tstbs  CMP     AX,#'H'-$40     ; Is it backspace
        JNZ     ?tstnl          ; No, try line-feed
        AND     DL,DL           ; Already at first col?
        JZ      ?vedit          ; Yes, don't backup
        DEC     DL              ; Reduce 'X' position
        JMP     <?vedit         ; And exit
; Handle NEWLINE
?tstnl  CMP     AX,#'J'-$40     ; Is it newline?
        JNZ     ?norchr         ; Yes, advance line
        MOV     AL,1[SI]        ; Get flags
        TEST    AL,#$04         ; Special case?
        JNZ     ?advy           ; Yes, don't reset 'X'
        XOR     DL,DL           ; Reset cursor
        JMP     <?advy          ; And goto a new line
; Normal Character, output it
?norchr CALL    ?xyaddr         ; Calculate address
        MOV     AH,[SI]         ; Get video attributes
        MOV     ES:[BX],AX      ; Write to video display
; Advance 'X' position
?advx   INC     DL              ; Advance 'X'
        CMP     DL,4[SI]        ; Are we over?
        JB      ?vedit          ; No, its ok
        DEC     DL              ; Restore it
        MOV     AL,1[SI]        ; Get flags
        TEST    AL,#$01         ; Line wrap enabled?
        JZ      ?vedit          ; No, skip it
        XOR     DL,DL           ; Reset 'X' position
; Advance 'Y' position
?advy   INC     DH              ; Advance 'Y' position
        CMP     DH,5[SI]        ; are we over?
        JB      ?vedit          ; No, Its OK
        DEC     DH              ; Reset it
        MOV     AL,1[SI]        ; Get flags
        TEST    AL,#$02         ; Screen wrap enabled?
        JZ      ?vedit          ; No, skip it
        CALL    ?scroll         ; Scroll window
; Restore callers environment & exit
?vedit  MOV     6[SI],DX        ; Resave 'X' and 'Y'
        POP     ES              ; Restore callers ES
        POP     BP              ; Restore callers stack frame
        RET
;
; CLEAR current window: wclwin()
;
wclwin  CALL    ?xsetup         ; Get current window
        JMP     <?clscr1        ; And proceed
;
; CLEAR entire window: w_clwin(window)
;
w_clwin CALL ?wsetup                ; Setup video
?clscr1 XOR     DX,DX           ; Reset to top of screen
        MOV     6[SI],DX        ; Reset cursor
        CALL    ?updt2          ; Position cursor
        XOR     DX,DX           ; Reset again
        JMP     <?clscr2        ; And proceed
;
; CLEAR to end of current window: wcleow()
;
wcleow  CALL    ?xsetup         ; Get current window
        JMP     <?clscr2        ; And proceed
;
; CLEAR to END OF WINDOW: w_cleow(window)
;
w_cleow CALL    ?wsetup         ; Setup video
?clscr2 CALL    ?cleos          ; Clear to end
        POP     ES              ; Restore ES
        POP     BP              ; Restore caller
        RET
;
; CLEAR to end of line in current: wcleol()
;
wcleol  CALL    ?xsetup         ; Get current window
        JMP     <?cleol1        ; And proceed
;
; CLEAR to END OF LINE: w_cleol(window)
;
w_cleol CALL    ?wsetup         ; Setup video
?cleol1 CALL    ?xyaddr         ; Get address
        MOV     DI,BX           ; Set up address
        MOV     CL,4[SI]        ; Get size of line
        SUB     CL,DL           ; Calculate remaining
        XOR     CH,CH           ; Zero high
        MOV     AH,[SI]         ; Get attribute
        MOV     AL,#' '         ; Clear to space
        REP
        STOSW                   ; Clear a line
        POP     ES              ; REstore ES
?cleol2 POP     BP              ; Restore caller
        RET
;
; Position the cursor in current window: wgotoxy(int x, int y)
;
wgotoxy PUSH    BP              ; Save callers stack frame
        MOV     BP,SP           ; Address parameters
        MOV     AL,6[BP]        ; Get 'X' value
        MOV     AH,4[BP]        ; Get 'Y' value
        MOV     SI,W_OPEN   ; Get open window
        JMP     <?goto1         ; And proceed
;
; Position the cursor in window: w_gotoxy(int x, int y, window)
;
w_gotoxy PUSH   BP              ; Save callers stack frame
        MOV     BP,SP           ; Address parameters
        MOV     AL,8[BP]        ; Get 'X' value
        MOV     AH,6[BP]        ; Get 'Y' value
        MOV     SI,4[BP]        ; Get window buffer
?goto1  CMP     AL,4[SI]        ; In range?
        JAE     ?cleol2         ; No, error
        CMP     AH,5[SI]        ; In range
        JAE     ?cleol2         ; No, error
        MOV     6[SI],AX        ; Save values
        JMP     <?updt1         ; And proceed
;
; Update the cursor in current window: wgotoxy()
;
wupdatexy MOV   SI,W_OPEN       ; Get open window
        JMP     <?updt2         ; And proceed
;
; Update the cursor position: w_updatexy(window)
;
w_updatexy PUSH BP              ; Save callers stack frame
        MOV     BP,SP           ; Address parameters
        MOV     SI,4[BP]        ; Get caller
?updt1  POP     BP              ; Resture caller
?updt2  MOV     DX,2[SI]        ; Get starting address
        ADD     DX,6[SI]        ; Offset into window
        MOV     BH,W_PAGE       ; Get video page
        MOV     AH,#$02         ; Set cursor function
        INT     $10             ; Call DOS
        RET
;
; Check for a key from the keyboard: c = w_tstc(window)
;
w_tstc  MOV     AH,#$01         ; Check for key
        INT     $16             ; Do we have key?
        JNZ     w_getc          ; Yes, read it
?wtst1  XOR     AX,AX           ; No key today
        RET
;
; Check for key with cursor in current window: c = wtstc()
;
wtstc   MOV     AH,#$01         ; Check for key
        INT     $16             ; Do we have key?
        JZ      ?wtst1          ; No, return zero
;
; Get in cursor in current window: c = wgetc()
;
wgetc   MOV     SI,W_OPEN       ; Get active window
        JMP     <?getc1         ; And proceed
;
; Get a key from the keyboard with translations: c = w_getc(window)
;
w_getc  MOV     BX,SP           ; Address parameters
        MOV     SI,2[BX]        ; Get window
?getc1  CALL    ?updt2          ; Update the cursor position
; Call BIOS to read key
        XOR     AH,AH           ; Function code 0 - read key
        INT     $16             ; Call bios
; Lookup key for special entries
        MOV     CL,#$80         ; Beginning function code
        MOV     BX,#?W_KEYS     ; Address of keys array
?lokkey MOV     DX,[BX]         ; Get key entry
        CMP     AX,DX           ; Does it match?
        JZ      ?fndkey         ; We found it
        ADD     BX,#2           ; Skip ahead
        INC     CL              ; Advance key code
        OR      DH,DL           ; End of table?
        JNZ     ?lokkey         ; Keep looking
; Translate ENTER key to newline
        CMP     AX,#$1C0D       ; ENTER key?
        JNZ     ?norkey         ; Yes, we have it
        MOV     CL,#'J'-$40     ; Newline is LINE-FEED
; Translate special keys
?fndkey MOV     AL,CL           ; Set return value
?norkey CBW                     ; Set high bits
        RET
;
; Set the CURSOR OFF: wcursor_off()
;
wcursor_off EQU *
        MOV     CX,#$2020       ; Value for cursor off
        JMP     <?setc2         ; And set it
;
; Set the CURSOR to a block: wcursor_block()
;
wcursor_block EQU *
        MOV     CX,#$0006       ; Color block cursor
        MOV     DX,#$000B       ; Monochrome block cursor
        JMP     <?setc1         ; and proceed
;
; Set the CURSOR to a LINE: wcursor_line()
;
wcursor_line EQU *
        MOV     CX,#$0607       ; Color line cursor
        MOV     DX,#$0B0C       ; Monochrome line cursor
?setc1  MOV     AX,W_BASE       ; Get video base
        CMP     AX,#?MBASE      ; Monochrome?
        JNZ     ?setc2          ; No, do it
        MOV     CX,DX           ; Copy for later
; Set cursor to value in CX
?setc2  MOV     AH,#1           ; Set cursor type
        INT     $10             ; Call BIOS
        RET
;
; Set up addressability to video display & stack registers
; Exit: ES = video base address
;       SI = window buffer address
;       DX = cursor address (X/Y)
;
?wsetup POP     AX              ; Get return address
        PUSH    BP              ; Save callers stack frame
        MOV     BP,SP           ; Address parameters
        MOV     SI,4[BP]        ; Get window buffer
?wset1  PUSH    ES              ; Save callers Extra segment
        PUSH    AX              ; Replace return address
        MOV     ES,W_BASE   ; Point to video base
        MOV     DX,6[SI]        ; Get 'X' and 'Y' position
        RET
;
; Set up addressability to currently open window
;
?xsetup POP     AX              ; Get return address
        PUSH    BP              ; Save callers stack frame
        MOV     BP,SP           ; Address parameters
        MOV     SI,W_OPEN   ; Get open window
        JMP     <?wset1         ; And proceed
;
; Scroll window forward one line
;
?scroll PUSH    DS              ; Save data segment
        PUSH    DX              ; Save cursor
        PUSH    SI              ; Save SI
; First, calculate base address of window
        MOV     AL,3[SI]        ; Get 'Y' position
        MOV     BL,#160         ; Size of line
        MUL     BL              ; Calculate 'Y' offset
        MOV     BL,2[SI]        ; Get 'X' position
        XOR     BH,BH           ; Zero high
        SHL     BX,1            ; * 2 for char & attribute bytes        
        ADD     BX,AX           ; BX = character position
        MOV     DI,BX           ; Get base window address
; Scroll one line at a time
        MOV     DX,4[SI]        ; Get 'X' and 'Y' sizes
        DEC     DH              ; Don't copy to last
        PUSH    ES              ; Stack ES
        POP     DS              ; To place in DS
?scrol1 PUSH    DI              ; Save DI
        MOV     SI,DI           ; Begin with same address
        ADD     SI,#160         ; Offset to next line
        MOV     CL,DL           ; Get 'X' size
        XOR     CH,CH           ; Zero high size
        REP
        MOVSW                   ; Move one line
        POP     DI              ; Restore it
        ADD     DI,#160         ; Offset to next line
        DEC     DH              ; Reduce count
        JNZ     ?scrol1         ; And proceed
; Clear bottom line of screen
        MOV     CL,DL           ; Get 'X' size
        POP     SI              ; Restore SI
        POP     DX              ; Restore cursor
        POP     DS              ; Restore data segment
        MOV     AH,[SI]         ; Get attribute
        MOV     AL,#' '         ; And space
        REP
        STOSW                   ; Clear it
        RET
;
; Clear from cursor(DX) to end of line
;
?cleos  PUSH    DX              ; Save cursor
?cleos1 CALL    ?xyaddr         ; Get address
        MOV     DI,BX           ; Set up address
        MOV     CL,4[SI]        ; Get size of line
        SUB     CL,DL           ; Calculate remaining
        XOR     CH,CH           ; Zero high
        MOV     AH,[SI]         ; Get attribute
        MOV     AL,#' '         ; Clear to space
        REP
        STOSW                   ; Clear a line
        XOR     DL,DL           ; Zero 'X'
        INC     DH              ; Advance 'Y'
        CMP     DH,5[SI]        ; Are we finished
        JB      ?cleos1         ; Keep going
        POP     DX              ; Restore cursor
        RET
;
; Calculate screen address from X/Y position (in DX)
; On exit: BX = address
;
?xyaddr PUSH    AX              ; Save AX
        MOV     AL,DH           ; Get 'Y' position
        ADD     AL,3[SI]        ; Offset from start of screen
        MOV     BL,#160         ; Size of physical screen
        MUL     BL              ; Calculate 'Y' offset
        MOV     BL,DL           ; Get 'X' position
        ADD     BL,2[SI]        ; Offset from start of screen
        XOR     BH,BH           ; Zero high byte
        SHL     BX,1            ; * 2 for char & attribute bytes
        ADD     BX,AX           ; BX = character position
        POP     AX              ; Restore it
        RET
$EX:malloc
$EX:free
„