.MODEL	SMALL

;-----------------------------------------------------------------------;
; This file contains procedures for working with the keyboard:		;
;									;
; READ_KEY			Read a character from the keyboard	;
; STRING_TO_UPPER		Converts a string to upper case		;
; CHAR_TO_UPPER			Converts a character to upper case	;
; READ_STRING			Reads in a string of characters		;
; READ_DECIMAL			Reads a decimal number			;
; READ_BYTE			Reads a single byte			;
; HEX_TO_BYTE			Converts to characters to a hex byte	;
; CONVERT_HEX_DIGIT		Converts a hex digit to a nibble	;
;-----------------------------------------------------------------------;


BS	EQU	8			;Backspace character
CR	EQU	13			;Carriage-return character
ESCAPE	EQU	27			;Escape character


.DATA

KEYBOARD_INPUT	LABEL BYTE
CHAR_NUM_LIMIT	DB	0		;Length of input buffer
NUM_CHARS_READ	DB	0		;Number of characters read
CHARS		DB	80 DUP (0)	;A buffer for keyboard input

.CODE

	PUBLIC	READ_KEY
;-----------------------------------------------------------------------;
; This procedure reads one key from the keyboard.			;
;									;
; Returns:	AL	Character code (unless AH = 1)			;
;		AH	0 if read ASCII char				;
;			1 if read a special key				;
;-----------------------------------------------------------------------;
READ_KEY	PROC
	XOR	AH,AH			;Ask for keyboard read function
	INT	16h			;Read character/scan code from keyboard
	OR	AL,AL			;Is it an extended code?
	JZ	EXTENDED_CODE		;Yes
NOT_EXTENDED:
	XOR	AH,AH			;Return just the ASCII code
DONE_READING:
	RET

EXTENDED_CODE:
	MOV	AL,AH			;Put scan code into AL
	MOV	AH,1			;Signal extended code
	JMP	DONE_READING
READ_KEY	ENDP


	PUBLIC	STRING_TO_UPPER
;-----------------------------------------------------------------------;
; This procedure converts the stinrg, using the DOS format for strings,	;
; to all uppercase letters.						;
;									;
;	DS:DX	Address of string buffer				;
;-----------------------------------------------------------------------;
STRING_TO_UPPER		PROC
	PUSH	AX
	PUSH	BX
	PUSH	CX
	MOV	BX,DX
	INC	BX			;Point to character count
	MOV	CL,[BX]			;Character count in 2nd byte of buffer
	XOR	CH,CH			;Clear upper byte of count
UPPER_LOOP:
	INC	BX			;Point to next character in buffer
	MOV	AL,[BX]			;Get one character from the string
	CALL	CHAR_TO_UPPER		;Convert to upper-case letter
	MOV	[BX],AL			;Save this character back to the string
	LOOP	UPPER_LOOP
	POP	CX
	POP	BX
	POP	AX
	RET
STRING_TO_UPPER		ENDP


	PUBLIC	CHAR_TO_UPPER
;-----------------------------------------------------------------------;
; This procedure converts a single character to upper case.		;
;									;
; On entry:	AL	Character to convert				;
; Returns:	AL	Upper-case version of the letter		;
;-----------------------------------------------------------------------;
CHAR_TO_UPPER	PROC
	CMP	AL,'a'			;Is this a lower-case letter?
	JB	NOT_LOWER		;No, then we're all done
	CMP	AL,'z'			;Is this a lower-case letter?
	JA	NOT_LOWER		;No, then we're all done
	ADD	AL,'A'-'a'		;Convert to upper-case letter
NOT_LOWER:
	RET
CHAR_TO_UPPER	ENDP


	PUBLIC	READ_STRING
	EXTRN	WRITE_CHAR:PROC
	EXTRN	UPDATE_REAL_CURSOR:PROC
;-----------------------------------------------------------------------;
; This procedure performs a function very similar to the DOS 0Ah	;
; function.  But this function will return a special character if a	;
; function or keyboard key is pressed--no return for these keys.  And	;
; ESC will erase the input and start over again.			;
;									;
;	DS:DX	Address for keyboard buffer.  The first byte must	;
;		contain the maximum number of characters to read (plus	;
;		one for the return).  And the second byte will be used	;
;		by this procedure to return the number of characters	;
;		actually read.						;
;			0	No characters read			;
;			-1	One special character read		;
;			otherwise number actually read (not including	;
;				Enter key)				;
;									;
; Uses:		BACK_SPACE, WRITE_CHAR, READ_KEY, UPDATE_REAL_CURSOR	;
;-----------------------------------------------------------------------;
READ_STRING	PROC
	PUSH	AX
	PUSH	BX
	PUSH	SI
	MOV	SI,DX			;Use SI for index register and
START_OVER:
	CALL	UPDATE_REAL_CURSOR	;Move to position of virtual cursor
	MOV	BX,2			;BX for offset to beginning of buffer
	CALL	READ_KEY		;Read one key from the keyboard
	OR	AH,AH			;Is character extended ASCII?
	JNZ	EXTENDED		;Yes, then process it.
STRING_NOT_EXTENDED:			;Extnd char is error unless buf empty
	CMP	AL,CR			;Is this a carriage return?
	JE	END_INPUT		;Yes, we are done with input
	CMP	AL,BS			;Is it a backspace character?
	JNE	NOT_BS			;Nope
	CALL	BACK_SPACE		;Yes, delete character
	CMP	BL,2			;Is buffer empty?
	JE	START_OVER		;Yes, can now read extended ASCII again
	JMP	SHORT READ_NEXT_CHAR	;No, continue reading normal characters
NOT_BS:	CMP	AL,ESCAPE		;Is it an ESC--purge buffer?
	JE	PURGE_BUFFER		;Yes, then purge the buffer
	CMP	BL,[SI]			;Check to see if buffer is full
	JA	BUFFER_FULL		;Buffer is full
	MOV	[SI+BX],AL		;Else save char in buffer
	INC	BX			;Point to next free character in buffer
	PUSH	DX
	MOV	DL,AL			;Echo character to screen
	CALL	WRITE_CHAR
	POP	DX
READ_NEXT_CHAR:
	CALL	UPDATE_REAL_CURSOR	;Move real cursor to virtual cursor
	CALL	READ_KEY
	OR	AH,AH			;An extended ASCII char is not valid
					; when the buffer is not empty
	JZ	STRING_NOT_EXTENDED	;Char is valid

;-----------------------------------------------;
; Signal an error condition by sending a beep	;
; character to the display: chr$(7).		;
;-----------------------------------------------;
SIGNAL_ERROR:
	PUSH	DX
	MOV	DL,7			;Sound the bell by writing chr$(7)
	MOV	AH,2
	INT	21h
	POP	DX
	JMP	SHORT READ_NEXT_CHAR	;Now read next character

;-----------------------------------------------;
; Empty the string buffer and erase all the	;
; characters displayed on the screen.		;
;-----------------------------------------------;
PURGE_BUFFER:
	PUSH	CX
	MOV	CL,[SI]			;Backspace over maximum number of
	XOR	CH,CH
PURGE_LOOP:				; characters in buffer.  BACK_SPACE
	CALL	BACK_SPACE		; will keep the cursor from moving too
	LOOP	PURGE_LOOP		; far back
	POP	CX
	JMP	START_OVER		;Can now read extended ASCII characters
					; since the buffer is empty

;-----------------------------------------------;
; The buffer was full, so can't read another	;
; character.  Send a beep to alert user of	;
; buffer-full condition.			;
;-----------------------------------------------;
BUFFER_FULL:
	JMP	SHORT SIGNAL_ERROR	;If buffer full, just beep

;-----------------------------------------------;
; Read the extended ASCII code and place this	;
; in the buffer as the only character, then	;
; return -1 as the number of characters read.	;
;-----------------------------------------------;
EXTENDED:				;Read an extended ASCII code
	MOV	[SI+2],AL		;Place just this char in buffer
	MOV	BL,0FFh			;Num chars read = -1 for special
	JMP	SHORT END_STRING

;-----------------------------------------------;
; Save the count of the number of characters	;
; read and return.				;
;-----------------------------------------------;
END_INPUT:				;Done with input
	SUB	BL,2			;Count of characters read
END_STRING:
	MOV	[SI+1],BL		;Return number of chars read
	POP	SI
	POP	BX
	POP	AX
	RET
READ_STRING	ENDP


	PUBLIC	BACK_SPACE
	EXTRN	WRITE_CHAR:PROC
	EXTRN	READ_CURSOR_POSITION:PROC, GOTO_XY:PROC
;-----------------------------------------------------------------------;
; This procedure deletes characters, one at a time, from the buffer and	;
; the screen when the buffer is not empty.  BACK_SPACE simply returns	;
; when the buffer is empty.						;
;									;
;	DS:SI+BX	Most recent character still in buffer		;
;									;
; Note:  The version of BACK_SPACE presented in the book doesn't work	;
;	 with the version of WRITE_CHAR that writes directly to the	;
;	 screen.  This version works properly with both versions.	;
;									;
; Uses:		WRITE_CHAR						;
;-----------------------------------------------------------------------;
BACK_SPACE	PROC			;Delete one character
	PUSH	AX
	PUSH	DX
	CMP	BX,2			;Is buffer empty?
	JE	END_BS			;Yes, read the next character

	DEC	BX			;Remove one character from buffer
	CALL	READ_CURSOR_POSITION	;Get the current cursor position
	DEC	DL			;Move cursor left one column
	CALL	GOTO_XY
	PUSH	DX			;Save new cursor position
	MOV	DL,20h			;Write space there
	CALL	WRITE_CHAR
	POP	DX			;Get back new cursor position
	CALL	GOTO_XY

END_BS:	POP	DX
	POP	AX
	RET
BACK_SPACE	ENDP


	PUBLIC	READ_DECIMAL
;-----------------------------------------------------------------------;
; This procedure takes the output buffer of READ_STRING and converts	;
; the string of decimal digits to a word.				;
;									;
;	AX	Word converted from decimal				;
;	CF	Set if error, clear if no error				;
;									;
; Uses:		READ_STRING						;
; Reads:	KEYBOARD_INPUT, etc.					;
; Writes:	KEYBOARD_INPUT, etc.					;
;-----------------------------------------------------------------------;
READ_DECIMAL	PROC
	PUSH	BX
	PUSH	CX
	PUSH	DX
	MOV	CHAR_NUM_LIMIT,6	;Max number is 5 digits (65535)
	LEA	DX,KEYBOARD_INPUT
	CALL	READ_STRING
	MOV	CL,NUM_CHARS_READ	;Get number of characters read
	XOR	CH,CH			;Set upper byte of count to 0
	CMP	CL,0			;Return error if no characters read
	JLE	BAD_DECIMAL_DIGIT	;No chars read, signal error
	XOR	AX,AX			;Start with number set to 0
	XOR	BX,BX			;Start at beginning of string
CONVERT_DIGIT:
	MOV	DX,10			;Multiply number by 10
	MUL	DX			;Multiply AX by 10
	JC	BAD_DECIMAL_DIGIT	;CF set if MUL overflowed one word
	MOV	DL,CHARS[BX]		;Get the next digit
	SUB	DL,'0'			;And convert to a nibble (4 bits)
	JS	BAD_DECIMAL_DIGIT	;Bad digit if < 0
	CMP	DL,9			;Is this a bad digit?
	JA	BAD_DECIMAL_DIGIT	;Yes
	ADD	AX,DX			;No, so add it to number
	INC	BX			;Point to next character
	LOOP	CONVERT_DIGIT		;Get the next digit
DONE_DECIMAL:
	POP	DX
	POP	CX
	POP	BX
	RET
BAD_DECIMAL_DIGIT:
	STC				;Set carry to signal error
	JMP	DONE_DECIMAL
READ_DECIMAL	ENDP


	PUBLIC	READ_BYTE
;-----------------------------------------------------------------------;
; This procedure reads either a single ASCII character or a two-digit	;
; hex number.  This is just a test version of READ_BYTE.		;
;									;
; Returns byte in	AL	Character code (unless AH = 0)		;
;			AH	0 if read ASCII char			;
;				1 if read a special key			;
;				-1 if no characters read		;
;									;
; Uses:		HEX_TO_BYTE, STRING_TO_UPPER, READ_STRING		;
; Reads:	KEYBOARD_INPUT, etc.					;
; Writes:	KEYBOARD_INPUT, etc.					;
;-----------------------------------------------------------------------;
READ_BYTE	PROC
	PUSH	DX
	MOV	CHAR_NUM_LIMIT,3	;Allow only two characters (plus Enter)
	LEA	DX,KEYBOARD_INPUT
	CALL	READ_STRING
	CMP	NUM_CHARS_READ,1	;See how many characters
	JE	ASCII_INPUT		;Just one, treat as ASCII character
	JB	NO_CHARACTERS		;Only Enter key hit
	CMP	BYTE PTR NUM_CHARS_READ,0FFh	;Special function key?
	JE	SPECIAL_KEY		;Yes
	CALL	STRING_TO_UPPER		;No, convert string to uppercase
	LEA	DX,CHARS		;Address of string to convert
	CALL	HEX_TO_BYTE		;Convert string from hex to byte
	JC	NO_CHARACTERS		;Error, so return 'no characters read'
	XOR	AH,AH			;Signal read one byte
DONE_READ:
	POP	DX
	RET
NO_CHARACTERS:
	XOR	AH,AH			;Set to 'no characters read'
	NOT	AH			;Return -1 in AH
	JMP	DONE_READ
ASCII_INPUT:
	MOV	AL,CHARS		;Load character read
	XOR	AH,AH			;Signal read one byte
	JMP	DONE_READ
SPECIAL_KEY:
	MOV	AL,CHARS[0]		;Return the scan code
	MOV	AH,1			;Signal special key with 1
	JMP	DONE_READ
READ_BYTE	ENDP


	PUBLIC	HEX_TO_BYTE
;-----------------------------------------------------------------------;
; This procedure converts the two characters at DS:DX from hex to one	;
; byte.									;
;									;
;	DS:DX	Address of two characters for hex number		;
;Returns:								;
;	AL	Byte							;
;	CF	Set for error, clear if no error			;
; AH changed								;
;									;
; Uses:		CONVERT_HEX_DIGIT					;
;-----------------------------------------------------------------------;
HEX_TO_BYTE	PROC
	PUSH	BX
	PUSH	CX
	MOV	BX,DX			;Put address in BX for indirect addr
	MOV	AL,[BX]			;Get first digit
	CALL	CONVERT_HEX_DIGIT
	JC	BAD_HEX			;Bad hex digit if carry set
	MOV	CX,4			;Now multiply by 16
	SHL	AL,CL
	MOV	AH,AL			;Retain a copy
	INC	BX			;Get second digit
	MOV	AL,[BX]
	CALL	CONVERT_HEX_DIGIT
	JC	BAD_HEX			;Bad hex digit if carry set
	OR	AL,AH			;Combine two nibbles
	CLC				;Clear carry for no error
DONE_HEX:
	POP	CX
	POP	BX
	RET
BAD_HEX:
	STC				;Set carry for error
	JMP	DONE_HEX
HEX_TO_BYTE	ENDP


	PUBLIC	CONVERT_HEX_DIGIT
;-----------------------------------------------------------------------;
; This procedure converts a character from ASCII (hex) to a nibble (4	;
; bits).								;
;									;
;		AL	character to convert				;
; Returns:	AL	nibble						;
;		CF	Set for error, cleared otherwise		;
;-----------------------------------------------------------------------;
CONVERT_HEX_DIGIT	PROC
	CMP	AL,'0'			;Is it a legal digit?
	JB	BAD_DIGIT		;Nope
	CMP	AL,'9'			;Not sure yet
	JA	TRY_HEX			;Might be hex digit
	SUB	AL,'0'			;Is decimal digit, convert to nibble
	CLC				;Clear the carry, no error
	RET
TRY_HEX:
	CMP	AL,'A'			;Not sure yet
	JB	BAD_DIGIT		;Not hex
	CMP	AL,'F'			;Not sure yet
	JA	BAD_DIGIT		;Not hex
	SUB	AL,'A'-10		;Is hex, convert to nibble
	CLC				;Clear the carry, no error
	RET
BAD_DIGIT:
	STC				;Set the carry, error
	RET
CONVERT_HEX_DIGIT	ENDP


	END
